Add configurability for max header field size in HPACK

This commit is contained in:
John Luo 2018-09-21 15:59:29 -07:00
parent a142c01a1c
commit ceaa3c86fc
6 changed files with 86 additions and 27 deletions

View File

@ -14,11 +14,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
private int _maxStreamsPerConnection = 100; private int _maxStreamsPerConnection = 100;
private int _headerTableSize = (int)Http2PeerSettings.DefaultHeaderTableSize; private int _headerTableSize = (int)Http2PeerSettings.DefaultHeaderTableSize;
private int _maxFrameSize = (int)Http2PeerSettings.DefaultMaxFrameSize; private int _maxFrameSize = (int)Http2PeerSettings.DefaultMaxFrameSize;
private int _maxRequestHeaderFieldSize = 8192;
/// <summary> /// <summary>
/// Limits the number of concurrent request streams per HTTP/2 connection. Excess streams will be refused. /// Limits the number of concurrent request streams per HTTP/2 connection. Excess streams will be refused.
/// <para> /// <para>
/// Defaults to 100 /// Value must be greater than 0, defaults to 100
/// </para> /// </para>
/// </summary> /// </summary>
public int MaxStreamsPerConnection public int MaxStreamsPerConnection
@ -74,5 +75,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
_maxFrameSize = value; _maxFrameSize = value;
} }
} }
/// <summary>
/// Indicates the size of the maximum allowed size of a request header field sequence. This limit applies to both name and value sequences in their compressed and uncompressed representations.
/// <para>
/// Value must be greater than 0, defaults to 8192
/// </para>
/// </summary>
public int MaxRequestHeaderFieldSize
{
get => _maxRequestHeaderFieldSize;
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), value, CoreStrings.GreaterThanZeroRequired);
}
_maxRequestHeaderFieldSize = value;
}
}
} }
} }

View File

@ -23,9 +23,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
DynamicTableSizeUpdate DynamicTableSizeUpdate
} }
// TODO: add new configurable limit
public const int MaxStringOctets = 4096;
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.1 // http://httpwg.org/specs/rfc7541.html#rfc.section.6.1
// 0 1 2 3 4 5 6 7 // 0 1 2 3 4 5 6 7
// +---+---+---+---+---+---+---+---+ // +---+---+---+---+---+---+---+---+
@ -83,9 +80,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
private readonly int _maxDynamicTableSize; private readonly int _maxDynamicTableSize;
private readonly DynamicTable _dynamicTable; private readonly DynamicTable _dynamicTable;
private readonly IntegerDecoder _integerDecoder = new IntegerDecoder(); private readonly IntegerDecoder _integerDecoder = new IntegerDecoder();
private readonly byte[] _stringOctets = new byte[MaxStringOctets]; private readonly byte[] _stringOctets;
private readonly byte[] _headerNameOctets = new byte[MaxStringOctets]; private readonly byte[] _headerNameOctets;
private readonly byte[] _headerValueOctets = new byte[MaxStringOctets]; private readonly byte[] _headerValueOctets;
private State _state = State.Ready; private State _state = State.Ready;
private byte[] _headerName; private byte[] _headerName;
@ -97,17 +94,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
private bool _huffman; private bool _huffman;
private bool _headersObserved; private bool _headersObserved;
public HPackDecoder(int maxDynamicTableSize) public HPackDecoder(int maxDynamicTableSize, int maxRequestHeaderFieldSize)
: this(maxDynamicTableSize, new DynamicTable(maxDynamicTableSize)) : this(maxDynamicTableSize, maxRequestHeaderFieldSize, new DynamicTable(maxDynamicTableSize)) { }
{
_maxDynamicTableSize = maxDynamicTableSize;
}
// For testing. // For testing.
internal HPackDecoder(int maxDynamicTableSize, DynamicTable dynamicTable) internal HPackDecoder(int maxDynamicTableSize, int maxRequestHeaderFieldSize, DynamicTable dynamicTable)
{ {
_maxDynamicTableSize = maxDynamicTableSize; _maxDynamicTableSize = maxDynamicTableSize;
_dynamicTable = dynamicTable; _dynamicTable = dynamicTable;
_stringOctets = new byte[maxRequestHeaderFieldSize];
_headerNameOctets = new byte[maxRequestHeaderFieldSize];
_headerValueOctets = new byte[maxRequestHeaderFieldSize];
} }
public void Decode(ReadOnlySequence<byte> data, bool endHeaders, IHttpHeadersHandler handler) public void Decode(ReadOnlySequence<byte> data, bool endHeaders, IHttpHeadersHandler handler)

