Put request trailers in a separate collection (#10410)

This commit is contained in:
Chris Ross 2019-05-23 09:00:39 -07:00 committed by GitHub
parent c2e2d7d135
commit 156c4feb65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 383 additions and 46 deletions

View File

@ -373,6 +373,13 @@ namespace Microsoft.AspNetCore.Http
public string ToUriComponent() { throw null; }
}
public delegate System.Threading.Tasks.Task RequestDelegate(Microsoft.AspNetCore.Http.HttpContext context);
public static partial class RequestTrailerExtensions
{
public static bool CheckTrailersAvailable(this Microsoft.AspNetCore.Http.HttpRequest request) { throw null; }
public static Microsoft.Extensions.Primitives.StringValues GetDeclaredTrailers(this Microsoft.AspNetCore.Http.HttpRequest request) { throw null; }
public static Microsoft.Extensions.Primitives.StringValues GetTrailer(this Microsoft.AspNetCore.Http.HttpRequest request, string trailerName) { throw null; }
public static bool SupportsTrailers(this Microsoft.AspNetCore.Http.HttpRequest request) { throw null; }
}
public static partial class ResponseTrailerExtensions
{
public static void AppendTrailer(this Microsoft.AspNetCore.Http.HttpResponse response, string trailerName, Microsoft.Extensions.Primitives.StringValues trailerValues) { }

View File

@ -0,0 +1,65 @@
// 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;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Http
{
/// <summary>
/// HttpRequest extensions for working with request trailing headers.
/// </summary>
public static class RequestTrailerExtensions
{
/// <summary>
/// Gets the request "Trailer" header that lists which trailers to expect after the body.
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public static StringValues GetDeclaredTrailers(this HttpRequest request)
{
return request.Headers.GetCommaSeparatedValues(HeaderNames.Trailer);
}
/// <summary>
/// Indicates if the request supports receiving trailer headers.
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public static bool SupportsTrailers(this HttpRequest request)
{
return request.HttpContext.Features.Get<IHttpRequestTrailersFeature>() != null;
}
/// <summary>
/// Checks if the request supports trailers and they are available to be read now.
/// This does not mean that there are any trailers to read.
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public static bool CheckTrailersAvailable(this HttpRequest request)
{
return request.HttpContext.Features.Get<IHttpRequestTrailersFeature>()?.Available == true;
}
/// <summary>
/// Gets the requested trailing header from the response. Check <see cref="SupportsTrailers"/>
/// or a NotSupportedException may be thrown.
/// Check <see cref="CheckTrailersAvailable" /> or an InvalidOperationException may be thrown.
/// </summary>
/// <param name="request"></param>
/// <param name="trailerName"></param>
public static StringValues GetTrailer(this HttpRequest request, string trailerName)
{
var feature = request.HttpContext.Features.Get<IHttpRequestTrailersFeature>();
if (feature == null)
{
throw new NotSupportedException("This request does not support trailers.");
}
return feature.Trailers[trailerName];
}
}
}

View File

@ -195,6 +195,11 @@ namespace Microsoft.AspNetCore.Http.Features
System.Threading.CancellationToken RequestAborted { get; set; }
void Abort();
}
public partial interface IHttpRequestTrailersFeature
{
bool Available { get; }
Microsoft.AspNetCore.Http.IHeaderDictionary Trailers { get; }
}
public partial interface IHttpResponseFeature
{
System.IO.Stream Body { get; set; }

View File

@ -0,0 +1,26 @@
// 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.Http.Features
{
/// <summary>
/// This feature exposes HTTP request trailer headers, either for HTTP/1.1 chunked bodies or HTTP/2 trailing headers.
/// </summary>
public interface IHttpRequestTrailersFeature
{
/// <summary>
/// Indicates if the <see cref="Trailers"/> are available yet. They may not be available until the
/// request body is fully read.
/// </summary>
bool Available { get; }
/// <summary>
/// The trailing headers received. This will throw <see cref="InvalidOperationException"/> if <see cref="Available"/>
/// returns false. They may not be available until the request body is fully read. If there are no trailers this will
/// return an empty collection.
/// </summary>
IHeaderDictionary Trailers { get; }
}
}

View File

@ -276,6 +276,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
public static string GetAsciiOrUTF8StringNonNullCharacters(this System.Span<byte> span) { throw null; }
public static string GetAsciiStringEscaped(this System.Span<byte> span, int maxChars) { throw null; }
public static string GetAsciiStringNonNullCharacters(this System.Span<byte> span) { throw null; }
public static string GetHeaderName(this System.Span<byte> span) { throw null; }
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]public static bool GetKnownHttpScheme(this System.Span<byte> span, out Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpScheme knownScheme) { throw null; }
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]public static bool GetKnownMethod(this System.Span<byte> span, out Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod method, out int length) { throw null; }
public static Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod GetKnownMethod(string value) { throw null; }

View File

@ -602,4 +602,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="Http2MinDataRateNotSupported" xml:space="preserve">
<value>This feature is not supported for HTTP/2 requests except to disable it entirely by setting the rate to null.</value>
</data>
<data name="RequestTrailersNotAvailable" xml:space="preserve">
<value>The request trailers are not available yet. They may not be available until the full request body is read.</value>
</data>
</root>

View File

@ -35,7 +35,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
: base(context)
{
RequestKeepAlive = keepAlive;
_requestBodyPipe = CreateRequestBodyPipe(context);
}
@ -301,7 +300,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
// _consumedBytes aren't tracked for trailer headers, since headers have separate limits.
if (_mode == Mode.TrailerHeaders)
{
if (_context.TakeMessageHeaders(readableBuffer, out consumed, out examined))
if (_context.TakeMessageHeaders(readableBuffer, trailers: true, out consumed, out examined))
{
_mode = Mode.Complete;
}
@ -489,6 +488,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
consumed = trailerBuffer.End;
AddAndCheckConsumedBytes(2);
_mode = Mode.Complete;
// No trailers
_context.OnTrailersComplete();
}
else
{

View File

@ -163,7 +163,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
break;
}
case RequestProcessingStatus.ParsingHeaders:
if (TakeMessageHeaders(buffer, out consumed, out examined))
if (TakeMessageHeaders(buffer, trailers: false, out consumed, out examined))
{
_requestProcessingStatus = RequestProcessingStatus.AppStarted;
}
@ -189,7 +189,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
return result;
}
public bool TakeMessageHeaders(ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined)
public bool TakeMessageHeaders(ReadOnlySequence<byte> buffer, bool trailers, out SequencePosition consumed, out SequencePosition examined)
{
// Make sure the buffer is limited
bool overLength = false;
@ -202,7 +202,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
overLength = true;
}
var result = _parser.ParseHeaders(new Http1ParsingHandler(this), buffer, out consumed, out examined, out var consumedBytes);
var result = _parser.ParseHeaders(new Http1ParsingHandler(this, trailers), buffer, out consumed, out examined, out var consumedBytes);
_remainingRequestHeadersBytesAllowed -= consumedBytes;
if (!result && overLength)

View File

@ -212,6 +212,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
_context.Input.AdvanceTo(consumed);
_finalAdvanceCalled = true;
_context.OnTrailersComplete();
}
return;

View File

@ -129,6 +129,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
BadHttpRequestException.Throw(RequestRejectionReason.UpgradeRequestCannotHavePayload);
}
context.OnTrailersComplete(); // No trailers for these.
return new Http1UpgradeMessageBody(context);
}
@ -173,6 +174,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
BadHttpRequestException.Throw(requestRejectionReason, context.Method);
}
context.OnTrailersComplete(); // No trailers for these.
return keepAlive ? MessageBody.ZeroContentLengthKeepAlive : MessageBody.ZeroContentLengthClose;
}
}

View File

