HTTP/2: validate request headers prior to starting new stream.
This commit is contained in:
parent
08c6c38667
commit
d46d2ce193
|
|
@ -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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|||
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|||
{
|
||||
throw new Http2ConnectionErrorException(Http2ErrorCode.STREAM_CLOSED);
|
||||
}
|
||||
|
||||
// TODO: trailers
|
||||
}
|
||||
else if (_incomingFrame.StreamId <= _highestOpenedStreamId)
|
||||
|
|
@ -354,17 +391,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|||
|
||||
_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,16 +561,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|||
}
|
||||
|
||||
var endHeaders = (_incomingFrame.ContinuationFlags & Http2ContinuationFrameFlags.END_HEADERS) == Http2ContinuationFrameFlags.END_HEADERS;
|
||||
_hpackDecoder.Decode(_incomingFrame.HeadersPayload, endHeaders, handler: this);
|
||||
|
||||
if (endHeaders)
|
||||
{
|
||||
_highestOpenedStreamId = _currentHeadersStream.StreamId;
|
||||
_ = _currentHeadersStream.ProcessRequestsAsync();
|
||||
_currentHeadersStream = null;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
return DecodeHeadersAsync(endHeaders, _incomingFrame.Payload);
|
||||
}
|
||||
|
||||
private Task ProcessUnknownFrameAsync()
|
||||
|
|
@ -555,6 +575,54 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task DecodeHeadersAsync(bool endHeaders, Span<byte> payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
_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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|||
|
||||
public void OnHeader(Span<byte> name, Span<byte> value)
|
||||
{
|
||||
ValidateHeader(name, value);
|
||||
_currentHeadersStream.OnHeader(name, value);
|
||||
}
|
||||
|
||||
private void ValidateHeader(Span<byte> name, Span<byte> 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<byte> 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<byte> name, Span<byte> value)
|
||||
{
|
||||
return name.SequenceEqual(_connectionBytes) || (name.SequenceEqual(_teBytes) && !value.SequenceEqual(_trailersBytes));
|
||||
}
|
||||
|
||||
void ITimeoutControl.SetTimeout(long ticks, TimeoutAction timeoutAction)
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,6 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
|
|||
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
{
|
||||
new KeyValuePair<string, string>(":method", "POST"),
|
||||
new KeyValuePair<string, string>(":path", "/"),
|
||||
new KeyValuePair<string, string>(":authority", "127.0.0.1"),
|
||||
new KeyValuePair<string, string>(":scheme", "https"),
|
||||
new KeyValuePair<string, string>(":scheme", "http"),
|
||||
};
|
||||
|
||||
private static readonly IEnumerable<KeyValuePair<string, string>> _browserRequestHeaders = new[]
|
||||
{
|
||||
new KeyValuePair<string, string>(":method", "GET"),
|
||||
new KeyValuePair<string, string>(":path", "/"),
|
||||
new KeyValuePair<string, string>(":authority", "127.0.0.1"),
|
||||
new KeyValuePair<string, string>(":scheme", "https"),
|
||||
new KeyValuePair<string, string>(":scheme", "http"),
|
||||
new KeyValuePair<string, string>("user-agent", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:54.0) Gecko/20100101 Firefox/54.0"),
|
||||
new KeyValuePair<string, string>("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"),
|
||||
new KeyValuePair<string, string>("accept-language", "en-US,en;q=0.5"),
|
||||
|
|
@ -50,8 +47,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
{
|
||||
new KeyValuePair<string, string>(":method", "GET"),
|
||||
new KeyValuePair<string, string>(":path", "/"),
|
||||
new KeyValuePair<string, string>(":authority", "127.0.0.1"),
|
||||
new KeyValuePair<string, string>(":scheme", "https"),
|
||||
new KeyValuePair<string, string>(":scheme", "http"),
|
||||
new KeyValuePair<string, string>("a", _largeHeaderValue),
|
||||
new KeyValuePair<string, string>("b", _largeHeaderValue),
|
||||
new KeyValuePair<string, string>("c", _largeHeaderValue),
|
||||
|
|
@ -62,8 +58,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
{
|
||||
new KeyValuePair<string, string>(":method", "GET"),
|
||||
new KeyValuePair<string, string>(":path", "/"),
|
||||
new KeyValuePair<string, string>(":authority", "127.0.0.1"),
|
||||
new KeyValuePair<string, string>(":scheme", "https"),
|
||||
new KeyValuePair<string, string>(":scheme", "http"),
|
||||
new KeyValuePair<string, string>("a", _largeHeaderValue),
|
||||
new KeyValuePair<string, string>("b", _largeHeaderValue),
|
||||
new KeyValuePair<string, string>("c", _largeHeaderValue),
|
||||
|
|
@ -770,7 +765,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HEADERS_Received_IncompleteHeaderBlockFragment_ConnectionError()
|
||||
public async Task HEADERS_Received_IncompleteHeaderBlock_ConnectionError()
|
||||
{
|
||||
await InitializeConnectionAsync(_noopApplication);
|
||||
|
||||
|
|
@ -779,6 +774,143 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
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<string, string>(":method", "GET"),
|
||||
new KeyValuePair<string, string>(":path", "/"),
|
||||
new KeyValuePair<string, string>(":scheme", "http"),
|
||||
new KeyValuePair<string, string>(":unknown", "0"),
|
||||
};
|
||||
|
||||
return HEADERS_Received_InvalidHeaderFields_StreamError(headers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public Task HEADERS_Received_HeaderBlockContainsResponsePseudoHeaderField_StreamError()
|
||||
{
|
||||
var headers = new[]
|
||||
{
|
||||
new KeyValuePair<string, string>(":method", "GET"),
|
||||
new KeyValuePair<string, string>(":path", "/"),
|
||||
new KeyValuePair<string, string>(":scheme", "http"),
|
||||
new KeyValuePair<string, string>(":status", "200"),
|
||||
};
|
||||
|
||||
return HEADERS_Received_InvalidHeaderFields_StreamError(headers);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(DuplicatePseudoHeaderFieldData))]
|
||||
public Task HEADERS_Received_HeaderBlockContainsDuplicatePseudoHeaderField_StreamError(IEnumerable<KeyValuePair<string, string>> headers)
|
||||
{
|
||||
return HEADERS_Received_InvalidHeaderFields_StreamError(headers);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(MissingPseudoHeaderFieldData))]
|
||||
public Task HEADERS_Received_HeaderBlockDoesNotContainMandatoryPseudoHeaderField_StreamError(IEnumerable<KeyValuePair<string, string>> headers)
|
||||
{
|
||||
return HEADERS_Received_InvalidHeaderFields_StreamError(headers);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ConnectMissingPseudoHeaderFieldData))]
|
||||
public async Task HEADERS_Received_HeaderBlockDoesNotContainMandatoryPseudoHeaderField_MethodIsCONNECT_NoError(IEnumerable<KeyValuePair<string, string>> 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<KeyValuePair<string, string>> headers)
|
||||
{
|
||||
return HEADERS_Received_InvalidHeaderFields_StreamError(headers);
|
||||
}
|
||||
|
||||
private async Task HEADERS_Received_InvalidHeaderFields_StreamError(IEnumerable<KeyValuePair<string, string>> 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<string, string>(":method", "GET"),
|
||||
new KeyValuePair<string, string>(":path", "/"),
|
||||
new KeyValuePair<string, string>(":scheme", "http"),
|
||||
new KeyValuePair<string, string>("connection", "keep-alive")
|
||||
};
|
||||
|
||||
return HEADERS_Received_InvalidHeaderFields_StreamError(headers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public Task HEADERS_Received_HeaderBlockContainsTEHeader_ValueIsNotTrailers_StreamError()
|
||||
{
|
||||
var headers = new[]
|
||||
{
|
||||
new KeyValuePair<string, string>(":method", "GET"),
|
||||
new KeyValuePair<string, string>(":path", "/"),
|
||||
new KeyValuePair<string, string>(":scheme", "http"),
|
||||
new KeyValuePair<string, string>("te", "trailers, deflate")
|
||||
};
|
||||
|
||||
return HEADERS_Received_InvalidHeaderFields_StreamError(headers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public Task HEADERS_Received_HeaderBlockContainsTEHeader_ValueIsTrailers_NoError()
|
||||
{
|
||||
var headers = new[]
|
||||
{
|
||||
new KeyValuePair<string, string>(":method", "GET"),
|
||||
new KeyValuePair<string, string>(":path", "/"),
|
||||
new KeyValuePair<string, string>(":scheme", "http"),
|
||||
new KeyValuePair<string, string>("te", "trailers, deflate")
|
||||
};
|
||||
|
||||
return HEADERS_Received_InvalidHeaderFields_StreamError(headers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PRIORITY_Received_StreamIdZero_ConnectionError()
|
||||
{
|
||||
|
|
@ -1220,7 +1352,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CONTINUATION_Received_IncompleteHeaderBlockFragment_ConnectionError()
|
||||
public async Task CONTINUATION_Received_IncompleteHeaderBlock_ConnectionError()
|
||||
{
|
||||
await InitializeConnectionAsync(_noopApplication);
|
||||
|
||||
|
|
@ -1230,6 +1362,43 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.COMPRESSION_ERROR, ignoreNonGoAwayFrames: false);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(MissingPseudoHeaderFieldData))]
|
||||
public async Task CONTINUATION_Received_HeaderBlockDoesNotContainMandatoryPseudoHeaderField_StreamError(IEnumerable<KeyValuePair<string, string>> 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<KeyValuePair<string, string>> 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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
Assert.Equal(header.Value, value, ignoreCase: true);
|
||||
}
|
||||
}
|
||||
|
||||
public static TheoryData<byte[]> 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<byte[]>();
|
||||
|
||||
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<IEnumerable<KeyValuePair<string, string>>> DuplicatePseudoHeaderFieldData
|
||||
{
|
||||
get
|
||||
{
|
||||
var data = new TheoryData<IEnumerable<KeyValuePair<string, string>>>();
|
||||
var requestHeaders = new[]
|
||||
{
|
||||
new KeyValuePair<string, string>(":method", "GET"),
|
||||
new KeyValuePair<string, string>(":path", "/"),
|
||||
new KeyValuePair<string, string>(":authority", "127.0.0.1"),
|
||||
new KeyValuePair<string, string>(":scheme", "http"),
|
||||
};
|
||||
|
||||
foreach (var headerField in requestHeaders)
|
||||
{
|
||||
var headers = requestHeaders.Concat(new[] { new KeyValuePair<string, string>(headerField.Key, headerField.Value) });
|
||||
data.Add(headers);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
public static TheoryData<IEnumerable<KeyValuePair<string, string>>> MissingPseudoHeaderFieldData
|
||||
{
|
||||
get
|
||||
{
|
||||
var data = new TheoryData<IEnumerable<KeyValuePair<string, string>>>();
|
||||
var requestHeaders = new[]
|
||||
{
|
||||
new KeyValuePair<string, string>(":method", "GET"),
|
||||
new KeyValuePair<string, string>(":path", "/"),
|
||||
new KeyValuePair<string, string>(":scheme", "http"),
|
||||
};
|
||||
|
||||
foreach (var headerField in requestHeaders)
|
||||
{
|
||||
var headers = requestHeaders.Except(new[] { headerField });
|
||||
data.Add(headers);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
public static TheoryData<IEnumerable<KeyValuePair<string, string>>> ConnectMissingPseudoHeaderFieldData
|
||||
{
|
||||
get
|
||||
{
|
||||
var data = new TheoryData<IEnumerable<KeyValuePair<string, string>>>();
|
||||
var methodHeader = new[] { new KeyValuePair<string, string>(":method", "CONNECT") };
|
||||
var requestHeaders = new[]
|
||||
{
|
||||
new KeyValuePair<string, string>(":path", "/"),
|
||||
new KeyValuePair<string, string>(":scheme", "http"),
|
||||
new KeyValuePair<string, string>(":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<IEnumerable<KeyValuePair<string, string>>> PseudoHeaderFieldAfterRegularHeadersData
|
||||
{
|
||||
get
|
||||
{
|
||||
var data = new TheoryData<IEnumerable<KeyValuePair<string, string>>>();
|
||||
var requestHeaders = new[]
|
||||
{
|
||||
new KeyValuePair<string, string>(":method", "GET"),
|
||||
new KeyValuePair<string, string>(":path", "/"),
|
||||
new KeyValuePair<string, string>(":authority", "127.0.0.1"),
|
||||
new KeyValuePair<string, string>(":scheme", "http"),
|
||||
new KeyValuePair<string, string>("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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue