From 6172677a0a748ecd4a7d3b989b0f5ee75f6e8b86 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 16 Sep 2018 12:14:36 -0700 Subject: [PATCH 1/5] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 54 ++++++++++++++++++++-------------------- korebuild-lock.txt | 4 +-- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 5915e116b1..784065f4b9 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -4,39 +4,39 @@ 0.10.13 - 2.2.0-preview1-20180907.8 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 0.6.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 + 2.2.0-preview1-20180911.1 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 0.6.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 15.6.82 15.6.82 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 - 2.2.0-preview3-35202 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 + 2.2.0-preview3-35252 2.0.9 2.1.3 2.2.0-preview2-26905-02 1.0.1 - 2.2.0-preview3-35202 + 2.2.0-preview3-35252 15.6.1 11.1.0 2.0.3 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 552300b0ce..1090ad6a92 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview1-20180907.8 -commithash:078918eb5c1f176ee1da351c584fb4a4d7491aa0 +version:2.2.0-preview1-20180911.1 +commithash:ddfecdfc6e8e4859db5a0daea578070b862aac65 From e477f47ba82b4c3e5555ae34739ccfdf54850076 Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Mon, 17 Sep 2018 08:07:16 -0700 Subject: [PATCH 2/5] Cleanup log file tests (#1398) --- .../Inprocess/StartupExceptionTests.cs | 46 +++++++++++-------- .../Utilities/LogFileTestBase.cs | 34 ++++++++++++++ .../Inprocess/StdOutRedirectionTests.cs | 13 +----- 3 files changed, 62 insertions(+), 31 deletions(-) create mode 100644 test/Common.FunctionalTests/Utilities/LogFileTestBase.cs diff --git a/test/Common.FunctionalTests/Inprocess/StartupExceptionTests.cs b/test/Common.FunctionalTests/Inprocess/StartupExceptionTests.cs index 0c69f10c03..cadfed9781 100644 --- a/test/Common.FunctionalTests/Inprocess/StartupExceptionTests.cs +++ b/test/Common.FunctionalTests/Inprocess/StartupExceptionTests.cs @@ -1,7 +1,6 @@ // 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.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; @@ -12,7 +11,7 @@ using Xunit; namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests { [Collection(PublishedSitesCollection.Name)] - public class StartupExceptionTests : IISFunctionalTestBase + public class StartupExceptionTests : LogFileTestBase { private readonly PublishedSitesFixture _fixture; @@ -22,18 +21,20 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests } [ConditionalTheory] - [InlineData("ConsoleWrite")] - [InlineData("ConsoleErrorWrite")] - public async Task CheckStdoutWithRandomNumber(string mode) + [InlineData("CheckLargeStdErrWrites")] + [InlineData("CheckLargeStdOutWrites")] + [InlineData("CheckOversizedStdErrWrites")] + [InlineData("CheckOversizedStdOutWrites")] + public async Task CheckStdoutWithLargeWrites_TestSink(string mode) { var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} {mode}"); + var deploymentResult = await DeployAsync(deploymentParameters); - var randomNumberString = new Random(Guid.NewGuid().GetHashCode()).Next(10000000).ToString(); - deploymentParameters.TransformArguments((a, _) => $"{a} {mode} {randomNumberString}"); - - await AssertFailsToStart(deploymentParameters); - - Assert.Contains(TestSink.Writes, context => context.Message.Contains($"Random number: {randomNumberString}")); + await AssertFailsToStart(deploymentResult); + var expectedString = new string('a', 30000); + Assert.Contains(TestSink.Writes, context => context.Message.Contains(expectedString)); + EventLogHelpers.VerifyEventLogEvent(deploymentResult, EventLogHelpers.InProcessThreadExitStdOut(deploymentResult, "12", expectedString)); } [ConditionalTheory] @@ -41,14 +42,21 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests [InlineData("CheckLargeStdOutWrites")] [InlineData("CheckOversizedStdErrWrites")] [InlineData("CheckOversizedStdOutWrites")] - public async Task CheckStdoutWithLargeWrites(string mode) + public async Task CheckStdoutWithLargeWrites_LogFile(string mode) { var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); deploymentParameters.TransformArguments((a, _) => $"{a} {mode}"); + deploymentParameters.EnableLogging(_logFolderPath); - await AssertFailsToStart(deploymentParameters); + var deploymentResult = await DeployAsync(deploymentParameters); - Assert.Contains(TestSink.Writes, context => context.Message.Contains(new string('a', 30000))); + await AssertFailsToStart(deploymentResult); + + var contents = GetLogFileContent(deploymentResult); + var expectedString = new string('a', 30000); + + Assert.Contains(expectedString, contents); + EventLogHelpers.VerifyEventLogEvent(deploymentResult, EventLogHelpers.InProcessThreadExitStdOut(deploymentResult, "12", expectedString)); } [ConditionalFact] @@ -57,16 +65,16 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); deploymentParameters.TransformArguments((a, _) => $"{a} CheckConsoleFunctions"); - await AssertFailsToStart(deploymentParameters); + var deploymentResult = await DeployAsync(deploymentParameters); + + await AssertFailsToStart(deploymentResult); Assert.Contains(TestSink.Writes, context => context.Message.Contains("Is Console redirection: True")); } - private async Task AssertFailsToStart(IISDeploymentParameters deploymentParameters) + private async Task AssertFailsToStart(IISDeploymentResult deploymentResult) { - var deploymentResult = await DeployAsync(deploymentParameters); - - var response = await deploymentResult.HttpClient.GetAsync("/"); + var response = await deploymentResult.HttpClient.GetAsync("/HelloWorld"); Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); diff --git a/test/Common.FunctionalTests/Utilities/LogFileTestBase.cs b/test/Common.FunctionalTests/Utilities/LogFileTestBase.cs new file mode 100644 index 0000000000..395ddfd40f --- /dev/null +++ b/test/Common.FunctionalTests/Utilities/LogFileTestBase.cs @@ -0,0 +1,34 @@ +// 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.IO; +using Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities +{ + public class LogFileTestBase : IISFunctionalTestBase + { + protected string _logFolderPath; + + public LogFileTestBase(ITestOutputHelper output = null) : base(output) + { + _logFolderPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + } + public override void Dispose() + { + base.Dispose(); + if (Directory.Exists(_logFolderPath)) + { + Directory.Delete(_logFolderPath, true); + } + } + + public string GetLogFileContent(IISDeploymentResult deploymentResult) + { + return File.ReadAllText(Helpers.GetExpectedLogName(deploymentResult, _logFolderPath)); + } + } +} diff --git a/test/IIS.FunctionalTests/Inprocess/StdOutRedirectionTests.cs b/test/IIS.FunctionalTests/Inprocess/StdOutRedirectionTests.cs index 1098384c9b..deab1d756b 100644 --- a/test/IIS.FunctionalTests/Inprocess/StdOutRedirectionTests.cs +++ b/test/IIS.FunctionalTests/Inprocess/StdOutRedirectionTests.cs @@ -15,26 +15,15 @@ using Xunit; namespace IIS.FunctionalTests.Inprocess { [Collection(PublishedSitesCollection.Name)] - public class StdOutRedirectionTests : IISFunctionalTestBase + public class StdOutRedirectionTests : LogFileTestBase { private readonly PublishedSitesFixture _fixture; - private readonly string _logFolderPath; public StdOutRedirectionTests(PublishedSitesFixture fixture) { - _logFolderPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); _fixture = fixture; } - public override void Dispose() - { - base.Dispose(); - if (Directory.Exists(_logFolderPath)) - { - Directory.Delete(_logFolderPath, true); - } - } - [ConditionalFact] [SkipIfDebug] public async Task FrameworkNotFoundExceptionLogged_Pipe() From 2cd6ad6d50cab4f4be52b652e355417c4880ede2 Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Mon, 17 Sep 2018 11:57:46 -0700 Subject: [PATCH 3/5] Add windows auth tests for inproc (#1385) --- .../IISExpressDeployer.cs | 3 ++- .../OutOfProcess/HelloWorldTest.cs | 2 +- .../{OutOfProcess => }/WindowsAuthTests.cs | 5 +++-- test/WebSites/OutOfProcessWebSite/Startup.cs | 14 -------------- .../shared/SharedStartup/Startup.shared.cs | 10 ++++++++++ 5 files changed, 16 insertions(+), 18 deletions(-) rename test/IISExpress.FunctionalTests/{OutOfProcess => }/WindowsAuthTests.cs (91%) diff --git a/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISExpressDeployer.cs b/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISExpressDeployer.cs index 8139a0f6b9..8fe8c66585 100644 --- a/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISExpressDeployer.cs +++ b/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISExpressDeployer.cs @@ -293,7 +293,8 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS ConfigureModuleAndBinding(config.Root, contentRoot, port); - if (!DeploymentParameters.PublishApplicationBeforeDeployment) + var webConfigPath = Path.Combine(contentRoot, "web.config"); + if (!DeploymentParameters.PublishApplicationBeforeDeployment && !File.Exists(webConfigPath)) { // The elements normally in the web.config are in the applicationhost.config for unpublished apps. AddAspNetCoreElement(config.Root); diff --git a/test/Common.FunctionalTests/OutOfProcess/HelloWorldTest.cs b/test/Common.FunctionalTests/OutOfProcess/HelloWorldTest.cs index 3f748f2314..edfd603e4c 100644 --- a/test/Common.FunctionalTests/OutOfProcess/HelloWorldTest.cs +++ b/test/Common.FunctionalTests/OutOfProcess/HelloWorldTest.cs @@ -66,7 +66,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests response = await deploymentResult.HttpClient.GetAsync("/Auth"); responseText = await response.Content.ReadAsStringAsync(); - Assert.True("backcompat;Windows".Equals(responseText) || "latest;null".Equals(responseText), "Auth"); + Assert.True("null".Equals(responseText), "Auth"); Assert.Equal( $"ContentRootPath {deploymentResult.ContentRoot}" + Environment.NewLine + diff --git a/test/IISExpress.FunctionalTests/OutOfProcess/WindowsAuthTests.cs b/test/IISExpress.FunctionalTests/WindowsAuthTests.cs similarity index 91% rename from test/IISExpress.FunctionalTests/OutOfProcess/WindowsAuthTests.cs rename to test/IISExpress.FunctionalTests/WindowsAuthTests.cs index b46b314d08..3720385d4c 100644 --- a/test/IISExpress.FunctionalTests/OutOfProcess/WindowsAuthTests.cs +++ b/test/IISExpress.FunctionalTests/WindowsAuthTests.cs @@ -26,7 +26,8 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests => TestMatrix.ForServers(DeployerSelector.ServerType) .WithTfms(Tfm.NetCoreApp22, Tfm.Net461) .WithAllApplicationTypes() - .WithAllAncmVersions(); + .WithAllAncmVersions() + .WithAllHostingModels(); [ConditionalTheory] [MemberData(nameof(TestVariants))] @@ -41,7 +42,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests var response = await deploymentResult.HttpClient.GetAsync("/Auth"); var responseText = await response.Content.ReadAsStringAsync(); - Assert.True("backcompat;Windows".Equals(responseText) || "latest;Windows".Equals(responseText), "Auth"); + Assert.Equal("Windows", responseText); } } } diff --git a/test/WebSites/OutOfProcessWebSite/Startup.cs b/test/WebSites/OutOfProcessWebSite/Startup.cs index 2547eb5735..a796f9a7a3 100644 --- a/test/WebSites/OutOfProcessWebSite/Startup.cs +++ b/test/WebSites/OutOfProcessWebSite/Startup.cs @@ -36,20 +36,6 @@ namespace TestSite public Task BodyLimit(HttpContext ctx) => ctx.Response.WriteAsync(ctx.Features.Get()?.MaxRequestBodySize?.ToString() ?? "null"); - public async Task Auth(HttpContext ctx) - { - var iisAuth = Environment.GetEnvironmentVariable("ASPNETCORE_IIS_HTTPAUTH"); - var authProvider = ctx.RequestServices.GetService(); - var authScheme = (await authProvider.GetAllSchemesAsync()).SingleOrDefault(); - if (string.IsNullOrEmpty(iisAuth)) - { - await ctx.Response.WriteAsync("backcompat;" + (authScheme?.Name ?? "null")); - } - else - { - await ctx.Response.WriteAsync("latest;" + (authScheme?.Name ?? "null")); - } - } public Task HelloWorld(HttpContext ctx) => ctx.Response.WriteAsync("Hello World"); diff --git a/test/WebSites/shared/SharedStartup/Startup.shared.cs b/test/WebSites/shared/SharedStartup/Startup.shared.cs index eef8f8bf7e..a804449029 100644 --- a/test/WebSites/shared/SharedStartup/Startup.shared.cs +++ b/test/WebSites/shared/SharedStartup/Startup.shared.cs @@ -2,7 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -33,5 +35,13 @@ namespace TestSite await ctx.Response.WriteAsync("Hello World"); } + + public async Task Auth(HttpContext ctx) + { + var authProvider = ctx.RequestServices.GetService(); + var authScheme = (await authProvider.GetAllSchemesAsync()).SingleOrDefault(); + + await ctx.Response.WriteAsync(authScheme?.Name ?? "null"); + } } } From 5e896ca50678547ce5a71a23edd7dcc5bab45ba7 Mon Sep 17 00:00:00 2001 From: Pavel Krymets Date: Mon, 17 Sep 2018 12:04:12 -0700 Subject: [PATCH 4/5] Implement IHttpBufferingFeature (#1391) --- .../managedexports.cpp | 24 +++++ .../Core/IISHttpContext.FeatureCollection.cs | 34 ++++++- .../Core/IISHttpContext.Features.cs | 24 +++-- .../IServerVariableFeature.cs | 4 +- .../NativeMethods.cs | 19 ++++ .../IISDeploymentParameterExtensions.cs | 26 +++++ .../Inprocess/CompressionTests.cs | 96 +++++++++++++++++++ .../Inprocess/ServerVariablesTest.cs | 6 ++ .../Utilities/IISCapability.cs | 3 +- .../Utilities/IISCompressionSiteCollection.cs | 13 +++ .../Utilities/IISCompressionSiteFixture.cs | 44 +++++++++ .../Utilities/IISTestSiteFixture.cs | 82 ++++++++++------ .../Common.Tests/Utilities/TestConnections.cs | 78 +++++++++++---- .../RequiresIISAttribute.cs | 41 ++++---- .../InProcess/WebSocketTests.cs | 2 +- test/WebSites/InProcessWebSite/Startup.cs | 41 +++++++- 16 files changed, 457 insertions(+), 80 deletions(-) create mode 100644 test/Common.FunctionalTests/Inprocess/CompressionTests.cs create mode 100644 test/Common.FunctionalTests/Utilities/IISCompressionSiteCollection.cs create mode 100644 test/Common.FunctionalTests/Utilities/IISCompressionSiteFixture.cs diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp b/src/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp index c1444a8571..27a41da909 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp @@ -86,6 +86,19 @@ Finished: return hr; } +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +HRESULT +http_set_server_variable( + _In_ IN_PROCESS_HANDLER* pInProcessHandler, + _In_ PCSTR pszVariableName, + _In_ PCWSTR pszVariableValue +) +{ + return pInProcessHandler + ->QueryHttpContext() + ->SetServerVariable(pszVariableName, pszVariableValue); +} + EXTERN_C __MIDL_DECLSPEC_DLLEXPORT HRESULT http_set_response_status_code( @@ -400,6 +413,17 @@ http_cancel_io( return pInProcessHandler->QueryHttpContext()->CancelIo(); } +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +HRESULT +http_disable_buffering( + _In_ IN_PROCESS_HANDLER* pInProcessHandler +) +{ + pInProcessHandler->QueryHttpContext()->GetResponse()->DisableBuffering(); + + return S_OK; +} + EXTERN_C __MIDL_DECLSPEC_DLLEXPORT HRESULT http_response_set_unknown_header( diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.FeatureCollection.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.FeatureCollection.cs index 4c2d4342b9..df6ba267d0 100644 --- a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.FeatureCollection.cs +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.FeatureCollection.cs @@ -23,7 +23,8 @@ namespace Microsoft.AspNetCore.Server.IIS.Core IHttpUpgradeFeature, IHttpRequestLifetimeFeature, IHttpAuthenticationFeature, - IServerVariablesFeature + IServerVariablesFeature, + IHttpBufferingFeature { // NOTE: When feature interfaces are added to or removed from this HttpProtocol implementation, // then the list of `implementedFeatures` in the generated code project MUST also be updated. @@ -202,7 +203,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core { if (string.IsNullOrEmpty(variableName)) { - return null; + throw new ArgumentException($"{nameof(variableName)} should be non-empty string"); } // Synchronize access to native methods that might run in parallel with IO loops @@ -211,6 +212,19 @@ namespace Microsoft.AspNetCore.Server.IIS.Core return NativeMethods.HttpTryGetServerVariable(_pInProcessHandler, variableName, out var value) ? value : null; } } + set + { + if (string.IsNullOrEmpty(variableName)) + { + throw new ArgumentException($"{nameof(variableName)} should be non-empty string"); + } + + // Synchronize access to native methods that might run in parallel with IO loops + lock (_contextLock) + { + NativeMethods.HttpSetServerVariable(_pInProcessHandler, variableName, value); + } + } } object IFeatureCollection.this[Type key] @@ -284,5 +298,21 @@ namespace Microsoft.AspNetCore.Server.IIS.Core { Abort(); } + + void IHttpBufferingFeature.DisableRequestBuffering() + { + } + + void IHttpBufferingFeature.DisableResponseBuffering() + { + NativeMethods.HttpDisableBuffering(_pInProcessHandler); + DisableCompression(); + } + + private void DisableCompression() + { + var serverVariableFeature = (IServerVariablesFeature)this; + serverVariableFeature["IIS_EnableDynamicCompression"] = "0"; + } } } diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.Features.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.Features.cs index 0064899340..f2a0016c0a 100644 --- a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.Features.cs +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.Features.cs @@ -27,6 +27,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core private static readonly Type IHttpSendFileFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature); private static readonly Type IISHttpContextType = typeof(IISHttpContext); private static readonly Type IServerVariablesFeature = typeof(global::Microsoft.AspNetCore.Http.Features.IServerVariablesFeature); + private static readonly Type IHttpBufferingFeature = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpBufferingFeature); private object _currentIHttpRequestFeature; private object _currentIHttpResponseFeature; @@ -41,14 +42,12 @@ namespace Microsoft.AspNetCore.Server.IIS.Core private object _currentIResponseCookiesFeature; private object _currentIItemsFeature; private object _currentITlsConnectionFeature; - private object _currentIHttpMaxRequestBodySizeFeature; - private object _currentIHttpMinRequestBodyDataRateFeature; - private object _currentIHttpMinResponseDataRateFeature; private object _currentIHttpWebSocketFeature; private object _currentISessionFeature; private object _currentIHttpBodyControlFeature; private object _currentIHttpSendFileFeature; private object _currentIServerVariablesFeature; + private object _currentIHttpBufferingFeature; private void Initialize() { @@ -58,12 +57,10 @@ namespace Microsoft.AspNetCore.Server.IIS.Core _currentIHttpRequestIdentifierFeature = this; _currentIHttpRequestLifetimeFeature = this; _currentIHttpConnectionFeature = this; - _currentIHttpMaxRequestBodySizeFeature = this; - _currentIHttpMinRequestBodyDataRateFeature = this; - _currentIHttpMinResponseDataRateFeature = this; _currentIHttpBodyControlFeature = this; _currentIHttpAuthenticationFeature = this; _currentIServerVariablesFeature = this; + _currentIHttpBufferingFeature = this; } internal object FastFeatureGet(Type key) @@ -142,7 +139,11 @@ namespace Microsoft.AspNetCore.Server.IIS.Core } if (key == IServerVariablesFeature) { - return this; + return _currentIServerVariablesFeature; + } + if (key == IHttpBufferingFeature) + { + return _currentIHttpBufferingFeature; } return ExtraFeatureGet(key); @@ -242,6 +243,11 @@ namespace Microsoft.AspNetCore.Server.IIS.Core _currentIServerVariablesFeature = feature; return; } + if (key == IHttpBufferingFeature) + { + _currentIHttpBufferingFeature = feature; + return; + } if (key == IISHttpContextType) { throw new InvalidOperationException("Cannot set IISHttpContext in feature collection"); @@ -323,6 +329,10 @@ namespace Microsoft.AspNetCore.Server.IIS.Core { yield return new KeyValuePair(IServerVariablesFeature, _currentIServerVariablesFeature as global::Microsoft.AspNetCore.Http.Features.IServerVariablesFeature); } + if (_currentIHttpBufferingFeature != null) + { + yield return new KeyValuePair(IHttpBufferingFeature, _currentIHttpBufferingFeature as global::Microsoft.AspNetCore.Http.Features.IHttpBufferingFeature); + } if (MaybeExtra != null) { diff --git a/src/Microsoft.AspNetCore.Server.IIS/IServerVariableFeature.cs b/src/Microsoft.AspNetCore.Server.IIS/IServerVariableFeature.cs index 3b54733a03..44f667cb41 100644 --- a/src/Microsoft.AspNetCore.Server.IIS/IServerVariableFeature.cs +++ b/src/Microsoft.AspNetCore.Server.IIS/IServerVariableFeature.cs @@ -15,10 +15,10 @@ namespace Microsoft.AspNetCore.Http.Features public interface IServerVariablesFeature { /// - /// Gets the value of a server variable for the current request. + /// Gets or sets the value of a server variable for the current request. /// /// The variable name /// May return null or empty if the variable does not exist or is not set. - string this[string variableName] { get; } + string this[string variableName] { get; set; } } } diff --git a/src/Microsoft.AspNetCore.Server.IIS/NativeMethods.cs b/src/Microsoft.AspNetCore.Server.IIS/NativeMethods.cs index e8aa17a430..c8feb2ef40 100644 --- a/src/Microsoft.AspNetCore.Server.IIS/NativeMethods.cs +++ b/src/Microsoft.AspNetCore.Server.IIS/NativeMethods.cs @@ -76,6 +76,9 @@ namespace Microsoft.AspNetCore.Server.IIS [DllImport(AspNetCoreModuleDll)] private static extern int http_stop_incoming_requests(IntPtr pInProcessApplication); + [DllImport(AspNetCoreModuleDll)] + private static extern int http_disable_buffering(IntPtr pInProcessApplication); + [DllImport(AspNetCoreModuleDll, CharSet = CharSet.Ansi)] private static extern int http_set_response_status_code(IntPtr pInProcessHandler, ushort statusCode, string pszReason); @@ -97,6 +100,12 @@ namespace Microsoft.AspNetCore.Server.IIS [MarshalAs(UnmanagedType.LPStr)] string variableName, [MarshalAs(UnmanagedType.BStr)] out string value); + [DllImport(AspNetCoreModuleDll)] + private static extern int http_set_server_variable( + IntPtr pInProcessHandler, + [MarshalAs(UnmanagedType.LPStr)] string variableName, + [MarshalAs(UnmanagedType.LPWStr)] string value); + [DllImport(AspNetCoreModuleDll)] private static extern unsafe int http_websockets_read_bytes( IntPtr pInProcessHandler, @@ -176,6 +185,11 @@ namespace Microsoft.AspNetCore.Server.IIS Validate(http_stop_incoming_requests(pInProcessApplication)); } + public static void HttpDisableBuffering(IntPtr pInProcessApplication) + { + Validate(http_disable_buffering(pInProcessApplication)); + } + public static void HttpSetResponseStatusCode(IntPtr pInProcessHandler, ushort statusCode, string pszReason) { Validate(http_set_response_status_code(pInProcessHandler, statusCode, pszReason)); @@ -208,6 +222,11 @@ namespace Microsoft.AspNetCore.Server.IIS return http_get_server_variable(pInProcessHandler, variableName, out value) == 0; } + public static void HttpSetServerVariable(IntPtr pInProcessHandler, string variableName, string value) + { + Validate(http_set_server_variable(pInProcessHandler, variableName, value)); + } + public static unsafe int HttpWebsocketsReadBytes( IntPtr pInProcessHandler, byte* pvBuffer, diff --git a/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeploymentParameterExtensions.cs b/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeploymentParameterExtensions.cs index 2d45f036a7..594e373b08 100644 --- a/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeploymentParameterExtensions.cs +++ b/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeploymentParameterExtensions.cs @@ -80,5 +80,31 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS aspNetCoreElement.SetAttributeValue("arguments", transformation((string)aspNetCoreElement.Attribute("arguments"), contentRoot)); }); } + + public static void EnableModule(this IISDeploymentParameters parameters, string moduleName, string modulePath) + { + if (parameters.ServerType == ServerType.IIS) + { + modulePath = modulePath.Replace("%IIS_BIN%", "%windir%\\System32\\inetsrv"); + } + + parameters.ServerConfigActionList.Add( + (element, _) => { + var webServerElement = element + .RequiredElement("system.webServer"); + + webServerElement + .RequiredElement("globalModules") + .GetOrAdd("add", "name", moduleName) + .SetAttributeValue("image", modulePath); + + (webServerElement.Element("modules") ?? + element + .Element("location") + .RequiredElement("system.webServer") + .RequiredElement("modules")) + .GetOrAdd("add", "name", moduleName); + }); + } } } diff --git a/test/Common.FunctionalTests/Inprocess/CompressionTests.cs b/test/Common.FunctionalTests/Inprocess/CompressionTests.cs new file mode 100644 index 0000000000..1a1218a526 --- /dev/null +++ b/test/Common.FunctionalTests/Inprocess/CompressionTests.cs @@ -0,0 +1,96 @@ +// 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.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISCompressionSiteCollection.Name)] + public class CompressionTests : FixtureLoggedTest + { + private readonly IISCompressionSiteFixture _fixture; + + public CompressionTests(IISCompressionSiteFixture fixture): base(fixture) + { + _fixture = fixture; + } + + [ConditionalTheory] + [RequiresIIS(IISCapability.DynamicCompression)] + [InlineData(true)] + [InlineData(false)] + public async Task BufferingDisabled(bool compression) + { + using (var connection = _fixture.CreateTestConnection()) + { + var requestLength = 0; + var messages = new List(); + for (var i = 1; i < 100; i++) + { + var message = i + Environment.NewLine; + requestLength += message.Length; + messages.Add(message); + } + + await connection.Send( + "POST /ReadAndWriteEchoLinesNoBuffering HTTP/1.1", + $"Content-Length: {requestLength}", + "Accept-Encoding: " + (compression ? "gzip": "identity"), + "Response-Content-Type: text/event-stream", + "Host: localhost", + "Connection: close", + "", + ""); + + await connection.Receive( + "HTTP/1.1 200 OK", + ""); + await connection.ReceiveHeaders(); + + foreach (var message in messages) + { + await connection.Send(message); + await connection.ReceiveChunk(message); + } + + await connection.Send("\r\n"); + await connection.ReceiveChunk(""); + await connection.WaitForConnectionClose(); + } + } + + [ConditionalFact] + [RequiresIIS(IISCapability.DynamicCompression)] + public async Task DynamicResponsesAreCompressed() + { + var handler = new HttpClientHandler + { + AutomaticDecompression = DecompressionMethods.GZip + }; + var client = new HttpClient(handler) + { + BaseAddress = _fixture.Client.BaseAddress, + }; + client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip")); + client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("identity", 0)); + client.DefaultRequestHeaders.Add("Response-Content-Type", "text/event-stream"); + var messages = "Message1\r\nMessage2\r\n"; + + // Send messages with terminator + var response = await client.PostAsync("ReadAndWriteEchoLines", new StringContent(messages + "\r\n")); + Assert.Equal(messages, await response.Content.ReadAsStringAsync()); + Assert.True(response.Content.Headers.TryGetValues("Content-Type", out var contentTypes)); + Assert.Single(contentTypes, "text/event-stream"); + // Not the cleanest check but I wasn't able to figure out other way to check + // that response was compressed + Assert.Contains("gzip", response.Content.GetType().FullName, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/test/Common.FunctionalTests/Inprocess/ServerVariablesTest.cs b/test/Common.FunctionalTests/Inprocess/ServerVariablesTest.cs index aa79727e19..d18c4c7f09 100644 --- a/test/Common.FunctionalTests/Inprocess/ServerVariablesTest.cs +++ b/test/Common.FunctionalTests/Inprocess/ServerVariablesTest.cs @@ -34,6 +34,12 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests Assert.Equal("THIS_VAR_IS_UNDEFINED: (null)", await _fixture.Client.GetStringAsync("/ServerVariable?q=THIS_VAR_IS_UNDEFINED")); } + [ConditionalFact] + public async Task CanSetAndReadVariable() + { + Assert.Equal("ROUNDTRIP: 1", await _fixture.Client.GetStringAsync("/ServerVariable?v=1&q=ROUNDTRIP")); + } + [ConditionalFact] public async Task BasePathIsNotPrefixedBySlashSlashQuestionMark() { diff --git a/test/Common.FunctionalTests/Utilities/IISCapability.cs b/test/Common.FunctionalTests/Utilities/IISCapability.cs index d62716db09..e96127ea9f 100644 --- a/test/Common.FunctionalTests/Utilities/IISCapability.cs +++ b/test/Common.FunctionalTests/Utilities/IISCapability.cs @@ -12,6 +12,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests Websockets = 1, WindowsAuthentication = 2, PoolEnvironmentVariables = 4, - ShutdownToken = 8 + ShutdownToken = 8, + DynamicCompression = 16 } } diff --git a/test/Common.FunctionalTests/Utilities/IISCompressionSiteCollection.cs b/test/Common.FunctionalTests/Utilities/IISCompressionSiteCollection.cs new file mode 100644 index 0000000000..2c424943f3 --- /dev/null +++ b/test/Common.FunctionalTests/Utilities/IISCompressionSiteCollection.cs @@ -0,0 +1,13 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [CollectionDefinition(Name)] + public class IISCompressionSiteCollection : ICollectionFixture + { + public const string Name = nameof(IISCompressionSiteCollection); + } +} diff --git a/test/Common.FunctionalTests/Utilities/IISCompressionSiteFixture.cs b/test/Common.FunctionalTests/Utilities/IISCompressionSiteFixture.cs new file mode 100644 index 0000000000..3aff68d11b --- /dev/null +++ b/test/Common.FunctionalTests/Utilities/IISCompressionSiteFixture.cs @@ -0,0 +1,44 @@ +// 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.Net.Http; +using System.Threading; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + public class IISCompressionSiteFixture : IISTestSiteFixture + { + public IISCompressionSiteFixture() : base(Configure) + { + } + + private static void Configure(IISDeploymentParameters deploymentParameters) + { + // Enable dynamic compression + deploymentParameters.ServerConfigActionList.Add( + (element, _) => { + var webServerElement = element + .RequiredElement("system.webServer"); + + webServerElement + .GetOrAdd("urlCompression") + .SetAttributeValue("doDynamicCompression", "true"); + + webServerElement + .GetOrAdd("httpCompression") + .GetOrAdd("dynamicTypes") + .GetOrAdd("add", "mimeType", "text/*") + .SetAttributeValue("enabled", "true"); + + }); + + deploymentParameters.EnableModule("DynamicCompressionModule", "%IIS_BIN%\\compdyn.dll"); + } + } +} diff --git a/test/Common.FunctionalTests/Utilities/IISTestSiteFixture.cs b/test/Common.FunctionalTests/Utilities/IISTestSiteFixture.cs index fc5a3b6df1..5628a1acfb 100644 --- a/test/Common.FunctionalTests/Utilities/IISTestSiteFixture.cs +++ b/test/Common.FunctionalTests/Utilities/IISTestSiteFixture.cs @@ -14,40 +14,36 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests { public class IISTestSiteFixture : IDisposable { - private readonly ApplicationDeployer _deployer; - private readonly ForwardingProvider _forwardingProvider; + private ApplicationDeployer _deployer; + private ILoggerFactory _loggerFactory; + private ForwardingProvider _forwardingProvider; + private IISDeploymentResult _deploymentResult; + private readonly Action _configure; - public IISTestSiteFixture() + public IISTestSiteFixture() : this(_ => { }) { - var logging = AssemblyTestLog.ForAssembly(typeof(IISTestSiteFixture).Assembly); - - var deploymentParameters = new IISDeploymentParameters(Helpers.GetInProcessTestSitesPath(), - DeployerSelector.ServerType, - RuntimeFlavor.CoreClr, - RuntimeArchitecture.x64) - { - TargetFramework = Tfm.NetCoreApp22, - AncmVersion = AncmVersion.AspNetCoreModuleV2, - HostingModel = HostingModel.InProcess, - PublishApplicationBeforeDeployment = true, - }; - - _forwardingProvider = new ForwardingProvider(); - var loggerFactory = logging.CreateLoggerFactory(null, nameof(IISTestSiteFixture)); - loggerFactory.AddProvider(_forwardingProvider); - - _deployer = IISApplicationDeployerFactory.Create(deploymentParameters, loggerFactory); - - DeploymentResult = (IISDeploymentResult)_deployer.DeployAsync().Result; - Client = DeploymentResult.HttpClient; - BaseUri = DeploymentResult.ApplicationBaseUri; - ShutdownToken = DeploymentResult.HostShutdownToken; } - public string BaseUri { get; } - public HttpClient Client { get; } - public CancellationToken ShutdownToken { get; } - public IISDeploymentResult DeploymentResult { get; } + public IISTestSiteFixture(Action configure) + { + var logging = AssemblyTestLog.ForAssembly(typeof(IISTestSiteFixture).Assembly); + _loggerFactory = logging.CreateLoggerFactory(null, nameof(IISTestSiteFixture)); + + _forwardingProvider = new ForwardingProvider(); + _loggerFactory.AddProvider(_forwardingProvider); + + _configure = configure; + } + + public HttpClient Client => DeploymentResult.HttpClient; + public IISDeploymentResult DeploymentResult + { + get + { + EnsureInitialized(); + return _deploymentResult; + } + } public TestConnection CreateTestConnection() { @@ -56,7 +52,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests public void Dispose() { - _deployer.Dispose(); + _deployer?.Dispose(); } public void Attach(LoggedTest test) @@ -79,6 +75,30 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests _forwardingProvider.LoggerFactory = null; } + private void EnsureInitialized() + { + if (_deployer != null) + { + return; + } + + var deploymentParameters = new IISDeploymentParameters(Helpers.GetInProcessTestSitesPath(), + DeployerSelector.ServerType, + RuntimeFlavor.CoreClr, + RuntimeArchitecture.x64) + { + TargetFramework = Tfm.NetCoreApp22, + AncmVersion = AncmVersion.AspNetCoreModuleV2, + HostingModel = HostingModel.InProcess, + PublishApplicationBeforeDeployment = true, + }; + + _configure(deploymentParameters); + + _deployer = IISApplicationDeployerFactory.Create(deploymentParameters, _loggerFactory); + _deploymentResult = (IISDeploymentResult)_deployer.DeployAsync().Result; + } + private class ForwardingProvider : ILoggerProvider { private readonly List _loggers = new List(); diff --git a/test/Common.Tests/Utilities/TestConnections.cs b/test/Common.Tests/Utilities/TestConnections.cs index 3c234a0f4d..722abc5b96 100644 --- a/test/Common.Tests/Utilities/TestConnections.cs +++ b/test/Common.Tests/Utilities/TestConnections.cs @@ -24,7 +24,6 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting private readonly bool _ownsSocket; private readonly Socket _socket; private readonly NetworkStream _stream; - private readonly StreamReader _reader; public TestConnection(int port) : this(port, AddressFamily.InterNetwork) @@ -46,13 +45,10 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting _ownsSocket = ownsSocket; _socket = socket; _stream = new NetworkStream(_socket, ownsSocket: false); - _reader = new StreamReader(_stream, Encoding.ASCII); } public Socket Socket => _socket; - public StreamReader Reader => _reader; - public void Dispose() { _stream.Dispose(); @@ -79,50 +75,96 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting await _stream.FlushAsync().ConfigureAwait(false); } + public async Task ReadCharAsync() + { + var bytes = new byte[1]; + return (await _stream.ReadAsync(bytes, 0, 1) == 1) ? bytes[0] : -1; + } + + public async Task ReadLineAsync() + { + var builder = new StringBuilder(); + var current = await ReadCharAsync(); + while (current != '\r') + { + builder.Append((char)current); + current = await ReadCharAsync(); + } + + // Consume \n + await ReadCharAsync(); + + return builder.ToString(); + } + + public async Task> ReceiveChunk() + { + var length = int.Parse(await ReadLineAsync(), System.Globalization.NumberStyles.HexNumber); + + var bytes = await Receive(length); + + await ReadLineAsync(); + + return bytes; + } + + public async Task ReceiveChunk(string expected) + { + Assert.Equal(expected, Encoding.ASCII.GetString((await ReceiveChunk()).Span)); + } + public async Task Receive(params string[] lines) { var expected = string.Join("\r\n", lines); - var actual = new char[expected.Length]; - var offset = 0; + var actual = await Receive(expected.Length); + Assert.Equal(expected, Encoding.ASCII.GetString(actual.Span)); + } + + private async Task> Receive(int length) + { + var actual = new byte[length]; + int offset = 0; try { - while (offset < expected.Length) + while (offset < length) { - var data = new byte[expected.Length]; - var task = _reader.ReadAsync(actual, offset, actual.Length - offset); + var task = _stream.ReadAsync(actual, offset, actual.Length - offset); if (!Debugger.IsAttached) { task = task.TimeoutAfter(Timeout); } + var count = await task.ConfigureAwait(false); if (count == 0) { break; } + offset += count; } } catch (TimeoutException ex) when (offset != 0) { - throw new TimeoutException($"Did not receive a complete response within {Timeout}.{Environment.NewLine}{Environment.NewLine}" + - $"Expected:{Environment.NewLine}{expected}{Environment.NewLine}{Environment.NewLine}" + - $"Actual:{Environment.NewLine}{new string(actual, 0, offset)}{Environment.NewLine}", + throw new TimeoutException( + $"Did not receive a complete response within {Timeout}.{Environment.NewLine}{Environment.NewLine}" + + $"Expected:{Environment.NewLine}{length} bytes of data{Environment.NewLine}{Environment.NewLine}" + + $"Actual:{Environment.NewLine}{Encoding.ASCII.GetString(actual, 0, offset)}{Environment.NewLine}", ex); } - Assert.Equal(expected, new string(actual, 0, offset)); + return actual.AsMemory(0, offset); } public async Task ReceiveStartsWith(string prefix, int maxLineLength = 1024) { - var actual = new char[maxLineLength]; + var actual = new byte[maxLineLength]; var offset = 0; while (offset < maxLineLength) { // Read one char at a time so we don't read past the end of the line. - var task = _reader.ReadAsync(actual, offset, 1); + var task = _stream.ReadAsync(actual, offset, 1); if (!Debugger.IsAttached) { Assert.True(task.Wait(4000), "timeout"); @@ -142,7 +184,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting } } - var actualLine = new string(actual, 0, offset); + var actualLine = Encoding.ASCII.GetString(actual, 0, offset); Assert.StartsWith(prefix, actualLine); } @@ -152,7 +194,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting string line; do { - line = await _reader.ReadLineAsync(); + line = await ReadLineAsync(); headers.Add(line); } while (line != ""); @@ -190,7 +232,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting else { tcs.SetException(new IOException( - $"Expected connection close, received data instead: \"{_reader.CurrentEncoding.GetString(e.Buffer, 0, e.BytesTransferred)}\"")); + $"Expected connection close, received data instead: \"{Encoding.ASCII.GetString(e.Buffer, 0, e.BytesTransferred)}\"")); } } diff --git a/test/IIS.FunctionalTests/RequiresIISAttribute.cs b/test/IIS.FunctionalTests/RequiresIISAttribute.cs index f94459fd8f..dc374293b8 100644 --- a/test/IIS.FunctionalTests/RequiresIISAttribute.cs +++ b/test/IIS.FunctionalTests/RequiresIISAttribute.cs @@ -18,12 +18,10 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests private static readonly bool _isMetStatic; private static readonly string _skipReasonStatic; - private readonly bool _isMet; - private readonly string _skipReason; - private static readonly bool _websocketsAvailable; private static readonly bool _windowsAuthAvailable; private static readonly bool _poolEnvironmentVariablesAvailable; + private static readonly bool _dynamicCompressionAvailable; static RequiresIISAttribute() { @@ -84,6 +82,8 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests _windowsAuthAvailable = File.Exists(Path.Combine(Environment.SystemDirectory, "inetsrv", "authsspi.dll")); + _dynamicCompressionAvailable = File.Exists(Path.Combine(Environment.SystemDirectory, "inetsrv", "compdyn.dll")); + var iisRegistryKey = Registry.LocalMachine.OpenSubKey(@"Software\Microsoft\InetStp", writable: false); if (iisRegistryKey == null) { @@ -98,47 +98,56 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests } } - public RequiresIISAttribute() + public RequiresIISAttribute() : this (IISCapability.None) { } public RequiresIISAttribute(IISCapability capabilities) { - _isMet = _isMetStatic; - _skipReason = _skipReasonStatic; + IsMet = _isMetStatic; + SkipReason = _skipReasonStatic; if (capabilities.HasFlag(IISCapability.Websockets)) { - _isMet &= _websocketsAvailable; + IsMet &= _websocketsAvailable; if (!_websocketsAvailable) { - _skipReason += "The machine does not have IIS websockets installed."; + SkipReason += "The machine does not have IIS websockets installed."; } } if (capabilities.HasFlag(IISCapability.WindowsAuthentication)) { - _isMet &= _windowsAuthAvailable; + IsMet &= _windowsAuthAvailable; if (!_windowsAuthAvailable) { - _skipReason += "The machine does not have IIS windows authentication installed."; + SkipReason += "The machine does not have IIS windows authentication installed."; } } if (capabilities.HasFlag(IISCapability.PoolEnvironmentVariables)) { - _isMet &= _poolEnvironmentVariablesAvailable; + IsMet &= _poolEnvironmentVariablesAvailable; if (!_poolEnvironmentVariablesAvailable) { - _skipReason += "The machine does allow for setting environment variables on application pools."; + SkipReason += "The machine does allow for setting environment variables on application pools."; } } if (capabilities.HasFlag(IISCapability.ShutdownToken)) { - _isMet = false; - _skipReason += "https://github.com/aspnet/IISIntegration/issues/1074"; + IsMet = false; + SkipReason += "https://github.com/aspnet/IISIntegration/issues/1074"; + } + + if (capabilities.HasFlag(IISCapability.DynamicCompression)) + { + IsMet &= _dynamicCompressionAvailable; + if (!_dynamicCompressionAvailable) + { + SkipReason += "The machine does not have IIS dynamic compression installed."; + } } } - public bool IsMet => _isMet; - public string SkipReason => _skipReason; + public bool IsMet { get; } + public string SkipReason { get; } } } diff --git a/test/IISExpress.FunctionalTests/InProcess/WebSocketTests.cs b/test/IISExpress.FunctionalTests/InProcess/WebSocketTests.cs index 2d97455d31..826001c96d 100644 --- a/test/IISExpress.FunctionalTests/InProcess/WebSocketTests.cs +++ b/test/IISExpress.FunctionalTests/InProcess/WebSocketTests.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests public WebSocketsTests(IISTestSiteFixture fixture) { - _webSocketUri = fixture.BaseUri.Replace("http:", "ws:"); + _webSocketUri = fixture.DeploymentResult.ApplicationBaseUri.Replace("http:", "ws:"); } [ConditionalFact] diff --git a/test/WebSites/InProcessWebSite/Startup.cs b/test/WebSites/InProcessWebSite/Startup.cs index 7bd530a9f9..ef1863ef2b 100644 --- a/test/WebSites/InProcessWebSite/Startup.cs +++ b/test/WebSites/InProcessWebSite/Startup.cs @@ -30,7 +30,13 @@ namespace TestSite private async Task ServerVariable(HttpContext ctx) { var varName = ctx.Request.Query["q"]; - await ctx.Response.WriteAsync($"{varName}: {ctx.GetIISServerVariable(varName) ?? "(null)"}"); + var newValue = ctx.Request.Query["v"]; + var feature = ctx.Features.Get(); + if (newValue.Count != 0) + { + feature[varName] = newValue; + } + await ctx.Response.WriteAsync($"{varName}: {feature[varName] ?? "(null)"}"); } private async Task AuthenticationAnonymous(HttpContext ctx) @@ -321,6 +327,11 @@ namespace TestSite private async Task ReadAndWriteEchoLines(HttpContext ctx) { + if (ctx.Request.Headers.TryGetValue("Response-Content-Type", out var contentType)) + { + ctx.Response.ContentType = contentType; + } + //Send headers await ctx.Response.Body.FlushAsync(); @@ -337,6 +348,31 @@ namespace TestSite } } + private async Task ReadAndWriteEchoLinesNoBuffering(HttpContext ctx) + { + var feature = ctx.Features.Get(); + feature.DisableResponseBuffering(); + + if (ctx.Request.Headers.TryGetValue("Response-Content-Type", out var contentType)) + { + ctx.Response.ContentType = contentType; + } + + //Send headers + await ctx.Response.Body.FlushAsync(); + + var reader = new StreamReader(ctx.Request.Body); + while (!reader.EndOfStream) + { + var line = await reader.ReadLineAsync(); + if (line == "") + { + return; + } + await ctx.Response.WriteAsync(line + Environment.NewLine); + } + } + private async Task ReadPartialBody(HttpContext ctx) { var data = new byte[5]; @@ -631,10 +667,11 @@ namespace TestSite // executed on background thread while request thread calls GetServerVariable // concurrent native calls may cause native object corruption + var serverVariableFeature = ctx.Features.Get(); await ctx.Response.WriteAsync("Response Begin"); for (int i = 0; i < 1000; i++) { - await ctx.Response.WriteAsync(ctx.GetIISServerVariable("REMOTE_PORT")); + await ctx.Response.WriteAsync(serverVariableFeature["REMOTE_PORT"]); await ctx.Response.Body.FlushAsync(); } await ctx.Response.WriteAsync("Response End"); From ece5ad36e22daea23e7428d744238ea146e47fd5 Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Tue, 18 Sep 2018 14:22:56 -0700 Subject: [PATCH 5/5] Implement ITlsConnectionFeature (#1390) --- .../Core/IISHttpContext.FeatureCollection.cs | 37 ++++- .../Core/IISHttpContext.Features.cs | 1 + .../NativeMethods.cs | 1 - .../ClientCertificateFixture.cs | 91 +++++++++++ .../ClientCertificateTests.cs | 85 ++++++++++ .../CommonStartupTests.cs | 4 - .../PublishedSitesFixture.cs | 2 +- .../SkipIfNotAdminAttribute.cs | 25 +++ test/IISExpress.FunctionalTests/HttpsTests.cs | 60 +++++++ .../OutOfProcess/HttpsTest.cs | 148 ------------------ test/WebSites/InProcessWebSite/Startup.cs | 3 + test/WebSites/OutOfProcessWebSite/Startup.cs | 4 - .../shared/SharedStartup/Startup.shared.cs | 6 + 13 files changed, 307 insertions(+), 160 deletions(-) create mode 100644 test/Common.FunctionalTests/ClientCertificateFixture.cs create mode 100644 test/Common.FunctionalTests/ClientCertificateTests.cs create mode 100644 test/Common.FunctionalTests/SkipIfNotAdminAttribute.cs create mode 100644 test/IISExpress.FunctionalTests/HttpsTests.cs delete mode 100644 test/IISExpress.FunctionalTests/OutOfProcess/HttpsTest.cs diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.FeatureCollection.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.FeatureCollection.cs index df6ba267d0..19f65bc900 100644 --- a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.FeatureCollection.cs +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.FeatureCollection.cs @@ -6,7 +6,9 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Runtime.InteropServices; using System.Security.Claims; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -24,13 +26,15 @@ namespace Microsoft.AspNetCore.Server.IIS.Core IHttpRequestLifetimeFeature, IHttpAuthenticationFeature, IServerVariablesFeature, - IHttpBufferingFeature + IHttpBufferingFeature, + ITlsConnectionFeature { // NOTE: When feature interfaces are added to or removed from this HttpProtocol implementation, // then the list of `implementedFeatures` in the generated code project MUST also be updated. private int _featureRevision; private string _httpProtocolVersion = null; + private X509Certificate2 _certificate; private List> MaybeExtra; public void ResetFeatureCollection() @@ -276,7 +280,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core ReasonPhrase = ReasonPhrases.GetReasonPhrase(StatusCodes.Status101SwitchingProtocols); // If we started reading before calling Upgrade Task should be completed at this point - // because read would return 0 syncronosly + // because read would return 0 synchronously Debug.Assert(_readBodyTask == null || _readBodyTask.IsCompleted); // Reset reading status to allow restarting with new IO @@ -290,6 +294,35 @@ namespace Microsoft.AspNetCore.Server.IIS.Core return new DuplexStream(RequestBody, ResponseBody); } + Task ITlsConnectionFeature.GetClientCertificateAsync(CancellationToken cancellationToken) + { + return Task.FromResult(((ITlsConnectionFeature)this).ClientCertificate); + } + + unsafe X509Certificate2 ITlsConnectionFeature.ClientCertificate + { + get + { + if (_certificate == null && + NativeRequest->pSslInfo != null && + NativeRequest->pSslInfo->pClientCertInfo != null && + NativeRequest->pSslInfo->pClientCertInfo->pCertEncoded != null && + NativeRequest->pSslInfo->pClientCertInfo->CertEncodedSize != 0) + { + // Based off of from https://referencesource.microsoft.com/#system/net/System/Net/HttpListenerRequest.cs,1037c8ec82879ba0,references + var rawCertificateCopy = new byte[NativeRequest->pSslInfo->pClientCertInfo->CertEncodedSize]; + Marshal.Copy((IntPtr)NativeRequest->pSslInfo->pClientCertInfo->pCertEncoded, rawCertificateCopy, 0, rawCertificateCopy.Length); + _certificate = new X509Certificate2(rawCertificateCopy); + } + + return _certificate; + } + set + { + _certificate = value; + } + } + IEnumerator> IEnumerable>.GetEnumerator() => FastEnumerable().GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => FastEnumerable().GetEnumerator(); diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.Features.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.Features.cs index f2a0016c0a..6e107e03f0 100644 --- a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.Features.cs +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.Features.cs @@ -61,6 +61,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core _currentIHttpAuthenticationFeature = this; _currentIServerVariablesFeature = this; _currentIHttpBufferingFeature = this; + _currentITlsConnectionFeature = this; } internal object FastFeatureGet(Type key) diff --git a/src/Microsoft.AspNetCore.Server.IIS/NativeMethods.cs b/src/Microsoft.AspNetCore.Server.IIS/NativeMethods.cs index c8feb2ef40..906a86cdda 100644 --- a/src/Microsoft.AspNetCore.Server.IIS/NativeMethods.cs +++ b/src/Microsoft.AspNetCore.Server.IIS/NativeMethods.cs @@ -23,7 +23,6 @@ namespace Microsoft.AspNetCore.Server.IIS public static extern bool CloseHandle(IntPtr handle); - [DllImport("kernel32.dll")] private static extern IntPtr GetModuleHandle(string lpModuleName); diff --git a/test/Common.FunctionalTests/ClientCertificateFixture.cs b/test/Common.FunctionalTests/ClientCertificateFixture.cs new file mode 100644 index 0000000000..c3fa927b46 --- /dev/null +++ b/test/Common.FunctionalTests/ClientCertificateFixture.cs @@ -0,0 +1,91 @@ +// 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.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + public class ClientCertificateFixture : IDisposable + { + public ClientCertificateFixture() + { + using (var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine)) + { + store.Open(OpenFlags.ReadWrite); + + foreach (var cert in store.Certificates) + { + if (cert.Issuer != "CN=IISIntegrationTest_Root") + { + continue; + } + Certificate = cert; + store.Close(); + return; + } + + var parentKey = CreateKeyMaterial(2048); + + // On first run of the test, creates the certificate in the trusted root certificate authorities. + var parentRequest = new CertificateRequest("CN=IISIntegrationTest_Root", parentKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + + parentRequest.CertificateExtensions.Add( + new X509BasicConstraintsExtension( + certificateAuthority: true, + hasPathLengthConstraint: false, + pathLengthConstraint: 0, + critical: true)); + + parentRequest.CertificateExtensions.Add( + new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.NonRepudiation, critical: true)); + + parentRequest.CertificateExtensions.Add( + new X509SubjectKeyIdentifierExtension(parentRequest.PublicKey, false)); + + var notBefore = DateTimeOffset.Now.AddDays(-1); + var notAfter = DateTimeOffset.Now.AddYears(5); + + var parentCert = parentRequest.CreateSelfSigned(notBefore, notAfter); + + // Need to export/import the certificate to associate the private key with the cert. + var imported = parentCert; + + var export = parentCert.Export(X509ContentType.Pkcs12, ""); + imported = new X509Certificate2(export, "", X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable); + Array.Clear(export, 0, export.Length); + + // Add the cert to the cert store + Certificate = imported; + + store.Add(certificate: imported); + store.Close(); + } + } + + public X509Certificate2 Certificate { get; } + + public void Dispose() + { + using (var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine)) + { + store.Open(OpenFlags.ReadWrite); + store.Remove(Certificate); + store.Close(); + } + } + + private RSA CreateKeyMaterial(int minimumKeySize) + { + var rsa = RSA.Create(minimumKeySize); + if (rsa.KeySize < minimumKeySize) + { + throw new InvalidOperationException($"Failed to create a key with a size of {minimumKeySize} bits"); + } + + return rsa; + } + } +} diff --git a/test/Common.FunctionalTests/ClientCertificateTests.cs b/test/Common.FunctionalTests/ClientCertificateTests.cs new file mode 100644 index 0000000000..289fb18c2f --- /dev/null +++ b/test/Common.FunctionalTests/ClientCertificateTests.cs @@ -0,0 +1,85 @@ +// 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.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Server.IntegrationTesting.Common; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + [SkipIfNotAdmin] + public class ClientCertificateTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + private readonly ClientCertificateFixture _certFixture; + + public ClientCertificateTests(PublishedSitesFixture fixture, ClientCertificateFixture certFixture) + { + _fixture = fixture; + _certFixture = certFixture; + } + + public static TestMatrix TestVariants + => TestMatrix.ForServers(DeployerSelector.ServerType) + .WithTfms(Tfm.NetCoreApp22, Tfm.Net461) + .WithAllApplicationTypes() + .WithAllAncmVersions() + .WithAllHostingModels(); + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public Task HttpsNoClientCert_NoClientCert(TestVariant variant) + { + return ClientCertTest(variant, sendClientCert: false); + } + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public Task HttpsClientCert_GetCertInformation(TestVariant variant) + { + return ClientCertTest(variant, sendClientCert: true); + } + + private async Task ClientCertTest(TestVariant variant, bool sendClientCert) + { + var port = TestPortHelper.GetNextSSLPort(); + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant); + deploymentParameters.ApplicationBaseUriHint = $"https://localhost:{port}/"; + deploymentParameters.AddHttpsToServerConfig(); + + var deploymentResult = await DeployAsync(deploymentParameters); + var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (a, b, c, d) => true, + ClientCertificateOptions = ClientCertificateOption.Manual, + }; + + if (sendClientCert) + { + Assert.NotNull(_certFixture.Certificate); + handler.ClientCertificates.Add(_certFixture.Certificate); + } + + var client = deploymentResult.CreateClient(handler); + var response = await client.GetAsync("GetClientCert"); + + var responseText = await response.Content.ReadAsStringAsync(); + + if (sendClientCert) + { + Assert.Equal($"Enabled;{_certFixture.Certificate.GetCertHashString()}", responseText); + } + else + { + Assert.Equal("Disabled", responseText); + } + } + } +} diff --git a/test/Common.FunctionalTests/CommonStartupTests.cs b/test/Common.FunctionalTests/CommonStartupTests.cs index 760c1ad04b..e2bcf2a8f9 100644 --- a/test/Common.FunctionalTests/CommonStartupTests.cs +++ b/test/Common.FunctionalTests/CommonStartupTests.cs @@ -1,14 +1,10 @@ // 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.IO; -using System.Linq; using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; using Microsoft.AspNetCore.Server.IntegrationTesting; -using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; using Microsoft.AspNetCore.Testing.xunit; using Xunit; diff --git a/test/Common.FunctionalTests/PublishedSitesFixture.cs b/test/Common.FunctionalTests/PublishedSitesFixture.cs index 1ebfdab38e..282ee26109 100644 --- a/test/Common.FunctionalTests/PublishedSitesFixture.cs +++ b/test/Common.FunctionalTests/PublishedSitesFixture.cs @@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests /// This type just maps collection names to available fixtures /// [CollectionDefinition(Name)] - public class PublishedSitesCollection : ICollectionFixture + public class PublishedSitesCollection : ICollectionFixture, ICollectionFixture { public const string Name = nameof(PublishedSitesCollection); } diff --git a/test/Common.FunctionalTests/SkipIfNotAdminAttribute.cs b/test/Common.FunctionalTests/SkipIfNotAdminAttribute.cs new file mode 100644 index 0000000000..d2acb70415 --- /dev/null +++ b/test/Common.FunctionalTests/SkipIfNotAdminAttribute.cs @@ -0,0 +1,25 @@ +// 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.Security.Principal; +using Microsoft.AspNetCore.Testing.xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] + public sealed class SkipIfNotAdminAttribute : Attribute, ITestCondition + { + public bool IsMet + { + get + { + var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + return principal.IsInRole(WindowsBuiltInRole.Administrator); + } + } + + public string SkipReason => "The current process is not running as admin."; + } +} diff --git a/test/IISExpress.FunctionalTests/HttpsTests.cs b/test/IISExpress.FunctionalTests/HttpsTests.cs new file mode 100644 index 0000000000..ac58f73c0e --- /dev/null +++ b/test/IISExpress.FunctionalTests/HttpsTests.cs @@ -0,0 +1,60 @@ +// 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.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Server.IntegrationTesting.Common; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class HttpsTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public HttpsTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + public static TestMatrix TestVariants + => TestMatrix.ForServers(DeployerSelector.ServerType) + .WithTfms(Tfm.NetCoreApp22, Tfm.Net461) + .WithAllApplicationTypes() + .WithAllAncmVersions() + .WithAllHostingModels(); + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task HttpsHelloWorld(TestVariant variant) + { + var port = TestPortHelper.GetNextSSLPort(); + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant); + deploymentParameters.ApplicationBaseUriHint = $"https://localhost:{port}/"; + deploymentParameters.AddHttpsToServerConfig(); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (a, b, c, d) => true + }; + var client = deploymentResult.CreateClient(handler); + var response = await client.GetAsync("HttpsHelloWorld"); + var responseText = await response.Content.ReadAsStringAsync(); + if (variant.HostingModel == HostingModel.OutOfProcess) + { + Assert.Equal("Scheme:https; Original:http", responseText); + } + else + { + Assert.Equal("Scheme:https; Original:", responseText); + } + } + } +} diff --git a/test/IISExpress.FunctionalTests/OutOfProcess/HttpsTest.cs b/test/IISExpress.FunctionalTests/OutOfProcess/HttpsTest.cs deleted file mode 100644 index 5735393c57..0000000000 --- a/test/IISExpress.FunctionalTests/OutOfProcess/HttpsTest.cs +++ /dev/null @@ -1,148 +0,0 @@ -// 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.Net.Http; -using System.Security.Cryptography.X509Certificates; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; -using Microsoft.AspNetCore.Server.IntegrationTesting; -using Microsoft.AspNetCore.Server.IntegrationTesting.Common; -using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; -using Microsoft.AspNetCore.Testing.xunit; -using Xunit; -using Xunit.Abstractions; - -namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests -{ - // IIS Express preregisteres 44300-44399 ports with SSL bindings. - // So these tests always have to use ports in this range, and we can't rely on OS-allocated ports without a whole lot of ceremony around - // creating self-signed certificates and registering SSL bindings with HTTP.sys - // Test specific to IISExpress - [Collection(PublishedSitesCollection.Name)] - public class HttpsTest : IISFunctionalTestBase - { - private readonly PublishedSitesFixture _fixture; - - public HttpsTest(PublishedSitesFixture fixture) - { - _fixture = fixture; - } - - public static TestMatrix TestVariants - => TestMatrix.ForServers(DeployerSelector.ServerType) - .WithTfms(Tfm.NetCoreApp22, Tfm.Net461) - .WithAllAncmVersions(); - - [ConditionalTheory] - [MemberData(nameof(TestVariants))] - public async Task HttpsHelloWorld(TestVariant variant) - { - var port = TestPortHelper.GetNextSSLPort(); - var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant); - deploymentParameters.ApplicationBaseUriHint = $"https://localhost:{port}/"; - deploymentParameters.AddHttpsToServerConfig(); - - var deploymentResult = await DeployAsync(deploymentParameters); - - var handler = new HttpClientHandler - { - ServerCertificateCustomValidationCallback = (a, b, c, d) => true - }; - var client = deploymentResult.CreateClient(handler); - var response = await client.GetAsync("HttpsHelloWorld"); - var responseText = await response.Content.ReadAsStringAsync(); - Assert.Equal("Scheme:https; Original:http", responseText); - } - - [ConditionalTheory] - [MemberData(nameof(TestVariants))] - public Task HttpsHelloWorld_NoClientCert(TestVariant variant) - { - return HttpsHelloWorldCerts(variant, sendClientCert: false); - } - -#pragma warning disable xUnit1004 // Test methods should not be skipped - [ConditionalTheory(Skip = "Manual test only, selecting a client cert is non-determanistic on different machines.")] - [MemberData(nameof(TestVariants))] -#pragma warning restore xUnit1004 // Test methods should not be skipped - public Task HttpsHelloWorld_ClientCert(TestVariant variant) - { - return HttpsHelloWorldCerts(variant, sendClientCert: true); - } - - private async Task HttpsHelloWorldCerts(TestVariant variant, bool sendClientCert) - { - var port = TestPortHelper.GetNextSSLPort(); - var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant); - deploymentParameters.ApplicationBaseUriHint = $"https://localhost:{port}/"; - deploymentParameters.AddHttpsToServerConfig(); - - var deploymentResult = await DeployAsync(deploymentParameters); - - var handler = new HttpClientHandler - { - ServerCertificateCustomValidationCallback = (a, b, c, d) => true, - ClientCertificateOptions = ClientCertificateOption.Manual - }; - - if (sendClientCert) - { - X509Certificate2 clientCert = FindClientCert(); - Assert.NotNull(clientCert); - handler.ClientCertificates.Add(clientCert); - } - - var client = deploymentResult.CreateClient(handler); - - // Request to base address and check if various parts of the body are rendered & measure the cold startup time. - var response = await client.GetAsync("checkclientcert"); - - var responseText = await response.Content.ReadAsStringAsync(); - if (sendClientCert) - { - Assert.Equal("Scheme:https; Original:http; has cert? True", responseText); - } - else - { - Assert.Equal("Scheme:https; Original:http; has cert? False", responseText); - } - } - - private X509Certificate2 FindClientCert() - { - var store = new X509Store(); - store.Open(OpenFlags.ReadOnly); - - foreach (var cert in store.Certificates) - { - bool isClientAuth = false; - bool isSmartCard = false; - foreach (var extension in cert.Extensions) - { - var eku = extension as X509EnhancedKeyUsageExtension; - if (eku != null) - { - foreach (var oid in eku.EnhancedKeyUsages) - { - if (oid.FriendlyName == "Client Authentication") - { - isClientAuth = true; - } - else if (oid.FriendlyName == "Smart Card Logon") - { - isSmartCard = true; - break; - } - } - } - } - - if (isClientAuth && !isSmartCard) - { - return cert; - } - } - return null; - } - } -} diff --git a/test/WebSites/InProcessWebSite/Startup.cs b/test/WebSites/InProcessWebSite/Startup.cs index ef1863ef2b..39d3b9f765 100644 --- a/test/WebSites/InProcessWebSite/Startup.cs +++ b/test/WebSites/InProcessWebSite/Startup.cs @@ -681,5 +681,8 @@ namespace TestSite { await ctx.Response.WriteAsync(string.Join("|", Environment.GetCommandLineArgs().Skip(1))); } + + public Task HttpsHelloWorld(HttpContext ctx) => + ctx.Response.WriteAsync("Scheme:" + ctx.Request.Scheme + "; Original:" + ctx.Request.Headers["x-original-proto"]); } } diff --git a/test/WebSites/OutOfProcessWebSite/Startup.cs b/test/WebSites/OutOfProcessWebSite/Startup.cs index a796f9a7a3..de54a85a8a 100644 --- a/test/WebSites/OutOfProcessWebSite/Startup.cs +++ b/test/WebSites/OutOfProcessWebSite/Startup.cs @@ -42,10 +42,6 @@ namespace TestSite public Task HttpsHelloWorld(HttpContext ctx) => ctx.Response.WriteAsync("Scheme:" + ctx.Request.Scheme + "; Original:" + ctx.Request.Headers["x-original-proto"]); - public Task CheckClientCert(HttpContext ctx) => - ctx.Response.WriteAsync("Scheme:" + ctx.Request.Scheme + "; Original:" + ctx.Request.Headers["x-original-proto"] - + "; has cert? " + (ctx.Connection.ClientCertificate != null)); - public Task Anonymous(HttpContext context) => context.Response.WriteAsync("Anonymous?" + !context.User.Identity.IsAuthenticated); public Task Restricted(HttpContext context) diff --git a/test/WebSites/shared/SharedStartup/Startup.shared.cs b/test/WebSites/shared/SharedStartup/Startup.shared.cs index a804449029..4c6d2eb4cf 100644 --- a/test/WebSites/shared/SharedStartup/Startup.shared.cs +++ b/test/WebSites/shared/SharedStartup/Startup.shared.cs @@ -43,5 +43,11 @@ namespace TestSite await ctx.Response.WriteAsync(authScheme?.Name ?? "null"); } + + public async Task GetClientCert(HttpContext context) + { + var clientCert = context.Connection.ClientCertificate; + await context.Response.WriteAsync(clientCert != null ? $"Enabled;{clientCert.GetCertHashString()}" : "Disabled"); + } } }