IIS: Identify if a request has a body (#25381)
This commit is contained in:
parent
c1b0fb5765
commit
b5515a8dac
|
|
@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
|
||||||
{
|
{
|
||||||
internal partial class IISHttpContext : IFeatureCollection,
|
internal partial class IISHttpContext : IFeatureCollection,
|
||||||
IHttpRequestFeature,
|
IHttpRequestFeature,
|
||||||
|
IHttpRequestBodyDetectionFeature,
|
||||||
IHttpResponseFeature,
|
IHttpResponseFeature,
|
||||||
IHttpResponseBodyFeature,
|
IHttpResponseBodyFeature,
|
||||||
IHttpUpgradeFeature,
|
IHttpUpgradeFeature,
|
||||||
|
|
@ -141,6 +142,8 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
|
||||||
set => RequestBody = value;
|
set => RequestBody = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IHttpRequestBodyDetectionFeature.CanHaveBody => RequestCanHaveBody;
|
||||||
|
|
||||||
int IHttpResponseFeature.StatusCode
|
int IHttpResponseFeature.StatusCode
|
||||||
{
|
{
|
||||||
get => StatusCode;
|
get => StatusCode;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
|
||||||
internal partial class IISHttpContext
|
internal partial class IISHttpContext
|
||||||
{
|
{
|
||||||
private static readonly Type IHttpRequestFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpRequestFeature);
|
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 IHttpResponseFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpResponseFeature);
|
||||||
private static readonly Type IHttpResponseBodyFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature);
|
private static readonly Type IHttpResponseBodyFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature);
|
||||||
private static readonly Type IHttpRequestIdentifierFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpRequestIdentifierFeature);
|
private static readonly Type IHttpRequestIdentifierFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpRequestIdentifierFeature);
|
||||||
|
|
@ -32,6 +33,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
|
||||||
private static readonly Type IHttpResetFeature = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpResetFeature);
|
private static readonly Type IHttpResetFeature = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpResetFeature);
|
||||||
|
|
||||||
private object _currentIHttpRequestFeature;
|
private object _currentIHttpRequestFeature;
|
||||||
|
private object _currentIHttpRequestBodyDetectionFeature;
|
||||||
private object _currentIHttpResponseFeature;
|
private object _currentIHttpResponseFeature;
|
||||||
private object _currentIHttpResponseBodyFeature;
|
private object _currentIHttpResponseBodyFeature;
|
||||||
private object _currentIHttpRequestIdentifierFeature;
|
private object _currentIHttpRequestIdentifierFeature;
|
||||||
|
|
@ -56,6 +58,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
|
||||||
private void Initialize()
|
private void Initialize()
|
||||||
{
|
{
|
||||||
_currentIHttpRequestFeature = this;
|
_currentIHttpRequestFeature = this;
|
||||||
|
_currentIHttpRequestBodyDetectionFeature = this;
|
||||||
_currentIHttpResponseFeature = this;
|
_currentIHttpResponseFeature = this;
|
||||||
_currentIHttpResponseBodyFeature = this;
|
_currentIHttpResponseBodyFeature = this;
|
||||||
_currentIHttpUpgradeFeature = this;
|
_currentIHttpUpgradeFeature = this;
|
||||||
|
|
@ -77,6 +80,10 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
|
||||||
{
|
{
|
||||||
return _currentIHttpRequestFeature;
|
return _currentIHttpRequestFeature;
|
||||||
}
|
}
|
||||||
|
if (key == IHttpRequestBodyDetectionFeature)
|
||||||
|
{
|
||||||
|
return _currentIHttpRequestBodyDetectionFeature;
|
||||||
|
}
|
||||||
if (key == IHttpResponseFeatureType)
|
if (key == IHttpResponseFeatureType)
|
||||||
{
|
{
|
||||||
return _currentIHttpResponseFeature;
|
return _currentIHttpResponseFeature;
|
||||||
|
|
@ -174,6 +181,11 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
|
||||||
_currentIHttpRequestFeature = feature;
|
_currentIHttpRequestFeature = feature;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (key == IHttpRequestBodyDetectionFeature)
|
||||||
|
{
|
||||||
|
_currentIHttpRequestBodyDetectionFeature = feature;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (key == IHttpResponseFeatureType)
|
if (key == IHttpResponseFeatureType)
|
||||||
{
|
{
|
||||||
_currentIHttpResponseFeature = feature;
|
_currentIHttpResponseFeature = feature;
|
||||||
|
|
@ -284,6 +296,10 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
|
||||||
{
|
{
|
||||||
yield return new KeyValuePair<Type, object>(IHttpRequestFeatureType, _currentIHttpRequestFeature as global::Microsoft.AspNetCore.Http.Features.IHttpRequestFeature);
|
yield return new KeyValuePair<Type, object>(IHttpRequestFeatureType, _currentIHttpRequestFeature as global::Microsoft.AspNetCore.Http.Features.IHttpRequestFeature);
|
||||||
}
|
}
|
||||||
|
if (_currentIHttpRequestBodyDetectionFeature != null)
|
||||||
|
{
|
||||||
|
yield return new KeyValuePair<Type, object>(IHttpRequestBodyDetectionFeature, _currentIHttpRequestBodyDetectionFeature as global::Microsoft.AspNetCore.Http.Features.IHttpRequestBodyDetectionFeature);
|
||||||
|
}
|
||||||
if (_currentIHttpResponseFeature != null)
|
if (_currentIHttpResponseFeature != null)
|
||||||
{
|
{
|
||||||
yield return new KeyValuePair<Type, object>(IHttpResponseFeatureType, _currentIHttpResponseFeature as global::Microsoft.AspNetCore.Http.Features.IHttpResponseFeature);
|
yield return new KeyValuePair<Type, object>(IHttpResponseFeatureType, _currentIHttpResponseFeature as global::Microsoft.AspNetCore.Http.Features.IHttpResponseFeature);
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
|
||||||
public string TraceIdentifier { get; set; }
|
public string TraceIdentifier { get; set; }
|
||||||
public ClaimsPrincipal User { get; set; }
|
public ClaimsPrincipal User { get; set; }
|
||||||
internal WindowsPrincipal WindowsUser { get; set; }
|
internal WindowsPrincipal WindowsUser { get; set; }
|
||||||
|
internal bool RequestCanHaveBody { get; private set; }
|
||||||
public Stream RequestBody { get; set; }
|
public Stream RequestBody { get; set; }
|
||||||
public Stream ResponseBody { get; set; }
|
public Stream ResponseBody { get; set; }
|
||||||
public PipeWriter ResponsePipeWrapper { get; set; }
|
public PipeWriter ResponsePipeWrapper { get; set; }
|
||||||
|
|
@ -165,6 +166,8 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
|
||||||
RequestHeaders = new RequestHeaders(this);
|
RequestHeaders = new RequestHeaders(this);
|
||||||
HttpResponseHeaders = new HeaderCollection();
|
HttpResponseHeaders = new HeaderCollection();
|
||||||
ResponseHeaders = HttpResponseHeaders;
|
ResponseHeaders = HttpResponseHeaders;
|
||||||
|
// Request headers can be modified by the app, read these first.
|
||||||
|
RequestCanHaveBody = CheckRequestCanHaveBody();
|
||||||
|
|
||||||
if (_options.ForwardWindowsAuthentication)
|
if (_options.ForwardWindowsAuthentication)
|
||||||
{
|
{
|
||||||
|
|
@ -250,6 +253,20 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
|
||||||
|
|
||||||
internal IISHttpServer Server => _server;
|
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)
|
private async Task InitializeResponse(bool flushHeaders)
|
||||||
{
|
{
|
||||||
await FireOnStarting();
|
await FireOnStarting();
|
||||||
|
|
|
||||||
|
|
@ -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<KeyValuePair<string, string>> Headers = new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Scheme, "https"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:443"),
|
||||||
|
new KeyValuePair<string, string>("user-agent", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:54.0) Gecko/20100101 Firefox/54.0"),
|
||||||
|
new KeyValuePair<string, string>("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"),
|
||||||
|
new KeyValuePair<string, string>("accept-language", "en-US,en;q=0.5"),
|
||||||
|
new KeyValuePair<string, string>("accept-encoding", "gzip, deflate, br"),
|
||||||
|
new KeyValuePair<string, string>("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<string, string>(HeaderNames.Method, method),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Path, "/Http2_MethodsRequestWithoutData_Success"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Scheme, "https"),
|
||||||
|
new KeyValuePair<string, string>(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<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: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<string, string>(HeaderNames.Method, method),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Path, "/Http2_RequestWithDataAndContentLength_Success"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Scheme, "https"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:443"),
|
||||||
|
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();
|
||||||
|
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<string, string>(HeaderNames.Method, method),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Path, "/Http2_RequestWithDataAndNoContentLength_Success"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Scheme, "https"),
|
||||||
|
new KeyValuePair<string, string>(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<KeyValuePair<string, string>> GetHeaders(string path)
|
||||||
|
{
|
||||||
|
var headers = Headers.ToList();
|
||||||
|
|
||||||
|
var kvp = new KeyValuePair<string, string>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<IHttpRequestBodyDetectionFeature>()?.CanHaveBody;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,7 +28,6 @@ using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using HttpFeatures = Microsoft.AspNetCore.Http.Features;
|
|
||||||
|
|
||||||
namespace TestSite
|
namespace TestSite
|
||||||
{
|
{
|
||||||
|
|
@ -470,6 +469,9 @@ namespace TestSite
|
||||||
|
|
||||||
private async Task ReadRequestBody(HttpContext ctx)
|
private async Task ReadRequestBody(HttpContext ctx)
|
||||||
{
|
{
|
||||||
|
#if !FORWARDCOMPAT
|
||||||
|
Assert.True(ctx.Request.CanHaveBody());
|
||||||
|
#endif
|
||||||
var readBuffer = new byte[1];
|
var readBuffer = new byte[1];
|
||||||
var result = await ctx.Request.Body.ReadAsync(readBuffer, 0, 1);
|
var result = await ctx.Request.Body.ReadAsync(readBuffer, 0, 1);
|
||||||
while (result != 0)
|
while (result != 0)
|
||||||
|
|
@ -480,6 +482,9 @@ namespace TestSite
|
||||||
|
|
||||||
private async Task ReadRequestBodyLarger(HttpContext ctx)
|
private async Task ReadRequestBodyLarger(HttpContext ctx)
|
||||||
{
|
{
|
||||||
|
#if !FORWARDCOMPAT
|
||||||
|
Assert.True(ctx.Request.CanHaveBody());
|
||||||
|
#endif
|
||||||
var readBuffer = new byte[4096];
|
var readBuffer = new byte[4096];
|
||||||
var result = await ctx.Request.Body.ReadAsync(readBuffer, 0, 4096);
|
var result = await ctx.Request.Body.ReadAsync(readBuffer, 0, 4096);
|
||||||
while (result != 0)
|
while (result != 0)
|
||||||
|
|
@ -515,6 +520,9 @@ namespace TestSite
|
||||||
|
|
||||||
private async Task ReadFullBody(HttpContext ctx)
|
private async Task ReadFullBody(HttpContext ctx)
|
||||||
{
|
{
|
||||||
|
#if !FORWARDCOMPAT
|
||||||
|
Assert.True(ctx.Request.CanHaveBody());
|
||||||
|
#endif
|
||||||
await ReadRequestBody(ctx);
|
await ReadRequestBody(ctx);
|
||||||
ctx.Response.ContentLength = 9;
|
ctx.Response.ContentLength = 9;
|
||||||
await ctx.Response.WriteAsync("Completed");
|
await ctx.Response.WriteAsync("Completed");
|
||||||
|
|
@ -530,6 +538,9 @@ namespace TestSite
|
||||||
|
|
||||||
private async Task ReadAndWriteEcho(HttpContext ctx)
|
private async Task ReadAndWriteEcho(HttpContext ctx)
|
||||||
{
|
{
|
||||||
|
#if !FORWARDCOMPAT
|
||||||
|
Assert.True(ctx.Request.CanHaveBody());
|
||||||
|
#endif
|
||||||
var readBuffer = new byte[4096];
|
var readBuffer = new byte[4096];
|
||||||
var result = await ctx.Request.Body.ReadAsync(readBuffer, 0, readBuffer.Length);
|
var result = await ctx.Request.Body.ReadAsync(readBuffer, 0, readBuffer.Length);
|
||||||
while (result != 0)
|
while (result != 0)
|
||||||
|
|
@ -540,6 +551,9 @@ namespace TestSite
|
||||||
}
|
}
|
||||||
private async Task ReadAndFlushEcho(HttpContext ctx)
|
private async Task ReadAndFlushEcho(HttpContext ctx)
|
||||||
{
|
{
|
||||||
|
#if !FORWARDCOMPAT
|
||||||
|
Assert.True(ctx.Request.CanHaveBody());
|
||||||
|
#endif
|
||||||
var readBuffer = new byte[4096];
|
var readBuffer = new byte[4096];
|
||||||
var result = await ctx.Request.Body.ReadAsync(readBuffer, 0, readBuffer.Length);
|
var result = await ctx.Request.Body.ReadAsync(readBuffer, 0, readBuffer.Length);
|
||||||
while (result != 0)
|
while (result != 0)
|
||||||
|
|
@ -552,6 +566,9 @@ namespace TestSite
|
||||||
|
|
||||||
private async Task ReadAndWriteEchoLines(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))
|
if (ctx.Request.Headers.TryGetValue("Response-Content-Type", out var contentType))
|
||||||
{
|
{
|
||||||
ctx.Response.ContentType = contentType;
|
ctx.Response.ContentType = contentType;
|
||||||
|
|
@ -581,6 +598,7 @@ namespace TestSite
|
||||||
#else
|
#else
|
||||||
var feature = ctx.Features.Get<IHttpResponseBodyFeature>();
|
var feature = ctx.Features.Get<IHttpResponseBodyFeature>();
|
||||||
feature.DisableBuffering();
|
feature.DisableBuffering();
|
||||||
|
Assert.True(ctx.Request.CanHaveBody());
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (ctx.Request.Headers.TryGetValue("Response-Content-Type", out var contentType))
|
if (ctx.Request.Headers.TryGetValue("Response-Content-Type", out var contentType))
|
||||||
|
|
@ -605,6 +623,9 @@ namespace TestSite
|
||||||
|
|
||||||
private async Task ReadPartialBody(HttpContext ctx)
|
private async Task ReadPartialBody(HttpContext ctx)
|
||||||
{
|
{
|
||||||
|
#if !FORWARDCOMPAT
|
||||||
|
Assert.True(ctx.Request.CanHaveBody());
|
||||||
|
#endif
|
||||||
var data = new byte[5];
|
var data = new byte[5];
|
||||||
var count = 0;
|
var count = 0;
|
||||||
do
|
do
|
||||||
|
|
@ -655,6 +676,9 @@ namespace TestSite
|
||||||
|
|
||||||
private async Task ReadAndWriteCopyToAsync(HttpContext ctx)
|
private async Task ReadAndWriteCopyToAsync(HttpContext ctx)
|
||||||
{
|
{
|
||||||
|
#if !FORWARDCOMPAT
|
||||||
|
Assert.True(ctx.Request.CanHaveBody());
|
||||||
|
#endif
|
||||||
await ctx.Request.Body.CopyToAsync(ctx.Response.Body);
|
await ctx.Request.Body.CopyToAsync(ctx.Response.Body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1313,6 +1337,10 @@ namespace TestSite
|
||||||
var feature = httpContext.Features.Get<IHttpResetFeature>();
|
var feature = httpContext.Features.Get<IHttpResetFeature>();
|
||||||
Assert.NotNull(feature);
|
Assert.NotNull(feature);
|
||||||
|
|
||||||
|
#if !FORWARDCOMPAT
|
||||||
|
Assert.True(httpContext.Request.CanHaveBody());
|
||||||
|
#endif
|
||||||
|
|
||||||
var read = await httpContext.Request.Body.ReadAsync(new byte[10], 0, 10);
|
var read = await httpContext.Request.Body.ReadAsync(new byte[10], 0, 10);
|
||||||
Assert.Equal(10, read);
|
Assert.Equal(10, read);
|
||||||
|
|
||||||
|
|
@ -1454,6 +1482,46 @@ namespace TestSite
|
||||||
await Assert.ThrowsAsync<IOException>(() => readTask);
|
await Assert.ThrowsAsync<IOException>(() => 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)
|
public Task IncreaseRequestLimit(HttpContext httpContext)
|
||||||
{
|
{
|
||||||
var maxRequestBodySizeFeature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
|
var maxRequestBodySizeFeature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
|
||||||
|
|
@ -1498,5 +1566,5 @@ namespace TestSite
|
||||||
HeaderNames.ContentEncoding, HeaderNames.ContentType, HeaderNames.ContentRange, HeaderNames.Trailer
|
HeaderNames.ContentEncoding, HeaderNames.ContentType, HeaderNames.ContentRange, HeaderNames.Trailer
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue