Add feature to optionally disallow synchronous IO (#1919)

* Allow synchronous IO by default
This commit is contained in:
Stephen Halter 2017-07-03 11:07:17 -07:00 committed by GitHub
parent 6e45de2205
commit e9ffcdb414
19 changed files with 503 additions and 111 deletions

View File

@ -336,4 +336,10 @@
<data name="MinimumGracePeriodRequired" xml:space="preserve"> <data name="MinimumGracePeriodRequired" xml:space="preserve">
<value>The request body rate enforcement grace period must be greater than {heartbeatInterval} second.</value> <value>The request body rate enforcement grace period must be greater than {heartbeatInterval} second.</value>
</data> </data>
<data name="SynchronousReadsDisallowed" xml:space="preserve">
<value>Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead.</value>
</data>
<data name="SynchronousWritesDisallowed" xml:space="preserve">
<value>Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead.</value>
</data>
</root> </root>

View File

@ -22,6 +22,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
IHttpConnectionFeature, IHttpConnectionFeature,
IHttpRequestLifetimeFeature, IHttpRequestLifetimeFeature,
IHttpRequestIdentifierFeature, IHttpRequestIdentifierFeature,
IHttpBodyControlFeature,
IHttpMaxRequestBodySizeFeature, IHttpMaxRequestBodySizeFeature,
IHttpMinRequestBodyDataRateFeature IHttpMinRequestBodyDataRateFeature
{ {
@ -205,6 +206,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
set => TraceIdentifier = value; set => TraceIdentifier = value;
} }
bool IHttpBodyControlFeature.AllowSynchronousIO
{
get => AllowSynchronousIO;
set => AllowSynchronousIO = value;
}
bool IHttpMaxRequestBodySizeFeature.IsReadOnly => HasStartedConsumingRequestBody || _wasUpgraded; bool IHttpMaxRequestBodySizeFeature.IsReadOnly => HasStartedConsumingRequestBody || _wasUpgraded;
long? IHttpMaxRequestBodySizeFeature.MaxRequestBodySize long? IHttpMaxRequestBodySizeFeature.MaxRequestBodySize

View File

@ -25,6 +25,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private static readonly Type ISessionFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.ISessionFeature); private static readonly Type ISessionFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.ISessionFeature);
private static readonly Type IHttpMaxRequestBodySizeFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature); private static readonly Type IHttpMaxRequestBodySizeFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature);
private static readonly Type IHttpMinRequestBodyDataRateFeatureType = typeof(global::Microsoft.AspNetCore.Server.Kestrel.Core.Features.IHttpMinRequestBodyDataRateFeature); private static readonly Type IHttpMinRequestBodyDataRateFeatureType = typeof(global::Microsoft.AspNetCore.Server.Kestrel.Core.Features.IHttpMinRequestBodyDataRateFeature);
private static readonly Type IHttpBodyControlFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpBodyControlFeature);
private static readonly Type IHttpSendFileFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature); private static readonly Type IHttpSendFileFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature);
private object _currentIHttpRequestFeature; private object _currentIHttpRequestFeature;
@ -44,6 +45,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private object _currentISessionFeature; private object _currentISessionFeature;
private object _currentIHttpMaxRequestBodySizeFeature; private object _currentIHttpMaxRequestBodySizeFeature;
private object _currentIHttpMinRequestBodyDataRateFeature; private object _currentIHttpMinRequestBodyDataRateFeature;
private object _currentIHttpBodyControlFeature;
private object _currentIHttpSendFileFeature; private object _currentIHttpSendFileFeature;
private void FastReset() private void FastReset()
@ -56,6 +58,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_currentIHttpConnectionFeature = this; _currentIHttpConnectionFeature = this;
_currentIHttpMaxRequestBodySizeFeature = this; _currentIHttpMaxRequestBodySizeFeature = this;
_currentIHttpMinRequestBodyDataRateFeature = this; _currentIHttpMinRequestBodyDataRateFeature = this;
_currentIHttpBodyControlFeature = this;
_currentIServiceProvidersFeature = null; _currentIServiceProvidersFeature = null;
_currentIHttpAuthenticationFeature = null; _currentIHttpAuthenticationFeature = null;
@ -139,6 +142,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
return _currentIHttpMinRequestBodyDataRateFeature; return _currentIHttpMinRequestBodyDataRateFeature;
} }
if (key == IHttpBodyControlFeatureType)
{
return _currentIHttpBodyControlFeature;
}
if (key == IHttpSendFileFeatureType) if (key == IHttpSendFileFeatureType)
{ {
return _currentIHttpSendFileFeature; return _currentIHttpSendFileFeature;
@ -235,6 +242,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_currentIHttpMinRequestBodyDataRateFeature = feature; _currentIHttpMinRequestBodyDataRateFeature = feature;
return; return;
} }
if (key == IHttpBodyControlFeatureType)
{
_currentIHttpBodyControlFeature = feature;
return;
}
if (key == IHttpSendFileFeatureType) if (key == IHttpSendFileFeatureType)
{ {
_currentIHttpSendFileFeature = feature; _currentIHttpSendFileFeature = feature;
@ -313,6 +325,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
yield return new KeyValuePair<Type, object>(IHttpMinRequestBodyDataRateFeatureType, _currentIHttpMinRequestBodyDataRateFeature as global::Microsoft.AspNetCore.Server.Kestrel.Core.Features.IHttpMinRequestBodyDataRateFeature); yield return new KeyValuePair<Type, object>(IHttpMinRequestBodyDataRateFeatureType, _currentIHttpMinRequestBodyDataRateFeature as global::Microsoft.AspNetCore.Server.Kestrel.Core.Features.IHttpMinRequestBodyDataRateFeature);
} }
if (_currentIHttpBodyControlFeature != null)
{
yield return new KeyValuePair<Type, object>(IHttpBodyControlFeatureType, _currentIHttpBodyControlFeature as global::Microsoft.AspNetCore.Http.Features.IHttpBodyControlFeature);
}
if (_currentIHttpSendFileFeature != null) if (_currentIHttpSendFileFeature != null)
{ {
yield return new KeyValuePair<Type, object>(IHttpSendFileFeatureType, _currentIHttpSendFileFeature as global::Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature); yield return new KeyValuePair<Type, object>(IHttpSendFileFeatureType, _currentIHttpSendFileFeature as global::Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature);

View File

@ -122,6 +122,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public string ConnectionIdFeature { get; set; } public string ConnectionIdFeature { get; set; }
public bool HasStartedConsumingRequestBody { get; set; } public bool HasStartedConsumingRequestBody { get; set; }
public long? MaxRequestBodySize { get; set; } public long? MaxRequestBodySize { get; set; }
public bool AllowSynchronousIO { get; set; }
/// <summary> /// <summary>
/// The request id. <seealso cref="HttpContext.TraceIdentifier"/> /// The request id. <seealso cref="HttpContext.TraceIdentifier"/>
@ -305,7 +306,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
if (_frameStreams == null) if (_frameStreams == null)
{ {
_frameStreams = new Streams(this); _frameStreams = new Streams(bodyControl: this, frameControl: this);
} }
(RequestBody, ResponseBody) = _frameStreams.Start(messageBody); (RequestBody, ResponseBody) = _frameStreams.Start(messageBody);
@ -329,6 +330,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
HasStartedConsumingRequestBody = false; HasStartedConsumingRequestBody = false;
MaxRequestBodySize = ServerOptions.Limits.MaxRequestBodySize; MaxRequestBodySize = ServerOptions.Limits.MaxRequestBodySize;
AllowSynchronousIO = ServerOptions.AllowSynchronousIO;
TraceIdentifier = null; TraceIdentifier = null;
Scheme = null; Scheme = null;
Method = null; Method = null;

View File

@ -7,17 +7,20 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
internal class FrameRequestStream : ReadOnlyStream internal class FrameRequestStream : ReadOnlyStream
{ {
private readonly IHttpBodyControlFeature _bodyControl;
private MessageBody _body; private MessageBody _body;
private FrameStreamState _state; private FrameStreamState _state;
private Exception _error; private Exception _error;
public FrameRequestStream() public FrameRequestStream(IHttpBodyControlFeature bodyControl)
{ {
_bodyControl = bodyControl;
_state = FrameStreamState.Closed; _state = FrameStreamState.Closed;
} }
@ -34,13 +37,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public override void Flush() public override void Flush()
{ {
// No-op. throw new NotSupportedException();
} }
public override Task FlushAsync(CancellationToken cancellationToken) public override Task FlushAsync(CancellationToken cancellationToken)
{ {
// No-op. throw new NotSupportedException();
return Task.CompletedTask;
} }
public override long Seek(long offset, SeekOrigin origin) public override long Seek(long offset, SeekOrigin origin)
@ -55,8 +57,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public override int Read(byte[] buffer, int offset, int count) public override int Read(byte[] buffer, int offset, int count)
{ {
// ValueTask uses .GetAwaiter().GetResult() if necessary if (!_bodyControl.AllowSynchronousIO)
return ReadAsync(buffer, offset, count).Result; {
throw new InvalidOperationException(CoreStrings.SynchronousReadsDisallowed);
}
return ReadAsync(buffer, offset, count).GetAwaiter().GetResult();
} }
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)

View File

@ -6,16 +6,19 @@ using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
internal class FrameResponseStream : WriteOnlyStream internal class FrameResponseStream : WriteOnlyStream
{ {
private IFrameControl _frameControl; private readonly IHttpBodyControlFeature _bodyControl;
private readonly IFrameControl _frameControl;
private FrameStreamState _state; private FrameStreamState _state;
public FrameResponseStream(IFrameControl frameControl) public FrameResponseStream(IHttpBodyControlFeature bodyControl, IFrameControl frameControl)
{ {
_bodyControl = bodyControl;
_frameControl = frameControl; _frameControl = frameControl;
_state = FrameStreamState.Closed; _state = FrameStreamState.Closed;
} }
@ -58,6 +61,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public override void Write(byte[] buffer, int offset, int count) public override void Write(byte[] buffer, int offset, int count)
{ {
if (!_bodyControl.AllowSynchronousIO)
{
throw new InvalidOperationException(CoreStrings.SynchronousWritesDisallowed);
}
WriteAsync(buffer, offset, count, default(CancellationToken)).GetAwaiter().GetResult(); WriteAsync(buffer, offset, count, default(CancellationToken)).GetAwaiter().GetResult();
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.IO; using System.IO;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
@ -17,11 +18,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
private readonly FrameRequestStream _emptyRequest; private readonly FrameRequestStream _emptyRequest;
private readonly Stream _upgradeStream; private readonly Stream _upgradeStream;
public Streams(IFrameControl frameControl) public Streams(IHttpBodyControlFeature bodyControl, IFrameControl frameControl)
{ {
_request = new FrameRequestStream(); _request = new FrameRequestStream(bodyControl);
_emptyRequest = new FrameRequestStream(); _emptyRequest = new FrameRequestStream(bodyControl);
_response = new FrameResponseStream(frameControl); _response = new FrameResponseStream(bodyControl, frameControl);
_upgradeableResponse = new WrappingStream(_response); _upgradeableResponse = new WrappingStream(_response);
_upgradeStream = new FrameDuplexStream(_request, _response); _upgradeStream = new FrameDuplexStream(_request, _response);
} }

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
namespace Microsoft.AspNetCore.Server.Kestrel.Core namespace Microsoft.AspNetCore.Server.Kestrel.Core
@ -35,6 +36,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
/// <remarks>The default mode is <see cref="SchedulingMode.Default"/></remarks> /// <remarks>The default mode is <see cref="SchedulingMode.Default"/></remarks>
public SchedulingMode ApplicationSchedulingMode { get; set; } = SchedulingMode.Default; public SchedulingMode ApplicationSchedulingMode { get; set; } = SchedulingMode.Default;
/// <summary>
/// Gets or sets a value that controls whether synchronous IO is allowed for the <see cref="HttpContext.Request"/> and <see cref="HttpContext.Response"/>
/// </summary>
/// <remarks>
/// Defaults to true.
/// </remarks>
public bool AllowSynchronousIO { get; set; } = true;
/// <summary> /// <summary>
/// Enables the Listen options callback to resolve and use services registered by the application during startup. /// Enables the Listen options callback to resolve and use services registered by the application during startup.
/// Typically initialized by UseKestrel()"/>. /// Typically initialized by UseKestrel()"/>.

View File

@ -1019,7 +1019,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
=> GetString("NonNegativeTimeSpanRequired"); => GetString("NonNegativeTimeSpanRequired");
/// <summary> /// <summary>
/// The request body rate enforcement grace period must be greater than {heartbeatInterval} seconds. /// The request body rate enforcement grace period must be greater than {heartbeatInterval} second.
/// </summary> /// </summary>
internal static string MinimumGracePeriodRequired internal static string MinimumGracePeriodRequired
{ {
@ -1027,11 +1027,39 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
} }
/// <summary> /// <summary>
/// The request body rate enforcement grace period must be greater than {heartbeatInterval} seconds. /// The request body rate enforcement grace period must be greater than {heartbeatInterval} second.
/// </summary> /// </summary>
internal static string FormatMinimumGracePeriodRequired(object heartbeatInterval) internal static string FormatMinimumGracePeriodRequired(object heartbeatInterval)
=> string.Format(CultureInfo.CurrentCulture, GetString("MinimumGracePeriodRequired", "heartbeatInterval"), heartbeatInterval); => string.Format(CultureInfo.CurrentCulture, GetString("MinimumGracePeriodRequired", "heartbeatInterval"), heartbeatInterval);
/// <summary>
/// Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead.
/// </summary>
internal static string SynchronousReadsDisallowed
{
get => GetString("SynchronousReadsDisallowed");
}
/// <summary>
/// Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead.
/// </summary>
internal static string FormatSynchronousReadsDisallowed()
=> GetString("SynchronousReadsDisallowed");
/// <summary>
/// Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead.
/// </summary>
internal static string SynchronousWritesDisallowed
{
get => GetString("SynchronousWritesDisallowed");
}
/// <summary>
/// Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead.
/// </summary>
internal static string FormatSynchronousWritesDisallowed()
=> GetString("SynchronousWritesDisallowed");
private static string GetString(string name, params string[] formatterNames) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -5,6 +5,7 @@ using System;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Moq; using Moq;
using Xunit; using Xunit;
@ -16,49 +17,49 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[Fact] [Fact]
public void CanReadReturnsTrue() public void CanReadReturnsTrue()
{ {
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
Assert.True(stream.CanRead); Assert.True(stream.CanRead);
} }
[Fact] [Fact]
public void CanSeekReturnsFalse() public void CanSeekReturnsFalse()
{ {
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
Assert.False(stream.CanSeek); Assert.False(stream.CanSeek);
} }
[Fact] [Fact]
public void CanWriteReturnsFalse() public void CanWriteReturnsFalse()
{ {
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
Assert.False(stream.CanWrite); Assert.False(stream.CanWrite);
} }
[Fact] [Fact]
public void SeekThrows() public void SeekThrows()
{ {
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
Assert.Throws<NotSupportedException>(() => stream.Seek(0, SeekOrigin.Begin)); Assert.Throws<NotSupportedException>(() => stream.Seek(0, SeekOrigin.Begin));
} }
[Fact] [Fact]
public void LengthThrows() public void LengthThrows()
{ {
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
Assert.Throws<NotSupportedException>(() => stream.Length); Assert.Throws<NotSupportedException>(() => stream.Length);
} }
[Fact] [Fact]
public void SetLengthThrows() public void SetLengthThrows()
{ {
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
Assert.Throws<NotSupportedException>(() => stream.SetLength(0)); Assert.Throws<NotSupportedException>(() => stream.SetLength(0));
} }
[Fact] [Fact]
public void PositionThrows() public void PositionThrows()
{ {
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
Assert.Throws<NotSupportedException>(() => stream.Position); Assert.Throws<NotSupportedException>(() => stream.Position);
Assert.Throws<NotSupportedException>(() => stream.Position = 0); Assert.Throws<NotSupportedException>(() => stream.Position = 0);
} }
@ -66,21 +67,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[Fact] [Fact]
public void WriteThrows() public void WriteThrows()
{ {
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
Assert.Throws<NotSupportedException>(() => stream.Write(new byte[1], 0, 1)); Assert.Throws<NotSupportedException>(() => stream.Write(new byte[1], 0, 1));
} }
[Fact] [Fact]
public void WriteByteThrows() public void WriteByteThrows()
{ {
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
Assert.Throws<NotSupportedException>(() => stream.WriteByte(0)); Assert.Throws<NotSupportedException>(() => stream.WriteByte(0));
} }
[Fact] [Fact]
public async Task WriteAsyncThrows() public async Task WriteAsyncThrows()
{ {
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
await Assert.ThrowsAsync<NotSupportedException>(() => stream.WriteAsync(new byte[1], 0, 1)); await Assert.ThrowsAsync<NotSupportedException>(() => stream.WriteAsync(new byte[1], 0, 1));
} }
@ -88,7 +89,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[Fact] [Fact]
public void BeginWriteThrows() public void BeginWriteThrows()
{ {
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
Assert.Throws<NotSupportedException>(() => stream.BeginWrite(new byte[1], 0, 1, null, null)); Assert.Throws<NotSupportedException>(() => stream.BeginWrite(new byte[1], 0, 1, null, null));
} }
#elif NETCOREAPP2_0 #elif NETCOREAPP2_0
@ -97,23 +98,47 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
#endif #endif
[Fact] [Fact]
public void FlushDoesNotThrow() public void FlushThrows()
{ {
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
stream.Flush(); Assert.Throws<NotSupportedException>(() => stream.Flush());
} }
[Fact] [Fact]
public async Task FlushAsyncDoesNotThrow() public async Task FlushAsyncThrows()
{ {
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
await stream.FlushAsync(); await Assert.ThrowsAsync<NotSupportedException>(() => stream.FlushAsync());
}
[Fact]
public async Task SynchronousReadsThrowIfDisallowedByIHttpBodyControlFeature()
{
var allowSynchronousIO = false;
var mockBodyControl = new Mock<IHttpBodyControlFeature>();
mockBodyControl.Setup(m => m.AllowSynchronousIO).Returns(() => allowSynchronousIO);
var mockMessageBody = new Mock<MessageBody>((Frame)null);
mockMessageBody.Setup(m => m.ReadAsync(It.IsAny<ArraySegment<byte>>(), CancellationToken.None)).ReturnsAsync(0);
var stream = new FrameRequestStream(mockBodyControl.Object);
stream.StartAcceptingReads(mockMessageBody.Object);
Assert.Equal(0, await stream.ReadAsync(new byte[1], 0, 1));
var ioEx = Assert.Throws<InvalidOperationException>(() => stream.Read(new byte[1], 0, 1));
Assert.Equal("Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead.", ioEx.Message);
var ioEx2 = Assert.Throws<InvalidOperationException>(() => stream.CopyTo(Stream.Null));
Assert.Equal("Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead.", ioEx2.Message);
allowSynchronousIO = true;
Assert.Equal(0, stream.Read(new byte[1], 0, 1));
} }
[Fact] [Fact]
public void AbortCausesReadToCancel() public void AbortCausesReadToCancel()
{ {
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
stream.StartAcceptingReads(null); stream.StartAcceptingReads(null);
stream.Abort(); stream.Abort();
var task = stream.ReadAsync(new byte[1], 0, 1); var task = stream.ReadAsync(new byte[1], 0, 1);
@ -123,7 +148,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[Fact] [Fact]
public void AbortWithErrorCausesReadToCancel() public void AbortWithErrorCausesReadToCancel()
{ {
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
stream.StartAcceptingReads(null); stream.StartAcceptingReads(null);
var error = new Exception(); var error = new Exception();
stream.Abort(error); stream.Abort(error);
@ -135,7 +160,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[Fact] [Fact]
public void StopAcceptingReadsCausesReadToThrowObjectDisposedException() public void StopAcceptingReadsCausesReadToThrowObjectDisposedException()
{ {
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
stream.StartAcceptingReads(null); stream.StartAcceptingReads(null);
stream.StopAcceptingReads(); stream.StopAcceptingReads();
Assert.Throws<ObjectDisposedException>(() => { stream.ReadAsync(new byte[1], 0, 1); }); Assert.Throws<ObjectDisposedException>(() => { stream.ReadAsync(new byte[1], 0, 1); });
@ -144,7 +169,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[Fact] [Fact]
public void AbortCausesCopyToAsyncToCancel() public void AbortCausesCopyToAsyncToCancel()
{ {
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
stream.StartAcceptingReads(null); stream.StartAcceptingReads(null);
stream.Abort(); stream.Abort();
var task = stream.CopyToAsync(Mock.Of<Stream>()); var task = stream.CopyToAsync(Mock.Of<Stream>());
@ -154,7 +179,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[Fact] [Fact]
public void AbortWithErrorCausesCopyToAsyncToCancel() public void AbortWithErrorCausesCopyToAsyncToCancel()
{ {
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
stream.StartAcceptingReads(null); stream.StartAcceptingReads(null);
var error = new Exception(); var error = new Exception();
stream.Abort(error); stream.Abort(error);
@ -166,7 +191,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[Fact] [Fact]
public void StopAcceptingReadsCausesCopyToAsyncToThrowObjectDisposedException() public void StopAcceptingReadsCausesCopyToAsyncToThrowObjectDisposedException()
{ {
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
stream.StartAcceptingReads(null); stream.StartAcceptingReads(null);
stream.StopAcceptingReads(); stream.StopAcceptingReads();
Assert.Throws<ObjectDisposedException>(() => { stream.CopyToAsync(Mock.Of<Stream>()); }); Assert.Throws<ObjectDisposedException>(() => { stream.CopyToAsync(Mock.Of<Stream>()); });
@ -175,7 +200,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[Fact] [Fact]
public void NullDestinationCausesCopyToAsyncToThrowArgumentNullException() public void NullDestinationCausesCopyToAsyncToThrowArgumentNullException()
{ {
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
stream.StartAcceptingReads(null); stream.StartAcceptingReads(null);
Assert.Throws<ArgumentNullException>(() => { stream.CopyToAsync(null); }); Assert.Throws<ArgumentNullException>(() => { stream.CopyToAsync(null); });
} }
@ -183,7 +208,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[Fact] [Fact]
public void ZeroBufferSizeCausesCopyToAsyncToThrowArgumentException() public void ZeroBufferSizeCausesCopyToAsyncToThrowArgumentException()
{ {
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
stream.StartAcceptingReads(null); stream.StartAcceptingReads(null);
Assert.Throws<ArgumentException>(() => { stream.CopyToAsync(Mock.Of<Stream>(), 0); }); Assert.Throws<ArgumentException>(() => { stream.CopyToAsync(Mock.Of<Stream>(), 0); });
} }

View File

@ -3,9 +3,12 @@
using System; using System;
using System.IO; using System.IO;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Tests.TestHelpers; using Microsoft.AspNetCore.Server.Kestrel.Core.Tests.TestHelpers;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
@ -15,79 +18,111 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[Fact] [Fact]
public void CanReadReturnsFalse() public void CanReadReturnsFalse()
{ {
var stream = new FrameResponseStream(new MockFrameControl()); var stream = new FrameResponseStream(Mock.Of<IHttpBodyControlFeature>(), new MockFrameControl());
Assert.False(stream.CanRead); Assert.False(stream.CanRead);
} }
[Fact] [Fact]
public void CanSeekReturnsFalse() public void CanSeekReturnsFalse()
{ {
var stream = new FrameResponseStream(new MockFrameControl()); var stream = new FrameResponseStream(Mock.Of<IHttpBodyControlFeature>(), new MockFrameControl());
Assert.False(stream.CanSeek); Assert.False(stream.CanSeek);
} }
[Fact] [Fact]
public void CanWriteReturnsTrue() public void CanWriteReturnsTrue()
{ {
var stream = new FrameResponseStream(new MockFrameControl()); var stream = new FrameResponseStream(Mock.Of<IHttpBodyControlFeature>(), new MockFrameControl());
Assert.True(stream.CanWrite); Assert.True(stream.CanWrite);
} }
[Fact] [Fact]
public void ReadThrows() public void ReadThrows()
{ {
var stream = new FrameResponseStream(new MockFrameControl()); var stream = new FrameResponseStream(Mock.Of<IHttpBodyControlFeature>(), new MockFrameControl());
Assert.Throws<NotSupportedException>(() => stream.Read(new byte[1], 0, 1)); Assert.Throws<NotSupportedException>(() => stream.Read(new byte[1], 0, 1));
} }
[Fact] [Fact]
public void ReadByteThrows() public void ReadByteThrows()
{ {
var stream = new FrameResponseStream(new MockFrameControl()); var stream = new FrameResponseStream(Mock.Of<IHttpBodyControlFeature>(), new MockFrameControl());
Assert.Throws<NotSupportedException>(() => stream.ReadByte()); Assert.Throws<NotSupportedException>(() => stream.ReadByte());
} }
[Fact] [Fact]
public async Task ReadAsyncThrows() public async Task ReadAsyncThrows()
{ {
var stream = new FrameResponseStream(new MockFrameControl()); var stream = new FrameResponseStream(Mock.Of<IHttpBodyControlFeature>(), new MockFrameControl());
await Assert.ThrowsAsync<NotSupportedException>(() => stream.ReadAsync(new byte[1], 0, 1)); await Assert.ThrowsAsync<NotSupportedException>(() => stream.ReadAsync(new byte[1], 0, 1));
} }
[Fact] [Fact]
public void BeginReadThrows() public void BeginReadThrows()
{ {
var stream = new FrameResponseStream(new MockFrameControl()); var stream = new FrameResponseStream(Mock.Of<IHttpBodyControlFeature>(), new MockFrameControl());
Assert.Throws<NotSupportedException>(() => stream.BeginRead(new byte[1], 0, 1, null, null)); Assert.Throws<NotSupportedException>(() => stream.BeginRead(new byte[1], 0, 1, null, null));
} }
[Fact] [Fact]
public void SeekThrows() public void SeekThrows()
{ {
var stream = new FrameResponseStream(new MockFrameControl()); var stream = new FrameResponseStream(Mock.Of<IHttpBodyControlFeature>(), new MockFrameControl());
Assert.Throws<NotSupportedException>(() => stream.Seek(0, SeekOrigin.Begin)); Assert.Throws<NotSupportedException>(() => stream.Seek(0, SeekOrigin.Begin));
} }
[Fact] [Fact]
public void LengthThrows() public void LengthThrows()
{ {
var stream = new FrameResponseStream(new MockFrameControl()); var stream = new FrameResponseStream(Mock.Of<IHttpBodyControlFeature>(), new MockFrameControl());
Assert.Throws<NotSupportedException>(() => stream.Length); Assert.Throws<NotSupportedException>(() => stream.Length);
} }
[Fact] [Fact]
public void SetLengthThrows() public void SetLengthThrows()
{ {
var stream = new FrameResponseStream(new MockFrameControl()); var stream = new FrameResponseStream(Mock.Of<IHttpBodyControlFeature>(), new MockFrameControl());
Assert.Throws<NotSupportedException>(() => stream.SetLength(0)); Assert.Throws<NotSupportedException>(() => stream.SetLength(0));
} }
[Fact] [Fact]
public void PositionThrows() public void PositionThrows()
{ {
var stream = new FrameResponseStream(new MockFrameControl()); var stream = new FrameResponseStream(Mock.Of<IHttpBodyControlFeature>(), new MockFrameControl());
Assert.Throws<NotSupportedException>(() => stream.Position); Assert.Throws<NotSupportedException>(() => stream.Position);
Assert.Throws<NotSupportedException>(() => stream.Position = 0); Assert.Throws<NotSupportedException>(() => stream.Position = 0);
} }
[Fact]
public void StopAcceptingWritesCausesWriteToThrowObjectDisposedException()
{
var stream = new FrameResponseStream(Mock.Of<IHttpBodyControlFeature>(), Mock.Of<IFrameControl>());
stream.StartAcceptingWrites();
stream.StopAcceptingWrites();
Assert.Throws<ObjectDisposedException>(() => { stream.WriteAsync(new byte[1], 0, 1); });
}
[Fact]
public async Task SynchronousWritesThrowIfDisallowedByIHttpBodyControlFeature()
{
var allowSynchronousIO = false;
var mockBodyControl = new Mock<IHttpBodyControlFeature>();
mockBodyControl.Setup(m => m.AllowSynchronousIO).Returns(() => allowSynchronousIO);
var mockFrameControl = new Mock<IFrameControl>();
mockFrameControl.Setup(m => m.WriteAsync(It.IsAny<ArraySegment<byte>>(), CancellationToken.None)).Returns(Task.CompletedTask);
var stream = new FrameResponseStream(mockBodyControl.Object, mockFrameControl.Object);
stream.StartAcceptingWrites();
// WriteAsync doesn't throw.
await stream.WriteAsync(new byte[1], 0, 1);
var ioEx = Assert.Throws<InvalidOperationException>(() => stream.Write(new byte[1], 0, 1));
Assert.Equal("Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead.", ioEx.Message);
allowSynchronousIO = true;
// If IHttpBodyControlFeature.AllowSynchronousIO is true, Write no longer throws.
stream.Write(new byte[1], 0, 1);
}
} }
} }

View File

@ -22,5 +22,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.True(o1.ListenOptions[0].NoDelay); Assert.True(o1.ListenOptions[0].NoDelay);
Assert.False(o1.ListenOptions[1].NoDelay); Assert.False(o1.ListenOptions[1].NoDelay);
} }
[Fact]
public void AllowSynchronousIODefaultsToTrue()
{
var options = new KestrelServerOptions();
Assert.True(options.AllowSynchronousIO);
}
} }
} }

View File

@ -9,6 +9,7 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
@ -28,7 +29,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
using (var input = new TestInput()) using (var input = new TestInput())
{ {
var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderContentLength = "5" }, input.Frame); var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderContentLength = "5" }, input.Frame);
var stream = new FrameRequestStream(); var mockBodyControl = new Mock<IHttpBodyControlFeature>();
mockBodyControl.Setup(m => m.AllowSynchronousIO).Returns(true);
var stream = new FrameRequestStream(mockBodyControl.Object);
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
input.Add("Hello"); input.Add("Hello");
@ -54,7 +57,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
using (var input = new TestInput()) using (var input = new TestInput())
{ {
var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderContentLength = "5" }, input.Frame); var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderContentLength = "5" }, input.Frame);
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
input.Add("Hello"); input.Add("Hello");
@ -78,7 +81,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
using (var input = new TestInput()) using (var input = new TestInput())
{ {
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame); var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame);
var stream = new FrameRequestStream(); var mockBodyControl = new Mock<IHttpBodyControlFeature>();
mockBodyControl.Setup(m => m.AllowSynchronousIO).Returns(true);
var stream = new FrameRequestStream(mockBodyControl.Object);
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
input.Add("5\r\nHello\r\n"); input.Add("5\r\nHello\r\n");
@ -104,7 +109,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
using (var input = new TestInput()) using (var input = new TestInput())
{ {
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame); var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame);
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
input.Add("5\r\nHello\r\n"); input.Add("5\r\nHello\r\n");
@ -130,7 +135,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
using (var input = new TestInput()) using (var input = new TestInput())
{ {
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame); var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame);
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
input.Add("5;\r\0"); input.Add("5;\r\0");
@ -155,7 +160,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
using (var input = new TestInput()) using (var input = new TestInput())
{ {
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame); var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame);
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
input.Add("80000000\r\n"); input.Add("80000000\r\n");
@ -176,7 +181,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
using (var input = new TestInput()) using (var input = new TestInput())
{ {
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame); var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame);
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
input.Add("012345678\r"); input.Add("012345678\r");
@ -199,7 +204,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
using (var input = new TestInput()) using (var input = new TestInput())
{ {
var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderConnection = "upgrade" }, input.Frame); var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderConnection = "upgrade" }, input.Frame);
var stream = new FrameRequestStream(); var mockBodyControl = new Mock<IHttpBodyControlFeature>();
mockBodyControl.Setup(m => m.AllowSynchronousIO).Returns(true);
var stream = new FrameRequestStream(mockBodyControl.Object);
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
input.Add("Hello"); input.Add("Hello");
@ -224,7 +231,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
using (var input = new TestInput()) using (var input = new TestInput())
{ {
var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderConnection = "upgrade" }, input.Frame); var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderConnection = "upgrade" }, input.Frame);
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
input.Add("Hello"); input.Add("Hello");
@ -249,7 +256,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
using (var input = new TestInput()) using (var input = new TestInput())
{ {
var body = MessageBody.For(httpVersion, new FrameRequestHeaders(), input.Frame); var body = MessageBody.For(httpVersion, new FrameRequestHeaders(), input.Frame);
var stream = new FrameRequestStream(); var mockBodyControl = new Mock<IHttpBodyControlFeature>();
mockBodyControl.Setup(m => m.AllowSynchronousIO).Returns(true);
var stream = new FrameRequestStream(mockBodyControl.Object);
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
input.Add("Hello"); input.Add("Hello");
@ -269,7 +278,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
using (var input = new TestInput()) using (var input = new TestInput())
{ {
var body = MessageBody.For(httpVersion, new FrameRequestHeaders(), input.Frame); var body = MessageBody.For(httpVersion, new FrameRequestHeaders(), input.Frame);
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
input.Add("Hello"); input.Add("Hello");
@ -287,7 +296,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
using (var input = new TestInput()) using (var input = new TestInput())
{ {
var body = MessageBody.For(HttpVersion.Http10, new FrameRequestHeaders { HeaderContentLength = "8197" }, input.Frame); var body = MessageBody.For(HttpVersion.Http10, new FrameRequestHeaders { HeaderContentLength = "8197" }, input.Frame);
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
// Input needs to be greater than 4032 bytes to allocate a block not backed by a slab. // Input needs to be greater than 4032 bytes to allocate a block not backed by a slab.
@ -479,13 +488,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
using (var input = new TestInput()) using (var input = new TestInput())
{ {
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderConnection = headerConnection }, input.Frame); var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderConnection = headerConnection }, input.Frame);
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
input.Add("Hello"); input.Add("Hello");
var buffer = new byte[1024]; var buffer = new byte[1024];
Assert.Equal(5, stream.Read(buffer, 0, buffer.Length)); Assert.Equal(5, await stream.ReadAsync(buffer, 0, buffer.Length));
AssertASCII("Hello", new ArraySegment<byte>(buffer, 0, 5)); AssertASCII("Hello", new ArraySegment<byte>(buffer, 0, 5));
input.Fin(); input.Fin();
@ -500,7 +509,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
using (var input = new TestInput()) using (var input = new TestInput())
{ {
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "2" }, input.Frame); var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "2" }, input.Frame);
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
// Add some input and consume it to ensure PumpAsync is running // Add some input and consume it to ensure PumpAsync is running
@ -523,7 +532,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
using (var input = new TestInput()) using (var input = new TestInput())
{ {
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "2" }, input.Frame); var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "2" }, input.Frame);
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
// Add some input and consume it to ensure PumpAsync is running // Add some input and consume it to ensure PumpAsync is running
@ -642,7 +651,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
input.Frame.TraceIdentifier = "RequestId"; input.Frame.TraceIdentifier = "RequestId";
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "2" }, input.Frame); var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "2" }, input.Frame);
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
// Add some input and consume it to ensure PumpAsync is running // Add some input and consume it to ensure PumpAsync is running
@ -672,7 +681,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
input.Frame.TraceIdentifier = "RequestId"; input.Frame.TraceIdentifier = "RequestId";
var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "2" }, input.Frame); var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "2" }, input.Frame);
var stream = new FrameRequestStream(); var stream = new FrameRequestStream(Mock.Of<IHttpBodyControlFeature>());
stream.StartAcceptingReads(body); stream.StartAcceptingReads(body);
// Add some input and consume it to ensure PumpAsync is running // Add some input and consume it to ensure PumpAsync is running

View File

@ -4,9 +4,11 @@
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Internal.System.IO.Pipelines; using Microsoft.AspNetCore.Server.Kestrel.Internal.System.IO.Pipelines;
using Microsoft.AspNetCore.Testing;
using Moq; using Moq;
using Xunit; using Xunit;
@ -17,7 +19,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[Fact] [Fact]
public async Task StreamsThrowAfterAbort() public async Task StreamsThrowAfterAbort()
{ {
var streams = new Streams(Mock.Of<IFrameControl>()); var streams = new Streams(Mock.Of<IHttpBodyControlFeature>(), Mock.Of<IFrameControl>());
var (request, response) = streams.Start(new MockMessageBody()); var (request, response) = streams.Start(new MockMessageBody());
var ex = new Exception("My error"); var ex = new Exception("My error");
@ -31,7 +33,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[Fact] [Fact]
public async Task StreamsThrowOnAbortAfterUpgrade() public async Task StreamsThrowOnAbortAfterUpgrade()
{ {
var streams = new Streams(Mock.Of<IFrameControl>()); var streams = new Streams(Mock.Of<IHttpBodyControlFeature>(), Mock.Of<IFrameControl>());
var (request, response) = streams.Start(new MockMessageBody(upgradeable: true)); var (request, response) = streams.Start(new MockMessageBody(upgradeable: true));
var upgrade = streams.Upgrade(); var upgrade = streams.Upgrade();
@ -53,7 +55,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[Fact] [Fact]
public async Task StreamsThrowOnUpgradeAfterAbort() public async Task StreamsThrowOnUpgradeAfterAbort()
{ {
var streams = new Streams(Mock.Of<IFrameControl>()); var streams = new Streams(Mock.Of<IHttpBodyControlFeature>(), Mock.Of<IFrameControl>());
var (request, response) = streams.Start(new MockMessageBody(upgradeable: true)); var (request, response) = streams.Start(new MockMessageBody(upgradeable: true));
var ex = new Exception("My error"); var ex = new Exception("My error");

View File

@ -291,7 +291,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
await clientFinishedSendingRequestBody.Task.TimeoutAfter(TimeSpan.FromSeconds(120)); await clientFinishedSendingRequestBody.Task.TimeoutAfter(TimeSpan.FromSeconds(120));
// Verify client didn't send extra bytes // Verify client didn't send extra bytes
if (context.Request.Body.ReadByte() != -1) if (await context.Request.Body.ReadAsync(new byte[1], 0, 1) != 0)
{ {
context.Response.StatusCode = StatusCodes.Status500InternalServerError; context.Response.StatusCode = StatusCodes.Status500InternalServerError;
await context.Response.WriteAsync("Client sent more bytes than expectedBody.Length"); await context.Response.WriteAsync("Client sent more bytes than expectedBody.Length");

View File

@ -1480,6 +1480,130 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
} }
} }
[Fact]
public async Task SynchronousReadsAllowedByDefault()
{
var firstRequest = true;
using (var server = new TestServer(async context =>
{
var bodyControlFeature = context.Features.Get<IHttpBodyControlFeature>();
Assert.True(bodyControlFeature.AllowSynchronousIO);
var buffer = new byte[6];
var offset = 0;
// The request body is 5 bytes long. The 6th byte (buffer[5]) is only used for writing the response body.
buffer[5] = (byte)(firstRequest ? '1' : '2');
if (firstRequest)
{
while (offset < 5)
{
offset += context.Request.Body.Read(buffer, offset, 5 - offset);
}
firstRequest = false;
}
else
{
bodyControlFeature.AllowSynchronousIO = false;
// Synchronous reads now throw.
var ioEx = Assert.Throws<InvalidOperationException>(() => context.Request.Body.Read(new byte[1], 0, 1));
Assert.Equal("Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead.", ioEx.Message);
var ioEx2 = Assert.Throws<InvalidOperationException>(() => context.Request.Body.CopyTo(Stream.Null));
Assert.Equal("Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead.", ioEx2.Message);
while (offset < 5)
{
offset += await context.Request.Body.ReadAsync(buffer, offset, 5 - offset);
}
}
Assert.Equal(0, await context.Request.Body.ReadAsync(new byte[1], 0, 1));
Assert.Equal("Hello", Encoding.ASCII.GetString(buffer, 0, 5));
context.Response.ContentLength = 6;
await context.Response.Body.WriteAsync(buffer, 0, 6);
}))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"POST / HTTP/1.1",
"Host:",
"Content-Length: 5",
"",
"HelloPOST / HTTP/1.1",
"Host:",
"Content-Length: 5",
"",
"Hello");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Content-Length: 6",
"",
"Hello1HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Content-Length: 6",
"",
"Hello2");
}
}
}
[Fact]
public async Task SynchronousReadsCanBeDisallowedGlobally()
{
var testContext = new TestServiceContext
{
ServerOptions = { AllowSynchronousIO = false }
};
using (var server = new TestServer(async context =>
{
var bodyControlFeature = context.Features.Get<IHttpBodyControlFeature>();
Assert.False(bodyControlFeature.AllowSynchronousIO);
// Synchronous reads now throw.
var ioEx = Assert.Throws<InvalidOperationException>(() => context.Request.Body.Read(new byte[1], 0, 1));
Assert.Equal("Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead.", ioEx.Message);
var ioEx2 = Assert.Throws<InvalidOperationException>(() => context.Request.Body.CopyTo(Stream.Null));
Assert.Equal("Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead.", ioEx2.Message);
var buffer = new byte[5];
var offset = 0;
while (offset < 5)
{
offset += await context.Request.Body.ReadAsync(buffer, offset, 5 - offset);
}
Assert.Equal(0, await context.Request.Body.ReadAsync(new byte[1], 0, 1));
Assert.Equal("Hello", Encoding.ASCII.GetString(buffer));
}, testContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"POST / HTTP/1.1",
"Host:",
"Content-Length: 5",
"",
"Hello");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Content-Length: 0",
"",
"");
}
}
}
private async Task TestRemoteIPAddress(string registerAddress, string requestAddress, string expectAddress) private async Task TestRemoteIPAddress(string registerAddress, string requestAddress, string expectAddress)
{ {
var builder = new WebHostBuilder() var builder = new WebHostBuilder()

View File

@ -508,7 +508,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
public async Task ThrowsAndClosesConnectionWhenAppWritesMoreThanContentLengthWrite() public async Task ThrowsAndClosesConnectionWhenAppWritesMoreThanContentLengthWrite()
{ {
var testLogger = new TestApplicationErrorLogger(); var testLogger = new TestApplicationErrorLogger();
var serviceContext = new TestServiceContext { Log = new TestKestrelTrace(testLogger) }; var serviceContext = new TestServiceContext
{
Log = new TestKestrelTrace(testLogger),
ServerOptions = { AllowSynchronousIO = true }
};
using (var server = new TestServer(httpContext => using (var server = new TestServer(httpContext =>
{ {
@ -536,7 +540,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
} }
} }
var logMessage = Assert.Single(testLogger.Messages, message => message.LogLevel == LogLevel.Error); var logMessage = Assert.Single(testLogger.Messages, message => message.LogLevel == LogLevel.Error);
Assert.Equal( Assert.Equal(
@ -584,7 +587,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
public async Task InternalServerErrorAndConnectionClosedOnWriteWithMoreThanContentLengthAndResponseNotStarted() public async Task InternalServerErrorAndConnectionClosedOnWriteWithMoreThanContentLengthAndResponseNotStarted()
{ {
var testLogger = new TestApplicationErrorLogger(); var testLogger = new TestApplicationErrorLogger();
var serviceContext = new TestServiceContext { Log = new TestKestrelTrace(testLogger) }; var serviceContext = new TestServiceContext
{
Log = new TestKestrelTrace(testLogger),
ServerOptions = { AllowSynchronousIO = true }
};
using (var server = new TestServer(httpContext => using (var server = new TestServer(httpContext =>
{ {
@ -966,6 +973,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
public async Task HeadResponseBodyNotWrittenWithSyncWrite() public async Task HeadResponseBodyNotWrittenWithSyncWrite()
{ {
var flushed = new SemaphoreSlim(0, 1); var flushed = new SemaphoreSlim(0, 1);
var serviceContext = new TestServiceContext { ServerOptions = { AllowSynchronousIO = true } };
using (var server = new TestServer(httpContext => using (var server = new TestServer(httpContext =>
{ {
@ -973,7 +981,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
httpContext.Response.Body.Write(Encoding.ASCII.GetBytes("hello, world"), 0, 12); httpContext.Response.Body.Write(Encoding.ASCII.GetBytes("hello, world"), 0, 12);
flushed.Wait(); flushed.Wait();
return Task.CompletedTask; return Task.CompletedTask;
})) }, serviceContext))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -1248,6 +1256,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
[Fact] [Fact]
public async Task FirstWriteVerifiedAfterOnStarting() public async Task FirstWriteVerifiedAfterOnStarting()
{ {
var serviceContext = new TestServiceContext { ServerOptions = { AllowSynchronousIO = true } };
using (var server = new TestServer(httpContext => using (var server = new TestServer(httpContext =>
{ {
httpContext.Response.OnStarting(() => httpContext.Response.OnStarting(() =>
@ -1263,7 +1273,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
// If OnStarting is not run before verifying writes, an error response will be sent. // If OnStarting is not run before verifying writes, an error response will be sent.
httpContext.Response.Body.Write(response, 0, response.Length); httpContext.Response.Body.Write(response, 0, response.Length);
return Task.CompletedTask; return Task.CompletedTask;
})) }, serviceContext))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -1289,6 +1299,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
[Fact] [Fact]
public async Task SubsequentWriteVerifiedAfterOnStarting() public async Task SubsequentWriteVerifiedAfterOnStarting()
{ {
var serviceContext = new TestServiceContext { ServerOptions = { AllowSynchronousIO = true } };
using (var server = new TestServer(httpContext => using (var server = new TestServer(httpContext =>
{ {
httpContext.Response.OnStarting(() => httpContext.Response.OnStarting(() =>
@ -1305,7 +1317,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
httpContext.Response.Body.Write(response, 0, response.Length / 2); httpContext.Response.Body.Write(response, 0, response.Length / 2);
httpContext.Response.Body.Write(response, response.Length / 2, response.Length - response.Length / 2); httpContext.Response.Body.Write(response, response.Length / 2, response.Length - response.Length / 2);
return Task.CompletedTask; return Task.CompletedTask;
})) }, serviceContext))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -2335,6 +2347,96 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
Assert.Equal(2, callOrder.Pop()); Assert.Equal(2, callOrder.Pop());
} }
[Fact]
public async Task SynchronousWritesAllowedByDefault()
{
var firstRequest = true;
using (var server = new TestServer(async context =>
{
var bodyControlFeature = context.Features.Get<IHttpBodyControlFeature>();
Assert.True(bodyControlFeature.AllowSynchronousIO);
context.Response.ContentLength = 6;
if (firstRequest)
{
context.Response.Body.Write(Encoding.ASCII.GetBytes("Hello1"), 0, 6);
firstRequest = false;
}
else
{
bodyControlFeature.AllowSynchronousIO = false;
// Synchronous writes now throw.
var ioEx = Assert.Throws<InvalidOperationException>(() => context.Response.Body.Write(Encoding.ASCII.GetBytes("What!?"), 0, 6));
Assert.Equal("Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead.", ioEx.Message);
await context.Response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello2"), 0, 6);
}
}))
{
using (var connection = server.CreateConnection())
{
await connection.SendEmptyGet();
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Content-Length: 6",
"",
"Hello1");
await connection.SendEmptyGet();
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Content-Length: 6",
"",
"Hello2");
}
}
}
[Fact]
public async Task SynchronousWritesCanBeDisallowedGlobally()
{
var testContext = new TestServiceContext
{
ServerOptions = { AllowSynchronousIO = false }
};
using (var server = new TestServer(context =>
{
var bodyControlFeature = context.Features.Get<IHttpBodyControlFeature>();
Assert.False(bodyControlFeature.AllowSynchronousIO);
context.Response.ContentLength = 6;
// Synchronous writes now throw.
var ioEx = Assert.Throws<InvalidOperationException>(() => context.Response.Body.Write(Encoding.ASCII.GetBytes("What!?"), 0, 6));
Assert.Equal("Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead.", ioEx.Message);
return context.Response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello!"), 0, 6);
}, testContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Content-Length: 6",
"",
"Hello!");
}
}
}
public static TheoryData<string, StringValues, string> NullHeaderData public static TheoryData<string, StringValues, string> NullHeaderData
{ {
get get

View File

@ -32,12 +32,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
var feature = context.Features.Get<IHttpUpgradeFeature>(); var feature = context.Features.Get<IHttpUpgradeFeature>();
var stream = await feature.UpgradeAsync(); var stream = await feature.UpgradeAsync();
var ex = Assert.Throws<InvalidOperationException>(() => context.Response.Body.WriteByte((byte)' ')); var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.Response.Body.WriteAsync(new byte[1], 0, 1));
Assert.Equal(CoreStrings.ResponseStreamWasUpgraded, ex.Message); Assert.Equal(CoreStrings.ResponseStreamWasUpgraded, ex.Message);
using (var writer = new StreamWriter(stream)) using (var writer = new StreamWriter(stream))
{ {
writer.WriteLine("New protocol data"); await writer.WriteLineAsync("New protocol data");
await writer.FlushAsync();
} }
upgrade.TrySetResult(true); upgrade.TrySetResult(true);
@ -82,6 +83,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
var line = await reader.ReadLineAsync(); var line = await reader.ReadLineAsync();
Assert.Equal(send, line); Assert.Equal(send, line);
await writer.WriteLineAsync(recv); await writer.WriteLineAsync(recv);
await writer.FlushAsync();
} }
upgrade.TrySetResult(true); upgrade.TrySetResult(true);

View File

@ -28,14 +28,14 @@ namespace CodeGenerator
typeof(IHttpRequestIdentifierFeature), typeof(IHttpRequestIdentifierFeature),
typeof(IServiceProvidersFeature), typeof(IServiceProvidersFeature),
typeof(IHttpRequestLifetimeFeature), typeof(IHttpRequestLifetimeFeature),
typeof(IHttpConnectionFeature) typeof(IHttpConnectionFeature),
}; };
var commonFeatures = new[] var commonFeatures = new[]
{ {
typeof(IHttpAuthenticationFeature), typeof(IHttpAuthenticationFeature),
typeof(IQueryFeature), typeof(IQueryFeature),
typeof(IFormFeature) typeof(IFormFeature),
}; };
var sometimesFeatures = new[] var sometimesFeatures = new[]
@ -48,6 +48,7 @@ namespace CodeGenerator
typeof(ISessionFeature), typeof(ISessionFeature),
typeof(IHttpMaxRequestBodySizeFeature), typeof(IHttpMaxRequestBodySizeFeature),
typeof(IHttpMinRequestBodyDataRateFeature), typeof(IHttpMinRequestBodyDataRateFeature),
typeof(IHttpBodyControlFeature),
}; };
var rareFeatures = new[] var rareFeatures = new[]
@ -69,6 +70,7 @@ namespace CodeGenerator
typeof(IHttpConnectionFeature), typeof(IHttpConnectionFeature),
typeof(IHttpMaxRequestBodySizeFeature), typeof(IHttpMaxRequestBodySizeFeature),
typeof(IHttpMinRequestBodyDataRateFeature), typeof(IHttpMinRequestBodyDataRateFeature),
typeof(IHttpBodyControlFeature),
}; };
return $@"// Copyright (c) .NET Foundation. All rights reserved. return $@"// Copyright (c) .NET Foundation. All rights reserved.