Merge branch 'release/2.2'

This commit is contained in:
John Luo 2018-09-26 17:19:14 -07:00
commit a83f02d954
8 changed files with 265 additions and 79 deletions

View File

@ -0,0 +1,41 @@
// 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 BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
{
public class IntegerDecoderBenchmark
{
private const int Iterations = 50_000;
private int _prefixLength = 5; // Arbitrary prefix length
private byte _singleByte = 0x1e; // 30
private byte[] _multiByte = new byte[] { 0x1f, 0xe0, 0xff, 0xff, 0xff, 0x03}; // int32.MaxValue
private IntegerDecoder _integerDecoder = new IntegerDecoder();
[Benchmark(Baseline = true, OperationsPerInvoke = Iterations)]
public void DecodeSingleByteInteger()
{
for (var i = 0; i < Iterations; i++)
{
_integerDecoder.BeginTryDecode(_singleByte, _prefixLength, out _);
}
}
[Benchmark(OperationsPerInvoke = Iterations)]
public void DecodeMultiByteInteger()
{
for (var i = 0; i < Iterations; i++)
{
_integerDecoder.BeginTryDecode(_multiByte[0], _prefixLength, out _);
for (var j = 1; j < _multiByte.Length; j++)
{
_integerDecoder.TryDecode(_multiByte[j], out _);
}
}
}
}
}

View File

@ -581,4 +581,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="HPackErrorNotEnoughBuffer" xml:space="preserve"> <data name="HPackErrorNotEnoughBuffer" xml:space="preserve">
<value>The given buffer was too small to encode any headers.</value> <value>The given buffer was too small to encode any headers.</value>
</data> </data>
<data name="HPackErrorIntegerTooBig" xml:space="preserve">
<value>The decoded integer exceeds the maximum value of Int32.MaxValue.</value>
</data>
</root> </root>

View File

