diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx index 95dacfea4c3e..0951807eede5 100644 --- a/src/Servers/Kestrel/Core/src/CoreStrings.resx +++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx @@ -654,9 +654,30 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l An error occurred after the response headers were sent, a reset is being sent. - The client sent a DATA frame before the HEADERS frame. + The client sent a DATA frame to a request stream before the HEADERS frame. The client sent a {frameType} frame after trailing HEADERS. - \ No newline at end of file + + The client sent a {frameType} frame to a request stream which isn't supported. + + + The client sent a {frameType} frame to the server which isn't supported. + + + The client sent a {frameType} frame to a control stream which isn't supported. + + + The client sent a SETTINGS frame to a control stream that already has settings. + + + The client sent a {frameType} frame to a control stream before the SETTINGS frame. + + + The client sent a reserved setting identifier: {identifier} + + + The client created multiple inbound {streamType} streams for the connection. + + diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.cs index f174f4b3266a..bdf9cacc1cd9 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.cs @@ -1,6 +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.AspNetCore.Server.Kestrel.Core.Internal.Http3; + namespace System.Net.Http { internal partial class Http3RawFrame @@ -9,9 +11,11 @@ internal partial class Http3RawFrame public Http3FrameType Type { get; internal set; } + public string FormattedType => Http3Formatting.ToFormattedType(Type); + public override string ToString() { - return $"{Type} Length: {Length}"; + return $"{FormattedType} Length: {Length}"; } } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs index 5088438ace97..343637bd41e1 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs @@ -39,7 +39,7 @@ internal class Http3Connection : ITimeoutHandler private readonly Http3PeerSettings _serverSettings = new Http3PeerSettings(); private readonly StreamCloseAwaitable _streamCompletionAwaitable = new StreamCloseAwaitable(); - + private readonly IProtocolErrorCodeFeature _errorCodeFeature; public Http3Connection(Http3ConnectionContext context) { @@ -49,6 +49,8 @@ public Http3Connection(Http3ConnectionContext context) _timeoutControl = new TimeoutControl(this); _context.TimeoutControl ??= _timeoutControl; + _errorCodeFeature = context.ConnectionFeatures.Get()!; + var httpLimits = context.ServiceContext.ServerOptions.Limits; _serverSettings.HeaderTableSize = (uint)httpLimits.Http3.HeaderTableSize; @@ -76,6 +78,7 @@ internal long HighestStreamId public Http3ControlStream? ControlStream { get; set; } public Http3ControlStream? EncoderStream { get; set; } public Http3ControlStream? DecoderStream { get; set; } + public string ConnectionId => _context.ConnectionId; public async Task ProcessStreamsAsync(IHttpApplication httpApplication) where TContext : notnull { @@ -163,7 +166,7 @@ private bool TryClose() return false; } - public void Abort(ConnectionAbortedException ex) + public void Abort(ConnectionAbortedException ex, Http3ErrorCode errorCode) { bool previousState; @@ -180,6 +183,7 @@ public void Abort(ConnectionAbortedException ex) SendGoAway(_highestOpenedStreamId); } + _errorCodeFeature.Error = (long)errorCode; _multiplexedContext.Abort(ex); } } @@ -326,9 +330,8 @@ internal async Task InnerProcessStreamsAsync(IHttpApplication(IHttpApplication 0) @@ -364,7 +367,7 @@ internal async Task InnerProcessStreamsAsync(IHttpApplication()!; + _protocolErrorCodeFeature = context.ConnectionFeatures.Get()!; _frameWriter = new Http3FrameWriter( context.Transport.Output, @@ -144,43 +147,53 @@ private async ValueTask TryReadStreamIdAsync() public async Task ProcessRequestAsync(IHttpApplication application) where TContext : notnull { - var streamType = await TryReadStreamIdAsync(); - - if (streamType == -1) + try { - return; - } + var streamType = await TryReadStreamIdAsync(); - if (streamType == ControlStream) - { - if (!_http3Connection.SetInboundControlStream(this)) + if (streamType == -1) { - // TODO propagate these errors to connection. - throw new Http3ConnectionException("HTTP_STREAM_CREATION_ERROR"); + return; } - await HandleControlStream(); - } - else if (streamType == EncoderStream) - { - if (!_http3Connection.SetInboundEncoderStream(this)) + if (streamType == ControlStream) { - throw new Http3ConnectionException("HTTP_STREAM_CREATION_ERROR"); + 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); + } + + 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 HandleEncodingDecodingTask(); - } - else if (streamType == DecoderStream) - { - if (!_http3Connection.SetInboundDecoderStream(this)) + await HandleEncodingDecodingTask(); + } + else if (streamType == DecoderStream) { - throw new Http3ConnectionException("HTTP_STREAM_CREATION_ERROR"); + 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(); } - else + catch (Http3ConnectionErrorException ex) { - // TODO Close the control stream as it's unexpected. + Log.Http3ConnectionError(_http3Connection.ConnectionId, ex); + _http3Connection.Abort(new ConnectionAbortedException(ex.Message, ex), ex.ErrorCode); } } @@ -212,8 +225,12 @@ private async Task HandleControlStream() return; } } - catch (Http3StreamErrorException) + catch (Http3ConnectionErrorException ex) { + _protocolErrorCodeFeature.Error = (long)ex.ErrorCode; + + Log.Http3ConnectionError(_http3Connection.ConnectionId, ex); + _http3Connection.Abort(new ConnectionAbortedException(ex.Message, ex), ex.ErrorCode); } finally { @@ -238,26 +255,23 @@ private async ValueTask HandleEncodingDecodingTask() private ValueTask ProcessHttp3ControlStream(in ReadOnlySequence payload) { - // Two things: - // settings must be sent as the first frame of each control stream by each peer - // Can't send more than two settings frames. switch (_incomingFrame.Type) { case Http3FrameType.Data: case Http3FrameType.Headers: - case Http3FrameType.DuplicatePush: case Http3FrameType.PushPromise: - throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED"); + // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-7.2 + throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ErrorUnsupportedFrameOnControlStream(_incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame); case Http3FrameType.Settings: return ProcessSettingsFrameAsync(payload); case Http3FrameType.GoAway: - return ProcessGoAwayFrameAsync(payload); + return ProcessGoAwayFrameAsync(); case Http3FrameType.CancelPush: return ProcessCancelPushFrameAsync(); case Http3FrameType.MaxPushId: return ProcessMaxPushIdFrameAsync(); default: - return ProcessUnknownFrameAsync(); + return ProcessUnknownFrameAsync(_incomingFrame.Type); } } @@ -265,7 +279,8 @@ private ValueTask ProcessSettingsFrameAsync(ReadOnlySequence payload) { if (_haveReceivedSettingsFrame) { - throw new Http3ConnectionException("H3_SETTINGS_ERROR"); + // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-settings + throw new Http3ConnectionErrorException(CoreStrings.Http3ErrorControlStreamMultipleSettingsFrames, Http3ErrorCode.UnexpectedFrame); } _haveReceivedSettingsFrame = true; @@ -299,10 +314,19 @@ private void ProcessSetting(long id, long value) // These are client settings, for outbound traffic. switch (id) { + case 0x0: + case 0x2: + case 0x3: + case 0x4: + case 0x5: + // HTTP/2 settings are reserved. + // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-7.2.4.1-5 + 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.MaxHeaderListSize: + case (long)Http3SettingType.MaxFieldSectionSize: _http3Connection.ApplyMaxHeaderListSize(value); break; case (long)Http3SettingType.QPackBlockedStreams: @@ -310,43 +334,58 @@ private void ProcessSetting(long id, long value) break; default: // Ignore all unknown settings. + // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-7.2.4 break; } } - private ValueTask ProcessGoAwayFrameAsync(ReadOnlySequence payload) + private ValueTask ProcessGoAwayFrameAsync() { - throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED"); + EnsureSettingsFrame(Http3FrameType.GoAway); + + // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-goaway + // PUSH is not implemented so nothing to do. + + // TODO: Double check the connection remains open. + return default; } private ValueTask ProcessCancelPushFrameAsync() { - if (!_haveReceivedSettingsFrame) - { - throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED"); - } + EnsureSettingsFrame(Http3FrameType.CancelPush); + + // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-cancel_push + // PUSH is not implemented so nothing to do. return default; } private ValueTask ProcessMaxPushIdFrameAsync() { - if (!_haveReceivedSettingsFrame) - { - throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED"); - } + EnsureSettingsFrame(Http3FrameType.MaxPushId); + + // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-cancel_push + // PUSH is not implemented so nothing to do. return default; } - private ValueTask ProcessUnknownFrameAsync() + private ValueTask ProcessUnknownFrameAsync(Http3FrameType frameType) + { + EnsureSettingsFrame(frameType); + + // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-9 + // Unknown frames must be explicitly ignored. + return default; + } + + private void EnsureSettingsFrame(Http3FrameType frameType) { if (!_haveReceivedSettingsFrame) { - throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED"); + var message = CoreStrings.FormatHttp3ErrorControlStreamFrameReceivedBeforeSettings(Http3Formatting.ToFormattedType(frameType)); + throw new Http3ConnectionErrorException(message, Http3ErrorCode.MissingSettings); } - - return default; } public void StopProcessingNextRequest() diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Formatting.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Formatting.cs new file mode 100644 index 000000000000..91ac629805a7 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Formatting.cs @@ -0,0 +1,49 @@ +// 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; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + internal static class Http3Formatting + { + public static string ToFormattedType(Http3FrameType type) + { + return type switch + { + Http3FrameType.Data => "DATA", + Http3FrameType.Headers => "HEADERS", + Http3FrameType.CancelPush => "CANCEL_PUSH", + Http3FrameType.Settings => "SETTINGS", + Http3FrameType.PushPromise => "PUSH_PROMISE", + Http3FrameType.GoAway => "GO_AWAY", + Http3FrameType.MaxPushId => "MAX_PUSH_ID", + _ => type.ToString() + }; + } + + public static string ToFormattedErrorCode(Http3ErrorCode errorCode) + { + return errorCode switch + { + Http3ErrorCode.NoError => "H3_NO_ERROR", + Http3ErrorCode.ProtocolError => "H3_GENERAL_PROTOCOL_ERROR", + Http3ErrorCode.InternalError => "H3_INTERNAL_ERROR", + Http3ErrorCode.StreamCreationError => "H3_STREAM_CREATION_ERROR", + Http3ErrorCode.ClosedCriticalStream => "H3_CLOSED_CRITICAL_STREAM", + Http3ErrorCode.UnexpectedFrame => "H3_FRAME_UNEXPECTED", + Http3ErrorCode.FrameError => "H3_FRAME_ERROR", + Http3ErrorCode.ExcessiveLoad => "H3_EXCESSIVE_LOAD", + Http3ErrorCode.IdError => "H3_ID_ERROR", + Http3ErrorCode.SettingsError => "H3_SETTINGS_ERROR", + Http3ErrorCode.MissingSettings => "H3_MISSING_SETTINGS", + Http3ErrorCode.RequestRejected => "H3_REQUEST_REJECTED", + Http3ErrorCode.RequestCancelled => "H3_REQUEST_CANCELLED", + Http3ErrorCode.RequestIncomplete => "H3_REQUEST_INCOMPLETE", + Http3ErrorCode.ConnectError => "H3_CONNECT_ERROR", + Http3ErrorCode.VersionFallback => "H3_VERSION_FALLBACK", + _ => errorCode.ToString() + }; + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3PeerSettings.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3PeerSettings.cs index 2d6d5b53b279..32a2616c09ed 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3PeerSettings.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3PeerSettings.cs @@ -28,7 +28,7 @@ internal List GetNonProtocolDefaults() if (MaxRequestHeaderFieldSize != DefaultMaxRequestHeaderFieldSize) { - list.Add(new Http3PeerSetting(Http3SettingType.MaxHeaderListSize, MaxRequestHeaderFieldSize)); + list.Add(new Http3PeerSetting(Http3SettingType.MaxFieldSectionSize, MaxRequestHeaderFieldSize)); } return list; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3SettingType.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3SettingType.cs index c90b2885c215..f7d971d2d0f0 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3SettingType.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3SettingType.cs @@ -5,11 +5,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 { internal enum Http3SettingType : long { + // https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#section-5 QPackMaxTableCapacity = 0x1, /// - /// SETTINGS_MAX_HEADER_LIST_SIZE, default is unlimited. + /// SETTINGS_MAX_FIELD_SECTION_SIZE, default is unlimited. + /// https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#section-5 /// - MaxHeaderListSize = 0x6, + MaxFieldSectionSize = 0x6, + // https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#section-5 QPackBlockedStreams = 0x7 } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.FeatureCollection.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.FeatureCollection.cs index 35a0d2e1b383..90cc3b151e43 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.FeatureCollection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.FeatureCollection.cs @@ -58,7 +58,8 @@ IHeaderDictionary IHttpResponseTrailersFeature.Trailers void IHttpResetFeature.Reset(int errorCode) { - var abortReason = new ConnectionAbortedException(CoreStrings.FormatHttp3StreamResetByApplication((Http3ErrorCode)errorCode)); + var message = CoreStrings.FormatHttp3StreamResetByApplication(Http3Formatting.ToFormattedErrorCode((Http3ErrorCode)errorCode)); + var abortReason = new ConnectionAbortedException(message); ApplicationAbort(abortReason, (Http3ErrorCode)errorCode); } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs index 6e65e4b18999..fa269864302c 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs @@ -387,6 +387,16 @@ public async Task ProcessRequestAsync(IHttpApplication appli error = ex; Abort(new ConnectionAbortedException(ex.Message, ex), ex.ErrorCode); } + catch (Http3ConnectionErrorException ex) + { + 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. + } catch (Exception ex) { error = ex; @@ -446,14 +456,16 @@ private Task ProcessHttp3Stream(IHttpApplication application return ProcessDataFrameAsync(payload); case Http3FrameType.Headers: return ProcessHeadersFrameAsync(application, payload); - // need to be on control stream - case Http3FrameType.DuplicatePush: - case Http3FrameType.PushPromise: case Http3FrameType.Settings: - case Http3FrameType.GoAway: case Http3FrameType.CancelPush: + case Http3FrameType.GoAway: case Http3FrameType.MaxPushId: - throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED"); + // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-7.2.4 + // These frames need to be on a control stream + throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ErrorUnsupportedFrameOnRequestStream(_incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame); + case Http3FrameType.PushPromise: + // The server should never receive push promise + throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ErrorUnsupportedFrameOnServer(_incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame); default: return ProcessUnknownFrameAsync(); } @@ -461,6 +473,7 @@ private Task ProcessHttp3Stream(IHttpApplication application private Task ProcessUnknownFrameAsync() { + // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-9 // Unknown frames must be explicitly ignored. return Task.CompletedTask; } @@ -471,7 +484,7 @@ private Task ProcessHeadersFrameAsync(IHttpApplication appli // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1 if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers) { - throw new Http3StreamErrorException(CoreStrings.FormatHttp3StreamErrorFrameReceivedAfterTrailers(Http3FrameType.Headers), Http3ErrorCode.UnexpectedFrame); + throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3StreamErrorFrameReceivedAfterTrailers(Http3Formatting.ToFormattedType(Http3FrameType.Headers)), Http3ErrorCode.UnexpectedFrame); } if (_requestHeaderParsingState == RequestHeaderParsingState.Headers) @@ -514,14 +527,15 @@ private Task ProcessDataFrameAsync(in ReadOnlySequence payload) // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1 if (_requestHeaderParsingState == RequestHeaderParsingState.Ready) { - throw new Http3StreamErrorException(CoreStrings.Http3StreamErrorDataReceivedBeforeHeaders, Http3ErrorCode.UnexpectedFrame); + throw new Http3ConnectionErrorException(CoreStrings.Http3StreamErrorDataReceivedBeforeHeaders, Http3ErrorCode.UnexpectedFrame); } // DATA frame after trailing headers is invalid. // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1 if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers) { - throw new Http3StreamErrorException(CoreStrings.FormatHttp3StreamErrorFrameReceivedAfterTrailers(Http3FrameType.Data), Http3ErrorCode.UnexpectedFrame); + var message = CoreStrings.FormatHttp3StreamErrorFrameReceivedAfterTrailers(CoreStrings.FormatHttp3StreamErrorFrameReceivedAfterTrailers(Http3Formatting.ToFormattedType(Http3FrameType.Data))); + throw new Http3ConnectionErrorException(message, Http3ErrorCode.UnexpectedFrame); } if (InputRemaining.HasValue) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamErrorException.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamErrorException.cs index 8edccac290bb..6c40b315b39d 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamErrorException.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamErrorException.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 { - class Http3StreamErrorException : Exception + internal class Http3StreamErrorException : Exception { public Http3StreamErrorException(string message, Http3ErrorCode errorCode) : base($"HTTP/3 stream error ({errorCode}): {message}") diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IKestrelTrace.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IKestrelTrace.cs index 7b0829ab9552..34356e071370 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IKestrelTrace.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IKestrelTrace.cs @@ -81,7 +81,7 @@ internal interface IKestrelTrace : ILogger void InvalidResponseHeaderRemoved(); - void Http3ConnectionError(string connectionId, Http3ConnectionException ex); + void Http3ConnectionError(string connectionId, Http3ConnectionErrorException ex); void Http3ConnectionClosing(string connectionId); diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.cs index 8260d6da80f7..82182456580a 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.cs @@ -134,16 +134,16 @@ internal class KestrelTrace : IKestrelTrace LoggerMessage.Define(LogLevel.Debug, new EventId(44, "Http3ConnectionClosed"), @"Connection id ""{ConnectionId}"" is closed. The last processed stream ID was {HighestOpenedStreamId}."); - private static readonly Action _http3StreamAbort = - LoggerMessage.Define(LogLevel.Debug, new EventId(45, "Http3StreamAbort"), + private static readonly Action _http3StreamAbort = + LoggerMessage.Define(LogLevel.Debug, new EventId(45, "Http3StreamAbort"), @"Trace id ""{TraceIdentifier}"": HTTP/3 stream error ""{error}"". An abort is being sent to the stream."); - private static readonly Action _http3FrameReceived = - LoggerMessage.Define(LogLevel.Trace, new EventId(46, "Http3FrameReceived"), + private static readonly Action _http3FrameReceived = + LoggerMessage.Define(LogLevel.Trace, new EventId(46, "Http3FrameReceived"), @"Connection id ""{ConnectionId}"" received {type} frame for stream ID {id} with length {length}."); - private static readonly Action _http3FrameSending = - LoggerMessage.Define(LogLevel.Trace, new EventId(47, "Http3FrameSending"), + private static readonly Action _http3FrameSending = + LoggerMessage.Define(LogLevel.Trace, new EventId(47, "Http3FrameSending"), @"Connection id ""{ConnectionId}"" sending {type} frame for stream ID {id} with length {length}."); protected readonly ILogger _logger; @@ -329,7 +329,7 @@ public void InvalidResponseHeaderRemoved() _invalidResponseHeaderRemoved(_logger, null); } - public void Http3ConnectionError(string connectionId, Http3ConnectionException ex) + public void Http3ConnectionError(string connectionId, Http3ConnectionErrorException ex) { _http3ConnectionError(_logger, connectionId, ex); } @@ -346,17 +346,26 @@ public void Http3ConnectionClosed(string connectionId, long highestOpenedStreamI public void Http3StreamAbort(string traceIdentifier, Http3ErrorCode error, ConnectionAbortedException abortReason) { - _http3StreamAbort(_logger, traceIdentifier, error, abortReason); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _http3StreamAbort(_logger, traceIdentifier, Http3Formatting.ToFormattedErrorCode(error), abortReason); + } } public void Http3FrameReceived(string connectionId, long streamId, Http3RawFrame frame) { - _http3FrameReceived(_logger, connectionId, frame.Type, streamId, frame.Length, null); + if (_logger.IsEnabled(LogLevel.Trace)) + { + _http3FrameReceived(_logger, connectionId, Http3Formatting.ToFormattedType(frame.Type), streamId, frame.Length, null); + } } public void Http3FrameSending(string connectionId, long streamId, Http3RawFrame frame) { - _http3FrameSending(_logger, connectionId, frame.Type, streamId, frame.Length, null); + if (_logger.IsEnabled(LogLevel.Trace)) + { + _http3FrameSending(_logger, connectionId, Http3Formatting.ToFormattedType(frame.Type), streamId, frame.Length, null); + } } public virtual void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTrace.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTrace.cs index 887980770300..2528dd7b6fc7 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTrace.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTrace.cs @@ -60,7 +60,7 @@ public void Http2FrameReceived(string connectionId, Http2Frame frame) { } public void Http2FrameSending(string connectionId, Http2Frame frame) { } public void Http2MaxConcurrentStreamsReached(string connectionId) { } public void InvalidResponseHeaderRemoved() { } - public void Http3ConnectionError(string connectionId, Http3ConnectionException ex) { } + public void Http3ConnectionError(string connectionId, Http3ConnectionErrorException ex) { } public void Http3ConnectionClosing(string connectionId) { } public void Http3ConnectionClosed(string connectionId, long highestOpenedStreamId) { } public void Http3StreamAbort(string traceIdentifier, Http3ErrorCode error, ConnectionAbortedException abortReason) { } diff --git a/src/Servers/Kestrel/shared/test/CompositeKestrelTrace.cs b/src/Servers/Kestrel/shared/test/CompositeKestrelTrace.cs index 3f5271b4ec0c..ef99df0548c6 100644 --- a/src/Servers/Kestrel/shared/test/CompositeKestrelTrace.cs +++ b/src/Servers/Kestrel/shared/test/CompositeKestrelTrace.cs @@ -245,7 +245,7 @@ public void InvalidResponseHeaderRemoved() _trace2.InvalidResponseHeaderRemoved(); } - public void Http3ConnectionError(string connectionId, Http3ConnectionException ex) + public void Http3ConnectionError(string connectionId, Http3ConnectionErrorException ex) { _trace1.Http3ConnectionError(connectionId, ex); _trace2.Http3ConnectionError(connectionId, ex); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs index ef03fbb55958..19bf3cb2e936 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs @@ -3,15 +3,12 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Globalization; using System.Net.Http; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Connections.Features; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Testing; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; using Microsoft.Net.Http.Headers; using Xunit; @@ -19,19 +16,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { public class Http3ConnectionTests : Http3TestBase { - [Fact] - public async Task GoAwayReceived() - { - await InitializeConnectionAsync(_echoApplication); - - var outboundcontrolStream = await CreateControlStream(); - var inboundControlStream = await GetInboundControlStream(); - - Connection.Abort(new ConnectionAbortedException()); - await _closedStateReached.Task.DefaultTimeout(); - await WaitForConnectionErrorAsync(ignoreNonGoAwayFrames: true, expectedLastStreamId: 0, expectedErrorCode: 0); - } - [Fact] public async Task CreateRequestStream_RequestCompleted_Disposed() { @@ -83,5 +67,69 @@ public async Task GracefulServerShutdownSendsGoawayClosesConnection() MultiplexedConnectionContext.ConnectionClosingCts.Cancel(); Assert.Null(await MultiplexedConnectionContext.AcceptAsync().DefaultTimeout()); } + + [Theory] + [InlineData(0x0)] + [InlineData(0x2)] + [InlineData(0x3)] + [InlineData(0x4)] + [InlineData(0x5)] + public async Task SETTINGS_ReservedSettingSent_ConnectionError(long settingIdentifier) + { + await InitializeConnectionAsync(_echoApplication); + + var outboundcontrolStream = await CreateControlStream(); + await outboundcontrolStream.SendSettingsAsync(new List + { + new Http3PeerSetting((Internal.Http3.Http3SettingType) settingIdentifier, 0) // reserved value + }); + + await GetInboundControlStream(); + + await WaitForConnectionErrorAsync( + ignoreNonGoAwayFrames: true, + expectedLastStreamId: 0, + expectedErrorCode: Http3ErrorCode.SettingsError, + expectedErrorMessage: CoreStrings.FormatHttp3ErrorControlStreamReservedSetting($"0x{settingIdentifier.ToString("X", CultureInfo.InvariantCulture)}")); + } + + [Theory] + [InlineData(0, "control")] + [InlineData(2, "encoder")] + [InlineData(3, "decoder")] + public async Task InboundStreams_CreateMultiple_ConnectionError(int streamId, string name) + { + await InitializeConnectionAsync(_noopApplication); + + await CreateControlStream(streamId); + await CreateControlStream(streamId); + + await WaitForConnectionErrorAsync( + ignoreNonGoAwayFrames: true, + expectedLastStreamId: 0, + expectedErrorCode: Http3ErrorCode.StreamCreationError, + expectedErrorMessage: CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams(name)); + } + + [Theory] + [InlineData(nameof(Http3FrameType.Data))] + [InlineData(nameof(Http3FrameType.Headers))] + [InlineData(nameof(Http3FrameType.PushPromise))] + public async Task ControlStream_UnexpectedFrameType_ConnectionError(string frameType) + { + await InitializeConnectionAsync(_noopApplication); + + var controlStream = await CreateControlStream(); + + var frame = new Http3RawFrame(); + frame.Type = Enum.Parse(frameType); + await controlStream.SendFrameAsync(frame, Memory.Empty); + + await WaitForConnectionErrorAsync( + ignoreNonGoAwayFrames: true, + expectedLastStreamId: 0, + expectedErrorCode: Http3ErrorCode.UnexpectedFrame, + expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnControlStream(Http3Formatting.ToFormattedType(frame.Type))); + } } } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs index b05114e7d204..a7fce40ae41f 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; using Microsoft.AspNetCore.Testing; using Microsoft.Net.Http.Headers; using Xunit; @@ -760,7 +761,9 @@ public async Task ResetStream_ReturnStreamError() var doneWithHeaders = await requestStream.SendHeadersAsync(headers, endStream: true); - await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.RequestCancelled, CoreStrings.FormatHttp3StreamResetByApplication(Http3ErrorCode.RequestCancelled)); + await requestStream.WaitForStreamErrorAsync( + Http3ErrorCode.RequestCancelled, + CoreStrings.FormatHttp3StreamResetByApplication(Http3Formatting.ToFormattedErrorCode(Http3ErrorCode.RequestCancelled))); } [Fact] @@ -1541,7 +1544,9 @@ public async Task ResetAfterCompleteAsync_GETWithResponseBodyAndTrailers_ResetsA var decodedTrailers = await requestStream.ExpectHeadersAsync(); Assert.Equal("Custom Value", decodedTrailers["CustomName"]); - await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.NoError, expectedErrorMessage: "The HTTP/3 stream was reset by the application with error code NoError."); + await requestStream.WaitForStreamErrorAsync( + Http3ErrorCode.NoError, + expectedErrorMessage: "The HTTP/3 stream was reset by the application with error code H3_NO_ERROR."); clientTcs.SetResult(0); await appTcs.Task; @@ -1609,7 +1614,9 @@ public async Task ResetAfterCompleteAsync_POSTWithResponseBodyAndTrailers_Reques var decodedTrailers = await requestStream.ExpectHeadersAsync(); Assert.Equal("Custom Value", decodedTrailers["CustomName"]); - await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.NoError, expectedErrorMessage: "The HTTP/3 stream was reset by the application with error code NoError."); + await requestStream.WaitForStreamErrorAsync( + Http3ErrorCode.NoError, + expectedErrorMessage: "The HTTP/3 stream was reset by the application with error code H3_NO_ERROR."); clientTcs.SetResult(0); await appTcs.Task; @@ -1622,7 +1629,9 @@ public async Task DataBeforeHeaders_UnexpectedFrameError() await requestStream.SendDataAsync(Encoding.UTF8.GetBytes("This is invalid.")); - await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.UnexpectedFrame, expectedErrorMessage: "The client sent a DATA frame before the HEADERS frame."); + await requestStream.WaitForStreamErrorAsync( + Http3ErrorCode.UnexpectedFrame, + expectedErrorMessage: CoreStrings.Http3StreamErrorDataReceivedBeforeHeaders); } [Fact] @@ -1680,7 +1689,9 @@ public async Task FrameAfterTrailers_UnexpectedFrameError() await requestStream.SendHeadersAsync(trailers, endStream: false); await requestStream.SendDataAsync(Encoding.UTF8.GetBytes("This is invalid.")); - await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.UnexpectedFrame, expectedErrorMessage: "The client sent a Data frame after trailing HEADERS."); + await requestStream.WaitForStreamErrorAsync( + Http3ErrorCode.UnexpectedFrame, + expectedErrorMessage: CoreStrings.FormatHttp3StreamErrorFrameReceivedAfterTrailers(Http3Formatting.ToFormattedType(Http3FrameType.Data))); } [Fact] @@ -1727,5 +1738,44 @@ public async Task TrailersWithoutEndingStream_ErrorAccessingTrailers() var ex = await Assert.ThrowsAsync(() => readTrailersTcs.Task).DefaultTimeout(); Assert.Equal("The request trailers are not available yet. They may not be available until the full request body is read.", ex.Message); } + + [Theory] + [InlineData(nameof(Http3FrameType.MaxPushId))] + [InlineData(nameof(Http3FrameType.Settings))] + [InlineData(nameof(Http3FrameType.CancelPush))] + [InlineData(nameof(Http3FrameType.GoAway))] + public async Task UnexpectedRequestFrame(string frameType) + { + var requestStream = await InitializeConnectionAndStreamsAsync(_echoApplication); + + var frame = new Http3RawFrame(); + frame.Type = Enum.Parse(frameType); + await requestStream.SendFrameAsync(frame, Memory.Empty); + + await requestStream.WaitForStreamErrorAsync( + Http3ErrorCode.UnexpectedFrame, + expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnRequestStream(frame.FormattedType)); + + await WaitForConnectionErrorAsync( + ignoreNonGoAwayFrames: true, + expectedLastStreamId: 0, + expectedErrorCode: Http3ErrorCode.UnexpectedFrame, + expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnRequestStream(frame.FormattedType)); + } + + [Theory] + [InlineData(nameof(Http3FrameType.PushPromise))] + public async Task UnexpectedServerFrame(string frameType) + { + var requestStream = await InitializeConnectionAndStreamsAsync(_echoApplication); + + var frame = new Http3RawFrame(); + frame.Type = Enum.Parse(frameType); + await requestStream.SendFrameAsync(frame, Memory.Empty); + + await requestStream.WaitForStreamErrorAsync( + Http3ErrorCode.UnexpectedFrame, + expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnServer(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 ea4338926c3f..44ce3ac90d39 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs @@ -132,12 +132,13 @@ internal async ValueTask GetInboundControlStream() return _inboundControlStream; } } - } - + } + return null; } - internal async Task WaitForConnectionErrorAsync(bool ignoreNonGoAwayFrames, long expectedLastStreamId, Http3ErrorCode expectedErrorCode) + internal async Task WaitForConnectionErrorAsync(bool ignoreNonGoAwayFrames, long expectedLastStreamId, Http3ErrorCode expectedErrorCode, params string[] expectedErrorMessage) + where TException : Exception { var frame = await _inboundControlStream.ReceiveFrameAsync(); @@ -149,10 +150,19 @@ internal async Task WaitForConnectionErrorAsync(bool ignoreNonGoAwayFrames, long } } - VerifyGoAway(frame, expectedLastStreamId, expectedErrorCode); + VerifyGoAway(frame, expectedLastStreamId); + + Assert.Equal((Http3ErrorCode)expectedErrorCode, (Http3ErrorCode)MultiplexedConnectionContext.Error); + + if (expectedErrorMessage?.Length > 0) + { + var message = Assert.Single(LogMessages, m => m.Exception is TException); + + Assert.Contains(expectedErrorMessage, expected => message.Exception.Message.Contains(expected)); + } } - internal void VerifyGoAway(Http3FrameWithPayload frame, long expectedLastStreamId, Http3ErrorCode expectedErrorCode) + internal void VerifyGoAway(Http3FrameWithPayload frame, long expectedLastStreamId) { Assert.Equal(Http3FrameType.GoAway, frame.Type); var payload = frame.Payload; @@ -170,6 +180,8 @@ protected async Task InitializeConnectionAsync(RequestDelegate application) // Skip all heartbeat and lifetime notification feature registrations. _connectionTask = Connection.ProcessStreamsAsync(new DummyApplication(application)); + await GetInboundControlStream(); + await Task.CompletedTask; } @@ -329,6 +341,19 @@ internal async Task ReceiveFrameAsync() } } } + + internal async Task SendFrameAsync(Http3RawFrame frame, Memory data, bool endStream = false) + { + var outputWriter = _pair.Application.Output; + frame.Length = data.Length; + Http3FrameWriter.WriteHeader(frame, outputWriter); + await SendAsync(data.Span); + + if (endStream) + { + await _pair.Application.Output.CompleteAsync(); + } + } } internal class Http3RequestStream : Http3StreamBase, IHttpHeadersHandler, IProtocolErrorCodeFeature @@ -364,37 +389,21 @@ public Http3RequestStream(Http3TestBase testBase, Http3Connection connection) public async Task SendHeadersAsync(IEnumerable> headers, bool endStream = false) { - var outputWriter = _pair.Application.Output; var frame = new Http3RawFrame(); frame.PrepareHeaders(); var buffer = _headerEncodingBuffer.AsMemory(); var done = _qpackEncoder.BeginEncode(headers, buffer.Span, out var length); - frame.Length = length; - // TODO may want to modify behavior of input frames to mock different client behavior (client can send anything). - Http3FrameWriter.WriteHeader(frame, outputWriter); - await SendAsync(buffer.Span.Slice(0, length)); - if (endStream) - { - await _pair.Application.Output.CompleteAsync(); - } + await SendFrameAsync(frame, buffer.Slice(0, length), endStream); return done; } internal async Task SendDataAsync(Memory data, bool endStream = false) { - var outputWriter = _pair.Application.Output; var frame = new Http3RawFrame(); frame.PrepareData(); - frame.Length = data.Length; - Http3FrameWriter.WriteHeader(frame, outputWriter); - await SendAsync(data.Span); - - if (endStream) - { - await _pair.Application.Output.CompleteAsync(); - } + await SendFrameAsync(frame, data, endStream); } internal async Task> ExpectHeadersAsync() @@ -446,7 +455,7 @@ internal async Task WaitForStreamErrorAsync(Http3ErrorCode protocolError, string _testBase.Logger.LogTrace("Input is completed"); Assert.True(readResult.IsCompleted); - Assert.Equal((long)protocolError, Error); + Assert.Equal(protocolError, (Http3ErrorCode)Error); if (expectedErrorMessage != null) { @@ -484,7 +493,7 @@ public Http3ControlStream(Http3TestBase 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: false, canWrite: true, _pair, this); + StreamContext = new TestStreamContext(canRead: true, canWrite: false, _pair, this); } public Http3ControlStream(ConnectionContext streamContext) @@ -508,6 +517,41 @@ void WriteSpan(PipeWriter pw) await FlushAsync(writableBuffer); } + internal async Task SendSettingsAsync(List settings, bool endStream = false) + { + var frame = new Http3RawFrame(); + frame.PrepareSettings(); + + var settingsLength = CalculateSettingsSize(settings); + var buffer = new byte[settingsLength]; + WriteSettings(settings, buffer); + + await SendFrameAsync(frame, buffer, endStream); + } + + internal static int CalculateSettingsSize(List settings) + { + var length = 0; + foreach (var setting in settings) + { + length += VariableLengthIntegerHelper.GetByteCount((long)setting.Parameter); + length += VariableLengthIntegerHelper.GetByteCount(setting.Value); + } + return length; + } + + internal static void WriteSettings(List settings, Span destination) + { + foreach (var setting in settings) + { + var parameterLength = VariableLengthIntegerHelper.WriteInteger(destination, (long)setting.Parameter); + destination = destination.Slice(parameterLength); + + var valueLength = VariableLengthIntegerHelper.WriteInteger(destination, (long)setting.Value); + destination = destination.Slice(valueLength); + } + } + public async ValueTask TryReadStreamIdAsync() { while (true) @@ -541,7 +585,7 @@ public async ValueTask TryReadStreamIdAsync() } } - public class TestMultiplexedConnectionContext : MultiplexedConnectionContext, IConnectionLifetimeNotificationFeature, IConnectionLifetimeFeature, IConnectionHeartbeatFeature + public class TestMultiplexedConnectionContext : MultiplexedConnectionContext, IConnectionLifetimeNotificationFeature, IConnectionLifetimeFeature, IConnectionHeartbeatFeature, IProtocolErrorCodeFeature { public readonly Channel ToServerAcceptQueue = Channel.CreateUnbounded(new UnboundedChannelOptions { @@ -563,6 +607,7 @@ public TestMultiplexedConnectionContext(Http3TestBase testBase) Features = new FeatureCollection(); Features.Set(this); Features.Set(this); + Features.Set(this); ConnectionClosedRequested = ConnectionClosingCts.Token; } @@ -576,6 +621,8 @@ public TestMultiplexedConnectionContext(Http3TestBase testBase) public CancellationTokenSource ConnectionClosingCts { get; set; } = new CancellationTokenSource(); + public long Error { get; set; } + public override void Abort() { Abort(new ConnectionAbortedException());