diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/CoreStrings.resx b/src/Microsoft.AspNetCore.Server.Kestrel.Core/CoreStrings.resx index 13454c5127..16e59e4c3a 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/CoreStrings.resx +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/CoreStrings.resx @@ -1,17 +1,17 @@  - @@ -336,4 +336,10 @@ The request body rate enforcement grace period must be greater than {heartbeatInterval} second. - + + Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead. + + + Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead. + + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.FeatureCollection.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.FeatureCollection.cs index e9737e5b21..cbc30427e5 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.FeatureCollection.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.FeatureCollection.cs @@ -22,6 +22,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http IHttpConnectionFeature, IHttpRequestLifetimeFeature, IHttpRequestIdentifierFeature, + IHttpBodyControlFeature, IHttpMaxRequestBodySizeFeature, IHttpMinRequestBodyDataRateFeature { @@ -205,6 +206,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http set => TraceIdentifier = value; } + bool IHttpBodyControlFeature.AllowSynchronousIO + { + get => AllowSynchronousIO; + set => AllowSynchronousIO = value; + } + bool IHttpMaxRequestBodySizeFeature.IsReadOnly => HasStartedConsumingRequestBody || _wasUpgraded; long? IHttpMaxRequestBodySizeFeature.MaxRequestBodySize diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.Generated.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.Generated.cs index f2b9c90d10..6aba909708 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.Generated.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.Generated.cs @@ -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 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 IHttpBodyControlFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpBodyControlFeature); private static readonly Type IHttpSendFileFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature); private object _currentIHttpRequestFeature; @@ -44,6 +45,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private object _currentISessionFeature; private object _currentIHttpMaxRequestBodySizeFeature; private object _currentIHttpMinRequestBodyDataRateFeature; + private object _currentIHttpBodyControlFeature; private object _currentIHttpSendFileFeature; private void FastReset() @@ -56,6 +58,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http _currentIHttpConnectionFeature = this; _currentIHttpMaxRequestBodySizeFeature = this; _currentIHttpMinRequestBodyDataRateFeature = this; + _currentIHttpBodyControlFeature = this; _currentIServiceProvidersFeature = null; _currentIHttpAuthenticationFeature = null; @@ -139,6 +142,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { return _currentIHttpMinRequestBodyDataRateFeature; } + if (key == IHttpBodyControlFeatureType) + { + return _currentIHttpBodyControlFeature; + } if (key == IHttpSendFileFeatureType) { return _currentIHttpSendFileFeature; @@ -235,6 +242,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http _currentIHttpMinRequestBodyDataRateFeature = feature; return; } + if (key == IHttpBodyControlFeatureType) + { + _currentIHttpBodyControlFeature = feature; + return; + } if (key == IHttpSendFileFeatureType) { _currentIHttpSendFileFeature = feature; @@ -313,6 +325,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { yield return new KeyValuePair(IHttpMinRequestBodyDataRateFeatureType, _currentIHttpMinRequestBodyDataRateFeature as global::Microsoft.AspNetCore.Server.Kestrel.Core.Features.IHttpMinRequestBodyDataRateFeature); } + if (_currentIHttpBodyControlFeature != null) + { + yield return new KeyValuePair(IHttpBodyControlFeatureType, _currentIHttpBodyControlFeature as global::Microsoft.AspNetCore.Http.Features.IHttpBodyControlFeature); + } if (_currentIHttpSendFileFeature != null) { yield return new KeyValuePair(IHttpSendFileFeatureType, _currentIHttpSendFileFeature as global::Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature); diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.cs index 90171989c3..c9e7ba118b 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.cs @@ -122,6 +122,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http public string ConnectionIdFeature { get; set; } public bool HasStartedConsumingRequestBody { get; set; } public long? MaxRequestBodySize { get; set; } + public bool AllowSynchronousIO { get; set; } /// /// The request id. @@ -305,7 +306,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (_frameStreams == null) { - _frameStreams = new Streams(this); + _frameStreams = new Streams(bodyControl: this, frameControl: this); } (RequestBody, ResponseBody) = _frameStreams.Start(messageBody); @@ -329,6 +330,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http HasStartedConsumingRequestBody = false; MaxRequestBodySize = ServerOptions.Limits.MaxRequestBodySize; + AllowSynchronousIO = ServerOptions.AllowSynchronousIO; TraceIdentifier = null; Scheme = null; Method = null; diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameRequestStream.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameRequestStream.cs index 6ae4b1c385..aa2e9de5a7 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameRequestStream.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameRequestStream.cs @@ -7,17 +7,20 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; +using Microsoft.AspNetCore.Http.Features; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { internal class FrameRequestStream : ReadOnlyStream { + private readonly IHttpBodyControlFeature _bodyControl; private MessageBody _body; private FrameStreamState _state; private Exception _error; - public FrameRequestStream() + public FrameRequestStream(IHttpBodyControlFeature bodyControl) { + _bodyControl = bodyControl; _state = FrameStreamState.Closed; } @@ -34,13 +37,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http public override void Flush() { - // No-op. + throw new NotSupportedException(); } public override Task FlushAsync(CancellationToken cancellationToken) { - // No-op. - return Task.CompletedTask; + throw new NotSupportedException(); } 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) { - // ValueTask uses .GetAwaiter().GetResult() if necessary - return ReadAsync(buffer, offset, count).Result; + if (!_bodyControl.AllowSynchronousIO) + { + 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) diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameResponseStream.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameResponseStream.cs index 3a76f4bf1b..d4c6784b4c 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameResponseStream.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/FrameResponseStream.cs @@ -6,16 +6,19 @@ using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.AspNetCore.Http.Features; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { internal class FrameResponseStream : WriteOnlyStream { - private IFrameControl _frameControl; + private readonly IHttpBodyControlFeature _bodyControl; + private readonly IFrameControl _frameControl; private FrameStreamState _state; - public FrameResponseStream(IFrameControl frameControl) + public FrameResponseStream(IHttpBodyControlFeature bodyControl, IFrameControl frameControl) { + _bodyControl = bodyControl; _frameControl = frameControl; _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) { + if (!_bodyControl.AllowSynchronousIO) + { + throw new InvalidOperationException(CoreStrings.SynchronousWritesDisallowed); + } + WriteAsync(buffer, offset, count, default(CancellationToken)).GetAwaiter().GetResult(); } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/Streams.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/Streams.cs index 994ee2d31c..a7762c6990 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/Streams.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/Streams.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; 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 Stream _upgradeStream; - public Streams(IFrameControl frameControl) + public Streams(IHttpBodyControlFeature bodyControl, IFrameControl frameControl) { - _request = new FrameRequestStream(); - _emptyRequest = new FrameRequestStream(); - _response = new FrameResponseStream(frameControl); + _request = new FrameRequestStream(bodyControl); + _emptyRequest = new FrameRequestStream(bodyControl); + _response = new FrameResponseStream(bodyControl, frameControl); _upgradeableResponse = new WrappingStream(_response); _upgradeStream = new FrameDuplexStream(_request, _response); } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/KestrelServerOptions.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/KestrelServerOptions.cs index b97eecf850..136b983533 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/KestrelServerOptions.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/KestrelServerOptions.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Net; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.Core @@ -35,6 +36,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core /// The default mode is public SchedulingMode ApplicationSchedulingMode { get; set; } = SchedulingMode.Default; + /// + /// Gets or sets a value that controls whether synchronous IO is allowed for the and + /// + /// + /// Defaults to true. + /// + public bool AllowSynchronousIO { get; set; } = true; + /// /// Enables the Listen options callback to resolve and use services registered by the application during startup. /// Typically initialized by UseKestrel()"/>. diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Properties/CoreStrings.Designer.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Properties/CoreStrings.Designer.cs index 5fb571f55b..aa43e6b644 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Properties/CoreStrings.Designer.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Properties/CoreStrings.Designer.cs @@ -1019,7 +1019,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core => GetString("NonNegativeTimeSpanRequired"); /// - /// 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. /// internal static string MinimumGracePeriodRequired { @@ -1027,11 +1027,39 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core } /// - /// 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. /// internal static string FormatMinimumGracePeriodRequired(object heartbeatInterval) => string.Format(CultureInfo.CurrentCulture, GetString("MinimumGracePeriodRequired", "heartbeatInterval"), heartbeatInterval); + /// + /// Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead. + /// + internal static string SynchronousReadsDisallowed + { + get => GetString("SynchronousReadsDisallowed"); + } + + /// + /// Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead. + /// + internal static string FormatSynchronousReadsDisallowed() + => GetString("SynchronousReadsDisallowed"); + + /// + /// Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead. + /// + internal static string SynchronousWritesDisallowed + { + get => GetString("SynchronousWritesDisallowed"); + } + + /// + /// Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead. + /// + internal static string FormatSynchronousWritesDisallowed() + => GetString("SynchronousWritesDisallowed"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameRequestStreamTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameRequestStreamTests.cs index 2250523f63..12a1a87c81 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameRequestStreamTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameRequestStreamTests.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Moq; using Xunit; @@ -16,49 +17,49 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void CanReadReturnsTrue() { - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); Assert.True(stream.CanRead); } [Fact] public void CanSeekReturnsFalse() { - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); Assert.False(stream.CanSeek); } [Fact] public void CanWriteReturnsFalse() { - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); Assert.False(stream.CanWrite); } [Fact] public void SeekThrows() { - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); } [Fact] public void LengthThrows() { - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); Assert.Throws(() => stream.Length); } [Fact] public void SetLengthThrows() { - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); Assert.Throws(() => stream.SetLength(0)); } [Fact] public void PositionThrows() { - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); Assert.Throws(() => stream.Position); Assert.Throws(() => stream.Position = 0); } @@ -66,21 +67,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void WriteThrows() { - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); Assert.Throws(() => stream.Write(new byte[1], 0, 1)); } [Fact] public void WriteByteThrows() { - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); Assert.Throws(() => stream.WriteByte(0)); } [Fact] public async Task WriteAsyncThrows() { - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); await Assert.ThrowsAsync(() => stream.WriteAsync(new byte[1], 0, 1)); } @@ -88,7 +89,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void BeginWriteThrows() { - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); Assert.Throws(() => stream.BeginWrite(new byte[1], 0, 1, null, null)); } #elif NETCOREAPP2_0 @@ -97,23 +98,47 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests #endif [Fact] - public void FlushDoesNotThrow() + public void FlushThrows() { - var stream = new FrameRequestStream(); - stream.Flush(); + var stream = new FrameRequestStream(Mock.Of()); + Assert.Throws(() => stream.Flush()); } [Fact] - public async Task FlushAsyncDoesNotThrow() + public async Task FlushAsyncThrows() { - var stream = new FrameRequestStream(); - await stream.FlushAsync(); + var stream = new FrameRequestStream(Mock.Of()); + await Assert.ThrowsAsync(() => stream.FlushAsync()); + } + + [Fact] + public async Task SynchronousReadsThrowIfDisallowedByIHttpBodyControlFeature() + { + var allowSynchronousIO = false; + var mockBodyControl = new Mock(); + mockBodyControl.Setup(m => m.AllowSynchronousIO).Returns(() => allowSynchronousIO); + var mockMessageBody = new Mock((Frame)null); + mockMessageBody.Setup(m => m.ReadAsync(It.IsAny>(), 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(() => 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(() => 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] public void AbortCausesReadToCancel() { - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); stream.StartAcceptingReads(null); stream.Abort(); var task = stream.ReadAsync(new byte[1], 0, 1); @@ -123,7 +148,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void AbortWithErrorCausesReadToCancel() { - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); stream.StartAcceptingReads(null); var error = new Exception(); stream.Abort(error); @@ -135,7 +160,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void StopAcceptingReadsCausesReadToThrowObjectDisposedException() { - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); stream.StartAcceptingReads(null); stream.StopAcceptingReads(); Assert.Throws(() => { stream.ReadAsync(new byte[1], 0, 1); }); @@ -144,7 +169,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void AbortCausesCopyToAsyncToCancel() { - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); stream.StartAcceptingReads(null); stream.Abort(); var task = stream.CopyToAsync(Mock.Of()); @@ -154,7 +179,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void AbortWithErrorCausesCopyToAsyncToCancel() { - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); stream.StartAcceptingReads(null); var error = new Exception(); stream.Abort(error); @@ -166,7 +191,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void StopAcceptingReadsCausesCopyToAsyncToThrowObjectDisposedException() { - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); stream.StartAcceptingReads(null); stream.StopAcceptingReads(); Assert.Throws(() => { stream.CopyToAsync(Mock.Of()); }); @@ -175,7 +200,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void NullDestinationCausesCopyToAsyncToThrowArgumentNullException() { - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); stream.StartAcceptingReads(null); Assert.Throws(() => { stream.CopyToAsync(null); }); } @@ -183,7 +208,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void ZeroBufferSizeCausesCopyToAsyncToThrowArgumentException() { - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); stream.StartAcceptingReads(null); Assert.Throws(() => { stream.CopyToAsync(Mock.Of(), 0); }); } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameResponseStreamTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameResponseStreamTests.cs index 12f0e0019c..14cc4ebb7e 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameResponseStreamTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameResponseStreamTests.cs @@ -3,9 +3,12 @@ using System; using System.IO; +using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Tests.TestHelpers; +using Moq; using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests @@ -15,79 +18,111 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void CanReadReturnsFalse() { - var stream = new FrameResponseStream(new MockFrameControl()); + var stream = new FrameResponseStream(Mock.Of(), new MockFrameControl()); Assert.False(stream.CanRead); } [Fact] public void CanSeekReturnsFalse() { - var stream = new FrameResponseStream(new MockFrameControl()); + var stream = new FrameResponseStream(Mock.Of(), new MockFrameControl()); Assert.False(stream.CanSeek); } [Fact] public void CanWriteReturnsTrue() { - var stream = new FrameResponseStream(new MockFrameControl()); + var stream = new FrameResponseStream(Mock.Of(), new MockFrameControl()); Assert.True(stream.CanWrite); } [Fact] public void ReadThrows() { - var stream = new FrameResponseStream(new MockFrameControl()); + var stream = new FrameResponseStream(Mock.Of(), new MockFrameControl()); Assert.Throws(() => stream.Read(new byte[1], 0, 1)); } [Fact] public void ReadByteThrows() { - var stream = new FrameResponseStream(new MockFrameControl()); + var stream = new FrameResponseStream(Mock.Of(), new MockFrameControl()); Assert.Throws(() => stream.ReadByte()); } [Fact] public async Task ReadAsyncThrows() { - var stream = new FrameResponseStream(new MockFrameControl()); + var stream = new FrameResponseStream(Mock.Of(), new MockFrameControl()); await Assert.ThrowsAsync(() => stream.ReadAsync(new byte[1], 0, 1)); } [Fact] public void BeginReadThrows() { - var stream = new FrameResponseStream(new MockFrameControl()); + var stream = new FrameResponseStream(Mock.Of(), new MockFrameControl()); Assert.Throws(() => stream.BeginRead(new byte[1], 0, 1, null, null)); } [Fact] public void SeekThrows() { - var stream = new FrameResponseStream(new MockFrameControl()); + var stream = new FrameResponseStream(Mock.Of(), new MockFrameControl()); Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); } [Fact] public void LengthThrows() { - var stream = new FrameResponseStream(new MockFrameControl()); + var stream = new FrameResponseStream(Mock.Of(), new MockFrameControl()); Assert.Throws(() => stream.Length); } [Fact] public void SetLengthThrows() { - var stream = new FrameResponseStream(new MockFrameControl()); + var stream = new FrameResponseStream(Mock.Of(), new MockFrameControl()); Assert.Throws(() => stream.SetLength(0)); } [Fact] public void PositionThrows() { - var stream = new FrameResponseStream(new MockFrameControl()); + var stream = new FrameResponseStream(Mock.Of(), new MockFrameControl()); Assert.Throws(() => stream.Position); Assert.Throws(() => stream.Position = 0); } + + [Fact] + public void StopAcceptingWritesCausesWriteToThrowObjectDisposedException() + { + var stream = new FrameResponseStream(Mock.Of(), Mock.Of()); + stream.StartAcceptingWrites(); + stream.StopAcceptingWrites(); + Assert.Throws(() => { stream.WriteAsync(new byte[1], 0, 1); }); + } + + [Fact] + public async Task SynchronousWritesThrowIfDisallowedByIHttpBodyControlFeature() + { + var allowSynchronousIO = false; + var mockBodyControl = new Mock(); + mockBodyControl.Setup(m => m.AllowSynchronousIO).Returns(() => allowSynchronousIO); + var mockFrameControl = new Mock(); + mockFrameControl.Setup(m => m.WriteAsync(It.IsAny>(), 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(() => 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); + } } } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/KestrelServerOptionsTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/KestrelServerOptionsTests.cs index e0e97f2aac..f0209cbea4 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/KestrelServerOptionsTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/KestrelServerOptionsTests.cs @@ -22,5 +22,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.True(o1.ListenOptions[0].NoDelay); Assert.False(o1.ListenOptions[1].NoDelay); } + + [Fact] + public void AllowSynchronousIODefaultsToTrue() + { + var options = new KestrelServerOptions(); + + Assert.True(options.AllowSynchronousIO); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/MessageBodyTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/MessageBodyTests.cs index 655582dcf1..c6d2283301 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/MessageBodyTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/MessageBodyTests.cs @@ -9,6 +9,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Testing; @@ -28,7 +29,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests using (var input = new TestInput()) { var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderContentLength = "5" }, input.Frame); - var stream = new FrameRequestStream(); + var mockBodyControl = new Mock(); + mockBodyControl.Setup(m => m.AllowSynchronousIO).Returns(true); + var stream = new FrameRequestStream(mockBodyControl.Object); stream.StartAcceptingReads(body); input.Add("Hello"); @@ -54,7 +57,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests using (var input = new TestInput()) { var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderContentLength = "5" }, input.Frame); - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); stream.StartAcceptingReads(body); input.Add("Hello"); @@ -78,7 +81,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests using (var input = new TestInput()) { var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame); - var stream = new FrameRequestStream(); + var mockBodyControl = new Mock(); + mockBodyControl.Setup(m => m.AllowSynchronousIO).Returns(true); + var stream = new FrameRequestStream(mockBodyControl.Object); stream.StartAcceptingReads(body); input.Add("5\r\nHello\r\n"); @@ -104,7 +109,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests using (var input = new TestInput()) { var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame); - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); stream.StartAcceptingReads(body); input.Add("5\r\nHello\r\n"); @@ -130,7 +135,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests using (var input = new TestInput()) { var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame); - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); stream.StartAcceptingReads(body); input.Add("5;\r\0"); @@ -155,7 +160,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests using (var input = new TestInput()) { var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame); - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); stream.StartAcceptingReads(body); input.Add("80000000\r\n"); @@ -176,7 +181,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests using (var input = new TestInput()) { var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderTransferEncoding = "chunked" }, input.Frame); - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); stream.StartAcceptingReads(body); input.Add("012345678\r"); @@ -199,7 +204,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests using (var input = new TestInput()) { var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderConnection = "upgrade" }, input.Frame); - var stream = new FrameRequestStream(); + var mockBodyControl = new Mock(); + mockBodyControl.Setup(m => m.AllowSynchronousIO).Returns(true); + var stream = new FrameRequestStream(mockBodyControl.Object); stream.StartAcceptingReads(body); input.Add("Hello"); @@ -224,7 +231,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests using (var input = new TestInput()) { var body = MessageBody.For(httpVersion, new FrameRequestHeaders { HeaderConnection = "upgrade" }, input.Frame); - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); stream.StartAcceptingReads(body); input.Add("Hello"); @@ -249,7 +256,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests using (var input = new TestInput()) { var body = MessageBody.For(httpVersion, new FrameRequestHeaders(), input.Frame); - var stream = new FrameRequestStream(); + var mockBodyControl = new Mock(); + mockBodyControl.Setup(m => m.AllowSynchronousIO).Returns(true); + var stream = new FrameRequestStream(mockBodyControl.Object); stream.StartAcceptingReads(body); input.Add("Hello"); @@ -269,7 +278,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests using (var input = new TestInput()) { var body = MessageBody.For(httpVersion, new FrameRequestHeaders(), input.Frame); - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); stream.StartAcceptingReads(body); input.Add("Hello"); @@ -287,7 +296,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests using (var input = new TestInput()) { var body = MessageBody.For(HttpVersion.Http10, new FrameRequestHeaders { HeaderContentLength = "8197" }, input.Frame); - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); stream.StartAcceptingReads(body); // 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()) { var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderConnection = headerConnection }, input.Frame); - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); stream.StartAcceptingReads(body); input.Add("Hello"); 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(buffer, 0, 5)); input.Fin(); @@ -500,7 +509,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests using (var input = new TestInput()) { var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "2" }, input.Frame); - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); stream.StartAcceptingReads(body); // 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()) { var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "2" }, input.Frame); - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); stream.StartAcceptingReads(body); // 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"; var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "2" }, input.Frame); - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); stream.StartAcceptingReads(body); // 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"; var body = MessageBody.For(HttpVersion.Http11, new FrameRequestHeaders { HeaderContentLength = "2" }, input.Frame); - var stream = new FrameRequestStream(); + var stream = new FrameRequestStream(Mock.Of()); stream.StartAcceptingReads(body); // Add some input and consume it to ensure PumpAsync is running diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/StreamsTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/StreamsTests.cs index 6a7b108228..b2657bc60a 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/StreamsTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/StreamsTests.cs @@ -4,9 +4,11 @@ using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Internal.System.IO.Pipelines; +using Microsoft.AspNetCore.Testing; using Moq; using Xunit; @@ -17,7 +19,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public async Task StreamsThrowAfterAbort() { - var streams = new Streams(Mock.Of()); + var streams = new Streams(Mock.Of(), Mock.Of()); var (request, response) = streams.Start(new MockMessageBody()); var ex = new Exception("My error"); @@ -31,7 +33,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public async Task StreamsThrowOnAbortAfterUpgrade() { - var streams = new Streams(Mock.Of()); + var streams = new Streams(Mock.Of(), Mock.Of()); var (request, response) = streams.Start(new MockMessageBody(upgradeable: true)); var upgrade = streams.Upgrade(); @@ -53,7 +55,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public async Task StreamsThrowOnUpgradeAfterAbort() { - var streams = new Streams(Mock.Of()); + var streams = new Streams(Mock.Of(), Mock.Of()); var (request, response) = streams.Start(new MockMessageBody(upgradeable: true)); var ex = new Exception("My error"); diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestBufferSizeTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestBufferSizeTests.cs index 40536f5d28..6b3d66de1a 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestBufferSizeTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestBufferSizeTests.cs @@ -291,7 +291,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests await clientFinishedSendingRequestBody.Task.TimeoutAfter(TimeSpan.FromSeconds(120)); // 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; await context.Response.WriteAsync("Client sent more bytes than expectedBody.Length"); diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs index c808346744..a130aadde3 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs @@ -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(); + 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(() => 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(() => 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(); + Assert.False(bodyControlFeature.AllowSynchronousIO); + + // Synchronous reads now throw. + var ioEx = Assert.Throws(() => 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(() => 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) { var builder = new WebHostBuilder() diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/ResponseTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/ResponseTests.cs index 155a1dac95..f43a1e5c2b 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/ResponseTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/ResponseTests.cs @@ -508,7 +508,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests public async Task ThrowsAndClosesConnectionWhenAppWritesMoreThanContentLengthWrite() { 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 => { @@ -536,7 +540,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - var logMessage = Assert.Single(testLogger.Messages, message => message.LogLevel == LogLevel.Error); Assert.Equal( @@ -584,7 +587,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests public async Task InternalServerErrorAndConnectionClosedOnWriteWithMoreThanContentLengthAndResponseNotStarted() { 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 => { @@ -966,6 +973,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests public async Task HeadResponseBodyNotWrittenWithSyncWrite() { var flushed = new SemaphoreSlim(0, 1); + var serviceContext = new TestServiceContext { ServerOptions = { AllowSynchronousIO = true } }; 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); flushed.Wait(); return Task.CompletedTask; - })) + }, serviceContext)) { using (var connection = server.CreateConnection()) { @@ -1248,6 +1256,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests [Fact] public async Task FirstWriteVerifiedAfterOnStarting() { + var serviceContext = new TestServiceContext { ServerOptions = { AllowSynchronousIO = true } }; + using (var server = new TestServer(httpContext => { 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. httpContext.Response.Body.Write(response, 0, response.Length); return Task.CompletedTask; - })) + }, serviceContext)) { using (var connection = server.CreateConnection()) { @@ -1289,6 +1299,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests [Fact] public async Task SubsequentWriteVerifiedAfterOnStarting() { + var serviceContext = new TestServiceContext { ServerOptions = { AllowSynchronousIO = true } }; + using (var server = new TestServer(httpContext => { 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, response.Length / 2, response.Length - response.Length / 2); return Task.CompletedTask; - })) + }, serviceContext)) { using (var connection = server.CreateConnection()) { @@ -2335,6 +2347,96 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests 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(); + 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(() => 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(); + Assert.False(bodyControlFeature.AllowSynchronousIO); + + context.Response.ContentLength = 6; + + // Synchronous writes now throw. + var ioEx = Assert.Throws(() => 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 NullHeaderData { get diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/UpgradeTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/UpgradeTests.cs index 5fa497dd0e..8a24951173 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/UpgradeTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/UpgradeTests.cs @@ -32,12 +32,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var feature = context.Features.Get(); var stream = await feature.UpgradeAsync(); - var ex = Assert.Throws(() => context.Response.Body.WriteByte((byte)' ')); + var ex = await Assert.ThrowsAsync(() => context.Response.Body.WriteAsync(new byte[1], 0, 1)); Assert.Equal(CoreStrings.ResponseStreamWasUpgraded, ex.Message); using (var writer = new StreamWriter(stream)) { - writer.WriteLine("New protocol data"); + await writer.WriteLineAsync("New protocol data"); + await writer.FlushAsync(); } upgrade.TrySetResult(true); @@ -82,6 +83,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var line = await reader.ReadLineAsync(); Assert.Equal(send, line); await writer.WriteLineAsync(recv); + await writer.FlushAsync(); } upgrade.TrySetResult(true); diff --git a/tools/CodeGenerator/FrameFeatureCollection.cs b/tools/CodeGenerator/FrameFeatureCollection.cs index 636f333854..5d5f83c11a 100644 --- a/tools/CodeGenerator/FrameFeatureCollection.cs +++ b/tools/CodeGenerator/FrameFeatureCollection.cs @@ -28,14 +28,14 @@ namespace CodeGenerator typeof(IHttpRequestIdentifierFeature), typeof(IServiceProvidersFeature), typeof(IHttpRequestLifetimeFeature), - typeof(IHttpConnectionFeature) + typeof(IHttpConnectionFeature), }; var commonFeatures = new[] { typeof(IHttpAuthenticationFeature), typeof(IQueryFeature), - typeof(IFormFeature) + typeof(IFormFeature), }; var sometimesFeatures = new[] @@ -48,6 +48,7 @@ namespace CodeGenerator typeof(ISessionFeature), typeof(IHttpMaxRequestBodySizeFeature), typeof(IHttpMinRequestBodyDataRateFeature), + typeof(IHttpBodyControlFeature), }; var rareFeatures = new[] @@ -69,6 +70,7 @@ namespace CodeGenerator typeof(IHttpConnectionFeature), typeof(IHttpMaxRequestBodySizeFeature), typeof(IHttpMinRequestBodyDataRateFeature), + typeof(IHttpBodyControlFeature), }; return $@"// Copyright (c) .NET Foundation. All rights reserved.