@ -8,17 +8,43 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
internal readonly struct Http1ParsingHandler : IHttpRequestLineHandler, IHttpHeadersHandler
{
public readonly Http1Connection Connection;
public readonly bool Trailers;
public Http1ParsingHandler(Http1Connection connection)
{
Connection = connection;
Trailers = false;
}
public Http1ParsingHandler(Http1Connection connection, bool trailers)
{
Connection = connection;
Trailers = trailers;
}
public void OnHeader(Span<byte> name, Span<byte> value)
=> Connection.OnHeader(name, value);
{
if (Trailers)
{
Connection.OnTrailer(name, value);
}
else
{
Connection.OnHeader(name, value);
}
}
public void OnHeadersComplete()
=> Connection.OnHeadersComplete();
{
if (Trailers)
{
Connection.OnTrailersComplete();
}
else
{
Connection.OnHeadersComplete();
}
}
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
=> Connection.OnStartLine(method, version, target, path, query, customMethod, pathEncoded);

View File

@ -26,6 +26,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
IHttpConnectionFeature,
IHttpRequestLifetimeFeature,
IHttpRequestIdentifierFeature,
IHttpRequestTrailersFeature,
IHttpBodyControlFeature,
IHttpMaxRequestBodySizeFeature,
IHttpResponseStartFeature,
@ -133,6 +134,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
}
bool IHttpRequestTrailersFeature.Available => RequestTrailersAvailable;
IHeaderDictionary IHttpRequestTrailersFeature.Trailers
{
get
{
if (!RequestTrailersAvailable)
{
throw new InvalidOperationException(CoreStrings.RequestTrailersNotAvailable);
}
return RequestTrailers;
}
}
int IHttpResponseFeature.StatusCode
{
get => StatusCode;

View File

@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private static readonly Type IRouteValuesFeatureType = typeof(IRouteValuesFeature);
private static readonly Type IEndpointFeatureType = typeof(IEndpointFeature);
private static readonly Type IHttpAuthenticationFeatureType = typeof(IHttpAuthenticationFeature);
private static readonly Type IHttpRequestTrailersFeatureType = typeof(IHttpRequestTrailersFeature);
private static readonly Type IQueryFeatureType = typeof(IQueryFeature);
private static readonly Type IFormFeatureType = typeof(IFormFeature);
private static readonly Type IHttpUpgradeFeatureType = typeof(IHttpUpgradeFeature);
@ -52,6 +53,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private object _currentIRouteValuesFeature;
private object _currentIEndpointFeature;
private object _currentIHttpAuthenticationFeature;
private object _currentIHttpRequestTrailersFeature;
private object _currentIQueryFeature;
private object _currentIFormFeature;
private object _currentIHttpUpgradeFeature;
@ -82,6 +84,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_currentIHttpUpgradeFeature = this;
_currentIHttpRequestIdentifierFeature = this;
_currentIHttpRequestLifetimeFeature = this;
_currentIHttpRequestTrailersFeature = this;
_currentIHttpConnectionFeature = this;
_currentIHttpMaxRequestBodySizeFeature = this;
_currentIHttpMinRequestBodyDataRateFeature = this;
@ -201,6 +204,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
feature = _currentIHttpAuthenticationFeature;
}
else if (key == IHttpRequestTrailersFeatureType)
{
feature = _currentIHttpRequestTrailersFeature;
}
else if (key == IQueryFeatureType)
{
feature = _currentIQueryFeature;
@ -321,6 +328,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
_currentIHttpAuthenticationFeature = value;
}
else if (key == IHttpRequestTrailersFeatureType)
{
_currentIHttpRequestTrailersFeature = value;
}
else if (key == IQueryFeatureType)
{
_currentIQueryFeature = value;
@ -439,6 +450,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
feature = (TFeature)_currentIHttpAuthenticationFeature;
}
else if (typeof(TFeature) == typeof(IHttpRequestTrailersFeature))
{
feature = (TFeature)_currentIHttpRequestTrailersFeature;
}
else if (typeof(TFeature) == typeof(IQueryFeature))
{
feature = (TFeature)_currentIQueryFeature;
@ -563,6 +578,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
_currentIHttpAuthenticationFeature = feature;
}
else if (typeof(TFeature) == typeof(IHttpRequestTrailersFeature))
{
_currentIHttpRequestTrailersFeature = feature;
}
else if (typeof(TFeature) == typeof(IQueryFeature))
{
_currentIQueryFeature = feature;
@ -679,6 +698,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
yield return new KeyValuePair<Type, object>(IHttpAuthenticationFeatureType, _currentIHttpAuthenticationFeature);
}
if (_currentIHttpRequestTrailersFeature != null)
{
yield return new KeyValuePair<Type, object>(IHttpRequestTrailersFeatureType, _currentIHttpRequestTrailersFeature);
}
if (_currentIQueryFeature != null)
{
yield return new KeyValuePair<Type, object>(IQueryFeatureType, _currentIQueryFeature);

View File

@ -206,6 +206,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
public IHeaderDictionary RequestHeaders { get; set; }
public IHeaderDictionary RequestTrailers { get; } = new HeaderDictionary();
public bool RequestTrailersAvailable { get; set; }
public Stream RequestBody { get; set; }
public PipeReader RequestBodyPipeReader { get; set; }
@ -369,6 +371,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
HttpResponseHeaders.Reset();
RequestHeaders = HttpRequestHeaders;
ResponseHeaders = HttpResponseHeaders;
RequestTrailers.Clear();
RequestTrailersAvailable = false;
_isLeasedMemoryInvalid = true;
_hasAdvanced = false;
@ -524,11 +528,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
HttpRequestHeaders.Append(name, value);
}
public void OnTrailer(Span<byte> name, Span<byte> value)
{
// Trailers still count towards the limit.
_requestHeadersParsed++;
if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount)
{
BadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders);
}
string key = name.GetHeaderName();
var valueStr = value.GetAsciiOrUTF8StringNonNullCharacters();
RequestTrailers.Append(key, valueStr);
}
public void OnHeadersComplete()
{
HttpRequestHeaders.OnHeadersComplete();
}
public void OnTrailersComplete()
{
RequestTrailersAvailable = true;
}
public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> application)
{
try

View File

@ -103,16 +103,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
[MethodImpl(MethodImplOptions.NoInlining)]
private unsafe void AppendUnknownHeaders(Span<byte> name, string valueString)
{
string key = new string('\0', name.Length);
fixed (byte* pKeyBytes = name)
fixed (char* keyBuffer = key)
{
if (!StringUtilities.TryGetAsciiString(pKeyBytes, keyBuffer, name.Length))
{
BadHttpRequestException.Throw(RequestRejectionReason.InvalidCharactersInHeaderName);
}
}
string key = name.GetHeaderName();
Unknown.TryGetValue(key, out var existing);
Unknown[key] = AppendValue(existing, valueString);
}

View File

@ -1073,9 +1073,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
ValidateHeader(name, value);
try
{
// Drop trailers for now. Adding them to the request headers is not thread safe.
// https://github.com/aspnet/KestrelHttpServer/issues/2051
if (_requestHeaderParsingState != RequestHeaderParsingState.Trailers)
if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers)
{
_currentHeadersStream.OnTrailer(name, value);
}
else
{
// Throws BadRequest for header count limit breaches.
// Throws InvalidOperation for bad encoding.

View File

@ -403,6 +403,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
}
}
OnTrailersComplete();
RequestBodyPipe.Writer.Complete();
_inputFlowControl.StopWindowUpdates();

