diff --git a/benchmarks/Kestrel.Performance/Http1ConnectionBenchmark.cs b/benchmarks/Kestrel.Performance/Http1ConnectionBenchmark.cs index b90221b4a7..0443a43b5e 100644 --- a/benchmarks/Kestrel.Performance/Http1ConnectionBenchmark.cs +++ b/benchmarks/Kestrel.Performance/Http1ConnectionBenchmark.cs @@ -43,7 +43,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance ConnectionFeatures = new FeatureCollection(), MemoryPool = memoryPool, TimeoutControl = new MockTimeoutControl(), - Application = pair.Application, Transport = pair.Transport }); diff --git a/benchmarks/Kestrel.Performance/Http1ConnectionParsingOverheadBenchmark.cs b/benchmarks/Kestrel.Performance/Http1ConnectionParsingOverheadBenchmark.cs index 3046c5f1e9..c7d3df7702 100644 --- a/benchmarks/Kestrel.Performance/Http1ConnectionParsingOverheadBenchmark.cs +++ b/benchmarks/Kestrel.Performance/Http1ConnectionParsingOverheadBenchmark.cs @@ -39,7 +39,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance ConnectionFeatures = new FeatureCollection(), MemoryPool = memoryPool, TimeoutControl = new MockTimeoutControl(), - Application = pair.Application, Transport = pair.Transport }); diff --git a/benchmarks/Kestrel.Performance/Http1WritingBenchmark.cs b/benchmarks/Kestrel.Performance/Http1WritingBenchmark.cs index 891d94ab0a..7052bf3a41 100644 --- a/benchmarks/Kestrel.Performance/Http1WritingBenchmark.cs +++ b/benchmarks/Kestrel.Performance/Http1WritingBenchmark.cs @@ -113,7 +113,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance ServiceContext = serviceContext, ConnectionFeatures = new FeatureCollection(), MemoryPool = _memoryPool, - Application = pair.Application, Transport = pair.Transport }); diff --git a/benchmarks/Kestrel.Performance/HttpProtocolFeatureCollection.cs b/benchmarks/Kestrel.Performance/HttpProtocolFeatureCollection.cs index 24b2c6c4b6..6498d49842 100644 --- a/benchmarks/Kestrel.Performance/HttpProtocolFeatureCollection.cs +++ b/benchmarks/Kestrel.Performance/HttpProtocolFeatureCollection.cs @@ -96,7 +96,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance ServiceContext = serviceContext, ConnectionFeatures = new FeatureCollection(), MemoryPool = memoryPool, - Application = pair.Application, Transport = pair.Transport }); diff --git a/benchmarks/Kestrel.Performance/RequestParsingBenchmark.cs b/benchmarks/Kestrel.Performance/RequestParsingBenchmark.cs index 2d881588d2..c03f8c7284 100644 --- a/benchmarks/Kestrel.Performance/RequestParsingBenchmark.cs +++ b/benchmarks/Kestrel.Performance/RequestParsingBenchmark.cs @@ -41,7 +41,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance ServiceContext = serviceContext, ConnectionFeatures = new FeatureCollection(), MemoryPool = _memoryPool, - Application = pair.Application, Transport = pair.Transport, TimeoutControl = new MockTimeoutControl() }); diff --git a/benchmarks/Kestrel.Performance/ResponseHeaderCollectionBenchmark.cs b/benchmarks/Kestrel.Performance/ResponseHeaderCollectionBenchmark.cs index e04f007222..abd19bc359 100644 --- a/benchmarks/Kestrel.Performance/ResponseHeaderCollectionBenchmark.cs +++ b/benchmarks/Kestrel.Performance/ResponseHeaderCollectionBenchmark.cs @@ -188,7 +188,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance ServiceContext = serviceContext, ConnectionFeatures = new FeatureCollection(), MemoryPool = memoryPool, - Application = pair.Application, Transport = pair.Transport }); diff --git a/benchmarks/Kestrel.Performance/ResponseHeadersWritingBenchmark.cs b/benchmarks/Kestrel.Performance/ResponseHeadersWritingBenchmark.cs index 2134a7d5b4..6eea85d72d 100644 --- a/benchmarks/Kestrel.Performance/ResponseHeadersWritingBenchmark.cs +++ b/benchmarks/Kestrel.Performance/ResponseHeadersWritingBenchmark.cs @@ -133,7 +133,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance ConnectionFeatures = new FeatureCollection(), MemoryPool = _memoryPool, TimeoutControl = new MockTimeoutControl(), - Application = _pair.Application, Transport = _pair.Transport }); diff --git a/src/Connections.Abstractions/ConnectionContext.cs b/src/Connections.Abstractions/ConnectionContext.cs index bb942f2627..381780b32e 100644 --- a/src/Connections.Abstractions/ConnectionContext.cs +++ b/src/Connections.Abstractions/ConnectionContext.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO.Pipelines; +using System.Threading; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http.Features; @@ -22,7 +23,7 @@ namespace Microsoft.AspNetCore.Connections { // We expect this to be overridden, but this helps maintain back compat // with implementations of ConnectionContext that predate the addition of - // ConnectioContext.Abort() + // ConnectionContext.Abort() Features.Get()?.Abort(); } diff --git a/src/Kestrel.Core/Adapter/Internal/AdaptedPipeline.cs b/src/Kestrel.Core/Adapter/Internal/AdaptedPipeline.cs index 2f44503c0e..eb65aac253 100644 --- a/src/Kestrel.Core/Adapter/Internal/AdaptedPipeline.cs +++ b/src/Kestrel.Core/Adapter/Internal/AdaptedPipeline.cs @@ -168,9 +168,5 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal _transport.Input.Complete(); } } - - public void Dispose() - { - } } } diff --git a/src/Kestrel.Core/CoreStrings.resx b/src/Kestrel.Core/CoreStrings.resx index 477d64597a..f119165500 100644 --- a/src/Kestrel.Core/CoreStrings.resx +++ b/src/Kestrel.Core/CoreStrings.resx @@ -557,13 +557,19 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l The request :scheme header '{requestScheme}' does not match the transport scheme '{transportScheme}'. - - An error occured after the response headers were sent, a reset is being sent. - Less data received than specified in the Content-Length header. More data received than specified in the Content-Length header. + + An error occured after the response headers were sent, a reset is being sent. + + + A new stream was refused because this connection has reached its stream limit. + + + A value greater than zero is required. + \ No newline at end of file diff --git a/src/Kestrel.Core/Http2Limits.cs b/src/Kestrel.Core/Http2Limits.cs new file mode 100644 index 0000000000..34a4ef36f1 --- /dev/null +++ b/src/Kestrel.Core/Http2Limits.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core +{ + /// + /// Limits only applicable to HTTP/2 connections. + /// + public class Http2Limits + { + private int _maxStreamsPerConnection = 100; + + /// + /// Limits the number of concurrent request streams per HTTP/2 connection. Excess streams will be refused. + /// + /// Defaults to 100 + /// + /// + public int MaxStreamsPerConnection + { + get => _maxStreamsPerConnection; + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, CoreStrings.GreaterThanZeroRequired); + } + _maxStreamsPerConnection = value; + } + } + } +} diff --git a/src/Kestrel.Core/Internal/Http/Http1Connection.cs b/src/Kestrel.Core/Internal/Http/Http1Connection.cs index 68c6187c43..5a4e1d68bb 100644 --- a/src/Kestrel.Core/Internal/Http/Http1Connection.cs +++ b/src/Kestrel.Core/Internal/Http/Http1Connection.cs @@ -6,13 +6,11 @@ using System.Buffers; using System.Diagnostics; using System.Globalization; using System.IO.Pipelines; -using System.Runtime.InteropServices; -using System.Text; +using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Connections.Abstractions; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http @@ -28,6 +26,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http protected readonly long _keepAliveTicks; private readonly long _requestHeadersTimeoutTicks; + private int _requestAborted; private volatile bool _requestTimedOut; private uint _requestCount; @@ -61,6 +60,31 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http public override bool IsUpgradableRequest => _upgradeAvailable; + /// + /// Immediately kill the connection and poison the request body stream with an error. + /// + public void Abort(ConnectionAbortedException abortReason) + { + if (Interlocked.Exchange(ref _requestAborted, 1) != 0) + { + return; + } + + // Abort output prior to calling OnIOCompleted() to give the transport the chance to complete the input + // with the correct error and message. + Output.Abort(abortReason); + + OnInputOrOutputCompleted(); + + PoisonRequestBodyStream(abortReason); + } + + protected override void ApplicationAbort() + { + Log.ApplicationAbortedConnection(ConnectionId, TraceIdentifier); + Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByApplication)); + } + /// /// Stops the request processing loop between requests. /// Called on all active connections when the server wants to initiate a shutdown diff --git a/src/Kestrel.Core/Internal/Http/Http1ConnectionContext.cs b/src/Kestrel.Core/Internal/Http/Http1ConnectionContext.cs index 28b5f95ed4..9ad2e9e9b3 100644 --- a/src/Kestrel.Core/Internal/Http/Http1ConnectionContext.cs +++ b/src/Kestrel.Core/Internal/Http/Http1ConnectionContext.cs @@ -21,6 +21,5 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http public IPEndPoint LocalEndPoint { get; set; } public ITimeoutControl TimeoutControl { get; set; } public IDuplexPipe Transport { get; set; } - public IDuplexPipe Application { get; set; } } } diff --git a/src/Kestrel.Core/Internal/Http/Http1MessageBody.cs b/src/Kestrel.Core/Internal/Http/Http1MessageBody.cs index d2edbff7f8..6e83307de2 100644 --- a/src/Kestrel.Core/Internal/Http/Http1MessageBody.cs +++ b/src/Kestrel.Core/Internal/Http/Http1MessageBody.cs @@ -399,7 +399,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private const int MaxChunkPrefixBytes = 10; private long _inputLength; - private long _consumedBytes; private Mode _mode = Mode.Prefix; @@ -490,16 +489,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http return _mode == Mode.Complete; } - private void AddAndCheckConsumedBytes(long consumedBytes) - { - _consumedBytes += consumedBytes; - - if (_consumedBytes > _context.MaxRequestBodySize) - { - BadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge); - } - } - private void ParseChunkedPrefix(ReadOnlySequence buffer, out SequencePosition consumed, out SequencePosition examined) { consumed = buffer.Start; diff --git a/src/Kestrel.Core/Internal/Http/HttpProtocol.FeatureCollection.cs b/src/Kestrel.Core/Internal/Http/HttpProtocol.FeatureCollection.cs index 96ff0ab391..7f3b047d70 100644 --- a/src/Kestrel.Core/Internal/Http/HttpProtocol.FeatureCollection.cs +++ b/src/Kestrel.Core/Internal/Http/HttpProtocol.FeatureCollection.cs @@ -6,7 +6,6 @@ using System.IO; using System.Net; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; @@ -230,10 +229,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http ApplicationAbort(); } - protected virtual void ApplicationAbort() - { - Log.ApplicationAbortedConnection(ConnectionId, TraceIdentifier); - Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByApplication)); - } + protected abstract void ApplicationAbort(); } } diff --git a/src/Kestrel.Core/Internal/Http/HttpProtocol.cs b/src/Kestrel.Core/Internal/Http/HttpProtocol.cs index b51d3ce6b8..f272f0ba29 100644 --- a/src/Kestrel.Core/Internal/Http/HttpProtocol.cs +++ b/src/Kestrel.Core/Internal/Http/HttpProtocol.cs @@ -18,7 +18,6 @@ using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; @@ -42,7 +41,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private Stack, object>> _onStarting; private Stack, object>> _onCompleted; - private int _requestAborted; private volatile int _ioCompleted; private CancellationTokenSource _abortedCts; private CancellationToken? _manuallySetRequestAbortToken; @@ -385,6 +383,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { } + protected virtual void OnErrorAfterResponseStarted() + { + } + protected virtual bool BeginRead(out ValueTask awaitable) { awaitable = default; @@ -425,23 +427,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http ServiceContext.Scheduler.Schedule(state => ((HttpProtocol)state).CancelRequestAbortedToken(), this); } - /// - /// Immediately kill the connection and poison the request and response streams with an error if there is one. - /// - public virtual void Abort(ConnectionAbortedException abortReason) + protected void PoisonRequestBodyStream(Exception abortReason) { - if (Interlocked.Exchange(ref _requestAborted, 1) != 0) - { - return; - } - _streams?.Abort(abortReason); - - // Abort output prior to calling OnIOCompleted() to give the transport the chance to - // complete the input with the correct error and message. - Output.Abort(abortReason); - - OnInputOrOutputCompleted(); } public void OnHeader(Span name, Span value) @@ -1032,7 +1020,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (HasResponseStarted) { - ErrorAfterResponseStarted(); + // We can no longer change the response, so we simply close the connection. + _keepAlive = false; + OnErrorAfterResponseStarted(); return Task.CompletedTask; } @@ -1057,12 +1047,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http return WriteSuffix(); } - protected virtual void ErrorAfterResponseStarted() - { - // We can no longer change the response, so we simply close the connection. - _keepAlive = false; - } - [MethodImpl(MethodImplOptions.NoInlining)] private async Task ProduceEndAwaited() { diff --git a/src/Kestrel.Core/Internal/Http/MessageBody.cs b/src/Kestrel.Core/Internal/Http/MessageBody.cs index 0cbf0e0ea0..ed0bab633b 100644 --- a/src/Kestrel.Core/Internal/Http/MessageBody.cs +++ b/src/Kestrel.Core/Internal/Http/MessageBody.cs @@ -18,6 +18,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private readonly HttpProtocol _context; private bool _send100Continue = true; + private long _consumedBytes; protected MessageBody(HttpProtocol context) { @@ -168,6 +169,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { } + protected void AddAndCheckConsumedBytes(long consumedBytes) + { + _consumedBytes += consumedBytes; + + if (_consumedBytes > _context.MaxRequestBodySize) + { + BadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge); + } + } + private class ForZeroContentLength : MessageBody { public ForZeroContentLength(bool keepAlive) diff --git a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs index 0ef973b98c..b447fffdf3 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs @@ -86,8 +86,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 public Http2Connection(Http2ConnectionContext context) { _context = context; - _frameWriter = new Http2FrameWriter(context.Transport.Output, context.Application.Input, _outputFlowControl, this, context.ConnectionId, context.ServiceContext.Log); + _frameWriter = new Http2FrameWriter(context.Transport.Output, context.ConnectionContext, _outputFlowControl, this, context.ConnectionId, context.ServiceContext.Log); _hpackDecoder = new HPackDecoder((int)_serverSettings.HeaderTableSize); + _serverSettings.MaxConcurrentStreams = (uint)context.ServiceContext.ServerOptions.Limits.Http2.MaxStreamsPerConnection; } public string ConnectionId => _context.ConnectionId; @@ -98,6 +99,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 public IFeatureCollection ConnectionFeatures => _context.ConnectionFeatures; + internal Http2PeerSettings ServerSettings => _serverSettings; + public void OnInputOrOutputCompleted() { lock (_stateLock) @@ -172,7 +175,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 if (_state != Http2ConnectionState.Closed) { - await _frameWriter.WriteSettingsAsync(_serverSettings); + await _frameWriter.WriteSettingsAsync(_serverSettings.GetNonProtocolDefaults()); } while (_state != Http2ConnectionState.Closed) @@ -201,7 +204,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 catch (Http2StreamErrorException ex) { Log.Http2StreamError(ConnectionId, ex); - AbortStream(_incomingFrame.StreamId, new ConnectionAbortedException(ex.Message, ex)); + AbortStream(_incomingFrame.StreamId, new IOException(ex.Message, ex)); await _frameWriter.WriteRstStreamAsync(ex.StreamId, ex.ErrorCode); } finally @@ -269,7 +272,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 foreach (var stream in _streams.Values) { - stream.Abort(connectionError); + stream.Abort(new IOException(CoreStrings.Http2StreamAborted, connectionError)); } await _streamsCompleted.Task; @@ -583,7 +586,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } ThrowIfIncomingFrameSentToIdleStream(); - AbortStream(_incomingFrame.StreamId, new ConnectionAbortedException(CoreStrings.Http2StreamResetByClient)); + AbortStream(_incomingFrame.StreamId, new IOException(CoreStrings.Http2StreamResetByClient)); return Task.CompletedTask; } @@ -617,10 +620,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 try { - // ParseFrame will not parse an InitialWindowSize > int.MaxValue. + // int.MaxValue is the largest allowed windows size. var previousInitialWindowSize = (int)_clientSettings.InitialWindowSize; - _clientSettings.ParseFrame(_incomingFrame); + _clientSettings.Update(_incomingFrame.GetSettings()); var ackTask = _frameWriter.WriteSettingsAckAsync(); // Ack before we update the windows, they could send data immediately. @@ -838,6 +841,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 throw new Http2StreamErrorException(_currentHeadersStream.StreamId, CoreStrings.Http2ErrorMissingMandatoryPseudoHeaderFields, Http2ErrorCode.PROTOCOL_ERROR); } + if (_streams.Count >= _serverSettings.MaxConcurrentStreams) + { + throw new Http2StreamErrorException(_currentHeadersStream.StreamId, CoreStrings.Http2ErrorMaxStreams, Http2ErrorCode.REFUSED_STREAM); + } + // This must be initialized before we offload the request or else we may start processing request body frames without it. _currentHeadersStream.InputRemaining = _currentHeadersStream.RequestHeaders.ContentLength; @@ -885,7 +893,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } - private void AbortStream(int streamId, ConnectionAbortedException error) + private void AbortStream(int streamId, IOException error) { if (_streams.TryGetValue(streamId, out var stream)) { diff --git a/src/Kestrel.Core/Internal/Http2/Http2ConnectionContext.cs b/src/Kestrel.Core/Internal/Http2/Http2ConnectionContext.cs index ae9ceb3b70..04a7ab8ca6 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2ConnectionContext.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2ConnectionContext.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.IO.Pipelines; using System.Net; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Features; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 @@ -11,13 +12,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 public class Http2ConnectionContext { public string ConnectionId { get; set; } + public ConnectionContext ConnectionContext { get; set; } public ServiceContext ServiceContext { get; set; } public IFeatureCollection ConnectionFeatures { get; set; } public MemoryPool MemoryPool { get; set; } public IPEndPoint LocalEndPoint { get; set; } public IPEndPoint RemoteEndPoint { get; set; } - public IDuplexPipe Transport { get; set; } - public IDuplexPipe Application { get; set; } } } diff --git a/src/Kestrel.Core/Internal/Http2/Http2Frame.Settings.cs b/src/Kestrel.Core/Internal/Http2/Http2Frame.Settings.cs index 04cc78b209..c3da784e01 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.Settings.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.Settings.cs @@ -1,42 +1,67 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; +using System.Buffers.Binary; +using System.Collections.Generic; using System.Linq; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { public partial class Http2Frame { + private const int SettingSize = 6; // 2 bytes for the id, 4 bytes for the value. + public Http2SettingsFrameFlags SettingsFlags { get => (Http2SettingsFrameFlags)Flags; set => Flags = (byte)value; } - public void PrepareSettings(Http2SettingsFrameFlags flags, Http2PeerSettings settings = null) + public int SettingsCount { - var settingCount = settings?.Count() ?? 0; + get => Length / SettingSize; + set => Length = value * SettingSize; + } - Length = 6 * settingCount; + public IList GetSettings() + { + var settings = new Http2PeerSetting[SettingsCount]; + for (int i = 0; i < settings.Length; i++) + { + settings[i] = GetSetting(i); + } + return settings; + } + + private Http2PeerSetting GetSetting(int index) + { + var offset = index * SettingSize; + var payload = Payload.Slice(offset); + var id = (Http2SettingsParameter)BinaryPrimitives.ReadUInt16BigEndian(payload); + var value = BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(2)); + + return new Http2PeerSetting(id, value); + } + + public void PrepareSettings(Http2SettingsFrameFlags flags, IList settings = null) + { + var settingCount = settings?.Count ?? 0; + SettingsCount = settingCount; Type = Http2FrameType.SETTINGS; SettingsFlags = flags; StreamId = 0; - - if (settings != null) + for (int i = 0; i < settingCount; i++) { - Span payload = Payload; - foreach (var setting in settings) - { - payload[0] = (byte)((ushort)setting.Parameter >> 8); - payload[1] = (byte)(ushort)setting.Parameter; - payload[2] = (byte)(setting.Value >> 24); - payload[3] = (byte)(setting.Value >> 16); - payload[4] = (byte)(setting.Value >> 8); - payload[5] = (byte)setting.Value; - payload = payload.Slice(6); - } + SetSetting(i, settings[i]); } } + + private void SetSetting(int index, Http2PeerSetting setting) + { + var offset = index * SettingSize; + var payload = Payload.Slice(offset); + BinaryPrimitives.WriteUInt16BigEndian(payload, (ushort)setting.Parameter); + BinaryPrimitives.WriteUInt32BigEndian(payload.Slice(2), setting.Value); + } } } diff --git a/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs b/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs index a0a679d15b..0a9b62e30e 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs @@ -26,7 +26,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 private readonly object _writeLock = new object(); private readonly HPackEncoder _hpackEncoder = new HPackEncoder(); private readonly PipeWriter _outputWriter; - private readonly PipeReader _outputReader; + private bool _aborted; + private readonly ConnectionContext _connectionContext; private readonly OutputFlowControl _connectionOutputFlowControl; private readonly string _connectionId; private readonly IKestrelTrace _log; @@ -36,15 +37,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 public Http2FrameWriter( PipeWriter outputPipeWriter, - PipeReader outputPipeReader, + ConnectionContext connectionContext, OutputFlowControl connectionOutputFlowControl, ITimeoutControl timeoutControl, string connectionId, IKestrelTrace log) { _outputWriter = outputPipeWriter; - _outputReader = outputPipeReader; - + _connectionContext = connectionContext; _connectionOutputFlowControl = connectionOutputFlowControl; _connectionId = connectionId; _log = log; @@ -66,11 +66,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } - public void Abort(ConnectionAbortedException ex) + public void Abort(ConnectionAbortedException error) { - // TODO: Really abort the connection using the ConnectionContex like Http1OutputProducer. - _outputReader.CancelPendingRead(); - Complete(); + lock (_writeLock) + { + if (_aborted) + { + return; + } + + _aborted = true; + _connectionContext.Abort(error); + + Complete(); + } } public Task FlushAsync(IHttpOutputProducer outputProducer, CancellationToken cancellationToken) @@ -239,7 +248,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } // This awaitable releases continuations in FIFO order when the window updates. - // It should be very rare for a continuation to run without any availability. + // It should be very rare for a continuation to run without any availability. if (availabilityAwaitable != null) { await availabilityAwaitable; @@ -270,12 +279,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } - public Task WriteSettingsAsync(Http2PeerSettings settings) + public Task WriteSettingsAsync(IList settings) { lock (_writeLock) { - // TODO: actually send settings - _outgoingFrame.PrepareSettings(Http2SettingsFrameFlags.NONE); + _outgoingFrame.PrepareSettings(Http2SettingsFrameFlags.NONE, settings); return WriteFrameUnsynchronizedAsync(); } } diff --git a/src/Kestrel.Core/Internal/Http2/Http2MessageBody.cs b/src/Kestrel.Core/Internal/Http2/Http2MessageBody.cs index 2ac43a2974..f835ffa570 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2MessageBody.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2MessageBody.cs @@ -16,6 +16,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _context = context; } + protected override void OnReadStarting() + { + // Note ContentLength or MaxRequestBodySize may be null + if (_context.RequestHeaders.ContentLength > _context.MaxRequestBodySize) + { + BadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge); + } + } + protected override void OnReadStarted() { // Produce 100-continue if no request body data for the stream has arrived yet. @@ -28,6 +37,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 protected override void OnDataRead(int bytesRead) { _context.OnDataRead(bytesRead); + AddAndCheckConsumedBytes(bytesRead); } protected override Task OnConsumeAsync() => Task.CompletedTask; diff --git a/src/Kestrel.Core/Internal/Http2/Http2PeerSettings.cs b/src/Kestrel.Core/Internal/Http2/Http2PeerSettings.cs index fcf78c4b42..eeb13bb808 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2PeerSettings.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2PeerSettings.cs @@ -1,13 +1,13 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Collections; using System.Collections.Generic; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { - public class Http2PeerSettings : IEnumerable + public class Http2PeerSettings { + // Note these are protocol defaults, not Kestrel defaults. public const uint DefaultHeaderTableSize = 4096; public const bool DefaultEnablePush = true; public const uint DefaultMaxConcurrentStreams = uint.MaxValue; @@ -28,20 +28,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 public uint MaxHeaderListSize { get; set; } = DefaultMaxHeaderListSize; - public void ParseFrame(Http2Frame frame) + // TODO: Return the diff so we can react + public void Update(IList settings) { - var settingsCount = frame.Length / 6; - - for (var i = 0; i < settingsCount; i++) + foreach (var setting in settings) { - var offset = i * 6; - var id = (Http2SettingsParameter)((frame.Payload[offset] << 8) | frame.Payload[offset + 1]); - var value = (uint)((frame.Payload[offset + 2] << 24) - | (frame.Payload[offset + 3] << 16) - | (frame.Payload[offset + 4] << 8) - | frame.Payload[offset + 5]); + var value = setting.Value; - switch (id) + switch (setting.Parameter) { case Http2SettingsParameter.SETTINGS_HEADER_TABLE_SIZE: HeaderTableSize = value; @@ -91,16 +85,42 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } - public IEnumerator GetEnumerator() + // Gets the settings that are different from the protocol defaults (as opposed to the server defaults). + internal IList GetNonProtocolDefaults() { - yield return new Http2PeerSetting(Http2SettingsParameter.SETTINGS_HEADER_TABLE_SIZE, HeaderTableSize); - yield return new Http2PeerSetting(Http2SettingsParameter.SETTINGS_ENABLE_PUSH, EnablePush ? 1u : 0); - yield return new Http2PeerSetting(Http2SettingsParameter.SETTINGS_MAX_CONCURRENT_STREAMS, MaxConcurrentStreams); - yield return new Http2PeerSetting(Http2SettingsParameter.SETTINGS_INITIAL_WINDOW_SIZE, InitialWindowSize); - yield return new Http2PeerSetting(Http2SettingsParameter.SETTINGS_MAX_FRAME_SIZE, MaxFrameSize); - yield return new Http2PeerSetting(Http2SettingsParameter.SETTINGS_MAX_HEADER_LIST_SIZE, MaxHeaderListSize); - } + var list = new List(1); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + if (HeaderTableSize != DefaultHeaderTableSize) + { + list.Add(new Http2PeerSetting(Http2SettingsParameter.SETTINGS_HEADER_TABLE_SIZE, HeaderTableSize)); + } + + if (EnablePush != DefaultEnablePush) + { + list.Add(new Http2PeerSetting(Http2SettingsParameter.SETTINGS_ENABLE_PUSH, EnablePush ? 1u : 0)); + } + + if (MaxConcurrentStreams != DefaultMaxConcurrentStreams) + { + list.Add(new Http2PeerSetting(Http2SettingsParameter.SETTINGS_MAX_CONCURRENT_STREAMS, MaxConcurrentStreams)); + } + + if (InitialWindowSize != DefaultInitialWindowSize) + { + list.Add(new Http2PeerSetting(Http2SettingsParameter.SETTINGS_INITIAL_WINDOW_SIZE, InitialWindowSize)); + } + + if (MaxFrameSize != DefaultMaxFrameSize) + { + list.Add(new Http2PeerSetting(Http2SettingsParameter.SETTINGS_MAX_FRAME_SIZE, MaxFrameSize)); + } + + if (MaxHeaderListSize != DefaultMaxHeaderListSize) + { + list.Add(new Http2PeerSetting(Http2SettingsParameter.SETTINGS_MAX_HEADER_LIST_SIZE, MaxHeaderListSize)); + } + + return list; + } } } diff --git a/src/Kestrel.Core/Internal/Http2/Http2Stream.cs b/src/Kestrel.Core/Internal/Http2/Http2Stream.cs index 464075cbd2..940fda25a6 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Stream.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Stream.cs @@ -352,7 +352,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 return _context.FrameWriter.TryUpdateStreamWindow(_outputFlowControl, bytes); } - public override void Abort(ConnectionAbortedException abortReason) + public void Abort(IOException abortReason) { if (!TryApplyCompletionFlag(StreamCompletionFlags.Aborted)) { @@ -362,10 +362,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 AbortCore(abortReason); } - protected override void ErrorAfterResponseStarted() + protected override void OnErrorAfterResponseStarted() { // We can no longer change the response, send a Reset instead. - base.ErrorAfterResponseStarted(); var abortReason = new ConnectionAbortedException(CoreStrings.Http2StreamErrorAfterHeaders); ResetAndAbort(abortReason, Http2ErrorCode.INTERNAL_ERROR); } @@ -391,12 +390,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 AbortCore(abortReason); } - private void AbortCore(ConnectionAbortedException abortReason) + private void AbortCore(Exception abortReason) { - base.Abort(abortReason); + // Call OnIOCompleted() which closes the output prior to poisoning the request body stream or pipe to + // ensure that an app that completes early due to the abort doesn't result in header frames being sent. + OnInputOrOutputCompleted(); // Unblock the request body. - RequestBodyPipe.Writer.Complete(new IOException(CoreStrings.Http2StreamAborted, abortReason)); + PoisonRequestBodyStream(abortReason); + RequestBodyPipe.Writer.Complete(abortReason); _inputFlowControl.Abort(); } @@ -420,7 +422,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 var lastCompletionState = _completionState; _completionState |= completionState; - if (ShoulStopTrackingStream(_completionState) && !ShoulStopTrackingStream(lastCompletionState)) + if (ShouldStopTrackingStream(_completionState) && !ShouldStopTrackingStream(lastCompletionState)) { _context.StreamLifetimeHandler.OnStreamCompleted(StreamId); } @@ -429,7 +431,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } - private static bool ShoulStopTrackingStream(StreamCompletionFlags completionState) + private static bool ShouldStopTrackingStream(StreamCompletionFlags completionState) { // This could be a single condition, but I think it reads better as two if's. if ((completionState & StreamCompletionFlags.RequestProcessingEnded) == StreamCompletionFlags.RequestProcessingEnded) diff --git a/src/Kestrel.Core/Internal/HttpConnection.cs b/src/Kestrel.Core/Internal/HttpConnection.cs index 4553f08426..28224a6ac0 100644 --- a/src/Kestrel.Core/Internal/HttpConnection.cs +++ b/src/Kestrel.Core/Internal/HttpConnection.cs @@ -112,7 +112,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal // _adaptedTransport must be set prior to adding the connection to the manager in order // to allow the connection to be aported prior to protocol selection. _adaptedTransport = _context.Transport; - var application = _context.Application; if (_context.ConnectionAdapters.Count > 0) @@ -147,14 +146,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal { case HttpProtocols.Http1: // _http1Connection must be initialized before adding the connection to the connection manager - requestProcessor = _http1Connection = CreateHttp1Connection(_adaptedTransport, application); + requestProcessor = _http1Connection = CreateHttp1Connection(_adaptedTransport); _protocolSelectionState = ProtocolSelectionState.Selected; break; case HttpProtocols.Http2: // _http2Connection must be initialized before yielding control to the transport thread, // to prevent a race condition where _http2Connection.Abort() is called just as // _http2Connection is about to be initialized. - requestProcessor = CreateHttp2Connection(_adaptedTransport, application); + requestProcessor = CreateHttp2Connection(_adaptedTransport); _protocolSelectionState = ProtocolSelectionState.Selected; break; case HttpProtocols.None: @@ -199,13 +198,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal } // For testing only - internal void Initialize(IDuplexPipe transport, IDuplexPipe application) + internal void Initialize(IDuplexPipe transport) { - _requestProcessor = _http1Connection = CreateHttp1Connection(transport, application); + _requestProcessor = _http1Connection = CreateHttp1Connection(transport); _protocolSelectionState = ProtocolSelectionState.Selected; } - private Http1Connection CreateHttp1Connection(IDuplexPipe transport, IDuplexPipe application) + private Http1Connection CreateHttp1Connection(IDuplexPipe transport) { return new Http1Connection(new Http1ConnectionContext { @@ -217,22 +216,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal ServiceContext = _context.ServiceContext, ConnectionContext = _context.ConnectionContext, TimeoutControl = this, - Transport = transport, - Application = application + Transport = transport }); } - private Http2Connection CreateHttp2Connection(IDuplexPipe transport, IDuplexPipe application) + private Http2Connection CreateHttp2Connection(IDuplexPipe transport) { return new Http2Connection(new Http2ConnectionContext { ConnectionId = _context.ConnectionId, + ConnectionContext = _context.ConnectionContext, ServiceContext = _context.ServiceContext, ConnectionFeatures = _context.ConnectionFeatures, MemoryPool = MemoryPool, LocalEndPoint = LocalEndPoint, RemoteEndPoint = RemoteEndPoint, - Application = application, Transport = transport }); } diff --git a/src/Kestrel.Core/Internal/HttpConnectionContext.cs b/src/Kestrel.Core/Internal/HttpConnectionContext.cs index 161ca647a7..da01a5a7b0 100644 --- a/src/Kestrel.Core/Internal/HttpConnectionContext.cs +++ b/src/Kestrel.Core/Internal/HttpConnectionContext.cs @@ -24,6 +24,5 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal public IPEndPoint LocalEndPoint { get; set; } public IPEndPoint RemoteEndPoint { get; set; } public IDuplexPipe Transport { get; set; } - public IDuplexPipe Application { get; set; } } } diff --git a/src/Kestrel.Core/Internal/HttpConnectionMiddleware.cs b/src/Kestrel.Core/Internal/HttpConnectionMiddleware.cs index 6b15c06705..5d05220811 100644 --- a/src/Kestrel.Core/Internal/HttpConnectionMiddleware.cs +++ b/src/Kestrel.Core/Internal/HttpConnectionMiddleware.cs @@ -37,7 +37,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal { // We need the transport feature so that we can cancel the output reader that the transport is using // This is a bit of a hack but it preserves the existing semantics - var applicationFeature = connectionContext.Features.Get(); var memoryPoolFeature = connectionContext.Features.Get(); var httpConnectionId = Interlocked.Increment(ref _lastHttpConnectionId); @@ -52,8 +51,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal ConnectionFeatures = connectionContext.Features, MemoryPool = memoryPoolFeature.MemoryPool, ConnectionAdapters = _connectionAdapters, - Transport = connectionContext.Transport, - Application = applicationFeature.Application + Transport = connectionContext.Transport }; var connectionFeature = connectionContext.Features.Get(); diff --git a/src/Kestrel.Core/KestrelServerLimits.cs b/src/Kestrel.Core/KestrelServerLimits.cs index f2f8e773ac..e7ddc479df 100644 --- a/src/Kestrel.Core/KestrelServerLimits.cs +++ b/src/Kestrel.Core/KestrelServerLimits.cs @@ -251,6 +251,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core } } + /// + /// Limits only applicable to HTTP/2 connections. + /// + public Http2Limits Http2 { get; } = new Http2Limits(); + /// /// Gets or sets the request body minimum data rate in bytes/second. /// Setting this property to null indicates no minimum data rate should be enforced. diff --git a/src/Kestrel.Core/Properties/AssemblyInfo.cs b/src/Kestrel.Core/Properties/AssemblyInfo.cs index 9e4800deed..c9518b4b60 100644 --- a/src/Kestrel.Core/Properties/AssemblyInfo.cs +++ b/src/Kestrel.Core/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("InMemory.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Sockets.BindTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Libuv.BindTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] -[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.Kestrel.Core.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Core.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Kestrel.Performance, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("PlatformBenchmarks, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs index c5de786f55..2d24e413e8 100644 --- a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs +++ b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs @@ -2058,20 +2058,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core internal static string FormatHttp2StreamErrorSchemeMismatch(object requestScheme, object transportScheme) => string.Format(CultureInfo.CurrentCulture, GetString("Http2StreamErrorSchemeMismatch", "requestScheme", "transportScheme"), requestScheme, transportScheme); - /// - /// An error occured after the response headers were sent, a reset is being sent. - /// - internal static string Http2StreamErrorAfterHeaders - { - get => GetString("Http2StreamErrorAfterHeaders"); - } - - /// - /// An error occured after the response headers were sent, a reset is being sent. - /// - internal static string FormatHttp2StreamErrorAfterHeaders() - => GetString("Http2StreamErrorAfterHeaders"); - /// /// Less data received than specified in the Content-Length header. /// @@ -2100,6 +2086,48 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core internal static string FormatHttp2StreamErrorMoreDataThanLength() => GetString("Http2StreamErrorMoreDataThanLength"); + /// + /// An error occured after the response headers were sent, a reset is being sent. + /// + internal static string Http2StreamErrorAfterHeaders + { + get => GetString("Http2StreamErrorAfterHeaders"); + } + + /// + /// An error occured after the response headers were sent, a reset is being sent. + /// + internal static string FormatHttp2StreamErrorAfterHeaders() + => GetString("Http2StreamErrorAfterHeaders"); + + /// + /// A new stream was refused because this connection has reached its stream limit. + /// + internal static string Http2ErrorMaxStreams + { + get => GetString("Http2ErrorMaxStreams"); + } + + /// + /// A new stream was refused because this connection has reached its stream limit. + /// + internal static string FormatHttp2ErrorMaxStreams() + => GetString("Http2ErrorMaxStreams"); + + /// + /// A value greater than zero is required. + /// + internal static string GreaterThanZeroRequired + { + get => GetString("GreaterThanZeroRequired"); + } + + /// + /// A value greater than zero is required. + /// + internal static string FormatGreaterThanZeroRequired() + => GetString("GreaterThanZeroRequired"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Kestrel.Transport.Abstractions/Properties/AssemblyInfo.cs b/src/Kestrel.Transport.Abstractions/Properties/AssemblyInfo.cs index 7056daa197..76b33e146d 100644 --- a/src/Kestrel.Transport.Abstractions/Properties/AssemblyInfo.cs +++ b/src/Kestrel.Transport.Abstractions/Properties/AssemblyInfo.cs @@ -3,7 +3,9 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.Kestrel.Core.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Core.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.Kestrel.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Sockets.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Libuv.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("InMemory.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/test/Kestrel.Core.Tests/Http1ConnectionTests.cs b/test/Kestrel.Core.Tests/Http1ConnectionTests.cs index de762a0a7a..19e17d4dc3 100644 --- a/test/Kestrel.Core.Tests/Http1ConnectionTests.cs +++ b/test/Kestrel.Core.Tests/Http1ConnectionTests.cs @@ -63,7 +63,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests ConnectionFeatures = connectionFeatures, MemoryPool = _pipelineFactory, TimeoutControl = _timeoutControl.Object, - Application = pair.Application, Transport = pair.Transport }; diff --git a/test/Kestrel.Core.Tests/HttpConnectionTests.cs b/test/Kestrel.Core.Tests/HttpConnectionTests.cs index c8dbf28ba0..2c78c922c7 100644 --- a/test/Kestrel.Core.Tests/HttpConnectionTests.cs +++ b/test/Kestrel.Core.Tests/HttpConnectionTests.cs @@ -44,7 +44,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests ConnectionFeatures = connectionFeatures, MemoryPool = _memoryPool, HttpConnectionId = long.MinValue, - Application = pair.Application, Transport = pair.Transport, ServiceContext = new TestServiceContext { @@ -66,7 +65,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var mockDebugger = new Mock(); mockDebugger.SetupGet(g => g.IsAttached).Returns(true); _httpConnection.Debugger = mockDebugger.Object; - _httpConnection.Initialize(_httpConnectionContext.Transport, _httpConnectionContext.Application); + _httpConnection.Initialize(_httpConnectionContext.Transport); var now = DateTimeOffset.Now; _httpConnection.Tick(now); @@ -113,7 +112,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _httpConnectionContext.ServiceContext.Log = logger; - _httpConnection.Initialize(_httpConnectionContext.Transport, _httpConnectionContext.Application); + _httpConnection.Initialize(_httpConnectionContext.Transport); _httpConnection.Http1Connection.Reset(); // Initialize timestamp @@ -140,7 +139,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var mockLogger = new Mock(); _httpConnectionContext.ServiceContext.Log = mockLogger.Object; - _httpConnection.Initialize(_httpConnectionContext.Transport, _httpConnectionContext.Application); + _httpConnection.Initialize(_httpConnectionContext.Transport); _httpConnection.Http1Connection.Reset(); // Initialize timestamp @@ -182,7 +181,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var mockLogger = new Mock(); _httpConnectionContext.ServiceContext.Log = mockLogger.Object; - _httpConnection.Initialize(_httpConnectionContext.Transport, _httpConnectionContext.Application); + _httpConnection.Initialize(_httpConnectionContext.Transport); _httpConnection.Http1Connection.Reset(); // Initialize timestamp @@ -259,7 +258,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var mockLogger = new Mock(); _httpConnectionContext.ServiceContext.Log = mockLogger.Object; - _httpConnection.Initialize(_httpConnectionContext.Transport, _httpConnectionContext.Application); + _httpConnection.Initialize(_httpConnectionContext.Transport); _httpConnection.Http1Connection.Reset(); // Initialize timestamp @@ -327,7 +326,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var mockLogger = new Mock(); _httpConnectionContext.ServiceContext.Log = mockLogger.Object; - _httpConnection.Initialize(_httpConnectionContext.Transport, _httpConnectionContext.Application); + _httpConnection.Initialize(_httpConnectionContext.Transport); _httpConnection.Http1Connection.Reset(); // Initialize timestamp @@ -389,7 +388,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var mockLogger = new Mock(); _httpConnectionContext.ServiceContext.Log = mockLogger.Object; - _httpConnection.Initialize(_httpConnectionContext.Transport, _httpConnectionContext.Application); + _httpConnection.Initialize(_httpConnectionContext.Transport); _httpConnection.Http1Connection.Reset(); var startTime = systemClock.UtcNow; @@ -430,7 +429,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var mockLogger = new Mock(); _httpConnectionContext.ServiceContext.Log = mockLogger.Object; - _httpConnection.Initialize(_httpConnectionContext.Transport, _httpConnectionContext.Application); + _httpConnection.Initialize(_httpConnectionContext.Transport); _httpConnection.Http1Connection.Reset(); _httpConnection.Http1Connection.RequestAborted.Register(() => { @@ -464,7 +463,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var mockLogger = new Mock(); _httpConnectionContext.ServiceContext.Log = mockLogger.Object; - _httpConnection.Initialize(_httpConnectionContext.Transport, _httpConnectionContext.Application); + _httpConnection.Initialize(_httpConnectionContext.Transport); _httpConnection.Http1Connection.Reset(); _httpConnection.Http1Connection.RequestAborted.Register(() => { @@ -506,7 +505,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var mockLogger = new Mock(); _httpConnectionContext.ServiceContext.Log = mockLogger.Object; - _httpConnection.Initialize(_httpConnectionContext.Transport, _httpConnectionContext.Application); + _httpConnection.Initialize(_httpConnectionContext.Transport); _httpConnection.Http1Connection.Reset(); _httpConnection.Http1Connection.RequestAborted.Register(() => { @@ -555,7 +554,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var mockLogger = new Mock(); _httpConnectionContext.ServiceContext.Log = mockLogger.Object; - _httpConnection.Initialize(_httpConnectionContext.Transport, _httpConnectionContext.Application); + _httpConnection.Initialize(_httpConnectionContext.Transport); _httpConnection.Http1Connection.Reset(); _httpConnection.Http1Connection.RequestAborted.Register(() => { diff --git a/test/Kestrel.Core.Tests/HttpProtocolFeatureCollectionTests.cs b/test/Kestrel.Core.Tests/HttpProtocolFeatureCollectionTests.cs index 84d08ff3d7..6ac87ba1a5 100644 --- a/test/Kestrel.Core.Tests/HttpProtocolFeatureCollectionTests.cs +++ b/test/Kestrel.Core.Tests/HttpProtocolFeatureCollectionTests.cs @@ -46,7 +46,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests ConnectionFeatures = new FeatureCollection(), MemoryPool = _memoryPool, TimeoutControl = _timeoutControl.Object, - Application = pair.Application, Transport = pair.Transport }; diff --git a/test/Kestrel.Core.Tests/HttpResponseHeadersTests.cs b/test/Kestrel.Core.Tests/HttpResponseHeadersTests.cs index 1afb559ad1..eddc7701fd 100644 --- a/test/Kestrel.Core.Tests/HttpResponseHeadersTests.cs +++ b/test/Kestrel.Core.Tests/HttpResponseHeadersTests.cs @@ -30,7 +30,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests ServiceContext = new TestServiceContext(), ConnectionFeatures = new FeatureCollection(), MemoryPool = memoryPool, - Application = pair.Application, Transport = pair.Transport, TimeoutControl = null }; diff --git a/test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj b/test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj index a8e44fa8c8..f6144e34fc 100644 --- a/test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj +++ b/test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj @@ -1,7 +1,7 @@  - Microsoft.AspNetCore.Server.Kestrel.Core.Tests + Core.Tests Microsoft.AspNetCore.Server.Kestrel.Core.Tests $(StandardTestTfms) true @@ -18,10 +18,11 @@ + + - - + diff --git a/test/Kestrel.Core.Tests/MessageBodyTests.cs b/test/Kestrel.Core.Tests/MessageBodyTests.cs index 3bd6a8ca74..870e89c47b 100644 --- a/test/Kestrel.Core.Tests/MessageBodyTests.cs +++ b/test/Kestrel.Core.Tests/MessageBodyTests.cs @@ -422,12 +422,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var options = new PipeOptions(pool: memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); var transport = pair.Transport; - var application = pair.Application; var http1ConnectionContext = new Http1ConnectionContext { ServiceContext = new TestServiceContext(), ConnectionFeatures = new FeatureCollection(), - Application = application, Transport = transport, MemoryPool = memoryPool, TimeoutControl = Mock.Of() diff --git a/test/Kestrel.Core.Tests/Properties/AssemblyInfo.cs b/test/Kestrel.Core.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..678468c757 --- /dev/null +++ b/test/Kestrel.Core.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.Logging.Testing; + +[assembly: ShortClassName] diff --git a/test/Kestrel.Core.Tests/TestInput.cs b/test/Kestrel.Core.Tests/TestHelpers/TestInput.cs similarity index 98% rename from test/Kestrel.Core.Tests/TestInput.cs rename to test/Kestrel.Core.Tests/TestHelpers/TestInput.cs index b032b334f0..78e28dcbc2 100644 --- a/test/Kestrel.Core.Tests/TestInput.cs +++ b/test/Kestrel.Core.Tests/TestHelpers/TestInput.cs @@ -38,7 +38,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests ServiceContext = new TestServiceContext(), ConnectionContext = Mock.Of(), ConnectionFeatures = connectionFeatures, - Application = Application, Transport = Transport, MemoryPool = _memoryPool, TimeoutControl = Mock.Of() diff --git a/test/Kestrel.InMemory.FunctionalTests/BadHttpRequestTests.cs b/test/Kestrel.InMemory.FunctionalTests/BadHttpRequestTests.cs index d58615c8a7..b566c5617f 100644 --- a/test/Kestrel.InMemory.FunctionalTests/BadHttpRequestTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/BadHttpRequestTests.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; @@ -191,7 +191,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests private async Task TestBadRequest(string request, string expectedResponseStatusCode, string expectedExceptionMessage, string expectedAllowHeader = null) { BadHttpRequestException loggedException = null; - var mockKestrelTrace = new Mock(Logger) { CallBase = true }; + var mockKestrelTrace = new Mock(); mockKestrelTrace .Setup(trace => trace.IsEnabled(LogLevel.Information)) .Returns(true); diff --git a/test/Kestrel.Core.Tests/Http2ConnectionTests.cs b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs similarity index 82% rename from test/Kestrel.Core.Tests/Http2ConnectionTests.cs rename to test/Kestrel.InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index a41526e333..b1b77db419 100644 --- a/test/Kestrel.Core.Tests/Http2ConnectionTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -3,35 +3,25 @@ using System; using System.Buffers; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; -using System.IO.Pipelines; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; -using Moq; using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { - public class Http2ConnectionTests : IDisposable, IHttpHeadersHandler + public class Http2ConnectionTests : Http2TestBase { - private static readonly string _largeHeaderValue = new string('a', HPackDecoder.MaxStringOctets); - private static readonly IEnumerable> _postRequestHeaders = new[] { new KeyValuePair(HeaderNames.Method, "POST"), @@ -49,19 +39,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests new KeyValuePair("expect", "100-continue"), }; - private static readonly IEnumerable> _browserRequestHeaders = new[] - { - new KeyValuePair(HeaderNames.Method, "GET"), - new KeyValuePair(HeaderNames.Path, "/"), - new KeyValuePair(HeaderNames.Scheme, "http"), - new KeyValuePair(HeaderNames.Authority, "localhost:80"), - new KeyValuePair("user-agent", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:54.0) Gecko/20100101 Firefox/54.0"), - new KeyValuePair("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"), - new KeyValuePair("accept-language", "en-US,en;q=0.5"), - new KeyValuePair("accept-encoding", "gzip, deflate, br"), - new KeyValuePair("upgrade-insecure-requests", "1"), - }; - private static readonly IEnumerable> _requestTrailers = new[] { new KeyValuePair("trailer-one", "1"), @@ -102,253 +79,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests private static readonly byte[] _noData = new byte[0]; private static readonly byte[] _maxData = Encoding.ASCII.GetBytes(new string('a', Http2Frame.MinAllowedMaxFrameSize)); - private readonly TestApplicationErrorLogger _logger; - private readonly Http2PeerSettings _clientSettings = new Http2PeerSettings(); - private readonly HPackEncoder _hpackEncoder = new HPackEncoder(); - private readonly HPackDecoder _hpackDecoder; - - private readonly ConcurrentDictionary> _runningStreams = new ConcurrentDictionary>(); - private readonly Dictionary _receivedHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - private readonly Dictionary _decodedHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - private readonly HashSet _abortedStreamIds = new HashSet(); - private readonly object _abortedStreamIdsLock = new object(); - private readonly TaskCompletionSource _closingStateReached = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - private readonly TaskCompletionSource _closedStateReached = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - private readonly RequestDelegate _noopApplication; - private readonly RequestDelegate _readHeadersApplication; - private readonly RequestDelegate _readTrailersApplication; - private readonly RequestDelegate _bufferingApplication; - private readonly RequestDelegate _echoApplication; - private readonly RequestDelegate _echoWaitForAbortApplication; - private readonly RequestDelegate _largeHeadersApplication; - private readonly RequestDelegate _waitForAbortApplication; - private readonly RequestDelegate _waitForAbortFlushingApplication; - private readonly RequestDelegate _waitForAbortWithDataApplication; - - private MemoryPool _memoryPool; - private DuplexPipe.DuplexPipePair _pair; - private Http2ConnectionContext _connectionContext; - private Http2Connection _connection; - - private Task _connectionTask; - - public Http2ConnectionTests() - { - _noopApplication = context => Task.CompletedTask; - - _readHeadersApplication = context => - { - foreach (var header in context.Request.Headers) - { - _receivedHeaders[header.Key] = header.Value.ToString(); - } - - return Task.CompletedTask; - }; - - _readTrailersApplication = async context => - { - using (var ms = new MemoryStream()) - { - // Consuming the entire request body guarantees trailers will be available - await context.Request.Body.CopyToAsync(ms); - } - - foreach (var header in context.Request.Headers) - { - _receivedHeaders[header.Key] = header.Value.ToString(); - } - }; - - _bufferingApplication = async context => - { - var data = new List(); - var buffer = new byte[1024]; - var received = 0; - - while ((received = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length)) > 0) - { - data.AddRange(new ArraySegment(buffer, 0, received)); - } - - await context.Response.Body.WriteAsync(data.ToArray(), 0, data.Count); - }; - - _echoApplication = async context => - { - var buffer = new byte[Http2Frame.MinAllowedMaxFrameSize]; - var received = 0; - - while ((received = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length)) > 0) - { - await context.Response.Body.WriteAsync(buffer, 0, received); - } - }; - - _echoWaitForAbortApplication = async context => - { - var buffer = new byte[Http2Frame.MinAllowedMaxFrameSize]; - var received = 0; - - while ((received = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length)) > 0) - { - await context.Response.Body.WriteAsync(buffer, 0, received); - } - - var sem = new SemaphoreSlim(0); - - context.RequestAborted.Register(() => - { - sem.Release(); - }); - - await sem.WaitAsync().DefaultTimeout(); - }; - - _largeHeadersApplication = context => - { - foreach (var name in new[] { "a", "b", "c", "d", "e", "f", "g", "h" }) - { - context.Response.Headers[name] = _largeHeaderValue; - } - - return Task.CompletedTask; - }; - - _waitForAbortApplication = async context => - { - var streamIdFeature = context.Features.Get(); - var sem = new SemaphoreSlim(0); - - context.RequestAborted.Register(() => - { - lock (_abortedStreamIdsLock) - { - _abortedStreamIds.Add(streamIdFeature.StreamId); - } - - sem.Release(); - }); - - await sem.WaitAsync().DefaultTimeout(); - - _runningStreams[streamIdFeature.StreamId].TrySetResult(null); - }; - - _waitForAbortFlushingApplication = async context => - { - var streamIdFeature = context.Features.Get(); - var sem = new SemaphoreSlim(0); - - context.RequestAborted.Register(() => - { - lock (_abortedStreamIdsLock) - { - _abortedStreamIds.Add(streamIdFeature.StreamId); - } - - sem.Release(); - }); - - await sem.WaitAsync().DefaultTimeout(); - - await context.Response.Body.FlushAsync(); - - _runningStreams[streamIdFeature.StreamId].TrySetResult(null); - }; - - _waitForAbortWithDataApplication = async context => - { - var streamIdFeature = context.Features.Get(); - var sem = new SemaphoreSlim(0); - - context.RequestAborted.Register(() => - { - lock (_abortedStreamIdsLock) - { - _abortedStreamIds.Add(streamIdFeature.StreamId); - } - - sem.Release(); - }); - - await sem.WaitAsync().DefaultTimeout(); - - await context.Response.Body.WriteAsync(new byte[10], 0, 10); - - _runningStreams[streamIdFeature.StreamId].TrySetResult(null); - }; - - _hpackDecoder = new HPackDecoder((int)_clientSettings.HeaderTableSize); - - _logger = new TestApplicationErrorLogger(); - - InitializeConnectionFields(KestrelMemoryPool.Create()); - } - - private void InitializeConnectionFields(MemoryPool memoryPool) - { - _memoryPool = memoryPool; - - // Always dispatch test code back to the ThreadPool. This prevents deadlocks caused by continuing - // Http2Connection.ProcessRequestsAsync() loop with writer locks acquired. Run product code inline to make - // it easier to verify request frames are processed correctly immediately after sending the them. - var inputPipeOptions = new PipeOptions( - pool: _memoryPool, - readerScheduler: PipeScheduler.Inline, - writerScheduler: PipeScheduler.ThreadPool, - useSynchronizationContext: false - ); - var outputPipeOptions = new PipeOptions( - pool: _memoryPool, - readerScheduler: PipeScheduler.ThreadPool, - writerScheduler: PipeScheduler.Inline, - useSynchronizationContext: false - ); - - _pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions); - - var mockKestrelTrace = new Mock(_logger) - { - CallBase = true - }; - mockKestrelTrace - .Setup(m => m.Http2ConnectionClosing(It.IsAny())) - .Callback(() => _closingStateReached.SetResult(null)); - mockKestrelTrace - .Setup(m => m.Http2ConnectionClosed(It.IsAny(), It.IsAny())) - .Callback(() => _closedStateReached.SetResult(null)); - - _connectionContext = new Http2ConnectionContext - { - ConnectionFeatures = new FeatureCollection(), - ServiceContext = new TestServiceContext() - { - Log = mockKestrelTrace.Object - }, - MemoryPool = _memoryPool, - Application = _pair.Application, - Transport = _pair.Transport - }; - - _connection = new Http2Connection(_connectionContext); - } - - public void Dispose() - { - _pair.Application.Input.Complete(); - _pair.Application.Output.Complete(); - _pair.Transport.Input.Complete(); - _pair.Transport.Output.Complete(); - _memoryPool.Dispose(); - } - - void IHttpHeadersHandler.OnHeader(Span name, Span value) - { - _decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiStringNonNullCharacters(); - } - [Fact] public async Task Frame_Received_OverMaxSize_FrameError() { @@ -1025,7 +755,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var closedMessage = CoreStrings.FormatHttp2ErrorStreamClosed(Http2FrameType.DATA, streamId: 1); var halfClosedMessage = CoreStrings.FormatHttp2ErrorStreamHalfClosedRemote(Http2FrameType.DATA, streamId: 1); - var message = Assert.Single(_logger.Messages, m => m.Exception is Http2ConnectionErrorException); + var message = Assert.Single(TestApplicationErrorLogger.Messages, m => m.Exception is Http2ConnectionErrorException); Assert.True(message.Exception.Message.IndexOf(closedMessage) >= 0 || message.Exception.Message.IndexOf(halfClosedMessage) >= 0); } @@ -1439,6 +1169,34 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); } + [Fact] + public async Task HEADERS_OverMaxStreamLimit_Refused() + { + _connection.ServerSettings.MaxConcurrentStreams = 1; + + var requestBlocker = new TaskCompletionSource(); + await InitializeConnectionAsync(context => requestBlocker.Task); + + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + + await StartStreamAsync(3, _browserRequestHeaders, endStream: true); + + await WaitForStreamErrorAsync(3, Http2ErrorCode.REFUSED_STREAM, CoreStrings.Http2ErrorMaxStreams); + + requestBlocker.SetResult(0); + + await ExpectAsync(Http2FrameType.HEADERS, + withLength: 55, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: 0, + withFlags: (byte)Http2DataFrameFlags.END_STREAM, + withStreamId: 1); + + await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + } + [Fact] public async Task HEADERS_Received_StreamIdZero_ConnectionError() { @@ -1496,7 +1254,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var closedMessage = CoreStrings.FormatHttp2ErrorStreamClosed(Http2FrameType.HEADERS, streamId: 1); var halfClosedMessage = CoreStrings.FormatHttp2ErrorStreamHalfClosedRemote(Http2FrameType.HEADERS, streamId: 1); - var message = Assert.Single(_logger.Messages, m => m.Exception is Http2ConnectionErrorException); + var message = Assert.Single(TestApplicationErrorLogger.Messages, m => m.Exception is Http2ConnectionErrorException); Assert.True(message.Exception.Message.IndexOf(closedMessage) >= 0 || message.Exception.Message.IndexOf(halfClosedMessage) >= 0); } @@ -2198,6 +1956,66 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests expectedErrorMessage: CoreStrings.FormatHttp2ErrorHeadersInterleaved(Http2FrameType.RST_STREAM, streamId: 1, headersStreamId: 1)); } + [Fact] + public async Task SETTINGS_KestrelDefaults_Sent() + { + _connectionTask = _connection.ProcessRequestsAsync(new DummyApplication(_noopApplication)); + + await SendPreambleAsync().ConfigureAwait(false); + await SendSettingsAsync(); + + var frame = await ExpectAsync(Http2FrameType.SETTINGS, + withLength: 6, + withFlags: 0, + withStreamId: 0); + + // Only non protocol defaults are sent + Assert.Equal(1, frame.SettingsCount); + var settings = frame.GetSettings(); + Assert.Equal(1, settings.Count); + var setting = settings[0]; + Assert.Equal(Http2SettingsParameter.SETTINGS_MAX_CONCURRENT_STREAMS, setting.Parameter); + Assert.Equal(100u, setting.Value); + + await ExpectAsync(Http2FrameType.SETTINGS, + withLength: 0, + withFlags: (byte)Http2SettingsFrameFlags.ACK, + withStreamId: 0); + + await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); + } + + [Fact] + public async Task SETTINGS_Custom_Sent() + { + _connection.ServerSettings.MaxConcurrentStreams = 1; + + _connectionTask = _connection.ProcessRequestsAsync(new DummyApplication(_noopApplication)); + + await SendPreambleAsync().ConfigureAwait(false); + await SendSettingsAsync(); + + var frame = await ExpectAsync(Http2FrameType.SETTINGS, + withLength: 6, + withFlags: 0, + withStreamId: 0); + + // Only non protocol defaults are sent + Assert.Equal(1, frame.SettingsCount); + var settings = frame.GetSettings(); + Assert.Equal(1, settings.Count); + var setting = settings[0]; + Assert.Equal(Http2SettingsParameter.SETTINGS_MAX_CONCURRENT_STREAMS, setting.Parameter); + Assert.Equal(1u, setting.Value); + + await ExpectAsync(Http2FrameType.SETTINGS, + withLength: 0, + withFlags: (byte)Http2SettingsFrameFlags.ACK, + withStreamId: 0); + + await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); + } + [Fact] public async Task SETTINGS_Received_Sends_ACK() { @@ -3275,7 +3093,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _pair.Application.Output.Complete(new ConnectionResetException(string.Empty)); await StopConnectionAsync(1, ignoreNonGoAwayFrames: false); - Assert.Single(_logger.Messages, m => m.Exception is ConnectionResetException); + Assert.Single(TestApplicationErrorLogger.Messages, m => m.Exception is ConnectionResetException); } [Fact] @@ -3287,7 +3105,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var result = await _pair.Application.Input.ReadAsync(); Assert.True(result.IsCompleted); - Assert.DoesNotContain(_logger.Messages, m => m.Exception is ConnectionResetException); + Assert.DoesNotContain(TestApplicationErrorLogger.Messages, m => m.Exception is ConnectionResetException); } [Fact] @@ -3469,7 +3287,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.Equal(TaskStatus.RanToCompletion, _connection.ProcessRequestsAsync(new DummyApplication(_noopApplication)).Status); - var logMessage = _logger.Messages.Single(m => m.LogLevel >= LogLevel.Information); + var logMessage = TestApplicationErrorLogger.Messages.Single(m => m.LogLevel >= LogLevel.Information); Assert.Equal(LogLevel.Information, logMessage.LogLevel); Assert.Equal("Connection id \"(null)\" request processing ended abnormally.", logMessage.Message); @@ -3484,572 +3302,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.Equal(TaskStatus.RanToCompletion, _connection.ProcessRequestsAsync(new DummyApplication(_noopApplication)).Status); - var logMessage = _logger.Messages.Single(m => m.LogLevel >= LogLevel.Information); + var logMessage = TestApplicationErrorLogger.Messages.Single(m => m.LogLevel >= LogLevel.Information); Assert.Equal(LogLevel.Warning, logMessage.LogLevel); Assert.Equal(CoreStrings.RequestProcessingEndError, logMessage.Message); Assert.Same(exception, logMessage.Exception); } - private async Task InitializeConnectionAsync(RequestDelegate application) - { - _connectionTask = _connection.ProcessRequestsAsync(new DummyApplication(application)); - - await SendPreambleAsync().ConfigureAwait(false); - await SendSettingsAsync(); - - await ExpectAsync(Http2FrameType.SETTINGS, - withLength: 0, - withFlags: 0, - withStreamId: 0); - - await ExpectAsync(Http2FrameType.SETTINGS, - withLength: 0, - withFlags: (byte)Http2SettingsFrameFlags.ACK, - withStreamId: 0); - } - - private async Task StartStreamAsync(int streamId, IEnumerable> headers, bool endStream) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - _runningStreams[streamId] = tcs; - - var frame = new Http2Frame(); - frame.PrepareHeaders(Http2HeadersFrameFlags.NONE, streamId); - var done = _hpackEncoder.BeginEncode(headers, frame.HeadersPayload, out var length); - frame.Length = length; - - if (done) - { - frame.HeadersFlags = Http2HeadersFrameFlags.END_HEADERS; - } - - if (endStream) - { - frame.HeadersFlags |= Http2HeadersFrameFlags.END_STREAM; - } - - await SendAsync(frame.Raw); - - while (!done) - { - frame.PrepareContinuation(Http2ContinuationFrameFlags.NONE, streamId); - done = _hpackEncoder.Encode(frame.HeadersPayload, out length); - frame.Length = length; - - if (done) - { - frame.ContinuationFlags = Http2ContinuationFrameFlags.END_HEADERS; - } - - await SendAsync(frame.Raw); - } - } - - private async Task SendHeadersWithPaddingAsync(int streamId, IEnumerable> headers, byte padLength, bool endStream) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - _runningStreams[streamId] = tcs; - - var frame = new Http2Frame(); - - frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.PADDED, streamId); - frame.HeadersPadLength = padLength; - - _hpackEncoder.BeginEncode(headers, frame.HeadersPayload, out var length); - - frame.Length = 1 + length + padLength; - frame.Payload.Slice(1 + length).Fill(0); - - if (endStream) - { - frame.HeadersFlags |= Http2HeadersFrameFlags.END_STREAM; - } - - await SendAsync(frame.Raw); - } - - private async Task SendHeadersWithPriorityAsync(int streamId, IEnumerable> headers, byte priority, int streamDependency, bool endStream) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - _runningStreams[streamId] = tcs; - - var frame = new Http2Frame(); - frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.PRIORITY, streamId); - frame.HeadersPriority = priority; - frame.HeadersStreamDependency = streamDependency; - - _hpackEncoder.BeginEncode(headers, frame.HeadersPayload, out var length); - - frame.Length = 5 + length; - - if (endStream) - { - frame.HeadersFlags |= Http2HeadersFrameFlags.END_STREAM; - } - - await SendAsync(frame.Raw); - } - - private async Task SendHeadersWithPaddingAndPriorityAsync(int streamId, IEnumerable> headers, byte padLength, byte priority, int streamDependency, bool endStream) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - _runningStreams[streamId] = tcs; - - var frame = new Http2Frame(); - frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.PADDED | Http2HeadersFrameFlags.PRIORITY, streamId); - frame.HeadersPadLength = padLength; - frame.HeadersPriority = priority; - frame.HeadersStreamDependency = streamDependency; - - _hpackEncoder.BeginEncode(headers, frame.HeadersPayload, out var length); - - frame.Length = 6 + length + padLength; - frame.Payload.Slice(6 + length).Fill(0); - - if (endStream) - { - frame.HeadersFlags |= Http2HeadersFrameFlags.END_STREAM; - } - - await SendAsync(frame.Raw); - } - - private Task WaitForAllStreamsAsync() - { - return Task.WhenAll(_runningStreams.Values.Select(tcs => tcs.Task)).DefaultTimeout(); - } - - private Task SendAsync(ReadOnlySpan span) - { - var writableBuffer = _pair.Application.Output; - writableBuffer.Write(span); - return FlushAsync(writableBuffer); - } - - private static async Task FlushAsync(PipeWriter writableBuffer) - { - await writableBuffer.FlushAsync(); - } - - private Task SendPreambleAsync() => SendAsync(new ArraySegment(Http2Connection.ClientPreface)); - - private Task SendSettingsAsync() - { - var frame = new Http2Frame(); - frame.PrepareSettings(Http2SettingsFrameFlags.NONE, _clientSettings); - return SendAsync(frame.Raw); - } - - private Task SendSettingsAckWithInvalidLengthAsync(int length) - { - var frame = new Http2Frame(); - frame.PrepareSettings(Http2SettingsFrameFlags.ACK); - frame.Length = length; - return SendAsync(frame.Raw); - } - - private Task SendSettingsWithInvalidStreamIdAsync(int streamId) - { - var frame = new Http2Frame(); - frame.PrepareSettings(Http2SettingsFrameFlags.NONE, _clientSettings); - frame.StreamId = streamId; - return SendAsync(frame.Raw); - } - - private Task SendSettingsWithInvalidLengthAsync(int length) - { - var frame = new Http2Frame(); - frame.PrepareSettings(Http2SettingsFrameFlags.NONE, _clientSettings); - frame.Length = length; - return SendAsync(frame.Raw); - } - - private Task SendSettingsWithInvalidParameterValueAsync(Http2SettingsParameter parameter, uint value) - { - var frame = new Http2Frame(); - frame.PrepareSettings(Http2SettingsFrameFlags.NONE); - frame.Length = 6; - - frame.Payload[0] = (byte)((ushort)parameter >> 8); - frame.Payload[1] = (byte)(ushort)parameter; - frame.Payload[2] = (byte)(value >> 24); - frame.Payload[3] = (byte)(value >> 16); - frame.Payload[4] = (byte)(value >> 8); - frame.Payload[5] = (byte)value; - - return SendAsync(frame.Raw); - } - - private Task SendPushPromiseFrameAsync() - { - var frame = new Http2Frame(); - frame.Length = 0; - frame.Type = Http2FrameType.PUSH_PROMISE; - frame.StreamId = 1; - return SendAsync(frame.Raw); - } - - private async Task SendHeadersAsync(int streamId, Http2HeadersFrameFlags flags, IEnumerable> headers) - { - var frame = new Http2Frame(); - - frame.PrepareHeaders(flags, streamId); - var done = _hpackEncoder.BeginEncode(headers, frame.Payload, out var length); - frame.Length = length; - - await SendAsync(frame.Raw); - - return done; - } - - private Task SendHeadersAsync(int streamId, Http2HeadersFrameFlags flags, byte[] headerBlock) - { - var frame = new Http2Frame(); - - frame.PrepareHeaders(flags, streamId); - frame.Length = headerBlock.Length; - headerBlock.CopyTo(frame.HeadersPayload); - - return SendAsync(frame.Raw); - } - - private Task SendInvalidHeadersFrameAsync(int streamId, int frameLength, byte padLength) - { - Assert.True(padLength >= frameLength, $"{nameof(padLength)} must be greater than or equal to {nameof(frameLength)} to create an invalid frame."); - - var frame = new Http2Frame(); - - frame.PrepareHeaders(Http2HeadersFrameFlags.PADDED, streamId); - frame.Payload[0] = padLength; - - // Set length last so .Payload can be written to - frame.Length = frameLength; - - return SendAsync(frame.Raw); - } - - private Task SendIncompleteHeadersFrameAsync(int streamId) - { - var frame = new Http2Frame(); - - frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS, streamId); - frame.Length = 3; - - // Set up an incomplete Literal Header Field w/ Incremental Indexing frame, - // with an incomplete new name - frame.Payload[0] = 0; - frame.Payload[1] = 2; - frame.Payload[2] = (byte)'a'; - - return SendAsync(frame.Raw); - } - - private async Task SendContinuationAsync(int streamId, Http2ContinuationFrameFlags flags) - { - var frame = new Http2Frame(); - - frame.PrepareContinuation(flags, streamId); - var done = _hpackEncoder.Encode(frame.Payload, out var length); - frame.Length = length; - - await SendAsync(frame.Raw); - - return done; - } - - private async Task SendContinuationAsync(int streamId, Http2ContinuationFrameFlags flags, byte[] payload) - { - var frame = new Http2Frame(); - - frame.PrepareContinuation(flags, streamId); - frame.Length = payload.Length; - payload.CopyTo(frame.Payload); - - await SendAsync(frame.Raw); - } - - private Task SendEmptyContinuationFrameAsync(int streamId, Http2ContinuationFrameFlags flags) - { - var frame = new Http2Frame(); - - frame.PrepareContinuation(flags, streamId); - frame.Length = 0; - - return SendAsync(frame.Raw); - } - - private Task SendIncompleteContinuationFrameAsync(int streamId) - { - var frame = new Http2Frame(); - - frame.PrepareContinuation(Http2ContinuationFrameFlags.END_HEADERS, streamId); - frame.Length = 3; - - // Set up an incomplete Literal Header Field w/ Incremental Indexing frame, - // with an incomplete new name - frame.Payload[0] = 0; - frame.Payload[1] = 2; - frame.Payload[2] = (byte)'a'; - - return SendAsync(frame.Raw); - } - - private Task SendDataAsync(int streamId, Span data, bool endStream) - { - var frame = new Http2Frame(); - - frame.PrepareData(streamId); - frame.Length = data.Length; - frame.DataFlags = endStream ? Http2DataFrameFlags.END_STREAM : Http2DataFrameFlags.NONE; - data.CopyTo(frame.DataPayload); - - return SendAsync(frame.Raw); - } - - private Task SendDataWithPaddingAsync(int streamId, Span data, byte padLength, bool endStream) - { - var frame = new Http2Frame(); - - frame.PrepareData(streamId, padLength); - frame.Length = data.Length + 1 + padLength; - data.CopyTo(frame.DataPayload); - - if (endStream) - { - frame.DataFlags |= Http2DataFrameFlags.END_STREAM; - } - - return SendAsync(frame.Raw); - } - - private Task SendInvalidDataFrameAsync(int streamId, int frameLength, byte padLength) - { - Assert.True(padLength >= frameLength, $"{nameof(padLength)} must be greater than or equal to {nameof(frameLength)} to create an invalid frame."); - - var frame = new Http2Frame(); - - frame.PrepareData(streamId); - frame.DataFlags = Http2DataFrameFlags.PADDED; - frame.Payload[0] = padLength; - - // Set length last so .Payload can be written to - frame.Length = frameLength; - - return SendAsync(frame.Raw); - } - - private Task SendPingAsync(Http2PingFrameFlags flags) - { - var pingFrame = new Http2Frame(); - pingFrame.PreparePing(flags); - return SendAsync(pingFrame.Raw); - } - - private Task SendPingWithInvalidLengthAsync(int length) - { - var pingFrame = new Http2Frame(); - pingFrame.PreparePing(Http2PingFrameFlags.NONE); - pingFrame.Length = length; - return SendAsync(pingFrame.Raw); - } - - private Task SendPingWithInvalidStreamIdAsync(int streamId) - { - Assert.NotEqual(0, streamId); - - var pingFrame = new Http2Frame(); - pingFrame.PreparePing(Http2PingFrameFlags.NONE); - pingFrame.StreamId = streamId; - return SendAsync(pingFrame.Raw); - } - - private Task SendPriorityAsync(int streamId, int streamDependency = 0) - { - var priorityFrame = new Http2Frame(); - priorityFrame.PreparePriority(streamId, streamDependency: streamDependency, exclusive: false, weight: 0); - return SendAsync(priorityFrame.Raw); - } - - private Task SendInvalidPriorityFrameAsync(int streamId, int length) - { - var priorityFrame = new Http2Frame(); - priorityFrame.PreparePriority(streamId, streamDependency: 0, exclusive: false, weight: 0); - priorityFrame.Length = length; - return SendAsync(priorityFrame.Raw); - } - - private Task SendRstStreamAsync(int streamId) - { - var rstStreamFrame = new Http2Frame(); - rstStreamFrame.PrepareRstStream(streamId, Http2ErrorCode.CANCEL); - return SendAsync(rstStreamFrame.Raw); - } - - private Task SendInvalidRstStreamFrameAsync(int streamId, int length) - { - var frame = new Http2Frame(); - frame.PrepareRstStream(streamId, Http2ErrorCode.CANCEL); - frame.Length = length; - return SendAsync(frame.Raw); - } - - private Task SendGoAwayAsync() - { - var frame = new Http2Frame(); - frame.PrepareGoAway(0, Http2ErrorCode.NO_ERROR); - return SendAsync(frame.Raw); - } - - private Task SendInvalidGoAwayFrameAsync() - { - var frame = new Http2Frame(); - frame.PrepareGoAway(0, Http2ErrorCode.NO_ERROR); - frame.StreamId = 1; - return SendAsync(frame.Raw); - } - - private Task SendWindowUpdateAsync(int streamId, int sizeIncrement) - { - var frame = new Http2Frame(); - frame.PrepareWindowUpdate(streamId, sizeIncrement); - return SendAsync(frame.Raw); - } - - private Task SendInvalidWindowUpdateAsync(int streamId, int sizeIncrement, int length) - { - var frame = new Http2Frame(); - frame.PrepareWindowUpdate(streamId, sizeIncrement); - frame.Length = length; - return SendAsync(frame.Raw); - } - - private Task SendUnknownFrameTypeAsync(int streamId, int frameType) - { - var frame = new Http2Frame(); - frame.StreamId = streamId; - frame.Type = (Http2FrameType)frameType; - frame.Length = 0; - return SendAsync(frame.Raw); - } - - private async Task ReceiveFrameAsync() - { - var frame = new Http2Frame(); - - while (true) - { - var result = await _pair.Application.Input.ReadAsync(); - var buffer = result.Buffer; - var consumed = buffer.Start; - var examined = buffer.End; - - try - { - Assert.True(buffer.Length > 0); - - if (Http2FrameReader.ReadFrame(buffer, frame, 16_384, out consumed, out examined)) - { - return frame; - } - - if (result.IsCompleted) - { - throw new IOException("The reader completed without returning a frame."); - } - } - finally - { - _pair.Application.Input.AdvanceTo(consumed, examined); - } - } - } - - private async Task ExpectAsync(Http2FrameType type, int withLength, byte withFlags, int withStreamId) - { - var frame = await ReceiveFrameAsync(); - - Assert.Equal(type, frame.Type); - Assert.Equal(withLength, frame.Length); - Assert.Equal(withFlags, frame.Flags); - Assert.Equal(withStreamId, frame.StreamId); - - return frame; - } - - private Task StopConnectionAsync(int expectedLastStreamId, bool ignoreNonGoAwayFrames) - { - _pair.Application.Output.Complete(); - - return WaitForConnectionStopAsync(expectedLastStreamId, ignoreNonGoAwayFrames); - } - - private Task WaitForConnectionStopAsync(int expectedLastStreamId, bool ignoreNonGoAwayFrames) - { - return WaitForConnectionErrorAsync(ignoreNonGoAwayFrames, expectedLastStreamId, Http2ErrorCode.NO_ERROR, expectedErrorMessage: null); - } - - private void VerifyGoAway(Http2Frame frame, int expectedLastStreamId, Http2ErrorCode expectedErrorCode) - { - Assert.Equal(Http2FrameType.GOAWAY, frame.Type); - Assert.Equal(8, frame.Length); - Assert.Equal(0, frame.Flags); - Assert.Equal(0, frame.StreamId); - Assert.Equal(expectedLastStreamId, frame.GoAwayLastStreamId); - Assert.Equal(expectedErrorCode, frame.GoAwayErrorCode); - } - - private async Task WaitForConnectionErrorAsync(bool ignoreNonGoAwayFrames, int expectedLastStreamId, Http2ErrorCode expectedErrorCode, string expectedErrorMessage) - where TException : Exception - { - var frame = await ReceiveFrameAsync(); - - if (ignoreNonGoAwayFrames) - { - while (frame.Type != Http2FrameType.GOAWAY) - { - frame = await ReceiveFrameAsync(); - } - } - - VerifyGoAway(frame, expectedLastStreamId, expectedErrorCode); - - if (expectedErrorMessage != null) - { - var message = Assert.Single(_logger.Messages, m => m.Exception is TException); - Assert.Contains(expectedErrorMessage, message.Exception.Message); - } - - await _connectionTask; - _pair.Application.Output.Complete(); - } - - private async Task WaitForStreamErrorAsync(int expectedStreamId, Http2ErrorCode expectedErrorCode, string expectedErrorMessage) - { - var frame = await ReceiveFrameAsync(); - - Assert.Equal(Http2FrameType.RST_STREAM, frame.Type); - Assert.Equal(4, frame.Length); - Assert.Equal(0, frame.Flags); - Assert.Equal(expectedStreamId, frame.StreamId); - Assert.Equal(expectedErrorCode, frame.RstStreamErrorCode); - - if (expectedErrorMessage != null) - { - var message = Assert.Single(_logger.Messages, m => m.Exception is Http2StreamErrorException); - Assert.Contains(expectedErrorMessage, message.Exception.Message); - } - } - - private void VerifyDecodedRequestHeaders(IEnumerable> expectedHeaders) - { - foreach (var header in expectedHeaders) - { - Assert.True(_receivedHeaders.TryGetValue(header.Key, out var value), header.Key); - Assert.Equal(header.Value, value, ignoreCase: true); - } - } - public static TheoryData UpperCaseHeaderNameData { get diff --git a/test/Kestrel.Core.Tests/Http2StreamTests.cs b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs similarity index 64% rename from test/Kestrel.Core.Tests/Http2StreamTests.cs rename to test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs index 670aeb1c53..7e97ad2cc6 100644 --- a/test/Kestrel.Core.Tests/Http2StreamTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs @@ -2,218 +2,27 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Buffers; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; -using System.IO.Pipelines; using System.Linq; +using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; +using Moq; using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { - public class Http2StreamTests : IDisposable, IHttpHeadersHandler + public class Http2StreamTests : Http2TestBase { - private static readonly string _largeHeaderValue = new string('a', HPackDecoder.MaxStringOctets); - - private static readonly IEnumerable> _browserRequestHeaders = new[] - { - new KeyValuePair(HeaderNames.Method, "GET"), - new KeyValuePair(HeaderNames.Path, "/"), - new KeyValuePair(HeaderNames.Scheme, "http"), - new KeyValuePair(HeaderNames.Authority, "localhost:80"), - new KeyValuePair("user-agent", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:54.0) Gecko/20100101 Firefox/54.0"), - new KeyValuePair("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"), - new KeyValuePair("accept-language", "en-US,en;q=0.5"), - new KeyValuePair("accept-encoding", "gzip, deflate, br"), - new KeyValuePair("upgrade-insecure-requests", "1"), - }; - - private MemoryPool _memoryPool = KestrelMemoryPool.Create(); - private DuplexPipe.DuplexPipePair _pair; - private readonly TestApplicationErrorLogger _logger; - private Http2ConnectionContext _connectionContext; - private Http2Connection _connection; - private readonly Http2PeerSettings _clientSettings = new Http2PeerSettings(); - private readonly HPackEncoder _hpackEncoder = new HPackEncoder(); - private readonly HPackDecoder _hpackDecoder; - - private readonly ConcurrentDictionary> _runningStreams = new ConcurrentDictionary>(); - private readonly Dictionary _decodedHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - private readonly HashSet _abortedStreamIds = new HashSet(); - private readonly object _abortedStreamIdsLock = new object(); - - private readonly RequestDelegate _noopApplication; - private readonly RequestDelegate _echoMethod; - private readonly RequestDelegate _echoHost; - private readonly RequestDelegate _echoPath; - private readonly RequestDelegate _waitForAbortApplication; - private readonly RequestDelegate _waitForAbortFlushingApplication; - private readonly RequestDelegate _waitForAbortWithDataApplication; - - private Task _connectionTask; - - public Http2StreamTests() - { - _noopApplication = context => Task.CompletedTask; - - _echoMethod = context => - { - context.Response.Headers["Method"] = context.Request.Method; - - return Task.CompletedTask; - }; - - _echoHost = context => - { - context.Response.Headers[HeaderNames.Host] = context.Request.Headers[HeaderNames.Host]; - - return Task.CompletedTask; - }; - - _echoPath = context => - { - context.Response.Headers["path"] = context.Request.Path.ToString(); - context.Response.Headers["rawtarget"] = context.Features.Get().RawTarget; - - return Task.CompletedTask; - }; - - _waitForAbortApplication = async context => - { - var streamIdFeature = context.Features.Get(); - var sem = new SemaphoreSlim(0); - - context.RequestAborted.Register(() => - { - lock (_abortedStreamIdsLock) - { - _abortedStreamIds.Add(streamIdFeature.StreamId); - } - - sem.Release(); - }); - - await sem.WaitAsync().DefaultTimeout(); - - _runningStreams[streamIdFeature.StreamId].TrySetResult(null); - }; - - _waitForAbortFlushingApplication = async context => - { - var streamIdFeature = context.Features.Get(); - var sem = new SemaphoreSlim(0); - - context.RequestAborted.Register(() => - { - lock (_abortedStreamIdsLock) - { - _abortedStreamIds.Add(streamIdFeature.StreamId); - } - - sem.Release(); - }); - - await sem.WaitAsync().DefaultTimeout(); - - await context.Response.Body.FlushAsync(); - - _runningStreams[streamIdFeature.StreamId].TrySetResult(null); - }; - - _waitForAbortWithDataApplication = async context => - { - var streamIdFeature = context.Features.Get(); - var sem = new SemaphoreSlim(0); - - context.RequestAborted.Register(() => - { - lock (_abortedStreamIdsLock) - { - _abortedStreamIds.Add(streamIdFeature.StreamId); - } - - sem.Release(); - }); - - await sem.WaitAsync().DefaultTimeout(); - - await context.Response.Body.WriteAsync(new byte[10], 0, 10); - - _runningStreams[streamIdFeature.StreamId].TrySetResult(null); - }; - - _hpackDecoder = new HPackDecoder((int)_clientSettings.HeaderTableSize); - - _logger = new TestApplicationErrorLogger(); - - InitializeConnectionFields(KestrelMemoryPool.Create()); - } - - private void InitializeConnectionFields(MemoryPool memoryPool) - { - _memoryPool = memoryPool; - - // Always dispatch test code back to the ThreadPool. This prevents deadlocks caused by continuing - // Http2Connection.ProcessRequestsAsync() loop with writer locks acquired. Run product code inline to make - // it easier to verify request frames are processed correctly immediately after sending the them. - var inputPipeOptions = new PipeOptions( - pool: _memoryPool, - readerScheduler: PipeScheduler.Inline, - writerScheduler: PipeScheduler.ThreadPool, - useSynchronizationContext: false - ); - var outputPipeOptions = new PipeOptions( - pool: _memoryPool, - readerScheduler: PipeScheduler.ThreadPool, - writerScheduler: PipeScheduler.Inline, - useSynchronizationContext: false - ); - - _pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions); - - _connectionContext = new Http2ConnectionContext - { - ConnectionFeatures = new FeatureCollection(), - ServiceContext = new TestServiceContext() - { - Log = new TestKestrelTrace(_logger) - }, - MemoryPool = _memoryPool, - Application = _pair.Application, - Transport = _pair.Transport - }; - - _connection = new Http2Connection(_connectionContext); - } - - public void Dispose() - { - _pair.Application.Input.Complete(); - _pair.Application.Output.Complete(); - _pair.Transport.Input.Complete(); - _pair.Transport.Output.Complete(); - _memoryPool.Dispose(); - } - - void IHttpHeadersHandler.OnHeader(Span name, Span value) - { - _decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiStringNonNullCharacters(); - } - [Fact] public async Task HEADERS_Received_EmptyMethod_Reset() { @@ -936,6 +745,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public async Task ContentLength_Received_SingleDataFrameOverSize_Reset() { + IOException thrownEx = null; + var headers = new[] { new KeyValuePair(HeaderNames.Method, "POST"), @@ -945,7 +756,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests }; await InitializeConnectionAsync(async context => { - await Assert.ThrowsAsync(async () => + thrownEx = await Assert.ThrowsAsync(async () => { var buffer = new byte[100]; while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) > 0) { } @@ -958,11 +769,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await WaitForStreamErrorAsync(1, Http2ErrorCode.PROTOCOL_ERROR, CoreStrings.Http2StreamErrorMoreDataThanLength); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + var expectedError = new Http2StreamErrorException(1, CoreStrings.Http2StreamErrorMoreDataThanLength, Http2ErrorCode.PROTOCOL_ERROR); + + Assert.NotNull(thrownEx); + Assert.Equal(expectedError.Message, thrownEx.Message); + Assert.IsType(thrownEx.InnerException); } [Fact] public async Task ContentLength_Received_SingleDataFrameUnderSize_Reset() { + IOException thrownEx = null; + var headers = new[] { new KeyValuePair(HeaderNames.Method, "POST"), @@ -972,7 +791,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests }; await InitializeConnectionAsync(async context => { - await Assert.ThrowsAsync(async () => + thrownEx = await Assert.ThrowsAsync(async () => { var buffer = new byte[100]; while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) > 0) { } @@ -985,11 +804,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await WaitForStreamErrorAsync(1, Http2ErrorCode.PROTOCOL_ERROR, CoreStrings.Http2StreamErrorLessDataThanLength); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + var expectedError = new Http2StreamErrorException(1, CoreStrings.Http2StreamErrorLessDataThanLength, Http2ErrorCode.PROTOCOL_ERROR); + + Assert.NotNull(thrownEx); + Assert.Equal(expectedError.Message, thrownEx.Message); + Assert.IsType(thrownEx.InnerException); } [Fact] public async Task ContentLength_Received_MultipleDataFramesOverSize_Reset() { + IOException thrownEx = null; + var headers = new[] { new KeyValuePair(HeaderNames.Method, "POST"), @@ -999,7 +826,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests }; await InitializeConnectionAsync(async context => { - await Assert.ThrowsAsync(async () => + thrownEx = await Assert.ThrowsAsync(async () => { var buffer = new byte[100]; while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) > 0) { } @@ -1015,11 +842,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await WaitForStreamErrorAsync(1, Http2ErrorCode.PROTOCOL_ERROR, CoreStrings.Http2StreamErrorMoreDataThanLength); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + var expectedError = new Http2StreamErrorException(1, CoreStrings.Http2StreamErrorMoreDataThanLength, Http2ErrorCode.PROTOCOL_ERROR); + + Assert.NotNull(thrownEx); + Assert.Equal(expectedError.Message, thrownEx.Message); + Assert.IsType(thrownEx.InnerException); } [Fact] public async Task ContentLength_Received_MultipleDataFramesUnderSize_Reset() { + IOException thrownEx = null; + var headers = new[] { new KeyValuePair(HeaderNames.Method, "POST"), @@ -1029,7 +864,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests }; await InitializeConnectionAsync(async context => { - await Assert.ThrowsAsync(async () => + thrownEx = await Assert.ThrowsAsync(async () => { var buffer = new byte[100]; while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) > 0) { } @@ -1043,6 +878,520 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await WaitForStreamErrorAsync(1, Http2ErrorCode.PROTOCOL_ERROR, CoreStrings.Http2StreamErrorLessDataThanLength); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + var expectedError = new Http2StreamErrorException(1, CoreStrings.Http2StreamErrorLessDataThanLength, Http2ErrorCode.PROTOCOL_ERROR); + + Assert.NotNull(thrownEx); + Assert.Equal(expectedError.Message, thrownEx.Message); + Assert.IsType(thrownEx.InnerException); + } + + [Fact] + public async Task ContentLength_Response_FirstWriteMoreBytesWritten_Throws_Sends500() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + }; + await InitializeConnectionAsync(async context => + { + context.Response.ContentLength = 11; + await context.Response.WriteAsync("hello, world"); // 12 + }); + + await StartStreamAsync(1, headers, endStream: true); + + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 55, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: 0, + withFlags: (byte)Http2DataFrameFlags.END_STREAM, + withStreamId: 1); + + Assert.Contains(TestApplicationErrorLogger.Messages, m => m.Exception?.Message.Contains("Response Content-Length mismatch: too many bytes written (12 of 11).") ?? false); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + + Assert.Equal(3, _decodedHeaders.Count); + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("500", _decodedHeaders[HeaderNames.Status]); + Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]); + } + + [Fact] + public async Task ContentLength_Response_MoreBytesWritten_ThrowsAndResetsStream() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + }; + await InitializeConnectionAsync(async context => + { + context.Response.ContentLength = 11; + await context.Response.WriteAsync("hello,"); + await context.Response.WriteAsync(" world"); + }); + + await StartStreamAsync(1, headers, endStream: true); + + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 56, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: 6, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 1); + + await WaitForStreamErrorAsync(1, Http2ErrorCode.INTERNAL_ERROR, "Response Content-Length mismatch: too many bytes written (12 of 11)."); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + + Assert.Equal(3, _decodedHeaders.Count); + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", _decodedHeaders[HeaderNames.Status]); + Assert.Equal("11", _decodedHeaders[HeaderNames.ContentLength]); + } + + [Fact] + public async Task ContentLength_Response_NoBytesWritten_Sends500() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + }; + await InitializeConnectionAsync(context => + { + context.Response.ContentLength = 11; + return Task.CompletedTask; + }); + + await StartStreamAsync(1, headers, endStream: true); + + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 55, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: 0, + withFlags: (byte)Http2DataFrameFlags.END_STREAM, + withStreamId: 1); + + Assert.Contains(TestApplicationErrorLogger.Messages, m => m.Exception?.Message.Contains("Response Content-Length mismatch: too few bytes written (0 of 11).") ?? false); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + + Assert.Equal(3, _decodedHeaders.Count); + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("500", _decodedHeaders[HeaderNames.Status]); + Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]); + } + + [Fact] + public async Task ContentLength_Response_TooFewBytesWritten_Resets() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + }; + await InitializeConnectionAsync(context => + { + context.Response.ContentLength = 11; + return context.Response.WriteAsync("hello,"); + }); + + await StartStreamAsync(1, headers, endStream: true); + + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 56, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: 6, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 1); + + await WaitForStreamErrorAsync(1, Http2ErrorCode.INTERNAL_ERROR, "Response Content-Length mismatch: too few bytes written (6 of 11)."); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + + Assert.Equal(3, _decodedHeaders.Count); + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", _decodedHeaders[HeaderNames.Status]); + Assert.Equal("11", _decodedHeaders[HeaderNames.ContentLength]); + } + + [Fact] + public async Task MaxRequestBodySize_ContentLengthUnder_200() + { + _connectionContext.ServiceContext.ServerOptions.Limits.MaxRequestBodySize = 15; + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "POST"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.ContentLength, "12"), + }; + await InitializeConnectionAsync(async context => + { + var buffer = new byte[100]; + var read = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length); + Assert.Equal(12, read); + }); + + await StartStreamAsync(1, headers, endStream: false); + await SendDataAsync(1, new byte[12].AsSpan(), endStream: true); + + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 55, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: 0, + withFlags: (byte)Http2DataFrameFlags.END_STREAM, + withStreamId: 1); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + + Assert.Equal(3, _decodedHeaders.Count); + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", _decodedHeaders[HeaderNames.Status]); + Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]); + } + + [Fact] + public async Task MaxRequestBodySize_ContentLengthOver_413() + { + BadHttpRequestException exception = null; + _connectionContext.ServiceContext.ServerOptions.Limits.MaxRequestBodySize = 10; + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "POST"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.ContentLength, "12"), + }; + await InitializeConnectionAsync(async context => + { + exception = await Assert.ThrowsAsync(async () => + { + var buffer = new byte[100]; + while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) > 0) { } + }); + ExceptionDispatchInfo.Capture(exception).Throw(); + }); + + await StartStreamAsync(1, headers, endStream: false); + + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 59, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: 0, + withFlags: (byte)Http2DataFrameFlags.END_STREAM, + withStreamId: 1); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + + Assert.Equal(3, _decodedHeaders.Count); + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("413", _decodedHeaders[HeaderNames.Status]); + Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]); + + Assert.NotNull(exception); + } + + [Fact] + public async Task MaxRequestBodySize_NoContentLength_Under_200() + { + _connectionContext.ServiceContext.ServerOptions.Limits.MaxRequestBodySize = 15; + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "POST"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + }; + await InitializeConnectionAsync(async context => + { + var buffer = new byte[100]; + var read = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length); + Assert.Equal(12, read); + }); + + await StartStreamAsync(1, headers, endStream: false); + await SendDataAsync(1, new byte[12].AsSpan(), endStream: true); + + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 55, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: 0, + withFlags: (byte)Http2DataFrameFlags.END_STREAM, + withStreamId: 1); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + + Assert.Equal(3, _decodedHeaders.Count); + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", _decodedHeaders[HeaderNames.Status]); + Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]); + } + + [Fact] + public async Task MaxRequestBodySize_NoContentLength_Over_413() + { + BadHttpRequestException exception = null; + _connectionContext.ServiceContext.ServerOptions.Limits.MaxRequestBodySize = 10; + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "POST"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + }; + await InitializeConnectionAsync(async context => + { + exception = await Assert.ThrowsAsync(async () => + { + var buffer = new byte[100]; + while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) > 0) { } + }); + ExceptionDispatchInfo.Capture(exception).Throw(); + }); + + await StartStreamAsync(1, headers, endStream: false); + await SendDataAsync(1, new byte[6].AsSpan(), endStream: false); + await SendDataAsync(1, new byte[6].AsSpan(), endStream: false); + await SendDataAsync(1, new byte[6].AsSpan(), endStream: true); + + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 59, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: 0, + withFlags: (byte)Http2DataFrameFlags.END_STREAM, + withStreamId: 1); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + + Assert.Equal(3, _decodedHeaders.Count); + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("413", _decodedHeaders[HeaderNames.Status]); + Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]); + + Assert.NotNull(exception); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task MaxRequestBodySize_AppCanLowerLimit(bool includeContentLength) + { + BadHttpRequestException exception = null; + _connectionContext.ServiceContext.ServerOptions.Limits.MaxRequestBodySize = 20; + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "POST"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + }; + if (includeContentLength) + { + headers.Concat(new[] + { + new KeyValuePair(HeaderNames.ContentLength, "18"), + }); + } + await InitializeConnectionAsync(async context => + { + Assert.False(context.Features.Get().IsReadOnly); + context.Features.Get().MaxRequestBodySize = 17; + exception = await Assert.ThrowsAsync(async () => + { + var buffer = new byte[100]; + while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) > 0) { } + }); + Assert.True(context.Features.Get().IsReadOnly); + ExceptionDispatchInfo.Capture(exception).Throw(); + }); + + await StartStreamAsync(1, headers, endStream: false); + await SendDataAsync(1, new byte[6].AsSpan(), endStream: false); + await SendDataAsync(1, new byte[6].AsSpan(), endStream: false); + await SendDataAsync(1, new byte[6].AsSpan(), endStream: true); + + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 59, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: 0, + withFlags: (byte)Http2DataFrameFlags.END_STREAM, + withStreamId: 1); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + + Assert.Equal(3, _decodedHeaders.Count); + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("413", _decodedHeaders[HeaderNames.Status]); + Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]); + + Assert.NotNull(exception); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task MaxRequestBodySize_AppCanRaiseLimit(bool includeContentLength) + { + _connectionContext.ServiceContext.ServerOptions.Limits.MaxRequestBodySize = 10; + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "POST"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + }; + if (includeContentLength) + { + headers.Concat(new[] + { + new KeyValuePair(HeaderNames.ContentLength, "12"), + }); + } + await InitializeConnectionAsync(async context => + { + Assert.False(context.Features.Get().IsReadOnly); + context.Features.Get().MaxRequestBodySize = 12; + var buffer = new byte[100]; + var read = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length); + Assert.Equal(12, read); + Assert.True(context.Features.Get().IsReadOnly); + }); + + await StartStreamAsync(1, headers, endStream: false); + await SendDataAsync(1, new byte[12].AsSpan(), endStream: true); + + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 55, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: 0, + withFlags: (byte)Http2DataFrameFlags.END_STREAM, + withStreamId: 1); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + + Assert.Equal(3, _decodedHeaders.Count); + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", _decodedHeaders[HeaderNames.Status]); + Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]); + } + + [Fact] + public async Task ApplicationExeption_BeforeFirstWrite_Sends500() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + }; + await InitializeConnectionAsync(context => + { + throw new Exception("App Faulted"); + }); + + await StartStreamAsync(1, headers, endStream: true); + + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 55, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: 0, + withFlags: (byte)Http2DataFrameFlags.END_STREAM, + withStreamId: 1); + + Assert.Contains(TestApplicationErrorLogger.Messages, m => (m.Exception?.Message.Contains("App Faulted") ?? false) && m.LogLevel == LogLevel.Error); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + + Assert.Equal(3, _decodedHeaders.Count); + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("500", _decodedHeaders[HeaderNames.Status]); + Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]); + } + + [Fact] + public async Task ApplicationExeption_AfterFirstWrite_Resets() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + }; + await InitializeConnectionAsync(async context => + { + await context.Response.WriteAsync("hello,"); + throw new Exception("App Faulted"); + }); + + await StartStreamAsync(1, headers, endStream: true); + + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 37, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: 6, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 1); + + await WaitForStreamErrorAsync(1, Http2ErrorCode.INTERNAL_ERROR, "App Faulted"); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + + Assert.Equal(2, _decodedHeaders.Count); + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", _decodedHeaders[HeaderNames.Status]); } [Fact] @@ -1262,219 +1611,60 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); } - private async Task InitializeConnectionAsync(RequestDelegate application) + [Fact] + public async Task RequestAbort_ThrowsOperationCanceledExceptionFromSubsequentRequestBodyStreamRead() { - _connectionTask = _connection.ProcessRequestsAsync(new DummyApplication(application)); + OperationCanceledException thrownEx = null; - await SendPreambleAsync().ConfigureAwait(false); - await SendSettingsAsync(); - - await ExpectAsync(Http2FrameType.SETTINGS, - withLength: 0, - withFlags: 0, - withStreamId: 0); - - await ExpectAsync(Http2FrameType.SETTINGS, - withLength: 0, - withFlags: (byte)Http2SettingsFrameFlags.ACK, - withStreamId: 0); - } - - private async Task StartStreamAsync(int streamId, IEnumerable> headers, bool endStream) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - _runningStreams[streamId] = tcs; - - var frame = new Http2Frame(); - frame.PrepareHeaders(Http2HeadersFrameFlags.NONE, streamId); - var done = _hpackEncoder.BeginEncode(headers, frame.HeadersPayload, out var length); - frame.Length = length; - - if (done) + await InitializeConnectionAsync(async context => { - frame.HeadersFlags = Http2HeadersFrameFlags.END_HEADERS; - } + context.Abort(); - if (endStream) + var buffer = new byte[100]; + var thrownExTask = Assert.ThrowsAnyAsync(() => context.Request.Body.ReadAsync(buffer, 0, buffer.Length)); + + Assert.True(thrownExTask.IsCompleted); + + thrownEx = await thrownExTask; + }); + + await StartStreamAsync(1, _browserRequestHeaders, endStream: false); + await WaitForStreamErrorAsync(expectedStreamId: 1, Http2ErrorCode.INTERNAL_ERROR, CoreStrings.ConnectionAbortedByApplication); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + Assert.NotNull(thrownEx); + Assert.IsType(thrownEx); + Assert.Equal(CoreStrings.ConnectionAbortedByApplication, thrownEx.Message); + } + + [Fact] + public async Task RequestAbort_ThrowsOperationCanceledExceptionFromOngoingRequestBodyStreamRead() + { + OperationCanceledException thrownEx = null; + + await InitializeConnectionAsync(async context => { - frame.HeadersFlags |= Http2HeadersFrameFlags.END_STREAM; - } + var buffer = new byte[100]; + var thrownExTask = Assert.ThrowsAnyAsync(() => context.Request.Body.ReadAsync(buffer, 0, buffer.Length)); - await SendAsync(frame.Raw); + Assert.False(thrownExTask.IsCompleted); - while (!done) - { - frame.PrepareContinuation(Http2ContinuationFrameFlags.NONE, streamId); - done = _hpackEncoder.Encode(frame.HeadersPayload, out length); - frame.Length = length; + context.Abort(); - if (done) - { - frame.ContinuationFlags = Http2ContinuationFrameFlags.END_HEADERS; - } + thrownEx = await thrownExTask.DefaultTimeout(); + }); - await SendAsync(frame.Raw); - } - } + await StartStreamAsync(1, _browserRequestHeaders, endStream: false); + await WaitForStreamErrorAsync(expectedStreamId: 1, Http2ErrorCode.INTERNAL_ERROR, CoreStrings.ConnectionAbortedByApplication); - private Task WaitForAllStreamsAsync() - { - return Task.WhenAll(_runningStreams.Values.Select(tcs => tcs.Task)).DefaultTimeout(); - } + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - private Task SendAsync(ReadOnlySpan span) - { - var writableBuffer = _pair.Application.Output; - writableBuffer.Write(span); - return FlushAsync(writableBuffer); - } - - private static async Task FlushAsync(PipeWriter writableBuffer) - { - await writableBuffer.FlushAsync(); - } - - private Task SendPreambleAsync() => SendAsync(new ArraySegment(Http2Connection.ClientPreface)); - - private Task SendSettingsAsync() - { - var frame = new Http2Frame(); - frame.PrepareSettings(Http2SettingsFrameFlags.NONE, _clientSettings); - return SendAsync(frame.Raw); - } - - private async Task SendHeadersAsync(int streamId, Http2HeadersFrameFlags flags, IEnumerable> headers) - { - var frame = new Http2Frame(); - - frame.PrepareHeaders(flags, streamId); - var done = _hpackEncoder.BeginEncode(headers, frame.Payload, out var length); - frame.Length = length; - - await SendAsync(frame.Raw); - - return done; - } - - private Task SendDataAsync(int streamId, Span data, bool endStream) - { - var frame = new Http2Frame(); - - frame.PrepareData(streamId); - frame.Length = data.Length; - frame.DataFlags = endStream ? Http2DataFrameFlags.END_STREAM : Http2DataFrameFlags.NONE; - data.CopyTo(frame.DataPayload); - - return SendAsync(frame.Raw); - } - - private Task SendRstStreamAsync(int streamId) - { - var rstStreamFrame = new Http2Frame(); - rstStreamFrame.PrepareRstStream(streamId, Http2ErrorCode.CANCEL); - return SendAsync(rstStreamFrame.Raw); - } - - private async Task ReceiveFrameAsync() - { - var frame = new Http2Frame(); - - while (true) - { - var result = await _pair.Application.Input.ReadAsync(); - var buffer = result.Buffer; - var consumed = buffer.Start; - var examined = buffer.End; - - try - { - Assert.True(buffer.Length > 0); - - if (Http2FrameReader.ReadFrame(buffer, frame, 16_384, out consumed, out examined)) - { - return frame; - } - - if (result.IsCompleted) - { - throw new IOException("The reader completed without returning a frame."); - } - } - finally - { - _pair.Application.Input.AdvanceTo(consumed, examined); - } - } - } - - private async Task ExpectAsync(Http2FrameType type, int withLength, byte withFlags, int withStreamId) - { - var frame = await ReceiveFrameAsync(); - - Assert.Equal(type, frame.Type); - Assert.Equal(withLength, frame.Length); - Assert.Equal(withFlags, frame.Flags); - Assert.Equal(withStreamId, frame.StreamId); - - return frame; - } - - private Task StopConnectionAsync(int expectedLastStreamId, bool ignoreNonGoAwayFrames) - { - _pair.Application.Output.Complete(); - - return WaitForConnectionStopAsync(expectedLastStreamId, ignoreNonGoAwayFrames); - } - - private Task WaitForConnectionStopAsync(int expectedLastStreamId, bool ignoreNonGoAwayFrames) - { - return WaitForConnectionErrorAsync(ignoreNonGoAwayFrames, expectedLastStreamId, Http2ErrorCode.NO_ERROR, expectedErrorMessage: null); - } - - private async Task WaitForConnectionErrorAsync(bool ignoreNonGoAwayFrames, int expectedLastStreamId, Http2ErrorCode expectedErrorCode, string expectedErrorMessage) - where TException : Exception - { - var frame = await ReceiveFrameAsync(); - - if (ignoreNonGoAwayFrames) - { - while (frame.Type != Http2FrameType.GOAWAY) - { - frame = await ReceiveFrameAsync(); - } - } - - Assert.Equal(Http2FrameType.GOAWAY, frame.Type); - Assert.Equal(8, frame.Length); - Assert.Equal(0, frame.Flags); - Assert.Equal(0, frame.StreamId); - Assert.Equal(expectedLastStreamId, frame.GoAwayLastStreamId); - Assert.Equal(expectedErrorCode, frame.GoAwayErrorCode); - - if (expectedErrorMessage != null) - { - var message = Assert.Single(_logger.Messages, m => m.Exception is TException); - Assert.Contains(expectedErrorMessage, message.Exception.Message); - } - - await _connectionTask; - _pair.Application.Output.Complete(); - } - - private async Task WaitForStreamErrorAsync(int expectedStreamId, Http2ErrorCode expectedErrorCode, string expectedErrorMessage) - { - var frame = await ReceiveFrameAsync(); - - Assert.Equal(Http2FrameType.RST_STREAM, frame.Type); - Assert.Equal(4, frame.Length); - Assert.Equal(0, frame.Flags); - Assert.Equal(expectedStreamId, frame.StreamId); - Assert.Equal(expectedErrorCode, frame.RstStreamErrorCode); - - if (expectedErrorMessage != null) - { - Assert.Contains(_logger.Messages, m => m.Exception?.Message.Contains(expectedErrorMessage) ?? false); - } + Assert.NotNull(thrownEx); + Assert.IsType(thrownEx); + Assert.Equal("The request was aborted", thrownEx.Message); + Assert.IsType(thrownEx.InnerException); + Assert.Equal(CoreStrings.ConnectionAbortedByApplication, thrownEx.InnerException.Message); } } } \ No newline at end of file diff --git a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2TestBase.cs b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2TestBase.cs new file mode 100644 index 0000000000..ec588826c2 --- /dev/null +++ b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -0,0 +1,868 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipelines; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; +using Microsoft.AspNetCore.Testing; +using Microsoft.AspNetCore.Connections; +using Microsoft.Net.Http.Headers; +using Moq; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests +{ + public class Http2TestBase : TestApplicationErrorLoggerLoggedTest, IDisposable, IHttpHeadersHandler + { + protected static readonly string _largeHeaderValue = new string('a', HPackDecoder.MaxStringOctets); + + protected static readonly IEnumerable> _browserRequestHeaders = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + new KeyValuePair("user-agent", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:54.0) Gecko/20100101 Firefox/54.0"), + new KeyValuePair("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"), + new KeyValuePair("accept-language", "en-US,en;q=0.5"), + new KeyValuePair("accept-encoding", "gzip, deflate, br"), + new KeyValuePair("upgrade-insecure-requests", "1"), + }; + + private readonly MemoryPool _memoryPool = KestrelMemoryPool.Create(); + internal readonly DuplexPipe.DuplexPipePair _pair; + + protected readonly Http2PeerSettings _clientSettings = new Http2PeerSettings(); + protected readonly HPackEncoder _hpackEncoder = new HPackEncoder(); + protected readonly HPackDecoder _hpackDecoder; + + protected readonly ConcurrentDictionary> _runningStreams = new ConcurrentDictionary>(); + protected readonly Dictionary _receivedHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); + protected readonly Dictionary _decodedHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); + protected readonly HashSet _abortedStreamIds = new HashSet(); + protected readonly object _abortedStreamIdsLock = new object(); + protected readonly TaskCompletionSource _closingStateReached = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + protected readonly TaskCompletionSource _closedStateReached = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + protected readonly RequestDelegate _noopApplication; + protected readonly RequestDelegate _readHeadersApplication; + protected readonly RequestDelegate _readTrailersApplication; + protected readonly RequestDelegate _bufferingApplication; + protected readonly RequestDelegate _echoApplication; + protected readonly RequestDelegate _echoWaitForAbortApplication; + protected readonly RequestDelegate _largeHeadersApplication; + protected readonly RequestDelegate _waitForAbortApplication; + protected readonly RequestDelegate _waitForAbortFlushingApplication; + protected readonly RequestDelegate _waitForAbortWithDataApplication; + protected readonly RequestDelegate _echoMethod; + protected readonly RequestDelegate _echoHost; + protected readonly RequestDelegate _echoPath; + + protected Http2ConnectionContext _connectionContext; + protected Http2Connection _connection; + protected Task _connectionTask; + + public Http2TestBase() + { + // Always dispatch test code back to the ThreadPool. This prevents deadlocks caused by continuing + // Http2Connection.ProcessRequestsAsync() loop with writer locks acquired. Run product code inline to make + // it easier to verify request frames are processed correctly immediately after sending the them. + var inputPipeOptions = new PipeOptions( + pool: _memoryPool, + readerScheduler: PipeScheduler.Inline, + writerScheduler: PipeScheduler.ThreadPool, + useSynchronizationContext: false + ); + var outputPipeOptions = new PipeOptions( + pool: _memoryPool, + readerScheduler: PipeScheduler.ThreadPool, + writerScheduler: PipeScheduler.Inline, + useSynchronizationContext: false + ); + + _pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions); + _hpackDecoder = new HPackDecoder((int)_clientSettings.HeaderTableSize); + + _noopApplication = context => Task.CompletedTask; + + _readHeadersApplication = context => + { + foreach (var header in context.Request.Headers) + { + _receivedHeaders[header.Key] = header.Value.ToString(); + } + + return Task.CompletedTask; + }; + + _readTrailersApplication = async context => + { + using (var ms = new MemoryStream()) + { + // Consuming the entire request body guarantees trailers will be available + await context.Request.Body.CopyToAsync(ms); + } + + foreach (var header in context.Request.Headers) + { + _receivedHeaders[header.Key] = header.Value.ToString(); + } + }; + + _bufferingApplication = async context => + { + var data = new List(); + var buffer = new byte[1024]; + var received = 0; + + while ((received = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + data.AddRange(new ArraySegment(buffer, 0, received)); + } + + await context.Response.Body.WriteAsync(data.ToArray(), 0, data.Count); + }; + + _echoApplication = async context => + { + var buffer = new byte[Http2Frame.MinAllowedMaxFrameSize]; + var received = 0; + + while ((received = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + await context.Response.Body.WriteAsync(buffer, 0, received); + } + }; + + _echoWaitForAbortApplication = async context => + { + var buffer = new byte[Http2Frame.MinAllowedMaxFrameSize]; + var received = 0; + + while ((received = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + await context.Response.Body.WriteAsync(buffer, 0, received); + } + + var sem = new SemaphoreSlim(0); + + context.RequestAborted.Register(() => + { + sem.Release(); + }); + + await sem.WaitAsync().DefaultTimeout(); + }; + + _largeHeadersApplication = context => + { + foreach (var name in new[] { "a", "b", "c", "d", "e", "f", "g", "h" }) + { + context.Response.Headers[name] = _largeHeaderValue; + } + + return Task.CompletedTask; + }; + + _waitForAbortApplication = async context => + { + var streamIdFeature = context.Features.Get(); + var sem = new SemaphoreSlim(0); + + context.RequestAborted.Register(() => + { + lock (_abortedStreamIdsLock) + { + _abortedStreamIds.Add(streamIdFeature.StreamId); + } + + sem.Release(); + }); + + await sem.WaitAsync().DefaultTimeout(); + + _runningStreams[streamIdFeature.StreamId].TrySetResult(null); + }; + + _waitForAbortFlushingApplication = async context => + { + var streamIdFeature = context.Features.Get(); + var sem = new SemaphoreSlim(0); + + context.RequestAborted.Register(() => + { + lock (_abortedStreamIdsLock) + { + _abortedStreamIds.Add(streamIdFeature.StreamId); + } + + sem.Release(); + }); + + await sem.WaitAsync().DefaultTimeout(); + + await context.Response.Body.FlushAsync(); + + _runningStreams[streamIdFeature.StreamId].TrySetResult(null); + }; + + _waitForAbortWithDataApplication = async context => + { + var streamIdFeature = context.Features.Get(); + var sem = new SemaphoreSlim(0); + + context.RequestAborted.Register(() => + { + lock (_abortedStreamIdsLock) + { + _abortedStreamIds.Add(streamIdFeature.StreamId); + } + + sem.Release(); + }); + + await sem.WaitAsync().DefaultTimeout(); + + await context.Response.Body.WriteAsync(new byte[10], 0, 10); + + _runningStreams[streamIdFeature.StreamId].TrySetResult(null); + }; + + _echoMethod = context => + { + context.Response.Headers["Method"] = context.Request.Method; + + return Task.CompletedTask; + }; + + _echoHost = context => + { + context.Response.Headers[HeaderNames.Host] = context.Request.Headers[HeaderNames.Host]; + + return Task.CompletedTask; + }; + + _echoPath = context => + { + context.Response.Headers["path"] = context.Request.Path.ToString(); + context.Response.Headers["rawtarget"] = context.Features.Get().RawTarget; + + return Task.CompletedTask; + }; + } + + public override void Initialize(MethodInfo methodInfo, object[] testMethodArguments, ITestOutputHelper testOutputHelper) + { + base.Initialize(methodInfo, testMethodArguments, testOutputHelper); + + var mockKestrelTrace = new Mock(); + mockKestrelTrace + .Setup(m => m.Http2ConnectionClosing(It.IsAny())) + .Callback(() => _closingStateReached.SetResult(null)); + mockKestrelTrace + .Setup(m => m.Http2ConnectionClosed(It.IsAny(), It.IsAny())) + .Callback(() => _closedStateReached.SetResult(null)); + + _connectionContext = new Http2ConnectionContext + { + ConnectionContext = Mock.Of(), + ConnectionFeatures = new FeatureCollection(), + ServiceContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object), + MemoryPool = _memoryPool, + Transport = _pair.Transport + }; + + _connection = new Http2Connection(_connectionContext); + } + + public override void Dispose() + { + _pair.Application.Input.Complete(); + _pair.Application.Output.Complete(); + _pair.Transport.Input.Complete(); + _pair.Transport.Output.Complete(); + _memoryPool.Dispose(); + + base.Dispose(); + } + + void IHttpHeadersHandler.OnHeader(Span name, Span value) + { + _decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiStringNonNullCharacters(); + } + + protected async Task InitializeConnectionAsync(RequestDelegate application) + { + _connectionTask = _connection.ProcessRequestsAsync(new DummyApplication(application)); + + await SendPreambleAsync().ConfigureAwait(false); + await SendSettingsAsync(); + + await ExpectAsync(Http2FrameType.SETTINGS, + withLength: 6, + withFlags: 0, + withStreamId: 0); + + await ExpectAsync(Http2FrameType.SETTINGS, + withLength: 0, + withFlags: (byte)Http2SettingsFrameFlags.ACK, + withStreamId: 0); + } + + protected async Task StartStreamAsync(int streamId, IEnumerable> headers, bool endStream) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _runningStreams[streamId] = tcs; + + var frame = new Http2Frame(); + frame.PrepareHeaders(Http2HeadersFrameFlags.NONE, streamId); + var done = _hpackEncoder.BeginEncode(headers, frame.HeadersPayload, out var length); + frame.Length = length; + + if (done) + { + frame.HeadersFlags = Http2HeadersFrameFlags.END_HEADERS; + } + + if (endStream) + { + frame.HeadersFlags |= Http2HeadersFrameFlags.END_STREAM; + } + + await SendAsync(frame.Raw); + + while (!done) + { + frame.PrepareContinuation(Http2ContinuationFrameFlags.NONE, streamId); + done = _hpackEncoder.Encode(frame.HeadersPayload, out length); + frame.Length = length; + + if (done) + { + frame.ContinuationFlags = Http2ContinuationFrameFlags.END_HEADERS; + } + + await SendAsync(frame.Raw); + } + } + + protected async Task SendHeadersWithPaddingAsync(int streamId, IEnumerable> headers, byte padLength, bool endStream) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _runningStreams[streamId] = tcs; + + var frame = new Http2Frame(); + + frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.PADDED, streamId); + frame.HeadersPadLength = padLength; + + _hpackEncoder.BeginEncode(headers, frame.HeadersPayload, out var length); + + frame.Length = 1 + length + padLength; + frame.Payload.Slice(1 + length).Fill(0); + + if (endStream) + { + frame.HeadersFlags |= Http2HeadersFrameFlags.END_STREAM; + } + + await SendAsync(frame.Raw); + } + + protected async Task SendHeadersWithPriorityAsync(int streamId, IEnumerable> headers, byte priority, int streamDependency, bool endStream) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _runningStreams[streamId] = tcs; + + var frame = new Http2Frame(); + frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.PRIORITY, streamId); + frame.HeadersPriority = priority; + frame.HeadersStreamDependency = streamDependency; + + _hpackEncoder.BeginEncode(headers, frame.HeadersPayload, out var length); + + frame.Length = 5 + length; + + if (endStream) + { + frame.HeadersFlags |= Http2HeadersFrameFlags.END_STREAM; + } + + await SendAsync(frame.Raw); + } + + protected async Task SendHeadersWithPaddingAndPriorityAsync(int streamId, IEnumerable> headers, byte padLength, byte priority, int streamDependency, bool endStream) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _runningStreams[streamId] = tcs; + + var frame = new Http2Frame(); + frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.PADDED | Http2HeadersFrameFlags.PRIORITY, streamId); + frame.HeadersPadLength = padLength; + frame.HeadersPriority = priority; + frame.HeadersStreamDependency = streamDependency; + + _hpackEncoder.BeginEncode(headers, frame.HeadersPayload, out var length); + + frame.Length = 6 + length + padLength; + frame.Payload.Slice(6 + length).Fill(0); + + if (endStream) + { + frame.HeadersFlags |= Http2HeadersFrameFlags.END_STREAM; + } + + await SendAsync(frame.Raw); + } + + protected Task WaitForAllStreamsAsync() + { + return Task.WhenAll(_runningStreams.Values.Select(tcs => tcs.Task)).DefaultTimeout(); + } + + protected Task SendAsync(ReadOnlySpan span) + { + var writableBuffer = _pair.Application.Output; + writableBuffer.Write(span); + return FlushAsync(writableBuffer); + } + + protected static async Task FlushAsync(PipeWriter writableBuffer) + { + await writableBuffer.FlushAsync(); + } + + protected Task SendPreambleAsync() => SendAsync(new ArraySegment(Http2Connection.ClientPreface)); + + protected Task SendSettingsAsync() + { + var frame = new Http2Frame(); + frame.PrepareSettings(Http2SettingsFrameFlags.NONE, _clientSettings.GetNonProtocolDefaults()); + return SendAsync(frame.Raw); + } + + protected Task SendSettingsAckWithInvalidLengthAsync(int length) + { + var frame = new Http2Frame(); + frame.PrepareSettings(Http2SettingsFrameFlags.ACK); + frame.Length = length; + return SendAsync(frame.Raw); + } + + protected Task SendSettingsWithInvalidStreamIdAsync(int streamId) + { + var frame = new Http2Frame(); + frame.PrepareSettings(Http2SettingsFrameFlags.NONE, _clientSettings.GetNonProtocolDefaults()); + frame.StreamId = streamId; + return SendAsync(frame.Raw); + } + + protected Task SendSettingsWithInvalidLengthAsync(int length) + { + var frame = new Http2Frame(); + frame.PrepareSettings(Http2SettingsFrameFlags.NONE, _clientSettings.GetNonProtocolDefaults()); + frame.Length = length; + return SendAsync(frame.Raw); + } + + protected Task SendSettingsWithInvalidParameterValueAsync(Http2SettingsParameter parameter, uint value) + { + var frame = new Http2Frame(); + frame.PrepareSettings(Http2SettingsFrameFlags.NONE); + frame.Length = 6; + + frame.Payload[0] = (byte)((ushort)parameter >> 8); + frame.Payload[1] = (byte)(ushort)parameter; + frame.Payload[2] = (byte)(value >> 24); + frame.Payload[3] = (byte)(value >> 16); + frame.Payload[4] = (byte)(value >> 8); + frame.Payload[5] = (byte)value; + + return SendAsync(frame.Raw); + } + + protected Task SendPushPromiseFrameAsync() + { + var frame = new Http2Frame(); + frame.Length = 0; + frame.Type = Http2FrameType.PUSH_PROMISE; + frame.StreamId = 1; + return SendAsync(frame.Raw); + } + + protected async Task SendHeadersAsync(int streamId, Http2HeadersFrameFlags flags, IEnumerable> headers) + { + var frame = new Http2Frame(); + + frame.PrepareHeaders(flags, streamId); + var done = _hpackEncoder.BeginEncode(headers, frame.Payload, out var length); + frame.Length = length; + + await SendAsync(frame.Raw); + + return done; + } + + protected Task SendHeadersAsync(int streamId, Http2HeadersFrameFlags flags, byte[] headerBlock) + { + var frame = new Http2Frame(); + + frame.PrepareHeaders(flags, streamId); + frame.Length = headerBlock.Length; + headerBlock.CopyTo(frame.HeadersPayload); + + return SendAsync(frame.Raw); + } + + protected Task SendInvalidHeadersFrameAsync(int streamId, int frameLength, byte padLength) + { + Assert.True(padLength >= frameLength, $"{nameof(padLength)} must be greater than or equal to {nameof(frameLength)} to create an invalid frame."); + + var frame = new Http2Frame(); + + frame.PrepareHeaders(Http2HeadersFrameFlags.PADDED, streamId); + frame.Payload[0] = padLength; + + // Set length last so .Payload can be written to + frame.Length = frameLength; + + return SendAsync(frame.Raw); + } + + protected Task SendIncompleteHeadersFrameAsync(int streamId) + { + var frame = new Http2Frame(); + + frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS, streamId); + frame.Length = 3; + + // Set up an incomplete Literal Header Field w/ Incremental Indexing frame, + // with an incomplete new name + frame.Payload[0] = 0; + frame.Payload[1] = 2; + frame.Payload[2] = (byte)'a'; + + return SendAsync(frame.Raw); + } + + protected async Task SendContinuationAsync(int streamId, Http2ContinuationFrameFlags flags) + { + var frame = new Http2Frame(); + + frame.PrepareContinuation(flags, streamId); + var done = _hpackEncoder.Encode(frame.Payload, out var length); + frame.Length = length; + + await SendAsync(frame.Raw); + + return done; + } + + protected async Task SendContinuationAsync(int streamId, Http2ContinuationFrameFlags flags, byte[] payload) + { + var frame = new Http2Frame(); + + frame.PrepareContinuation(flags, streamId); + frame.Length = payload.Length; + payload.CopyTo(frame.Payload); + + await SendAsync(frame.Raw); + } + + protected Task SendEmptyContinuationFrameAsync(int streamId, Http2ContinuationFrameFlags flags) + { + var frame = new Http2Frame(); + + frame.PrepareContinuation(flags, streamId); + frame.Length = 0; + + return SendAsync(frame.Raw); + } + + protected Task SendIncompleteContinuationFrameAsync(int streamId) + { + var frame = new Http2Frame(); + + frame.PrepareContinuation(Http2ContinuationFrameFlags.END_HEADERS, streamId); + frame.Length = 3; + + // Set up an incomplete Literal Header Field w/ Incremental Indexing frame, + // with an incomplete new name + frame.Payload[0] = 0; + frame.Payload[1] = 2; + frame.Payload[2] = (byte)'a'; + + return SendAsync(frame.Raw); + } + + protected Task SendDataAsync(int streamId, Span data, bool endStream) + { + var frame = new Http2Frame(); + + frame.PrepareData(streamId); + frame.Length = data.Length; + frame.DataFlags = endStream ? Http2DataFrameFlags.END_STREAM : Http2DataFrameFlags.NONE; + data.CopyTo(frame.DataPayload); + + return SendAsync(frame.Raw); + } + + protected Task SendDataWithPaddingAsync(int streamId, Span data, byte padLength, bool endStream) + { + var frame = new Http2Frame(); + + frame.PrepareData(streamId, padLength); + frame.Length = data.Length + 1 + padLength; + data.CopyTo(frame.DataPayload); + + if (endStream) + { + frame.DataFlags |= Http2DataFrameFlags.END_STREAM; + } + + return SendAsync(frame.Raw); + } + + protected Task SendInvalidDataFrameAsync(int streamId, int frameLength, byte padLength) + { + Assert.True(padLength >= frameLength, $"{nameof(padLength)} must be greater than or equal to {nameof(frameLength)} to create an invalid frame."); + + var frame = new Http2Frame(); + + frame.PrepareData(streamId); + frame.DataFlags = Http2DataFrameFlags.PADDED; + frame.Payload[0] = padLength; + + // Set length last so .Payload can be written to + frame.Length = frameLength; + + return SendAsync(frame.Raw); + } + + protected Task SendPingAsync(Http2PingFrameFlags flags) + { + var pingFrame = new Http2Frame(); + pingFrame.PreparePing(flags); + return SendAsync(pingFrame.Raw); + } + + protected Task SendPingWithInvalidLengthAsync(int length) + { + var pingFrame = new Http2Frame(); + pingFrame.PreparePing(Http2PingFrameFlags.NONE); + pingFrame.Length = length; + return SendAsync(pingFrame.Raw); + } + + protected Task SendPingWithInvalidStreamIdAsync(int streamId) + { + Assert.NotEqual(0, streamId); + + var pingFrame = new Http2Frame(); + pingFrame.PreparePing(Http2PingFrameFlags.NONE); + pingFrame.StreamId = streamId; + return SendAsync(pingFrame.Raw); + } + + protected Task SendPriorityAsync(int streamId, int streamDependency = 0) + { + var priorityFrame = new Http2Frame(); + priorityFrame.PreparePriority(streamId, streamDependency: streamDependency, exclusive: false, weight: 0); + return SendAsync(priorityFrame.Raw); + } + + protected Task SendInvalidPriorityFrameAsync(int streamId, int length) + { + var priorityFrame = new Http2Frame(); + priorityFrame.PreparePriority(streamId, streamDependency: 0, exclusive: false, weight: 0); + priorityFrame.Length = length; + return SendAsync(priorityFrame.Raw); + } + + protected Task SendRstStreamAsync(int streamId) + { + var rstStreamFrame = new Http2Frame(); + rstStreamFrame.PrepareRstStream(streamId, Http2ErrorCode.CANCEL); + return SendAsync(rstStreamFrame.Raw); + } + + protected Task SendInvalidRstStreamFrameAsync(int streamId, int length) + { + var frame = new Http2Frame(); + frame.PrepareRstStream(streamId, Http2ErrorCode.CANCEL); + frame.Length = length; + return SendAsync(frame.Raw); + } + + protected Task SendGoAwayAsync() + { + var frame = new Http2Frame(); + frame.PrepareGoAway(0, Http2ErrorCode.NO_ERROR); + return SendAsync(frame.Raw); + } + + protected Task SendInvalidGoAwayFrameAsync() + { + var frame = new Http2Frame(); + frame.PrepareGoAway(0, Http2ErrorCode.NO_ERROR); + frame.StreamId = 1; + return SendAsync(frame.Raw); + } + + protected Task SendWindowUpdateAsync(int streamId, int sizeIncrement) + { + var frame = new Http2Frame(); + frame.PrepareWindowUpdate(streamId, sizeIncrement); + return SendAsync(frame.Raw); + } + + protected Task SendInvalidWindowUpdateAsync(int streamId, int sizeIncrement, int length) + { + var frame = new Http2Frame(); + frame.PrepareWindowUpdate(streamId, sizeIncrement); + frame.Length = length; + return SendAsync(frame.Raw); + } + + protected Task SendUnknownFrameTypeAsync(int streamId, int frameType) + { + var frame = new Http2Frame(); + frame.StreamId = streamId; + frame.Type = (Http2FrameType)frameType; + frame.Length = 0; + return SendAsync(frame.Raw); + } + + protected async Task ReceiveFrameAsync() + { + var frame = new Http2Frame(); + + while (true) + { + var result = await _pair.Application.Input.ReadAsync().AsTask().DefaultTimeout(); + var buffer = result.Buffer; + var consumed = buffer.Start; + var examined = buffer.End; + + try + { + Assert.True(buffer.Length > 0); + + if (Http2FrameReader.ReadFrame(buffer, frame, 16_384, out consumed, out examined)) + { + return frame; + } + + if (result.IsCompleted) + { + throw new IOException("The reader completed without returning a frame."); + } + } + finally + { + _pair.Application.Input.AdvanceTo(consumed, examined); + } + } + } + + protected async Task ExpectAsync(Http2FrameType type, int withLength, byte withFlags, int withStreamId) + { + var frame = await ReceiveFrameAsync(); + + Assert.Equal(type, frame.Type); + Assert.Equal(withLength, frame.Length); + Assert.Equal(withFlags, frame.Flags); + Assert.Equal(withStreamId, frame.StreamId); + + return frame; + } + + protected Task StopConnectionAsync(int expectedLastStreamId, bool ignoreNonGoAwayFrames) + { + _pair.Application.Output.Complete(); + + return WaitForConnectionStopAsync(expectedLastStreamId, ignoreNonGoAwayFrames); + } + + protected Task WaitForConnectionStopAsync(int expectedLastStreamId, bool ignoreNonGoAwayFrames) + { + return WaitForConnectionErrorAsync(ignoreNonGoAwayFrames, expectedLastStreamId, Http2ErrorCode.NO_ERROR, expectedErrorMessage: null); + } + + protected void VerifyGoAway(Http2Frame frame, int expectedLastStreamId, Http2ErrorCode expectedErrorCode) + { + Assert.Equal(Http2FrameType.GOAWAY, frame.Type); + Assert.Equal(8, frame.Length); + Assert.Equal(0, frame.Flags); + Assert.Equal(0, frame.StreamId); + Assert.Equal(expectedLastStreamId, frame.GoAwayLastStreamId); + Assert.Equal(expectedErrorCode, frame.GoAwayErrorCode); + } + + protected async Task WaitForConnectionErrorAsync(bool ignoreNonGoAwayFrames, int expectedLastStreamId, Http2ErrorCode expectedErrorCode, string expectedErrorMessage) + where TException : Exception + { + var frame = await ReceiveFrameAsync(); + + if (ignoreNonGoAwayFrames) + { + while (frame.Type != Http2FrameType.GOAWAY) + { + frame = await ReceiveFrameAsync(); + } + } + + VerifyGoAway(frame, expectedLastStreamId, expectedErrorCode); + + if (expectedErrorMessage != null) + { + var message = Assert.Single(TestApplicationErrorLogger.Messages, m => m.Exception is TException); + Assert.Contains(expectedErrorMessage, message.Exception.Message); + } + + await _connectionTask; + _pair.Application.Output.Complete(); + } + + protected async Task WaitForStreamErrorAsync(int expectedStreamId, Http2ErrorCode expectedErrorCode, string expectedErrorMessage) + { + var frame = await ReceiveFrameAsync(); + + Assert.Equal(Http2FrameType.RST_STREAM, frame.Type); + Assert.Equal(4, frame.Length); + Assert.Equal(0, frame.Flags); + Assert.Equal(expectedStreamId, frame.StreamId); + Assert.Equal(expectedErrorCode, frame.RstStreamErrorCode); + + if (expectedErrorMessage != null) + { + Assert.Contains(TestApplicationErrorLogger.Messages, m => m.Exception?.Message.Contains(expectedErrorMessage) ?? false); + } + } + + protected void VerifyDecodedRequestHeaders(IEnumerable> expectedHeaders) + { + foreach (var header in expectedHeaders) + { + Assert.True(_receivedHeaders.TryGetValue(header.Key, out var value), header.Key); + Assert.Equal(header.Value, value, ignoreCase: true); + } + } + } +} diff --git a/test/Kestrel.InMemory.FunctionalTests/HttpProtocolSelectionTests.cs b/test/Kestrel.InMemory.FunctionalTests/HttpProtocolSelectionTests.cs index 4af9d531a3..4ad2c374dc 100644 --- a/test/Kestrel.InMemory.FunctionalTests/HttpProtocolSelectionTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/HttpProtocolSelectionTests.cs @@ -37,8 +37,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests [Fact] public Task Server_Http2Only_Cleartext_Success() { - // Expect a SETTINGS frame (type 0x4) with no payload and no flags - return TestSuccess(HttpProtocols.Http2, Encoding.ASCII.GetString(Http2Connection.ClientPreface), "\x00\x00\x00\x04\x00\x00\x00\x00\x00"); + // Expect a SETTINGS frame (type 0x4) with default settings + return TestSuccess(HttpProtocols.Http2, Encoding.ASCII.GetString(Http2Connection.ClientPreface), + "\x00\x00\x06\x04\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x64"); } private async Task TestSuccess(HttpProtocols serverProtocols, string request, string expectedResponse) diff --git a/test/Kestrel.InMemory.FunctionalTests/Kestrel.InMemory.FunctionalTests.csproj b/test/Kestrel.InMemory.FunctionalTests/Kestrel.InMemory.FunctionalTests.csproj index 6d4915600c..54d56c1077 100644 --- a/test/Kestrel.InMemory.FunctionalTests/Kestrel.InMemory.FunctionalTests.csproj +++ b/test/Kestrel.InMemory.FunctionalTests/Kestrel.InMemory.FunctionalTests.csproj @@ -9,7 +9,6 @@ - diff --git a/test/Kestrel.InMemory.FunctionalTests/ResponseTests.cs b/test/Kestrel.InMemory.FunctionalTests/ResponseTests.cs index 23d6e390a7..e0e63fd0d5 100644 --- a/test/Kestrel.InMemory.FunctionalTests/ResponseTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/ResponseTests.cs @@ -11,8 +11,8 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; @@ -421,7 +421,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests const string response = "hello, world"; var logTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var mockKestrelTrace = new Mock(Logger) { CallBase = true }; + var mockKestrelTrace = new Mock(); mockKestrelTrace .Setup(trace => trace.ConnectionHeadResponseBodyWrite(It.IsAny(), response.Length)) .Callback((connectionId, count) => logTcs.SetResult(null)); @@ -610,7 +610,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests public async Task WhenAppWritesLessThanContentLengthErrorLogged() { var logTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var mockTrace = new Mock(Logger) { CallBase = true }; + var mockTrace = new Mock(); mockTrace .Setup(trace => trace.ApplicationError(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((connectionId, requestId, ex) => @@ -663,7 +663,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests public async Task WhenAppWritesLessThanContentLengthButRequestIsAbortedErrorNotLogged() { var requestAborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var mockTrace = new Mock(Logger) { CallBase = true }; + var mockTrace = new Mock(); using (var server = new TestServer(async httpContext => { diff --git a/test/Kestrel.Tests/Kestrel.Tests.csproj b/test/Kestrel.Tests/Kestrel.Tests.csproj index ec0ae9149a..ffeff4838b 100644 --- a/test/Kestrel.Tests/Kestrel.Tests.csproj +++ b/test/Kestrel.Tests/Kestrel.Tests.csproj @@ -18,7 +18,11 @@ + + + + diff --git a/test/Kestrel.Transport.FunctionalTests/RequestTests.cs b/test/Kestrel.Transport.FunctionalTests/RequestTests.cs index 81ef08d614..fc5a5d382b 100644 --- a/test/Kestrel.Transport.FunctionalTests/RequestTests.cs +++ b/test/Kestrel.Transport.FunctionalTests/RequestTests.cs @@ -19,7 +19,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing.xunit; using Microsoft.Extensions.Logging; @@ -318,8 +318,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests .Callback>((logLevel, eventId, state, exception, formatter) => { Logger.Log(logLevel, eventId, state, exception, formatter); - var log = $"Log {logLevel}[{eventId}]: {formatter(state, exception)} {exception}"; - TestOutputHelper.WriteLine(log); if (eventId.Id == _connectionResetEventId) { @@ -707,10 +705,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets"))) .Returns(mockLogger.Object); - var mockKestrelTrace = new Mock(Logger) { CallBase = true }; - var testContext = new TestServiceContext(mockLoggerFactory.Object) + var mockKestrelTrace = new Mock(); + var testContext = new TestServiceContext(mockLoggerFactory.Object, mockKestrelTrace.Object) { - Log = mockKestrelTrace.Object, ServerOptions = { Limits = @@ -765,11 +762,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var readTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var appStartedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var mockKestrelTrace = new Mock(Logger) { CallBase = true }; - var testContext = new TestServiceContext() - { - Log = mockKestrelTrace.Object, - }; + var mockKestrelTrace = new Mock(); + var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object); var scratchBuffer = new byte[4096]; diff --git a/test/Kestrel.Transport.FunctionalTests/ResponseTests.cs b/test/Kestrel.Transport.FunctionalTests/ResponseTests.cs index 88d54f6d22..bc61229b9c 100644 --- a/test/Kestrel.Transport.FunctionalTests/ResponseTests.cs +++ b/test/Kestrel.Transport.FunctionalTests/ResponseTests.cs @@ -18,7 +18,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; @@ -250,7 +249,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var clientClosedConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var writeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var mockKestrelTrace = new Mock(Logger) { CallBase = true }; + var mockKestrelTrace = new Mock(); var mockLogger = new Mock(); mockLogger .Setup(logger => logger.IsEnabled(It.IsAny())) @@ -276,9 +275,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets"))) .Returns(mockLogger.Object); - var testContext = new TestServiceContext(mockLoggerFactory.Object) + var testContext = new TestServiceContext(mockLoggerFactory.Object, mockKestrelTrace.Object) { - Log = mockKestrelTrace.Object, ServerOptions = { Limits = @@ -470,7 +468,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var requestAborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var mockKestrelTrace = new Mock(Logger) { CallBase = true }; + var mockKestrelTrace = new Mock(); mockKestrelTrace .Setup(trace => trace.ResponseMininumDataRateNotSatisfied(It.IsAny(), It.IsAny())) .Callback(() => responseRateTimeoutMessageLogged.SetResult(null)); @@ -478,10 +476,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests .Setup(trace => trace.ConnectionStop(It.IsAny())) .Callback(() => connectionStopMessageLogged.SetResult(null)); - var testContext = new TestServiceContext + var testContext = new TestServiceContext(loggerFactory, mockKestrelTrace.Object) { - LoggerFactory = loggerFactory, - Log = mockKestrelTrace.Object, ServerOptions = { Limits = @@ -539,7 +535,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests await responseRateTimeoutMessageLogged.Task.DefaultTimeout(); await connectionStopMessageLogged.Task.DefaultTimeout(); await appFuncCompleted.Task.DefaultTimeout(); - await AssertStreamAborted(connection.Reader.BaseStream, chunkSize * chunks); + await AssertStreamAborted(connection.Stream, chunkSize * chunks); sw.Stop(); logger.LogInformation("Connection was aborted after {totalMilliseconds}ms.", sw.ElapsedMilliseconds); @@ -562,7 +558,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var aborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var mockKestrelTrace = new Mock(Logger) { CallBase = true }; + var mockKestrelTrace = new Mock(); mockKestrelTrace .Setup(trace => trace.ResponseMininumDataRateNotSatisfied(It.IsAny(), It.IsAny())) .Callback(() => responseRateTimeoutMessageLogged.SetResult(null)); @@ -616,7 +612,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { using (var connection = server.CreateConnection()) { - using (var sslStream = new SslStream(connection.Reader.BaseStream, false, (sender, cert, chain, errors) => true, null)) + using (var sslStream = new SslStream(connection.Stream, false, (sender, cert, chain, errors) => true, null)) { await sslStream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false); @@ -628,7 +624,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests await connectionStopMessageLogged.Task.DefaultTimeout(); await appFuncCompleted.Task.DefaultTimeout(); - await AssertStreamAborted(connection.Reader.BaseStream, chunkSize * chunks); + await AssertStreamAborted(connection.Stream, chunkSize * chunks); } } } @@ -647,7 +643,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var requestAborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var copyToAsyncCts = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var mockKestrelTrace = new Mock(Logger) { CallBase = true }; + var mockKestrelTrace = new Mock(); mockKestrelTrace .Setup(trace => trace.ResponseMininumDataRateNotSatisfied(It.IsAny(), It.IsAny())) .Callback(() => responseRateTimeoutMessageLogged.SetResult(null)); @@ -655,10 +651,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests .Setup(trace => trace.ConnectionStop(It.IsAny())) .Callback(() => connectionStopMessageLogged.SetResult(null)); - var testContext = new TestServiceContext + var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object) { - LoggerFactory = LoggerFactory, - Log = mockKestrelTrace.Object, ServerOptions = { Limits = @@ -734,11 +728,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var requestAborted = false; var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var mockKestrelTrace = new Mock(Logger) { CallBase = true }; + var mockKestrelTrace = new Mock(); - var testContext = new TestServiceContext + var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object) { - Log = mockKestrelTrace.Object, ServerOptions = { Limits = @@ -783,7 +776,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests // Make sure consuming a single chunk exceeds the 2 second timeout. var targetBytesPerSecond = chunkSize / 4; - await AssertStreamCompleted(connection.Reader.BaseStream, minTotalOutputSize, targetBytesPerSecond); + await AssertStreamCompleted(connection.Stream, minTotalOutputSize, targetBytesPerSecond); await appFuncCompleted.Task.DefaultTimeout(); mockKestrelTrace.Verify(t => t.ResponseMininumDataRateNotSatisfied(It.IsAny(), It.IsAny()), Times.Never()); @@ -803,11 +796,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var headerStringValues = new StringValues(Enumerable.Repeat(headerValue, headerCount).ToArray()); var requestAborted = false; - var mockKestrelTrace = new Mock(Logger) { CallBase = true }; + var mockKestrelTrace = new Mock(); - var testContext = new TestServiceContext + var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object) { - Log = mockKestrelTrace.Object, ServerOptions = { Limits = @@ -860,7 +852,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests // Make sure consuming a single set of response headers exceeds the 2 second timeout. var targetBytesPerSecond = responseSize / 4; - await AssertStreamCompleted(connection.Reader.BaseStream, minTotalOutputSize, targetBytesPerSecond); + await AssertStreamCompleted(connection.Stream, minTotalOutputSize, targetBytesPerSecond); mockKestrelTrace.Verify(t => t.ResponseMininumDataRateNotSatisfied(It.IsAny(), It.IsAny()), Times.Never()); mockKestrelTrace.Verify(t => t.ConnectionStop(It.IsAny()), Times.Once()); diff --git a/test/Kestrel.Transport.Libuv.BindTests/Kestrel.Transport.Libuv.BindTests.csproj b/test/Kestrel.Transport.Libuv.BindTests/Kestrel.Transport.Libuv.BindTests.csproj index edf6bd9a96..2e1b2bbe54 100644 --- a/test/Kestrel.Transport.Libuv.BindTests/Kestrel.Transport.Libuv.BindTests.csproj +++ b/test/Kestrel.Transport.Libuv.BindTests/Kestrel.Transport.Libuv.BindTests.csproj @@ -12,7 +12,6 @@ - diff --git a/test/Kestrel.Transport.Libuv.FunctionalTests/Kestrel.Transport.Libuv.FunctionalTests.csproj b/test/Kestrel.Transport.Libuv.FunctionalTests/Kestrel.Transport.Libuv.FunctionalTests.csproj index b2e93c9777..7c9eb1fa1d 100644 --- a/test/Kestrel.Transport.Libuv.FunctionalTests/Kestrel.Transport.Libuv.FunctionalTests.csproj +++ b/test/Kestrel.Transport.Libuv.FunctionalTests/Kestrel.Transport.Libuv.FunctionalTests.csproj @@ -13,7 +13,6 @@ - diff --git a/test/Kestrel.Transport.Libuv.Tests/Kestrel.Transport.Libuv.Tests.csproj b/test/Kestrel.Transport.Libuv.Tests/Kestrel.Transport.Libuv.Tests.csproj index 0ad5d2346d..dbdec3565c 100644 --- a/test/Kestrel.Transport.Libuv.Tests/Kestrel.Transport.Libuv.Tests.csproj +++ b/test/Kestrel.Transport.Libuv.Tests/Kestrel.Transport.Libuv.Tests.csproj @@ -18,9 +18,10 @@ - + + diff --git a/test/Kestrel.Transport.Libuv.Tests/LibuvOutputConsumerTests.cs b/test/Kestrel.Transport.Libuv.Tests/LibuvOutputConsumerTests.cs index d2f6a4fb89..2aa92e37d2 100644 --- a/test/Kestrel.Transport.Libuv.Tests/LibuvOutputConsumerTests.cs +++ b/test/Kestrel.Transport.Libuv.Tests/LibuvOutputConsumerTests.cs @@ -743,7 +743,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests ConnectionFeatures = connectionFeatures, MemoryPool = _memoryPool, TimeoutControl = Mock.Of(), - Application = pair.Application, Transport = pair.Transport }); diff --git a/test/Kestrel.Transport.Sockets.BindTests/Kestrel.Transport.Sockets.BindTests.csproj b/test/Kestrel.Transport.Sockets.BindTests/Kestrel.Transport.Sockets.BindTests.csproj index 4aa0791ed7..6393beddf0 100644 --- a/test/Kestrel.Transport.Sockets.BindTests/Kestrel.Transport.Sockets.BindTests.csproj +++ b/test/Kestrel.Transport.Sockets.BindTests/Kestrel.Transport.Sockets.BindTests.csproj @@ -12,7 +12,6 @@ - diff --git a/test/Kestrel.Transport.Sockets.FunctionalTests/Kestrel.Transport.Sockets.FunctionalTests.csproj b/test/Kestrel.Transport.Sockets.FunctionalTests/Kestrel.Transport.Sockets.FunctionalTests.csproj index e717642f93..35be01d361 100644 --- a/test/Kestrel.Transport.Sockets.FunctionalTests/Kestrel.Transport.Sockets.FunctionalTests.csproj +++ b/test/Kestrel.Transport.Sockets.FunctionalTests/Kestrel.Transport.Sockets.FunctionalTests.csproj @@ -12,7 +12,6 @@ - diff --git a/test/shared/FunctionalTestHelpers/DiagnosticMemoryPoolFactory.cs b/test/shared/DiagnosticMemoryPoolFactory.cs similarity index 100% rename from test/shared/FunctionalTestHelpers/DiagnosticMemoryPoolFactory.cs rename to test/shared/DiagnosticMemoryPoolFactory.cs diff --git a/test/shared/FunctionalTestHelpers/TestApplicationErrorLoggerLoggedTest.cs b/test/shared/TestApplicationErrorLoggerLoggedTest.cs similarity index 100% rename from test/shared/FunctionalTestHelpers/TestApplicationErrorLoggerLoggedTest.cs rename to test/shared/TestApplicationErrorLoggerLoggedTest.cs index 312c64660c..c149111fdb 100644 --- a/test/shared/FunctionalTestHelpers/TestApplicationErrorLoggerLoggedTest.cs +++ b/test/shared/TestApplicationErrorLoggerLoggedTest.cs @@ -1,8 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.Extensions.Logging.Testing; using System.Reflection; +using Microsoft.Extensions.Logging.Testing; using Xunit.Abstractions; namespace Microsoft.AspNetCore.Testing diff --git a/test/shared/TransportTestHelpers/TestServer.cs b/test/shared/TransportTestHelpers/TestServer.cs index e5b9376289..df73888129 100644 --- a/test/shared/TransportTestHelpers/TestServer.cs +++ b/test/shared/TransportTestHelpers/TestServer.cs @@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests @@ -85,11 +86,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests .Build(); _host.Start(); + + Context.Log.LogDebug($"TestServer is listening on port {Port}"); } - public IPEndPoint EndPoint => _listenOptions.IPEndPoint; - public int Port => _listenOptions.IPEndPoint.Port; - public AddressFamily AddressFamily => _listenOptions.IPEndPoint.AddressFamily; + // Avoid NullReferenceException in the CanListenToOpenTcpSocketHandle test + public int Port => _listenOptions.IPEndPoint?.Port ?? 0; public TestServiceContext Context { get; } @@ -107,7 +109,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests public TestConnection CreateConnection() { - return new TestConnection(Port, AddressFamily); + return new TestConnection(Port, _listenOptions.IPEndPoint.AddressFamily); } public Task StopAsync(CancellationToken token = default)