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
|
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");
|
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 Http2ConnectionContext _context;
|
||||||
private readonly Http2FrameWriter _frameWriter;
|
private readonly Http2FrameWriter _frameWriter;
|
||||||
private readonly HPackDecoder _hpackDecoder;
|
private readonly HPackDecoder _hpackDecoder;
|
||||||
|
|
@ -30,6 +63,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
private readonly Http2Frame _incomingFrame = new Http2Frame();
|
private readonly Http2Frame _incomingFrame = new Http2Frame();
|
||||||
|
|
||||||
private Http2Stream _currentHeadersStream;
|
private Http2Stream _currentHeadersStream;
|
||||||
|
private RequestHeaderParsingState _requestHeaderParsingState;
|
||||||
|
private PseudoHeaderFields _parsedPseudoHeaderFields;
|
||||||
|
private bool _isMethodConnect;
|
||||||
private int _highestOpenedStreamId;
|
private int _highestOpenedStreamId;
|
||||||
|
|
||||||
private bool _stopping;
|
private bool _stopping;
|
||||||
|
|
@ -318,6 +354,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
{
|
{
|
||||||
throw new Http2ConnectionErrorException(Http2ErrorCode.STREAM_CLOSED);
|
throw new Http2ConnectionErrorException(Http2ErrorCode.STREAM_CLOSED);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: trailers
|
// TODO: trailers
|
||||||
}
|
}
|
||||||
else if (_incomingFrame.StreamId <= _highestOpenedStreamId)
|
else if (_incomingFrame.StreamId <= _highestOpenedStreamId)
|
||||||
|
|
@ -354,17 +391,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
|
|
||||||
_currentHeadersStream.Reset();
|
_currentHeadersStream.Reset();
|
||||||
|
|
||||||
_streams[_incomingFrame.StreamId] = _currentHeadersStream;
|
|
||||||
|
|
||||||
var endHeaders = (_incomingFrame.HeadersFlags & Http2HeadersFrameFlags.END_HEADERS) == Http2HeadersFrameFlags.END_HEADERS;
|
var endHeaders = (_incomingFrame.HeadersFlags & Http2HeadersFrameFlags.END_HEADERS) == Http2HeadersFrameFlags.END_HEADERS;
|
||||||
_hpackDecoder.Decode(_incomingFrame.HeadersPayload, endHeaders, handler: this);
|
await DecodeHeadersAsync(endHeaders, _incomingFrame.HeadersPayload);
|
||||||
|
|
||||||
if (endHeaders)
|
|
||||||
{
|
|
||||||
_highestOpenedStreamId = _incomingFrame.StreamId;
|
|
||||||
_ = _currentHeadersStream.ProcessRequestsAsync();
|
|
||||||
_currentHeadersStream = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -533,16 +561,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
}
|
}
|
||||||
|
|
||||||
var endHeaders = (_incomingFrame.ContinuationFlags & Http2ContinuationFrameFlags.END_HEADERS) == Http2ContinuationFrameFlags.END_HEADERS;
|
var endHeaders = (_incomingFrame.ContinuationFlags & Http2ContinuationFrameFlags.END_HEADERS) == Http2ContinuationFrameFlags.END_HEADERS;
|
||||||
_hpackDecoder.Decode(_incomingFrame.HeadersPayload, endHeaders, handler: this);
|
|
||||||
|
|
||||||
if (endHeaders)
|
return DecodeHeadersAsync(endHeaders, _incomingFrame.Payload);
|
||||||
{
|
|
||||||
_highestOpenedStreamId = _currentHeadersStream.StreamId;
|
|
||||||
_ = _currentHeadersStream.ProcessRequestsAsync();
|
|
||||||
_currentHeadersStream = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task ProcessUnknownFrameAsync()
|
private Task ProcessUnknownFrameAsync()
|
||||||
|
|
@ -555,6 +575,54 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
return Task.CompletedTask;
|
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()
|
private void ThrowIfIncomingFrameSentToIdleStream()
|
||||||
{
|
{
|
||||||
// http://httpwg.org/specs/rfc7540.html#rfc.section.5.1
|
// 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)
|
public void OnHeader(Span<byte> name, Span<byte> value)
|
||||||
{
|
{
|
||||||
|
ValidateHeader(name, value);
|
||||||
_currentHeadersStream.OnHeader(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)
|
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.Http2.HPack;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.AspNetCore.Testing;
|
||||||
using Microsoft.Extensions.Primitives;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
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>(":method", "POST"),
|
||||||
new KeyValuePair<string, string>(":path", "/"),
|
new KeyValuePair<string, string>(":path", "/"),
|
||||||
new KeyValuePair<string, string>(":authority", "127.0.0.1"),
|
new KeyValuePair<string, string>(":scheme", "http"),
|
||||||
new KeyValuePair<string, string>(":scheme", "https"),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly IEnumerable<KeyValuePair<string, string>> _browserRequestHeaders = new[]
|
private static readonly IEnumerable<KeyValuePair<string, string>> _browserRequestHeaders = new[]
|
||||||
{
|
{
|
||||||
new KeyValuePair<string, string>(":method", "GET"),
|
new KeyValuePair<string, string>(":method", "GET"),
|
||||||
new KeyValuePair<string, string>(":path", "/"),
|
new KeyValuePair<string, string>(":path", "/"),
|
||||||
new KeyValuePair<string, string>(":authority", "127.0.0.1"),
|
new KeyValuePair<string, string>(":scheme", "http"),
|
||||||
new KeyValuePair<string, string>(":scheme", "https"),
|
|
||||||
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>("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", "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"),
|
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>(":method", "GET"),
|
||||||
new KeyValuePair<string, string>(":path", "/"),
|
new KeyValuePair<string, string>(":path", "/"),
|
||||||
new KeyValuePair<string, string>(":authority", "127.0.0.1"),
|
new KeyValuePair<string, string>(":scheme", "http"),
|
||||||
new KeyValuePair<string, string>(":scheme", "https"),
|
|
||||||
new KeyValuePair<string, string>("a", _largeHeaderValue),
|
new KeyValuePair<string, string>("a", _largeHeaderValue),
|
||||||
new KeyValuePair<string, string>("b", _largeHeaderValue),
|
new KeyValuePair<string, string>("b", _largeHeaderValue),
|
||||||
new KeyValuePair<string, string>("c", _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>(":method", "GET"),
|
||||||
new KeyValuePair<string, string>(":path", "/"),
|
new KeyValuePair<string, string>(":path", "/"),
|
||||||
new KeyValuePair<string, string>(":authority", "127.0.0.1"),
|
new KeyValuePair<string, string>(":scheme", "http"),
|
||||||
new KeyValuePair<string, string>(":scheme", "https"),
|
|
||||||
new KeyValuePair<string, string>("a", _largeHeaderValue),
|
new KeyValuePair<string, string>("a", _largeHeaderValue),
|
||||||
new KeyValuePair<string, string>("b", _largeHeaderValue),
|
new KeyValuePair<string, string>("b", _largeHeaderValue),
|
||||||
new KeyValuePair<string, string>("c", _largeHeaderValue),
|
new KeyValuePair<string, string>("c", _largeHeaderValue),
|
||||||
|
|
@ -770,7 +765,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task HEADERS_Received_IncompleteHeaderBlockFragment_ConnectionError()
|
public async Task HEADERS_Received_IncompleteHeaderBlock_ConnectionError()
|
||||||
{
|
{
|
||||||
await InitializeConnectionAsync(_noopApplication);
|
await InitializeConnectionAsync(_noopApplication);
|
||||||
|
|
||||||
|
|
@ -779,6 +774,143 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.COMPRESSION_ERROR, ignoreNonGoAwayFrames: false);
|
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]
|
[Fact]
|
||||||
public async Task PRIORITY_Received_StreamIdZero_ConnectionError()
|
public async Task PRIORITY_Received_StreamIdZero_ConnectionError()
|
||||||
{
|
{
|
||||||
|
|
@ -1220,7 +1352,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CONTINUATION_Received_IncompleteHeaderBlockFragment_ConnectionError()
|
public async Task CONTINUATION_Received_IncompleteHeaderBlock_ConnectionError()
|
||||||
{
|
{
|
||||||
await InitializeConnectionAsync(_noopApplication);
|
await InitializeConnectionAsync(_noopApplication);
|
||||||
|
|
||||||
|
|
@ -1230,6 +1362,43 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.COMPRESSION_ERROR, ignoreNonGoAwayFrames: false);
|
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]
|
[Fact]
|
||||||
public async Task CONTINUATION_Sent_WhenHeadersLargerThanFrameLength()
|
public async Task CONTINUATION_Sent_WhenHeadersLargerThanFrameLength()
|
||||||
{
|
{
|
||||||
|
|
@ -1552,6 +1721,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
return done;
|
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)
|
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.");
|
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;
|
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)
|
private Task SendIncompleteContinuationFrameAsync(int streamId)
|
||||||
{
|
{
|
||||||
var frame = new Http2Frame();
|
var frame = new Http2Frame();
|
||||||
|
|
@ -1859,5 +2049,134 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
Assert.Equal(header.Value, value, ignoreCase: true);
|
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