View File

@ -84,6 +84,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
}
}
// The same as GetAsciiStringNonNullCharacters but throws BadRequest
public static unsafe string GetHeaderName(this Span<byte> span)
{
if (span.IsEmpty)
{
return string.Empty;
}
var asciiString = new string('\0', span.Length);
fixed (char* output = asciiString)
fixed (byte* buffer = span)
{
// This version if AsciiUtilities returns null if there are any null (0 byte) characters
// in the string
if (!StringUtilities.TryGetAsciiString(buffer, output, span.Length))
{
BadHttpRequestException.Throw(RequestRejectionReason.InvalidCharactersInHeaderName);
}
}
return asciiString;
}
public static unsafe string GetAsciiStringNonNullCharacters(this Span<byte> span)
{
if (span.IsEmpty)

View File

@ -2268,6 +2268,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
internal static string FormatHttp2MinDataRateNotSupported()
=> GetString("Http2MinDataRateNotSupported");
/// <summary>
/// The request trailers are not available yet. They may not be available until the full request body is read.
/// </summary>
internal static string RequestTrailersNotAvailable
{
get => GetString("RequestTrailersNotAvailable");
}
/// <summary>
/// The request trailers are not available yet. They may not be available until the full request body is read.
/// </summary>
internal static string FormatRequestTrailersNotAvailable()
=> GetString("RequestTrailersNotAvailable");
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -97,7 +97,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await _application.Output.WriteAsync(Encoding.UTF8.GetBytes("\r\n\r\n"));
var readableBuffer = (await _transport.Input.ReadAsync()).Buffer;
_http1Connection.TakeMessageHeaders(readableBuffer, out _consumed, out _examined);
_http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined);
_transport.Input.AdvanceTo(_consumed, _examined);
Assert.Equal(headerValue, _http1Connection.RequestHeaders[headerName]);
@ -116,7 +116,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await _application.Output.WriteAsync(extendedAsciiEncoding.GetBytes("\r\n\r\n"));
var readableBuffer = (await _transport.Input.ReadAsync()).Buffer;
var exception = Assert.Throws<InvalidOperationException>(() => _http1Connection.TakeMessageHeaders(readableBuffer, out _consumed, out _examined));
var exception = Assert.Throws<InvalidOperationException>(() => _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined));
}
[Fact]
@ -129,7 +129,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await _application.Output.WriteAsync(Encoding.ASCII.GetBytes($"{headerLine}\r\n"));
var readableBuffer = (await _transport.Input.ReadAsync()).Buffer;
var exception = Assert.Throws<BadHttpRequestException>(() => _http1Connection.TakeMessageHeaders(readableBuffer, out _consumed, out _examined));
var exception = Assert.Throws<BadHttpRequestException>(() => _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined));
_transport.Input.AdvanceTo(_consumed, _examined);
Assert.Equal(CoreStrings.BadRequest_HeadersExceedMaxTotalSize, exception.Message);
@ -145,7 +145,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await _application.Output.WriteAsync(Encoding.ASCII.GetBytes($"{headerLines}\r\n"));
var readableBuffer = (await _transport.Input.ReadAsync()).Buffer;
var exception = Assert.Throws<BadHttpRequestException>(() => _http1Connection.TakeMessageHeaders(readableBuffer, out _consumed, out _examined));
var exception = Assert.Throws<BadHttpRequestException>(() => _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined));
_transport.Input.AdvanceTo(_consumed, _examined);
Assert.Equal(CoreStrings.BadRequest_TooManyHeaders, exception.Message);
@ -250,7 +250,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await _application.Output.WriteAsync(Encoding.ASCII.GetBytes($"{headerLine1}\r\n"));
var readableBuffer = (await _transport.Input.ReadAsync()).Buffer;
var takeMessageHeaders = _http1Connection.TakeMessageHeaders(readableBuffer, out _consumed, out _examined);
var takeMessageHeaders = _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined);
_transport.Input.AdvanceTo(_consumed, _examined);
Assert.True(takeMessageHeaders);
@ -262,7 +262,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await _application.Output.WriteAsync(Encoding.ASCII.GetBytes($"{headerLine2}\r\n"));
readableBuffer = (await _transport.Input.ReadAsync()).Buffer;
takeMessageHeaders = _http1Connection.TakeMessageHeaders(readableBuffer, out _consumed, out _examined);
takeMessageHeaders = _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined);
_transport.Input.AdvanceTo(_consumed, _examined);
Assert.True(takeMessageHeaders);

View File

@ -121,6 +121,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_collection[typeof(IRequestBodyPipeFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpRequestIdentifierFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpRequestLifetimeFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpRequestTrailersFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpConnectionFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpMaxRequestBodySizeFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpMinRequestBodyDataRateFeature)] = CreateHttp1Connection();
@ -144,6 +145,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_collection.Set<IRequestBodyPipeFeature>(CreateHttp1Connection());
_collection.Set<IHttpRequestIdentifierFeature>(CreateHttp1Connection());
_collection.Set<IHttpRequestLifetimeFeature>(CreateHttp1Connection());
_collection.Set<IHttpRequestTrailersFeature>(CreateHttp1Connection());
_collection.Set<IHttpConnectionFeature>(CreateHttp1Connection());
_collection.Set<IHttpMaxRequestBodySizeFeature>(CreateHttp1Connection());
_collection.Set<IHttpMinRequestBodyDataRateFeature>(CreateHttp1Connection());

View File

@ -83,7 +83,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
ErrorUtilities.ThrowInvalidRequestLine();
}
if (!_http1Connection.TakeMessageHeaders(_buffer, out consumed, out examined))
if (!_http1Connection.TakeMessageHeaders(_buffer, trailers: false, out consumed, out examined))
{
ErrorUtilities.ThrowInvalidRequestHeaders();
}
@ -103,7 +103,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
{
_http1Connection.Reset();
if (!_http1Connection.TakeMessageHeaders(_buffer, out var consumed, out var examined))
if (!_http1Connection.TakeMessageHeaders(_buffer, trailers: false, out var consumed, out var examined))
{
ErrorUtilities.ThrowInvalidRequestHeaders();
}

View File

@ -159,7 +159,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
readableBuffer = readableBuffer.Slice(consumed);
if (!Http1Connection.TakeMessageHeaders(readableBuffer, out consumed, out examined))
if (!Http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out consumed, out examined))
{
ErrorUtilities.ThrowInvalidRequestHeaders();
}
@ -196,7 +196,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
result = Pipe.Reader.ReadAsync().GetAwaiter().GetResult();
readableBuffer = result.Buffer;
if (!Http1Connection.TakeMessageHeaders(readableBuffer, out consumed, out examined))
if (!Http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out consumed, out examined))
{
ErrorUtilities.ThrowInvalidRequestHeaders();
}