View File

@ -86,13 +86,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
public Http2Connection(Http2ConnectionContext context) public Http2Connection(Http2ConnectionContext context)
{ {
var httpLimits = context.ServiceContext.ServerOptions.Limits;
var http2Limits = httpLimits.Http2;
_context = context; _context = context;
_frameWriter = new Http2FrameWriter(context.Transport.Output, context.ConnectionContext, _outputFlowControl, this, context.ConnectionId, context.ServiceContext.Log); _frameWriter = new Http2FrameWriter(context.Transport.Output, context.ConnectionContext, _outputFlowControl, this, context.ConnectionId, context.ServiceContext.Log);
_serverSettings.MaxConcurrentStreams = (uint)context.ServiceContext.ServerOptions.Limits.Http2.MaxStreamsPerConnection; _serverSettings.MaxConcurrentStreams = (uint)http2Limits.MaxStreamsPerConnection;
_serverSettings.MaxFrameSize = (uint)context.ServiceContext.ServerOptions.Limits.Http2.MaxFrameSize; _serverSettings.MaxFrameSize = (uint)http2Limits.MaxFrameSize;
_serverSettings.HeaderTableSize = (uint)context.ServiceContext.ServerOptions.Limits.Http2.HeaderTableSize; _serverSettings.HeaderTableSize = (uint)http2Limits.HeaderTableSize;
_hpackDecoder = new HPackDecoder((int)_serverSettings.HeaderTableSize); _hpackDecoder = new HPackDecoder(http2Limits.HeaderTableSize, http2Limits.MaxRequestHeaderFieldSize);
_serverSettings.MaxHeaderListSize = (uint)context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize; _serverSettings.MaxHeaderListSize = (uint)httpLimits.MaxRequestHeadersTotalSize;
} }
public string ConnectionId => _context.ConnectionId; public string ConnectionId => _context.ConnectionId;

View File

@ -17,6 +17,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
public class HPackDecoderTests : IHttpHeadersHandler public class HPackDecoderTests : IHttpHeadersHandler
{ {
private const int DynamicTableInitialMaxSize = 4096; private const int DynamicTableInitialMaxSize = 4096;
private const int MaxRequestHeaderFieldSize = 8192;
// Indexed Header Field Representation - Static Table - Index 2 (:method: GET) // Indexed Header Field Representation - Static Table - Index 2 (:method: GET)
private static readonly byte[] _indexedHeaderStatic = new byte[] { 0x82 }; private static readonly byte[] _indexedHeaderStatic = new byte[] { 0x82 };
@ -94,7 +95,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
public HPackDecoderTests() public HPackDecoderTests()
{ {
_dynamicTable = new DynamicTable(DynamicTableInitialMaxSize); _dynamicTable = new DynamicTable(DynamicTableInitialMaxSize);
_decoder = new HPackDecoder(DynamicTableInitialMaxSize, _dynamicTable); _decoder = new HPackDecoder(DynamicTableInitialMaxSize, MaxRequestHeaderFieldSize, _dynamicTable);
} }
void IHttpHeadersHandler.OnHeader(Span<byte> name, Span<byte> value) void IHttpHeadersHandler.OnHeader(Span<byte> name, Span<byte> value)
@ -438,14 +439,32 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
public void DecodesStringLength_GreaterThanLimit_Error() public void DecodesStringLength_GreaterThanLimit_Error()
{ {
var encoded = _literalHeaderFieldWithoutIndexingNewName var encoded = _literalHeaderFieldWithoutIndexingNewName
.Concat(new byte[] { 0xff, 0x82, 0x1f }) // 4097 encoded with 7-bit prefix .Concat(new byte[] { 0xff, 0x82, 0x3f }) // 8193 encoded with 7-bit prefix
.ToArray(); .ToArray();
var exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(new ReadOnlySequence<byte>(encoded), endHeaders: true, handler: this)); var exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(new ReadOnlySequence<byte>(encoded), endHeaders: true, handler: this));
Assert.Equal(CoreStrings.FormatHPackStringLengthTooLarge(4097, HPackDecoder.MaxStringOctets), exception.Message); Assert.Equal(CoreStrings.FormatHPackStringLengthTooLarge(MaxRequestHeaderFieldSize + 1, MaxRequestHeaderFieldSize), exception.Message);
Assert.Empty(_decodedHeaders); Assert.Empty(_decodedHeaders);
} }
[Fact]
public void DecodesStringLength_LimitConfigurable()
{
var decoder = new HPackDecoder(DynamicTableInitialMaxSize, MaxRequestHeaderFieldSize + 1);
var string8193 = new string('a', MaxRequestHeaderFieldSize + 1);
var encoded = _literalHeaderFieldWithoutIndexingNewName
.Concat(new byte[] { 0x7f, 0x82, 0x3f }) // 8193 encoded with 7-bit prefix, no Huffman encoding
.Concat(Encoding.ASCII.GetBytes(string8193))
.Concat(new byte[] { 0x7f, 0x82, 0x3f }) // 8193 encoded with 7-bit prefix, no Huffman encoding
.Concat(Encoding.ASCII.GetBytes(string8193))
.ToArray();
decoder.Decode(new ReadOnlySequence<byte>(encoded), endHeaders: true, handler: this);
Assert.Equal(string8193, _decodedHeaders[string8193]);
}
public static readonly TheoryData<byte[]> _incompleteHeaderBlockData = new TheoryData<byte[]> public static readonly TheoryData<byte[]> _incompleteHeaderBlockData = new TheoryData<byte[]>
{ {
// Indexed Header Field Representation - incomplete index encoding // Indexed Header Field Representation - incomplete index encoding

View File

@ -315,7 +315,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
} }
[Theory] [Theory]
[InlineData(1 << 14 - 1)] [InlineData((1 << 14) - 1)]
[InlineData(1 << 24)] [InlineData(1 << 24)]
[InlineData(-1)] [InlineData(-1)]
public void Http2MaxFrameSizeInvalid(int value) public void Http2MaxFrameSizeInvalid(int value)
@ -331,12 +331,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
} }
[Theory] [Theory]
[InlineData(4097)] [InlineData(int.MinValue)]
[InlineData(-1)] [InlineData(-1)]
[InlineData(0)]
public void Http2HeaderTableSizeInvalid(int value) public void Http2HeaderTableSizeInvalid(int value)
{ {
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new KestrelServerLimits().Http2.MaxFrameSize = value); var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new KestrelServerLimits().Http2.HeaderTableSize = value);
Assert.Contains("A value between", ex.Message); Assert.StartsWith(CoreStrings.GreaterThanZeroRequired, ex.Message);
}
[Fact]
public void Http2MaxRequestHeaderFieldSizeDefault()
{
Assert.Equal(8192, new KestrelServerLimits().Http2.MaxRequestHeaderFieldSize);
}
[Theory]
[InlineData(int.MinValue)]
[InlineData(-1)]
[InlineData(0)]
public void Http2MaxRequestHeaderFieldSizeInvalid(int value)
{
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new KestrelServerLimits().Http2.MaxRequestHeaderFieldSize = value);
Assert.StartsWith(CoreStrings.GreaterThanZeroRequired, ex.Message);
} }
public static TheoryData<TimeSpan> TimeoutValidData => new TheoryData<TimeSpan> public static TheoryData<TimeSpan> TimeoutValidData => new TheoryData<TimeSpan>

View File

@ -31,7 +31,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{ {
public class Http2TestBase : TestApplicationErrorLoggerLoggedTest, IDisposable, IHttpHeadersHandler public class Http2TestBase : TestApplicationErrorLoggerLoggedTest, IDisposable, IHttpHeadersHandler
{ {
protected static readonly string _4kHeaderValue = new string('a', HPackDecoder.MaxStringOctets); protected static readonly int MaxRequestHeaderFieldSize = 8192;
protected static readonly string _4kHeaderValue = new string('a', 4096);
protected static readonly IEnumerable<KeyValuePair<string, string>> _browserRequestHeaders = new[] protected static readonly IEnumerable<KeyValuePair<string, string>> _browserRequestHeaders = new[]
{ {
@ -99,7 +100,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
); );
_pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions); _pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions);
_hpackDecoder = new HPackDecoder((int)_clientSettings.HeaderTableSize); _hpackDecoder = new HPackDecoder((int)_clientSettings.HeaderTableSize, MaxRequestHeaderFieldSize);
_noopApplication = context => Task.CompletedTask; _noopApplication = context => Task.CompletedTask;