@ -132,6 +132,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
private void OnByte(byte b, IHttpHeadersHandler handler) private void OnByte(byte b, IHttpHeadersHandler handler)
{ {
int intResult;
switch (_state) switch (_state)
{ {
case State.Ready: case State.Ready:
@ -140,9 +141,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
_headersObserved = true; _headersObserved = true;
var val = b & ~IndexedHeaderFieldMask; var val = b & ~IndexedHeaderFieldMask;
if (_integerDecoder.BeginDecode((byte)val, IndexedHeaderFieldPrefix)) if (_integerDecoder.BeginTryDecode((byte)val, IndexedHeaderFieldPrefix, out intResult))
{ {
OnIndexedHeaderField(_integerDecoder.Value, handler); OnIndexedHeaderField(intResult, handler);
} }
else else
{ {
@ -159,9 +160,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
{ {
_state = State.HeaderNameLength; _state = State.HeaderNameLength;
} }
else if (_integerDecoder.BeginDecode((byte)val, LiteralHeaderFieldWithIncrementalIndexingPrefix)) else if (_integerDecoder.BeginTryDecode((byte)val, LiteralHeaderFieldWithIncrementalIndexingPrefix, out intResult))
{ {
OnIndexedHeaderName(_integerDecoder.Value); OnIndexedHeaderName(intResult);
} }
else else
{ {
@ -178,9 +179,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
{ {
_state = State.HeaderNameLength; _state = State.HeaderNameLength;
} }
else if (_integerDecoder.BeginDecode((byte)val, LiteralHeaderFieldWithoutIndexingPrefix)) else if (_integerDecoder.BeginTryDecode((byte)val, LiteralHeaderFieldWithoutIndexingPrefix, out intResult))
{ {
OnIndexedHeaderName(_integerDecoder.Value); OnIndexedHeaderName(intResult);
} }
else else
{ {
@ -197,9 +198,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
{ {
_state = State.HeaderNameLength; _state = State.HeaderNameLength;
} }
else if (_integerDecoder.BeginDecode((byte)val, LiteralHeaderFieldNeverIndexedPrefix)) else if (_integerDecoder.BeginTryDecode((byte)val, LiteralHeaderFieldNeverIndexedPrefix, out intResult))
{ {
OnIndexedHeaderName(_integerDecoder.Value); OnIndexedHeaderName(intResult);
} }
else else
{ {
@ -217,9 +218,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
throw new HPackDecodingException(CoreStrings.HPackErrorDynamicTableSizeUpdateNotAtBeginningOfHeaderBlock); throw new HPackDecodingException(CoreStrings.HPackErrorDynamicTableSizeUpdateNotAtBeginningOfHeaderBlock);
} }
if (_integerDecoder.BeginDecode((byte)(b & ~DynamicTableSizeUpdateMask), DynamicTableSizeUpdatePrefix)) if (_integerDecoder.BeginTryDecode((byte)(b & ~DynamicTableSizeUpdateMask), DynamicTableSizeUpdatePrefix, out intResult))
{ {
SetDynamicHeaderTableSize(_integerDecoder.Value); SetDynamicHeaderTableSize(intResult);
} }
else else
{ {
@ -234,25 +235,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
break; break;
case State.HeaderFieldIndex: case State.HeaderFieldIndex:
if (_integerDecoder.Decode(b)) if (_integerDecoder.TryDecode(b, out intResult))
{ {
OnIndexedHeaderField(_integerDecoder.Value, handler); OnIndexedHeaderField(intResult, handler);
} }
break; break;
case State.HeaderNameIndex: case State.HeaderNameIndex:
if (_integerDecoder.Decode(b)) if (_integerDecoder.TryDecode(b, out intResult))
{ {
OnIndexedHeaderName(_integerDecoder.Value); OnIndexedHeaderName(intResult);
} }
break; break;
case State.HeaderNameLength: case State.HeaderNameLength:
_huffman = (b & HuffmanMask) != 0; _huffman = (b & HuffmanMask) != 0;
if (_integerDecoder.BeginDecode((byte)(b & ~HuffmanMask), StringLengthPrefix)) if (_integerDecoder.BeginTryDecode((byte)(b & ~HuffmanMask), StringLengthPrefix, out intResult))
{ {
OnStringLength(_integerDecoder.Value, nextState: State.HeaderName); OnStringLength(intResult, nextState: State.HeaderName);
} }
else else
{ {
@ -261,9 +262,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
break; break;
case State.HeaderNameLengthContinue: case State.HeaderNameLengthContinue:
if (_integerDecoder.Decode(b)) if (_integerDecoder.TryDecode(b, out intResult))
{ {
OnStringLength(_integerDecoder.Value, nextState: State.HeaderName); OnStringLength(intResult, nextState: State.HeaderName);
} }
break; break;
@ -279,10 +280,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
case State.HeaderValueLength: case State.HeaderValueLength:
_huffman = (b & HuffmanMask) != 0; _huffman = (b & HuffmanMask) != 0;
if (_integerDecoder.BeginDecode((byte)(b & ~HuffmanMask), StringLengthPrefix)) if (_integerDecoder.BeginTryDecode((byte)(b & ~HuffmanMask), StringLengthPrefix, out intResult))
{ {
OnStringLength(_integerDecoder.Value, nextState: State.HeaderValue); OnStringLength(intResult, nextState: State.HeaderValue);
if (_integerDecoder.Value == 0) if (intResult == 0)
{ {
ProcessHeaderValue(handler); ProcessHeaderValue(handler);
} }
@ -294,10 +295,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
break; break;
case State.HeaderValueLengthContinue: case State.HeaderValueLengthContinue:
if (_integerDecoder.Decode(b)) if (_integerDecoder.TryDecode(b, out intResult))
{ {
OnStringLength(_integerDecoder.Value, nextState: State.HeaderValue); OnStringLength(intResult, nextState: State.HeaderValue);
if (_integerDecoder.Value == 0) if (intResult == 0)
{ {
ProcessHeaderValue(handler); ProcessHeaderValue(handler);
} }
@ -314,9 +315,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
break; break;
case State.DynamicTableSizeUpdate: case State.DynamicTableSizeUpdate:
if (_integerDecoder.Decode(b)) if (_integerDecoder.TryDecode(b, out intResult))
{ {
SetDynamicHeaderTableSize(_integerDecoder.Value); SetDynamicHeaderTableSize(intResult);
_state = State.Ready; _state = State.Ready;
} }

View File

@ -3,40 +3,65 @@
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
{ {
/// <summary>
/// The maximum we will decode is Int32.MaxValue, which is also the maximum request header field size.
/// </summary>
public class IntegerDecoder public class IntegerDecoder
{ {
private int _i; private int _i;
private int _m; private int _m;
public int Value { get; private set; } /// <summary>
/// Callers must ensure higher bits above the prefix are cleared before calling this method.
public bool BeginDecode(byte b, int prefixLength) /// </summary>
/// <param name="b"></param>
/// <param name="prefixLength"></param>
/// <param name="result"></param>
/// <returns></returns>
public bool BeginTryDecode(byte b, int prefixLength, out int result)
{ {
if (b < ((1 << prefixLength) - 1)) if (b < ((1 << prefixLength) - 1))
{ {
Value = b; result = b;
return true;
}
else
{
_i = b;
_m = 0;
return false;
}
}
public bool Decode(byte b)
{
_i = _i + (b & 127) * (1 << _m);
_m = _m + 7;
if ((b & 128) != 128)
{
Value = _i;
return true; return true;
} }
_i = b;
_m = 0;
result = 0;
return false; return false;
} }
public bool TryDecode(byte b, out int result)
{
var m = _m; // Enregister
var i = _i + ((b & 0x7f) << m); // Enregister
if ((b & 0x80) == 0)
{
// Int32.MaxValue only needs a maximum of 5 bytes to represent and the last byte cannot have any value set larger than 0x7
if ((m > 21 && b > 0x7) || i < 0)
{
ThrowIntegerTooBigException();
}
result = i;
return true;
}
else if (m > 21)
{
// Int32.MaxValue only needs a maximum of 5 bytes to represent
ThrowIntegerTooBigException();
}
_m = m + 7;
_i = i;
result = 0;
return false;
}
public static void ThrowIntegerTooBigException()
=> throw new HPackDecodingException(CoreStrings.HPackErrorIntegerTooBig);
} }
} }

View File

@ -2170,6 +2170,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
internal static string FormatHPackErrorNotEnoughBuffer() internal static string FormatHPackErrorNotEnoughBuffer()
=> GetString("HPackErrorNotEnoughBuffer"); => GetString("HPackErrorNotEnoughBuffer");
/// <summary>
/// The decoded integer exceeds the maximum value of Int32.MaxValue.
/// </summary>
internal static string HPackErrorIntegerTooBig
{
get => GetString("HPackErrorIntegerTooBig");
}
/// <summary>
/// The decoded integer exceeds the maximum value of Int32.MaxValue.
/// </summary>
internal static string FormatHPackErrorIntegerTooBig()
=> GetString("HPackErrorIntegerTooBig");
private static string GetString(string name, params string[] formatterNames) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -0,0 +1,39 @@
// 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.Linq;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
public class HPackIntegerTests
{
[Fact]
public void IntegerEncoderDecoderRoundtrips()
{
var decoder = new IntegerDecoder();
var range = 1 << 8;
foreach (var i in Enumerable.Range(0, range).Concat(Enumerable.Range(int.MaxValue - range + 1, range)))
{
for (int n = 1; n <= 8; n++)
{
var integerBytes = new byte[6];
Assert.True(IntegerEncoder.Encode(i, n, integerBytes, out var length));
var decodeResult = decoder.BeginTryDecode(integerBytes[0], n, out var intResult);
for (int j = 1; j < length; j++)
{
Assert.False(decodeResult);
decodeResult = decoder.TryDecode(integerBytes[j], out intResult);
}
Assert.True(decodeResult);
Assert.Equal(i, intResult);
}
}
}
}
}

View File

@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
public void IntegerDecode(int i, int prefixLength, byte[] octets) public void IntegerDecode(int i, int prefixLength, byte[] octets)
{ {
var decoder = new IntegerDecoder(); var decoder = new IntegerDecoder();
var result = decoder.BeginDecode(octets[0], prefixLength); var result = decoder.BeginTryDecode(octets[0], prefixLength, out var intResult);
if (octets.Length == 1) if (octets.Length == 1)
{ {
@ -25,13 +25,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
for (; j < octets.Length - 1; j++) for (; j < octets.Length - 1; j++)
{ {
Assert.False(decoder.Decode(octets[j])); Assert.False(decoder.TryDecode(octets[j], out intResult));
} }
Assert.True(decoder.Decode(octets[j])); Assert.True(decoder.TryDecode(octets[j], out intResult));
} }
Assert.Equal(i, decoder.Value); Assert.Equal(i, intResult);
}
[Theory]
[MemberData(nameof(IntegerData_OverMax))]
public void IntegerDecode_Throws_IfMaxExceeded(int prefixLength, byte[] octets)
{
var decoder = new IntegerDecoder();
var result = decoder.BeginTryDecode(octets[0], prefixLength, out var intResult);
for (var j = 1; j < octets.Length - 1; j++)
{
Assert.False(decoder.TryDecode(octets[j], out intResult));
}
Assert.Throws<HPackDecodingException>(() => decoder.TryDecode(octets[octets.Length - 1], out intResult));
} }
public static TheoryData<int, int, byte[]> IntegerData public static TheoryData<int, int, byte[]> IntegerData
@ -43,6 +58,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
data.Add(10, 5, new byte[] { 10 }); data.Add(10, 5, new byte[] { 10 });
data.Add(1337, 5, new byte[] { 0x1f, 0x9a, 0x0a }); data.Add(1337, 5, new byte[] { 0x1f, 0x9a, 0x0a });
data.Add(42, 8, new byte[] { 42 }); data.Add(42, 8, new byte[] { 42 });
data.Add(7, 3, new byte[] { 0x7, 0x0 });
data.Add(int.MaxValue, 1, new byte[] { 0x01, 0xfe, 0xff, 0xff, 0xff, 0x07 });
data.Add(int.MaxValue, 8, new byte[] { 0xff, 0x80, 0xfe, 0xff, 0xff, 0x07 });
return data;
}
}
public static TheoryData<int, byte[]> IntegerData_OverMax
{
get
{
var data = new TheoryData<int, byte[]>();
data.Add(1, new byte[] { 0x01, 0xff, 0xff, 0xff, 0xff, 0x07 }); // Int32.MaxValue + 1
data.Add(1, new byte[] { 0x01, 0xff, 0xff, 0xff, 0xff, 0x08 }); // MSB exceeds maximum
data.Add(1, new byte[] { 0x01, 0xff, 0xff, 0xff, 0xff, 0x80 }); // Undefined since continuation bit set
data.Add(8, new byte[] { 0xff, 0x81, 0xfe, 0xff, 0xff, 0x07 }); // Int32.MaxValue + 1
data.Add(8, new byte[] { 0xff, 0x81, 0xfe, 0xff, 0xff, 0x08 }); // MSB exceeds maximum
data.Add(8, new byte[] { 0xff, 0x81, 0xfe, 0xff, 0xff, 0x80 }); // Undefined since continuation bit set
return data; return data;
} }

View File

@ -1527,6 +1527,34 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
expectedErrorMessage: CoreStrings.HPackErrorIncompleteHeaderBlock); expectedErrorMessage: CoreStrings.HPackErrorIncompleteHeaderBlock);
} }
[Fact]
public async Task HEADERS_Received_IntegerOverLimit_ConnectionError()
{
await InitializeConnectionAsync(_noopApplication);
var outputWriter = _pair.Application.Output;
var frame = new Http2Frame();
frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS, 1);
frame.PayloadLength = 7;
var payload = new byte[]
{
// Set up an incomplete Literal Header Field w/ Incremental Indexing frame,
0x00,
// with an name of size that's greater than int.MaxValue
0x7f, 0x80, 0x80, 0x80, 0x80, 0x7f
};
Http2FrameWriter.WriteHeader(frame, outputWriter);
await SendAsync(payload);
await WaitForConnectionErrorAsync<HPackDecodingException>(
ignoreNonGoAwayFrames: false,
expectedLastStreamId: 1,
expectedErrorCode: Http2ErrorCode.COMPRESSION_ERROR,
expectedErrorMessage: CoreStrings.HPackErrorIntegerTooBig);
}
[Theory] [Theory]
[MemberData(nameof(IllegalTrailerData))] [MemberData(nameof(IllegalTrailerData))]
public async Task HEADERS_Received_WithTrailers_ContainsIllegalTrailer_ConnectionError(byte[] trailers, string expectedErrorMessage) public async Task HEADERS_Received_WithTrailers_ContainsIllegalTrailer_ConnectionError(byte[] trailers, string expectedErrorMessage)