diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.FeatureCollection.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.FeatureCollection.cs index b7dafbcdfa04..1fab0f694ad9 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.FeatureCollection.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.FeatureCollection.cs @@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core { internal partial class IISHttpContext : IFeatureCollection, IHttpRequestFeature, + IHttpRequestBodyDetectionFeature, IHttpResponseFeature, IHttpResponseBodyFeature, IHttpUpgradeFeature, @@ -141,6 +142,8 @@ Stream IHttpRequestFeature.Body set => RequestBody = value; } + bool IHttpRequestBodyDetectionFeature.CanHaveBody => RequestCanHaveBody; + int IHttpResponseFeature.StatusCode { get => StatusCode; diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.Features.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.Features.cs index e918e18f8450..96ba46388908 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.Features.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.Features.cs @@ -9,6 +9,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core internal partial class IISHttpContext { private static readonly Type IHttpRequestFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpRequestFeature); + private static readonly Type IHttpRequestBodyDetectionFeature = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpRequestBodyDetectionFeature); private static readonly Type IHttpResponseFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpResponseFeature); private static readonly Type IHttpResponseBodyFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature); private static readonly Type IHttpRequestIdentifierFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpRequestIdentifierFeature); @@ -32,6 +33,7 @@ internal partial class IISHttpContext private static readonly Type IHttpResetFeature = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpResetFeature); private object _currentIHttpRequestFeature; + private object _currentIHttpRequestBodyDetectionFeature; private object _currentIHttpResponseFeature; private object _currentIHttpResponseBodyFeature; private object _currentIHttpRequestIdentifierFeature; @@ -56,6 +58,7 @@ internal partial class IISHttpContext private void Initialize() { _currentIHttpRequestFeature = this; + _currentIHttpRequestBodyDetectionFeature = this; _currentIHttpResponseFeature = this; _currentIHttpResponseBodyFeature = this; _currentIHttpUpgradeFeature = this; @@ -77,6 +80,10 @@ internal object FastFeatureGet(Type key) { return _currentIHttpRequestFeature; } + if (key == IHttpRequestBodyDetectionFeature) + { + return _currentIHttpRequestBodyDetectionFeature; + } if (key == IHttpResponseFeatureType) { return _currentIHttpResponseFeature; @@ -174,6 +181,11 @@ internal void FastFeatureSet(Type key, object feature) _currentIHttpRequestFeature = feature; return; } + if (key == IHttpRequestBodyDetectionFeature) + { + _currentIHttpRequestBodyDetectionFeature = feature; + return; + } if (key == IHttpResponseFeatureType) { _currentIHttpResponseFeature = feature; @@ -284,6 +296,10 @@ private IEnumerable> FastEnumerable() { yield return new KeyValuePair(IHttpRequestFeatureType, _currentIHttpRequestFeature as global::Microsoft.AspNetCore.Http.Features.IHttpRequestFeature); } + if (_currentIHttpRequestBodyDetectionFeature != null) + { + yield return new KeyValuePair(IHttpRequestBodyDetectionFeature, _currentIHttpRequestBodyDetectionFeature as global::Microsoft.AspNetCore.Http.Features.IHttpRequestBodyDetectionFeature); + } if (_currentIHttpResponseFeature != null) { yield return new KeyValuePair(IHttpResponseFeatureType, _currentIHttpResponseFeature as global::Microsoft.AspNetCore.Http.Features.IHttpResponseFeature); diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs index cded2c724f68..17796ae354b5 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs @@ -108,6 +108,7 @@ internal unsafe IISHttpContext( public string TraceIdentifier { get; set; } public ClaimsPrincipal User { get; set; } internal WindowsPrincipal WindowsUser { get; set; } + internal bool RequestCanHaveBody { get; private set; } public Stream RequestBody { get; set; } public Stream ResponseBody { get; set; } public PipeWriter ResponsePipeWrapper { get; set; } @@ -165,6 +166,8 @@ protected void InitializeContext() RequestHeaders = new RequestHeaders(this); HttpResponseHeaders = new HeaderCollection(); ResponseHeaders = HttpResponseHeaders; + // Request headers can be modified by the app, read these first. + RequestCanHaveBody = CheckRequestCanHaveBody(); if (_options.ForwardWindowsAuthentication) { @@ -250,6 +253,20 @@ public string ReasonPhrase internal IISHttpServer Server => _server; + private bool CheckRequestCanHaveBody() + { + // Http/1.x requests with bodies require either a Content-Length or Transfer-Encoding header. + // Note Http.Sys adds the Transfer-Encoding: chunked header to HTTP/2 requests with bodies for back compat. + // Transfer-Encoding takes priority over Content-Length. + string transferEncoding = RequestHeaders[HttpKnownHeaderNames.TransferEncoding]; + if (string.Equals("chunked", transferEncoding?.Trim(), StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + return RequestHeaders.ContentLength.GetValueOrDefault() > 0; + } + private async Task InitializeResponse(bool flushHeaders) { await FireOnStarting(); diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/Http2Tests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/Http2Tests.cs new file mode 100644 index 000000000000..d6371821a1ad --- /dev/null +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/Http2Tests.cs @@ -0,0 +1,348 @@ +// 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 System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http2Cat; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting.Common; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.InProcess +{ + [Collection(PublishedSitesCollection.Name)] + public class Http2Tests : IISFunctionalTestBase + { + // TODO: Remove when the regression is fixed. + // https://github.com/dotnet/aspnetcore/issues/23164#issuecomment-652646163 + private static readonly Version Win10_Regressed_DataFrame = new Version(10, 0, 20145, 0); + + public static readonly IEnumerable> Headers = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Scheme, "https"), + new KeyValuePair(HeaderNames.Authority, "localhost:443"), + new KeyValuePair("user-agent", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:54.0) Gecko/20100101 Firefox/54.0"), + new KeyValuePair("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"), + new KeyValuePair("accept-language", "en-US,en;q=0.5"), + new KeyValuePair("accept-encoding", "gzip, deflate, br"), + new KeyValuePair("upgrade-insecure-requests", "1"), + }; + + public Http2Tests(PublishedSitesFixture fixture) : base(fixture) + { + } + + [ConditionalTheory] + [InlineData("GET")] + [InlineData("HEAD")] + [InlineData("PATCH")] + [InlineData("DELETE")] + [InlineData("CUSTOM")] + [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10, SkipReason = "Http2 requires Win10")] + public async Task Http2_MethodsRequestWithoutData_Success(string method) + { + var deploymentParameters = GetHttpsDeploymentParameters(); + var deploymentResult = await DeployAsync(deploymentParameters); + + await new HostBuilder() + .UseHttp2Cat(deploymentResult.ApplicationBaseUri, async h2Connection => + { + await h2Connection.InitializeConnectionAsync(); + + h2Connection.Logger.LogInformation("Initialized http2 connection. Starting stream 1."); + + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, method), + new KeyValuePair(HeaderNames.Path, "/Http2_MethodsRequestWithoutData_Success"), + new KeyValuePair(HeaderNames.Scheme, "https"), + new KeyValuePair(HeaderNames.Authority, "localhost:443"), + }; + + 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("POST")] + [InlineData("PUT")] + [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10, SkipReason = "Http2 requires Win10")] + public async Task Http2_PostRequestWithoutData_LengthRequired(string method) + { + var deploymentParameters = GetHttpsDeploymentParameters(); + var deploymentResult = await DeployAsync(deploymentParameters); + + await new HostBuilder() + .UseHttp2Cat(deploymentResult.ApplicationBaseUri, async h2Connection => + { + await h2Connection.InitializeConnectionAsync(); + + h2Connection.Logger.LogInformation("Initialized http2 connection. Starting stream 1."); + + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, method), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "https"), + new KeyValuePair(HeaderNames.Authority, "localhost:443"), + }; + + 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")] Reset with code HTTP_1_1_REQUIRED + [InlineData("POST")] + [InlineData("PUT")] + [InlineData("PATCH")] + [InlineData("DELETE")] + [InlineData("CUSTOM")] + [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H1, SkipReason = "Http2 requires Win10, and older versions of Win10 send some odd empty data frames.")] + public async Task Http2_RequestWithDataAndContentLength_Success(string method) + { + var deploymentParameters = GetHttpsDeploymentParameters(); + var deploymentResult = await DeployAsync(deploymentParameters); + + await new HostBuilder() + .UseHttp2Cat(deploymentResult.ApplicationBaseUri, async h2Connection => + { + await h2Connection.InitializeConnectionAsync(); + + h2Connection.Logger.LogInformation("Initialized http2 connection. Starting stream 1."); + + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, method), + new KeyValuePair(HeaderNames.Path, "/Http2_RequestWithDataAndContentLength_Success"), + new KeyValuePair(HeaderNames.Scheme, "https"), + new KeyValuePair(HeaderNames.Authority, "localhost:443"), + new KeyValuePair(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(); + Assert.Equal(Http2FrameType.DATA, dataFrame.Type); + Assert.Equal(1, dataFrame.StreamId); + + // Some versions send an empty data frame first. + if (dataFrame.PayloadLength == 0) + { + Assert.False(dataFrame.DataEndStream); + dataFrame = await h2Connection.ReceiveFrameAsync(); + Assert.Equal(Http2FrameType.DATA, dataFrame.Type); + Assert.Equal(1, dataFrame.StreamId); + } + + Assert.Equal(11, dataFrame.PayloadLength); + Assert.Equal("Hello World", Encoding.UTF8.GetString(dataFrame.Payload.Span)); + + if (!dataFrame.DataEndStream) + { + 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_20H1, SkipReason = "Http2 requires Win10, and older versions of Win10 send some odd empty data frames.")] + public async Task Http2_RequestWithDataAndNoContentLength_Success(string method) + { + var deploymentParameters = GetHttpsDeploymentParameters(); + var deploymentResult = await DeployAsync(deploymentParameters); + + await new HostBuilder() + .UseHttp2Cat(deploymentResult.ApplicationBaseUri, async h2Connection => + { + await h2Connection.InitializeConnectionAsync(); + + h2Connection.Logger.LogInformation("Initialized http2 connection. Starting stream 1."); + + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, method), + new KeyValuePair(HeaderNames.Path, "/Http2_RequestWithDataAndNoContentLength_Success"), + new KeyValuePair(HeaderNames.Scheme, "https"), + new KeyValuePair(HeaderNames.Authority, "localhost:443"), + }; + + 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(); + Assert.Equal(Http2FrameType.DATA, dataFrame.Type); + Assert.Equal(1, dataFrame.StreamId); + + // Some versions send an empty data frame first. + if (dataFrame.PayloadLength == 0) + { + Assert.False(dataFrame.DataEndStream); + dataFrame = await h2Connection.ReceiveFrameAsync(); + Assert.Equal(Http2FrameType.DATA, dataFrame.Type); + Assert.Equal(1, dataFrame.StreamId); + } + + Assert.Equal(11, dataFrame.PayloadLength); + Assert.Equal("Hello World", Encoding.UTF8.GetString(dataFrame.Payload.Span)); + + if (!dataFrame.DataEndStream) + { + 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_20H1, SkipReason = "Http2 requires Win10, and older versions of Win10 send some odd empty data frames.")] + public async Task Http2_ResponseWithData_Success() + { + var deploymentParameters = GetHttpsDeploymentParameters(); + var deploymentResult = await DeployAsync(deploymentParameters); + + await new HostBuilder() + .UseHttp2Cat(deploymentResult.ApplicationBaseUri, async h2Connection => + { + await h2Connection.InitializeConnectionAsync(); + + h2Connection.Logger.LogInformation("Initialized http2 connection. Starting stream 1."); + + await h2Connection.StartStreamAsync(1, GetHeaders("/Http2_ResponseWithData_Success"), endStream: true); + + await h2Connection.ReceiveHeadersAsync(1, decodedHeaders => + { + Assert.Equal("200", decodedHeaders[HeaderNames.Status]); + }); + + var dataFrame = await h2Connection.ReceiveFrameAsync(); + Assert.Equal(Http2FrameType.DATA, dataFrame.Type); + Assert.Equal(1, dataFrame.StreamId); + + // Some versions send an empty data frame first. + if (dataFrame.PayloadLength == 0) + { + Assert.False(dataFrame.DataEndStream); + dataFrame = await h2Connection.ReceiveFrameAsync(); + Assert.Equal(Http2FrameType.DATA, dataFrame.Type); + Assert.Equal(1, dataFrame.StreamId); + } + + Assert.Equal(11, dataFrame.PayloadLength); + Assert.Equal("Hello World", Encoding.UTF8.GetString(dataFrame.Payload.Span)); + + if (!dataFrame.DataEndStream) + { + dataFrame = await h2Connection.ReceiveFrameAsync(); + Http2Utilities.VerifyDataFrame(dataFrame, 1, endOfStream: true, length: 0); + } + + h2Connection.Logger.LogInformation("Connection stopped."); + }) + .Build().RunAsync(); + } + + private static List> GetHeaders(string path) + { + var headers = Headers.ToList(); + + var kvp = new KeyValuePair(HeaderNames.Path, path); + headers.Add(kvp); + return headers; + } + + private IISDeploymentParameters GetHttpsDeploymentParameters() + { + var port = TestPortHelper.GetNextSSLPort(); + var deploymentParameters = Fixture.GetBaseDeploymentParameters(); + deploymentParameters.ApplicationBaseUriHint = $"https://localhost:{port}/"; + deploymentParameters.AddHttpsToServerConfig(); + return deploymentParameters; + } + } +} diff --git a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Helpers.cs b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Helpers.cs new file mode 100644 index 000000000000..4b7dd0b81fb0 --- /dev/null +++ b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Helpers.cs @@ -0,0 +1,20 @@ +// 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; +using Microsoft.AspNetCore.Http.Features; + +namespace TestSite +{ + public static class Helpers + { + internal static bool? CanHaveBody(this HttpRequest request) + { +#if FORWARDCOMPAT + return null; +#else + return request.HttpContext.Features.Get()?.CanHaveBody; +#endif + } + } +} diff --git a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs index 0d23030f82ce..7f963a7a52c8 100644 --- a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs +++ b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs @@ -28,7 +28,6 @@ using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Xunit; -using HttpFeatures = Microsoft.AspNetCore.Http.Features; namespace TestSite { @@ -470,6 +469,9 @@ private async Task ReadAndWriteSynchronously(HttpContext ctx) private async Task ReadRequestBody(HttpContext ctx) { +#if !FORWARDCOMPAT + Assert.True(ctx.Request.CanHaveBody()); +#endif var readBuffer = new byte[1]; var result = await ctx.Request.Body.ReadAsync(readBuffer, 0, 1); while (result != 0) @@ -480,6 +482,9 @@ private async Task ReadRequestBody(HttpContext ctx) private async Task ReadRequestBodyLarger(HttpContext ctx) { +#if !FORWARDCOMPAT + Assert.True(ctx.Request.CanHaveBody()); +#endif var readBuffer = new byte[4096]; var result = await ctx.Request.Body.ReadAsync(readBuffer, 0, 4096); while (result != 0) @@ -515,6 +520,9 @@ private async Task WaitForAppToStartShuttingDown(HttpContext ctx) private async Task ReadFullBody(HttpContext ctx) { +#if !FORWARDCOMPAT + Assert.True(ctx.Request.CanHaveBody()); +#endif await ReadRequestBody(ctx); ctx.Response.ContentLength = 9; await ctx.Response.WriteAsync("Completed"); @@ -530,6 +538,9 @@ private async Task WriteManyTimesToResponseBody(HttpContext ctx) private async Task ReadAndWriteEcho(HttpContext ctx) { +#if !FORWARDCOMPAT + Assert.True(ctx.Request.CanHaveBody()); +#endif var readBuffer = new byte[4096]; var result = await ctx.Request.Body.ReadAsync(readBuffer, 0, readBuffer.Length); while (result != 0) @@ -540,6 +551,9 @@ private async Task ReadAndWriteEcho(HttpContext ctx) } private async Task ReadAndFlushEcho(HttpContext ctx) { +#if !FORWARDCOMPAT + Assert.True(ctx.Request.CanHaveBody()); +#endif var readBuffer = new byte[4096]; var result = await ctx.Request.Body.ReadAsync(readBuffer, 0, readBuffer.Length); while (result != 0) @@ -552,6 +566,9 @@ private async Task ReadAndFlushEcho(HttpContext ctx) private async Task ReadAndWriteEchoLines(HttpContext ctx) { +#if !FORWARDCOMPAT + Assert.True(ctx.Request.CanHaveBody()); +#endif if (ctx.Request.Headers.TryGetValue("Response-Content-Type", out var contentType)) { ctx.Response.ContentType = contentType; @@ -581,6 +598,7 @@ private async Task ReadAndWriteEchoLinesNoBuffering(HttpContext ctx) #else var feature = ctx.Features.Get(); feature.DisableBuffering(); + Assert.True(ctx.Request.CanHaveBody()); #endif if (ctx.Request.Headers.TryGetValue("Response-Content-Type", out var contentType)) @@ -605,6 +623,9 @@ private async Task ReadAndWriteEchoLinesNoBuffering(HttpContext ctx) private async Task ReadPartialBody(HttpContext ctx) { +#if !FORWARDCOMPAT + Assert.True(ctx.Request.CanHaveBody()); +#endif var data = new byte[5]; var count = 0; do @@ -655,6 +676,9 @@ private async Task WriteResponseBodyAFewTimes(HttpContext ctx) private async Task ReadAndWriteCopyToAsync(HttpContext ctx) { +#if !FORWARDCOMPAT + Assert.True(ctx.Request.CanHaveBody()); +#endif await ctx.Request.Body.CopyToAsync(ctx.Response.Body); } @@ -1313,6 +1337,10 @@ public async Task Reset_DuringRequestBody_Resets(HttpContext httpContext) var feature = httpContext.Features.Get(); Assert.NotNull(feature); +#if !FORWARDCOMPAT + Assert.True(httpContext.Request.CanHaveBody()); +#endif + var read = await httpContext.Request.Body.ReadAsync(new byte[10], 0, 10); Assert.Equal(10, read); @@ -1454,6 +1482,46 @@ public async Task Reset_CompleteAsyncDuringRequestBody_Resets(HttpContext httpCo await Assert.ThrowsAsync(() => readTask); } + public Task Http2_MethodsRequestWithoutData_Success(HttpContext httpContext) + { + Assert.Equal("HTTP/2", httpContext.Request.Protocol); +#if !FORWARDCOMPAT + Assert.False(httpContext.Request.CanHaveBody()); +#endif + Assert.Null(httpContext.Request.ContentLength); + Assert.False(httpContext.Request.Headers.ContainsKey(HeaderNames.TransferEncoding)); + return Task.CompletedTask; + } + + public Task Http2_RequestWithDataAndContentLength_Success(HttpContext httpContext) + { + Assert.Equal("HTTP/2", httpContext.Request.Protocol); +#if !FORWARDCOMPAT + Assert.True(httpContext.Request.CanHaveBody()); +#endif + Assert.Equal(11, httpContext.Request.ContentLength); + Assert.False(httpContext.Request.Headers.ContainsKey(HeaderNames.TransferEncoding)); + return httpContext.Request.Body.CopyToAsync(httpContext.Response.Body); + } + + public Task Http2_RequestWithDataAndNoContentLength_Success(HttpContext httpContext) + { + Assert.Equal("HTTP/2", httpContext.Request.Protocol); +#if !FORWARDCOMPAT + Assert.True(httpContext.Request.CanHaveBody()); +#endif + 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); + } + + public Task Http2_ResponseWithData_Success(HttpContext httpContext) + { + Assert.Equal("HTTP/2", httpContext.Request.Protocol); + return httpContext.Response.WriteAsync("Hello World"); + } + public Task IncreaseRequestLimit(HttpContext httpContext) { var maxRequestBodySizeFeature = httpContext.Features.Get(); @@ -1498,5 +1566,5 @@ public Task IncreaseRequestLimit(HttpContext httpContext) HeaderNames.ContentEncoding, HeaderNames.ContentType, HeaderNames.ContentRange, HeaderNames.Trailer }; #endif + } } -} diff --git a/src/Tools/Extensions.ApiDescription.Server/src/build/Microsoft.Extensions.ApiDescription.Server.targets b/src/Tools/Extensions.ApiDescription.Server/src/build/Microsoft.Extensions.ApiDescription.Server.targets index 45410bca5387..3b9bc09c38e9 100644 --- a/src/Tools/Extensions.ApiDescription.Server/src/build/Microsoft.Extensions.ApiDescription.Server.targets +++ b/src/Tools/Extensions.ApiDescription.Server/src/build/Microsoft.Extensions.ApiDescription.Server.targets @@ -3,7 +3,7 @@ <_IsMicrosoftNETCoreApp20OrOlder>false <_IsMicrosoftNETCoreApp20OrOlder Condition=" '$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND - $([MSBuild]::VersionLessThanOrEquals('$(TargetFrameworkVersion)', '2.0') ">true + $([MSBuild]::VersionLessThanOrEquals('$(TargetFrameworkVersion)', '2.0')) ">true true