HTTP/2: support trailers.
This commit is contained in:
parent
66a3c9496a
commit
9dfffd14bb
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue