diff --git a/benchmarks/Kestrel.Performance/BinaryPrimitivesBenchmark.cs b/benchmarks/Kestrel.Performance/BinaryPrimitivesBenchmark.cs new file mode 100644 index 0000000000..1761178991 --- /dev/null +++ b/benchmarks/Kestrel.Performance/BinaryPrimitivesBenchmark.cs @@ -0,0 +1,44 @@ +// 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 BenchmarkDotNet.Attributes; + +namespace Microsoft.AspNetCore.Server.Kestrel.Performance +{ + public class BinaryPrimitivesBenchmark + { + private const int Iterations = 100; + + private byte[] _data; + + [GlobalSetup] + public void Setup() + { + _data = new byte[4]; + } + + [Benchmark(Baseline = true, OperationsPerInvoke = Iterations)] + public uint GetUInt32AsBitwise() + { + var v = 0u; + for (int i = 0; i < 1_000_000; i++) + { + v = (uint)((_data[0] << 24) | (_data[1] << 16) | (_data[2] << 8) | _data[3]); + } + return v; + } + + [Benchmark(OperationsPerInvoke = Iterations)] + public unsafe uint GetUInt32AsBinary() + { + var v = 0u; + for (int i = 0; i < 1_000_000; i++) + { + v = BinaryPrimitives.ReadUInt32BigEndian(_data.AsSpan()); + } + return v; + } + } +} diff --git a/benchmarks/Kestrel.Performance/README.md b/benchmarks/Kestrel.Performance/README.md index 91991f82ff..50418ef343 100644 --- a/benchmarks/Kestrel.Performance/README.md +++ b/benchmarks/Kestrel.Performance/README.md @@ -1,5 +1,7 @@ Compile the solution in Release mode (so Kestrel is available in release) - +``` +build /t:compile /p:Configuration=Release +``` To run a specific benchmark add it as parameter ``` dotnet run -f netcoreapp2.0 -c Release RequestParsing diff --git a/src/Kestrel.Core/CoreStrings.resx b/src/Kestrel.Core/CoreStrings.resx index f119165500..6d72c97ca4 100644 --- a/src/Kestrel.Core/CoreStrings.resx +++ b/src/Kestrel.Core/CoreStrings.resx @@ -564,7 +564,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l More data received than specified in the Content-Length header. - An error occured after the response headers were sent, a reset is being sent. + An error occurred after the response headers were sent, a reset is being sent. A new stream was refused because this connection has reached its stream limit. @@ -572,4 +572,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l A value greater than zero is required. + + The frame is too short to contain the fields indicated by the given flags. + \ No newline at end of file diff --git a/src/Kestrel.Core/Internal/Http2/Bitshifter.cs b/src/Kestrel.Core/Internal/Http2/Bitshifter.cs new file mode 100644 index 0000000000..7c3915203f --- /dev/null +++ b/src/Kestrel.Core/Internal/Http2/Bitshifter.cs @@ -0,0 +1,46 @@ +// 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.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 +{ + // Mimics BinaryPrimities with oddly sized units + internal class Bitshifter + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ReadUInt24BigEndian(ReadOnlySpan source) + { + return (uint)((source[0] << 16) | (source[1] << 8) | source[2]); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt24BigEndian(Span destination, uint value) + { + Debug.Assert(value <= 0xFF_FF_FF, value.ToString()); + destination[0] = (byte)((value & 0xFF_00_00) >> 16); + destination[1] = (byte)((value & 0x00_FF_00) >> 8); + destination[2] = (byte)(value & 0x00_00_FF); + } + + // Drops the highest order bit + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ReadUInt31BigEndian(ReadOnlySpan source) + { + return BinaryPrimitives.ReadUInt32BigEndian(source) & 0x7F_FF_FF_FF; + } + + // Does not overwrite the highest order bit + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt31BigEndian(Span destination, uint value) + { + Debug.Assert(value <= 0x7F_FF_FF_FF, value.ToString()); + // Keep the highest bit + var reserved = (destination[0] & 0x80u) << 24; + BinaryPrimitives.WriteUInt32BigEndian(destination, value | reserved); + } + } +} diff --git a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs index b447fffdf3..3bd7aa7c29 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs @@ -417,7 +417,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamIdZero(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR); } - if (_incomingFrame.DataHasPadding && _incomingFrame.DataPadLength >= _incomingFrame.Length) + // The Padding field is missing + if (_incomingFrame.DataPayloadOffset > _incomingFrame.PayloadLength) + { + throw new Http2ConnectionErrorException(CoreStrings.Http2FrameMissingFields, Http2ErrorCode.PROTOCOL_ERROR); + } + + if (_incomingFrame.DataHasPadding && _incomingFrame.DataPadLength >= _incomingFrame.PayloadLength) { throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorPaddingTooLong(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR); } @@ -467,7 +473,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamIdZero(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR); } - if (_incomingFrame.HeadersHasPadding && _incomingFrame.HeadersPadLength >= _incomingFrame.Length) + // Padding or priority fields are missing + if (_incomingFrame.HeadersPayloadOffset > _incomingFrame.PayloadLength) + { + throw new Http2ConnectionErrorException(CoreStrings.Http2FrameMissingFields, Http2ErrorCode.PROTOCOL_ERROR); + } + + if (_incomingFrame.HeadersHasPadding && _incomingFrame.HeadersPadLength >= _incomingFrame.PayloadLength - 1) { throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorPaddingTooLong(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR); } @@ -492,7 +504,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } // This is the last chance for the client to send END_STREAM - if ((_incomingFrame.HeadersFlags & Http2HeadersFrameFlags.END_STREAM) == 0) + if (!_incomingFrame.HeadersEndStream) { throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorHeadersWithTrailersNoEndStream, Http2ErrorCode.PROTOCOL_ERROR); } @@ -501,8 +513,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _currentHeadersStream = stream; _requestHeaderParsingState = RequestHeaderParsingState.Trailers; - var endHeaders = (_incomingFrame.HeadersFlags & Http2HeadersFrameFlags.END_HEADERS) == Http2HeadersFrameFlags.END_HEADERS; - await DecodeTrailersAsync(endHeaders, _incomingFrame.HeadersPayload); + await DecodeTrailersAsync(_incomingFrame.HeadersEndHeaders, _incomingFrame.HeadersPayload); } else if (_incomingFrame.StreamId <= _highestOpenedStreamId) { @@ -538,8 +549,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _currentHeadersStream.Reset(); _headerFlags = _incomingFrame.HeadersFlags; - var endHeaders = (_incomingFrame.HeadersFlags & Http2HeadersFrameFlags.END_HEADERS) == Http2HeadersFrameFlags.END_HEADERS; - await DecodeHeadersAsync(application, endHeaders, _incomingFrame.HeadersPayload); + await DecodeHeadersAsync(application, _incomingFrame.HeadersEndHeaders, _incomingFrame.HeadersPayload); } } @@ -560,7 +570,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamSelfDependency(_incomingFrame.Type, _incomingFrame.StreamId), Http2ErrorCode.PROTOCOL_ERROR); } - if (_incomingFrame.Length != 5) + if (_incomingFrame.PayloadLength != 5) { throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(_incomingFrame.Type, 5), Http2ErrorCode.FRAME_SIZE_ERROR); } @@ -580,7 +590,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamIdZero(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR); } - if (_incomingFrame.Length != 4) + if (_incomingFrame.PayloadLength != 4) { throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(_incomingFrame.Type, 4), Http2ErrorCode.FRAME_SIZE_ERROR); } @@ -603,9 +613,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamIdNotZero(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR); } - if ((_incomingFrame.SettingsFlags & Http2SettingsFrameFlags.ACK) == Http2SettingsFrameFlags.ACK) + if (_incomingFrame.SettingsAck) { - if (_incomingFrame.Length != 0) + if (_incomingFrame.PayloadLength != 0) { throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorSettingsAckLengthNotZero, Http2ErrorCode.FRAME_SIZE_ERROR); } @@ -613,7 +623,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 return Task.CompletedTask; } - if (_incomingFrame.Length % 6 != 0) + if (_incomingFrame.PayloadLength % 6 != 0) { throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorSettingsLengthNotMultipleOfSix, Http2ErrorCode.FRAME_SIZE_ERROR); } @@ -666,12 +676,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamIdNotZero(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR); } - if (_incomingFrame.Length != 8) + if (_incomingFrame.PayloadLength != 8) { throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(_incomingFrame.Type, 8), Http2ErrorCode.FRAME_SIZE_ERROR); } - if ((_incomingFrame.PingFlags & Http2PingFrameFlags.ACK) == Http2PingFrameFlags.ACK) + if (_incomingFrame.PingAck) { // TODO: verify that payload is equal to the outgoing PING frame return Task.CompletedTask; @@ -704,7 +714,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorHeadersInterleaved(_incomingFrame.Type, _incomingFrame.StreamId, _currentHeadersStream.StreamId), Http2ErrorCode.PROTOCOL_ERROR); } - if (_incomingFrame.Length != 4) + if (_incomingFrame.PayloadLength != 4) { throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(_incomingFrame.Type, 4), Http2ErrorCode.FRAME_SIZE_ERROR); } @@ -767,15 +777,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorHeadersInterleaved(_incomingFrame.Type, _incomingFrame.StreamId, _currentHeadersStream.StreamId), Http2ErrorCode.PROTOCOL_ERROR); } - var endHeaders = (_incomingFrame.ContinuationFlags & Http2ContinuationFrameFlags.END_HEADERS) == Http2ContinuationFrameFlags.END_HEADERS; - if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers) { - return DecodeTrailersAsync(endHeaders, _incomingFrame.Payload); + return DecodeTrailersAsync(_incomingFrame.ContinuationEndHeaders, _incomingFrame.Payload); } else { - return DecodeHeadersAsync(application, endHeaders, _incomingFrame.Payload); + return DecodeHeadersAsync(application, _incomingFrame.ContinuationEndHeaders, _incomingFrame.Payload); } } diff --git a/src/Kestrel.Core/Internal/Http2/Http2Frame.Continuation.cs b/src/Kestrel.Core/Internal/Http2/Http2Frame.Continuation.cs index d599864b7b..26f3afe92b 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.Continuation.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.Continuation.cs @@ -4,6 +4,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { + /* https://tools.ietf.org/html/rfc7540#section-6.10 + +---------------------------------------------------------------+ + | Header Block Fragment (*) ... + +---------------------------------------------------------------+ + */ public partial class Http2Frame { public Http2ContinuationFrameFlags ContinuationFlags @@ -12,9 +17,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 set => Flags = (byte)value; } + public bool ContinuationEndHeaders => (ContinuationFlags & Http2ContinuationFrameFlags.END_HEADERS) == Http2ContinuationFrameFlags.END_HEADERS; + public void PrepareContinuation(Http2ContinuationFrameFlags flags, int streamId) { - Length = MinAllowedMaxFrameSize - HeaderLength; + PayloadLength = MinAllowedMaxFrameSize - HeaderLength; Type = Http2FrameType.CONTINUATION; ContinuationFlags = flags; StreamId = streamId; diff --git a/src/Kestrel.Core/Internal/Http2/Http2Frame.Data.cs b/src/Kestrel.Core/Internal/Http2/Http2Frame.Data.cs index 2310db23ad..b2df5c4bfd 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.Data.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.Data.cs @@ -2,10 +2,18 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { + /* + +---------------+ + |Pad Length? (8)| + +---------------+-----------------------------------------------+ + | Data (*) ... + +---------------------------------------------------------------+ + | Padding (*) ... + +---------------------------------------------------------------+ + */ public partial class Http2Frame { public Http2DataFrameFlags DataFlags @@ -14,23 +22,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 set => Flags = (byte)value; } + public bool DataEndStream => (DataFlags & Http2DataFrameFlags.END_STREAM) == Http2DataFrameFlags.END_STREAM; + public bool DataHasPadding => (DataFlags & Http2DataFrameFlags.PADDED) == Http2DataFrameFlags.PADDED; public byte DataPadLength { - get => DataHasPadding ? _data[PayloadOffset] : (byte)0; - set => _data[PayloadOffset] = value; + get => DataHasPadding ? Payload[0] : (byte)0; + set => Payload[0] = value; } - public ArraySegment DataPayload => DataHasPadding - ? new ArraySegment(_data, PayloadOffset + 1, Length - DataPadLength - 1) - : new ArraySegment(_data, PayloadOffset, Length); + public int DataPayloadOffset => DataHasPadding ? 1 : 0; + + private int DataPayloadLength => PayloadLength - DataPayloadOffset - DataPadLength; + + public Span DataPayload => Payload.Slice(DataPayloadOffset, DataPayloadLength); public void PrepareData(int streamId, byte? padLength = null) { var padded = padLength != null; - Length = MinAllowedMaxFrameSize; + PayloadLength = MinAllowedMaxFrameSize; Type = Http2FrameType.DATA; DataFlags = padded ? Http2DataFrameFlags.PADDED : Http2DataFrameFlags.NONE; StreamId = streamId; @@ -38,40 +50,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 if (padded) { DataPadLength = padLength.Value; - Payload.Slice(Length - padLength.Value).Fill(0); - } - } - - private void DataTraceFrame(ILogger logger) - { - logger.LogTrace("'DATA' Frame. Flags = {DataFlags}, PadLength = {PadLength}, PayloadLength = {PayloadLength}", DataFlags, DataPadLength, DataPayload.Count); - } - - internal object ShowFlags() - { - switch (Type) - { - case Http2FrameType.CONTINUATION: - return ContinuationFlags; - case Http2FrameType.DATA: - return DataFlags; - case Http2FrameType.HEADERS: - return HeadersFlags; - case Http2FrameType.SETTINGS: - return SettingsFlags; - case Http2FrameType.PING: - return PingFlags; - - // Not Implemented - case Http2FrameType.PUSH_PROMISE: - - // No flags defined - case Http2FrameType.PRIORITY: - case Http2FrameType.RST_STREAM: - case Http2FrameType.GOAWAY: - case Http2FrameType.WINDOW_UPDATE: - default: - return $"0x{Flags:x}"; + Payload.Slice(PayloadLength - padLength.Value).Fill(0); } } } diff --git a/src/Kestrel.Core/Internal/Http2/Http2Frame.GoAway.cs b/src/Kestrel.Core/Internal/Http2/Http2Frame.GoAway.cs index e3bec2afa2..602c5f1fac 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.GoAway.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.GoAway.cs @@ -1,37 +1,38 @@ // 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.Buffers.Binary; + namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { + /* https://tools.ietf.org/html/rfc7540#section-6.8 + +-+-------------------------------------------------------------+ + |R| Last-Stream-ID (31) | + +-+-------------------------------------------------------------+ + | Error Code (32) | + +---------------------------------------------------------------+ + | Additional Debug Data (*) | + +---------------------------------------------------------------+ + */ public partial class Http2Frame { + private const int ErrorCodeOffset = 4; + public int GoAwayLastStreamId { - get => (Payload[0] << 24) | (Payload[1] << 16) | (Payload[2] << 8) | Payload[3]; - set - { - Payload[0] = (byte)((value >> 24) & 0xff); - Payload[1] = (byte)((value >> 16) & 0xff); - Payload[2] = (byte)((value >> 8) & 0xff); - Payload[3] = (byte)(value & 0xff); - } + get => (int)Bitshifter.ReadUInt31BigEndian(Payload); + set => Bitshifter.WriteUInt31BigEndian(Payload, (uint)value); } public Http2ErrorCode GoAwayErrorCode { - get => (Http2ErrorCode)((Payload[4] << 24) | (Payload[5] << 16) | (Payload[6] << 8) | Payload[7]); - set - { - Payload[4] = (byte)(((uint)value >> 24) & 0xff); - Payload[5] = (byte)(((uint)value >> 16) & 0xff); - Payload[6] = (byte)(((uint)value >> 8) & 0xff); - Payload[7] = (byte)((uint)value & 0xff); - } + get => (Http2ErrorCode)BinaryPrimitives.ReadUInt32BigEndian(Payload.Slice(ErrorCodeOffset)); + set => BinaryPrimitives.WriteUInt32BigEndian(Payload.Slice(ErrorCodeOffset), (uint)value); } public void PrepareGoAway(int lastStreamId, Http2ErrorCode errorCode) { - Length = 8; + PayloadLength = 8; Type = Http2FrameType.GOAWAY; Flags = 0; StreamId = 0; diff --git a/src/Kestrel.Core/Internal/Http2/Http2Frame.Headers.cs b/src/Kestrel.Core/Internal/Http2/Http2Frame.Headers.cs index 52c20bde94..e89a2e1675 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.Headers.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.Headers.cs @@ -5,6 +5,19 @@ using System; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { + /* https://tools.ietf.org/html/rfc7540#section-6.2 + +---------------+ + |Pad Length? (8)| + +-+-------------+-----------------------------------------------+ + |E| Stream Dependency? (31) | + +-+-------------+-----------------------------------------------+ + | Weight? (8) | + +-+-------------+-----------------------------------------------+ + | Header Block Fragment (*) ... + +---------------------------------------------------------------+ + | Padding (*) ... + +---------------------------------------------------------------+ + */ public partial class Http2Frame { public Http2HeadersFrameFlags HeadersFlags @@ -13,57 +26,45 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 set => Flags = (byte)value; } - public bool HeadersHasPadding => (HeadersFlags & Http2HeadersFrameFlags.PADDED) == Http2HeadersFrameFlags.PADDED; + public bool HeadersEndHeaders => (HeadersFlags & Http2HeadersFrameFlags.END_HEADERS) == Http2HeadersFrameFlags.END_HEADERS; - public byte HeadersPadLength - { - get => HeadersHasPadding ? _data[HeaderLength] : (byte)0; - set => _data[HeaderLength] = value; - } + public bool HeadersEndStream => (HeadersFlags & Http2HeadersFrameFlags.END_STREAM) == Http2HeadersFrameFlags.END_STREAM; + + public bool HeadersHasPadding => (HeadersFlags & Http2HeadersFrameFlags.PADDED) == Http2HeadersFrameFlags.PADDED; public bool HeadersHasPriority => (HeadersFlags & Http2HeadersFrameFlags.PRIORITY) == Http2HeadersFrameFlags.PRIORITY; - public byte HeadersPriority + public byte HeadersPadLength { - get => _data[HeadersPriorityOffset]; - set => _data[HeadersPriorityOffset] = value; + get => HeadersHasPadding ? Payload[0] : (byte)0; + set => Payload[0] = value; } - private int HeadersPriorityOffset => PayloadOffset + (HeadersHasPadding ? 1 : 0) + 4; + private int HeadersStreamDependencyOffset => HeadersHasPadding ? 1 : 0; public int HeadersStreamDependency { - get - { - var offset = HeadersStreamDependencyOffset; - - return (int)((uint)((_data[offset] << 24) - | (_data[offset + 1] << 16) - | (_data[offset + 2] << 8) - | _data[offset + 3]) & 0x7fffffff); - } - set - { - var offset = HeadersStreamDependencyOffset; - - _data[offset] = (byte)((value & 0xff000000) >> 24); - _data[offset + 1] = (byte)((value & 0x00ff0000) >> 16); - _data[offset + 2] = (byte)((value & 0x0000ff00) >> 8); - _data[offset + 3] = (byte)(value & 0x000000ff); - } + get => (int)Bitshifter.ReadUInt31BigEndian(Payload.Slice(HeadersStreamDependencyOffset)); + set => Bitshifter.WriteUInt31BigEndian(Payload.Slice(HeadersStreamDependencyOffset), (uint)value); } - private int HeadersStreamDependencyOffset => PayloadOffset + (HeadersHasPadding ? 1 : 0); + private int HeadersPriorityWeightOffset => HeadersStreamDependencyOffset + 4; - public Span HeadersPayload => new Span(_data, HeadersPayloadOffset, HeadersPayloadLength); + public byte HeadersPriorityWeight + { + get => Payload[HeadersPriorityWeightOffset]; + set => Payload[HeadersPriorityWeightOffset] = value; + } - private int HeadersPayloadOffset => PayloadOffset + (HeadersHasPadding ? 1 : 0) + (HeadersHasPriority ? 5 : 0); + public int HeadersPayloadOffset => (HeadersHasPadding ? 1 : 0) + (HeadersHasPriority ? 5 : 0); - private int HeadersPayloadLength => Length - ((HeadersHasPadding ? 1 : 0) + (HeadersHasPriority ? 5 : 0)) - HeadersPadLength; + private int HeadersPayloadLength => PayloadLength - HeadersPayloadOffset - HeadersPadLength; + + public Span HeadersPayload => Payload.Slice(HeadersPayloadOffset, HeadersPayloadLength); public void PrepareHeaders(Http2HeadersFrameFlags flags, int streamId) { - Length = MinAllowedMaxFrameSize - HeaderLength; + PayloadLength = MinAllowedMaxFrameSize - HeaderLength; Type = Http2FrameType.HEADERS; HeadersFlags = flags; StreamId = streamId; diff --git a/src/Kestrel.Core/Internal/Http2/Http2Frame.Ping.cs b/src/Kestrel.Core/Internal/Http2/Http2Frame.Ping.cs index cbbdc88d41..239c006c1b 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.Ping.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.Ping.cs @@ -4,6 +4,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { + /* https://tools.ietf.org/html/rfc7540#section-6.7 + +---------------------------------------------------------------+ + | | + | Opaque Data (64) | + | | + +---------------------------------------------------------------+ + */ public partial class Http2Frame { public Http2PingFrameFlags PingFlags @@ -12,9 +19,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 set => Flags = (byte)value; } + public bool PingAck => (PingFlags & Http2PingFrameFlags.ACK) == Http2PingFrameFlags.ACK; + public void PreparePing(Http2PingFrameFlags flags) { - Length = 8; + PayloadLength = 8; Type = Http2FrameType.PING; PingFlags = flags; StreamId = 0; diff --git a/src/Kestrel.Core/Internal/Http2/Http2Frame.Priority.cs b/src/Kestrel.Core/Internal/Http2/Http2Frame.Priority.cs index 02f9bf02f9..53f1d9e368 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.Priority.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.Priority.cs @@ -3,50 +3,49 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { + /* https://tools.ietf.org/html/rfc7540#section-6.3 + +-+-------------------------------------------------------------+ + |E| Stream Dependency (31) | + +-+-------------+-----------------------------------------------+ + | Weight (8) | + +-+-------------+ + */ public partial class Http2Frame { + private const int PriorityWeightOffset = 4; + public int PriorityStreamDependency { - get => ((_data[PayloadOffset] << 24) - | (_data[PayloadOffset + 1] << 16) - | (_data[PayloadOffset + 2] << 8) - | _data[PayloadOffset + 3]) & 0x7fffffff; - set - { - _data[PayloadOffset] = (byte)((value & 0x7f000000) >> 24); - _data[PayloadOffset + 1] = (byte)((value & 0x00ff0000) >> 16); - _data[PayloadOffset + 2] = (byte)((value & 0x0000ff00) >> 8); - _data[PayloadOffset + 3] = (byte)(value & 0x000000ff); - } + get => (int)Bitshifter.ReadUInt31BigEndian(Payload); + set => Bitshifter.WriteUInt31BigEndian(Payload, (uint)value); } - public bool PriorityIsExclusive { - get => (_data[PayloadOffset] & 0x80000000) != 0; + get => (Payload[0] & 0x80) != 0; set { if (value) { - _data[PayloadOffset] |= 0x80; + Payload[0] |= 0x80; } else { - _data[PayloadOffset] &= 0x7f; + Payload[0] &= 0x7f; } } } public byte PriorityWeight { - get => _data[PayloadOffset + 4]; - set => _data[PayloadOffset] = value; + get => Payload[PriorityWeightOffset]; + set => Payload[PriorityWeightOffset] = value; } public void PreparePriority(int streamId, int streamDependency, bool exclusive, byte weight) { - Length = 5; + PayloadLength = 5; Type = Http2FrameType.PRIORITY; StreamId = streamId; PriorityStreamDependency = streamDependency; diff --git a/src/Kestrel.Core/Internal/Http2/Http2Frame.RstStream.cs b/src/Kestrel.Core/Internal/Http2/Http2Frame.RstStream.cs index 612d778d14..c9c2716f3b 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.RstStream.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.RstStream.cs @@ -1,25 +1,26 @@ // 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.Buffers.Binary; + namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { + /* https://tools.ietf.org/html/rfc7540#section-6.4 + +---------------------------------------------------------------+ + | Error Code (32) | + +---------------------------------------------------------------+ + */ public partial class Http2Frame { public Http2ErrorCode RstStreamErrorCode { - get => (Http2ErrorCode)((Payload[0] << 24) | (Payload[1] << 16) | (Payload[2] << 8) | Payload[3]); - set - { - Payload[0] = (byte)(((uint)value >> 24) & 0xff); - Payload[1] = (byte)(((uint)value >> 16) & 0xff); - Payload[2] = (byte)(((uint)value >> 8) & 0xff); - Payload[3] = (byte)((uint)value & 0xff); - } + get => (Http2ErrorCode)BinaryPrimitives.ReadUInt32BigEndian(Payload); + set => BinaryPrimitives.WriteUInt32BigEndian(Payload, (uint)value); } public void PrepareRstStream(int streamId, Http2ErrorCode errorCode) { - Length = 4; + PayloadLength = 4; Type = Http2FrameType.RST_STREAM; Flags = 0; StreamId = streamId; diff --git a/src/Kestrel.Core/Internal/Http2/Http2Frame.Settings.cs b/src/Kestrel.Core/Internal/Http2/Http2Frame.Settings.cs index c3da784e01..3039ec1b76 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.Settings.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.Settings.cs @@ -3,10 +3,17 @@ using System.Buffers.Binary; using System.Collections.Generic; -using System.Linq; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { + /* https://tools.ietf.org/html/rfc7540#section-6.5.1 + List of: + +-------------------------------+ + | Identifier (16) | + +-------------------------------+-------------------------------+ + | Value (32) | + +---------------------------------------------------------------+ + */ public partial class Http2Frame { private const int SettingSize = 6; // 2 bytes for the id, 4 bytes for the value. @@ -17,10 +24,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 set => Flags = (byte)value; } + public bool SettingsAck => (SettingsFlags & Http2SettingsFrameFlags.ACK) == Http2SettingsFrameFlags.ACK; + public int SettingsCount { - get => Length / SettingSize; - set => Length = value * SettingSize; + get => PayloadLength / SettingSize; + set => PayloadLength = value * SettingSize; } public IList GetSettings() diff --git a/src/Kestrel.Core/Internal/Http2/Http2Frame.WindowUpdate.cs b/src/Kestrel.Core/Internal/Http2/Http2Frame.WindowUpdate.cs index deb06ac02a..098064b3e3 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.WindowUpdate.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.WindowUpdate.cs @@ -3,23 +3,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { + /* https://tools.ietf.org/html/rfc7540#section-6.9 + +-+-------------------------------------------------------------+ + |R| Window Size Increment (31) | + +-+-------------------------------------------------------------+ + */ public partial class Http2Frame { public int WindowUpdateSizeIncrement { - get => ((Payload[0] << 24) | (Payload[1] << 16) | (Payload[2] << 8) | Payload[3]) & 0x7fffffff; - set - { - Payload[0] = (byte)(((uint)value >> 24) & 0x7f); - Payload[1] = (byte)(((uint)value >> 16) & 0xff); - Payload[2] = (byte)(((uint)value >> 8) & 0xff); - Payload[3] = (byte)((uint)value & 0xff); - } + get => (int)Bitshifter.ReadUInt31BigEndian(Payload); + set => Bitshifter.WriteUInt31BigEndian(Payload, (uint)value); } public void PrepareWindowUpdate(int streamId, int sizeIncrement) { - Length = 4; + PayloadLength = 4; Type = Http2FrameType.WINDOW_UPDATE; Flags = 0; StreamId = streamId; diff --git a/src/Kestrel.Core/Internal/Http2/Http2Frame.cs b/src/Kestrel.Core/Internal/Http2/Http2Frame.cs index 00e2968de1..f5f447ae7f 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.cs @@ -5,6 +5,17 @@ using System; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { + /* https://tools.ietf.org/html/rfc7540#section-4.1 + +-----------------------------------------------+ + | Length (24) | + +---------------+---------------+---------------+ + | Type (8) | Flags (8) | + +-+-------------+---------------+-------------------------------+ + |R| Stream Identifier (31) | + +=+=============================================================+ + | Frame Payload (0...) ... + +---------------------------------------------------------------+ + */ public partial class Http2Frame { public const int MinAllowedMaxFrameSize = 16 * 1024; @@ -19,52 +30,60 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 private readonly byte[] _data = new byte[HeaderLength + MinAllowedMaxFrameSize]; - public Span Raw => new Span(_data, 0, HeaderLength + Length); + public Span Raw => new Span(_data, 0, HeaderLength + PayloadLength); - public int Length + public int PayloadLength { - get => (_data[LengthOffset] << 16) | (_data[LengthOffset + 1] << 8) | _data[LengthOffset + 2]; - set - { - _data[LengthOffset] = (byte)((value & 0x00ff0000) >> 16); - _data[LengthOffset + 1] = (byte)((value & 0x0000ff00) >> 8); - _data[LengthOffset + 2] = (byte)(value & 0x000000ff); - } + get => (int)Bitshifter.ReadUInt24BigEndian(_data.AsSpan(LengthOffset)); + set => Bitshifter.WriteUInt24BigEndian(_data.AsSpan(LengthOffset), (uint)value); } public Http2FrameType Type { get => (Http2FrameType)_data[TypeOffset]; - set - { - _data[TypeOffset] = (byte)value; - } + set => _data[TypeOffset] = (byte)value; } public byte Flags { get => _data[FlagsOffset]; - set - { - _data[FlagsOffset] = value; - } + set => _data[FlagsOffset] = value; } public int StreamId { - get => (int)((uint)((_data[StreamIdOffset] << 24) - | (_data[StreamIdOffset + 1] << 16) - | (_data[StreamIdOffset + 2] << 8) - | _data[StreamIdOffset + 3]) & 0x7fffffff); - set - { - _data[StreamIdOffset] = (byte)((value & 0xff000000) >> 24); - _data[StreamIdOffset + 1] = (byte)((value & 0x00ff0000) >> 16); - _data[StreamIdOffset + 2] = (byte)((value & 0x0000ff00) >> 8); - _data[StreamIdOffset + 3] = (byte)(value & 0x000000ff); - } + get => (int)Bitshifter.ReadUInt31BigEndian(_data.AsSpan(StreamIdOffset)); + set => Bitshifter.WriteUInt31BigEndian(_data.AsSpan(StreamIdOffset), (uint)value); } - public Span Payload => new Span(_data, PayloadOffset, Length); + public Span Payload => new Span(_data, PayloadOffset, PayloadLength); + + internal object ShowFlags() + { + switch (Type) + { + case Http2FrameType.CONTINUATION: + return ContinuationFlags; + case Http2FrameType.DATA: + return DataFlags; + case Http2FrameType.HEADERS: + return HeadersFlags; + case Http2FrameType.SETTINGS: + return SettingsFlags; + case Http2FrameType.PING: + return PingFlags; + + // Not Implemented + case Http2FrameType.PUSH_PROMISE: + + // No flags defined + case Http2FrameType.PRIORITY: + case Http2FrameType.RST_STREAM: + case Http2FrameType.GOAWAY: + case Http2FrameType.WINDOW_UPDATE: + default: + return $"0x{Flags:x}"; + } + } } } diff --git a/src/Kestrel.Core/Internal/Http2/Http2FrameReader.cs b/src/Kestrel.Core/Internal/Http2/Http2FrameReader.cs index 4ca9c09aed..15f3d430a6 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2FrameReader.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2FrameReader.cs @@ -3,7 +3,6 @@ using System; using System.Buffers; -using System.Collections; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { @@ -22,18 +21,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 var headerSlice = readableBuffer.Slice(0, Http2Frame.HeaderLength); headerSlice.CopyTo(frame.Raw); - if (frame.Length > maxFrameSize) + var payloadLength = frame.PayloadLength; + if (payloadLength > maxFrameSize) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorFrameOverLimit(frame.Length, maxFrameSize), Http2ErrorCode.FRAME_SIZE_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorFrameOverLimit(payloadLength, maxFrameSize), Http2ErrorCode.FRAME_SIZE_ERROR); } - if (readableBuffer.Length < Http2Frame.HeaderLength + frame.Length) + var frameLength = Http2Frame.HeaderLength + payloadLength; + if (readableBuffer.Length < frameLength) { return false; } - readableBuffer.Slice(Http2Frame.HeaderLength, frame.Length).CopyTo(frame.Payload); - consumed = examined = readableBuffer.GetPosition(Http2Frame.HeaderLength + frame.Length); + readableBuffer.Slice(Http2Frame.HeaderLength, payloadLength).CopyTo(frame.Payload); + consumed = examined = readableBuffer.GetPosition(frameLength); return true; } diff --git a/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs b/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs index 0a9b62e30e..1d101d1591 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs @@ -100,7 +100,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 lock (_writeLock) { _outgoingFrame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS, streamId); - _outgoingFrame.Length = _continueBytes.Length; + _outgoingFrame.PayloadLength = _continueBytes.Length; _continueBytes.CopyTo(_outgoingFrame.HeadersPayload); return WriteFrameUnsynchronizedAsync(); @@ -119,7 +119,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _outgoingFrame.PrepareHeaders(Http2HeadersFrameFlags.NONE, streamId); var done = _hpackEncoder.BeginEncode(statusCode, EnumerateHeaders(headers), _outgoingFrame.Payload, out var payloadLength); - _outgoingFrame.Length = payloadLength; + _outgoingFrame.PayloadLength = payloadLength; if (done) { @@ -134,7 +134,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _outgoingFrame.PrepareContinuation(Http2ContinuationFrameFlags.NONE, streamId); done = _hpackEncoder.Encode(_outgoingFrame.Payload, out var length); - _outgoingFrame.Length = length; + _outgoingFrame.PayloadLength = length; if (done) { @@ -207,7 +207,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _outgoingFrame.DataFlags = Http2DataFrameFlags.END_STREAM; } - _outgoingFrame.Length = unwrittenPayloadLength; + _outgoingFrame.PayloadLength = unwrittenPayloadLength; _log.Http2FrameSending(_connectionId, _outgoingFrame); _outputWriter.Write(_outgoingFrame.Raw); diff --git a/src/Kestrel.Core/Internal/Http2/Http2Stream.cs b/src/Kestrel.Core/Internal/Http2/Http2Stream.cs index 940fda25a6..9ae576a1b8 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Stream.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Stream.cs @@ -274,9 +274,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } var payload = dataFrame.DataPayload; - var endStream = (dataFrame.DataFlags & Http2DataFrameFlags.END_STREAM) == Http2DataFrameFlags.END_STREAM; + var endStream = dataFrame.DataEndStream; - if (payload.Count > 0) + if (payload.Length > 0) { RequestBodyStarted = true; @@ -287,7 +287,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _inputFlowControl.StopWindowUpdates(); } - _inputFlowControl.Advance(payload.Count); + _inputFlowControl.Advance(payload.Length); if (IsAborted) { @@ -300,12 +300,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 if (InputRemaining.HasValue) { // https://tools.ietf.org/html/rfc7540#section-8.1.2.6 - if (payload.Count > InputRemaining.Value) + if (payload.Length > InputRemaining.Value) { throw new Http2StreamErrorException(StreamId, CoreStrings.Http2StreamErrorMoreDataThanLength, Http2ErrorCode.PROTOCOL_ERROR); } - InputRemaining -= payload.Count; + InputRemaining -= payload.Length; } RequestBodyPipe.Writer.Write(payload); diff --git a/src/Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs b/src/Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs index 752759a969..3c269b0ac9 100644 --- a/src/Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs +++ b/src/Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs @@ -256,12 +256,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal public void Http2FrameReceived(string connectionId, Http2Frame frame) { - _http2FrameReceived(_logger, connectionId, frame.Type, frame.StreamId, frame.Length, frame.ShowFlags(), null); + _http2FrameReceived(_logger, connectionId, frame.Type, frame.StreamId, frame.PayloadLength, frame.ShowFlags(), null); } public void Http2FrameSending(string connectionId, Http2Frame frame) { - _http2FrameSending(_logger, connectionId, frame.Type, frame.StreamId, frame.Length, frame.ShowFlags(), null); + _http2FrameSending(_logger, connectionId, frame.Type, frame.StreamId, frame.PayloadLength, frame.ShowFlags(), null); } public virtual void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) diff --git a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs index 2d24e413e8..77e6f5c678 100644 --- a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs +++ b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs @@ -2087,7 +2087,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core => GetString("Http2StreamErrorMoreDataThanLength"); /// - /// An error occured after the response headers were sent, a reset is being sent. + /// An error occurred after the response headers were sent, a reset is being sent. /// internal static string Http2StreamErrorAfterHeaders { @@ -2095,7 +2095,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core } /// - /// An error occured after the response headers were sent, a reset is being sent. + /// An error occurred after the response headers were sent, a reset is being sent. /// internal static string FormatHttp2StreamErrorAfterHeaders() => GetString("Http2StreamErrorAfterHeaders"); @@ -2128,6 +2128,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core internal static string FormatGreaterThanZeroRequired() => GetString("GreaterThanZeroRequired"); + /// + /// The frame is too short to contain the fields indicated by the given flags. + /// + internal static string Http2FrameMissingFields + { + get => GetString("Http2FrameMissingFields"); + } + + /// + /// The frame is too short to contain the fields indicated by the given flags. + /// + internal static string FormatHttp2FrameMissingFields() + => GetString("Http2FrameMissingFields"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index b1b77db419..c4b6c3ab44 100644 --- a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -124,7 +124,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - Assert.Equal(_helloWorldBytes, dataFrame.DataPayload); + Assert.True(_helloWorldBytes.AsSpan().SequenceEqual(dataFrame.DataPayload)); } [Fact] @@ -151,7 +151,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - Assert.Equal(_maxData, dataFrame.DataPayload); + Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame.DataPayload)); } [Fact] @@ -223,10 +223,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - Assert.Equal(_maxData, dataFrame1.DataPayload); - Assert.Equal(_maxData, dataFrame2.DataPayload); - Assert.Equal(_maxData, dataFrame3.DataPayload); - Assert.Equal(_maxData, dataFrame4.DataPayload); + Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame1.DataPayload)); + Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame2.DataPayload)); + Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame3.DataPayload)); + Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame4.DataPayload)); Assert.Equal(_maxData.Length * 2, streamWindowUpdateFrame1.WindowUpdateSizeIncrement); Assert.Equal(_maxData.Length * 2, connectionWindowUpdateFrame1.WindowUpdateSizeIncrement); Assert.Equal(_maxData.Length * 2, connectionWindowUpdateFrame2.WindowUpdateSizeIncrement); @@ -261,7 +261,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - Assert.Equal(_helloWorldBytes, dataFrame.DataPayload); + Assert.True(_helloWorldBytes.AsSpan().SequenceEqual(dataFrame.DataPayload)); } [Fact] @@ -324,10 +324,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); - Assert.Equal(stream1DataFrame1.DataPayload, _helloBytes); - Assert.Equal(stream1DataFrame2.DataPayload, _worldBytes); - Assert.Equal(stream3DataFrame1.DataPayload, _helloBytes); - Assert.Equal(stream3DataFrame2.DataPayload, _worldBytes); + Assert.True(_helloBytes.AsSpan().SequenceEqual(stream1DataFrame1.DataPayload)); + Assert.True(_worldBytes.AsSpan().SequenceEqual(stream1DataFrame2.DataPayload)); + Assert.True(_helloBytes.AsSpan().SequenceEqual(stream3DataFrame1.DataPayload)); + Assert.True(_worldBytes.AsSpan().SequenceEqual(stream3DataFrame2.DataPayload)); } [Fact] @@ -417,16 +417,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); - Assert.Equal(_maxData, dataFrame1.DataPayload); - Assert.Equal(_maxData, dataFrame2.DataPayload); - Assert.Equal(_maxData, dataFrame3.DataPayload); + Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame1.DataPayload)); + Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame2.DataPayload)); + Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame3.DataPayload)); Assert.Equal(_maxData.Length * 2, streamWindowUpdateFrame.WindowUpdateSizeIncrement); Assert.Equal(_maxData.Length * 2, connectionWindowUpdateFrame1.WindowUpdateSizeIncrement); - Assert.Equal(_maxData, dataFrame4.DataPayload); + Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame4.DataPayload)); Assert.Equal(_maxData.Length * 2, connectionWindowUpdateFrame2.WindowUpdateSizeIncrement); - Assert.Equal(_maxData, dataFrame5.DataPayload); + Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame5.DataPayload)); } [Fact] @@ -521,7 +521,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - Assert.Equal(_helloWorldBytes, dataFrame.DataPayload); + Assert.True(_helloWorldBytes.AsSpan().SequenceEqual(dataFrame.DataPayload)); } [Theory] @@ -533,12 +533,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests // _maxData should be 1/4th of the default initial window size + 1. Assert.Equal(Http2PeerSettings.DefaultInitialWindowSize + 1, (uint)_maxData.Length * 4); - var maxDataMinusPadding = new ArraySegment(_maxData, 0, _maxData.Length - padLength - 1); + var maxDataMinusPadding = _maxData.AsMemory(0, _maxData.Length - padLength - 1); await InitializeConnectionAsync(_echoApplication); await StartStreamAsync(1, _browserRequestHeaders, endStream: false); - await SendDataWithPaddingAsync(1, maxDataMinusPadding, padLength, endStream: false); + await SendDataWithPaddingAsync(1, maxDataMinusPadding.Span, padLength, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, withLength: 37, @@ -546,7 +546,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests withStreamId: 1); var dataFrame1 = await ExpectAsync(Http2FrameType.DATA, - withLength: maxDataMinusPadding.Count, + withLength: maxDataMinusPadding.Length, withFlags: (byte)Http2DataFrameFlags.NONE, withStreamId: 1); @@ -569,8 +569,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - Assert.Equal(maxDataMinusPadding, dataFrame1.DataPayload); - Assert.Equal(_maxData, dataFrame2.DataPayload); + Assert.True(maxDataMinusPadding.Span.SequenceEqual(dataFrame1.DataPayload)); + Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame2.DataPayload)); Assert.Equal(_maxData.Length * 2, connectionWindowUpdateFrame.WindowUpdateSizeIncrement); } @@ -678,7 +678,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests ignoreNonGoAwayFrames: true, expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, - expectedErrorMessage: CoreStrings.FormatHttp2ErrorPaddingTooLong(Http2FrameType.DATA)); + expectedErrorMessage: CoreStrings.Http2FrameMissingFields); } [Fact] @@ -1303,14 +1303,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests } [Theory] - [InlineData(0)] [InlineData(1)] [InlineData(255)] public async Task HEADERS_Received_PaddingEqualToFramePayloadLength_ConnectionError(byte padLength) { await InitializeConnectionAsync(_noopApplication); - await SendInvalidHeadersFrameAsync(1, frameLength: padLength, padLength: padLength); + // The payload length includes the pad length field + await SendInvalidHeadersFrameAsync(1, payloadLength: padLength, padLength: padLength); await WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: true, @@ -1319,8 +1319,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests expectedErrorMessage: CoreStrings.FormatHttp2ErrorPaddingTooLong(Http2FrameType.HEADERS)); } + [Fact] + public async Task HEADERS_Received_PaddingFieldMissing_ConnectionError() + { + await InitializeConnectionAsync(_noopApplication); + + await SendInvalidHeadersFrameAsync(1, payloadLength: 0, padLength: 1); + + await WaitForConnectionErrorAsync( + ignoreNonGoAwayFrames: true, + expectedLastStreamId: 0, + expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, + expectedErrorMessage: CoreStrings.Http2FrameMissingFields); + } + [Theory] - [InlineData(0, 1)] [InlineData(1, 2)] [InlineData(254, 255)] public async Task HEADERS_Received_PaddingGreaterThanFramePayloadLength_ConnectionError(int frameLength, byte padLength) @@ -1825,7 +1838,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests withFlags: (byte)Http2DataFrameFlags.NONE, withStreamId: streamId); - Assert.Equal(new ArraySegment(_helloWorldBytes, 0, initialWindowSize), dataFrame.DataPayload); + Assert.True(_helloWorldBytes.AsSpan(0, initialWindowSize).SequenceEqual(dataFrame.DataPayload)); Assert.False(writeTasks[streamId].IsCompleted); } @@ -2445,7 +2458,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests withFlags: (byte)Http2DataFrameFlags.NONE, withStreamId: streamId); - Assert.Equal(new ArraySegment(_helloWorldBytes, 0, initialWindowSize), dataFrame.DataPayload); + Assert.True(_helloWorldBytes.AsSpan(0, initialWindowSize).SequenceEqual(dataFrame.DataPayload)); Assert.False(writeTasks[streamId].IsCompleted); } @@ -2749,8 +2762,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - Assert.Equal(new ArraySegment(_helloWorldBytes, 0, initialWindowSize), dataFrame1.DataPayload); - Assert.Equal(new ArraySegment(_helloWorldBytes, initialWindowSize, initialWindowSize), dataFrame2.DataPayload); + Assert.True(_helloWorldBytes.AsSpan(0, initialWindowSize).SequenceEqual(dataFrame1.DataPayload)); + Assert.True(_helloWorldBytes.AsSpan(initialWindowSize, initialWindowSize).SequenceEqual(dataFrame2.DataPayload)); } [Fact] @@ -2804,9 +2817,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - Assert.Equal(new ArraySegment(_helloWorldBytes, 0, 6), dataFrame1.DataPayload); - Assert.Equal(new ArraySegment(_helloWorldBytes, 6, 3), dataFrame2.DataPayload); - Assert.Equal(new ArraySegment(_helloWorldBytes, 9, 3), dataFrame3.DataPayload); + Assert.True(_helloWorldBytes.AsSpan(0, 6).SequenceEqual(dataFrame1.DataPayload)); + Assert.True(_helloWorldBytes.AsSpan(6, 3).SequenceEqual(dataFrame2.DataPayload)); + Assert.True(_helloWorldBytes.AsSpan(9, 3).SequenceEqual(dataFrame3.DataPayload)); } [Fact] diff --git a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs index f2dfb600b8..6a727466d1 100644 --- a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs @@ -16,7 +16,6 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; -using Moq; using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests diff --git a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2TestBase.cs b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2TestBase.cs index ec588826c2..4fa61a11df 100644 --- a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2TestBase.cs +++ b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -333,7 +333,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var frame = new Http2Frame(); frame.PrepareHeaders(Http2HeadersFrameFlags.NONE, streamId); var done = _hpackEncoder.BeginEncode(headers, frame.HeadersPayload, out var length); - frame.Length = length; + frame.PayloadLength = length; if (done) { @@ -351,7 +351,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { frame.PrepareContinuation(Http2ContinuationFrameFlags.NONE, streamId); done = _hpackEncoder.Encode(frame.HeadersPayload, out length); - frame.Length = length; + frame.PayloadLength = length; if (done) { @@ -374,7 +374,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _hpackEncoder.BeginEncode(headers, frame.HeadersPayload, out var length); - frame.Length = 1 + length + padLength; + frame.PayloadLength = 1 + length + padLength; frame.Payload.Slice(1 + length).Fill(0); if (endStream) @@ -392,12 +392,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var frame = new Http2Frame(); frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.PRIORITY, streamId); - frame.HeadersPriority = priority; + frame.HeadersPriorityWeight = priority; frame.HeadersStreamDependency = streamDependency; _hpackEncoder.BeginEncode(headers, frame.HeadersPayload, out var length); - frame.Length = 5 + length; + frame.PayloadLength = 5 + length; if (endStream) { @@ -415,12 +415,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var frame = new Http2Frame(); frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.PADDED | Http2HeadersFrameFlags.PRIORITY, streamId); frame.HeadersPadLength = padLength; - frame.HeadersPriority = priority; + frame.HeadersPriorityWeight = priority; frame.HeadersStreamDependency = streamDependency; _hpackEncoder.BeginEncode(headers, frame.HeadersPayload, out var length); - frame.Length = 6 + length + padLength; + frame.PayloadLength = 6 + length + padLength; frame.Payload.Slice(6 + length).Fill(0); if (endStream) @@ -461,7 +461,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { var frame = new Http2Frame(); frame.PrepareSettings(Http2SettingsFrameFlags.ACK); - frame.Length = length; + frame.PayloadLength = length; return SendAsync(frame.Raw); } @@ -477,7 +477,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { var frame = new Http2Frame(); frame.PrepareSettings(Http2SettingsFrameFlags.NONE, _clientSettings.GetNonProtocolDefaults()); - frame.Length = length; + frame.PayloadLength = length; return SendAsync(frame.Raw); } @@ -485,7 +485,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { var frame = new Http2Frame(); frame.PrepareSettings(Http2SettingsFrameFlags.NONE); - frame.Length = 6; + frame.PayloadLength = 6; frame.Payload[0] = (byte)((ushort)parameter >> 8); frame.Payload[1] = (byte)(ushort)parameter; @@ -500,7 +500,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected Task SendPushPromiseFrameAsync() { var frame = new Http2Frame(); - frame.Length = 0; + frame.PayloadLength = 0; frame.Type = Http2FrameType.PUSH_PROMISE; frame.StreamId = 1; return SendAsync(frame.Raw); @@ -512,7 +512,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests frame.PrepareHeaders(flags, streamId); var done = _hpackEncoder.BeginEncode(headers, frame.Payload, out var length); - frame.Length = length; + frame.PayloadLength = length; await SendAsync(frame.Raw); @@ -524,15 +524,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var frame = new Http2Frame(); frame.PrepareHeaders(flags, streamId); - frame.Length = headerBlock.Length; + frame.PayloadLength = headerBlock.Length; headerBlock.CopyTo(frame.HeadersPayload); return SendAsync(frame.Raw); } - protected Task SendInvalidHeadersFrameAsync(int streamId, int frameLength, byte padLength) + protected Task SendInvalidHeadersFrameAsync(int streamId, int payloadLength, byte padLength) { - Assert.True(padLength >= frameLength, $"{nameof(padLength)} must be greater than or equal to {nameof(frameLength)} to create an invalid frame."); + Assert.True(padLength >= payloadLength, $"{nameof(padLength)} must be greater than or equal to {nameof(payloadLength)} to create an invalid frame."); var frame = new Http2Frame(); @@ -540,7 +540,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests frame.Payload[0] = padLength; // Set length last so .Payload can be written to - frame.Length = frameLength; + frame.PayloadLength = payloadLength; return SendAsync(frame.Raw); } @@ -550,7 +550,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var frame = new Http2Frame(); frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS, streamId); - frame.Length = 3; + frame.PayloadLength = 3; // Set up an incomplete Literal Header Field w/ Incremental Indexing frame, // with an incomplete new name @@ -567,7 +567,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests frame.PrepareContinuation(flags, streamId); var done = _hpackEncoder.Encode(frame.Payload, out var length); - frame.Length = length; + frame.PayloadLength = length; await SendAsync(frame.Raw); @@ -579,7 +579,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var frame = new Http2Frame(); frame.PrepareContinuation(flags, streamId); - frame.Length = payload.Length; + frame.PayloadLength = payload.Length; payload.CopyTo(frame.Payload); await SendAsync(frame.Raw); @@ -590,7 +590,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var frame = new Http2Frame(); frame.PrepareContinuation(flags, streamId); - frame.Length = 0; + frame.PayloadLength = 0; return SendAsync(frame.Raw); } @@ -600,7 +600,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var frame = new Http2Frame(); frame.PrepareContinuation(Http2ContinuationFrameFlags.END_HEADERS, streamId); - frame.Length = 3; + frame.PayloadLength = 3; // Set up an incomplete Literal Header Field w/ Incremental Indexing frame, // with an incomplete new name @@ -616,7 +616,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var frame = new Http2Frame(); frame.PrepareData(streamId); - frame.Length = data.Length; + frame.PayloadLength = data.Length; frame.DataFlags = endStream ? Http2DataFrameFlags.END_STREAM : Http2DataFrameFlags.NONE; data.CopyTo(frame.DataPayload); @@ -628,7 +628,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var frame = new Http2Frame(); frame.PrepareData(streamId, padLength); - frame.Length = data.Length + 1 + padLength; + frame.PayloadLength = data.Length + 1 + padLength; data.CopyTo(frame.DataPayload); if (endStream) @@ -650,7 +650,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests frame.Payload[0] = padLength; // Set length last so .Payload can be written to - frame.Length = frameLength; + frame.PayloadLength = frameLength; return SendAsync(frame.Raw); } @@ -666,7 +666,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { var pingFrame = new Http2Frame(); pingFrame.PreparePing(Http2PingFrameFlags.NONE); - pingFrame.Length = length; + pingFrame.PayloadLength = length; return SendAsync(pingFrame.Raw); } @@ -691,7 +691,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { var priorityFrame = new Http2Frame(); priorityFrame.PreparePriority(streamId, streamDependency: 0, exclusive: false, weight: 0); - priorityFrame.Length = length; + priorityFrame.PayloadLength = length; return SendAsync(priorityFrame.Raw); } @@ -706,7 +706,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { var frame = new Http2Frame(); frame.PrepareRstStream(streamId, Http2ErrorCode.CANCEL); - frame.Length = length; + frame.PayloadLength = length; return SendAsync(frame.Raw); } @@ -736,7 +736,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { var frame = new Http2Frame(); frame.PrepareWindowUpdate(streamId, sizeIncrement); - frame.Length = length; + frame.PayloadLength = length; return SendAsync(frame.Raw); } @@ -745,7 +745,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var frame = new Http2Frame(); frame.StreamId = streamId; frame.Type = (Http2FrameType)frameType; - frame.Length = 0; + frame.PayloadLength = 0; return SendAsync(frame.Raw); } @@ -786,7 +786,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var frame = await ReceiveFrameAsync(); Assert.Equal(type, frame.Type); - Assert.Equal(withLength, frame.Length); + Assert.Equal(withLength, frame.PayloadLength); Assert.Equal(withFlags, frame.Flags); Assert.Equal(withStreamId, frame.StreamId); @@ -808,7 +808,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected void VerifyGoAway(Http2Frame frame, int expectedLastStreamId, Http2ErrorCode expectedErrorCode) { Assert.Equal(Http2FrameType.GOAWAY, frame.Type); - Assert.Equal(8, frame.Length); + Assert.Equal(8, frame.PayloadLength); Assert.Equal(0, frame.Flags); Assert.Equal(0, frame.StreamId); Assert.Equal(expectedLastStreamId, frame.GoAwayLastStreamId); @@ -845,7 +845,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var frame = await ReceiveFrameAsync(); Assert.Equal(Http2FrameType.RST_STREAM, frame.Type); - Assert.Equal(4, frame.Length); + Assert.Equal(4, frame.PayloadLength); Assert.Equal(0, frame.Flags); Assert.Equal(expectedStreamId, frame.StreamId); Assert.Equal(expectedErrorCode, frame.RstStreamErrorCode); diff --git a/test/Kestrel.InMemory.FunctionalTests/Http2/TlsTests.cs b/test/Kestrel.InMemory.FunctionalTests/Http2/TlsTests.cs index 98d041a09c..fda221726e 100644 --- a/test/Kestrel.InMemory.FunctionalTests/Http2/TlsTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/Http2/TlsTests.cs @@ -83,7 +83,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.Http2 } Assert.Equal(Http2FrameType.GOAWAY, frame.Type); - Assert.Equal(8, frame.Length); + Assert.Equal(8, frame.PayloadLength); Assert.Equal(0, frame.Flags); Assert.Equal(0, frame.StreamId); Assert.Equal(expectedLastStreamId, frame.GoAwayLastStreamId);