From b8423b8530f6fdcdcdfdfede5311d8f81fb1d7f2 Mon Sep 17 00:00:00 2001 From: "Chris Ross (ASP.NET)" Date: Sat, 1 Sep 2018 02:03:32 -0700 Subject: [PATCH] Change how HTTP/2 frames are parsed and generated #2858 --- src/Kestrel.Core/CoreStrings.resx | 6 +- .../Internal/Http2/HPack/HPackDecoder.cs | 11 +- .../Internal/Http2/HPack/HPackEncoder.cs | 11 +- .../Http2/HPack/HPackEncodingException.cs | 19 + .../Internal/Http2/Http2Connection.cs | 73 ++- .../Internal/Http2/Http2Frame.Continuation.cs | 3 +- .../Internal/Http2/Http2Frame.Data.cs | 27 +- .../Internal/Http2/Http2Frame.GoAway.cs | 16 +- .../Internal/Http2/Http2Frame.Headers.cs | 32 +- .../Internal/Http2/Http2Frame.Ping.cs | 1 - .../Internal/Http2/Http2Frame.Priority.cs | 31 +- .../Internal/Http2/Http2Frame.RstStream.cs | 8 +- .../Internal/Http2/Http2Frame.Settings.cs | 48 +- .../Internal/Http2/Http2Frame.WindowUpdate.cs | 6 +- src/Kestrel.Core/Internal/Http2/Http2Frame.cs | 53 +-- .../Internal/Http2/Http2FrameReader.cs | 223 ++++++++- .../Internal/Http2/Http2FrameWriter.cs | 298 +++++++++--- .../Internal/Http2/Http2Stream.cs | 17 +- .../Properties/CoreStrings.Designer.cs | 28 +- test/Kestrel.Core.Tests/HPackDecoderTests.cs | 39 +- .../Http2/Http2ConnectionTests.cs | 131 +++-- .../Http2/Http2StreamTests.cs | 116 +++-- .../Http2/Http2TestBase.cs | 447 +++++++++++++----- .../Http2/TlsTests.cs | 11 +- 24 files changed, 1073 insertions(+), 582 deletions(-) create mode 100644 src/Kestrel.Core/Internal/Http2/HPack/HPackEncodingException.cs diff --git a/src/Kestrel.Core/CoreStrings.resx b/src/Kestrel.Core/CoreStrings.resx index 76953b051c..5a04c01d0c 100644 --- a/src/Kestrel.Core/CoreStrings.resx +++ b/src/Kestrel.Core/CoreStrings.resx @@ -572,13 +572,13 @@ 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. - A value between {min} and {max} is required. Dynamic tables size update did not occur at the beginning of the first header block. + + The given buffer was too small to encode any headers. + \ No newline at end of file diff --git a/src/Kestrel.Core/Internal/Http2/HPack/HPackDecoder.cs b/src/Kestrel.Core/Internal/Http2/HPack/HPackDecoder.cs index 51d6068ae1..3b6cc0f00b 100644 --- a/src/Kestrel.Core/Internal/Http2/HPack/HPackDecoder.cs +++ b/src/Kestrel.Core/Internal/Http2/HPack/HPackDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Buffers; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack @@ -109,11 +110,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack _dynamicTable = dynamicTable; } - public void Decode(Span data, bool endHeaders, IHttpHeadersHandler handler) + public void Decode(ReadOnlySequence data, bool endHeaders, IHttpHeadersHandler handler) { - for (var i = 0; i < data.Length; i++) + foreach (var segment in data) { - OnByte(data[i], handler); + var span = segment.Span; + for (var i = 0; i < span.Length; i++) + { + OnByte(span[i], handler); + } } if (endHeaders) diff --git a/src/Kestrel.Core/Internal/Http2/HPack/HPackEncoder.cs b/src/Kestrel.Core/Internal/Http2/HPack/HPackEncoder.cs index b364ec7e49..a474f975e1 100644 --- a/src/Kestrel.Core/Internal/Http2/HPack/HPackEncoder.cs +++ b/src/Kestrel.Core/Internal/Http2/HPack/HPackEncoder.cs @@ -24,13 +24,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack _enumerator.MoveNext(); var statusCodeLength = EncodeStatusCode(statusCode, buffer); - var done = Encode(buffer.Slice(statusCodeLength), out var headersLength); + var done = Encode(buffer.Slice(statusCodeLength), throwIfNoneEncoded: false, out var headersLength); length = statusCodeLength + headersLength; return done; } public bool Encode(Span buffer, out int length) + { + return Encode(buffer, throwIfNoneEncoded: true, out length); + } + + private bool Encode(Span buffer, bool throwIfNoneEncoded, out int length) { length = 0; @@ -38,6 +43,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack { if (!EncodeHeader(_enumerator.Current.Key, _enumerator.Current.Value, buffer.Slice(length), out var headerLength)) { + if (length == 0 && throwIfNoneEncoded) + { + throw new HPackEncodingException(CoreStrings.HPackErrorNotEnoughBuffer); + } return false; } diff --git a/src/Kestrel.Core/Internal/Http2/HPack/HPackEncodingException.cs b/src/Kestrel.Core/Internal/Http2/HPack/HPackEncodingException.cs new file mode 100644 index 0000000000..ded6cb50df --- /dev/null +++ b/src/Kestrel.Core/Internal/Http2/HPack/HPackEncodingException.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack +{ + public class HPackEncodingException : Exception + { + public HPackEncodingException(string message) + : base(message) + { + } + public HPackEncodingException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs index d9dc7910b4..25500bb28a 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs @@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 private readonly Http2PeerSettings _serverSettings = new Http2PeerSettings(); private readonly Http2PeerSettings _clientSettings = new Http2PeerSettings(); - private readonly Http2Frame _incomingFrame; + private readonly Http2Frame _incomingFrame = new Http2Frame(); private Http2Stream _currentHeadersStream; private RequestHeaderParsingState _requestHeaderParsingState; @@ -92,7 +92,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _serverSettings.MaxFrameSize = (uint)context.ServiceContext.ServerOptions.Limits.Http2.MaxFrameSize; _serverSettings.HeaderTableSize = (uint)context.ServiceContext.ServerOptions.Limits.Http2.HeaderTableSize; _hpackDecoder = new HPackDecoder((int)_serverSettings.HeaderTableSize); - _incomingFrame = new Http2Frame(_serverSettings.MaxFrameSize); _serverSettings.MaxHeaderListSize = (uint)context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize; } @@ -188,16 +187,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 var result = await Input.ReadAsync(); var readableBuffer = result.Buffer; var consumed = readableBuffer.Start; - var examined = readableBuffer.End; + var examined = readableBuffer.Start; try { if (!readableBuffer.IsEmpty) { - if (Http2FrameReader.ReadFrame(readableBuffer, _incomingFrame, _serverSettings.MaxFrameSize, out consumed, out examined)) + if (Http2FrameReader.ReadFrame(readableBuffer, _incomingFrame, _serverSettings.MaxFrameSize, out var framePayload)) { Log.Http2FrameReceived(ConnectionId, _incomingFrame); - await ProcessFrameAsync(application); + consumed = examined = framePayload.End; + await ProcessFrameAsync(application, framePayload); + } + else + { + examined = readableBuffer.End; } } @@ -372,7 +376,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 return true; } - private Task ProcessFrameAsync(IHttpApplication application) + private Task ProcessFrameAsync(IHttpApplication application, ReadOnlySequence payload) { // http://httpwg.org/specs/rfc7540.html#rfc.section.5.1.1 // Streams initiated by a client MUST use odd-numbered stream identifiers; ... @@ -386,31 +390,31 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 switch (_incomingFrame.Type) { case Http2FrameType.DATA: - return ProcessDataFrameAsync(); + return ProcessDataFrameAsync(payload); case Http2FrameType.HEADERS: - return ProcessHeadersFrameAsync(application); + return ProcessHeadersFrameAsync(application, payload); case Http2FrameType.PRIORITY: return ProcessPriorityFrameAsync(); case Http2FrameType.RST_STREAM: return ProcessRstStreamFrameAsync(); case Http2FrameType.SETTINGS: - return ProcessSettingsFrameAsync(); + return ProcessSettingsFrameAsync(payload); case Http2FrameType.PUSH_PROMISE: throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorPushPromiseReceived, Http2ErrorCode.PROTOCOL_ERROR); case Http2FrameType.PING: - return ProcessPingFrameAsync(); + return ProcessPingFrameAsync(payload); case Http2FrameType.GOAWAY: return ProcessGoAwayFrameAsync(); case Http2FrameType.WINDOW_UPDATE: return ProcessWindowUpdateFrameAsync(); case Http2FrameType.CONTINUATION: - return ProcessContinuationFrameAsync(application); + return ProcessContinuationFrameAsync(application, payload); default: return ProcessUnknownFrameAsync(); } } - private Task ProcessDataFrameAsync() + private Task ProcessDataFrameAsync(ReadOnlySequence payload) { if (_currentHeadersStream != null) { @@ -422,12 +426,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamIdZero(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR); } - // 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); @@ -449,7 +447,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamHalfClosedRemote(_incomingFrame.Type, stream.StreamId), Http2ErrorCode.STREAM_CLOSED); } - return stream.OnDataAsync(_incomingFrame); + return stream.OnDataAsync(_incomingFrame, payload); } // If we couldn't find the stream, it was either alive previously but closed with @@ -466,7 +464,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamClosed(_incomingFrame.Type, _incomingFrame.StreamId), Http2ErrorCode.STREAM_CLOSED); } - private async Task ProcessHeadersFrameAsync(IHttpApplication application) + private Task ProcessHeadersFrameAsync(IHttpApplication application, ReadOnlySequence payload) { if (_currentHeadersStream != null) { @@ -478,12 +476,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamIdZero(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR); } - // 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); @@ -518,7 +510,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _currentHeadersStream = stream; _requestHeaderParsingState = RequestHeaderParsingState.Trailers; - await DecodeTrailersAsync(_incomingFrame.HeadersEndHeaders, _incomingFrame.HeadersPayload); + var headersPayload = payload.Slice(0, _incomingFrame.HeadersPayloadLength); // Minus padding + return DecodeTrailersAsync(_incomingFrame.HeadersEndHeaders, headersPayload); } else if (_incomingFrame.StreamId <= _highestOpenedStreamId) { @@ -554,7 +547,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _currentHeadersStream.Reset(); _headerFlags = _incomingFrame.HeadersFlags; - await DecodeHeadersAsync(application, _incomingFrame.HeadersEndHeaders, _incomingFrame.HeadersPayload); + var headersPayload = payload.Slice(0, _incomingFrame.HeadersPayloadLength); // Minus padding + return DecodeHeadersAsync(application, _incomingFrame.HeadersEndHeaders, headersPayload); } } @@ -606,7 +600,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 return Task.CompletedTask; } - private Task ProcessSettingsFrameAsync() + private Task ProcessSettingsFrameAsync(ReadOnlySequence payload) { if (_currentHeadersStream != null) { @@ -639,14 +633,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 var previousInitialWindowSize = (int)_clientSettings.InitialWindowSize; var previousMaxFrameSize = _clientSettings.MaxFrameSize; - _clientSettings.Update(_incomingFrame.GetSettings()); + _clientSettings.Update(Http2FrameReader.ReadSettings(payload)); // Ack before we update the windows, they could send data immediately. - var ackTask = _frameWriter.WriteSettingsAckAsync(); + var ackTask = _frameWriter.WriteSettingsAckAsync(); if (_clientSettings.MaxFrameSize != previousMaxFrameSize) { - _frameWriter.UpdateMaxFrameSize(_clientSettings.MaxFrameSize); + // Don't let the client choose an arbitrarily large size, this will be used for response buffers. + _frameWriter.UpdateMaxFrameSize(Math.Min(_clientSettings.MaxFrameSize, _serverSettings.MaxFrameSize)); } // This difference can be negative. @@ -676,7 +671,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } - private Task ProcessPingFrameAsync() + private Task ProcessPingFrameAsync(ReadOnlySequence payload) { if (_currentHeadersStream != null) { @@ -699,7 +694,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 return Task.CompletedTask; } - return _frameWriter.WritePingAsync(Http2PingFrameFlags.ACK, _incomingFrame.Payload); + return _frameWriter.WritePingAsync(Http2PingFrameFlags.ACK, payload); } private Task ProcessGoAwayFrameAsync() @@ -777,7 +772,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 return Task.CompletedTask; } - private Task ProcessContinuationFrameAsync(IHttpApplication application) + private Task ProcessContinuationFrameAsync(IHttpApplication application, ReadOnlySequence payload) { if (_currentHeadersStream == null) { @@ -791,11 +786,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers) { - return DecodeTrailersAsync(_incomingFrame.ContinuationEndHeaders, _incomingFrame.Payload); + return DecodeTrailersAsync(_incomingFrame.ContinuationEndHeaders, payload); } else { - return DecodeHeadersAsync(application, _incomingFrame.ContinuationEndHeaders, _incomingFrame.Payload); + return DecodeHeadersAsync(application, _incomingFrame.ContinuationEndHeaders, payload); } } @@ -809,7 +804,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 return Task.CompletedTask; } - private Task DecodeHeadersAsync(IHttpApplication application, bool endHeaders, Span payload) + private Task DecodeHeadersAsync(IHttpApplication application, bool endHeaders, ReadOnlySequence payload) { try { @@ -838,7 +833,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 return Task.CompletedTask; } - private Task DecodeTrailersAsync(bool endHeaders, Span payload) + private Task DecodeTrailersAsync(bool endHeaders, ReadOnlySequence payload) { _hpackDecoder.Decode(payload, endHeaders, handler: this); diff --git a/src/Kestrel.Core/Internal/Http2/Http2Frame.Continuation.cs b/src/Kestrel.Core/Internal/Http2/Http2Frame.Continuation.cs index 56712f5289..d971385b95 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.Continuation.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.Continuation.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { /* https://tools.ietf.org/html/rfc7540#section-6.10 @@ -21,7 +20,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 public void PrepareContinuation(Http2ContinuationFrameFlags flags, int streamId) { - PayloadLength = (int)_maxFrameSize; + PayloadLength = 0; 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 3bc53971e5..93765602e0 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.Data.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.Data.cs @@ -1,8 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; - namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { /* @@ -26,32 +24,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 public bool DataHasPadding => (DataFlags & Http2DataFrameFlags.PADDED) == Http2DataFrameFlags.PADDED; - public byte DataPadLength - { - get => DataHasPadding ? Payload[0] : (byte)0; - set => Payload[0] = value; - } + public byte DataPadLength { get; set; } - public int DataPayloadOffset => DataHasPadding ? 1 : 0; + private int DataPayloadOffset => DataHasPadding ? 1 : 0; - private int DataPayloadLength => PayloadLength - DataPayloadOffset - DataPadLength; - - public Span DataPayload => Payload.Slice(DataPayloadOffset, DataPayloadLength); + public int DataPayloadLength => PayloadLength - DataPayloadOffset - DataPadLength; public void PrepareData(int streamId, byte? padLength = null) { - var padded = padLength != null; - - PayloadLength = (int)_maxFrameSize; + PayloadLength = 0; Type = Http2FrameType.DATA; - DataFlags = padded ? Http2DataFrameFlags.PADDED : Http2DataFrameFlags.NONE; + DataFlags = padLength.HasValue ? Http2DataFrameFlags.PADDED : Http2DataFrameFlags.NONE; StreamId = streamId; - - if (padded) - { - DataPadLength = padLength.Value; - Payload.Slice(PayloadLength - padLength.Value).Fill(0); - } + DataPadLength = padLength ?? 0; } } } diff --git a/src/Kestrel.Core/Internal/Http2/Http2Frame.GoAway.cs b/src/Kestrel.Core/Internal/Http2/Http2Frame.GoAway.cs index 602c5f1fac..cb0aa22501 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.GoAway.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.GoAway.cs @@ -1,8 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Buffers.Binary; - namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { /* https://tools.ietf.org/html/rfc7540#section-6.8 @@ -16,19 +14,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 */ public partial class Http2Frame { - private const int ErrorCodeOffset = 4; + public int GoAwayLastStreamId { get; set; } - public int GoAwayLastStreamId - { - get => (int)Bitshifter.ReadUInt31BigEndian(Payload); - set => Bitshifter.WriteUInt31BigEndian(Payload, (uint)value); - } - - public Http2ErrorCode GoAwayErrorCode - { - get => (Http2ErrorCode)BinaryPrimitives.ReadUInt32BigEndian(Payload.Slice(ErrorCodeOffset)); - set => BinaryPrimitives.WriteUInt32BigEndian(Payload.Slice(ErrorCodeOffset), (uint)value); - } + public Http2ErrorCode GoAwayErrorCode { get; set; } public void PrepareGoAway(int lastStreamId, Http2ErrorCode errorCode) { diff --git a/src/Kestrel.Core/Internal/Http2/Http2Frame.Headers.cs b/src/Kestrel.Core/Internal/Http2/Http2Frame.Headers.cs index 3028e27200..16f369570e 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.Headers.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.Headers.cs @@ -1,8 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; - namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { /* https://tools.ietf.org/html/rfc7540#section-6.2 @@ -34,37 +32,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 public bool HeadersHasPriority => (HeadersFlags & Http2HeadersFrameFlags.PRIORITY) == Http2HeadersFrameFlags.PRIORITY; - public byte HeadersPadLength - { - get => HeadersHasPadding ? Payload[0] : (byte)0; - set => Payload[0] = value; - } + public byte HeadersPadLength { get; set; } - private int HeadersStreamDependencyOffset => HeadersHasPadding ? 1 : 0; + public int HeadersStreamDependency { get; set; } - public int HeadersStreamDependency - { - get => (int)Bitshifter.ReadUInt31BigEndian(Payload.Slice(HeadersStreamDependencyOffset)); - set => Bitshifter.WriteUInt31BigEndian(Payload.Slice(HeadersStreamDependencyOffset), (uint)value); - } + public byte HeadersPriorityWeight { get; set; } - private int HeadersPriorityWeightOffset => HeadersStreamDependencyOffset + 4; + private int HeadersPayloadOffset => (HeadersHasPadding ? 1 : 0) + (HeadersHasPriority ? 5 : 0); - public byte HeadersPriorityWeight - { - get => Payload[HeadersPriorityWeightOffset]; - set => Payload[HeadersPriorityWeightOffset] = value; - } - - public int HeadersPayloadOffset => (HeadersHasPadding ? 1 : 0) + (HeadersHasPriority ? 5 : 0); - - private int HeadersPayloadLength => PayloadLength - HeadersPayloadOffset - HeadersPadLength; - - public Span HeadersPayload => Payload.Slice(HeadersPayloadOffset, HeadersPayloadLength); + public int HeadersPayloadLength => PayloadLength - HeadersPayloadOffset - HeadersPadLength; public void PrepareHeaders(Http2HeadersFrameFlags flags, int streamId) { - PayloadLength = (int)_maxFrameSize; + PayloadLength = 0; 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 239c006c1b..2c69de1b21 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.Ping.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.Ping.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { /* https://tools.ietf.org/html/rfc7540#section-6.7 diff --git a/src/Kestrel.Core/Internal/Http2/Http2Frame.Priority.cs b/src/Kestrel.Core/Internal/Http2/Http2Frame.Priority.cs index 53f1d9e368..4c293e9de6 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.Priority.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.Priority.cs @@ -12,36 +12,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 */ public partial class Http2Frame { - private const int PriorityWeightOffset = 4; + public int PriorityStreamDependency { get; set; } - public int PriorityStreamDependency - { - get => (int)Bitshifter.ReadUInt31BigEndian(Payload); - set => Bitshifter.WriteUInt31BigEndian(Payload, (uint)value); - } - - public bool PriorityIsExclusive - { - get => (Payload[0] & 0x80) != 0; - set - { - if (value) - { - Payload[0] |= 0x80; - } - else - { - Payload[0] &= 0x7f; - } - } - } - - public byte PriorityWeight - { - get => Payload[PriorityWeightOffset]; - set => Payload[PriorityWeightOffset] = value; - } + public bool PriorityIsExclusive { get; set; } + public byte PriorityWeight { get; set; } public void PreparePriority(int streamId, int streamDependency, bool exclusive, byte weight) { diff --git a/src/Kestrel.Core/Internal/Http2/Http2Frame.RstStream.cs b/src/Kestrel.Core/Internal/Http2/Http2Frame.RstStream.cs index c9c2716f3b..98a8fa22ca 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.RstStream.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.RstStream.cs @@ -1,8 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Buffers.Binary; - namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { /* https://tools.ietf.org/html/rfc7540#section-6.4 @@ -12,11 +10,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 */ public partial class Http2Frame { - public Http2ErrorCode RstStreamErrorCode - { - get => (Http2ErrorCode)BinaryPrimitives.ReadUInt32BigEndian(Payload); - set => BinaryPrimitives.WriteUInt32BigEndian(Payload, (uint)value); - } + public Http2ErrorCode RstStreamErrorCode { get; set; } public void PrepareRstStream(int streamId, Http2ErrorCode errorCode) { diff --git a/src/Kestrel.Core/Internal/Http2/Http2Frame.Settings.cs b/src/Kestrel.Core/Internal/Http2/Http2Frame.Settings.cs index ceddf8089d..b8f3ffe198 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.Settings.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.Settings.cs @@ -1,9 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Buffers.Binary; -using System.Collections.Generic; - namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { /* https://tools.ietf.org/html/rfc7540#section-6.5.1 @@ -16,8 +13,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 */ public partial class Http2Frame { - internal const int SettingSize = 6; // 2 bytes for the id, 4 bytes for the value. - public Http2SettingsFrameFlags SettingsFlags { get => (Http2SettingsFrameFlags)Flags; @@ -26,51 +21,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 public bool SettingsAck => (SettingsFlags & Http2SettingsFrameFlags.ACK) == Http2SettingsFrameFlags.ACK; - public int SettingsCount + public void PrepareSettings(Http2SettingsFrameFlags flags) { - get => PayloadLength / SettingSize; - set => PayloadLength = value * SettingSize; - } - - public IList GetSettings() - { - var settings = new Http2PeerSetting[SettingsCount]; - for (int i = 0; i < settings.Length; i++) - { - settings[i] = GetSetting(i); - } - return settings; - } - - private Http2PeerSetting GetSetting(int index) - { - var offset = index * SettingSize; - var payload = Payload.Slice(offset); - var id = (Http2SettingsParameter)BinaryPrimitives.ReadUInt16BigEndian(payload); - var value = BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(2)); - - return new Http2PeerSetting(id, value); - } - - public void PrepareSettings(Http2SettingsFrameFlags flags, IList settings = null) - { - var settingCount = settings?.Count ?? 0; - SettingsCount = settingCount; + PayloadLength = 0; Type = Http2FrameType.SETTINGS; SettingsFlags = flags; StreamId = 0; - for (int i = 0; i < settingCount; i++) - { - SetSetting(i, settings[i]); - } - } - - private void SetSetting(int index, Http2PeerSetting setting) - { - var offset = index * SettingSize; - var payload = Payload.Slice(offset); - BinaryPrimitives.WriteUInt16BigEndian(payload, (ushort)setting.Parameter); - BinaryPrimitives.WriteUInt32BigEndian(payload.Slice(2), setting.Value); } } } diff --git a/src/Kestrel.Core/Internal/Http2/Http2Frame.WindowUpdate.cs b/src/Kestrel.Core/Internal/Http2/Http2Frame.WindowUpdate.cs index 098064b3e3..404de2c446 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.WindowUpdate.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.WindowUpdate.cs @@ -10,11 +10,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 */ public partial class Http2Frame { - public int WindowUpdateSizeIncrement - { - get => (int)Bitshifter.ReadUInt31BigEndian(Payload); - set => Bitshifter.WriteUInt31BigEndian(Payload, (uint)value); - } + public int WindowUpdateSizeIncrement { get; set; } public void PrepareWindowUpdate(int streamId, int sizeIncrement) { diff --git a/src/Kestrel.Core/Internal/Http2/Http2Frame.cs b/src/Kestrel.Core/Internal/Http2/Http2Frame.cs index f9d0723bdc..9880572ed9 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Frame.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Frame.cs @@ -1,8 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; - namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { /* https://tools.ietf.org/html/rfc7540#section-4.1 @@ -18,51 +16,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 */ public partial class Http2Frame { - public const int HeaderLength = 9; + public int PayloadLength { get; set; } - private const int LengthOffset = 0; - private const int TypeOffset = 3; - private const int FlagsOffset = 4; - private const int StreamIdOffset = 5; - private const int PayloadOffset = 9; + public Http2FrameType Type { get; set; } - private uint _maxFrameSize; + public byte Flags { get; set; } - private readonly byte[] _data; - - public Http2Frame(uint maxFrameSize) - { - _maxFrameSize = maxFrameSize; - _data = new byte[HeaderLength + _maxFrameSize]; - } - - public Span Raw => new Span(_data, 0, HeaderLength + PayloadLength); - - public int PayloadLength - { - 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; - } - - public byte Flags - { - get => _data[FlagsOffset]; - set => _data[FlagsOffset] = value; - } - - public int StreamId - { - get => (int)Bitshifter.ReadUInt31BigEndian(_data.AsSpan(StreamIdOffset)); - set => Bitshifter.WriteUInt31BigEndian(_data.AsSpan(StreamIdOffset), (uint)value); - } - - public Span Payload => new Span(_data, PayloadOffset, PayloadLength); + public int StreamId { get; set; } internal object ShowFlags() { @@ -91,5 +51,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 return $"0x{Flags:x}"; } } + + public override string ToString() + { + return $"{Type} Stream: {StreamId} Length: {PayloadLength} Flags: {ShowFlags()}"; + } } } diff --git a/src/Kestrel.Core/Internal/Http2/Http2FrameReader.cs b/src/Kestrel.Core/Internal/Http2/Http2FrameReader.cs index 15f3d430a6..0bc2617ac3 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2FrameReader.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2FrameReader.cs @@ -3,40 +3,241 @@ using System; using System.Buffers; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { public static class Http2FrameReader { - public static bool ReadFrame(ReadOnlySequence readableBuffer, Http2Frame frame, uint maxFrameSize, out SequencePosition consumed, out SequencePosition examined) - { - consumed = readableBuffer.Start; - examined = readableBuffer.End; + /* https://tools.ietf.org/html/rfc7540#section-4.1 + +-----------------------------------------------+ + | Length (24) | + +---------------+---------------+---------------+ + | Type (8) | Flags (8) | + +-+-------------+---------------+-------------------------------+ + |R| Stream Identifier (31) | + +=+=============================================================+ + | Frame Payload (0...) ... + +---------------------------------------------------------------+ + */ + public const int HeaderLength = 9; - if (readableBuffer.Length < Http2Frame.HeaderLength) + private const int TypeOffset = 3; + private const int FlagsOffset = 4; + private const int StreamIdOffset = 5; + + public const int SettingSize = 6; // 2 bytes for the id, 4 bytes for the value. + + public static bool ReadFrame(ReadOnlySequence readableBuffer, Http2Frame frame, uint maxFrameSize, out ReadOnlySequence framePayload) + { + framePayload = ReadOnlySequence.Empty; + + if (readableBuffer.Length < HeaderLength) { return false; } - var headerSlice = readableBuffer.Slice(0, Http2Frame.HeaderLength); - headerSlice.CopyTo(frame.Raw); + var headerSlice = readableBuffer.Slice(0, HeaderLength); + var header = headerSlice.ToSpan(); - var payloadLength = frame.PayloadLength; + var payloadLength = (int)Bitshifter.ReadUInt24BigEndian(header); if (payloadLength > maxFrameSize) { throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorFrameOverLimit(payloadLength, maxFrameSize), Http2ErrorCode.FRAME_SIZE_ERROR); } - var frameLength = Http2Frame.HeaderLength + payloadLength; + // Make sure the whole frame is buffered + var frameLength = HeaderLength + payloadLength; if (readableBuffer.Length < frameLength) { return false; } - readableBuffer.Slice(Http2Frame.HeaderLength, payloadLength).CopyTo(frame.Payload); - consumed = examined = readableBuffer.GetPosition(frameLength); + frame.PayloadLength = payloadLength; + frame.Type = (Http2FrameType)header[TypeOffset]; + frame.Flags = header[FlagsOffset]; + frame.StreamId = (int)Bitshifter.ReadUInt31BigEndian(header.Slice(StreamIdOffset)); + + var extendedHeaderLength = ReadExtendedFields(frame, readableBuffer); + + // The remaining payload minus the extra fields + framePayload = readableBuffer.Slice(HeaderLength + extendedHeaderLength, payloadLength - extendedHeaderLength); return true; } + + private static int ReadExtendedFields(Http2Frame frame, ReadOnlySequence readableBuffer) + { + // Copy in any extra fields for the given frame type + var extendedHeaderLength = GetPayloadFieldsLength(frame); + + if (extendedHeaderLength > frame.PayloadLength) + { + throw new Http2ConnectionErrorException( + CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(frame.Type, expectedLength: extendedHeaderLength), Http2ErrorCode.FRAME_SIZE_ERROR); + } + + var extendedHeaders = readableBuffer.Slice(HeaderLength, extendedHeaderLength).ToSpan(); + + // Parse frame type specific fields + switch (frame.Type) + { + /* + +---------------+ + |Pad Length? (8)| + +---------------+-----------------------------------------------+ + | Data (*) ... + +---------------------------------------------------------------+ + | Padding (*) ... + +---------------------------------------------------------------+ + */ + case Http2FrameType.DATA: // Variable 0 or 1 + frame.DataPadLength = frame.DataHasPadding ? extendedHeaders[0] : (byte)0; + break; + + /* https://tools.ietf.org/html/rfc7540#section-6.2 + +---------------+ + |Pad Length? (8)| + +-+-------------+-----------------------------------------------+ + |E| Stream Dependency? (31) | + +-+-------------+-----------------------------------------------+ + | Weight? (8) | + +-+-------------+-----------------------------------------------+ + | Header Block Fragment (*) ... + +---------------------------------------------------------------+ + | Padding (*) ... + +---------------------------------------------------------------+ + */ + case Http2FrameType.HEADERS: + if (frame.HeadersHasPadding) + { + frame.HeadersPadLength = extendedHeaders[0]; + extendedHeaders = extendedHeaders.Slice(1); + } + else + { + frame.HeadersPadLength = 0; + } + + if (frame.HeadersHasPriority) + { + frame.HeadersStreamDependency = (int)Bitshifter.ReadUInt31BigEndian(extendedHeaders); + frame.HeadersPriorityWeight = extendedHeaders.Slice(4)[0]; + } + else + { + frame.HeadersStreamDependency = 0; + frame.HeadersPriorityWeight = 0; + } + break; + + /* https://tools.ietf.org/html/rfc7540#section-6.8 + +-+-------------------------------------------------------------+ + |R| Last-Stream-ID (31) | + +-+-------------------------------------------------------------+ + | Error Code (32) | + +---------------------------------------------------------------+ + | Additional Debug Data (*) | + +---------------------------------------------------------------+ + */ + case Http2FrameType.GOAWAY: + frame.GoAwayLastStreamId = (int)Bitshifter.ReadUInt31BigEndian(extendedHeaders); + frame.GoAwayErrorCode = (Http2ErrorCode)BinaryPrimitives.ReadUInt32BigEndian(extendedHeaders.Slice(4)); + break; + + /* https://tools.ietf.org/html/rfc7540#section-6.3 + +-+-------------------------------------------------------------+ + |E| Stream Dependency (31) | + +-+-------------+-----------------------------------------------+ + | Weight (8) | + +-+-------------+ + */ + case Http2FrameType.PRIORITY: + frame.PriorityStreamDependency = (int)Bitshifter.ReadUInt31BigEndian(extendedHeaders); + frame.PriorityWeight = extendedHeaders.Slice(4)[0]; + break; + + /* https://tools.ietf.org/html/rfc7540#section-6.4 + +---------------------------------------------------------------+ + | Error Code (32) | + +---------------------------------------------------------------+ + */ + case Http2FrameType.RST_STREAM: + frame.RstStreamErrorCode = (Http2ErrorCode)BinaryPrimitives.ReadUInt32BigEndian(extendedHeaders); + break; + + /* https://tools.ietf.org/html/rfc7540#section-6.9 + +-+-------------------------------------------------------------+ + |R| Window Size Increment (31) | + +-+-------------------------------------------------------------+ + */ + case Http2FrameType.WINDOW_UPDATE: + frame.WindowUpdateSizeIncrement = (int)Bitshifter.ReadUInt31BigEndian(extendedHeaders); + break; + + case Http2FrameType.PING: // Opaque payload 8 bytes long + case Http2FrameType.SETTINGS: // Settings are general payload + case Http2FrameType.CONTINUATION: // None + case Http2FrameType.PUSH_PROMISE: // Not implemented frames are ignored at this phase + default: + return 0; + } + + return extendedHeaderLength; + } + + // The length in bytes of additional fields stored in the payload section. + // This may be variable based on flags, but should be no more than 8 bytes. + public static int GetPayloadFieldsLength(Http2Frame frame) + { + switch (frame.Type) + { + // TODO: Extract constants + case Http2FrameType.DATA: // Variable 0 or 1 + return frame.DataHasPadding ? 1 : 0; + case Http2FrameType.HEADERS: + return (frame.HeadersHasPadding ? 1 : 0) + (frame.HeadersHasPriority ? 5 : 0); // Variable 0 to 6 + case Http2FrameType.GOAWAY: + return 8; // Last stream id and error code. + case Http2FrameType.PRIORITY: + return 5; // Stream dependency and weight + case Http2FrameType.RST_STREAM: + return 4; // Error code + case Http2FrameType.WINDOW_UPDATE: + return 4; // Update size + case Http2FrameType.PING: // 8 bytes of opaque data + case Http2FrameType.SETTINGS: // Settings are general payload + case Http2FrameType.CONTINUATION: // None + case Http2FrameType.PUSH_PROMISE: // Not implemented frames are ignored at this phase + default: + return 0; + } + } + + public static IList ReadSettings(ReadOnlySequence payload) + { + var data = payload.ToSpan(); + Debug.Assert(data.Length % SettingSize == 0, "Invalid settings payload length"); + var settingsCount = data.Length / SettingSize; + + var settings = new Http2PeerSetting[settingsCount]; + for (int i = 0; i < settings.Length; i++) + { + settings[i] = ReadSetting(data); + data = data.Slice(SettingSize); + } + return settings; + } + + private static Http2PeerSetting ReadSetting(ReadOnlySpan payload) + { + var id = (Http2SettingsParameter)BinaryPrimitives.ReadUInt16BigEndian(payload); + var value = BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(2)); + + return new Http2PeerSetting(id, value); + } } } diff --git a/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs b/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs index 426c330314..c71ee387f0 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs @@ -3,7 +3,9 @@ using System; using System.Buffers; +using System.Buffers.Binary; using System.Collections.Generic; +using System.Diagnostics; using System.IO.Pipelines; using System.Threading; using System.Threading.Tasks; @@ -22,6 +24,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 private static readonly byte[] _continueBytes = new byte[] { 0x08, 0x03, (byte)'1', (byte)'0', (byte)'0' }; private uint _maxFrameSize = Http2PeerSettings.MinAllowedMaxFrameSize; + private byte[] _headerEncodingBuffer; private Http2Frame _outgoingFrame; private readonly object _writeLock = new object(); private readonly HPackEncoder _hpackEncoder = new HPackEncoder(); @@ -49,15 +52,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _connectionId = connectionId; _log = log; _flusher = new StreamSafePipeFlusher(_outputWriter, timeoutControl); - _outgoingFrame = new Http2Frame(_maxFrameSize); + _outgoingFrame = new Http2Frame(); + _headerEncodingBuffer = new byte[_maxFrameSize]; } public void UpdateMaxFrameSize(uint maxFrameSize) { lock (_writeLock) { - _maxFrameSize = maxFrameSize; - _outgoingFrame = new Http2Frame(maxFrameSize); + if (_maxFrameSize != maxFrameSize) + { + _maxFrameSize = maxFrameSize; + _headerEncodingBuffer = new byte[_maxFrameSize]; + } } } @@ -109,14 +116,33 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { lock (_writeLock) { + if (_completed) + { + return Task.CompletedTask; + } + _outgoingFrame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS, streamId); _outgoingFrame.PayloadLength = _continueBytes.Length; - _continueBytes.CopyTo(_outgoingFrame.HeadersPayload); - - return WriteFrameUnsynchronizedAsync(); + WriteHeaderUnsynchronized(); + _outputWriter.Write(_continueBytes); + return _flusher.FlushAsync(); } } + // Optional header fields for padding and priority are not implemented. + /* https://tools.ietf.org/html/rfc7540#section-6.2 + +---------------+ + |Pad Length? (8)| + +-+-------------+-----------------------------------------------+ + |E| Stream Dependency? (31) | + +-+-------------+-----------------------------------------------+ + | Weight? (8) | + +-+-------------+-----------------------------------------------+ + | Header Block Fragment (*) ... + +---------------------------------------------------------------+ + | Padding (*) ... + +---------------------------------------------------------------+ + */ public void WriteResponseHeaders(int streamId, int statusCode, IHeaderDictionary headers) { lock (_writeLock) @@ -126,33 +152,43 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 return; } - _outgoingFrame.PrepareHeaders(Http2HeadersFrameFlags.NONE, streamId); - - var done = _hpackEncoder.BeginEncode(statusCode, EnumerateHeaders(headers), _outgoingFrame.Payload, out var payloadLength); - _outgoingFrame.PayloadLength = payloadLength; - - if (done) + try { - _outgoingFrame.HeadersFlags = Http2HeadersFrameFlags.END_HEADERS; - } + _outgoingFrame.PrepareHeaders(Http2HeadersFrameFlags.NONE, streamId); + var buffer = _headerEncodingBuffer.AsSpan(); + var done = _hpackEncoder.BeginEncode(statusCode, EnumerateHeaders(headers), buffer, out var payloadLength); - _log.Http2FrameSending(_connectionId, _outgoingFrame); - _outputWriter.Write(_outgoingFrame.Raw); - - while (!done) - { - _outgoingFrame.PrepareContinuation(Http2ContinuationFrameFlags.NONE, streamId); - - done = _hpackEncoder.Encode(_outgoingFrame.Payload, out var length); - _outgoingFrame.PayloadLength = length; + _outgoingFrame.PayloadLength = payloadLength; if (done) { - _outgoingFrame.ContinuationFlags = Http2ContinuationFrameFlags.END_HEADERS; + _outgoingFrame.HeadersFlags = Http2HeadersFrameFlags.END_HEADERS; } - _log.Http2FrameSending(_connectionId, _outgoingFrame); - _outputWriter.Write(_outgoingFrame.Raw); + WriteHeaderUnsynchronized(); + _outputWriter.Write(buffer.Slice(0, payloadLength)); + + while (!done) + { + _outgoingFrame.PrepareContinuation(Http2ContinuationFrameFlags.NONE, streamId); + + done = _hpackEncoder.Encode(buffer, out payloadLength); + _outgoingFrame.PayloadLength = payloadLength; + + if (done) + { + _outgoingFrame.ContinuationFlags = Http2ContinuationFrameFlags.END_HEADERS; + } + + WriteHeaderUnsynchronized(); + _outputWriter.Write(buffer.Slice(0, payloadLength)); + } + } + catch (HPackEncodingException hex) + { + // Header errors are fatal to the connection. We don't have a direct way to signal this to the Http2Connection. + _connectionContext.Abort(new ConnectionAbortedException("", hex)); + throw new InvalidOperationException("", hex); // Report the error to the user if this was the first write. } } } @@ -182,45 +218,54 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } + /* Padding is not implemented + +---------------+ + |Pad Length? (8)| + +---------------+-----------------------------------------------+ + | Data (*) ... + +---------------------------------------------------------------+ + | Padding (*) ... + +---------------------------------------------------------------+ + */ private Task WriteDataUnsynchronizedAsync(int streamId, ReadOnlySequence data, bool endStream) { + // Note padding is not implemented _outgoingFrame.PrepareData(streamId); - var payload = _outgoingFrame.Payload; - var unwrittenPayloadLength = 0; + var dataPayloadLength = (int)_maxFrameSize; // Minus padding - foreach (var buffer in data) + while (data.Length > dataPayloadLength) { - var current = buffer; + var currentData = data.Slice(0, dataPayloadLength); + _outgoingFrame.PayloadLength = dataPayloadLength; // Plus padding - while (current.Length > payload.Length) + WriteHeaderUnsynchronized(); + + foreach (var buffer in currentData) { - current.Span.Slice(0, payload.Length).CopyTo(payload); - current = current.Slice(payload.Length); - - _log.Http2FrameSending(_connectionId, _outgoingFrame); - _outputWriter.Write(_outgoingFrame.Raw); - payload = _outgoingFrame.Payload; - unwrittenPayloadLength = 0; + _outputWriter.Write(buffer.Span); } - if (current.Length > 0) - { - current.Span.CopyTo(payload); - payload = payload.Slice(current.Length); - unwrittenPayloadLength += current.Length; - } + // Plus padding + + data = data.Slice(dataPayloadLength); } if (endStream) { - _outgoingFrame.DataFlags = Http2DataFrameFlags.END_STREAM; + _outgoingFrame.DataFlags |= Http2DataFrameFlags.END_STREAM; } - _outgoingFrame.PayloadLength = unwrittenPayloadLength; + _outgoingFrame.PayloadLength = (int)data.Length; // Plus padding - _log.Http2FrameSending(_connectionId, _outgoingFrame); - _outputWriter.Write(_outgoingFrame.Raw); + WriteHeaderUnsynchronized(); + + foreach (var buffer in data) + { + _outputWriter.Write(buffer.Span); + } + + // Plus padding return _flusher.FlushAsync(); } @@ -271,71 +316,194 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 await ThreadPoolAwaitable.Instance; } + /* https://tools.ietf.org/html/rfc7540#section-6.9 + +-+-------------------------------------------------------------+ + |R| Window Size Increment (31) | + +-+-------------------------------------------------------------+ + */ public Task WriteWindowUpdateAsync(int streamId, int sizeIncrement) { lock (_writeLock) { + if (_completed) + { + return Task.CompletedTask; + } + _outgoingFrame.PrepareWindowUpdate(streamId, sizeIncrement); - return WriteFrameUnsynchronizedAsync(); + WriteHeaderUnsynchronized(); + var buffer = _outputWriter.GetSpan(4); + Bitshifter.WriteUInt31BigEndian(buffer, (uint)sizeIncrement); + _outputWriter.Advance(4); + return _flusher.FlushAsync(); } } + /* https://tools.ietf.org/html/rfc7540#section-6.4 + +---------------------------------------------------------------+ + | Error Code (32) | + +---------------------------------------------------------------+ + */ public Task WriteRstStreamAsync(int streamId, Http2ErrorCode errorCode) { lock (_writeLock) { + if (_completed) + { + return Task.CompletedTask; + } + _outgoingFrame.PrepareRstStream(streamId, errorCode); - return WriteFrameUnsynchronizedAsync(); + WriteHeaderUnsynchronized(); + var buffer = _outputWriter.GetSpan(4); + BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)errorCode); + _outputWriter.Advance(4); + + return _flusher.FlushAsync(); } } + /* https://tools.ietf.org/html/rfc7540#section-6.5.1 + List of: + +-------------------------------+ + | Identifier (16) | + +-------------------------------+-------------------------------+ + | Value (32) | + +---------------------------------------------------------------+ + */ public Task WriteSettingsAsync(IList settings) { lock (_writeLock) { - _outgoingFrame.PrepareSettings(Http2SettingsFrameFlags.NONE, settings); - return WriteFrameUnsynchronizedAsync(); + if (_completed) + { + return Task.CompletedTask; + } + + _outgoingFrame.PrepareSettings(Http2SettingsFrameFlags.NONE); + var settingsSize = settings.Count * Http2FrameReader.SettingSize; + _outgoingFrame.PayloadLength = settingsSize; + WriteHeaderUnsynchronized(); + + var buffer = _outputWriter.GetSpan(settingsSize).Slice(0, settingsSize); // GetSpan isn't precise + WriteSettings(settings, buffer); + _outputWriter.Advance(settingsSize); + + return _flusher.FlushAsync(); } } + internal static void WriteSettings(IList settings, Span destination) + { + foreach (var setting in settings) + { + BinaryPrimitives.WriteUInt16BigEndian(destination, (ushort)setting.Parameter); + BinaryPrimitives.WriteUInt32BigEndian(destination.Slice(2), setting.Value); + destination = destination.Slice(Http2FrameReader.SettingSize); + } + } + + // No payload public Task WriteSettingsAckAsync() { lock (_writeLock) { + if (_completed) + { + return Task.CompletedTask; + } + _outgoingFrame.PrepareSettings(Http2SettingsFrameFlags.ACK); - return WriteFrameUnsynchronizedAsync(); + WriteHeaderUnsynchronized(); + return _flusher.FlushAsync(); } } - public Task WritePingAsync(Http2PingFrameFlags flags, ReadOnlySpan payload) + /* https://tools.ietf.org/html/rfc7540#section-6.7 + +---------------------------------------------------------------+ + | | + | Opaque Data (64) | + | | + +---------------------------------------------------------------+ + */ + public Task WritePingAsync(Http2PingFrameFlags flags, ReadOnlySequence payload) { lock (_writeLock) { + if (_completed) + { + return Task.CompletedTask; + } + _outgoingFrame.PreparePing(Http2PingFrameFlags.ACK); - payload.CopyTo(_outgoingFrame.Payload); - return WriteFrameUnsynchronizedAsync(); + Debug.Assert(payload.Length == _outgoingFrame.PayloadLength); // 8 + WriteHeaderUnsynchronized(); + foreach (var segment in payload) + { + _outputWriter.Write(segment.Span); + } + + return _flusher.FlushAsync(); } } + /* https://tools.ietf.org/html/rfc7540#section-6.8 + +-+-------------------------------------------------------------+ + |R| Last-Stream-ID (31) | + +-+-------------------------------------------------------------+ + | Error Code (32) | + +---------------------------------------------------------------+ + | Additional Debug Data (*) | (not implemented) + +---------------------------------------------------------------+ + */ public Task WriteGoAwayAsync(int lastStreamId, Http2ErrorCode errorCode) { lock (_writeLock) { _outgoingFrame.PrepareGoAway(lastStreamId, errorCode); - return WriteFrameUnsynchronizedAsync(); + WriteHeaderUnsynchronized(); + + var buffer = _outputWriter.GetSpan(8); + Bitshifter.WriteUInt31BigEndian(buffer, (uint)lastStreamId); + buffer = buffer.Slice(4); + BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)errorCode); + _outputWriter.Advance(8); + + return _flusher.FlushAsync(); } } - private Task WriteFrameUnsynchronizedAsync() + private void WriteHeaderUnsynchronized() { - if (_completed) - { - return Task.CompletedTask; - } - _log.Http2FrameSending(_connectionId, _outgoingFrame); - _outputWriter.Write(_outgoingFrame.Raw); - return _flusher.FlushAsync(); + WriteHeader(_outgoingFrame, _outputWriter); + } + + /* https://tools.ietf.org/html/rfc7540#section-4.1 + +-----------------------------------------------+ + | Length (24) | + +---------------+---------------+---------------+ + | Type (8) | Flags (8) | + +-+-------------+---------------+-------------------------------+ + |R| Stream Identifier (31) | + +=+=============================================================+ + | Frame Payload (0...) ... + +---------------------------------------------------------------+ + */ + internal static void WriteHeader(Http2Frame frame, PipeWriter output) + { + var buffer = output.GetSpan(Http2FrameReader.HeaderLength); + + Bitshifter.WriteUInt24BigEndian(buffer, (uint)frame.PayloadLength); + buffer = buffer.Slice(3); + + buffer[0] = (byte)frame.Type; + buffer[1] = frame.Flags; + buffer = buffer.Slice(2); + + Bitshifter.WriteUInt31BigEndian(buffer, (uint)frame.StreamId); + + output.Advance(Http2FrameReader.HeaderLength); } public bool TryUpdateConnectionWindow(int bytes) diff --git a/src/Kestrel.Core/Internal/Http2/Http2Stream.cs b/src/Kestrel.Core/Internal/Http2/Http2Stream.cs index 64e32d560e..8bcdd5fcdc 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Stream.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Stream.cs @@ -279,7 +279,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } - public Task OnDataAsync(Http2Frame dataFrame) + public Task OnDataAsync(Http2Frame dataFrame, ReadOnlySequence payload) { // Since padding isn't buffered, immediately count padding bytes as read for flow control purposes. if (dataFrame.DataHasPadding) @@ -288,10 +288,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 OnDataRead(dataFrame.DataPadLength + 1); } - var payload = dataFrame.DataPayload; + var dataPayload = payload.Slice(0, dataFrame.DataPayloadLength); // minus padding var endStream = dataFrame.DataEndStream; - if (payload.Length > 0) + if (dataPayload.Length > 0) { RequestBodyStarted = true; @@ -302,7 +302,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _inputFlowControl.StopWindowUpdates(); } - _inputFlowControl.Advance(payload.Length); + _inputFlowControl.Advance((int)dataPayload.Length); if (IsAborted) { @@ -315,15 +315,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 if (InputRemaining.HasValue) { // https://tools.ietf.org/html/rfc7540#section-8.1.2.6 - if (payload.Length > InputRemaining.Value) + if (dataPayload.Length > InputRemaining.Value) { throw new Http2StreamErrorException(StreamId, CoreStrings.Http2StreamErrorMoreDataThanLength, Http2ErrorCode.PROTOCOL_ERROR); } - InputRemaining -= payload.Length; + InputRemaining -= dataPayload.Length; } - RequestBodyPipe.Writer.Write(payload); + foreach (var segment in dataPayload) + { + RequestBodyPipe.Writer.Write(segment.Span); + } var flushTask = RequestBodyPipe.Writer.FlushAsync(); // It shouldn't be possible for the RequestBodyPipe to fill up an return an incomplete task if diff --git a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs index c9710cd26e..4717a70da3 100644 --- a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs +++ b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs @@ -2128,20 +2128,6 @@ 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"); - /// /// A value between {min} and {max} is required. /// @@ -2170,6 +2156,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core internal static string FormatHPackErrorDynamicTableSizeUpdateNotAtBeginningOfHeaderBlock() => GetString("HPackErrorDynamicTableSizeUpdateNotAtBeginningOfHeaderBlock"); + /// + /// The given buffer was too small to encode any headers. + /// + internal static string HPackErrorNotEnoughBuffer + { + get => GetString("HPackErrorNotEnoughBuffer"); + } + + /// + /// The given buffer was too small to encode any headers. + /// + internal static string FormatHPackErrorNotEnoughBuffer() + => GetString("HPackErrorNotEnoughBuffer"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/test/Kestrel.Core.Tests/HPackDecoderTests.cs b/test/Kestrel.Core.Tests/HPackDecoderTests.cs index 70d1f3e085..c45614348a 100644 --- a/test/Kestrel.Core.Tests/HPackDecoderTests.cs +++ b/test/Kestrel.Core.Tests/HPackDecoderTests.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Text; @@ -104,7 +105,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void DecodesIndexedHeaderField_StaticTable() { - _decoder.Decode(_indexedHeaderStatic, endHeaders: true, handler: this); + _decoder.Decode(new ReadOnlySequence(_indexedHeaderStatic), endHeaders: true, handler: this); Assert.Equal("GET", _decodedHeaders[HeaderNames.Method]); } @@ -115,14 +116,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _dynamicTable.Insert(_headerNameBytes, _headerValueBytes); // Index it - _decoder.Decode(_indexedHeaderDynamic, endHeaders: true, handler: this); + _decoder.Decode(new ReadOnlySequence(_indexedHeaderDynamic), endHeaders: true, handler: this); Assert.Equal(_headerValueString, _decodedHeaders[_headerNameString]); } [Fact] public void DecodesIndexedHeaderField_OutOfRange_Error() { - var exception = Assert.Throws(() => _decoder.Decode(_indexedHeaderDynamic, endHeaders: true, handler: this)); + var exception = Assert.Throws(() => + _decoder.Decode(new ReadOnlySequence(_indexedHeaderDynamic), endHeaders: true, handler: this)); Assert.Equal(CoreStrings.FormatHPackErrorIndexOutOfRange(62), exception.Message); Assert.Empty(_decodedHeaders); } @@ -198,7 +200,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests // 11 1110 (Indexed Name - Index 62 encoded with 6-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation) // Index 62 is the first entry in the dynamic table. If there's nothing there, the decoder should throw. - var exception = Assert.Throws(() => _decoder.Decode(new byte[] { 0x7e }, endHeaders: true, handler: this)); + var exception = Assert.Throws(() => _decoder.Decode(new ReadOnlySequence(new byte[] { 0x7e }), endHeaders: true, handler: this)); Assert.Equal(CoreStrings.FormatHPackErrorIndexOutOfRange(62), exception.Message); Assert.Empty(_decodedHeaders); } @@ -274,7 +276,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests // 1111 0010 1111 (Indexed Name - Index 62 encoded with 4-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation) // Index 62 is the first entry in the dynamic table. If there's nothing there, the decoder should throw. - var exception = Assert.Throws(() => _decoder.Decode(new byte[] { 0x0f, 0x2f }, endHeaders: true, handler: this)); + var exception = Assert.Throws(() => _decoder.Decode(new ReadOnlySequence(new byte[] { 0x0f, 0x2f }), endHeaders: true, handler: this)); Assert.Equal(CoreStrings.FormatHPackErrorIndexOutOfRange(62), exception.Message); Assert.Empty(_decodedHeaders); } @@ -356,7 +358,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests // 1111 0010 1111 (Indexed Name - Index 62 encoded with 4-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation) // Index 62 is the first entry in the dynamic table. If there's nothing there, the decoder should throw. - var exception = Assert.Throws(() => _decoder.Decode(new byte[] { 0x1f, 0x2f }, endHeaders: true, handler: this)); + var exception = Assert.Throws(() => _decoder.Decode(new ReadOnlySequence(new byte[] { 0x1f, 0x2f }), endHeaders: true, handler: this)); Assert.Equal(CoreStrings.FormatHPackErrorIndexOutOfRange(62), exception.Message); Assert.Empty(_decodedHeaders); } @@ -369,7 +371,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize); - _decoder.Decode(new byte[] { 0x3e }, endHeaders: true, handler: this); + _decoder.Decode(new ReadOnlySequence(new byte[] { 0x3e }), endHeaders: true, handler: this); Assert.Equal(30, _dynamicTable.MaxSize); Assert.Empty(_decodedHeaders); @@ -383,7 +385,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize); - var exception = Assert.Throws(() => _decoder.Decode(_indexedHeaderStatic.Concat(new byte[] { 0x3e }).ToArray(), endHeaders: true, handler: this)); + var data = new ReadOnlySequence(_indexedHeaderStatic.Concat(new byte[] { 0x3e }).ToArray()); + var exception = Assert.Throws(() => _decoder.Decode(data, endHeaders: true, handler: this)); Assert.Equal(CoreStrings.HPackErrorDynamicTableSizeUpdateNotAtBeginningOfHeaderBlock, exception.Message); } @@ -392,12 +395,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize); - _decoder.Decode(_indexedHeaderStatic, endHeaders: false, handler: this); + _decoder.Decode(new ReadOnlySequence(_indexedHeaderStatic), endHeaders: false, handler: this); Assert.Equal("GET", _decodedHeaders[HeaderNames.Method]); // 001 (Dynamic Table Size Update) // 11110 (30 encoded with 5-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation) - var exception = Assert.Throws(() => _decoder.Decode(new byte[] { 0x3e }, endHeaders: true, handler: this)); + var data = new ReadOnlySequence(new byte[] { 0x3e }); + var exception = Assert.Throws(() => _decoder.Decode(data, endHeaders: true, handler: this)); Assert.Equal(CoreStrings.HPackErrorDynamicTableSizeUpdateNotAtBeginningOfHeaderBlock, exception.Message); } @@ -406,12 +410,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize); - _decoder.Decode(_indexedHeaderStatic, endHeaders: true, handler: this); + _decoder.Decode(new ReadOnlySequence(_indexedHeaderStatic), endHeaders: true, handler: this); Assert.Equal("GET", _decodedHeaders[HeaderNames.Method]); // 001 (Dynamic Table Size Update) // 11110 (30 encoded with 5-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation) - _decoder.Decode(new byte[] { 0x3e }, endHeaders: true, handler: this); + _decoder.Decode(new ReadOnlySequence(new byte[] { 0x3e }), endHeaders: true, handler: this); Assert.Equal(30, _dynamicTable.MaxSize); } @@ -424,7 +428,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize); - var exception = Assert.Throws(() => _decoder.Decode(new byte[] { 0x3f, 0xe2, 0x1f }, endHeaders: true, handler: this)); + var exception = Assert.Throws(() => + _decoder.Decode(new ReadOnlySequence(new byte[] { 0x3f, 0xe2, 0x1f }), endHeaders: true, handler: this)); Assert.Equal(CoreStrings.FormatHPackErrorDynamicTableSizeUpdateTooLarge(4097, DynamicTableInitialMaxSize), exception.Message); Assert.Empty(_decodedHeaders); } @@ -436,7 +441,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests .Concat(new byte[] { 0xff, 0x82, 0x1f }) // 4097 encoded with 7-bit prefix .ToArray(); - var exception = Assert.Throws(() => _decoder.Decode(encoded, endHeaders: true, handler: this)); + var exception = Assert.Throws(() => _decoder.Decode(new ReadOnlySequence(encoded), endHeaders: true, handler: this)); Assert.Equal(CoreStrings.FormatHPackStringLengthTooLarge(4097, HPackDecoder.MaxStringOctets), exception.Message); Assert.Empty(_decodedHeaders); } @@ -526,7 +531,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [MemberData(nameof(_incompleteHeaderBlockData))] public void DecodesIncompleteHeaderBlock_Error(byte[] encoded) { - var exception = Assert.Throws(() => _decoder.Decode(encoded, endHeaders: true, handler: this)); + var exception = Assert.Throws(() => _decoder.Decode(new ReadOnlySequence(encoded), endHeaders: true, handler: this)); Assert.Equal(CoreStrings.HPackErrorIncompleteHeaderBlock, exception.Message); Assert.Empty(_decodedHeaders); } @@ -560,7 +565,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [MemberData(nameof(_huffmanDecodingErrorData))] public void WrapsHuffmanDecodingExceptionInHPackDecodingException(byte[] encoded) { - var exception = Assert.Throws(() => _decoder.Decode(encoded, endHeaders: true, handler: this)); + var exception = Assert.Throws(() => _decoder.Decode(new ReadOnlySequence(encoded), endHeaders: true, handler: this)); Assert.Equal(CoreStrings.HPackHuffmanError, exception.Message); Assert.IsType(exception.InnerException); Assert.Empty(_decodedHeaders); @@ -581,7 +586,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.Equal(0, _dynamicTable.Count); Assert.Equal(0, _dynamicTable.Size); - _decoder.Decode(encoded, endHeaders: true, handler: this); + _decoder.Decode(new ReadOnlySequence(encoded), endHeaders: true, handler: this); Assert.Equal(expectedHeaderValue, _decodedHeaders[expectedHeaderName]); diff --git a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index 3a97c60a79..a098609f91 100644 --- a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -84,8 +84,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await InitializeConnectionAsync(_echoApplication); await StartStreamAsync(1, _browserRequestHeaders, endStream: false); + uint length = Http2PeerSettings.MinAllowedMaxFrameSize + 1; - await SendDataAsync(1, new byte[length].AsSpan(), endStream: true); + await SendDataAsync(1, new byte[length], endStream: true); await WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: true, @@ -104,7 +105,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await InitializeConnectionAsync(_echoApplication, expectedSettingsCount: 3); await StartStreamAsync(1, _browserRequestHeaders, endStream: false); - await SendDataAsync(1, new byte[length].AsSpan(), endStream: true); + await SendDataAsync(1, new byte[length], endStream: true); await ExpectAsync(Http2FrameType.HEADERS, withLength: 37, @@ -150,7 +151,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - Assert.True(_helloWorldBytes.AsSpan().SequenceEqual(dataFrame.DataPayload)); + Assert.True(_helloWorldBytes.AsSpan().SequenceEqual(dataFrame.PayloadSequence.ToArray())); } [Fact] @@ -177,7 +178,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame.DataPayload)); + Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame.PayloadSequence.ToArray())); } [Fact] @@ -249,10 +250,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - 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.True(_maxData.AsSpan().SequenceEqual(dataFrame1.PayloadSequence.ToArray())); + Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame2.PayloadSequence.ToArray())); + Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame3.PayloadSequence.ToArray())); + Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame4.PayloadSequence.ToArray())); Assert.Equal(_maxData.Length * 2, streamWindowUpdateFrame1.WindowUpdateSizeIncrement); Assert.Equal(_maxData.Length * 2, connectionWindowUpdateFrame1.WindowUpdateSizeIncrement); Assert.Equal(_maxData.Length * 2, connectionWindowUpdateFrame2.WindowUpdateSizeIncrement); @@ -287,7 +288,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - Assert.True(_helloWorldBytes.AsSpan().SequenceEqual(dataFrame.DataPayload)); + Assert.True(_helloWorldBytes.AsSpan().SequenceEqual(dataFrame.PayloadSequence.ToArray())); } [Fact] @@ -350,10 +351,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); - 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)); + Assert.True(_helloBytes.AsSpan().SequenceEqual(stream1DataFrame1.PayloadSequence.ToArray())); + Assert.True(_worldBytes.AsSpan().SequenceEqual(stream1DataFrame2.PayloadSequence.ToArray())); + Assert.True(_helloBytes.AsSpan().SequenceEqual(stream3DataFrame1.PayloadSequence.ToArray())); + Assert.True(_worldBytes.AsSpan().SequenceEqual(stream3DataFrame2.PayloadSequence.ToArray())); } [Fact] @@ -443,16 +444,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); - 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(dataFrame1.PayloadSequence.ToArray())); + Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame2.PayloadSequence.ToArray())); + Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame3.PayloadSequence.ToArray())); Assert.Equal(_maxData.Length * 2, streamWindowUpdateFrame.WindowUpdateSizeIncrement); Assert.Equal(_maxData.Length * 2, connectionWindowUpdateFrame1.WindowUpdateSizeIncrement); - Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame4.DataPayload)); + Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame4.PayloadSequence.ToArray())); Assert.Equal(_maxData.Length * 2, connectionWindowUpdateFrame2.WindowUpdateSizeIncrement); - Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame5.DataPayload)); + Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame5.PayloadSequence.ToArray())); } [Fact] @@ -547,7 +548,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - Assert.True(_helloWorldBytes.AsSpan().SequenceEqual(dataFrame.DataPayload)); + Assert.True(_helloWorldBytes.AsSpan().SequenceEqual(dataFrame.PayloadSequence.ToArray())); } [Theory] @@ -564,7 +565,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await InitializeConnectionAsync(_echoApplication); await StartStreamAsync(1, _browserRequestHeaders, endStream: false); - await SendDataWithPaddingAsync(1, maxDataMinusPadding.Span, padLength, endStream: false); + await SendDataWithPaddingAsync(1, maxDataMinusPadding, padLength, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, withLength: 37, @@ -595,8 +596,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - Assert.True(maxDataMinusPadding.Span.SequenceEqual(dataFrame1.DataPayload)); - Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame2.DataPayload)); + Assert.True(maxDataMinusPadding.Span.SequenceEqual(dataFrame1.PayloadSequence.ToArray())); + Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame2.PayloadSequence.ToArray())); Assert.Equal(_maxData.Length * 2, connectionWindowUpdateFrame.WindowUpdateSizeIncrement); } @@ -703,8 +704,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: true, expectedLastStreamId: 1, - expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, - expectedErrorMessage: CoreStrings.Http2FrameMissingFields); + expectedErrorCode: Http2ErrorCode.FRAME_SIZE_ERROR, + expectedErrorMessage: CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(Http2FrameType.DATA, expectedLength: 1)); } [Fact] @@ -1127,6 +1128,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); + Assert.Equal(new byte[] { 0x08, 0x03, (byte)'1', (byte)'0', (byte)'0' }, frame.PayloadSequence.ToArray()); + await SendDataAsync(1, _helloBytes, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, @@ -1142,8 +1145,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests withFlags: (byte)Http2DataFrameFlags.END_STREAM, withStreamId: 1); - Assert.Equal(new byte[] { 0x08, 0x03, (byte)'1', (byte)'0', (byte)'0' }, frame.HeadersPayload.ToArray()); - await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); } @@ -1361,8 +1362,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: true, expectedLastStreamId: 0, - expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, - expectedErrorMessage: CoreStrings.Http2FrameMissingFields); + expectedErrorCode: Http2ErrorCode.FRAME_SIZE_ERROR, + expectedErrorMessage: CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(Http2FrameType.HEADERS, expectedLength: 1)); } [Theory] @@ -1924,7 +1925,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests withFlags: (byte)Http2DataFrameFlags.NONE, withStreamId: streamId); - Assert.True(_helloWorldBytes.AsSpan(0, initialWindowSize).SequenceEqual(dataFrame.DataPayload)); + Assert.True(_helloWorldBytes.AsSpan(0, initialWindowSize).SequenceEqual(dataFrame.PayloadSequence.ToArray())); Assert.False(writeTasks[streamId].IsCompleted); } @@ -2064,13 +2065,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendSettingsAsync(); var frame = await ExpectAsync(Http2FrameType.SETTINGS, - withLength: Http2Frame.SettingSize * 2, + withLength: Http2FrameReader.SettingSize * 2, withFlags: 0, withStreamId: 0); // Only non protocol defaults are sent - Assert.Equal(2, frame.SettingsCount); - var settings = frame.GetSettings(); + var settings = Http2FrameReader.ReadSettings(frame.PayloadSequence); Assert.Equal(2, settings.Count); var setting = settings[0]; @@ -2101,13 +2101,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendSettingsAsync(); var frame = await ExpectAsync(Http2FrameType.SETTINGS, - withLength: 6 * 2, + withLength: Http2FrameReader.SettingSize * 2, withFlags: 0, withStreamId: 0); // Only non protocol defaults are sent - Assert.Equal(2, frame.SettingsCount); - var settings = frame.GetSettings(); + var settings = Http2FrameReader.ReadSettings(frame.PayloadSequence); Assert.Equal(2, settings.Count); var setting = settings[0]; @@ -2139,9 +2138,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { await InitializeConnectionAsync(_noopApplication); - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var frame = new Http2Frame(); frame.PrepareSettings(Http2SettingsFrameFlags.ACK); - await SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, _pair.Application.Output); + await FlushAsync(_pair.Application.Output); await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); } @@ -2260,6 +2260,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public async Task SETTINGS_Received_ChangesAllowedResponseMaxFrameSize() { + _connection.ServerSettings.MaxFrameSize = Http2PeerSettings.MaxAllowedMaxFrameSize; // This includes the default response headers such as :status, etc var defaultResponseHeaderLength = 37; var headerValueLength = Http2PeerSettings.MinAllowedMaxFrameSize; @@ -2276,7 +2277,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests context.Response.Headers["A"] = new string('a', headerValueLength); context.Response.Headers["B"] = new string('b', headerValueLength); return context.Response.Body.WriteAsync(new byte[payloadLength], 0, payloadLength); - }); + }, expectedSettingsCount: 3); // Update client settings _clientSettings.MaxFrameSize = (uint)payloadLength; @@ -2311,6 +2312,42 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); } + [Fact] + public async Task SETTINGS_Received_ClientMaxFrameSizeCannotExceedServerMaxFrameSize() + { + var serverMaxFrame = Http2PeerSettings.MinAllowedMaxFrameSize + 1024; + _connection.ServerSettings.MaxFrameSize = Http2PeerSettings.MinAllowedMaxFrameSize + 1024; + var clientMaxFrame = serverMaxFrame + 1024 * 5; + _clientSettings.MaxFrameSize = (uint)clientMaxFrame; + + await InitializeConnectionAsync(context => + { + return context.Response.Body.WriteAsync(new byte[clientMaxFrame], 0, clientMaxFrame); + }, expectedSettingsCount: 3); + + // Start request + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + + await ExpectAsync(Http2FrameType.HEADERS, + withLength: 37, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: serverMaxFrame, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: clientMaxFrame - serverMaxFrame, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: 0, + withFlags: (byte)Http2DataFrameFlags.END_STREAM, + withStreamId: 1); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + } + [Fact] public async Task SETTINGS_Received_ChangesHeaderTableSize() { @@ -2627,7 +2664,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests withFlags: (byte)Http2DataFrameFlags.NONE, withStreamId: streamId); - Assert.True(_helloWorldBytes.AsSpan(0, initialWindowSize).SequenceEqual(dataFrame.DataPayload)); + Assert.True(_helloWorldBytes.AsSpan(0, initialWindowSize).SequenceEqual(dataFrame.PayloadSequence.ToArray())); Assert.False(writeTasks[streamId].IsCompleted); } @@ -2931,8 +2968,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - Assert.True(_helloWorldBytes.AsSpan(0, initialWindowSize).SequenceEqual(dataFrame1.DataPayload)); - Assert.True(_helloWorldBytes.AsSpan(initialWindowSize, initialWindowSize).SequenceEqual(dataFrame2.DataPayload)); + Assert.True(_helloWorldBytes.AsSpan(0, initialWindowSize).SequenceEqual(dataFrame1.PayloadSequence.ToArray())); + Assert.True(_helloWorldBytes.AsSpan(initialWindowSize, initialWindowSize).SequenceEqual(dataFrame2.PayloadSequence.ToArray())); } [Fact] @@ -2986,9 +3023,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - 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)); + Assert.True(_helloWorldBytes.AsSpan(0, 6).SequenceEqual(dataFrame1.PayloadSequence.ToArray())); + Assert.True(_helloWorldBytes.AsSpan(6, 3).SequenceEqual(dataFrame2.PayloadSequence.ToArray())); + Assert.True(_helloWorldBytes.AsSpan(9, 3).SequenceEqual(dataFrame3.PayloadSequence.ToArray())); } [Fact] @@ -3190,9 +3227,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); - _hpackDecoder.Decode(continuationFrame1.HeadersPayload, endHeaders: false, handler: this); - _hpackDecoder.Decode(continuationFrame2.HeadersPayload, endHeaders: true, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); + _hpackDecoder.Decode(continuationFrame1.PayloadSequence, endHeaders: false, handler: this); + _hpackDecoder.Decode(continuationFrame2.PayloadSequence, endHeaders: true, handler: this); Assert.Equal(11, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); diff --git a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs index 8c623167fb..c8623ced46 100644 --- a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs @@ -86,7 +86,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(4, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -115,7 +115,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(4, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -146,7 +146,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(5, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -178,7 +178,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(5, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -216,7 +216,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(6, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -262,7 +262,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(3, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -328,7 +328,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(3, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -361,7 +361,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(3, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -394,7 +394,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(4, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -429,7 +429,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(4, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -464,7 +464,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(4, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -499,7 +499,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(4, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -610,7 +610,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests }); await StartStreamAsync(1, headers, endStream: false); - await SendDataAsync(1, new byte[12].AsSpan(), endStream: true); + await SendDataAsync(1, new byte[12], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, withLength: 55, @@ -623,7 +623,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(3, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -653,7 +653,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests new KeyValuePair(HeaderNames.ContentLength, "12"), }; await StartStreamAsync(1, headers, endStream: false); - await SendDataAsync(1, new byte[12].AsSpan(), endStream: true); + await SendDataAsync(1, new byte[12], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, withLength: 55, @@ -666,7 +666,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(3, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -699,9 +699,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, headers, endStream: false); - await SendDataAsync(1, new byte[1].AsSpan(), endStream: false); - await SendDataAsync(1, new byte[3].AsSpan(), endStream: false); - await SendDataAsync(1, new byte[8].AsSpan(), endStream: true); + await SendDataAsync(1, new byte[1], endStream: false); + await SendDataAsync(1, new byte[3], endStream: false); + await SendDataAsync(1, new byte[8], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, withLength: 55, @@ -714,7 +714,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(3, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -786,7 +786,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests }); await StartStreamAsync(1, headers, endStream: false); - await SendDataAsync(1, new byte[13].AsSpan(), endStream: true); + await SendDataAsync(1, new byte[13], endStream: true); await WaitForStreamErrorAsync(1, Http2ErrorCode.PROTOCOL_ERROR, CoreStrings.Http2StreamErrorMoreDataThanLength); @@ -821,7 +821,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests }); await StartStreamAsync(1, headers, endStream: false); - await SendDataAsync(1, new byte[11].AsSpan(), endStream: true); + await SendDataAsync(1, new byte[11], endStream: true); await WaitForStreamErrorAsync(1, Http2ErrorCode.PROTOCOL_ERROR, CoreStrings.Http2StreamErrorLessDataThanLength); @@ -856,10 +856,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests }); await StartStreamAsync(1, headers, endStream: false); - await SendDataAsync(1, new byte[1].AsSpan(), endStream: false); - await SendDataAsync(1, new byte[2].AsSpan(), endStream: false); - await SendDataAsync(1, new byte[10].AsSpan(), endStream: false); - await SendDataAsync(1, new byte[2].AsSpan(), endStream: true); + await SendDataAsync(1, new byte[1], endStream: false); + await SendDataAsync(1, new byte[2], endStream: false); + await SendDataAsync(1, new byte[10], endStream: false); + await SendDataAsync(1, new byte[2], endStream: true); await WaitForStreamErrorAsync(1, Http2ErrorCode.PROTOCOL_ERROR, CoreStrings.Http2StreamErrorMoreDataThanLength); @@ -894,8 +894,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests }); await StartStreamAsync(1, headers, endStream: false); - await SendDataAsync(1, new byte[1].AsSpan(), endStream: false); - await SendDataAsync(1, new byte[2].AsSpan(), endStream: true); + await SendDataAsync(1, new byte[1], endStream: false); + await SendDataAsync(1, new byte[2], endStream: true); await WaitForStreamErrorAsync(1, Http2ErrorCode.PROTOCOL_ERROR, CoreStrings.Http2StreamErrorLessDataThanLength); @@ -938,7 +938,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(3, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -977,7 +977,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(3, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -1015,7 +1015,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(3, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -1053,7 +1053,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(3, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -1080,7 +1080,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests }); await StartStreamAsync(1, headers, endStream: false); - await SendDataAsync(1, new byte[12].AsSpan(), endStream: true); + await SendDataAsync(1, new byte[12], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, withLength: 55, @@ -1093,7 +1093,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(3, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -1136,7 +1136,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(3, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -1164,7 +1164,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests }); await StartStreamAsync(1, headers, endStream: false); - await SendDataAsync(1, new byte[12].AsSpan(), endStream: true); + await SendDataAsync(1, new byte[12], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, withLength: 55, @@ -1177,7 +1177,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(3, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -1207,9 +1207,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests }); await StartStreamAsync(1, headers, endStream: false); - await SendDataAsync(1, new byte[6].AsSpan(), endStream: false); - await SendDataAsync(1, new byte[6].AsSpan(), endStream: false); - await SendDataAsync(1, new byte[6].AsSpan(), endStream: true); + await SendDataAsync(1, new byte[6], endStream: false); + await SendDataAsync(1, new byte[6], endStream: false); + await SendDataAsync(1, new byte[6], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, withLength: 59, @@ -1222,7 +1222,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(3, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -1266,9 +1266,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests }); await StartStreamAsync(1, headers, endStream: false); - await SendDataAsync(1, new byte[6].AsSpan(), endStream: false); - await SendDataAsync(1, new byte[6].AsSpan(), endStream: false); - await SendDataAsync(1, new byte[6].AsSpan(), endStream: true); + await SendDataAsync(1, new byte[6], endStream: false); + await SendDataAsync(1, new byte[6], endStream: false); + await SendDataAsync(1, new byte[6], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, withLength: 59, @@ -1281,7 +1281,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(3, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -1321,7 +1321,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests }); await StartStreamAsync(1, headers, endStream: false); - await SendDataAsync(1, new byte[12].AsSpan(), endStream: true); + await SendDataAsync(1, new byte[12], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, withLength: 55, @@ -1334,7 +1334,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(3, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -1371,7 +1371,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(3, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -1409,7 +1409,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); - _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this); + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); Assert.Equal(2, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); @@ -1735,5 +1735,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); } + + [Fact] + public async Task ResponseWithHeadersTooLarge_AbortsConnection() + { + var appFinished = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + await InitializeConnectionAsync(async context => + { + context.Response.Headers["too_long"] = new string('a', (int)Http2PeerSettings.DefaultMaxFrameSize); + var ex = await Assert.ThrowsAsync(() => context.Response.WriteAsync("Hello World")).DefaultTimeout(); + appFinished.TrySetResult(ex.InnerException.Message); + }); + + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + + var message = await appFinished.Task.DefaultTimeout(); + Assert.Equal(CoreStrings.HPackErrorNotEnoughBuffer, message); + } } } \ No newline at end of file diff --git a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2TestBase.cs b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2TestBase.cs index 126cbf8ef1..ed1fecc8d9 100644 --- a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2TestBase.cs +++ b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -3,6 +3,7 @@ using System; using System.Buffers; +using System.Buffers.Binary; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; @@ -11,6 +12,7 @@ using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; @@ -20,7 +22,6 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Testing; -using Microsoft.AspNetCore.Connections; using Microsoft.Net.Http.Headers; using Moq; using Xunit; @@ -51,6 +52,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected readonly Http2PeerSettings _clientSettings = new Http2PeerSettings(); protected readonly HPackEncoder _hpackEncoder = new HPackEncoder(); protected readonly HPackDecoder _hpackDecoder; + private readonly byte[] _headerEncodingBuffer = new byte[Http2PeerSettings.MinAllowedMaxFrameSize]; protected readonly ConcurrentDictionary> _runningStreams = new ConcurrentDictionary>(); protected readonly Dictionary _receivedHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -315,7 +317,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendSettingsAsync(); await ExpectAsync(Http2FrameType.SETTINGS, - withLength: expectedSettingsCount * Http2Frame.SettingSize, + withLength: expectedSettingsCount * Http2FrameReader.SettingSize, withFlags: 0, withStreamId: 0); @@ -325,14 +327,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests withStreamId: 0); } - protected async Task StartStreamAsync(int streamId, IEnumerable> headers, bool endStream) + protected Task StartStreamAsync(int streamId, IEnumerable> headers, bool endStream) { + var writableBuffer = _pair.Application.Output; var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _runningStreams[streamId] = tcs; - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var frame = new Http2Frame(); frame.PrepareHeaders(Http2HeadersFrameFlags.NONE, streamId); - var done = _hpackEncoder.BeginEncode(headers, frame.HeadersPayload, out var length); + + var buffer = _headerEncodingBuffer.AsSpan(); + var done = _hpackEncoder.BeginEncode(headers, buffer, out var length); frame.PayloadLength = length; if (done) @@ -345,12 +350,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests frame.HeadersFlags |= Http2HeadersFrameFlags.END_STREAM; } - await SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, writableBuffer); + writableBuffer.Write(buffer.Slice(0, length)); while (!done) { frame.PrepareContinuation(Http2ContinuationFrameFlags.NONE, streamId); - done = _hpackEncoder.Encode(frame.HeadersPayload, out length); + + done = _hpackEncoder.Encode(buffer, out length); frame.PayloadLength = length; if (done) @@ -358,77 +365,143 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests frame.ContinuationFlags = Http2ContinuationFrameFlags.END_HEADERS; } - await SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, writableBuffer); + writableBuffer.Write(buffer.Slice(0, length)); } + + return FlushAsync(writableBuffer); } - protected async Task SendHeadersWithPaddingAsync(int streamId, IEnumerable> headers, byte padLength, bool endStream) + /* https://tools.ietf.org/html/rfc7540#section-6.2 + +---------------+ + |Pad Length? (8)| + +-+-------------+-----------------------------------------------+ + | Header Block Fragment (*) ... + +---------------------------------------------------------------+ + | Padding (*) ... + +---------------------------------------------------------------+ + */ + protected Task SendHeadersWithPaddingAsync(int streamId, IEnumerable> headers, byte padLength, bool endStream) { + var writableBuffer = _pair.Application.Output; var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _runningStreams[streamId] = tcs; - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var frame = new Http2Frame(); frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.PADDED, streamId); frame.HeadersPadLength = padLength; - _hpackEncoder.BeginEncode(headers, frame.HeadersPayload, out var length); + var extendedHeaderLength = 1; // Padding length field + var buffer = _headerEncodingBuffer.AsSpan(); + var extendedHeader = buffer.Slice(0, extendedHeaderLength); + extendedHeader[0] = padLength; + var payload = buffer.Slice(extendedHeaderLength, buffer.Length - padLength - extendedHeaderLength); - frame.PayloadLength = 1 + length + padLength; - frame.Payload.Slice(1 + length).Fill(0); + _hpackEncoder.BeginEncode(headers, payload, out var length); + var padding = buffer.Slice(extendedHeaderLength + length, padLength); + padding.Fill(0); + + frame.PayloadLength = extendedHeaderLength + length + padLength; if (endStream) { frame.HeadersFlags |= Http2HeadersFrameFlags.END_STREAM; } - await SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, writableBuffer); + writableBuffer.Write(buffer.Slice(0, frame.PayloadLength)); + return FlushAsync(writableBuffer); } - protected async Task SendHeadersWithPriorityAsync(int streamId, IEnumerable> headers, byte priority, int streamDependency, bool endStream) + /* https://tools.ietf.org/html/rfc7540#section-6.2 + +-+-------------+-----------------------------------------------+ + |E| Stream Dependency? (31) | + +-+-------------+-----------------------------------------------+ + | Weight? (8) | + +-+-------------+-----------------------------------------------+ + | Header Block Fragment (*) ... + +---------------------------------------------------------------+ + */ + protected Task SendHeadersWithPriorityAsync(int streamId, IEnumerable> headers, byte priority, int streamDependency, bool endStream) { + var writableBuffer = _pair.Application.Output; var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _runningStreams[streamId] = tcs; - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var frame = new Http2Frame(); frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.PRIORITY, streamId); frame.HeadersPriorityWeight = priority; frame.HeadersStreamDependency = streamDependency; - _hpackEncoder.BeginEncode(headers, frame.HeadersPayload, out var length); + var extendedHeaderLength = 5; // stream dependency + weight + var buffer = _headerEncodingBuffer.AsSpan(); + var extendedHeader = buffer.Slice(0, extendedHeaderLength); + Bitshifter.WriteUInt31BigEndian(extendedHeader, (uint)streamDependency); + extendedHeader[4] = priority; + var payload = buffer.Slice(extendedHeaderLength); - frame.PayloadLength = 5 + length; + _hpackEncoder.BeginEncode(headers, payload, out var length); + + frame.PayloadLength = extendedHeaderLength + length; if (endStream) { frame.HeadersFlags |= Http2HeadersFrameFlags.END_STREAM; } - await SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, writableBuffer); + writableBuffer.Write(buffer.Slice(0, frame.PayloadLength)); + return FlushAsync(writableBuffer); } - protected async Task SendHeadersWithPaddingAndPriorityAsync(int streamId, IEnumerable> headers, byte padLength, byte priority, int streamDependency, bool endStream) + /* https://tools.ietf.org/html/rfc7540#section-6.2 + +---------------+ + |Pad Length? (8)| + +-+-------------+-----------------------------------------------+ + |E| Stream Dependency? (31) | + +-+-------------+-----------------------------------------------+ + | Weight? (8) | + +-+-------------+-----------------------------------------------+ + | Header Block Fragment (*) ... + +---------------------------------------------------------------+ + | Padding (*) ... + +---------------------------------------------------------------+ + */ + protected Task SendHeadersWithPaddingAndPriorityAsync(int streamId, IEnumerable> headers, byte padLength, byte priority, int streamDependency, bool endStream) { + var writableBuffer = _pair.Application.Output; var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _runningStreams[streamId] = tcs; - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var frame = new Http2Frame(); frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.PADDED | Http2HeadersFrameFlags.PRIORITY, streamId); frame.HeadersPadLength = padLength; frame.HeadersPriorityWeight = priority; frame.HeadersStreamDependency = streamDependency; - _hpackEncoder.BeginEncode(headers, frame.HeadersPayload, out var length); + var extendedHeaderLength = 6; // pad length + stream dependency + weight + var buffer = _headerEncodingBuffer.AsSpan(); + var extendedHeader = buffer.Slice(0, extendedHeaderLength); + extendedHeader[0] = padLength; + Bitshifter.WriteUInt31BigEndian(extendedHeader.Slice(1), (uint)streamDependency); + extendedHeader[5] = priority; + var payload = buffer.Slice(extendedHeaderLength, buffer.Length - padLength - extendedHeaderLength); - frame.PayloadLength = 6 + length + padLength; - frame.Payload.Slice(6 + length).Fill(0); + _hpackEncoder.BeginEncode(headers, payload, out var length); + var padding = buffer.Slice(extendedHeaderLength + length, padLength); + padding.Fill(0); + + frame.PayloadLength = extendedHeaderLength + length + padLength; if (endStream) { frame.HeadersFlags |= Http2HeadersFrameFlags.END_STREAM; } - await SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, writableBuffer); + writableBuffer.Write(buffer.Slice(0, frame.PayloadLength)); + return FlushAsync(writableBuffer); } protected Task WaitForAllStreamsAsync() @@ -450,324 +523,428 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected Task SendPreambleAsync() => SendAsync(new ArraySegment(Http2Connection.ClientPreface)); - protected Task SendSettingsAsync() + protected async Task SendSettingsAsync() { - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); - frame.PrepareSettings(Http2SettingsFrameFlags.NONE, _clientSettings.GetNonProtocolDefaults()); - return SendAsync(frame.Raw); + var writableBuffer = _pair.Application.Output; + var frame = new Http2Frame(); + frame.PrepareSettings(Http2SettingsFrameFlags.NONE); + var settings = _clientSettings.GetNonProtocolDefaults(); + var payload = new byte[settings.Count * Http2FrameReader.SettingSize]; + frame.PayloadLength = payload.Length; + Http2FrameWriter.WriteSettings(settings, payload); + Http2FrameWriter.WriteHeader(frame, writableBuffer); + await SendAsync(payload); } - protected Task SendSettingsAckWithInvalidLengthAsync(int length) + protected async Task SendSettingsAckWithInvalidLengthAsync(int length) { - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var writableBuffer = _pair.Application.Output; + var frame = new Http2Frame(); frame.PrepareSettings(Http2SettingsFrameFlags.ACK); frame.PayloadLength = length; - return SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, writableBuffer); + await SendAsync(new byte[length]); } - protected Task SendSettingsWithInvalidStreamIdAsync(int streamId) + protected async Task SendSettingsWithInvalidStreamIdAsync(int streamId) { - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); - frame.PrepareSettings(Http2SettingsFrameFlags.NONE, _clientSettings.GetNonProtocolDefaults()); + var writableBuffer = _pair.Application.Output; + var frame = new Http2Frame(); + frame.PrepareSettings(Http2SettingsFrameFlags.NONE); frame.StreamId = streamId; - return SendAsync(frame.Raw); + var settings = _clientSettings.GetNonProtocolDefaults(); + var payload = new byte[settings.Count * Http2FrameReader.SettingSize]; + frame.PayloadLength = payload.Length; + Http2FrameWriter.WriteSettings(settings, payload); + Http2FrameWriter.WriteHeader(frame, writableBuffer); + await SendAsync(payload); } - protected Task SendSettingsWithInvalidLengthAsync(int length) + protected async Task SendSettingsWithInvalidLengthAsync(int length) { - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); - frame.PrepareSettings(Http2SettingsFrameFlags.NONE, _clientSettings.GetNonProtocolDefaults()); + var writableBuffer = _pair.Application.Output; + var frame = new Http2Frame(); + frame.PrepareSettings(Http2SettingsFrameFlags.NONE); + frame.PayloadLength = length; - return SendAsync(frame.Raw); + var payload = new byte[length]; + Http2FrameWriter.WriteHeader(frame, writableBuffer); + await SendAsync(payload); } - protected Task SendSettingsWithInvalidParameterValueAsync(Http2SettingsParameter parameter, uint value) + protected async Task SendSettingsWithInvalidParameterValueAsync(Http2SettingsParameter parameter, uint value) { - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var writableBuffer = _pair.Application.Output; + var frame = new Http2Frame(); frame.PrepareSettings(Http2SettingsFrameFlags.NONE); frame.PayloadLength = 6; + var payload = new byte[Http2FrameReader.SettingSize]; + payload[0] = (byte)((ushort)parameter >> 8); + payload[1] = (byte)(ushort)parameter; + payload[2] = (byte)(value >> 24); + payload[3] = (byte)(value >> 16); + payload[4] = (byte)(value >> 8); + payload[5] = (byte)value; - frame.Payload[0] = (byte)((ushort)parameter >> 8); - frame.Payload[1] = (byte)(ushort)parameter; - frame.Payload[2] = (byte)(value >> 24); - frame.Payload[3] = (byte)(value >> 16); - frame.Payload[4] = (byte)(value >> 8); - frame.Payload[5] = (byte)value; - - return SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, writableBuffer); + await SendAsync(payload); } protected Task SendPushPromiseFrameAsync() { - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var writableBuffer = _pair.Application.Output; + var frame = new Http2Frame(); frame.PayloadLength = 0; frame.Type = Http2FrameType.PUSH_PROMISE; frame.StreamId = 1; - return SendAsync(frame.Raw); + + Http2FrameWriter.WriteHeader(frame, writableBuffer); + return FlushAsync(writableBuffer); } protected async Task SendHeadersAsync(int streamId, Http2HeadersFrameFlags flags, IEnumerable> headers) { - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var outputWriter = _pair.Application.Output; + var frame = new Http2Frame(); frame.PrepareHeaders(flags, streamId); - var done = _hpackEncoder.BeginEncode(headers, frame.Payload, out var length); + var buffer = _headerEncodingBuffer.AsMemory(); + var done = _hpackEncoder.BeginEncode(headers, buffer.Span, out var length); frame.PayloadLength = length; - await SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, outputWriter); + await SendAsync(buffer.Span.Slice(0, length)); return done; } - protected Task SendHeadersAsync(int streamId, Http2HeadersFrameFlags flags, byte[] headerBlock) + protected async Task SendHeadersAsync(int streamId, Http2HeadersFrameFlags flags, byte[] headerBlock) { - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var outputWriter = _pair.Application.Output; + var frame = new Http2Frame(); frame.PrepareHeaders(flags, streamId); frame.PayloadLength = headerBlock.Length; - headerBlock.CopyTo(frame.HeadersPayload); - return SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, outputWriter); + await SendAsync(headerBlock); } - protected Task SendInvalidHeadersFrameAsync(int streamId, int payloadLength, byte padLength) + protected async Task SendInvalidHeadersFrameAsync(int streamId, int payloadLength, byte padLength) { Assert.True(padLength >= payloadLength, $"{nameof(padLength)} must be greater than or equal to {nameof(payloadLength)} to create an invalid frame."); - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var outputWriter = _pair.Application.Output; + var frame = new Http2Frame(); frame.PrepareHeaders(Http2HeadersFrameFlags.PADDED, streamId); - frame.Payload[0] = padLength; - - // Set length last so .Payload can be written to frame.PayloadLength = payloadLength; + var payload = new byte[payloadLength]; + if (payloadLength > 0) + { + payload[0] = padLength; + } - return SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, outputWriter); + await SendAsync(payload); } - protected Task SendIncompleteHeadersFrameAsync(int streamId) + protected async Task SendIncompleteHeadersFrameAsync(int streamId) { - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var outputWriter = _pair.Application.Output; + var frame = new Http2Frame(); frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS, streamId); frame.PayloadLength = 3; - + var payload = new byte[3]; // Set up an incomplete Literal Header Field w/ Incremental Indexing frame, // with an incomplete new name - frame.Payload[0] = 0; - frame.Payload[1] = 2; - frame.Payload[2] = (byte)'a'; + payload[0] = 0; + payload[1] = 2; + payload[2] = (byte)'a'; - return SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, outputWriter); + await SendAsync(payload); } protected async Task SendContinuationAsync(int streamId, Http2ContinuationFrameFlags flags) { - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var outputWriter = _pair.Application.Output; + var frame = new Http2Frame(); frame.PrepareContinuation(flags, streamId); - var done = _hpackEncoder.Encode(frame.Payload, out var length); + var buffer = _headerEncodingBuffer.AsMemory(); + var done = _hpackEncoder.Encode(buffer.Span, out var length); frame.PayloadLength = length; - await SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, outputWriter); + await SendAsync(buffer.Span.Slice(0, length)); return done; } protected async Task SendContinuationAsync(int streamId, Http2ContinuationFrameFlags flags, byte[] payload) { - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var outputWriter = _pair.Application.Output; + var frame = new Http2Frame(); frame.PrepareContinuation(flags, streamId); frame.PayloadLength = payload.Length; - payload.CopyTo(frame.Payload); - await SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, outputWriter); + await SendAsync(payload); } protected Task SendEmptyContinuationFrameAsync(int streamId, Http2ContinuationFrameFlags flags) { - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var outputWriter = _pair.Application.Output; + var frame = new Http2Frame(); frame.PrepareContinuation(flags, streamId); frame.PayloadLength = 0; - return SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, outputWriter); + return FlushAsync(outputWriter); } - protected Task SendIncompleteContinuationFrameAsync(int streamId) + protected async Task SendIncompleteContinuationFrameAsync(int streamId) { - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var outputWriter = _pair.Application.Output; + var frame = new Http2Frame(); frame.PrepareContinuation(Http2ContinuationFrameFlags.END_HEADERS, streamId); frame.PayloadLength = 3; - + var payload = new byte[3]; // Set up an incomplete Literal Header Field w/ Incremental Indexing frame, // with an incomplete new name - frame.Payload[0] = 0; - frame.Payload[1] = 2; - frame.Payload[2] = (byte)'a'; + payload[0] = 0; + payload[1] = 2; + payload[2] = (byte)'a'; - return SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, outputWriter); + await SendAsync(payload); } - protected Task SendDataAsync(int streamId, Span data, bool endStream) + protected Task SendDataAsync(int streamId, Memory data, bool endStream) { - var frame = new Http2Frame((uint)data.Length); + var outputWriter = _pair.Application.Output; + var frame = new Http2Frame(); frame.PrepareData(streamId); frame.PayloadLength = data.Length; frame.DataFlags = endStream ? Http2DataFrameFlags.END_STREAM : Http2DataFrameFlags.NONE; - data.CopyTo(frame.DataPayload); - return SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, outputWriter); + return SendAsync(data.Span); } - protected Task SendDataWithPaddingAsync(int streamId, Span data, byte padLength, bool endStream) + protected async Task SendDataWithPaddingAsync(int streamId, Memory data, byte padLength, bool endStream) { - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var outputWriter = _pair.Application.Output; + var frame = new Http2Frame(); frame.PrepareData(streamId, padLength); frame.PayloadLength = data.Length + 1 + padLength; - data.CopyTo(frame.DataPayload); if (endStream) { frame.DataFlags |= Http2DataFrameFlags.END_STREAM; } - return SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, outputWriter); + outputWriter.GetSpan(1)[0] = padLength; + outputWriter.Advance(1); + await SendAsync(data.Span); + await SendAsync(new byte[padLength]); } protected Task SendInvalidDataFrameAsync(int streamId, int frameLength, byte padLength) { Assert.True(padLength >= frameLength, $"{nameof(padLength)} must be greater than or equal to {nameof(frameLength)} to create an invalid frame."); - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var outputWriter = _pair.Application.Output; + var frame = new Http2Frame(); frame.PrepareData(streamId); frame.DataFlags = Http2DataFrameFlags.PADDED; - frame.Payload[0] = padLength; - - // Set length last so .Payload can be written to frame.PayloadLength = frameLength; + var payload = new byte[frameLength]; + if (frameLength > 0) + { + payload[0] = padLength; + } - return SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, outputWriter); + return SendAsync(payload); } protected Task SendPingAsync(Http2PingFrameFlags flags) { - var pingFrame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var outputWriter = _pair.Application.Output; + var pingFrame = new Http2Frame(); pingFrame.PreparePing(flags); - return SendAsync(pingFrame.Raw); + Http2FrameWriter.WriteHeader(pingFrame, outputWriter); + return SendAsync(new byte[8]); // Empty payload } protected Task SendPingWithInvalidLengthAsync(int length) { - var pingFrame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var outputWriter = _pair.Application.Output; + var pingFrame = new Http2Frame(); pingFrame.PreparePing(Http2PingFrameFlags.NONE); pingFrame.PayloadLength = length; - return SendAsync(pingFrame.Raw); + Http2FrameWriter.WriteHeader(pingFrame, outputWriter); + return SendAsync(new byte[length]); } protected Task SendPingWithInvalidStreamIdAsync(int streamId) { Assert.NotEqual(0, streamId); - var pingFrame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var outputWriter = _pair.Application.Output; + var pingFrame = new Http2Frame(); pingFrame.PreparePing(Http2PingFrameFlags.NONE); pingFrame.StreamId = streamId; - return SendAsync(pingFrame.Raw); + Http2FrameWriter.WriteHeader(pingFrame, outputWriter); + return SendAsync(new byte[pingFrame.PayloadLength]); } + /* https://tools.ietf.org/html/rfc7540#section-6.3 + +-+-------------------------------------------------------------+ + |E| Stream Dependency (31) | + +-+-------------+-----------------------------------------------+ + | Weight (8) | + +-+-------------+ + */ protected Task SendPriorityAsync(int streamId, int streamDependency = 0) { - var priorityFrame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var outputWriter = _pair.Application.Output; + var priorityFrame = new Http2Frame(); priorityFrame.PreparePriority(streamId, streamDependency: streamDependency, exclusive: false, weight: 0); - return SendAsync(priorityFrame.Raw); + + var payload = new byte[priorityFrame.PayloadLength].AsSpan(); + Bitshifter.WriteUInt31BigEndian(payload, (uint)streamDependency); + payload[4] = 0; // Weight + + Http2FrameWriter.WriteHeader(priorityFrame, outputWriter); + return SendAsync(payload); } protected Task SendInvalidPriorityFrameAsync(int streamId, int length) { - var priorityFrame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var outputWriter = _pair.Application.Output; + var priorityFrame = new Http2Frame(); priorityFrame.PreparePriority(streamId, streamDependency: 0, exclusive: false, weight: 0); priorityFrame.PayloadLength = length; - return SendAsync(priorityFrame.Raw); + + Http2FrameWriter.WriteHeader(priorityFrame, outputWriter); + return SendAsync(new byte[length]); } + /* https://tools.ietf.org/html/rfc7540#section-6.4 + +---------------------------------------------------------------+ + | Error Code (32) | + +---------------------------------------------------------------+ + */ protected Task SendRstStreamAsync(int streamId) { - var rstStreamFrame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var outputWriter = _pair.Application.Output; + var rstStreamFrame = new Http2Frame(); rstStreamFrame.PrepareRstStream(streamId, Http2ErrorCode.CANCEL); - return SendAsync(rstStreamFrame.Raw); + var payload = new byte[rstStreamFrame.PayloadLength]; + BinaryPrimitives.WriteUInt32BigEndian(payload, (uint)Http2ErrorCode.CANCEL); + + Http2FrameWriter.WriteHeader(rstStreamFrame, outputWriter); + return SendAsync(payload); } protected Task SendInvalidRstStreamFrameAsync(int streamId, int length) { - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var outputWriter = _pair.Application.Output; + var frame = new Http2Frame(); frame.PrepareRstStream(streamId, Http2ErrorCode.CANCEL); frame.PayloadLength = length; - return SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, outputWriter); + return SendAsync(new byte[length]); } protected Task SendGoAwayAsync() { - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var outputWriter = _pair.Application.Output; + var frame = new Http2Frame(); frame.PrepareGoAway(0, Http2ErrorCode.NO_ERROR); - return SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, outputWriter); + return SendAsync(new byte[frame.PayloadLength]); } protected Task SendInvalidGoAwayFrameAsync() { - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var outputWriter = _pair.Application.Output; + var frame = new Http2Frame(); frame.PrepareGoAway(0, Http2ErrorCode.NO_ERROR); frame.StreamId = 1; - return SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, outputWriter); + return SendAsync(new byte[frame.PayloadLength]); } protected Task SendWindowUpdateAsync(int streamId, int sizeIncrement) { - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var outputWriter = _pair.Application.Output; + var frame = new Http2Frame(); frame.PrepareWindowUpdate(streamId, sizeIncrement); - return SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, outputWriter); + var buffer = outputWriter.GetSpan(4); + Bitshifter.WriteUInt31BigEndian(buffer, (uint)sizeIncrement); + outputWriter.Advance(4); + return FlushAsync(outputWriter); } protected Task SendInvalidWindowUpdateAsync(int streamId, int sizeIncrement, int length) { - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var outputWriter = _pair.Application.Output; + var frame = new Http2Frame(); frame.PrepareWindowUpdate(streamId, sizeIncrement); frame.PayloadLength = length; - return SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, outputWriter); + return SendAsync(new byte[length]); } protected Task SendUnknownFrameTypeAsync(int streamId, int frameType) { - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var outputWriter = _pair.Application.Output; + var frame = new Http2Frame(); frame.StreamId = streamId; frame.Type = (Http2FrameType)frameType; frame.PayloadLength = 0; - return SendAsync(frame.Raw); + Http2FrameWriter.WriteHeader(frame, outputWriter); + return FlushAsync(outputWriter); } - protected async Task ReceiveFrameAsync(uint maxFrameSize = Http2PeerSettings.DefaultMaxFrameSize) + protected async Task ReceiveFrameAsync(uint maxFrameSize = Http2PeerSettings.DefaultMaxFrameSize) { - var frame = new Http2Frame(maxFrameSize); + var frame = new Http2FrameWithPayload(); while (true) { var result = await _pair.Application.Input.ReadAsync().AsTask().DefaultTimeout(); var buffer = result.Buffer; var consumed = buffer.Start; - var examined = buffer.End; + var examined = buffer.Start; try { Assert.True(buffer.Length > 0); - if (Http2FrameReader.ReadFrame(buffer, frame, maxFrameSize, out consumed, out examined)) + if (Http2FrameReader.ReadFrame(buffer, frame, maxFrameSize, out var framePayload)) { + consumed = examined = framePayload.End; + frame.Payload = framePayload.ToArray(); return frame; } + else + { + examined = buffer.End; + } if (result.IsCompleted) { @@ -781,7 +958,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests } } - protected async Task ExpectAsync(Http2FrameType type, int withLength, byte withFlags, int withStreamId) + protected async Task ExpectAsync(Http2FrameType type, int withLength, byte withFlags, int withStreamId) { var frame = await ReceiveFrameAsync((uint)withLength); @@ -864,5 +1041,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.Equal(header.Value, value, ignoreCase: true); } } + + public class Http2FrameWithPayload : Http2Frame + { + public Http2FrameWithPayload() : base() + { + } + + // This does not contain extended headers + public Memory Payload { get; set; } + + public ReadOnlySequence PayloadSequence => new ReadOnlySequence(Payload); + } } } diff --git a/test/Kestrel.InMemory.FunctionalTests/Http2/TlsTests.cs b/test/Kestrel.InMemory.FunctionalTests/Http2/TlsTests.cs index 1181b8030a..d7d57f567c 100644 --- a/test/Kestrel.InMemory.FunctionalTests/Http2/TlsTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/Http2/TlsTests.cs @@ -92,21 +92,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.Http2 private async Task ReceiveFrameAsync(PipeReader reader) { - var frame = new Http2Frame(Http2PeerSettings.MinAllowedMaxFrameSize); + var frame = new Http2Frame(); while (true) { var result = await reader.ReadAsync(); var buffer = result.Buffer; var consumed = buffer.Start; - var examined = buffer.End; + var examined = buffer.Start; try { - if (Http2FrameReader.ReadFrame(buffer, frame, 16_384, out consumed, out examined)) + if (Http2FrameReader.ReadFrame(buffer, frame, 16_384, out var framePayload)) { + consumed = examined = framePayload.End; return frame; } + else + { + examined = buffer.End; + } if (result.IsCompleted) {