diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx index f755e96f072c..66e0bff77f2c 100644 --- a/src/Servers/Kestrel/Core/src/CoreStrings.resx +++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx @@ -683,4 +683,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l Request stream ended without headers. + + Reading the control stream header timed out. + \ No newline at end of file diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs index b6ead9433d5b..db9be53ed3a6 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -14,14 +15,15 @@ using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 { - internal class Http3Connection : ITimeoutHandler + internal class Http3Connection : ITimeoutHandler, IHttp3StreamLifetimeHandler { - internal readonly Dictionary _streams = new Dictionary(); + internal readonly Dictionary _streams = new Dictionary(); private long _highestOpenedStreamId; private readonly object _sync = new object(); @@ -84,9 +86,6 @@ public async Task ProcessStreamsAsync(IHttpApplication httpA { try { - // Ensure TimeoutControl._lastTimestamp is initialized before anything that could set timeouts runs. - _timeoutControl.Initialize(_systemClock.UtcNowTicks); - var connectionHeartbeatFeature = _context.ConnectionFeatures.Get(); var connectionLifetimeNotificationFeature = _context.ConnectionFeatures.Get(); @@ -198,11 +197,42 @@ public void Tick() return; } - // It's safe to use UtcNowUnsynchronized since Tick is called by the Heartbeat. - var now = _systemClock.UtcNowUnsynchronized; - _timeoutControl.Tick(now); + UpdateStartingStreams(); + } + + private void UpdateStartingStreams() + { + var now = _systemClock.UtcNow.Ticks; + + lock (_streams) + { + foreach (var stream in _streams.Values) + { + if (stream.ReceivedHeader) + { + continue; + } + + if (stream.HeaderTimeoutTicks == default) + { + // On expiration overflow, use max value. + var expirationTicks = now + _context.ServiceContext.ServerOptions.Limits.RequestHeadersTimeout.Ticks; + stream.HeaderTimeoutTicks = expirationTicks >= 0 ? expirationTicks : long.MaxValue; + } - // TODO cancel process stream loop to update logic. + if (stream.HeaderTimeoutTicks < now) + { + if (stream.IsRequestStream) + { + stream.Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestHeadersTimeout), Http3ErrorCode.RequestRejected); + } + else + { + stream.Abort(new ConnectionAbortedException(CoreStrings.Http3ControlStreamHeaderTimeout), Http3ErrorCode.StreamCreationError); + } + } + } + } } public void OnTimeout(TimeoutReason reason) @@ -213,13 +243,11 @@ public void OnTimeout(TimeoutReason reason) // TODO what timeouts should we handle here? Is keep alive something we should care about? switch (reason) { - case TimeoutReason.KeepAlive: - SendGoAway(GetHighestStreamId()).Preserve(); - break; case TimeoutReason.TimeoutFeature: SendGoAway(GetHighestStreamId()).Preserve(); break; - case TimeoutReason.RequestHeaders: + case TimeoutReason.RequestHeaders: // Request header timeout is handled in starting stream queue + case TimeoutReason.KeepAlive: // Keep-alive is handled by msquic case TimeoutReason.ReadDataRate: case TimeoutReason.WriteDataRate: case TimeoutReason.RequestBodyDrain: @@ -245,8 +273,6 @@ internal async Task InnerProcessStreamsAsync(IHttpApplication(IHttpApplication(application, this, httpConnectionContext); + var stream = new Http3ControlStream(application, httpConnectionContext); + lock (_streams) + { + _streams[streamId] = stream; + } + ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false); } else { - var streamId = streamIdFeature.StreamId; - + // Request stream UpdateHighestStreamId(streamId); - var http3Stream = new Http3Stream(application, this, httpConnectionContext); - var stream = http3Stream; + var stream = new Http3Stream(application, httpConnectionContext); lock (_streams) { _activeRequestCount++; - _streams[streamId] = http3Stream; + _streams[streamId] = stream; } + KestrelEventSource.Log.RequestQueuedStart(stream, AspNetCore.Http.HttpProtocol.Http3); ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false); } @@ -363,8 +396,6 @@ internal async Task InnerProcessStreamsAsync(IHttpApplication CreateNewUnidirectionalStreamAsync(application, this, httpConnectionContext); + return new Http3ControlStream(application, httpConnectionContext); } private ValueTask SendGoAway(long id) @@ -466,33 +487,10 @@ private ValueTask SendGoAway(long id) return OutboundControlStream.SendGoAway(id); } } - return new ValueTask(); + return default; } - public void ApplyMaxHeaderListSize(long value) - { - } - - internal void ApplyBlockedStream(long value) - { - } - - internal void ApplyMaxTableCapacity(long value) - { - } - - internal void RemoveStream(long streamId) - { - lock (_streams) - { - _activeRequestCount--; - _streams.Remove(streamId); - } - - _streamCompletionAwaitable.Complete(); - } - - public bool SetInboundControlStream(Http3ControlStream stream) + public bool OnInboundControlStream(Http3ControlStream stream) { lock (_sync) { @@ -505,7 +503,7 @@ public bool SetInboundControlStream(Http3ControlStream stream) } } - public bool SetInboundEncoderStream(Http3ControlStream stream) + public bool OnInboundEncoderStream(Http3ControlStream stream) { lock (_sync) { @@ -518,7 +516,7 @@ public bool SetInboundEncoderStream(Http3ControlStream stream) } } - public bool SetInboundDecoderStream(Http3ControlStream stream) + public bool OnInboundDecoderStream(Http3ControlStream stream) { lock (_sync) { @@ -531,6 +529,38 @@ public bool SetInboundDecoderStream(Http3ControlStream stream) } } + public void OnStreamCompleted(IHttp3Stream stream) + { + lock (_streams) + { + _activeRequestCount--; + _streams.Remove(stream.StreamId); + } + + _streamCompletionAwaitable.Complete(); + } + + public void OnStreamConnectionError(Http3ConnectionErrorException ex) + { + Log.Http3ConnectionError(ConnectionId, ex); + Abort(new ConnectionAbortedException(ex.Message, ex), ex.ErrorCode); + } + + public void OnInboundControlStreamSetting(Http3SettingType type, long value) + { + switch (type) + { + case Http3SettingType.QPackMaxTableCapacity: + break; + case Http3SettingType.MaxFieldSectionSize: + break; + case Http3SettingType.QPackBlockedStreams: + break; + default: + throw new InvalidOperationException("Unexpected setting: " + type); + } + } + private static class GracefulCloseInitiator { public const int None = 0; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs index 39dce967bde6..9e8fcfb2d136 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs @@ -17,32 +17,34 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 { - internal abstract class Http3ControlStream : IThreadPoolWorkItem + internal abstract class Http3ControlStream : IHttp3Stream, IThreadPoolWorkItem { - private const int ControlStream = 0; - private const int EncoderStream = 2; - private const int DecoderStream = 3; + private const int ControlStreamTypeId = 0; + private const int EncoderStreamTypeId = 2; + private const int DecoderStreamTypeId = 3; private readonly Http3FrameWriter _frameWriter; - private readonly Http3Connection _http3Connection; private readonly Http3StreamContext _context; private readonly Http3PeerSettings _serverPeerSettings; private readonly IStreamIdFeature _streamIdFeature; - private readonly IProtocolErrorCodeFeature _protocolErrorCodeFeature; + private readonly IProtocolErrorCodeFeature _errorCodeFeature; private readonly Http3RawFrame _incomingFrame = new Http3RawFrame(); private volatile int _isClosed; private int _gracefulCloseInitiator; + private long _headerType; private bool _haveReceivedSettingsFrame; - public Http3ControlStream(Http3Connection http3Connection, Http3StreamContext context) + public long StreamId => _streamIdFeature.StreamId; + + public Http3ControlStream(Http3StreamContext context) { var httpLimits = context.ServiceContext.ServerOptions.Limits; - _http3Connection = http3Connection; _context = context; _serverPeerSettings = context.ServerSettings; _streamIdFeature = context.ConnectionFeatures.Get()!; - _protocolErrorCodeFeature = context.ConnectionFeatures.Get()!; + _errorCodeFeature = context.ConnectionFeatures.Get()!; + _headerType = -1; _frameWriter = new Http3FrameWriter( context.Transport.Output, @@ -57,27 +59,28 @@ public Http3ControlStream(Http3Connection http3Connection, Http3StreamContext co private void OnStreamClosed() { - Abort(new ConnectionAbortedException("HTTP_CLOSED_CRITICAL_STREAM")); + Abort(new ConnectionAbortedException("HTTP_CLOSED_CRITICAL_STREAM"), Http3ErrorCode.InternalError); } public PipeReader Input => _context.Transport.Input; public IKestrelTrace Log => _context.ServiceContext.Log; - public void Abort(ConnectionAbortedException ex) - { + public long HeaderTimeoutTicks { get; set; } + public bool ReceivedHeader => _headerType >= 0; - } + public bool IsRequestStream => false; - public void HandleReadDataRateTimeout() + public void Abort(ConnectionAbortedException abortReason, Http3ErrorCode errorCode) { - //Log.RequestBodyMinimumDataRateNotSatisfied(ConnectionId, null, Limits.MinRequestBodyDataRate.BytesPerSecond); - Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestBodyTimeout)); - } + // TODO - Should there be a check here to track abort state to avoid + // running twice for a request? - public void HandleRequestHeadersTimeout() - { - //Log.ConnectionBadRequest(ConnectionId, KestrelBadHttpRequestException.GetException(RequestRejectionReason.RequestHeadersTimeout)); - Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestHeadersTimeout)); + Log.Http3StreamAbort(_context.ConnectionId, errorCode, abortReason); + + _errorCodeFeature.Error = (long)errorCode; + _frameWriter.Abort(abortReason); + + Input.Complete(abortReason); } public void OnInputOrOutputCompleted() @@ -111,8 +114,9 @@ internal async ValueTask SendSettingsFrameAsync() await _frameWriter.WriteSettingsAsync(_serverPeerSettings.GetNonProtocolDefaults()); } - private async ValueTask TryReadStreamIdAsync() + private async ValueTask TryReadStreamHeaderAsync() { + // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-6.2 while (_isClosed == 0) { var result = await Input.ReadAsync(); @@ -149,51 +153,47 @@ public async Task ProcessRequestAsync(IHttpApplication appli { try { - var streamType = await TryReadStreamIdAsync(); - - if (streamType == -1) - { - return; - } + _headerType = await TryReadStreamHeaderAsync(); - if (streamType == ControlStream) + switch (_headerType) { - if (!_http3Connection.SetInboundControlStream(this)) - { - // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-6.2.1 - throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams("control"), Http3ErrorCode.StreamCreationError); - } + case -1: + return; + case ControlStreamTypeId: + if (!_context.StreamLifetimeHandler.OnInboundControlStream(this)) + { + // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-6.2.1 + throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams("control"), Http3ErrorCode.StreamCreationError); + } - await HandleControlStream(); - } - else if (streamType == EncoderStream) - { - if (!_http3Connection.SetInboundEncoderStream(this)) - { - // https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#section-4.2 - throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams("encoder"), Http3ErrorCode.StreamCreationError); - } + await HandleControlStream(); + break; + case EncoderStreamTypeId: + if (!_context.StreamLifetimeHandler.OnInboundEncoderStream(this)) + { + // https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#section-4.2 + throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams("encoder"), Http3ErrorCode.StreamCreationError); + } - await HandleEncodingDecodingTask(); - } - else if (streamType == DecoderStream) - { - if (!_http3Connection.SetInboundDecoderStream(this)) - { - // https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#section-4.2 - throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams("decoder"), Http3ErrorCode.StreamCreationError); - } - await HandleEncodingDecodingTask(); - } - else - { - // TODO Close the control stream as it's unexpected. + await HandleEncodingDecodingTask(); + break; + case DecoderStreamTypeId: + if (!_context.StreamLifetimeHandler.OnInboundDecoderStream(this)) + { + // https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#section-4.2 + throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams("decoder"), Http3ErrorCode.StreamCreationError); + } + await HandleEncodingDecodingTask(); + break; + default: + // TODO Close the control stream as it's unexpected. + break; } } catch (Http3ConnectionErrorException ex) { - Log.Http3ConnectionError(_http3Connection.ConnectionId, ex); - _http3Connection.Abort(new ConnectionAbortedException(ex.Message, ex), ex.ErrorCode); + _errorCodeFeature.Error = (long)ex.ErrorCode; + _context.StreamLifetimeHandler.OnStreamConnectionError(ex); } } @@ -227,10 +227,8 @@ private async Task HandleControlStream() } catch (Http3ConnectionErrorException ex) { - _protocolErrorCodeFeature.Error = (long)ex.ErrorCode; - - Log.Http3ConnectionError(_http3Connection.ConnectionId, ex); - _http3Connection.Abort(new ConnectionAbortedException(ex.Message, ex), ex.ErrorCode); + _errorCodeFeature.Error = (long)ex.ErrorCode; + _context.StreamLifetimeHandler.OnStreamConnectionError(ex); } finally { @@ -324,13 +322,9 @@ private void ProcessSetting(long id, long value) var message = CoreStrings.FormatHttp3ErrorControlStreamReservedSetting("0x" + id.ToString("X", CultureInfo.InvariantCulture)); throw new Http3ConnectionErrorException(message, Http3ErrorCode.SettingsError); case (long)Http3SettingType.QPackMaxTableCapacity: - _http3Connection.ApplyMaxTableCapacity(value); - break; case (long)Http3SettingType.MaxFieldSectionSize: - _http3Connection.ApplyMaxHeaderListSize(value); - break; case (long)Http3SettingType.QPackBlockedStreams: - _http3Connection.ApplyBlockedStream(value); + _context.StreamLifetimeHandler.OnInboundControlStreamSetting((Http3SettingType)id, value); break; default: // Ignore all unknown settings. diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStreamOfT.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStreamOfT.cs index 3b407bc3e81b..630b74b00a44 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStreamOfT.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStreamOfT.cs @@ -10,7 +10,7 @@ internal sealed class Http3ControlStream : Http3ControlStream, IHostCo { private readonly IHttpApplication _application; - public Http3ControlStream(IHttpApplication application, Http3Connection connection, Http3StreamContext context) : base(connection, context) + public Http3ControlStream(IHttpApplication application, Http3StreamContext context) : base(context) { _application = application; } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs index 899b464d7f3d..a5768a2eb637 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 { - internal abstract partial class Http3Stream : HttpProtocol, IHttpHeadersHandler, IThreadPoolWorkItem, ITimeoutHandler, IRequestProcessor + internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpHeadersHandler, IThreadPoolWorkItem, ITimeoutHandler, IRequestProcessor { private static ReadOnlySpan AuthorityBytes => new byte[10] { (byte)':', (byte)'a', (byte)'u', (byte)'t', (byte)'h', (byte)'o', (byte)'r', (byte)'i', (byte)'t', (byte)'y' }; private static ReadOnlySpan MethodBytes => new byte[7] { (byte)':', (byte)'m', (byte)'e', (byte)'t', (byte)'h', (byte)'o', (byte)'d' }; @@ -48,21 +48,16 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttpHeadersHandler, private int _totalParsedHeaderSize; private bool _isMethodConnect; - private readonly Http3Connection _http3Connection; private TaskCompletionSource? _appCompleted; public Pipe RequestBodyPipe { get; } - public Http3Stream(Http3Connection http3Connection, Http3StreamContext context) + public Http3Stream(Http3StreamContext context) { Initialize(context); InputRemaining = null; - // First, determine how we know if an Http3stream is unidirectional or bidirectional - var httpLimits = context.ServiceContext.ServerOptions.Limits; - var http3Limits = httpLimits.Http3; - _http3Connection = http3Connection; _context = context; _errorCodeFeature = _context.ConnectionFeatures.Get()!; @@ -72,7 +67,7 @@ public Http3Stream(Http3Connection http3Connection, Http3StreamContext context) context.Transport.Output, context.StreamContext, context.TimeoutControl, - httpLimits.MinResponseDataRate, + context.ServiceContext.ServerOptions.Limits.MinResponseDataRate, context.ConnectionId, context.MemoryPool, context.ServiceContext.Log, @@ -99,6 +94,12 @@ public Http3Stream(Http3Connection http3Connection, Http3StreamContext context) public ISystemClock SystemClock => _context.ServiceContext.SystemClock; public KestrelServerLimits Limits => _context.ServiceContext.ServerOptions.Limits; + public long StreamId => _streamIdFeature.StreamId; + + public long HeaderTimeoutTicks { get; set; } + public bool ReceivedHeader => _appCompleted != null; // TCS is assigned once headers are received + + public bool IsRequestStream => true; public void Abort(ConnectionAbortedException ex) { @@ -406,10 +407,7 @@ public async Task ProcessRequestAsync(IHttpApplication appli error = ex; _errorCodeFeature.Error = (long)ex.ErrorCode; - Log.Http3ConnectionError(_http3Connection.ConnectionId, ex); - _http3Connection.Abort(new ConnectionAbortedException(ex.Message, ex), ex.ErrorCode); - - // TODO: HTTP/3 stream will be aborted by connection. Check this is correct. + _context.StreamLifetimeHandler.OnStreamConnectionError(ex); } catch (Exception ex) { @@ -442,7 +440,7 @@ public async Task ProcessRequestAsync(IHttpApplication appli { await _context.StreamContext.DisposeAsync(); - _http3Connection.RemoveStream(_streamIdFeature.StreamId); + _context.StreamLifetimeHandler.OnStreamCompleted(this); } } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamOfT.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamOfT.cs index e88de7400f4e..63a25e94f485 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamOfT.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamOfT.cs @@ -11,7 +11,7 @@ class Http3Stream : Http3Stream, IHostContextContainer where { private readonly IHttpApplication _application; - public Http3Stream(IHttpApplication application, Http3Connection connection, Http3StreamContext context) : base(connection, context) + public Http3Stream(IHttpApplication application, Http3StreamContext context) : base(context) { _application = application; } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/IHttp3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/IHttp3Stream.cs new file mode 100644 index 000000000000..5cdeb8301e21 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/IHttp3Stream.cs @@ -0,0 +1,33 @@ +// 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.Net.Http; +using Microsoft.AspNetCore.Connections; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + internal interface IHttp3Stream + { + /// + /// The stream ID is set by QUIC. + /// + long StreamId { get; } + + /// + /// Used to track the timeout between when the stream was started by the client, and getting a header. + /// Value is driven by . + /// + long HeaderTimeoutTicks { get; set; } + + /// + /// The stream has received and parsed the header frame. + /// - Request streams = HEADERS frame. + /// - Control streams = unidirectional stream header. + /// + bool ReceivedHeader { get; } + + bool IsRequestStream { get; } + + void Abort(ConnectionAbortedException abortReason, Http3ErrorCode errorCode); + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/IHttp3StreamLifetimeHandler.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/IHttp3StreamLifetimeHandler.cs new file mode 100644 index 000000000000..57bbbf99c597 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/IHttp3StreamLifetimeHandler.cs @@ -0,0 +1,16 @@ +// 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. + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + internal interface IHttp3StreamLifetimeHandler + { + void OnStreamCompleted(IHttp3Stream stream); + void OnStreamConnectionError(Http3ConnectionErrorException ex); + + bool OnInboundControlStream(Http3ControlStream stream); + bool OnInboundEncoderStream(Http3ControlStream stream); + bool OnInboundDecoderStream(Http3ControlStream stream); + void OnInboundControlStreamSetting(Http3SettingType type, long value); + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3ConnectionContext.cs b/src/Servers/Kestrel/Core/src/Internal/Http3ConnectionContext.cs index 8cd1cc177130..5f8130e2413d 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3ConnectionContext.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3ConnectionContext.cs @@ -1,10 +1,12 @@ // 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.Net; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3StreamContext.cs b/src/Servers/Kestrel/Core/src/Internal/Http3StreamContext.cs index dcb4a2c0492e..bb01e49d4baa 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3StreamContext.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3StreamContext.cs @@ -22,13 +22,16 @@ public Http3StreamContext( IPEndPoint? localEndPoint, IPEndPoint? remoteEndPoint, IDuplexPipe transport, + IHttp3StreamLifetimeHandler streamLifetimeHandler, ConnectionContext streamContext, Http3PeerSettings settings) : base(connectionId, protocols, connectionContext, serviceContext, connectionFeatures, memoryPool, localEndPoint, remoteEndPoint, transport) { + StreamLifetimeHandler = streamLifetimeHandler; StreamContext = streamContext; ServerSettings = settings; } + public IHttp3StreamLifetimeHandler StreamLifetimeHandler { get; } public ConnectionContext StreamContext { get; } public Http3PeerSettings ServerSettings { get; } } diff --git a/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs b/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs index 65ed15f0179a..876828f349d1 100644 --- a/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs @@ -74,7 +74,7 @@ public async Task ProcessRequestsAsync(IHttpApplication http default: // SelectProtocol() only returns Http1, Http2 or None. - throw new NotSupportedException($"{nameof(SelectProtocol)} returned something other than Http1, Http2, Http3 or None."); + throw new NotSupportedException($"{nameof(SelectProtocol)} returned something other than Http1, Http2 or None."); } _requestProcessor = requestProcessor; diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ITimeoutControl.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ITimeoutControl.cs index 6f303d56b218..f45577fb6287 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ITimeoutControl.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ITimeoutControl.cs @@ -1,6 +1,7 @@ // 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 Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure @@ -14,6 +15,8 @@ internal interface ITimeoutControl void CancelTimeout(); void InitializeHttp2(InputFlowControl connectionInputFlowControl); + void Tick(DateTimeOffset now); + void StartRequestBody(MinDataRate minRate); void StopRequestBody(); void StartTimingRead(); diff --git a/src/Servers/Kestrel/Core/test/Http3HttpProtocolFeatureCollectionTests.cs b/src/Servers/Kestrel/Core/test/Http3HttpProtocolFeatureCollectionTests.cs index 1314e1699787..d1474418ca23 100644 --- a/src/Servers/Kestrel/Core/test/Http3HttpProtocolFeatureCollectionTests.cs +++ b/src/Servers/Kestrel/Core/test/Http3HttpProtocolFeatureCollectionTests.cs @@ -18,11 +18,9 @@ public class Http3HttpProtocolFeatureCollectionTests public Http3HttpProtocolFeatureCollectionTests() { - var connection = new Http3Connection(TestContextFactory.CreateHttp3ConnectionContext()); - var streamContext = TestContextFactory.CreateHttp3StreamContext(transport: DuplexPipe.CreateConnectionPair(new PipeOptions(), new PipeOptions()).Application); - var http3Stream = new TestHttp3Stream(connection, streamContext); + var http3Stream = new TestHttp3Stream(streamContext); http3Stream.Reset(); _http3Collection = http3Stream; } @@ -61,7 +59,7 @@ public void Http3StreamFeatureCollectionDoesIncludeIHttpMinRequestBodyDataRateFe private class TestHttp3Stream : Http3Stream { - public TestHttp3Stream(Http3Connection connection, Http3StreamContext context) : base(connection, context) + public TestHttp3Stream(Http3StreamContext context) : base(context) { } diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTimeoutControl.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTimeoutControl.cs index 115ba593ef38..1f0885cedd52 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTimeoutControl.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTimeoutControl.cs @@ -1,6 +1,7 @@ // 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 Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; @@ -58,5 +59,9 @@ public void StopTimingRead() public void StopTimingWrite() { } + + public void Tick(DateTimeOffset now) + { + } } } diff --git a/src/Servers/Kestrel/shared/test/TestContextFactory.cs b/src/Servers/Kestrel/shared/test/TestContextFactory.cs index a50e672c0bf1..6fc84e150970 100644 --- a/src/Servers/Kestrel/shared/test/TestContextFactory.cs +++ b/src/Servers/Kestrel/shared/test/TestContextFactory.cs @@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Logging; @@ -171,7 +172,8 @@ public static Http3StreamContext CreateHttp3StreamContext( IPEndPoint localEndPoint = null, IPEndPoint remoteEndPoint = null, IDuplexPipe transport = null, - ITimeoutControl timeoutControl = null) + ITimeoutControl timeoutControl = null, + IHttp3StreamLifetimeHandler streamLifetimeHandler = null) { var context = new Http3StreamContext ( @@ -184,6 +186,7 @@ public static Http3StreamContext CreateHttp3StreamContext( localEndPoint: localEndPoint, remoteEndPoint: remoteEndPoint, transport: transport, + streamLifetimeHandler: streamLifetimeHandler, streamContext: null, settings: null ); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs index ed710fdc6b61..e11b5d8e2262 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -432,8 +432,6 @@ void IHttpHeadersHandler.OnHeadersComplete(bool endStream) { } protected void CreateConnection() { - var limits = _serviceContext.ServerOptions.Limits; - // Always dispatch test code back to the ThreadPool. This prevents deadlocks caused by continuing // Http2Connection.ProcessRequestsAsync() loop with writer locks acquired. Run product code inline to make // it easier to verify request frames are processed correctly immediately after sending the them. @@ -1381,6 +1379,11 @@ public virtual void BytesWrittenToBuffer(MinDataRate minRate, long size) { _realTimeoutControl.BytesWrittenToBuffer(minRate, size); } + + public virtual void Tick(DateTimeOffset now) + { + _realTimeoutControl.Tick(now); + } } } } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs index a8266f60d566..5c89c5577e0f 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs @@ -1823,7 +1823,7 @@ await requestStream.WaitForStreamErrorAsync( await WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: true, - expectedLastStreamId: 0, + expectedLastStreamId: 8, expectedErrorCode: Http3ErrorCode.UnexpectedFrame, expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnRequestStream(frame.FormattedType)); } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs index 0c3c1c5980f3..443adc786522 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs @@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { - public class Http3TestBase : TestApplicationErrorLoggerLoggedTest, IDisposable + public abstract class Http3TestBase : TestApplicationErrorLoggerLoggedTest, IDisposable { protected static readonly int MaxRequestHeaderFieldSize = 16 * 1024; protected static readonly string _4kHeaderValue = new string('a', 4096); @@ -55,6 +55,7 @@ public class Http3TestBase : TestApplicationErrorLoggerLoggedTest, IDisposable protected readonly RequestDelegate _echoHost; private Http3ControlStream _inboundControlStream; + private long _currentStreamId; public Http3TestBase() { @@ -119,6 +120,15 @@ public override void Initialize(TestContext context, MethodInfo methodInfo, obje }; } + internal long GetStreamId(long mask) + { + var id = (_currentStreamId << 2) | mask; + + _currentStreamId += 1; + + return id; + } + internal async ValueTask GetInboundControlStream() { if (_inboundControlStream == null) @@ -136,7 +146,7 @@ internal async ValueTask GetInboundControlStream() } } - return null; + return _inboundControlStream; } internal async Task WaitForConnectionErrorAsync(bool ignoreNonGoAwayFrames, long expectedLastStreamId, Http3ErrorCode expectedErrorCode, params string[] expectedErrorMessage) @@ -172,6 +182,27 @@ internal void VerifyGoAway(Http3FrameWithPayload frame, long expectedLastStreamI Assert.Equal(expectedLastStreamId, streamId); } + protected void AdvanceClock(TimeSpan timeSpan) + { + var clock = _serviceContext.MockSystemClock; + var endTime = clock.UtcNow + timeSpan; + + while (clock.UtcNow + Heartbeat.Interval < endTime) + { + clock.UtcNow += Heartbeat.Interval; + _timeoutControl.Tick(clock.UtcNow); + } + + clock.UtcNow = endTime; + _timeoutControl.Tick(clock.UtcNow); + } + + protected void TriggerTick(DateTimeOffset now) + { + _serviceContext.MockSystemClock.UtcNow = now; + Connection?.Tick(); + } + protected async Task InitializeConnectionAsync(RequestDelegate application) { if (Connection == null) @@ -198,9 +229,6 @@ internal async ValueTask InitializeConnectionAndStreamsAsync protected void CreateConnection() { - var limits = _serviceContext.ServerOptions.Limits; - - MultiplexedConnectionContext = new TestMultiplexedConnectionContext(this); var httpConnectionContext = new Http3ConnectionContext( @@ -263,11 +291,14 @@ public ValueTask CreateControlStream() return CreateControlStream(id: 0); } - public async ValueTask CreateControlStream(int id) + public async ValueTask CreateControlStream(int? id) { - var stream = new Http3ControlStream(this); + var stream = new Http3ControlStream(this, StreamInitiator.Client); MultiplexedConnectionContext.ToServerAcceptQueue.Writer.TryWrite(stream.StreamContext); - await stream.WriteStreamIdAsync(id); + if (id != null) + { + await stream.WriteStreamIdAsync(id.GetValueOrDefault()); + } return stream; } @@ -285,12 +316,13 @@ public ValueTask StartBidirectionalStreamAsync() return new ValueTask(stream.StreamContext); } - public class Http3StreamBase + public class Http3StreamBase : IProtocolErrorCodeFeature { internal DuplexPipe.DuplexPipePair _pair; internal Http3TestBase _testBase; internal Http3Connection _connection; private long _bytesReceived; + public long Error { get; set; } protected Task SendAsync(ReadOnlySpan span) { @@ -361,21 +393,35 @@ internal Task EndStreamAsync() { return _pair.Application.Output.CompleteAsync().AsTask(); } + + internal async Task WaitForStreamErrorAsync(Http3ErrorCode protocolError, string expectedErrorMessage) + { + var readResult = await _pair.Application.Input.ReadAsync().DefaultTimeout(); + _testBase.Logger.LogTrace("Input is completed"); + + Assert.True(readResult.IsCompleted); + Assert.Equal(protocolError, (Http3ErrorCode)Error); + + if (expectedErrorMessage != null) + { + Assert.Contains(_testBase.LogMessages, m => m.Exception?.Message.Contains(expectedErrorMessage) ?? false); + } + } } - internal class Http3RequestStream : Http3StreamBase, IHttpHeadersHandler, IProtocolErrorCodeFeature + internal class Http3RequestStream : Http3StreamBase, IHttpHeadersHandler { private TestStreamContext _testStreamContext; + private long _streamId; internal ConnectionContext StreamContext { get; } public bool CanRead => true; public bool CanWrite => true; - public long StreamId => 0; + public long StreamId => _streamId; public bool Disposed => _testStreamContext.Disposed; - public long Error { get; set; } private readonly byte[] _headerEncodingBuffer = new byte[64 * 1024]; private QPackEncoder _qpackEncoder = new QPackEncoder(); @@ -390,7 +436,8 @@ public Http3RequestStream(Http3TestBase testBase, Http3Connection connection) var outputPipeOptions = GetOutputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool); _pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions); - _testStreamContext = new TestStreamContext(canRead: true, canWrite: true, _pair, this); + _streamId = testBase.GetStreamId(0x00); + _testStreamContext = new TestStreamContext(canRead: true, canWrite: true, _pair, this, _streamId); StreamContext = _testStreamContext; } @@ -405,6 +452,17 @@ public async Task SendHeadersAsync(IEnumerable> hea await SendFrameAsync(frame, buffer.Slice(0, length), endStream); } + internal async Task SendHeadersPartialAsync() + { + // Send HEADERS frame header without content. + var outputWriter = _pair.Application.Output; + var frame = new Http3RawFrame(); + frame.PrepareData(); + frame.Length = 10; + Http3FrameWriter.WriteHeader(frame, outputWriter); + await SendAsync(Span.Empty); + } + internal async Task SendDataAsync(Memory data, bool endStream = false) { var frame = new Http3RawFrame(); @@ -454,20 +512,6 @@ public void OnStaticIndexedHeader(int index, ReadOnlySpan value) { _decodedHeaders[((Span)H3StaticTable.GetHeaderFieldAt(index).Name).GetAsciiStringNonNullCharacters()] = value.GetAsciiOrUTF8StringNonNullCharacters(); } - - internal async Task WaitForStreamErrorAsync(Http3ErrorCode protocolError, string expectedErrorMessage) - { - var readResult = await _pair.Application.Input.ReadAsync().DefaultTimeout(); - _testBase.Logger.LogTrace("Input is completed"); - - Assert.True(readResult.IsCompleted); - Assert.Equal(protocolError, (Http3ErrorCode)Error); - - if (expectedErrorMessage != null) - { - Assert.Contains(_testBase.LogMessages, m => m.Exception?.Message.Contains(expectedErrorMessage) ?? false); - } - } } internal class Http3FrameWithPayload : Http3RawFrame @@ -482,24 +526,30 @@ public Http3FrameWithPayload() : base() public ReadOnlySequence PayloadSequence => new ReadOnlySequence(Payload); } - public class Http3ControlStream : Http3StreamBase, IProtocolErrorCodeFeature + public enum StreamInitiator + { + Client, + Server + } + + public class Http3ControlStream : Http3StreamBase { internal ConnectionContext StreamContext { get; } + private long _streamId; public bool CanRead => true; public bool CanWrite => false; - public long StreamId => 0; - - public long Error { get; set; } + public long StreamId => _streamId; - public Http3ControlStream(Http3TestBase testBase) + public Http3ControlStream(Http3TestBase testBase, StreamInitiator initiator) { _testBase = testBase; var inputPipeOptions = GetInputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool); var outputPipeOptions = GetOutputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool); _pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions); - StreamContext = new TestStreamContext(canRead: true, canWrite: false, _pair, this); + _streamId = testBase.GetStreamId(initiator == StreamInitiator.Client ? 0x02 : 0x03); + StreamContext = new TestStreamContext(canRead: true, canWrite: false, _pair, this, _streamId); } public Http3ControlStream(ConnectionContext streamContext) @@ -507,6 +557,12 @@ public Http3ControlStream(ConnectionContext streamContext) StreamContext = streamContext; } + internal async Task ExpectSettingsAsync() + { + var http3WithPayload = await ReceiveFrameAsync(); + Assert.Equal(Http3FrameType.Settings, http3WithPayload.Type); + } + public async Task WriteStreamIdAsync(int id) { var writableBuffer = _pair.Application.Output; @@ -654,7 +710,7 @@ public override async ValueTask AcceptAsync(CancellationToken public override ValueTask ConnectAsync(IFeatureCollection features = null, CancellationToken cancellationToken = default) { - var stream = new Http3ControlStream(_testBase); + var stream = new Http3ControlStream(_testBase, StreamInitiator.Server); ToClientAcceptQueue.Writer.WriteAsync(stream); return new ValueTask(stream.StreamContext); } @@ -672,16 +728,17 @@ public void RequestClose() private class TestStreamContext : ConnectionContext, IStreamDirectionFeature, IStreamIdFeature { private DuplexPipePair _pair; - public TestStreamContext(bool canRead, bool canWrite, DuplexPipePair pair, IProtocolErrorCodeFeature feature) + public TestStreamContext(bool canRead, bool canWrite, DuplexPipePair pair, IProtocolErrorCodeFeature errorCodeFeature, long streamId) { _pair = pair; Features = new FeatureCollection(); Features.Set(this); Features.Set(this); - Features.Set(feature); + Features.Set(errorCodeFeature); CanRead = canRead; CanWrite = canWrite; + StreamId = streamId; } public bool Disposed { get; private set; } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs new file mode 100644 index 000000000000..86922da93e77 --- /dev/null +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs @@ -0,0 +1,215 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests +{ + public class Http3TimeoutTests : Http3TestBase + { + [Fact] + public async Task HEADERS_IncompleteFrameReceivedWithinRequestHeadersTimeout_StreamError() + { + var now = _serviceContext.MockSystemClock.UtcNow; + var limits = _serviceContext.ServerOptions.Limits; + + var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication).DefaultTimeout(); + + var controlStream = await GetInboundControlStream().DefaultTimeout(); + await controlStream.ExpectSettingsAsync().DefaultTimeout(); + + await AssertIsTrueRetryAsync( + () => Connection._streams.Count == 2, + "Wait until streams have been created.").DefaultTimeout(); + + var serverRequestStream = Connection._streams[requestStream.StreamId]; + + await requestStream.SendHeadersPartialAsync().DefaultTimeout(); + + TriggerTick(now); + TriggerTick(now + limits.RequestHeadersTimeout); + + Assert.Equal((now + limits.RequestHeadersTimeout).Ticks, serverRequestStream.HeaderTimeoutTicks); + + TriggerTick(now + limits.RequestHeadersTimeout + TimeSpan.FromTicks(1)); + + await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.RequestRejected, CoreStrings.BadRequest_RequestHeadersTimeout); + } + + [Fact] + public async Task HEADERS_HeaderFrameReceivedWithinRequestHeadersTimeout_Success() + { + var now = _serviceContext.MockSystemClock.UtcNow; + var limits = _serviceContext.ServerOptions.Limits; + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "Custom"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + }; + + var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication).DefaultTimeout(); + + var controlStream = await GetInboundControlStream().DefaultTimeout(); + await controlStream.ExpectSettingsAsync().DefaultTimeout(); + + await AssertIsTrueRetryAsync( + () => Connection._streams.Count == 2, + "Wait until streams have been created.").DefaultTimeout(); + + var serverRequestStream = Connection._streams[requestStream.StreamId]; + + TriggerTick(now); + TriggerTick(now + limits.RequestHeadersTimeout); + + Assert.Equal((now + limits.RequestHeadersTimeout).Ticks, serverRequestStream.HeaderTimeoutTicks); + + await requestStream.SendHeadersAsync(headers).DefaultTimeout(); + + await AssertIsTrueRetryAsync( + () => serverRequestStream.ReceivedHeader, + "Request stream has read headers.").DefaultTimeout(); + + TriggerTick(now + limits.RequestHeadersTimeout + TimeSpan.FromTicks(1)); + + await requestStream.SendDataAsync(Memory.Empty, endStream: true); + + await requestStream.ExpectReceiveEndOfStream(); + } + + [Fact] + public async Task ControlStream_HeaderNotReceivedWithinRequestHeadersTimeout_StreamError() + { + var now = _serviceContext.MockSystemClock.UtcNow; + var limits = _serviceContext.ServerOptions.Limits; + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "Custom"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + }; + + await InitializeConnectionAsync(_noopApplication).DefaultTimeout(); + + var controlStream = await GetInboundControlStream().DefaultTimeout(); + await controlStream.ExpectSettingsAsync().DefaultTimeout(); + + var outboundControlStream = await CreateControlStream(id: null); + + await AssertIsTrueRetryAsync( + () => Connection._streams.Count == 1, + "Wait until streams have been created.").DefaultTimeout(); + + var serverInboundControlStream = Connection._streams[outboundControlStream.StreamId]; + + TriggerTick(now); + TriggerTick(now + limits.RequestHeadersTimeout); + + Assert.Equal((now + limits.RequestHeadersTimeout).Ticks, serverInboundControlStream.HeaderTimeoutTicks); + + TriggerTick(now + limits.RequestHeadersTimeout + TimeSpan.FromTicks(1)); + + await outboundControlStream.WaitForStreamErrorAsync(Http3ErrorCode.StreamCreationError, CoreStrings.Http3ControlStreamHeaderTimeout); + } + + [Fact] + public async Task ControlStream_HeaderReceivedWithinRequestHeadersTimeout_StreamError() + { + var now = _serviceContext.MockSystemClock.UtcNow; + var limits = _serviceContext.ServerOptions.Limits; + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "Custom"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + }; + + await InitializeConnectionAsync(_noopApplication).DefaultTimeout(); + + var controlStream = await GetInboundControlStream().DefaultTimeout(); + await controlStream.ExpectSettingsAsync().DefaultTimeout(); + + var outboundControlStream = await CreateControlStream(id: null); + + await AssertIsTrueRetryAsync( + () => Connection._streams.Count == 1, + "Wait until streams have been created.").DefaultTimeout(); + + var serverInboundControlStream = Connection._streams[outboundControlStream.StreamId]; + + TriggerTick(now); + TriggerTick(now + limits.RequestHeadersTimeout); + + await outboundControlStream.WriteStreamIdAsync(id: 0); + + await AssertIsTrueRetryAsync( + () => serverInboundControlStream.ReceivedHeader, + "Control stream has read header.").DefaultTimeout(); + + TriggerTick(now + limits.RequestHeadersTimeout + TimeSpan.FromTicks(1)); + } + + [Fact] + public async Task ControlStream_RequestHeadersTimeoutMaxValue_ExpirationIsMaxValue() + { + var now = _serviceContext.MockSystemClock.UtcNow; + var limits = _serviceContext.ServerOptions.Limits; + limits.RequestHeadersTimeout = TimeSpan.MaxValue; + + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "Custom"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + }; + + await InitializeConnectionAsync(_noopApplication).DefaultTimeout(); + + var controlStream = await GetInboundControlStream().DefaultTimeout(); + await controlStream.ExpectSettingsAsync().DefaultTimeout(); + + var outboundControlStream = await CreateControlStream(id: null); + + await AssertIsTrueRetryAsync( + () => Connection._streams.Count == 1, + "Wait until streams have been created.").DefaultTimeout(); + + var serverInboundControlStream = Connection._streams[outboundControlStream.StreamId]; + + TriggerTick(now); + + Assert.Equal(TimeSpan.MaxValue.Ticks, serverInboundControlStream.HeaderTimeoutTicks); + } + + private static async Task AssertIsTrueRetryAsync(Func assert, string message) + { + const int Retries = 10; + + for (var i = 0; i < Retries; i++) + { + if (i > 0) + { + await Task.Delay((i + 1) * 10); + } + + if (assert()) + { + return; + } + } + + throw new Exception($"Assert failed after {Retries} retries: {message}"); + } + } +}