View File

@ -232,18 +232,59 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
var buffer = new byte[200];
// The first request is chunked with no trailers.
if (requestsReceived == 0)
{
Assert.True(request.SupportsTrailers(), "SupportsTrailers");
Assert.False(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); // Not yet
Assert.Throws<InvalidOperationException>(() => request.GetTrailer("X-Trailer-Header")); // Not yet
}
// The middle requests are chunked with trailers.
else if (requestsReceived < requestCount)
{
Assert.True(request.SupportsTrailers(), "SupportsTrailers");
Assert.False(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); // Not yet
Assert.Throws<InvalidOperationException>(() => request.GetTrailer("X-Trailer-Header")); // Not yet
Assert.Equal("X-Trailer-Header", request.GetDeclaredTrailers().ToString());
}
// The last request is content-length with no trailers.
else
{
Assert.True(request.SupportsTrailers(), "SupportsTrailers");
Assert.False(request.CheckTrailersAvailable(), "CheckTrailersAvailable");
Assert.Throws<InvalidOperationException>(() => request.GetTrailer("X-Trailer-Header"));
}
while (await request.Body.ReadAsync(buffer, 0, buffer.Length) != 0)
{
;// read to end
}
if (requestsReceived < requestCount)
Assert.False(request.Headers.ContainsKey("X-Trailer-Header"));
// The first request is chunked with no trailers.
if (requestsReceived == 0)
{
Assert.Equal(new string('a', requestsReceived), request.Headers["X-Trailer-Header"].ToString());
Assert.True(request.SupportsTrailers(), "SupportsTrailers");
Assert.True(request.CheckTrailersAvailable(), "CheckTrailersAvailable");
Assert.Equal(string.Empty, request.GetDeclaredTrailers().ToString());
Assert.Equal(string.Empty, request.GetTrailer("X-Trailer-Header").ToString());
}
// The middle requests are chunked with trailers.
else if (requestsReceived < requestCount)
{
Assert.True(request.SupportsTrailers(), "SupportsTrailers");
Assert.True(request.CheckTrailersAvailable(), "CheckTrailersAvailable");
Assert.Equal("X-Trailer-Header", request.GetDeclaredTrailers().ToString());
Assert.Equal(new string('a', requestsReceived), request.GetTrailer("X-Trailer-Header").ToString());
}
// The last request is content-length with no trailers.
else
{
Assert.True(string.IsNullOrEmpty(request.Headers["X-Trailer-Header"]));
Assert.True(request.SupportsTrailers(), "SupportsTrailers");
Assert.True(request.CheckTrailersAvailable(), "CheckTrailersAvailable");
Assert.Equal(string.Empty, request.GetDeclaredTrailers().ToString());
Assert.Equal(string.Empty, request.GetTrailer("X-Trailer-Header").ToString());
}
requestsReceived++;
@ -278,6 +319,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
"POST / HTTP/1.1",
"Host:",
"Transfer-Encoding: chunked",
"Trailer: X-Trailer-Header",
"",
"C",
$"HelloChunk{i:00}",
@ -315,6 +357,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
var response = httpContext.Response;
var request = httpContext.Request;
// The first request is chunked with no trailers.
if (requestsReceived == 0)
{
Assert.True(request.SupportsTrailers(), "SupportsTrailers");
Assert.False(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); // Not yet
Assert.Throws<InvalidOperationException>(() => request.GetTrailer("X-Trailer-Header")); // Not yet
}
// The middle requests are chunked with trailers.
else if (requestsReceived < requestCount)
{
Assert.True(request.SupportsTrailers(), "SupportsTrailers");
Assert.False(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); // Not yet
Assert.Throws<InvalidOperationException>(() => request.GetTrailer("X-Trailer-Header")); // Not yet
Assert.Equal("X-Trailer-Header", request.GetDeclaredTrailers().ToString());
}
// The last request is content-length with no trailers.
else
{
Assert.True(request.SupportsTrailers(), "SupportsTrailers");
Assert.False(request.CheckTrailersAvailable(), "CheckTrailersAvailable");
Assert.Throws<InvalidOperationException>(() => request.GetTrailer("X-Trailer-Header"));
}
while (true)
{
var result = await request.BodyReader.ReadAsync();
@ -325,13 +390,31 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
}
}
if (requestsReceived < requestCount)
Assert.False(request.Headers.ContainsKey("X-Trailer-Header"));
// The first request is chunked with no trailers.
if (requestsReceived == 0)
{
Assert.Equal(new string('a', requestsReceived), request.Headers["X-Trailer-Header"].ToString());
Assert.True(request.SupportsTrailers(), "SupportsTrailers");
Assert.True(request.CheckTrailersAvailable(), "CheckTrailersAvailable");
Assert.Equal(string.Empty, request.GetDeclaredTrailers().ToString());
Assert.Equal(string.Empty, request.GetTrailer("X-Trailer-Header").ToString());
}
// The middle requests are chunked with trailers.
else if (requestsReceived < requestCount)
{
Assert.True(request.SupportsTrailers(), "SupportsTrailers");
Assert.True(request.CheckTrailersAvailable(), "CheckTrailersAvailable");
Assert.Equal("X-Trailer-Header", request.GetDeclaredTrailers().ToString());
Assert.Equal(new string('a', requestsReceived), request.GetTrailer("X-Trailer-Header").ToString());
}
// The last request is content-length with no trailers.
else
{
Assert.True(string.IsNullOrEmpty(request.Headers["X-Trailer-Header"]));
Assert.True(request.SupportsTrailers(), "SupportsTrailers");
Assert.True(request.CheckTrailersAvailable(), "CheckTrailersAvailable");
Assert.Equal(string.Empty, request.GetDeclaredTrailers().ToString());
Assert.Equal(string.Empty, request.GetTrailer("X-Trailer-Header").ToString());
}
requestsReceived++;
@ -366,6 +449,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
"POST / HTTP/1.1",
"Host:",
"Transfer-Encoding: chunked",
"Trailer: X-Trailer-Header",
"",
"C",
$"HelloChunk{i:00}",
@ -495,13 +579,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
;// read to end
}
Assert.True(string.IsNullOrEmpty(request.Headers["X-Trailer-Header"]));
if (requestsReceived < requestCount)
{
Assert.Equal(new string('a', requestsReceived), request.Headers["X-Trailer-Header"].ToString());
}
else
{
Assert.True(string.IsNullOrEmpty(request.Headers["X-Trailer-Header"]));
Assert.Equal(new string('a', requestsReceived), request.GetTrailer("X-Trailer-Header").ToString());
}
requestsReceived++;

View File

@ -1174,7 +1174,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task HEADERS_Received_WithTrailers_Discarded(bool sendData)
public async Task HEADERS_Received_WithTrailers_Available(bool sendData)
{
await InitializeConnectionAsync(_readTrailersApplication);
@ -1206,10 +1206,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
VerifyDecodedRequestHeaders(_browserRequestHeaders);
// Make sure the trailers are missing. https://github.com/aspnet/KestrelHttpServer/issues/2630
// Make sure the trailers are in the trailers collection.
foreach (var header in _requestTrailers)
{
Assert.False(_receivedHeaders.ContainsKey(header.Key));
Assert.True(_receivedTrailers.ContainsKey(header.Key));
Assert.Equal(header.Value, _receivedTrailers[header.Key]);
}
await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false);
@ -3288,7 +3290,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task CONTINUATION_Received_WithTrailers_Discarded(bool sendData)
public async Task CONTINUATION_Received_WithTrailers_Available(bool sendData)
{
await InitializeConnectionAsync(_readTrailersApplication);
@ -3331,9 +3333,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
VerifyDecodedRequestHeaders(_browserRequestHeaders);
// Make sure the trailers are missing. https://github.com/aspnet/KestrelHttpServer/issues/2630
// Make sure the trailers are in the trailers collection.
Assert.False(_receivedHeaders.ContainsKey("trailer-1"));
Assert.False(_receivedHeaders.ContainsKey("trailer-2"));
Assert.True(_receivedTrailers.ContainsKey("trailer-1"));
Assert.True(_receivedTrailers.ContainsKey("trailer-2"));
Assert.Equal("1", _receivedTrailers["trailer-1"]);
Assert.Equal("2", _receivedTrailers["trailer-2"]);
await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false);
}

View File

@ -130,6 +130,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
protected readonly ConcurrentDictionary<int, TaskCompletionSource<object>> _runningStreams = new ConcurrentDictionary<int, TaskCompletionSource<object>>();
protected readonly Dictionary<string, string> _receivedHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
protected readonly Dictionary<string, string> _receivedTrailers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
protected readonly Dictionary<string, string> _decodedHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
protected readonly HashSet<int> _abortedStreamIds = new HashSet<int>();
protected readonly object _abortedStreamIdsLock = new object();
@ -199,16 +200,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_readTrailersApplication = async context =>
{
Assert.True(context.Request.SupportsTrailers(), "SupportsTrailers");
Assert.False(context.Request.CheckTrailersAvailable(), "SupportsTrailers");
using (var ms = new MemoryStream())
{
// Consuming the entire request body guarantees trailers will be available
await context.Request.Body.CopyToAsync(ms);
}
Assert.True(context.Request.SupportsTrailers(), "SupportsTrailers");
Assert.True(context.Request.CheckTrailersAvailable(), "SupportsTrailers");
foreach (var header in context.Request.Headers)
{
_receivedHeaders[header.Key] = header.Value.ToString();
}
var trailers = context.Features.Get<IHttpRequestTrailersFeature>().Trailers;
foreach (var header in trailers)
{
_receivedTrailers[header.Key] = header.Value.ToString();
}
};
_bufferingApplication = async context =>

View File

@ -5,6 +5,7 @@ using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport;
@ -360,7 +361,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
} while (count != 0);
Assert.Equal("Hello World", Encoding.ASCII.GetString(buffer));
Assert.Equal("trailing-value", context.Request.Headers["Trailing-Header"].ToString());
Assert.Equal("trailing-value", context.Request.GetTrailer("Trailing-Header").ToString());
},
new TestServiceContext(LoggerFactory) { ServerOptions = { Limits = { MaxRequestBodySize = globalMaxRequestBodySize } } }))
{

View File

@ -26,6 +26,7 @@ namespace CodeGenerator
var commonFeatures = new[]
{
"IHttpAuthenticationFeature",
"IHttpRequestTrailersFeature",
"IQueryFeature",
"IFormFeature",
};
@ -69,6 +70,7 @@ namespace CodeGenerator
"IHttpUpgradeFeature",
"IHttpRequestIdentifierFeature",
"IHttpRequestLifetimeFeature",
"IHttpRequestTrailersFeature",
"IHttpConnectionFeature",
"IHttpMaxRequestBodySizeFeature",
"IHttpMinRequestBodyDataRateFeature",