Identifying if a request has a body #24175 (#24984)

This commit is contained in:
Chris Ross 2020-08-23 23:38:40 -07:00 committed by GitHub
parent df37d00307
commit 11bae8a112
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 591 additions and 41 deletions

View File

@ -66,32 +66,35 @@ namespace Microsoft.AspNetCore.TestHost
var contextBuilder = new HttpContextBuilder(_application, AllowSynchronousIO, PreserveExecutionContext);
var requestContent = request.Content ?? new StreamContent(Stream.Null);
var requestContent = request.Content;
// Read content from the request HttpContent into a pipe in a background task. This will allow the request
// delegate to start before the request HttpContent is complete. A background task allows duplex streaming scenarios.
contextBuilder.SendRequestStream(async writer =>
if (requestContent != null)
{
if (requestContent is StreamContent)
// Read content from the request HttpContent into a pipe in a background task. This will allow the request
// delegate to start before the request HttpContent is complete. A background task allows duplex streaming scenarios.
contextBuilder.SendRequestStream(async writer =>
{
if (requestContent is StreamContent)
{
// This is odd but required for backwards compat. If StreamContent is passed in then seek to beginning.
// This is safe because StreamContent.ReadAsStreamAsync doesn't block. It will return the inner stream.
var body = await requestContent.ReadAsStreamAsync();
if (body.CanSeek)
{
if (body.CanSeek)
{
// This body may have been consumed before, rewind it.
body.Seek(0, SeekOrigin.Begin);
}
await body.CopyToAsync(writer);
}
else
{
await requestContent.CopyToAsync(writer.AsStream());
}
await body.CopyToAsync(writer);
}
else
{
await requestContent.CopyToAsync(writer.AsStream());
}
await writer.CompleteAsync();
});
await writer.CompleteAsync();
});
}
contextBuilder.Configure((context, reader) =>
{
@ -110,6 +113,39 @@ namespace Microsoft.AspNetCore.TestHost
req.Scheme = request.RequestUri.Scheme;
var canHaveBody = false;
if (requestContent != null)
{
canHaveBody = true;
// Chunked takes precedence over Content-Length, don't create a request with both Content-Length and chunked.
if (request.Headers.TransferEncodingChunked != true)
{
// Reading the ContentLength will add it to the Headers‼
// https://github.com/dotnet/runtime/blob/874399ab15e47c2b4b7c6533cc37d27d47cb5242/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HttpContentHeaders.cs#L68-L87
var contentLength = requestContent.Headers.ContentLength;
if (!contentLength.HasValue && request.Version == HttpVersion.Version11)
{
// HTTP/1.1 requests with a body require either Content-Length or Transfer-Encoding: chunked.
request.Headers.TransferEncodingChunked = true;
}
else if (contentLength == 0)
{
canHaveBody = false;
}
}
foreach (var header in requestContent.Headers)
{
req.Headers.Append(header.Key, header.Value.ToArray());
}
if (canHaveBody)
{
req.Body = new AsyncStreamWrapper(reader.AsStream(), () => contextBuilder.AllowSynchronousIO);
}
}
context.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(canHaveBody));
foreach (var header in request.Headers)
{
// User-Agent is a space delineated single line header but HttpRequestHeaders parses it as multiple elements.
@ -141,17 +177,6 @@ namespace Microsoft.AspNetCore.TestHost
req.PathBase = _pathBase;
}
req.QueryString = QueryString.FromUriComponent(request.RequestUri);
// Reading the ContentLength will add it to the Headers‼
// https://github.com/dotnet/runtime/blob/874399ab15e47c2b4b7c6533cc37d27d47cb5242/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HttpContentHeaders.cs#L68-L87
_ = requestContent.Headers.ContentLength;
foreach (var header in requestContent.Headers)
{
req.Headers.Append(header.Key, header.Value.ToArray());
}
req.Body = new AsyncStreamWrapper(reader.AsStream(), () => contextBuilder.AllowSynchronousIO);
});
var response = new HttpResponseMessage();

View File

@ -0,0 +1,17 @@
// 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 Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.TestHost
{
internal class RequestBodyDetectionFeature : IHttpRequestBodyDetectionFeature
{
public RequestBodyDetectionFeature(bool canHaveBody)
{
CanHaveBody = canHaveBody;
}
public bool CanHaveBody { get; }
}
}

View File

@ -111,7 +111,9 @@ namespace Microsoft.AspNetCore.TestHost
var contentBytes = Encoding.UTF8.GetBytes("This is a content!");
var handler = new ClientHandler(new PathString(""), new DummyApplication(context =>
{
Assert.True(context.Request.CanHaveBody());
Assert.Equal(contentBytes.LongLength, context.Request.ContentLength);
Assert.False(context.Request.Headers.ContainsKey(HeaderNames.TransferEncoding));
return Task.CompletedTask;
}));
@ -122,11 +124,13 @@ namespace Microsoft.AspNetCore.TestHost
}
[Fact]
public Task ContentLengthWithNoBodyWorks()
public Task ContentLengthNotPresentWithNoBody()
{
var handler = new ClientHandler(new PathString(""), new DummyApplication(context =>
{
Assert.Equal(0, context.Request.ContentLength);
Assert.False(context.Request.CanHaveBody());
Assert.Null(context.Request.ContentLength);
Assert.False(context.Request.Headers.ContainsKey(HeaderNames.TransferEncoding));
return Task.CompletedTask;
}));
@ -136,11 +140,13 @@ namespace Microsoft.AspNetCore.TestHost
}
[Fact]
public Task ContentLengthWithChunkedTransferEncodingWorks()
public Task ContentLengthWithImplicitChunkedTransferEncodingWorks()
{
var handler = new ClientHandler(new PathString(""), new DummyApplication(context =>
{
Assert.True(context.Request.CanHaveBody());
Assert.Null(context.Request.ContentLength);
Assert.Equal("chunked", context.Request.Headers[HeaderNames.TransferEncoding]);
return Task.CompletedTask;
}));
@ -150,6 +156,26 @@ namespace Microsoft.AspNetCore.TestHost
return httpClient.PostAsync("http://example.com", new UnlimitedContent());
}
[Fact]
public Task ContentLengthWithExplicitChunkedTransferEncodingWorks()
{
var handler = new ClientHandler(new PathString(""), new DummyApplication(context =>
{
Assert.True(context.Request.CanHaveBody());
Assert.Null(context.Request.ContentLength);
Assert.Equal("chunked", context.Request.Headers[HeaderNames.TransferEncoding]);
return Task.CompletedTask;
}));
var httpClient = new HttpClient(handler);
httpClient.DefaultRequestHeaders.TransferEncodingChunked = true;
var contentBytes = Encoding.UTF8.GetBytes("This is a content!");
var content = new ByteArrayContent(contentBytes);
return httpClient.PostAsync("http://example.com", content);
}
[Fact]
public async Task ServerTrailersSetOnResponseAfterContentRead()
{

View File

@ -40,6 +40,7 @@ namespace Microsoft.AspNetCore.TestHost
Assert.Equal("/A/Path", context.Request.PathBase.Value);
Assert.Equal("/and/file.txt", context.Request.Path.Value);
Assert.Equal("?and=query", context.Request.QueryString.Value);
Assert.Null(context.Request.CanHaveBody());
Assert.NotNull(context.Request.Body);
Assert.NotNull(context.Request.Headers);
Assert.NotNull(context.Response.Headers);

View File

@ -228,6 +228,7 @@ namespace Microsoft.AspNetCore.TestHost
var stream = new ThrowOnDisposeStream();
stream.Write(Encoding.ASCII.GetBytes("Hello World"));
stream.Seek(0, SeekOrigin.Begin);
var response = await server.CreateClient().PostAsync("/", new StreamContent(stream));
Assert.True(response.IsSuccessStatusCode);
Assert.Equal("Hello World", await response.Content.ReadAsStringAsync());

View File

@ -3,6 +3,8 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Testing;
namespace Microsoft.AspNetCore.TestHost
@ -14,5 +16,10 @@ namespace Microsoft.AspNetCore.TestHost
internal static Task<T> WithTimeout<T>(this Task<T> task) => task.TimeoutAfter(DefaultTimeout);
internal static Task WithTimeout(this Task task) => task.TimeoutAfter(DefaultTimeout);
internal static bool? CanHaveBody(this HttpRequest request)
{
return request.HttpContext.Features.Get<IHttpRequestBodyDetectionFeature>()?.CanHaveBody;
}
}
}

View File

