diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Filter/Internal/FilteredStreamAdapter.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Filter/Internal/FilteredStreamAdapter.cs index bcd6b709e..8a3712a79 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Filter/Internal/FilteredStreamAdapter.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Filter/Internal/FilteredStreamAdapter.cs @@ -24,9 +24,10 @@ public FilteredStreamAdapter( Stream filteredStream, MemoryPool memory, IKestrelTrace logger, - IThreadPool threadPool) + IThreadPool threadPool, + IBufferLengthControl bufferLengthControl) { - SocketInput = new SocketInput(memory, threadPool); + SocketInput = new SocketInput(memory, threadPool, bufferLengthControl); SocketOutput = new StreamSocketOutput(connectionId, filteredStream, memory, logger); _connectionId = connectionId; diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/BufferLengthControl.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/BufferLengthControl.cs new file mode 100644 index 000000000..4c3f21c65 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/BufferLengthControl.cs @@ -0,0 +1,83 @@ +using System.Diagnostics; + +namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http +{ + public class BufferLengthControl : IBufferLengthControl + { + private readonly int _maxLength; + private readonly IConnectionControl _connectionControl; + private readonly KestrelThread _connectionThread; + + private readonly object _lock = new object(); + + private int _length; + private bool _connectionPaused; + + public BufferLengthControl(int maxLength, IConnectionControl connectionControl, KestrelThread connectionThread) + { + _maxLength = maxLength; + _connectionControl = connectionControl; + _connectionThread = connectionThread; + } + + private int Length + { + get + { + return _length; + } + set + { + // Caller should ensure that bytes are never consumed before the producer has called Add() + Debug.Assert(value >= 0); + _length = value; + } + } + + public void Add(int count) + { + Debug.Assert(count >= 0); + + if (count == 0) + { + // No-op and avoid taking lock to reduce contention + return; + } + + lock (_lock) + { + Length += count; + if (!_connectionPaused && Length >= _maxLength) + { + _connectionPaused = true; + _connectionThread.Post( + (connectionControl) => ((IConnectionControl)connectionControl).Pause(), + _connectionControl); + } + } + } + + public void Subtract(int count) + { + Debug.Assert(count >= 0); + + if (count == 0) + { + // No-op and avoid taking lock to reduce contention + return; + } + + lock (_lock) + { + Length -= count; + if (_connectionPaused && Length < _maxLength) + { + _connectionPaused = false; + _connectionThread.Post( + (connectionControl) => ((IConnectionControl)connectionControl).Resume(), + _connectionControl); + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs index 647254316..b9cd6e436 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Server.Kestrel.Filter; @@ -41,6 +42,8 @@ public class Connection : ConnectionContext, IConnectionControl private ConnectionState _connectionState; private TaskCompletionSource _socketClosedTcs; + private BufferLengthControl _bufferLengthControl; + public Connection(ListenerContext context, UvStreamHandle socket) : base(context) { _socket = socket; @@ -49,7 +52,12 @@ public Connection(ListenerContext context, UvStreamHandle socket) : base(context ConnectionId = GenerateConnectionId(Interlocked.Increment(ref _lastConnectionId)); - _rawSocketInput = new SocketInput(Memory, ThreadPool); + if (ServerOptions.MaxInputBufferLength.HasValue) + { + _bufferLengthControl = new BufferLengthControl(ServerOptions.MaxInputBufferLength.Value, this, Thread); + } + + _rawSocketInput = new SocketInput(Memory, ThreadPool, _bufferLengthControl); _rawSocketOutput = new SocketOutput(Thread, _socket, Memory, this, ConnectionId, Log, ThreadPool, WriteReqPool); } @@ -217,7 +225,7 @@ private void ApplyConnectionFilter() if (_filterContext.Connection != _libuvStream) { - _filteredStreamAdapter = new FilteredStreamAdapter(ConnectionId, _filterContext.Connection, Memory, Log, ThreadPool); + _filteredStreamAdapter = new FilteredStreamAdapter(ConnectionId, _filterContext.Connection, Memory, Log, ThreadPool, _bufferLengthControl); SocketInput = _filteredStreamAdapter.SocketInput; SocketOutput = _filteredStreamAdapter.SocketOutput; @@ -316,7 +324,17 @@ void IConnectionControl.Pause() void IConnectionControl.Resume() { Log.ConnectionResume(ConnectionId); - _socket.ReadStart(_allocCallback, _readCallback, this); + try + { + _socket.ReadStart(_allocCallback, _readCallback, this); + } + catch (UvException) + { + // ReadStart() can throw a UvException in some cases (e.g. socket is no longer connected). + // This should be treated the same as OnRead() seeing a "normalDone" condition. + Log.ConnectionReadFin(ConnectionId); + _rawSocketInput.IncomingComplete(0, null); + } } void IConnectionControl.End(ProduceEndType endType) diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/IBufferLengthControl.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/IBufferLengthControl.cs new file mode 100644 index 000000000..28fa24c4b --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/IBufferLengthControl.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http +{ + public interface IBufferLengthControl + { + void Add(int count); + void Subtract(int count); + } +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/SocketInput.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/SocketInput.cs index 3082edf70..3ef7fbe1a 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/SocketInput.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/SocketInput.cs @@ -18,6 +18,7 @@ public class SocketInput : ICriticalNotifyCompletion, IDisposable private readonly MemoryPool _memory; private readonly IThreadPool _threadPool; + private readonly IBufferLengthControl _bufferLengthControl; private readonly ManualResetEventSlim _manualResetEvent = new ManualResetEventSlim(false, 0); private Action _awaitableState; @@ -32,10 +33,11 @@ public class SocketInput : ICriticalNotifyCompletion, IDisposable private bool _consuming; private bool _disposed; - public SocketInput(MemoryPool memory, IThreadPool threadPool) + public SocketInput(MemoryPool memory, IThreadPool threadPool, IBufferLengthControl bufferLengthControl = null) { _memory = memory; _threadPool = threadPool; + _bufferLengthControl = bufferLengthControl; _awaitableState = _awaitableIsNotCompleted; } @@ -63,6 +65,9 @@ public void IncomingData(byte[] buffer, int offset, int count) { lock (_sync) { + // Must call Add() before bytes are available to consumer, to ensure that Length is >= 0 + _bufferLengthControl?.Add(count); + if (count > 0) { if (_tail == null) @@ -93,6 +98,9 @@ public void IncomingComplete(int count, Exception error) { lock (_sync) { + // Must call Add() before bytes are available to consumer, to ensure that Length is >= 0 + _bufferLengthControl?.Add(count); + if (_pinned != null) { _pinned.End += count; @@ -189,10 +197,21 @@ public void ConsumingComplete( { if (!consumed.IsDefault) { + // Compute lengthConsumed before modifying _head or consumed + var lengthConsumed = 0; + if (_bufferLengthControl != null) + { + lengthConsumed = new MemoryPoolIterator(_head).GetLength(consumed); + } + returnStart = _head; returnEnd = consumed.Block; _head = consumed.Block; _head.Start = consumed.Index; + + // Must call Subtract() after _head has been advanced, to avoid producer starting too early and growing + // buffer beyond max length. + _bufferLengthControl?.Subtract(lengthConsumed); } if (!examined.IsDefault && diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/UvStreamHandle.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/UvStreamHandle.cs index bc0bbc64a..1a930f604 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/UvStreamHandle.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/UvStreamHandle.cs @@ -105,16 +105,16 @@ public void ReadStart( } } + // UvStreamHandle.ReadStop() should be idempotent to match uv_read_stop() public void ReadStop() { - if (!_readVitality.IsAllocated) + if (_readVitality.IsAllocated) { - throw new InvalidOperationException("TODO: ReadStart must be called before ReadStop may be called"); + _readVitality.Free(); } _allocCallback = null; _readCallback = null; _readState = null; - _readVitality.Free(); _uv.read_stop(this); } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerOptions.cs b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerOptions.cs index 7a6c7953e..150e02e2b 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerOptions.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerOptions.cs @@ -8,16 +8,40 @@ namespace Microsoft.AspNetCore.Server.Kestrel { public class KestrelServerOptions { + // Matches the default client_max_body_size in nginx. Also large enough that most requests + // should be under the limit. + private int? _maxInputBufferLength = 1024 * 1024; + + /// + /// Gets or sets whether the Server header should be included in each response. + /// + public bool AddServerHeader { get; set; } = true; + public IServiceProvider ApplicationServices { get; set; } public IConnectionFilter ConnectionFilter { get; set; } - public bool NoDelay { get; set; } = true; - /// - /// Gets or sets whether the Server header should be included in each response. + /// Maximum number of bytes used to buffer input for each connection. + /// Default is 1,048,576 bytes (1 MB). /// - public bool AddServerHeader { get; set; } = true; + public int? MaxInputBufferLength + { + get + { + return _maxInputBufferLength; + } + set + { + if (value.HasValue && value.Value <= 0) + { + throw new ArgumentOutOfRangeException("value", "Value must be null or a positive integer."); + } + _maxInputBufferLength = value; + } + } + + public bool NoDelay { get; set; } = true; /// /// The amount of time after the server begins shutting down before connections will be forcefully closed. diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/IWebHostPortExtensions.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/IWebHostPortExtensions.cs index 9b3509503..625a10a7a 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/IWebHostPortExtensions.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/IWebHostPortExtensions.cs @@ -10,16 +10,35 @@ namespace Microsoft.AspNetCore.Hosting { public static class IWebHostPortExtensions { + public static string GetHost(this IWebHost host) + { + return host.GetUris().First().Host; + } + public static int GetPort(this IWebHost host) { return host.GetPorts().First(); } + public static int GetPort(this IWebHost host, string scheme) + { + return host.GetUris() + .Where(u => u.Scheme.Equals(scheme, StringComparison.OrdinalIgnoreCase)) + .Select(u => u.Port) + .First(); + } + public static IEnumerable GetPorts(this IWebHost host) + { + return host.GetUris() + .Select(u => u.Port); + } + + public static IEnumerable GetUris(this IWebHost host) { return host.ServerFeatures.Get().Addresses .Select(a => a.Replace("://+", "://localhost")) - .Select(a => (new Uri(a)).Port); + .Select(a => new Uri(a)); } } } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxInputBufferLengthTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxInputBufferLengthTests.cs new file mode 100644 index 000000000..e6cb1e798 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxInputBufferLengthTests.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Authentication; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +{ + public class MaxInputBufferLengthTests + { + private const int _dataLength = 20 * 1024 * 1024; + + public static IEnumerable LargeUploadData + { + get + { + var maxInputBufferLengthValues = new Tuple[] { + // Smallest allowed buffer. Server should call pause/resume between each read. + Tuple.Create((int?)1, true), + + // Small buffer, but large enough to hold all request headers. + Tuple.Create((int?)16 * 1024, true), + + // Default buffer. + Tuple.Create((int?)1024 * 1024, true), + + // Larger than default, but still significantly lower than data, so client should be paused. + // On Windows, the client is usually paused around (MaxInputBufferLength + 700,000). + // On Linux, the client is usually paused around (MaxInputBufferLength + 10,000,000). + Tuple.Create((int?)5 * 1024 * 1024, true), + + // Even though maxInputBufferLength < _dataLength, client should not be paused since the + // OS-level buffers in client and/or server will handle the overflow. + Tuple.Create((int?)_dataLength - 1, false), + + // Buffer is exactly the same size as data. Exposed race condition where + // IConnectionControl.Resume() was called after socket was disconnected. + Tuple.Create((int?)_dataLength, false), + + // Largest possible buffer, should never trigger backpressure. + Tuple.Create((int?)Int32.MaxValue, false), + + // Disables all code related to computing and limiting the size of the input buffer. + Tuple.Create((int?)null, false) + }; + var sendContentLengthHeaderValues = new[] { true, false }; + var sslValues = new[] { true, false }; + + return from maxInputBufferLength in maxInputBufferLengthValues + from sendContentLengthHeader in sendContentLengthHeaderValues + from ssl in sslValues + select new object[] { + maxInputBufferLength.Item1, + sendContentLengthHeader, + ssl, + maxInputBufferLength.Item2 + }; + } + } + + private static void Log(StringBuilder builder, string s) + { + builder.AppendLine($"[{DateTime.Now.ToString("hh:mm:ss.fff")}] {s}"); + } + + [Theory] + [MemberData("LargeUploadData")] + public async Task LargeUpload(int? maxInputBufferLength, bool sendContentLengthHeader, bool ssl, bool expectPause) + { + // Parameters + var data = new byte[_dataLength]; + var bytesWrittenTimeout = TimeSpan.FromMilliseconds(100); + var bytesWrittenPollingInterval = TimeSpan.FromMilliseconds(bytesWrittenTimeout.TotalMilliseconds / 10); + var maxSendSize = 4096; + + // Initialize data with random bytes + (new Random()).NextBytes(data); + + var startReadingRequestBody = new ManualResetEvent(false); + var clientFinishedSendingRequestBody = new ManualResetEvent(false); + var lastBytesWritten = DateTime.MaxValue; + + using (var host = StartWebHost(maxInputBufferLength, data, startReadingRequestBody, clientFinishedSendingRequestBody)) + { + var port = host.GetPort(ssl ? "https" : "http"); + using (var socket = CreateSocket(port)) + using (var stream = await CreateStreamAsync(socket, ssl, host.GetHost())) + { + await WritePostRequestHeaders(stream, sendContentLengthHeader ? (int?)data.Length : null); + + var bytesWritten = 0; + var logBuilder = new StringBuilder(); + + Func sendFunc = async () => + { + while (bytesWritten < data.Length) + { + var size = Math.Min(data.Length - bytesWritten, maxSendSize); + await stream.WriteAsync(data, bytesWritten, size); + bytesWritten += size; + lastBytesWritten = DateTime.Now; + Log(logBuilder, $"bytesWritten: {bytesWritten}, lastBytesWritten: {lastBytesWritten.ToString("hh:mm:ss.fff")}"); + } + + Assert.Equal(data.Length, bytesWritten); + socket.Shutdown(SocketShutdown.Send); + clientFinishedSendingRequestBody.Set(); + }; + + var sendTask = sendFunc(); + + if (expectPause) + { + // Block until the send task has gone a while without writing bytes, which likely means + // the server input buffer is full. + while ((DateTime.Now - lastBytesWritten) < bytesWrittenTimeout) + { + await Task.Delay(bytesWrittenPollingInterval); + } + Log(logBuilder, $"bytesWrittenTimeout: {bytesWrittenTimeout}, " + + $"DateTime.Now - lastBytesWritten: {DateTime.Now - lastBytesWritten}"); + + // Verify the number of bytes written before the client was paused. + // + // The minimum is (maxInputBufferLength - maxSendSize + 1), since if bytesWritten is + // (maxInputBufferLength - maxSendSize) or smaller, the client should be able to + // complete another send. + // + // The maximum is harder to determine, since there can be OS-level buffers in both the client + // and server, which allow the client to send more than maxInputBufferLength before getting + // paused. We assume the combined buffers are smaller than the difference between + // data.Length and maxInputBufferLength. + try + { + Assert.InRange(bytesWritten, maxInputBufferLength.Value - maxSendSize + 1, data.Length - 1); + } + catch + { + Console.WriteLine(logBuilder.ToString()); + throw; + } + + // Tell server to start reading request body + startReadingRequestBody.Set(); + + // Wait for sendTask to finish sending the remaining bytes + await sendTask; + } + else + { + // Ensure all bytes can be sent before the server starts reading + await sendTask; + + // Tell server to start reading request body + startReadingRequestBody.Set(); + } + + using (var reader = new StreamReader(stream, Encoding.ASCII)) + { + var response = reader.ReadToEnd(); + Assert.Contains($"bytesRead: {data.Length}", response); + } + } + } + } + + private static IWebHost StartWebHost(int? maxInputBufferLength, byte[] expectedBody, ManualResetEvent startReadingRequestBody, + ManualResetEvent clientFinishedSendingRequestBody) + { + var host = new WebHostBuilder() + .UseKestrel(options => + { + options.MaxInputBufferLength = maxInputBufferLength; + options.UseHttps(@"TestResources/testCert.pfx", "testPassword"); + }) + .UseUrls("http://127.0.0.1:0/", "https://127.0.0.1:0/") + .UseContentRoot(Directory.GetCurrentDirectory()) + .Configure(app => app.Run(async context => + { + startReadingRequestBody.WaitOne(); + + var buffer = new byte[expectedBody.Length]; + var bytesRead = 0; + while (bytesRead < buffer.Length) + { + bytesRead += await context.Request.Body.ReadAsync(buffer, bytesRead, buffer.Length - bytesRead); + } + + clientFinishedSendingRequestBody.WaitOne(); + + // Verify client didn't send extra bytes + if (context.Request.Body.ReadByte() != -1) + { + context.Response.StatusCode = 500; + await context.Response.WriteAsync("Client sent more bytes than expectedBody.Length"); + return; + } + + // Verify bytes received match expectedBody + for (int i = 0; i < expectedBody.Length; i++) + { + if (buffer[i] != expectedBody[i]) + { + context.Response.StatusCode = 500; + await context.Response.WriteAsync($"Bytes received do not match expectedBody at position {i}"); + return; + } + } + + await context.Response.WriteAsync($"bytesRead: {bytesRead.ToString()}"); + })) + .Build(); + + host.Start(); + + return host; + } + + private static Socket CreateSocket(int port) + { + var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + + // Timeouts large enough to prevent false positives, but small enough to fail quickly. + socket.SendTimeout = 10 * 1000; + socket.ReceiveTimeout = 10 * 1000; + + socket.Connect(IPAddress.Loopback, port); + + return socket; + } + + private static async Task WritePostRequestHeaders(Stream stream, int? contentLength) + { + using (var writer = new StreamWriter(stream, Encoding.ASCII, bufferSize: 1024, leaveOpen: true)) + { + await writer.WriteAsync("POST / HTTP/1.0\r\n"); + if (contentLength.HasValue) + { + await writer.WriteAsync($"Content-Length: {contentLength.Value}\r\n"); + } + await writer.WriteAsync("\r\n"); + } + } + + private static async Task CreateStreamAsync(Socket socket, bool ssl, string targetHost) + { + var networkStream = new NetworkStream(socket); + if (ssl) + { + var sslStream = new SslStream(networkStream, leaveInnerStreamOpen: false, + userCertificateValidationCallback: (a, b, c, d) => true); + await sslStream.AuthenticateAsClientAsync(targetHost, clientCertificates: null, + enabledSslProtocols: SslProtocols.Tls11 | SslProtocols.Tls12, checkCertificateRevocation: false); + return sslStream; + } + else + { + return networkStream; + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/TestResources/testCert.pfx b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/TestResources/testCert.pfx new file mode 100644 index 000000000..7118908c2 Binary files /dev/null and b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/TestResources/testCert.pfx differ diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/project.json b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/project.json index ac30597ea..1c5d4d519 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/project.json +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/project.json @@ -7,6 +7,7 @@ "Microsoft.AspNetCore.Server.Kestrel": "1.0.0-*", "Microsoft.AspNetCore.Server.Kestrel.Https": "1.0.0-*", "Microsoft.AspNetCore.Testing": "1.0.0-*", + "Microsoft.Extensions.Logging.Console": "1.0.0-*", "Newtonsoft.Json": "9.0.1-beta1", "xunit": "2.1.0" }, @@ -37,7 +38,15 @@ } }, "buildOptions": { - "allowUnsafe": true + "allowUnsafe": true, + "copyToOutput": { + "include": "TestResources/testCert.pfx" + } }, - "testRunner": "xunit" + "testRunner": "xunit", + "publishOptions": { + "include": [ + "TestResources/testCert.pfx" + ] + } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerOptionsTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerOptionsTests.cs index ff03086b7..89825d4a5 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerOptionsTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerOptionsTests.cs @@ -11,6 +11,33 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { public class KestrelServerInformationTests { + [Fact] + public void MaxInputBufferLengthDefault() + { + Assert.Equal(1024 * 1024, (new KestrelServerOptions()).MaxInputBufferLength); + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + public void MaxInputBufferInvalid(int value) + { + Assert.Throws(() => + { + (new KestrelServerOptions()).MaxInputBufferLength = value; + }); + } + + [Theory] + [InlineData(null)] + [InlineData(1)] + public void MaxInputBufferValid(int? value) + { + var o = new KestrelServerOptions(); + o.MaxInputBufferLength = value; + Assert.Equal(value, o.MaxInputBufferLength); + } + [Fact] public void SetThreadCountUsingProcessorCount() { diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/SocketInputTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/SocketInputTests.cs index 15c7bc0c2..66dad80d7 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/SocketInputTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/SocketInputTests.cs @@ -3,16 +3,65 @@ using System; using System.Threading.Tasks; -using Microsoft.AspNetCore.Server.Kestrel; using Microsoft.AspNetCore.Server.Kestrel.Internal; using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; +using Microsoft.AspNetCore.Server.KestrelTests.TestHelpers; +using Moq; using Xunit; namespace Microsoft.AspNetCore.Server.KestrelTests { public class SocketInputTests { + public static readonly TheoryData> MockBufferLengthControlData = + new TheoryData>() { new Mock(), null }; + + [Theory] + [MemberData("MockBufferLengthControlData")] + public void IncomingDataCallsBufferLengthControlAdd(Mock mockBufferLengthControl) + { + using (var memory = new MemoryPool()) + using (var socketInput = new SocketInput(memory, null, mockBufferLengthControl?.Object)) + { + socketInput.IncomingData(new byte[5], 0, 5); + mockBufferLengthControl?.Verify(b => b.Add(5)); + } + } + + [Theory] + [MemberData("MockBufferLengthControlData")] + public void IncomingCompleteCallsBufferLengthControlAdd(Mock mockBufferLengthControl) + { + using (var memory = new MemoryPool()) + using (var socketInput = new SocketInput(memory, null, mockBufferLengthControl?.Object)) + { + socketInput.IncomingComplete(5, null); + mockBufferLengthControl?.Verify(b => b.Add(5)); + } + } + + [Theory] + [MemberData("MockBufferLengthControlData")] + public void ConsumingCompleteCallsBufferLengthControlSubtract(Mock mockBufferLengthControl) + { + using (var kestrelEngine = new KestrelEngine(new MockLibuv(), new TestServiceContext())) + { + kestrelEngine.Start(1); + + using (var memory = new MemoryPool()) + using (var socketInput = new SocketInput(memory, null, mockBufferLengthControl?.Object)) + { + socketInput.IncomingData(new byte[20], 0, 20); + + var iterator = socketInput.ConsumingStart(); + iterator.Skip(5); + socketInput.ConsumingComplete(iterator, iterator); + mockBufferLengthControl?.Verify(b => b.Subtract(5)); + } + } + } + [Fact] public async Task ConcurrentReadsFailGracefully() { diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/UvStreamHandleTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/UvStreamHandleTests.cs new file mode 100644 index 000000000..51dbce5b5 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/UvStreamHandleTests.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; +using Microsoft.AspNetCore.Server.Kestrel.Internal.Networking; +using Microsoft.AspNetCore.Server.KestrelTests.TestHelpers; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Server.KestrelTests +{ + public class UvStreamHandleTests + { + [Fact] + public void ReadStopIsIdempotent() + { + var mockKestrelTrace = Mock.Of(); + var mockUvLoopHandle = new Mock(mockKestrelTrace).Object; + mockUvLoopHandle.Init(new MockLibuv()); + + // Need to mock UvTcpHandle instead of UvStreamHandle, since the latter lacks an Init() method + var mockUvStreamHandle = new Mock(mockKestrelTrace).Object; + mockUvStreamHandle.Init(mockUvLoopHandle, null); + + mockUvStreamHandle.ReadStop(); + mockUvStreamHandle.ReadStop(); + } + } +} diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/project.json b/test/Microsoft.AspNetCore.Server.KestrelTests/project.json index 3f64d5ae4..dd95f5856 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/project.json +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/project.json @@ -21,7 +21,8 @@ "System.Net.Http": "4.1.0-*", "System.Net.Http.WinHttpHandler": "4.0.0-*", "System.Net.Sockets": "4.1.0-*", - "System.Runtime.Handles": "4.0.1-*" + "System.Runtime.Handles": "4.0.1-*", + "moq.netcore": "4.4.0-beta8" }, "imports": [ "dnxcore50", @@ -30,7 +31,8 @@ }, "net451": { "dependencies": { - "xunit.runner.console": "2.1.0" + "xunit.runner.console": "2.1.0", + "Moq": "4.2.1312.1622" }, "frameworkAssemblies": { "System.Net.Http": "4.0.0.0"