diff --git a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs index b4e54e7b0..50e023196 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs @@ -18,8 +18,41 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { public class Http2Connection : ITimeoutControl, IHttp2StreamLifetimeHandler, IHttpHeadersHandler { + private enum RequestHeaderParsingState + { + Ready, + PseudoHeaderFields, + Headers, + Trailers + } + + [Flags] + private enum PseudoHeaderFields + { + None = 0x0, + Authority = 0x1, + Method = 0x2, + Path = 0x4, + Scheme = 0x8, + Status = 0x10, + Unknown = 0x40000000 + } + public static byte[] ClientPreface { get; } = Encoding.ASCII.GetBytes("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"); + private static readonly PseudoHeaderFields _mandatoryRequestPseudoHeaderFields = + PseudoHeaderFields.Method | PseudoHeaderFields.Path | PseudoHeaderFields.Scheme; + + private static readonly byte[] _authorityBytes = Encoding.ASCII.GetBytes("authority"); + private static readonly byte[] _methodBytes = Encoding.ASCII.GetBytes("method"); + private static readonly byte[] _pathBytes = Encoding.ASCII.GetBytes("path"); + private static readonly byte[] _schemeBytes = Encoding.ASCII.GetBytes("scheme"); + private static readonly byte[] _statusBytes = Encoding.ASCII.GetBytes("status"); + private static readonly byte[] _connectionBytes = Encoding.ASCII.GetBytes("connection"); + private static readonly byte[] _teBytes = Encoding.ASCII.GetBytes("te"); + private static readonly byte[] _trailersBytes = Encoding.ASCII.GetBytes("trailers"); + private static readonly byte[] _connectBytes = Encoding.ASCII.GetBytes("CONNECT"); + private readonly Http2ConnectionContext _context; private readonly Http2FrameWriter _frameWriter; private readonly HPackDecoder _hpackDecoder; @@ -30,6 +63,9 @@ public class Http2Connection : ITimeoutControl, IHttp2StreamLifetimeHandler, IHt private readonly Http2Frame _incomingFrame = new Http2Frame(); private Http2Stream _currentHeadersStream; + private RequestHeaderParsingState _requestHeaderParsingState; + private PseudoHeaderFields _parsedPseudoHeaderFields; + private bool _isMethodConnect; private int _highestOpenedStreamId; private bool _stopping; @@ -318,6 +354,7 @@ private async Task ProcessHeadersFrameAsync(IHttpApplication { throw new Http2ConnectionErrorException(Http2ErrorCode.STREAM_CLOSED); } + // TODO: trailers } else if (_incomingFrame.StreamId <= _highestOpenedStreamId) @@ -354,17 +391,8 @@ private async Task ProcessHeadersFrameAsync(IHttpApplication _currentHeadersStream.Reset(); - _streams[_incomingFrame.StreamId] = _currentHeadersStream; - var endHeaders = (_incomingFrame.HeadersFlags & Http2HeadersFrameFlags.END_HEADERS) == Http2HeadersFrameFlags.END_HEADERS; - _hpackDecoder.Decode(_incomingFrame.HeadersPayload, endHeaders, handler: this); - - if (endHeaders) - { - _highestOpenedStreamId = _incomingFrame.StreamId; - _ = _currentHeadersStream.ProcessRequestsAsync(); - _currentHeadersStream = null; - } + await DecodeHeadersAsync(endHeaders, _incomingFrame.HeadersPayload); } } @@ -533,28 +561,68 @@ private Task ProcessContinuationFrameAsync(IHttpApplication } var endHeaders = (_incomingFrame.ContinuationFlags & Http2ContinuationFrameFlags.END_HEADERS) == Http2ContinuationFrameFlags.END_HEADERS; - _hpackDecoder.Decode(_incomingFrame.HeadersPayload, endHeaders, handler: this); - if (endHeaders) + return DecodeHeadersAsync(endHeaders, _incomingFrame.Payload); + } + + private Task ProcessUnknownFrameAsync() + { + if (_currentHeadersStream != null) { - _highestOpenedStreamId = _currentHeadersStream.StreamId; - _ = _currentHeadersStream.ProcessRequestsAsync(); - _currentHeadersStream = null; + throw new Http2ConnectionErrorException(Http2ErrorCode.PROTOCOL_ERROR); } return Task.CompletedTask; } - private Task ProcessUnknownFrameAsync() + private Task DecodeHeadersAsync(bool endHeaders, Span payload) { - if (_currentHeadersStream != null) + try { - throw new Http2ConnectionErrorException(Http2ErrorCode.PROTOCOL_ERROR); + _hpackDecoder.Decode(payload, endHeaders, handler: this); + + if (endHeaders) + { + StartStream(); + ResetRequestHeaderParsingState(); + } + } + catch (Http2StreamErrorException ex) + { + ResetRequestHeaderParsingState(); + return _frameWriter.WriteRstStreamAsync(ex.StreamId, ex.ErrorCode); } return Task.CompletedTask; } + private void StartStream() + { + if (!_isMethodConnect && (_parsedPseudoHeaderFields & _mandatoryRequestPseudoHeaderFields) != _mandatoryRequestPseudoHeaderFields) + { + // All HTTP/2 requests MUST include exactly one valid value for the :method, :scheme, and :path pseudo-header + // fields, unless it is a CONNECT request (Section 8.3). An HTTP request that omits mandatory pseudo-header + // fields is malformed (Section 8.1.2.6). + throw new Http2StreamErrorException(_currentHeadersStream.StreamId, Http2ErrorCode.PROTOCOL_ERROR); + } + + _streams[_incomingFrame.StreamId] = _currentHeadersStream; + _ = _currentHeadersStream.ProcessRequestsAsync(); + } + + private void ResetRequestHeaderParsingState() + { + if (_requestHeaderParsingState != RequestHeaderParsingState.Trailers) + { + _highestOpenedStreamId = _currentHeadersStream.StreamId; + } + + _currentHeadersStream = null; + _requestHeaderParsingState = RequestHeaderParsingState.Ready; + _parsedPseudoHeaderFields = PseudoHeaderFields.None; + _isMethodConnect = false; + } + private void ThrowIfIncomingFrameSentToIdleStream() { // http://httpwg.org/specs/rfc7540.html#rfc.section.5.1 @@ -581,9 +649,122 @@ void IHttp2StreamLifetimeHandler.OnStreamCompleted(int streamId) public void OnHeader(Span name, Span value) { + ValidateHeader(name, value); _currentHeadersStream.OnHeader(name, value); } + private void ValidateHeader(Span name, Span value) + { + // http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2.1 + if (IsPseudoHeaderField(name, out var headerField)) + { + if (_requestHeaderParsingState == RequestHeaderParsingState.Headers || + _requestHeaderParsingState == RequestHeaderParsingState.Trailers) + { + // Pseudo-header fields MUST NOT appear in trailers. + // ... + // All pseudo-header fields MUST appear in the header block before regular header fields. + // Any request or response that contains a pseudo-header field that appears in a header + // block after a regular header field MUST be treated as malformed (Section 8.1.2.6). + throw new Http2StreamErrorException(_currentHeadersStream.StreamId, Http2ErrorCode.PROTOCOL_ERROR); + } + + _requestHeaderParsingState = RequestHeaderParsingState.PseudoHeaderFields; + + if (headerField == PseudoHeaderFields.Unknown) + { + // Endpoints MUST treat a request or response that contains undefined or invalid pseudo-header + // fields as malformed (Section 8.1.2.6). + throw new Http2StreamErrorException(_currentHeadersStream.StreamId, Http2ErrorCode.PROTOCOL_ERROR); + } + + if (headerField == PseudoHeaderFields.Status) + { + // Pseudo-header fields defined for requests MUST NOT appear in responses; pseudo-header fields + // defined for responses MUST NOT appear in requests. + throw new Http2StreamErrorException(_currentHeadersStream.StreamId, Http2ErrorCode.PROTOCOL_ERROR); + } + + if ((_parsedPseudoHeaderFields & headerField) == headerField) + { + // http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2.3 + // All HTTP/2 requests MUST include exactly one valid value for the :method, :scheme, and :path pseudo-header fields + throw new Http2StreamErrorException(_currentHeadersStream.StreamId, Http2ErrorCode.PROTOCOL_ERROR); + } + + if (headerField == PseudoHeaderFields.Method) + { + _isMethodConnect = value.SequenceEqual(_connectBytes); + } + + _parsedPseudoHeaderFields |= headerField; + } + else if (_requestHeaderParsingState != RequestHeaderParsingState.Trailers) + { + _requestHeaderParsingState = RequestHeaderParsingState.Headers; + } + + if (IsConnectionSpecificHeaderField(name, value)) + { + throw new Http2StreamErrorException(_currentHeadersStream.StreamId, Http2ErrorCode.PROTOCOL_ERROR); + } + + // http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2 + // A request or response containing uppercase header field names MUST be treated as malformed (Section 8.1.2.6). + for (var i = 0; i < name.Length; i++) + { + if (name[i] >= 65 && name[i] <= 90) + { + throw new Http2StreamErrorException(_currentHeadersStream.StreamId, Http2ErrorCode.PROTOCOL_ERROR); + } + } + } + + private bool IsPseudoHeaderField(Span name, out PseudoHeaderFields headerField) + { + headerField = PseudoHeaderFields.None; + + if (name.IsEmpty || name[0] != (byte)':') + { + return false; + } + + // Skip ':' + name = name.Slice(1); + + if (name.SequenceEqual(_pathBytes)) + { + headerField = PseudoHeaderFields.Path; + } + else if (name.SequenceEqual(_methodBytes)) + { + headerField = PseudoHeaderFields.Method; + } + else if (name.SequenceEqual(_schemeBytes)) + { + headerField = PseudoHeaderFields.Scheme; + } + else if (name.SequenceEqual(_statusBytes)) + { + headerField = PseudoHeaderFields.Status; + } + else if (name.SequenceEqual(_authorityBytes)) + { + headerField = PseudoHeaderFields.Authority; + } + else + { + headerField = PseudoHeaderFields.Unknown; + } + + return true; + } + + private static bool IsConnectionSpecificHeaderField(Span name, Span value) + { + return name.SequenceEqual(_connectionBytes) || (name.SequenceEqual(_teBytes) && !value.SequenceEqual(_trailersBytes)); + } + void ITimeoutControl.SetTimeout(long ticks, TimeoutAction timeoutAction) { } diff --git a/src/Kestrel.Core/Internal/Http2/Http2StreamErrorException.cs b/src/Kestrel.Core/Internal/Http2/Http2StreamErrorException.cs new file mode 100644 index 000000000..d2fcdc6bb --- /dev/null +++ b/src/Kestrel.Core/Internal/Http2/Http2StreamErrorException.cs @@ -0,0 +1,21 @@ +// 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 +{ + public class Http2StreamErrorException : Exception + { + public Http2StreamErrorException(int streamId, Http2ErrorCode errorCode) + : base($"HTTP/2 stream ID {streamId} error: {errorCode}") + { + StreamId = streamId; + ErrorCode = errorCode; + } + + public int StreamId { get; } + + public Http2ErrorCode ErrorCode { get; } + } +} diff --git a/test/Kestrel.Core.Tests/Http2ConnectionTests.cs b/test/Kestrel.Core.Tests/Http2ConnectionTests.cs index 3851a86db..8e7a80aeb 100644 --- a/test/Kestrel.Core.Tests/Http2ConnectionTests.cs +++ b/test/Kestrel.Core.Tests/Http2ConnectionTests.cs @@ -16,7 +16,6 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Testing; -using Microsoft.Extensions.Primitives; using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests @@ -29,16 +28,14 @@ public class Http2ConnectionTests : IDisposable, IHttpHeadersHandler { new KeyValuePair(":method", "POST"), new KeyValuePair(":path", "/"), - new KeyValuePair(":authority", "127.0.0.1"), - new KeyValuePair(":scheme", "https"), + new KeyValuePair(":scheme", "http"), }; private static readonly IEnumerable> _browserRequestHeaders = new[] { new KeyValuePair(":method", "GET"), new KeyValuePair(":path", "/"), - new KeyValuePair(":authority", "127.0.0.1"), - new KeyValuePair(":scheme", "https"), + new KeyValuePair(":scheme", "http"), new KeyValuePair("user-agent", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:54.0) Gecko/20100101 Firefox/54.0"), new KeyValuePair("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"), new KeyValuePair("accept-language", "en-US,en;q=0.5"), @@ -50,8 +47,7 @@ public class Http2ConnectionTests : IDisposable, IHttpHeadersHandler { new KeyValuePair(":method", "GET"), new KeyValuePair(":path", "/"), - new KeyValuePair(":authority", "127.0.0.1"), - new KeyValuePair(":scheme", "https"), + new KeyValuePair(":scheme", "http"), new KeyValuePair("a", _largeHeaderValue), new KeyValuePair("b", _largeHeaderValue), new KeyValuePair("c", _largeHeaderValue), @@ -62,8 +58,7 @@ public class Http2ConnectionTests : IDisposable, IHttpHeadersHandler { new KeyValuePair(":method", "GET"), new KeyValuePair(":path", "/"), - new KeyValuePair(":authority", "127.0.0.1"), - new KeyValuePair(":scheme", "https"), + new KeyValuePair(":scheme", "http"), new KeyValuePair("a", _largeHeaderValue), new KeyValuePair("b", _largeHeaderValue), new KeyValuePair("c", _largeHeaderValue), @@ -770,7 +765,7 @@ public async Task HEADERS_Received_WithPriority_StreamDependencyOnSelf_Connectio } [Fact] - public async Task HEADERS_Received_IncompleteHeaderBlockFragment_ConnectionError() + public async Task HEADERS_Received_IncompleteHeaderBlock_ConnectionError() { await InitializeConnectionAsync(_noopApplication); @@ -779,6 +774,143 @@ public async Task HEADERS_Received_IncompleteHeaderBlockFragment_ConnectionError await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.COMPRESSION_ERROR, ignoreNonGoAwayFrames: false); } + [Theory] + [MemberData(nameof(UpperCaseHeaderNameData))] + public async Task HEADERS_Received_HeaderNameContainsUpperCaseCharacter_StreamError(byte[] headerBlock) + { + await InitializeConnectionAsync(_noopApplication); + + await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS, headerBlock); + await WaitForStreamErrorAsync(1, Http2ErrorCode.PROTOCOL_ERROR, ignoreNonRstStreamFrames: false); + + // Verify that the stream ID can't be re-used + await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS, _browserRequestHeaders); + await WaitForConnectionErrorAsync(expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, ignoreNonGoAwayFrames: false); + } + + [Fact] + public Task HEADERS_Received_HeaderBlockContainsUnknownPseudoHeaderField_StreamError() + { + var headers = new[] + { + new KeyValuePair(":method", "GET"), + new KeyValuePair(":path", "/"), + new KeyValuePair(":scheme", "http"), + new KeyValuePair(":unknown", "0"), + }; + + return HEADERS_Received_InvalidHeaderFields_StreamError(headers); + } + + [Fact] + public Task HEADERS_Received_HeaderBlockContainsResponsePseudoHeaderField_StreamError() + { + var headers = new[] + { + new KeyValuePair(":method", "GET"), + new KeyValuePair(":path", "/"), + new KeyValuePair(":scheme", "http"), + new KeyValuePair(":status", "200"), + }; + + return HEADERS_Received_InvalidHeaderFields_StreamError(headers); + } + + [Theory] + [MemberData(nameof(DuplicatePseudoHeaderFieldData))] + public Task HEADERS_Received_HeaderBlockContainsDuplicatePseudoHeaderField_StreamError(IEnumerable> headers) + { + return HEADERS_Received_InvalidHeaderFields_StreamError(headers); + } + + [Theory] + [MemberData(nameof(MissingPseudoHeaderFieldData))] + public Task HEADERS_Received_HeaderBlockDoesNotContainMandatoryPseudoHeaderField_StreamError(IEnumerable> headers) + { + return HEADERS_Received_InvalidHeaderFields_StreamError(headers); + } + + [Theory] + [MemberData(nameof(ConnectMissingPseudoHeaderFieldData))] + public async Task HEADERS_Received_HeaderBlockDoesNotContainMandatoryPseudoHeaderField_MethodIsCONNECT_NoError(IEnumerable> headers) + { + await InitializeConnectionAsync(_noopApplication); + + await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers); + + await ExpectAsync(Http2FrameType.HEADERS, + withLength: 55, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: 0, + withFlags: (byte)Http2HeadersFrameFlags.END_STREAM, + withStreamId: 1); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + } + + [Theory] + [MemberData(nameof(PseudoHeaderFieldAfterRegularHeadersData))] + public Task HEADERS_Received_HeaderBlockContainsPseudoHeaderFieldAfterRegularHeaders_StreamError(IEnumerable> headers) + { + return HEADERS_Received_InvalidHeaderFields_StreamError(headers); + } + + private async Task HEADERS_Received_InvalidHeaderFields_StreamError(IEnumerable> headers) + { + await InitializeConnectionAsync(_noopApplication); + + await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS, headers); + await WaitForStreamErrorAsync(1, Http2ErrorCode.PROTOCOL_ERROR, ignoreNonRstStreamFrames: false); + + // Verify that the stream ID can't be re-used + await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS, _browserRequestHeaders); + await WaitForConnectionErrorAsync(expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, ignoreNonGoAwayFrames: false); + } + + [Fact] + public Task HEADERS_Received_HeaderBlockContainsConnectionSpecificHeader_StreamError() + { + var headers = new[] + { + new KeyValuePair(":method", "GET"), + new KeyValuePair(":path", "/"), + new KeyValuePair(":scheme", "http"), + new KeyValuePair("connection", "keep-alive") + }; + + return HEADERS_Received_InvalidHeaderFields_StreamError(headers); + } + + [Fact] + public Task HEADERS_Received_HeaderBlockContainsTEHeader_ValueIsNotTrailers_StreamError() + { + var headers = new[] + { + new KeyValuePair(":method", "GET"), + new KeyValuePair(":path", "/"), + new KeyValuePair(":scheme", "http"), + new KeyValuePair("te", "trailers, deflate") + }; + + return HEADERS_Received_InvalidHeaderFields_StreamError(headers); + } + + [Fact] + public Task HEADERS_Received_HeaderBlockContainsTEHeader_ValueIsTrailers_NoError() + { + var headers = new[] + { + new KeyValuePair(":method", "GET"), + new KeyValuePair(":path", "/"), + new KeyValuePair(":scheme", "http"), + new KeyValuePair("te", "trailers, deflate") + }; + + return HEADERS_Received_InvalidHeaderFields_StreamError(headers); + } + [Fact] public async Task PRIORITY_Received_StreamIdZero_ConnectionError() { @@ -1220,7 +1352,7 @@ public async Task CONTINUATION_Received_StreamIdMismatch_ConnectionError() } [Fact] - public async Task CONTINUATION_Received_IncompleteHeaderBlockFragment_ConnectionError() + public async Task CONTINUATION_Received_IncompleteHeaderBlock_ConnectionError() { await InitializeConnectionAsync(_noopApplication); @@ -1230,6 +1362,43 @@ public async Task CONTINUATION_Received_IncompleteHeaderBlockFragment_Connection await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.COMPRESSION_ERROR, ignoreNonGoAwayFrames: false); } + [Theory] + [MemberData(nameof(MissingPseudoHeaderFieldData))] + public async Task CONTINUATION_Received_HeaderBlockDoesNotContainMandatoryPseudoHeaderField_StreamError(IEnumerable> headers) + { + await InitializeConnectionAsync(_noopApplication); + + Assert.True(await SendHeadersAsync(1, Http2HeadersFrameFlags.NONE, headers)); + await SendEmptyContinuationFrameAsync(1, Http2ContinuationFrameFlags.END_HEADERS); + + await WaitForStreamErrorAsync(1, Http2ErrorCode.PROTOCOL_ERROR, ignoreNonRstStreamFrames: false); + + // Verify that the stream ID can't be re-used + await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS, headers); + await WaitForConnectionErrorAsync(expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, ignoreNonGoAwayFrames: false); + } + + [Theory] + [MemberData(nameof(ConnectMissingPseudoHeaderFieldData))] + public async Task CONTINUATION_Received_HeaderBlockDoesNotContainMandatoryPseudoHeaderField_MethodIsCONNECT_NoError(IEnumerable> headers) + { + await InitializeConnectionAsync(_noopApplication); + + await SendHeadersAsync(1, Http2HeadersFrameFlags.END_STREAM, headers); + await SendEmptyContinuationFrameAsync(1, Http2ContinuationFrameFlags.END_HEADERS); + + await ExpectAsync(Http2FrameType.HEADERS, + withLength: 55, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: 0, + withFlags: (byte)Http2HeadersFrameFlags.END_STREAM, + withStreamId: 1); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + } + [Fact] public async Task CONTINUATION_Sent_WhenHeadersLargerThanFrameLength() { @@ -1552,6 +1721,17 @@ private async Task SendHeadersAsync(int streamId, Http2HeadersFrameFlags f return done; } + private Task SendHeadersAsync(int streamId, Http2HeadersFrameFlags flags, byte[] headerBlock) + { + var frame = new Http2Frame(); + + frame.PrepareHeaders(flags, streamId); + frame.Length = headerBlock.Length; + headerBlock.CopyTo(frame.HeadersPayload); + + return SendAsync(frame.Raw); + } + private Task SendInvalidHeadersFrameAsync(int streamId, int frameLength, byte padLength) { Assert.True(padLength >= frameLength, $"{nameof(padLength)} must be greater than or equal to {nameof(frameLength)} to create an invalid frame."); @@ -1596,6 +1776,16 @@ private async Task SendContinuationAsync(int streamId, Http2ContinuationFr return done; } + private Task SendEmptyContinuationFrameAsync(int streamId, Http2ContinuationFrameFlags flags) + { + var frame = new Http2Frame(); + + frame.PrepareContinuation(flags, streamId); + frame.Length = 0; + + return SendAsync(frame.Raw); + } + private Task SendIncompleteContinuationFrameAsync(int streamId) { var frame = new Http2Frame(); @@ -1859,5 +2049,134 @@ private void VerifyDecodedRequestHeaders(IEnumerable UpperCaseHeaderNameData + { + get + { + // We can't use HPackEncoder here because it will convert header names to lowercase + var headerName = "abcdefghijklmnopqrstuvwxyz"; + + var headerBlockStart = new byte[] + { + 0x82, // Indexed Header Field - :method: GET + 0x84, // Indexed Header Field - :path: / + 0x86, // Indexed Header Field - :scheme: http + 0x00, // Literal Header Field without Indexing - New Name + (byte)headerName.Length, // Header name length + }; + + var headerBlockEnd = new byte[] + { + 0x01, // Header value length + 0x30 // "0" + }; + + var data = new TheoryData(); + + for (var i = 0; i < headerName.Length; i++) + { + var bytes = Encoding.ASCII.GetBytes(headerName); + bytes[i] &= 0xdf; + + var headerBlock = headerBlockStart.Concat(bytes).Concat(headerBlockEnd).ToArray(); + data.Add(headerBlock); + } + + return data; + } + } + + public static TheoryData>> DuplicatePseudoHeaderFieldData + { + get + { + var data = new TheoryData>>(); + var requestHeaders = new[] + { + new KeyValuePair(":method", "GET"), + new KeyValuePair(":path", "/"), + new KeyValuePair(":authority", "127.0.0.1"), + new KeyValuePair(":scheme", "http"), + }; + + foreach (var headerField in requestHeaders) + { + var headers = requestHeaders.Concat(new[] { new KeyValuePair(headerField.Key, headerField.Value) }); + data.Add(headers); + } + + return data; + } + } + + public static TheoryData>> MissingPseudoHeaderFieldData + { + get + { + var data = new TheoryData>>(); + var requestHeaders = new[] + { + new KeyValuePair(":method", "GET"), + new KeyValuePair(":path", "/"), + new KeyValuePair(":scheme", "http"), + }; + + foreach (var headerField in requestHeaders) + { + var headers = requestHeaders.Except(new[] { headerField }); + data.Add(headers); + } + + return data; + } + } + + public static TheoryData>> ConnectMissingPseudoHeaderFieldData + { + get + { + var data = new TheoryData>>(); + var methodHeader = new[] { new KeyValuePair(":method", "CONNECT") }; + var requestHeaders = new[] + { + new KeyValuePair(":path", "/"), + new KeyValuePair(":scheme", "http"), + new KeyValuePair(":authority", "127.0.0.1"), + }; + + foreach (var headerField in requestHeaders) + { + var headers = methodHeader.Concat(requestHeaders.Except(new[] { headerField })); + data.Add(headers); + } + + return data; + } + } + + public static TheoryData>> PseudoHeaderFieldAfterRegularHeadersData + { + get + { + var data = new TheoryData>>(); + var requestHeaders = new[] + { + new KeyValuePair(":method", "GET"), + new KeyValuePair(":path", "/"), + new KeyValuePair(":authority", "127.0.0.1"), + new KeyValuePair(":scheme", "http"), + new KeyValuePair("content-length", "0") + }; + + foreach (var headerField in requestHeaders.Where(h => h.Key.StartsWith(":"))) + { + var headers = requestHeaders.Except(new[] { headerField }).Concat(new[] { headerField }); + data.Add(headers); + } + + return data; + } + } } }