HTTP/2: support trailers.

This commit is contained in:
Cesar Blum Silveira 2017-10-12 17:26:20 -07:00 committed by GitHub
parent 66a3c9496a
commit 9dfffd14bb
2 changed files with 269 additions and 8 deletions

View File

@ -358,13 +358,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
// END_STREAM flag set MUST treat that as a connection error (Section 5.4.1)
// of type STREAM_CLOSED, unless the frame is permitted as described below.
//
// (The allowed frame types for this situation are WINDOW_UPDATE, RST_STREAM and PRIORITY)
// (The allowed frame types after END_STREAM are WINDOW_UPDATE, RST_STREAM and PRIORITY)
if (stream.EndStreamReceived)
{
throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamHalfClosedRemote(_incomingFrame.Type, stream.StreamId), Http2ErrorCode.STREAM_CLOSED);
}
// TODO: trailers
// This is the last chance for the client to send END_STREAM
if ((_incomingFrame.HeadersFlags & Http2HeadersFrameFlags.END_STREAM) == 0)
{
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorHeadersWithTrailersNoEndStream, Http2ErrorCode.PROTOCOL_ERROR);
}
// Since we found an active stream, this HEADERS frame contains trailers
_currentHeadersStream = stream;
_requestHeaderParsingState = RequestHeaderParsingState.Trailers;
var endHeaders = (_incomingFrame.HeadersFlags & Http2HeadersFrameFlags.END_HEADERS) == Http2HeadersFrameFlags.END_HEADERS;
await DecodeTrailersAsync(endHeaders, _incomingFrame.HeadersPayload);
}
else if (_incomingFrame.StreamId <= _highestOpenedStreamId)
{
@ -585,7 +596,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
var endHeaders = (_incomingFrame.ContinuationFlags & Http2ContinuationFrameFlags.END_HEADERS) == Http2ContinuationFrameFlags.END_HEADERS;
return DecodeHeadersAsync(endHeaders, _incomingFrame.Payload);
if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers)
{
return DecodeTrailersAsync(endHeaders, _incomingFrame.Payload);
}
else
{
return DecodeHeadersAsync(endHeaders, _incomingFrame.Payload);
}
}
private Task ProcessUnknownFrameAsync()
@ -620,6 +638,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
return Task.CompletedTask;
}
private Task DecodeTrailersAsync(bool endHeaders, Span<byte> payload)
{
_hpackDecoder.Decode(payload, endHeaders, handler: this);
if (endHeaders)
{
var endStreamTask = _currentHeadersStream.OnDataAsync(Constants.EmptyData, endStream: true);
ResetRequestHeaderParsingState();
return endStreamTask;
}
return Task.CompletedTask;
}
private void StartStream()
{
if (!_isMethodConnect && (_parsedPseudoHeaderFields & _mandatoryRequestPseudoHeaderFields) != _mandatoryRequestPseudoHeaderFields)
@ -682,17 +714,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
// 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)
if (_requestHeaderParsingState == RequestHeaderParsingState.Headers)
{
// 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, CoreStrings.Http2ErrorPseudoHeaderFieldAfterRegularHeaders, Http2ErrorCode.PROTOCOL_ERROR);
}
if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers)
{
// Pseudo-header fields MUST NOT appear in trailers.
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorTrailersContainPseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR);
}
_requestHeaderParsingState = RequestHeaderParsingState.PseudoHeaderFields;
if (headerField == PseudoHeaderFields.Unknown)
@ -739,7 +774,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
if (name[i] >= 65 && name[i] <= 90)
{
throw new Http2StreamErrorException(_currentHeadersStream.StreamId, CoreStrings.Http2ErrorHeaderNameUppercase, Http2ErrorCode.PROTOCOL_ERROR);
if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers)
{
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorTrailerNameUppercase, Http2ErrorCode.PROTOCOL_ERROR);
}
else
{
throw new Http2StreamErrorException(_currentHeadersStream.StreamId, CoreStrings.Http2ErrorHeaderNameUppercase, Http2ErrorCode.PROTOCOL_ERROR);
}
}
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipelines;
using System.Linq;
using System.Text;
@ -44,6 +45,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
new KeyValuePair<string, string>("upgrade-insecure-requests", "1"),
};
private static readonly IEnumerable<KeyValuePair<string, string>> _requestTrailers = new[]
{
new KeyValuePair<string, string>("trailer-one", "1"),
new KeyValuePair<string, string>("trailer-two", "2"),
};
private static readonly IEnumerable<KeyValuePair<string, string>> _oneContinuationRequestHeaders = new[]
{
new KeyValuePair<string, string>(":method", "GET"),
@ -93,6 +100,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
private readonly RequestDelegate _noopApplication;
private readonly RequestDelegate _readHeadersApplication;
private readonly RequestDelegate _readTrailersApplication;
private readonly RequestDelegate _bufferingApplication;
private readonly RequestDelegate _echoApplication;
private readonly RequestDelegate _echoWaitForAbortApplication;
@ -118,6 +126,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
return Task.CompletedTask;
};
_readTrailersApplication = async context =>
{
using (var ms = new MemoryStream())
{
// Consuming the entire request body guarantees trailers will be available
await context.Request.Body.CopyToAsync(ms);
}
foreach (var header in context.Request.Headers)
{
_receivedHeaders[header.Key] = header.Value.ToString();
}
};
_bufferingApplication = async context =>
{
var data = new List<byte>();
@ -687,6 +709,52 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task HEADERS_Received_WithTrailers_Decoded(bool sendData)
{
await InitializeConnectionAsync(_readTrailersApplication);
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS, _browserRequestHeaders);
// Initialize another stream with a higher stream ID, and verify that after trailers are
// decoded by the other stream, the highest opened stream ID is not reset to the lower ID
// (the highest opened stream ID is sent by the server in the GOAWAY frame when shutting
// down the connection).
await SendHeadersAsync(3, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, _browserRequestHeaders);
// The second stream should end first, since the first one is waiting for the request body.
await ExpectAsync(Http2FrameType.HEADERS,
withLength: 55,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 3);
await ExpectAsync(Http2FrameType.DATA,
withLength: 0,
withFlags: (byte)Http2DataFrameFlags.END_STREAM,
withStreamId: 3);
if (sendData)
{
await SendDataAsync(1, _helloBytes, endStream: false);
}
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, _requestTrailers);
await ExpectAsync(Http2FrameType.HEADERS,
withLength: 55,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
withLength: 0,
withFlags: (byte)Http2DataFrameFlags.END_STREAM,
withStreamId: 1);
VerifyDecodedRequestHeaders(_browserRequestHeaders.Concat(_requestTrailers));
await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false);
}
[Fact]
public async Task HEADERS_Received_StreamIdZero_ConnectionError()
{
@ -861,6 +929,39 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
expectedErrorMessage: CoreStrings.HPackErrorIncompleteHeaderBlock);
}
[Theory]
[MemberData(nameof(IllegalTrailerData))]
public async Task HEADERS_Received_WithTrailers_ContainsIllegalTrailer_ConnectionError(byte[] trailers, string expectedErrorMessage)
{
await InitializeConnectionAsync(_readTrailersApplication);
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS, _browserRequestHeaders);
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, trailers);
await WaitForConnectionErrorAsync<Http2ConnectionErrorException>(
ignoreNonGoAwayFrames: false,
expectedLastStreamId: 1,
expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR,
expectedErrorMessage: expectedErrorMessage);
}
[Theory]
[InlineData(Http2HeadersFrameFlags.NONE)]
[InlineData(Http2HeadersFrameFlags.END_HEADERS)]
public async Task HEADERS_Received_WithTrailers_EndStreamNotSet_ConnectionError(Http2HeadersFrameFlags flags)
{
await InitializeConnectionAsync(_readTrailersApplication);
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS, _browserRequestHeaders);
await SendHeadersAsync(1, flags, _requestTrailers);
await WaitForConnectionErrorAsync<Http2ConnectionErrorException>(
ignoreNonGoAwayFrames: false,
expectedLastStreamId: 1,
expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR,
expectedErrorMessage: CoreStrings.Http2ErrorHeadersWithTrailersNoEndStream);
}
[Theory]
[MemberData(nameof(UpperCaseHeaderNameData))]
public async Task HEADERS_Received_HeaderNameContainsUpperCaseCharacter_StreamError(byte[] headerBlock)
@ -1562,6 +1663,67 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task CONTINUATION_Received_WithTrailers_Decoded(bool sendData)
{
await InitializeConnectionAsync(_readTrailersApplication);
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS, _browserRequestHeaders);
// Initialize another stream with a higher stream ID, and verify that after trailers are
// decoded by the other stream, the highest opened stream ID is not reset to the lower ID
// (the highest opened stream ID is sent by the server in the GOAWAY frame when shutting
// down the connection).
await SendHeadersAsync(3, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, _browserRequestHeaders);
// The second stream should end first, since the first one is waiting for the request body.
await ExpectAsync(Http2FrameType.HEADERS,
withLength: 55,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 3);
await ExpectAsync(Http2FrameType.DATA,
withLength: 0,
withFlags: (byte)Http2DataFrameFlags.END_STREAM,
withStreamId: 3);
if (sendData)
{
await SendDataAsync(1, _helloBytes, endStream: false);
}
// Trailers encoded as Literal Header Field without Indexing - New Name
// trailer-1: 1
// trailer-2: 2
var trailers = new byte[] { 0x00, 0x09 }
.Concat(Encoding.ASCII.GetBytes("trailer-1"))
.Concat(new byte[] { 0x01, (byte)'1' })
.Concat(new byte[] { 0x00, 0x09 })
.Concat(Encoding.ASCII.GetBytes("trailer-2"))
.Concat(new byte[] { 0x01, (byte)'2' })
.ToArray();
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_STREAM, new byte[0]);
await SendContinuationAsync(1, Http2ContinuationFrameFlags.END_HEADERS, trailers);
await ExpectAsync(Http2FrameType.HEADERS,
withLength: 55,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
withLength: 0,
withFlags: (byte)Http2DataFrameFlags.END_STREAM,
withStreamId: 1);
VerifyDecodedRequestHeaders(_browserRequestHeaders.Concat(new[]
{
new KeyValuePair<string, string>("trailer-1", "1"),
new KeyValuePair<string, string>("trailer-2", "2")
}));
await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false);
}
[Fact]
public async Task CONTINUATION_Received_StreamIdMismatch_ConnectionError()
{
@ -1592,6 +1754,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
expectedErrorMessage: CoreStrings.HPackErrorIncompleteHeaderBlock);
}
[Theory]
[MemberData(nameof(IllegalTrailerData))]
public async Task CONTINUATION_Received_WithTrailers_ContainsIllegalTrailer_ConnectionError(byte[] trailers, string expectedErrorMessage)
{
await InitializeConnectionAsync(_readTrailersApplication);
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS, _browserRequestHeaders);
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_STREAM, new byte[0]);
await SendContinuationAsync(1, Http2ContinuationFrameFlags.END_HEADERS, trailers);
await WaitForConnectionErrorAsync<Http2ConnectionErrorException>(
ignoreNonGoAwayFrames: false,
expectedLastStreamId: 1,
expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR,
expectedErrorMessage: expectedErrorMessage);
}
[Theory]
[MemberData(nameof(MissingPseudoHeaderFieldData))]
public async Task CONTINUATION_Received_HeaderBlockDoesNotContainMandatoryPseudoHeaderField_StreamError(IEnumerable<KeyValuePair<string, string>> headers)
@ -2048,6 +2227,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
return done;
}
private async Task SendContinuationAsync(int streamId, Http2ContinuationFrameFlags flags, byte[] payload)
{
var frame = new Http2Frame();
frame.PrepareContinuation(flags, streamId);
frame.Length = payload.Length;
payload.CopyTo(frame.Payload);
await SendAsync(frame.Raw);
}
private Task SendEmptyContinuationFrameAsync(int streamId, Http2ContinuationFrameFlags flags)
{
var frame = new Http2Frame();
@ -2463,5 +2653,34 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
return data;
}
}
public static TheoryData<byte[], string> IllegalTrailerData
{
get
{
// We can't use HPackEncoder here because it will convert header names to lowercase
var data = new TheoryData<byte[], string>();
// Indexed Header Field - :method: GET
data.Add(new byte[] { 0x82 }, CoreStrings.Http2ErrorTrailersContainPseudoHeaderField);
// Indexed Header Field - :path: /
data.Add(new byte[] { 0x84 }, CoreStrings.Http2ErrorTrailersContainPseudoHeaderField);
// Indexed Header Field - :scheme: http
data.Add(new byte[] { 0x86 }, CoreStrings.Http2ErrorTrailersContainPseudoHeaderField);
// Literal Header Field without Indexing - Indexed Name - :authority: 127.0.0.1
data.Add(new byte[] { 0x01, 0x09 }.Concat(Encoding.ASCII.GetBytes("127.0.0.1")).ToArray(), CoreStrings.Http2ErrorTrailersContainPseudoHeaderField);
// Literal Header Field without Indexing - New Name - contains-Uppercase: 0
data.Add(new byte[] { 0x00, 0x12 }
.Concat(Encoding.ASCII.GetBytes("contains-Uppercase"))
.Concat(new byte[] { 0x01, (byte)'0' })
.ToArray(), CoreStrings.Http2ErrorTrailerNameUppercase);
return data;
}
}
}
}