@ -0,0 +1,29 @@
// 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.
namespace Microsoft.AspNetCore.Http.Features
{
/// <summary>
/// Used to indicate if the request can have a body.
/// </summary>
public interface IHttpRequestBodyDetectionFeature
{
/// <summary>
/// Indicates if the request can have a body.
/// </summary>
/// <remarks>
/// This returns true when:
/// - It's an HTTP/1.x request with a non-zero Content-Length or a 'Transfer-Encoding: chunked' header.
/// - It's an HTTP/2 request that did not set the END_STREAM flag on the initial headers frame.
/// The final request body length may still be zero for the chunked or HTTP/2 scenarios.
///
/// This returns false when:
/// - It's an HTTP/1.x request with no Content-Length or 'Transfer-Encoding: chunked' header, or the Content-Length is 0.
/// - It's an HTTP/1.x request with Connection: Upgrade (e.g. WebSockets). There is no HTTP request body for these requests and
/// no data should be received until after the upgrade.
/// - It's an HTTP/2 request that set END_STREAM on the initial headers frame.
/// When false, the request body should never return data.
/// </remarks>
bool CanHaveBody { get; }
}
}

View File

@ -23,6 +23,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{
internal class FeatureContext :
IHttpRequestFeature,
IHttpRequestBodyDetectionFeature,
IHttpConnectionFeature,
IHttpResponseFeature,
IHttpResponseBodyFeature,
@ -212,6 +213,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys
set { _scheme = value; }
}
bool IHttpRequestBodyDetectionFeature.CanHaveBody => Request.HasEntityBody;
IPAddress IHttpConnectionFeature.LocalIpAddress
{
get

View File

@ -135,6 +135,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{
if (_contentBoundaryType == BoundaryType.None)
{
// Note Http.Sys adds the Transfer-Encoding: chunked header to HTTP/2 requests with bodies for back compat.
string transferEncoding = Headers[HttpKnownHeaderNames.TransferEncoding];
if (string.Equals("chunked", transferEncoding?.Trim(), StringComparison.OrdinalIgnoreCase))
{

View File

@ -17,6 +17,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
private static readonly Dictionary<Type, Func<FeatureContext, object>> _featureFuncLookup = new Dictionary<Type, Func<FeatureContext, object>>()
{
{ typeof(IHttpRequestFeature), _identityFunc },
{ typeof(IHttpRequestBodyDetectionFeature), _identityFunc },
{ typeof(IHttpConnectionFeature), _identityFunc },
{ typeof(IHttpResponseFeature), _identityFunc },
{ typeof(IHttpResponseBodyFeature), _identityFunc },

View File

@ -2,9 +2,12 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
@ -31,6 +34,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests
using var server = Utilities.CreateDynamicHttpsServer(out var address, httpContext =>
{
// Default 200
Assert.False(httpContext.Request.CanHaveBody());
return Task.CompletedTask;
});
@ -64,6 +68,233 @@ namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests
.Build().RunAsync();
}
[ConditionalTheory]
[InlineData("POST")]
[InlineData("PUT")]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10, SkipReason = "Http2 requires Win10")]
public async Task RequestWithoutData_LengthRequired_Rejected(string method)
{
using var server = Utilities.CreateDynamicHttpsServer(out var address, httpContext =>
{
throw new NotImplementedException();
});
await new HostBuilder()
.UseHttp2Cat(address, async h2Connection =>
{
await h2Connection.InitializeConnectionAsync();
h2Connection.Logger.LogInformation("Initialized http2 connection. Starting stream 1.");
var headers = new[]
{
new KeyValuePair<string, string>(HeaderNames.Method, method),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "https"),
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
};
await h2Connection.StartStreamAsync(1, headers, endStream: true);
await h2Connection.ReceiveHeadersAsync(1, decodedHeaders =>
{
Assert.Equal("411", decodedHeaders[HeaderNames.Status]);
});
var dataFrame = await h2Connection.ReceiveFrameAsync();
Http2Utilities.VerifyDataFrame(dataFrame, 1, endOfStream: false, length: 344);
dataFrame = await h2Connection.ReceiveFrameAsync();
Http2Utilities.VerifyDataFrame(dataFrame, 1, endOfStream: true, length: 0);
h2Connection.Logger.LogInformation("Connection stopped.");
})
.Build().RunAsync();
}
[ConditionalTheory]
[InlineData("GET")]
[InlineData("HEAD")]
[InlineData("PATCH")]
[InlineData("DELETE")]
[InlineData("CUSTOM")]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10, SkipReason = "Http2 requires Win10")]
public async Task RequestWithoutData_Success(string method)
{
using var server = Utilities.CreateDynamicHttpsServer(out var address, httpContext =>
{
Assert.True(HttpMethods.Equals(method, httpContext.Request.Method));
Assert.False(httpContext.Request.CanHaveBody());
Assert.Null(httpContext.Request.ContentLength);
Assert.False(httpContext.Request.Headers.ContainsKey(HeaderNames.TransferEncoding));
return Task.CompletedTask;
});
await new HostBuilder()
.UseHttp2Cat(address, async h2Connection =>
{
await h2Connection.InitializeConnectionAsync();
h2Connection.Logger.LogInformation("Initialized http2 connection. Starting stream 1.");
var headers = new[]
{
new KeyValuePair<string, string>(HeaderNames.Method, method),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "https"),
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
};
await h2Connection.StartStreamAsync(1, headers, endStream: true);
await h2Connection.ReceiveHeadersAsync(1, decodedHeaders =>
{
Assert.Equal("200", decodedHeaders[HeaderNames.Status]);
});
var dataFrame = await h2Connection.ReceiveFrameAsync();
if (Environment.OSVersion.Version >= Win10_Regressed_DataFrame)
{
// TODO: Remove when the regression is fixed.
// https://github.com/dotnet/aspnetcore/issues/23164#issuecomment-652646163
Http2Utilities.VerifyDataFrame(dataFrame, 1, endOfStream: false, length: 0);
dataFrame = await h2Connection.ReceiveFrameAsync();
}
Http2Utilities.VerifyDataFrame(dataFrame, 1, endOfStream: true, length: 0);
h2Connection.Logger.LogInformation("Connection stopped.");
})
.Build().RunAsync();
}
[ConditionalTheory]
[InlineData("GET")]
// [InlineData("HEAD")] Reset with code HTTP_1_1_REQUIRED
[InlineData("POST")]
[InlineData("PUT")]
[InlineData("PATCH")]
[InlineData("DELETE")]
[InlineData("CUSTOM")]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10, SkipReason = "Http2 requires Win10")]
public async Task RequestWithDataAndContentLength_Success(string method)
{
using var server = Utilities.CreateDynamicHttpsServer(out var address, httpContext =>
{
Assert.True(HttpMethods.Equals(method, httpContext.Request.Method));
Assert.True(httpContext.Request.CanHaveBody());
Assert.Equal(11, httpContext.Request.ContentLength);
Assert.False(httpContext.Request.Headers.ContainsKey(HeaderNames.TransferEncoding));
return httpContext.Request.Body.CopyToAsync(httpContext.Response.Body);
});
await new HostBuilder()
.UseHttp2Cat(address, async h2Connection =>
{
await h2Connection.InitializeConnectionAsync();
h2Connection.Logger.LogInformation("Initialized http2 connection. Starting stream 1.");
var headers = new[]
{
new KeyValuePair<string, string>(HeaderNames.Method, method),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "https"),
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
new KeyValuePair<string, string>(HeaderNames.ContentLength, "11"),
};
await h2Connection.StartStreamAsync(1, headers, endStream: false);
await h2Connection.SendDataAsync(1, Encoding.UTF8.GetBytes("Hello World"), endStream: true);
// Http.Sys no longer sends a window update here on later versions.
if (Environment.OSVersion.Version < new Version(10, 0, 19041, 0))
{
var windowUpdate = await h2Connection.ReceiveFrameAsync();
Assert.Equal(Http2FrameType.WINDOW_UPDATE, windowUpdate.Type);
}
await h2Connection.ReceiveHeadersAsync(1, decodedHeaders =>
{
Assert.Equal("200", decodedHeaders[HeaderNames.Status]);
});
var dataFrame = await h2Connection.ReceiveFrameAsync();
Http2Utilities.VerifyDataFrame(dataFrame, 1, endOfStream: false, length: 11);
Assert.Equal("Hello World", Encoding.UTF8.GetString(dataFrame.Payload.Span));
dataFrame = await h2Connection.ReceiveFrameAsync();
Http2Utilities.VerifyDataFrame(dataFrame, 1, endOfStream: true, length: 0);
h2Connection.Logger.LogInformation("Connection stopped.");
})
.Build().RunAsync();
}
[ConditionalTheory]
[InlineData("GET")]
// [InlineData("HEAD")] Reset with code HTTP_1_1_REQUIRED
[InlineData("POST")]
[InlineData("PUT")]
[InlineData("PATCH")]
[InlineData("DELETE")]
[InlineData("CUSTOM")]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10, SkipReason = "Http2 requires Win10")]
public async Task RequestWithDataAndNoContentLength_Success(string method)
{
using var server = Utilities.CreateDynamicHttpsServer(out var address, httpContext =>
{
Assert.True(HttpMethods.Equals(method, httpContext.Request.Method));
Assert.True(httpContext.Request.CanHaveBody());
Assert.Null(httpContext.Request.ContentLength);
// The client didn't send this header, Http.Sys added it for back compat with HTTP/1.1.
Assert.Equal("chunked", httpContext.Request.Headers[HeaderNames.TransferEncoding]);
return httpContext.Request.Body.CopyToAsync(httpContext.Response.Body);
});
await new HostBuilder()
.UseHttp2Cat(address, async h2Connection =>
{
await h2Connection.InitializeConnectionAsync();
h2Connection.Logger.LogInformation("Initialized http2 connection. Starting stream 1.");
var headers = new[]
{
new KeyValuePair<string, string>(HeaderNames.Method, method),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "https"),
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
};
await h2Connection.StartStreamAsync(1, headers, endStream: false);
await h2Connection.SendDataAsync(1, Encoding.UTF8.GetBytes("Hello World"), endStream: true);
// Http.Sys no longer sends a window update here on later versions.
if (Environment.OSVersion.Version < new Version(10, 0, 19041, 0))
{
var windowUpdate = await h2Connection.ReceiveFrameAsync();
Assert.Equal(Http2FrameType.WINDOW_UPDATE, windowUpdate.Type);
}
await h2Connection.ReceiveHeadersAsync(1, decodedHeaders =>
{
Assert.Equal("200", decodedHeaders[HeaderNames.Status]);
});
var dataFrame = await h2Connection.ReceiveFrameAsync();
Http2Utilities.VerifyDataFrame(dataFrame, 1, endOfStream: false, length: 11);
Assert.Equal("Hello World", Encoding.UTF8.GetString(dataFrame.Payload.Span));
dataFrame = await h2Connection.ReceiveFrameAsync();
Http2Utilities.VerifyDataFrame(dataFrame, 1, endOfStream: true, length: 0);
h2Connection.Logger.LogInformation("Connection stopped.");
})
.Build().RunAsync();
}
[ConditionalFact]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10, SkipReason = "Http2 requires Win10")]
public async Task ResponseWithData_Success()

View File

@ -48,6 +48,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
Assert.NotNull(feature);
Assert.False(feature.IsReadOnly);
Assert.True(httpContext.Request.CanHaveBody());
Assert.Equal(11, httpContext.Request.ContentLength);
byte[] input = new byte[100];
int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length);
@ -114,6 +115,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
Assert.NotNull(feature);
Assert.False(feature.IsReadOnly);
Assert.True(httpContext.Request.CanHaveBody());
Assert.Null(httpContext.Request.ContentLength);
byte[] input = new byte[100];
int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length);

View File

@ -9,6 +9,7 @@ using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Testing;
using Xunit;
@ -23,6 +24,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
string address;
using (Utilities.CreateHttpServer(out address, httpContext =>
{
Assert.True(httpContext.Request.CanHaveBody());
byte[] input = new byte[100];
httpContext.Features.Get<IHttpBodyControlFeature>().AllowSynchronousIO = true;
int read = httpContext.Request.Body.Read(input, 0, input.Length);
@ -42,6 +44,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
string address;
using (Utilities.CreateHttpServer(out address, async httpContext =>
{
Assert.True(httpContext.Request.CanHaveBody());
byte[] input = new byte[100];
int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length);
httpContext.Response.ContentLength = read;

View File

@ -44,6 +44,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
Assert.Equal("/basepath/SomePath?SomeQuery", requestInfo.RawTarget);
Assert.Equal("HTTP/1.1", requestInfo.Protocol);
Assert.False(httpContext.Request.CanHaveBody());
var connectionInfo = httpContext.Features.Get<IHttpConnectionFeature>();
Assert.Equal("::1", connectionInfo.RemoteIpAddress.ToString());
Assert.NotEqual(0, connectionInfo.RemotePort);

View File

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@ -170,5 +171,10 @@ namespace Microsoft.AspNetCore.Server.HttpSys
internal static Task WithTimeout(this Task task) => task.TimeoutAfter(DefaultTimeout);
internal static Task<T> WithTimeout<T>(this Task<T> task) => task.TimeoutAfter(DefaultTimeout);
internal static bool? CanHaveBody(this HttpRequest request)
{
return request.HttpContext.Features.Get<IHttpRequestBodyDetectionFeature>()?.CanHaveBody;
}
}
}

View File

@ -18,6 +18,7 @@ using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
internal partial class HttpProtocol : IHttpRequestFeature,
IHttpRequestBodyDetectionFeature,
IHttpResponseFeature,
IHttpResponseBodyFeature,
IRequestBodyPipeFeature,
@ -121,6 +122,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
}
bool IHttpRequestBodyDetectionFeature.CanHaveBody => _bodyControl.CanHaveBody;
bool IHttpRequestTrailersFeature.Available => RequestTrailersAvailable;
IHeaderDictionary IHttpRequestTrailersFeature.Trailers

View File

@ -14,6 +14,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
internal partial class HttpProtocol : IFeatureCollection
{
private object _currentIHttpRequestFeature;
private object _currentIHttpRequestBodyDetectionFeature;
private object _currentIHttpResponseFeature;
private object _currentIHttpResponseBodyFeature;
private object _currentIRequestBodyPipeFeature;
@ -48,6 +49,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private void FastReset()
{
_currentIHttpRequestFeature = this;
_currentIHttpRequestBodyDetectionFeature = this;
_currentIHttpResponseFeature = this;
_currentIHttpResponseBodyFeature = this;
_currentIRequestBodyPipeFeature = this;
@ -133,6 +135,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
feature = _currentIHttpRequestFeature;
}
else if (key == typeof(IHttpRequestBodyDetectionFeature))
{
feature = _currentIHttpRequestBodyDetectionFeature;
}
else if (key == typeof(IHttpResponseFeature))
{
feature = _currentIHttpResponseFeature;
@ -253,6 +259,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
_currentIHttpRequestFeature = value;
}
else if (key == typeof(IHttpRequestBodyDetectionFeature))
{
_currentIHttpRequestBodyDetectionFeature = value;
}
else if (key == typeof(IHttpResponseFeature))
{
_currentIHttpResponseFeature = value;
@ -371,6 +381,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
feature = (TFeature)_currentIHttpRequestFeature;
}
else if (typeof(TFeature) == typeof(IHttpRequestBodyDetectionFeature))
{
feature = (TFeature)_currentIHttpRequestBodyDetectionFeature;
}
else if (typeof(TFeature) == typeof(IHttpResponseFeature))
{
feature = (TFeature)_currentIHttpResponseFeature;
@ -495,6 +509,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
_currentIHttpRequestFeature = feature;
}
else if (typeof(TFeature) == typeof(IHttpRequestBodyDetectionFeature))
{
_currentIHttpRequestBodyDetectionFeature = feature;
}
else if (typeof(TFeature) == typeof(IHttpResponseFeature))
{
_currentIHttpResponseFeature = feature;
@ -611,6 +629,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
yield return new KeyValuePair<Type, object>(typeof(IHttpRequestFeature), _currentIHttpRequestFeature);
}
if (_currentIHttpRequestBodyDetectionFeature != null)
{
yield return new KeyValuePair<Type, object>(typeof(IHttpRequestBodyDetectionFeature), _currentIHttpRequestBodyDetectionFeature);
}
if (_currentIHttpResponseFeature != null)
{
yield return new KeyValuePair<Type, object>(typeof(IHttpResponseFeature), _currentIHttpResponseFeature);

View File

@ -36,6 +36,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
_upgradeStream = new HttpUpgradeStream(_request, _response);
}
public bool CanHaveBody { get; private set; }
public Stream Upgrade()
{
// causes writes to context.Response.Body to throw
@ -46,6 +48,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
public (Stream request, Stream response, PipeReader reader, PipeWriter writer) Start(MessageBody body)
{
CanHaveBody = !body.IsEmpty;
_requestReader.StartAcceptingReads(body);
_emptyRequestReader.StartAcceptingReads(MessageBody.ZeroContentLengthClose);
_responseWriter.StartAcceptingWrites();

View File

@ -116,6 +116,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
public void FeaturesSetByTypeSameAsGeneric()
{
_collection[typeof(IHttpRequestFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpRequestBodyDetectionFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpResponseFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpResponseBodyFeature)] = CreateHttp1Connection();
_collection[typeof(IRequestBodyPipeFeature)] = CreateHttp1Connection();
@ -139,6 +140,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
public void FeaturesSetByGenericSameAsByType()
{
_collection.Set<IHttpRequestFeature>(CreateHttp1Connection());
_collection.Set<IHttpRequestBodyDetectionFeature>(CreateHttp1Connection());
_collection.Set<IHttpResponseFeature>(CreateHttp1Connection());
_collection.Set<IHttpResponseBodyFeature>(CreateHttp1Connection());
_collection.Set<IRequestBodyPipeFeature>(CreateHttp1Connection());
@ -194,6 +196,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
private void CompareGenericGetterToIndexer()
{
Assert.Same(_collection.Get<IHttpRequestFeature>(), _collection[typeof(IHttpRequestFeature)]);
Assert.Same(_collection.Get<IHttpRequestBodyDetectionFeature>(), _collection[typeof(IHttpRequestBodyDetectionFeature)]);
Assert.Same(_collection.Get<IHttpResponseFeature>(), _collection[typeof(IHttpResponseFeature)]);
Assert.Same(_collection.Get<IHttpResponseBodyFeature>(), _collection[typeof(IHttpResponseBodyFeature)]);
Assert.Same(_collection.Get<IRequestBodyPipeFeature>(), _collection[typeof(IRequestBodyPipeFeature)]);

View File

@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
{
var request = httpContext.Request;
var response = httpContext.Response;
Assert.True(request.CanHaveBody());
while (true)
{
var buffer = new byte[8192];
@ -40,6 +41,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
{
var request = httpContext.Request;
var response = httpContext.Response;
Assert.True(request.CanHaveBody());
while (true)
{
var readResult = await request.BodyReader.ReadAsync();
@ -58,6 +60,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
{
var request = httpContext.Request;
var response = httpContext.Response;
Assert.True(request.CanHaveBody());
var data = new MemoryStream();
await request.Body.CopyToAsync(data);
var bytes = data.ToArray();
@ -174,6 +177,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
{
var response = httpContext.Response;
var request = httpContext.Request;
Assert.True(request.CanHaveBody());
Assert.Equal("POST", request.Method);
@ -229,6 +233,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
{
var response = httpContext.Response;
var request = httpContext.Request;
Assert.True(request.CanHaveBody());
var buffer = new byte[200];
@ -356,6 +361,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
{
var response = httpContext.Response;
var request = httpContext.Request;
Assert.True(request.CanHaveBody());
// The first request is chunked with no trailers.
if (requestsReceived == 0)
@ -652,6 +658,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
{
var response = httpContext.Response;
var request = httpContext.Request;
Assert.True(request.CanHaveBody());
var buffer = new byte[200];
@ -695,6 +702,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
{
var response = httpContext.Response;
var request = httpContext.Request;
Assert.True(request.CanHaveBody());
var buffer = new byte[200];

View File

@ -64,22 +64,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
}
[Fact]
public async Task HEADERS_Received_CustomMethod_Accepted()
[Theory]
[InlineData("GET")]
[InlineData("POST")]
[InlineData("PUT")]
[InlineData("PATCH")]
[InlineData("DELETE")]
[InlineData("CUSTOM")]
public async Task HEADERS_Received_KnownOrCustomMethods_Accepted(string method)
{
var headers = new[]
{
new KeyValuePair<string, string>(HeaderNames.Method, "Custom"),
new KeyValuePair<string, string>(HeaderNames.Method, method),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
};
await InitializeConnectionAsync(_echoMethod);
await InitializeConnectionAsync(_echoMethodNoBody);
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
withLength: 51,
withLength: 45 + method.Length,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@ -90,14 +96,146 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Equal(4, _decodedHeaders.Count);
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
Assert.Equal("Custom", _decodedHeaders["Method"]);
Assert.Equal("0", _decodedHeaders["content-length"]);
Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
Assert.Equal(method, _decodedHeaders["Method"]);
}
[Fact]
public async Task HEADERS_Received_HEADMethod_Accepted()
{
await InitializeConnectionAsync(_echoMethodNoBody);
var headers = new[]
{
new KeyValuePair<string, string>(HeaderNames.Method, "HEAD"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
};
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
withLength: 45,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
_hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this);
Assert.Equal(3, _decodedHeaders.Count);
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
Assert.Equal("HEAD", _decodedHeaders["Method"]);
}
[Theory]
[InlineData("GET")]
[InlineData("POST")]
[InlineData("PUT")]
[InlineData("PATCH")]
[InlineData("DELETE")]
[InlineData("CUSTOM")]
public async Task HEADERS_Received_MethodsWithContentLength_Accepted(string method)
{
var headers = new[]
{
new KeyValuePair<string, string>(HeaderNames.Method, method),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
new KeyValuePair<string, string>(HeaderNames.ContentLength, "11"),
};
await InitializeConnectionAsync(context =>
{
Assert.True(HttpMethods.Equals(method, context.Request.Method));
Assert.True(context.Request.CanHaveBody());
Assert.Equal(11, context.Request.ContentLength);
Assert.False(context.Request.Headers.ContainsKey(HeaderNames.TransferEncoding));
return context.Request.BodyReader.CopyToAsync(context.Response.BodyWriter);
});
await StartStreamAsync(1, headers, endStream: false);
await SendDataAsync(1, Encoding.UTF8.GetBytes("Hello World"), endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
withLength: 32,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS),
withStreamId: 1);
var dataFrame = await ExpectAsync(Http2FrameType.DATA,
withLength: 11,
withFlags: (byte)(Http2HeadersFrameFlags.NONE),
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
withLength: 0,
withFlags: (byte)(Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
_hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this);
Assert.Equal(2, _decodedHeaders.Count);
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
Assert.Equal("Hello World", Encoding.UTF8.GetString(dataFrame.Payload.Span));
}
[Theory]
[InlineData("GET")]
[InlineData("POST")]
[InlineData("PUT")]
[InlineData("PATCH")]
[InlineData("DELETE")]
[InlineData("CUSTOM")]
public async Task HEADERS_Received_MethodsWithoutContentLength_Accepted(string method)
{
var headers = new[]
{
new KeyValuePair<string, string>(HeaderNames.Method, method),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
};
await InitializeConnectionAsync(context =>
{
Assert.True(HttpMethods.Equals(method, context.Request.Method));
Assert.True(context.Request.CanHaveBody());
Assert.Null(context.Request.ContentLength);
Assert.False(context.Request.Headers.ContainsKey(HeaderNames.TransferEncoding));
return context.Request.BodyReader.CopyToAsync(context.Response.BodyWriter);
});
await StartStreamAsync(1, headers, endStream: false);
await SendDataAsync(1, Encoding.UTF8.GetBytes("Hello World"), endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
withLength: 32,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS),
withStreamId: 1);
var dataFrame = await ExpectAsync(Http2FrameType.DATA,
withLength: 11,
withFlags: (byte)(Http2HeadersFrameFlags.NONE),
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
withLength: 0,
withFlags: (byte)(Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
_hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this);
Assert.Equal(2, _decodedHeaders.Count);
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
Assert.Equal("Hello World", Encoding.UTF8.GetString(dataFrame.Payload.Span));
}
[Fact]
public async Task HEADERS_Received_CONNECTMethod_Accepted()
{
await InitializeConnectionAsync(_echoMethod);
await InitializeConnectionAsync(_echoMethodNoBody);
// :path and :scheme are not allowed, :authority is optional
var headers = new[] { new KeyValuePair<string, string>(HeaderNames.Method, "CONNECT") };

View File

@ -150,7 +150,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
protected readonly RequestDelegate _waitForAbortApplication;
protected readonly RequestDelegate _waitForAbortFlushingApplication;
protected readonly RequestDelegate _readRateApplication;
protected readonly RequestDelegate _echoMethod;
protected readonly RequestDelegate _echoMethodNoBody;
protected readonly RequestDelegate _echoHost;
protected readonly RequestDelegate _echoPath;
protected readonly RequestDelegate _appAbort;
@ -346,8 +346,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await stalledReadTask;
};
_echoMethod = context =>
_echoMethodNoBody = context =>
{
Assert.False(context.Request.CanHaveBody());
context.Response.Headers["Method"] = context.Request.Method;
return Task.CompletedTask;

View File

@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
await using (var server = new TestServer(async context =>
{
Assert.True(context.Request.CanHaveBody());
var buffer = new byte[1];
#pragma warning disable CS0618 // Type or member is obsolete
requestRejectedEx = await Assert.ThrowsAsync<BadHttpRequestException>(

View File

@ -1933,7 +1933,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
var request = httpContext.Request;
Assert.Equal("POST", request.Method);
Assert.True(request.CanHaveBody());
var readResult = await request.BodyReader.ReadAsync();
request.BodyReader.AdvanceTo(readResult.Buffer.End);

View File

@ -0,0 +1,15 @@
// 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 Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.Http
{
internal static class RequestExtensions
{
internal static bool? CanHaveBody(this HttpRequest request)
{
return request.HttpContext.Features.Get<IHttpRequestBodyDetectionFeature>()?.CanHaveBody;
}
}
}

View File

@ -12,6 +12,7 @@ namespace CodeGenerator
var alwaysFeatures = new[]
{
"IHttpRequestFeature",
"IHttpRequestBodyDetectionFeature",
"IHttpResponseFeature",
"IHttpResponseBodyFeature",
"IRequestBodyPipeFeature",
@ -58,6 +59,7 @@ namespace CodeGenerator
var implementedFeatures = new[]
{
"IHttpRequestFeature",
"IHttpRequestBodyDetectionFeature",
"IHttpResponseFeature",
"IHttpResponseBodyFeature",
"IRequestBodyPipeFeature",