diff --git a/mv_to_src.sh b/mv_to_src.sh new file mode 100644 index 0000000000..2360a52087 --- /dev/null +++ b/mv_to_src.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +dir="$(pwd)" +name="IISIntegration" + +if [ "$skip_src" != false ]; then + echo "Moving $dir into src/$name/" + if [ -d "src/" ]; then + git mv src/ src_tmp/ + fi + mkdir -p "src/$name" + if [ -d "src_tmp/" ]; then + git mv src_tmp/ "src/$name/src/" + fi +fi + +files_to_mv=(NuGetPackageVerifier.json .gitignore README.md version.props Directory.Build.props Directory.Build.targets *.sln shared test tools samples build NuGet benchmarks korebuild.json nuget) +for f in "${files_to_mv[@]}"; do + if [ -e $f ]; then + echo "Moving $f" + git mv $f "src/$name/$f" + fi +done + +files_to_rm=(build.sh build.cmd run.cmd run.sh run.ps1 NuGet.config korebuild-lock.txt .github .vsts-pipelines .vscode .appveyor.yml .travis.yml CONTRIBUTING.md) +for f in "${files_to_rm[@]}"; do + if [ -e $f ]; then + echo "Removing $f" + git rm -r $f + fi +done + +echo "Reorganize source code from aspnet/$name into a subfolder" > .git/COMMIT_EDITMSG +echo "" >> .git/COMMIT_EDITMSG +echo "Prior to reorg, this source existed at https://github.com/aspnet/$name/tree/$(git rev-parse HEAD)" >> .git/COMMIT_EDITMSG \ No newline at end of file diff --git a/src/IISIntegration/.gitignore b/src/IISIntegration/.gitignore new file mode 100644 index 0000000000..c18e14397c --- /dev/null +++ b/src/IISIntegration/.gitignore @@ -0,0 +1,69 @@ +[Oo]bj/ +[Bb]in/ +TestResults/ +.nuget/ +*.sln.ide/ +_ReSharper.*/ +packages/ +artifacts/ +PublishProfiles/ +BenchmarkDotNet.Artifacts/ +*.user +*.suo +*.cache +*.docstates +_ReSharper.* +nuget.exe +project.lock.json +*net45.csproj +*net451.csproj +*k10.csproj +*.psess +*.vsp +*.pidb +*.userprefs +*DS_Store +*.ncrunchsolution +*.*sdf +*.ipch +.vscode/ +*.nuget.props +*.nuget.targets +*.bin +*.vs/ +.testPublish/ + +*.obj +*.tlog +*.CppClean.log +*msbuild.log +gtest.log + +src/*/*/Debug/ +src/*/*/x64/Debug/ +src/*/*/Release/ +src/*/*/x64/Release/ +x64/ + +*vcxproj.filters +*.aps +*.pdb +*.lib +*.idb +*.TMP + +src/*/AspNetCore/aspnetcoremodule.h +src/*/AspNetCore/aspnetcore_msg.h +src/*/AspNetCore/aspnetcore_msg.rc +src/*/*/version.h +src/*/InProcessRequestHandler/version.h +src/*/OutOfProcessRequestHandler/version.h +src/*/CommonLib/aspnetcore_msg.h +src/*/CommonLib/aspnetcore_msg.rc +test/*/Debug +test/*/Release +.build + +*.VC.*db +global.json +msbuild.binlog diff --git a/src/IISIntegration/Directory.Build.props b/src/IISIntegration/Directory.Build.props new file mode 100644 index 0000000000..45a38d3e61 --- /dev/null +++ b/src/IISIntegration/Directory.Build.props @@ -0,0 +1,22 @@ + + + + + + + + + Microsoft ASP.NET Core + https://github.com/aspnet/IISIntegration + git + $(MSBuildThisFileDirectory) + $(MSBuildThisFileDirectory)build\Key.snk + true + true + + false + + + diff --git a/src/IISIntegration/Directory.Build.targets b/src/IISIntegration/Directory.Build.targets new file mode 100644 index 0000000000..73b97f2807 --- /dev/null +++ b/src/IISIntegration/Directory.Build.targets @@ -0,0 +1,9 @@ + + + $(MicrosoftNETCoreApp21PackageVersion) + $(MicrosoftNETCoreApp22PackageVersion) + $(NETStandardLibrary20PackageVersion) + + 99.9 + + diff --git a/src/IISIntegration/IISIntegration.NoV1.sln b/src/IISIntegration/IISIntegration.NoV1.sln new file mode 100644 index 0000000000..f7eb135f73 --- /dev/null +++ b/src/IISIntegration/IISIntegration.NoV1.sln @@ -0,0 +1,463 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27130.2036 +MinimumVisualStudioVersion = 15.0.26730.03 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{04B1EDB6-E967-4D25-89B9-E6F8304038CD}" + ProjectSection(SolutionItems) = preProject + src\Directory.Build.props = src\Directory.Build.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0EF45656-B25D-40D8-959C-726EAF185E60}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets + NuGet.Config = NuGet.Config + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{EF30B533-D715-421A-92B7-92FEF460AC9C}" + ProjectSection(SolutionItems) = preProject + test\Directory.Build.props = test\Directory.Build.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{C74B8F36-FD2F-45C9-9B8A-00E7CF0126A9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IISSample", "samples\IISSample\IISSample.csproj", "{E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.IISIntegration", "src\Microsoft.AspNetCore.Server.IISIntegration\Microsoft.AspNetCore.Server.IISIntegration.csproj", "{8B3446E8-E6A8-4591-AA63-A95837C6E97C}" + ProjectSection(ProjectDependencies) = postProject + {46A8612B-418B-4D70-B3A7-A21DD0627473} = {46A8612B-418B-4D70-B3A7-A21DD0627473} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.IISIntegration.Tests", "test\Microsoft.AspNetCore.Server.IISIntegration.Tests\Microsoft.AspNetCore.Server.IISIntegration.Tests.csproj", "{4106DB10-E09F-480E-9CE6-B39235512EE6}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{7E80C58E-9CC8-450C-8A8D-94FC76428150}" + ProjectSection(SolutionItems) = preProject + build\applicationhost.config = build\applicationhost.config + build\applicationhost.iis.config = build\applicationhost.iis.config + build\Build.Settings = build\Build.Settings + build\Config.Definitions.Props = build\Config.Definitions.Props + build\dependencies.props = build\dependencies.props + build\functional-test-assets.targets = build\functional-test-assets.targets + build\Key.snk = build\Key.snk + build\launchSettings.json = build\launchSettings.json + build\native.targets = build\native.targets + build\repo.props = build\repo.props + build\repo.targets = build\repo.targets + build\sources.props = build\sources.props + build\testsite.props = build\testsite.props + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IISExpress.FunctionalTests", "test\IISExpress.FunctionalTests\IISExpress.FunctionalTests.csproj", "{4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}" + ProjectSection(ProjectDependencies) = postProject + {7F87406C-A3C8-4139-A68D-E4C344294A67} = {7F87406C-A3C8-4139-A68D-E4C344294A67} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NativeIISSample", "samples\NativeIISSample\NativeIISSample.csproj", "{9BC4AFCB-325D-4C81-8228-8CF301CE2F97}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InProcessWebSite", "test\WebSites\InProcessWebSite\InProcessWebSite.csproj", "{679FA2A2-898B-4320-884E-C2D294A97CE1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.IIS", "src\Microsoft.AspNetCore.Server.IIS\Microsoft.AspNetCore.Server.IIS.csproj", "{46A8612B-418B-4D70-B3A7-A21DD0627473}" + ProjectSection(ProjectDependencies) = postProject + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B} = {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B} + {55494E58-E061-4C4C-A0A8-837008E72F85} = {55494E58-E061-4C4C-A0A8-837008E72F85} + {7F87406C-A3C8-4139-A68D-E4C344294A67} = {7F87406C-A3C8-4139-A68D-E4C344294A67} + {D57EA297-6DC2-4BC0-8C91-334863327863} = {D57EA297-6DC2-4BC0-8C91-334863327863} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StressTestWebSite", "test\WebSites\StressTestWebSite\StressTestWebSite.csproj", "{13FD8F12-FFBE-4D01-B4AC-444F2994B04F}" + ProjectSection(ProjectDependencies) = postProject + {46A8612B-418B-4D70-B3A7-A21DD0627473} = {46A8612B-418B-4D70-B3A7-A21DD0627473} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestTasks", "test\TestTasks\TestTasks.csproj", "{064D860B-4D7C-4B1D-918F-E020F1B99E2A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebSites", "WebSites", "{744ACDC6-F6A0-4FF9-9421-F25C5F2DC520}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CommonLibTests", "test\CommonLibTests\CommonLibTests.vcxproj", "{1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AspNetCoreModuleV2", "AspNetCoreModuleV2", "{06CA2C2B-83B0-4D83-905A-E0C74790009E}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "AspNetCore", "src\AspNetCoreModuleV2\AspNetCore\AspNetCore.vcxproj", "{EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CommonLib", "src\AspNetCoreModuleV2\CommonLib\CommonLib.vcxproj", "{55494E58-E061-4C4C-A0A8-837008E72F85}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "IISLib", "src\AspNetCoreModuleV2\IISLib\IISLib.vcxproj", "{09D9D1D6-2951-4E14-BC35-76A23CF9391A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OutOfProcessWebSite", "test\WebSites\OutOfProcessWebSite\OutOfProcessWebSite.csproj", "{42E60F88-E23F-417A-8143-0CCEC05E1D02}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{622D35C9-627B-466E-8D15-752968CC79AF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IIS.Performance", "benchmarks\IIS.Performance\IIS.Performance.csproj", "{48F46909-E76A-4788-BCE1-E543C0E140FE}" + ProjectSection(ProjectDependencies) = postProject + {46A8612B-418B-4D70-B3A7-A21DD0627473} = {46A8612B-418B-4D70-B3A7-A21DD0627473} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "InProcessRequestHandler", "src\AspNetCoreModuleV2\InProcessRequestHandler\InProcessRequestHandler.vcxproj", "{D57EA297-6DC2-4BC0-8C91-334863327863}" + ProjectSection(ProjectDependencies) = postProject + {09D9D1D6-2951-4E14-BC35-76A23CF9391A} = {09D9D1D6-2951-4E14-BC35-76A23CF9391A} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "OutOfProcessRequestHandler", "src\AspNetCoreModuleV2\OutOfProcessRequestHandler\OutOfProcessRequestHandler.vcxproj", "{7F87406C-A3C8-4139-A68D-E4C344294A67}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "gtest", "test\gtest\gtest.vcxproj", "{CAC1267B-8778-4257-AAC6-CAF481723B01}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RequestHandlerLib", "src\AspNetCoreModuleV2\RequestHandlerLib\RequestHandlerLib.vcxproj", "{1533E271-F61B-441B-8B74-59FB61DF0552}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IIS.FunctionalTests", "test\IIS.FunctionalTests\IIS.FunctionalTests.csproj", "{D182103F-8405-4647-B158-C36F598657EF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.IntegrationTesting.IIS", "src\Microsoft.AspNetCore.Server.IntegrationTesting.IIS\Microsoft.AspNetCore.Server.IntegrationTesting.IIS.csproj", "{34135ED7-313D-4E68-860C-D6B51AA28523}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IIS.Tests", "test\IIS.Tests\IIS.Tests.csproj", "{C0310D84-BC2F-4B2E-870E-D35044DB3E3E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.Tests", "test\Common.Tests\Common.Tests.csproj", "{D17B7B35-5361-4A50-B499-E03E5C3CC095}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IIS.BackwardsCompatibility.FunctionalTests", "test\IIS.BackwardsCompatibility.FunctionalTests\IIS.BackwardsCompatibility.FunctionalTests.csproj", "{582B07BC-73F4-4689-8557-B039298BD82C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IIS.ForwardsCompatibility.FunctionalTests", "test\IIS.ForwardsCompatibility.FunctionalTests\IIS.ForwardsCompatibility.FunctionalTests.csproj", "{D1EA5D99-28FD-4197-81DE-17098846B38B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InProcessForwardsCompatWebSite", "test\WebSites\InProcessForwardsCompatWebSite\InProcessWebSite.csproj", "{BBBC85B2-5D7A-4D09-90B1-8DBCC9059493}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.Debug|x64.ActiveCfg = Debug|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.Debug|x64.Build.0 = Debug|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.Debug|x86.ActiveCfg = Debug|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.Debug|x86.Build.0 = Debug|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.Release|Any CPU.Build.0 = Release|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.Release|x64.ActiveCfg = Release|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.Release|x64.Build.0 = Release|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.Release|x86.ActiveCfg = Release|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.Release|x86.Build.0 = Release|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.Debug|x64.ActiveCfg = Debug|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.Debug|x64.Build.0 = Debug|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.Debug|x86.ActiveCfg = Debug|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.Debug|x86.Build.0 = Debug|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.Release|Any CPU.Build.0 = Release|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.Release|x64.ActiveCfg = Release|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.Release|x64.Build.0 = Release|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.Release|x86.ActiveCfg = Release|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.Release|x86.Build.0 = Release|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.Debug|x64.ActiveCfg = Debug|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.Debug|x64.Build.0 = Debug|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.Debug|x86.ActiveCfg = Debug|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.Debug|x86.Build.0 = Debug|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.Release|Any CPU.Build.0 = Release|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.Release|x64.ActiveCfg = Release|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.Release|x64.Build.0 = Release|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.Release|x86.ActiveCfg = Release|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.Release|x86.Build.0 = Release|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.Debug|x64.ActiveCfg = Debug|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.Debug|x64.Build.0 = Debug|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.Debug|x86.ActiveCfg = Debug|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.Debug|x86.Build.0 = Debug|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.Release|Any CPU.Build.0 = Release|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.Release|x64.ActiveCfg = Release|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.Release|x64.Build.0 = Release|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.Release|x86.ActiveCfg = Release|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.Release|x86.Build.0 = Release|Any CPU + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.Debug|Any CPU.ActiveCfg = Debug|x64 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.Debug|Any CPU.Build.0 = Debug|x64 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.Debug|x64.ActiveCfg = Debug|x64 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.Debug|x64.Build.0 = Debug|x64 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.Debug|x86.ActiveCfg = Debug|x86 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.Debug|x86.Build.0 = Debug|x86 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.Release|Any CPU.ActiveCfg = Release|x64 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.Release|Any CPU.Build.0 = Release|x64 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.Release|x64.ActiveCfg = Release|x64 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.Release|x64.Build.0 = Release|x64 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.Release|x86.ActiveCfg = Release|x86 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.Release|x86.Build.0 = Release|x86 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.Debug|Any CPU.ActiveCfg = Debug|x64 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.Debug|Any CPU.Build.0 = Debug|x64 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.Debug|x64.ActiveCfg = Debug|x64 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.Debug|x64.Build.0 = Debug|x64 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.Debug|x86.ActiveCfg = Debug|x86 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.Debug|x86.Build.0 = Debug|x86 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.Release|Any CPU.ActiveCfg = Release|x64 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.Release|Any CPU.Build.0 = Release|x64 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.Release|x64.ActiveCfg = Release|x64 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.Release|x64.Build.0 = Release|x64 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.Release|x86.ActiveCfg = Release|x86 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.Release|x86.Build.0 = Release|x86 + {46A8612B-418B-4D70-B3A7-A21DD0627473}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.Debug|Any CPU.Build.0 = Debug|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.Debug|x64.ActiveCfg = Debug|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.Debug|x64.Build.0 = Debug|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.Debug|x86.ActiveCfg = Debug|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.Debug|x86.Build.0 = Debug|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.Release|Any CPU.ActiveCfg = Release|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.Release|Any CPU.Build.0 = Release|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.Release|x64.ActiveCfg = Release|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.Release|x64.Build.0 = Release|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.Release|x86.ActiveCfg = Release|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.Release|x86.Build.0 = Release|Any CPU + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Debug|Any CPU.ActiveCfg = Debug|x64 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Debug|Any CPU.Build.0 = Debug|x64 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Debug|x64.ActiveCfg = Debug|x64 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Debug|x64.Build.0 = Debug|x64 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Debug|x86.ActiveCfg = Debug|x86 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Debug|x86.Build.0 = Debug|x86 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Release|Any CPU.ActiveCfg = Release|x64 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Release|Any CPU.Build.0 = Release|x64 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Release|x64.ActiveCfg = Release|x64 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Release|x64.Build.0 = Release|x64 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Release|x86.ActiveCfg = Release|x86 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Release|x86.Build.0 = Release|x86 + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.Debug|x64.ActiveCfg = Debug|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.Debug|x64.Build.0 = Debug|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.Debug|x86.ActiveCfg = Debug|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.Debug|x86.Build.0 = Debug|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.Release|Any CPU.Build.0 = Release|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.Release|x64.ActiveCfg = Release|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.Release|x64.Build.0 = Release|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.Release|x86.ActiveCfg = Release|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.Release|x86.Build.0 = Release|Any CPU + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.Debug|x64.ActiveCfg = Debug|x64 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.Debug|x64.Build.0 = Debug|x64 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.Debug|x86.ActiveCfg = Debug|Win32 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.Debug|x86.Build.0 = Debug|Win32 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.Release|Any CPU.ActiveCfg = Release|Win32 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.Release|x64.ActiveCfg = Release|x64 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.Release|x64.Build.0 = Release|x64 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.Release|x86.ActiveCfg = Release|Win32 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.Release|x86.Build.0 = Release|Win32 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.Debug|x64.ActiveCfg = Debug|x64 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.Debug|x64.Build.0 = Debug|x64 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.Debug|x86.ActiveCfg = Debug|Win32 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.Debug|x86.Build.0 = Debug|Win32 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.Release|Any CPU.ActiveCfg = Release|Win32 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.Release|x64.ActiveCfg = Release|x64 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.Release|x64.Build.0 = Release|x64 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.Release|x86.ActiveCfg = Release|Win32 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.Release|x86.Build.0 = Release|Win32 + {55494E58-E061-4C4C-A0A8-837008E72F85}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {55494E58-E061-4C4C-A0A8-837008E72F85}.Debug|x64.ActiveCfg = Debug|x64 + {55494E58-E061-4C4C-A0A8-837008E72F85}.Debug|x64.Build.0 = Debug|x64 + {55494E58-E061-4C4C-A0A8-837008E72F85}.Debug|x86.ActiveCfg = Debug|Win32 + {55494E58-E061-4C4C-A0A8-837008E72F85}.Debug|x86.Build.0 = Debug|Win32 + {55494E58-E061-4C4C-A0A8-837008E72F85}.Release|Any CPU.ActiveCfg = Release|Win32 + {55494E58-E061-4C4C-A0A8-837008E72F85}.Release|x64.ActiveCfg = Release|x64 + {55494E58-E061-4C4C-A0A8-837008E72F85}.Release|x64.Build.0 = Release|x64 + {55494E58-E061-4C4C-A0A8-837008E72F85}.Release|x86.ActiveCfg = Release|Win32 + {55494E58-E061-4C4C-A0A8-837008E72F85}.Release|x86.Build.0 = Release|Win32 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.Debug|x64.ActiveCfg = Debug|x64 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.Debug|x64.Build.0 = Debug|x64 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.Debug|x86.ActiveCfg = Debug|Win32 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.Debug|x86.Build.0 = Debug|Win32 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.Release|Any CPU.ActiveCfg = Release|Win32 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.Release|x64.ActiveCfg = Release|x64 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.Release|x64.Build.0 = Release|x64 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.Release|x86.ActiveCfg = Release|Win32 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.Release|x86.Build.0 = Release|Win32 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.Debug|Any CPU.ActiveCfg = Debug|x64 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.Debug|Any CPU.Build.0 = Debug|x64 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.Debug|x64.ActiveCfg = Debug|x64 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.Debug|x64.Build.0 = Debug|x64 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.Debug|x86.ActiveCfg = Debug|x86 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.Debug|x86.Build.0 = Debug|x86 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.Release|Any CPU.ActiveCfg = Release|x64 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.Release|Any CPU.Build.0 = Release|x64 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.Release|x64.ActiveCfg = Release|x64 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.Release|x64.Build.0 = Release|x64 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.Release|x86.ActiveCfg = Release|x86 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.Release|x86.Build.0 = Release|x86 + {48F46909-E76A-4788-BCE1-E543C0E140FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.Debug|x64.ActiveCfg = Debug|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.Debug|x64.Build.0 = Debug|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.Debug|x86.ActiveCfg = Debug|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.Debug|x86.Build.0 = Debug|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.Release|Any CPU.Build.0 = Release|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.Release|x64.ActiveCfg = Release|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.Release|x64.Build.0 = Release|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.Release|x86.ActiveCfg = Release|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.Release|x86.Build.0 = Release|Any CPU + {D57EA297-6DC2-4BC0-8C91-334863327863}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {D57EA297-6DC2-4BC0-8C91-334863327863}.Debug|x64.ActiveCfg = Debug|x64 + {D57EA297-6DC2-4BC0-8C91-334863327863}.Debug|x64.Build.0 = Debug|x64 + {D57EA297-6DC2-4BC0-8C91-334863327863}.Debug|x86.ActiveCfg = Debug|Win32 + {D57EA297-6DC2-4BC0-8C91-334863327863}.Debug|x86.Build.0 = Debug|Win32 + {D57EA297-6DC2-4BC0-8C91-334863327863}.Release|Any CPU.ActiveCfg = Release|Win32 + {D57EA297-6DC2-4BC0-8C91-334863327863}.Release|x64.ActiveCfg = Release|x64 + {D57EA297-6DC2-4BC0-8C91-334863327863}.Release|x64.Build.0 = Release|x64 + {D57EA297-6DC2-4BC0-8C91-334863327863}.Release|x86.ActiveCfg = Release|Win32 + {D57EA297-6DC2-4BC0-8C91-334863327863}.Release|x86.Build.0 = Release|Win32 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.Debug|x64.ActiveCfg = Debug|x64 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.Debug|x64.Build.0 = Debug|x64 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.Debug|x86.ActiveCfg = Debug|Win32 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.Debug|x86.Build.0 = Debug|Win32 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.Release|Any CPU.ActiveCfg = Release|Win32 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.Release|x64.ActiveCfg = Release|x64 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.Release|x64.Build.0 = Release|x64 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.Release|x86.ActiveCfg = Release|Win32 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.Release|x86.Build.0 = Release|Win32 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.Debug|x64.ActiveCfg = Debug|x64 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.Debug|x64.Build.0 = Debug|x64 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.Debug|x86.ActiveCfg = Debug|Win32 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.Debug|x86.Build.0 = Debug|Win32 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.Release|Any CPU.ActiveCfg = Release|Win32 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.Release|x64.ActiveCfg = Release|x64 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.Release|x64.Build.0 = Release|x64 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.Release|x86.ActiveCfg = Release|Win32 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.Release|x86.Build.0 = Release|Win32 + {1533E271-F61B-441B-8B74-59FB61DF0552}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {1533E271-F61B-441B-8B74-59FB61DF0552}.Debug|x64.ActiveCfg = Debug|x64 + {1533E271-F61B-441B-8B74-59FB61DF0552}.Debug|x64.Build.0 = Debug|x64 + {1533E271-F61B-441B-8B74-59FB61DF0552}.Debug|x86.ActiveCfg = Debug|Win32 + {1533E271-F61B-441B-8B74-59FB61DF0552}.Debug|x86.Build.0 = Debug|Win32 + {1533E271-F61B-441B-8B74-59FB61DF0552}.Release|Any CPU.ActiveCfg = Release|Win32 + {1533E271-F61B-441B-8B74-59FB61DF0552}.Release|x64.ActiveCfg = Release|x64 + {1533E271-F61B-441B-8B74-59FB61DF0552}.Release|x64.Build.0 = Release|x64 + {1533E271-F61B-441B-8B74-59FB61DF0552}.Release|x86.ActiveCfg = Release|Win32 + {1533E271-F61B-441B-8B74-59FB61DF0552}.Release|x86.Build.0 = Release|Win32 + {D182103F-8405-4647-B158-C36F598657EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D182103F-8405-4647-B158-C36F598657EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D182103F-8405-4647-B158-C36F598657EF}.Debug|x64.ActiveCfg = Debug|Any CPU + {D182103F-8405-4647-B158-C36F598657EF}.Debug|x64.Build.0 = Debug|Any CPU + {D182103F-8405-4647-B158-C36F598657EF}.Debug|x86.ActiveCfg = Debug|Any CPU + {D182103F-8405-4647-B158-C36F598657EF}.Debug|x86.Build.0 = Debug|Any CPU + {D182103F-8405-4647-B158-C36F598657EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D182103F-8405-4647-B158-C36F598657EF}.Release|Any CPU.Build.0 = Release|Any CPU + {D182103F-8405-4647-B158-C36F598657EF}.Release|x64.ActiveCfg = Release|Any CPU + {D182103F-8405-4647-B158-C36F598657EF}.Release|x64.Build.0 = Release|Any CPU + {D182103F-8405-4647-B158-C36F598657EF}.Release|x86.ActiveCfg = Release|Any CPU + {D182103F-8405-4647-B158-C36F598657EF}.Release|x86.Build.0 = Release|Any CPU + {34135ED7-313D-4E68-860C-D6B51AA28523}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34135ED7-313D-4E68-860C-D6B51AA28523}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34135ED7-313D-4E68-860C-D6B51AA28523}.Debug|x64.ActiveCfg = Debug|Any CPU + {34135ED7-313D-4E68-860C-D6B51AA28523}.Debug|x64.Build.0 = Debug|Any CPU + {34135ED7-313D-4E68-860C-D6B51AA28523}.Debug|x86.ActiveCfg = Debug|Any CPU + {34135ED7-313D-4E68-860C-D6B51AA28523}.Debug|x86.Build.0 = Debug|Any CPU + {34135ED7-313D-4E68-860C-D6B51AA28523}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34135ED7-313D-4E68-860C-D6B51AA28523}.Release|Any CPU.Build.0 = Release|Any CPU + {34135ED7-313D-4E68-860C-D6B51AA28523}.Release|x64.ActiveCfg = Release|Any CPU + {34135ED7-313D-4E68-860C-D6B51AA28523}.Release|x64.Build.0 = Release|Any CPU + {34135ED7-313D-4E68-860C-D6B51AA28523}.Release|x86.ActiveCfg = Release|Any CPU + {34135ED7-313D-4E68-860C-D6B51AA28523}.Release|x86.Build.0 = Release|Any CPU + {C0310D84-BC2F-4B2E-870E-D35044DB3E3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C0310D84-BC2F-4B2E-870E-D35044DB3E3E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0310D84-BC2F-4B2E-870E-D35044DB3E3E}.Debug|x64.ActiveCfg = Debug|Any CPU + {C0310D84-BC2F-4B2E-870E-D35044DB3E3E}.Debug|x64.Build.0 = Debug|Any CPU + {C0310D84-BC2F-4B2E-870E-D35044DB3E3E}.Debug|x86.ActiveCfg = Debug|Any CPU + {C0310D84-BC2F-4B2E-870E-D35044DB3E3E}.Debug|x86.Build.0 = Debug|Any CPU + {C0310D84-BC2F-4B2E-870E-D35044DB3E3E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C0310D84-BC2F-4B2E-870E-D35044DB3E3E}.Release|Any CPU.Build.0 = Release|Any CPU + {C0310D84-BC2F-4B2E-870E-D35044DB3E3E}.Release|x64.ActiveCfg = Release|Any CPU + {C0310D84-BC2F-4B2E-870E-D35044DB3E3E}.Release|x64.Build.0 = Release|Any CPU + {C0310D84-BC2F-4B2E-870E-D35044DB3E3E}.Release|x86.ActiveCfg = Release|Any CPU + {C0310D84-BC2F-4B2E-870E-D35044DB3E3E}.Release|x86.Build.0 = Release|Any CPU + {D17B7B35-5361-4A50-B499-E03E5C3CC095}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D17B7B35-5361-4A50-B499-E03E5C3CC095}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D17B7B35-5361-4A50-B499-E03E5C3CC095}.Debug|x64.ActiveCfg = Debug|Any CPU + {D17B7B35-5361-4A50-B499-E03E5C3CC095}.Debug|x64.Build.0 = Debug|Any CPU + {D17B7B35-5361-4A50-B499-E03E5C3CC095}.Debug|x86.ActiveCfg = Debug|Any CPU + {D17B7B35-5361-4A50-B499-E03E5C3CC095}.Debug|x86.Build.0 = Debug|Any CPU + {D17B7B35-5361-4A50-B499-E03E5C3CC095}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D17B7B35-5361-4A50-B499-E03E5C3CC095}.Release|Any CPU.Build.0 = Release|Any CPU + {D17B7B35-5361-4A50-B499-E03E5C3CC095}.Release|x64.ActiveCfg = Release|Any CPU + {D17B7B35-5361-4A50-B499-E03E5C3CC095}.Release|x64.Build.0 = Release|Any CPU + {D17B7B35-5361-4A50-B499-E03E5C3CC095}.Release|x86.ActiveCfg = Release|Any CPU + {D17B7B35-5361-4A50-B499-E03E5C3CC095}.Release|x86.Build.0 = Release|Any CPU + {582B07BC-73F4-4689-8557-B039298BD82C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {582B07BC-73F4-4689-8557-B039298BD82C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {582B07BC-73F4-4689-8557-B039298BD82C}.Debug|x64.ActiveCfg = Debug|Any CPU + {582B07BC-73F4-4689-8557-B039298BD82C}.Debug|x64.Build.0 = Debug|Any CPU + {582B07BC-73F4-4689-8557-B039298BD82C}.Debug|x86.ActiveCfg = Debug|Any CPU + {582B07BC-73F4-4689-8557-B039298BD82C}.Debug|x86.Build.0 = Debug|Any CPU + {582B07BC-73F4-4689-8557-B039298BD82C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {582B07BC-73F4-4689-8557-B039298BD82C}.Release|Any CPU.Build.0 = Release|Any CPU + {582B07BC-73F4-4689-8557-B039298BD82C}.Release|x64.ActiveCfg = Release|Any CPU + {582B07BC-73F4-4689-8557-B039298BD82C}.Release|x64.Build.0 = Release|Any CPU + {582B07BC-73F4-4689-8557-B039298BD82C}.Release|x86.ActiveCfg = Release|Any CPU + {582B07BC-73F4-4689-8557-B039298BD82C}.Release|x86.Build.0 = Release|Any CPU + {D1EA5D99-28FD-4197-81DE-17098846B38B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1EA5D99-28FD-4197-81DE-17098846B38B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1EA5D99-28FD-4197-81DE-17098846B38B}.Debug|x64.ActiveCfg = Debug|Any CPU + {D1EA5D99-28FD-4197-81DE-17098846B38B}.Debug|x64.Build.0 = Debug|Any CPU + {D1EA5D99-28FD-4197-81DE-17098846B38B}.Debug|x86.ActiveCfg = Debug|Any CPU + {D1EA5D99-28FD-4197-81DE-17098846B38B}.Debug|x86.Build.0 = Debug|Any CPU + {D1EA5D99-28FD-4197-81DE-17098846B38B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1EA5D99-28FD-4197-81DE-17098846B38B}.Release|Any CPU.Build.0 = Release|Any CPU + {D1EA5D99-28FD-4197-81DE-17098846B38B}.Release|x64.ActiveCfg = Release|Any CPU + {D1EA5D99-28FD-4197-81DE-17098846B38B}.Release|x64.Build.0 = Release|Any CPU + {D1EA5D99-28FD-4197-81DE-17098846B38B}.Release|x86.ActiveCfg = Release|Any CPU + {D1EA5D99-28FD-4197-81DE-17098846B38B}.Release|x86.Build.0 = Release|Any CPU + {BBBC85B2-5D7A-4D09-90B1-8DBCC9059493}.Debug|Any CPU.ActiveCfg = Debug|x86 + {BBBC85B2-5D7A-4D09-90B1-8DBCC9059493}.Debug|x64.ActiveCfg = Debug|x64 + {BBBC85B2-5D7A-4D09-90B1-8DBCC9059493}.Debug|x64.Build.0 = Debug|x64 + {BBBC85B2-5D7A-4D09-90B1-8DBCC9059493}.Debug|x86.ActiveCfg = Debug|x86 + {BBBC85B2-5D7A-4D09-90B1-8DBCC9059493}.Debug|x86.Build.0 = Debug|x86 + {BBBC85B2-5D7A-4D09-90B1-8DBCC9059493}.Release|Any CPU.ActiveCfg = Release|x86 + {BBBC85B2-5D7A-4D09-90B1-8DBCC9059493}.Release|x64.ActiveCfg = Release|x64 + {BBBC85B2-5D7A-4D09-90B1-8DBCC9059493}.Release|x64.Build.0 = Release|x64 + {BBBC85B2-5D7A-4D09-90B1-8DBCC9059493}.Release|x86.ActiveCfg = Release|x86 + {BBBC85B2-5D7A-4D09-90B1-8DBCC9059493}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64} = {C74B8F36-FD2F-45C9-9B8A-00E7CF0126A9} + {8B3446E8-E6A8-4591-AA63-A95837C6E97C} = {04B1EDB6-E967-4D25-89B9-E6F8304038CD} + {4106DB10-E09F-480E-9CE6-B39235512EE6} = {EF30B533-D715-421A-92B7-92FEF460AC9C} + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA} = {EF30B533-D715-421A-92B7-92FEF460AC9C} + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97} = {C74B8F36-FD2F-45C9-9B8A-00E7CF0126A9} + {679FA2A2-898B-4320-884E-C2D294A97CE1} = {744ACDC6-F6A0-4FF9-9421-F25C5F2DC520} + {46A8612B-418B-4D70-B3A7-A21DD0627473} = {04B1EDB6-E967-4D25-89B9-E6F8304038CD} + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F} = {744ACDC6-F6A0-4FF9-9421-F25C5F2DC520} + {064D860B-4D7C-4B1D-918F-E020F1B99E2A} = {EF30B533-D715-421A-92B7-92FEF460AC9C} + {744ACDC6-F6A0-4FF9-9421-F25C5F2DC520} = {EF30B533-D715-421A-92B7-92FEF460AC9C} + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1} = {EF30B533-D715-421A-92B7-92FEF460AC9C} + {06CA2C2B-83B0-4D83-905A-E0C74790009E} = {04B1EDB6-E967-4D25-89B9-E6F8304038CD} + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B} = {06CA2C2B-83B0-4D83-905A-E0C74790009E} + {55494E58-E061-4C4C-A0A8-837008E72F85} = {06CA2C2B-83B0-4D83-905A-E0C74790009E} + {09D9D1D6-2951-4E14-BC35-76A23CF9391A} = {06CA2C2B-83B0-4D83-905A-E0C74790009E} + {42E60F88-E23F-417A-8143-0CCEC05E1D02} = {744ACDC6-F6A0-4FF9-9421-F25C5F2DC520} + {48F46909-E76A-4788-BCE1-E543C0E140FE} = {622D35C9-627B-466E-8D15-752968CC79AF} + {D57EA297-6DC2-4BC0-8C91-334863327863} = {06CA2C2B-83B0-4D83-905A-E0C74790009E} + {7F87406C-A3C8-4139-A68D-E4C344294A67} = {06CA2C2B-83B0-4D83-905A-E0C74790009E} + {CAC1267B-8778-4257-AAC6-CAF481723B01} = {EF30B533-D715-421A-92B7-92FEF460AC9C} + {1533E271-F61B-441B-8B74-59FB61DF0552} = {06CA2C2B-83B0-4D83-905A-E0C74790009E} + {D182103F-8405-4647-B158-C36F598657EF} = {EF30B533-D715-421A-92B7-92FEF460AC9C} + {34135ED7-313D-4E68-860C-D6B51AA28523} = {04B1EDB6-E967-4D25-89B9-E6F8304038CD} + {C0310D84-BC2F-4B2E-870E-D35044DB3E3E} = {EF30B533-D715-421A-92B7-92FEF460AC9C} + {D17B7B35-5361-4A50-B499-E03E5C3CC095} = {EF30B533-D715-421A-92B7-92FEF460AC9C} + {582B07BC-73F4-4689-8557-B039298BD82C} = {EF30B533-D715-421A-92B7-92FEF460AC9C} + {D1EA5D99-28FD-4197-81DE-17098846B38B} = {EF30B533-D715-421A-92B7-92FEF460AC9C} + {BBBC85B2-5D7A-4D09-90B1-8DBCC9059493} = {744ACDC6-F6A0-4FF9-9421-F25C5F2DC520} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {DB4F868D-E1AE-4FD7-9333-69FA15B268C5} + EndGlobalSection +EndGlobal diff --git a/src/IISIntegration/IISIntegration.sln b/src/IISIntegration/IISIntegration.sln new file mode 100644 index 0000000000..440001a8ac --- /dev/null +++ b/src/IISIntegration/IISIntegration.sln @@ -0,0 +1,742 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27130.2036 +MinimumVisualStudioVersion = 15.0.26730.03 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{04B1EDB6-E967-4D25-89B9-E6F8304038CD}" + ProjectSection(SolutionItems) = preProject + src\Directory.Build.props = src\Directory.Build.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0EF45656-B25D-40D8-959C-726EAF185E60}" + ProjectSection(SolutionItems) = preProject + .appveyor.yml = .appveyor.yml + .editorconfig = .editorconfig + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets + NuGet.Config = NuGet.Config + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{EF30B533-D715-421A-92B7-92FEF460AC9C}" + ProjectSection(SolutionItems) = preProject + test\Directory.Build.props = test\Directory.Build.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{C74B8F36-FD2F-45C9-9B8A-00E7CF0126A9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IISSample", "samples\IISSample\IISSample.csproj", "{E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.IISIntegration", "src\Microsoft.AspNetCore.Server.IISIntegration\Microsoft.AspNetCore.Server.IISIntegration.csproj", "{8B3446E8-E6A8-4591-AA63-A95837C6E97C}" + ProjectSection(ProjectDependencies) = postProject + {46A8612B-418B-4D70-B3A7-A21DD0627473} = {46A8612B-418B-4D70-B3A7-A21DD0627473} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.IISIntegration.Tests", "test\Microsoft.AspNetCore.Server.IISIntegration.Tests\Microsoft.AspNetCore.Server.IISIntegration.Tests.csproj", "{4106DB10-E09F-480E-9CE6-B39235512EE6}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{7E80C58E-9CC8-450C-8A8D-94FC76428150}" + ProjectSection(SolutionItems) = preProject + build\applicationhost.config = build\applicationhost.config + build\applicationhost.iis.config = build\applicationhost.iis.config + build\build.msbuild = build\build.msbuild + build\Build.Settings = build\Build.Settings + build\Config.Definitions.Props = build\Config.Definitions.Props + build\dependencies.props = build\dependencies.props + build\Key.snk = build\Key.snk + build\launchSettings.json = build\launchSettings.json + build\native.targets = build\native.targets + build\repo.props = build\repo.props + build\repo.targets = build\repo.targets + build\sources.props = build\sources.props + build\testsite.props = build\testsite.props + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IISExpress.FunctionalTests", "test\IISExpress.FunctionalTests\IISExpress.FunctionalTests.csproj", "{4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}" + ProjectSection(ProjectDependencies) = postProject + {7F87406C-A3C8-4139-A68D-E4C344294A67} = {7F87406C-A3C8-4139-A68D-E4C344294A67} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NativeIISSample", "samples\NativeIISSample\NativeIISSample.csproj", "{9BC4AFCB-325D-4C81-8228-8CF301CE2F97}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InProcessWebSite", "test\WebSites\InProcessWebSite\InProcessWebSite.csproj", "{679FA2A2-898B-4320-884E-C2D294A97CE1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.IIS", "src\Microsoft.AspNetCore.Server.IIS\Microsoft.AspNetCore.Server.IIS.csproj", "{46A8612B-418B-4D70-B3A7-A21DD0627473}" + ProjectSection(ProjectDependencies) = postProject + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B} = {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B} + {55494E58-E061-4C4C-A0A8-837008E72F85} = {55494E58-E061-4C4C-A0A8-837008E72F85} + {7F87406C-A3C8-4139-A68D-E4C344294A67} = {7F87406C-A3C8-4139-A68D-E4C344294A67} + {D57EA297-6DC2-4BC0-8C91-334863327863} = {D57EA297-6DC2-4BC0-8C91-334863327863} + {439824F9-1455-4CC4-BD79-B44FA0A16552} = {439824F9-1455-4CC4-BD79-B44FA0A16552} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StressTestWebSite", "test\WebSites\StressTestWebSite\StressTestWebSite.csproj", "{13FD8F12-FFBE-4D01-B4AC-444F2994B04F}" + ProjectSection(ProjectDependencies) = postProject + {46A8612B-418B-4D70-B3A7-A21DD0627473} = {46A8612B-418B-4D70-B3A7-A21DD0627473} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestTasks", "test\TestTasks\TestTasks.csproj", "{064D860B-4D7C-4B1D-918F-E020F1B99E2A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebSites", "WebSites", "{744ACDC6-F6A0-4FF9-9421-F25C5F2DC520}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CommonLibTests", "test\CommonLibTests\CommonLibTests.vcxproj", "{1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AspNetCoreModuleV1", "AspNetCoreModuleV1", "{16E521CE-77F1-4B1C-A183-520A41C4F372}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AspNetCoreModuleV2", "AspNetCoreModuleV2", "{06CA2C2B-83B0-4D83-905A-E0C74790009E}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "IISLib", "src\AspNetCoreModuleV1\IISLib\IISLib.vcxproj", "{4787A64F-9A3E-4867-A55A-70CB4B2B2FFE}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "AspNetCore", "src\AspNetCoreModuleV1\AspNetCore\AspNetCore.vcxproj", "{439824F9-1455-4CC4-BD79-B44FA0A16552}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "AspNetCore", "src\AspNetCoreModuleV2\AspNetCore\AspNetCore.vcxproj", "{EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CommonLib", "src\AspNetCoreModuleV2\CommonLib\CommonLib.vcxproj", "{55494E58-E061-4C4C-A0A8-837008E72F85}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "IISLib", "src\AspNetCoreModuleV2\IISLib\IISLib.vcxproj", "{09D9D1D6-2951-4E14-BC35-76A23CF9391A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OutOfProcessWebSite", "test\WebSites\OutOfProcessWebSite\OutOfProcessWebSite.csproj", "{42E60F88-E23F-417A-8143-0CCEC05E1D02}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{622D35C9-627B-466E-8D15-752968CC79AF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IIS.Performance", "benchmarks\IIS.Performance\IIS.Performance.csproj", "{48F46909-E76A-4788-BCE1-E543C0E140FE}" + ProjectSection(ProjectDependencies) = postProject + {46A8612B-418B-4D70-B3A7-A21DD0627473} = {46A8612B-418B-4D70-B3A7-A21DD0627473} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "InProcessRequestHandler", "src\AspNetCoreModuleV2\InProcessRequestHandler\InProcessRequestHandler.vcxproj", "{D57EA297-6DC2-4BC0-8C91-334863327863}" + ProjectSection(ProjectDependencies) = postProject + {09D9D1D6-2951-4E14-BC35-76A23CF9391A} = {09D9D1D6-2951-4E14-BC35-76A23CF9391A} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "OutOfProcessRequestHandler", "src\AspNetCoreModuleV2\OutOfProcessRequestHandler\OutOfProcessRequestHandler.vcxproj", "{7F87406C-A3C8-4139-A68D-E4C344294A67}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "gtest", "test\gtest\gtest.vcxproj", "{CAC1267B-8778-4257-AAC6-CAF481723B01}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RequestHandlerLib", "src\AspNetCoreModuleV2\RequestHandlerLib\RequestHandlerLib.vcxproj", "{1533E271-F61B-441B-8B74-59FB61DF0552}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IIS.FunctionalTests", "test\IIS.FunctionalTests\IIS.FunctionalTests.csproj", "{1F0C8D9B-F47B-41F3-9FC9-6954B6DC7712}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.IntegrationTesting.IIS", "src\Microsoft.AspNetCore.Server.IntegrationTesting.IIS\Microsoft.AspNetCore.Server.IntegrationTesting.IIS.csproj", "{CE4FB142-91FB-4B34-BC96-A31120EF4009}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IIS.Tests", "test\IIS.Tests\IIS.Tests.csproj", "{A091777D-66B3-42E1-B95C-85322DE40706}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.Tests", "test\Common.Tests\Common.Tests.csproj", "{A641A208-2974-4E48-BCFF-54E3AAFA4FB9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IIS.BackwardsCompatibility.FunctionalTests", "test\IIS.BackwardsCompatibility.FunctionalTests\IIS.BackwardsCompatibility.FunctionalTests.csproj", "{28055B05-25D4-4F17-9F36-A1D09FDDA607}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IIS.ForwardsCompatibility.FunctionalTests", "test\IIS.ForwardsCompatibility.FunctionalTests\IIS.ForwardsCompatibility.FunctionalTests.csproj", "{F6FAA65F-AA29-4DDA-AA89-C16AF4A69F9F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InProcessForwardsCompatWebSite", "test\WebSites\InProcessForwardsCompatWebSite\InProcessWebSite.csproj", "{980DAB60-6471-46EC-82EE-B457D91C3789}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + NativeDebug|Any CPU = NativeDebug|Any CPU + NativeDebug|x64 = NativeDebug|x64 + NativeDebug|x86 = NativeDebug|x86 + NativeRelease|Any CPU = NativeRelease|Any CPU + NativeRelease|x64 = NativeRelease|x64 + NativeRelease|x86 = NativeRelease|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.Debug|x64.ActiveCfg = Debug|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.Debug|x64.Build.0 = Debug|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.Debug|x86.ActiveCfg = Debug|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.Debug|x86.Build.0 = Debug|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.NativeDebug|Any CPU.ActiveCfg = Debug|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.NativeDebug|Any CPU.Build.0 = Debug|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.NativeDebug|x64.ActiveCfg = Debug|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.NativeDebug|x86.ActiveCfg = Debug|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.NativeRelease|Any CPU.ActiveCfg = Release|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.NativeRelease|Any CPU.Build.0 = Release|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.NativeRelease|x64.ActiveCfg = Release|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.NativeRelease|x86.ActiveCfg = Release|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.Release|Any CPU.Build.0 = Release|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.Release|x64.ActiveCfg = Release|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.Release|x64.Build.0 = Release|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.Release|x86.ActiveCfg = Release|Any CPU + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64}.Release|x86.Build.0 = Release|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.Debug|x64.ActiveCfg = Debug|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.Debug|x64.Build.0 = Debug|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.Debug|x86.ActiveCfg = Debug|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.Debug|x86.Build.0 = Debug|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.NativeDebug|Any CPU.ActiveCfg = Debug|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.NativeDebug|Any CPU.Build.0 = Debug|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.NativeDebug|x64.ActiveCfg = Debug|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.NativeDebug|x86.ActiveCfg = Debug|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.NativeRelease|Any CPU.ActiveCfg = Release|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.NativeRelease|Any CPU.Build.0 = Release|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.NativeRelease|x64.ActiveCfg = Release|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.NativeRelease|x86.ActiveCfg = Release|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.Release|Any CPU.Build.0 = Release|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.Release|x64.ActiveCfg = Release|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.Release|x64.Build.0 = Release|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.Release|x86.ActiveCfg = Release|Any CPU + {8B3446E8-E6A8-4591-AA63-A95837C6E97C}.Release|x86.Build.0 = Release|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.Debug|x64.ActiveCfg = Debug|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.Debug|x64.Build.0 = Debug|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.Debug|x86.ActiveCfg = Debug|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.Debug|x86.Build.0 = Debug|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.NativeDebug|Any CPU.ActiveCfg = Debug|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.NativeDebug|Any CPU.Build.0 = Debug|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.NativeDebug|x64.ActiveCfg = Debug|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.NativeDebug|x86.ActiveCfg = Debug|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.NativeRelease|Any CPU.ActiveCfg = Release|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.NativeRelease|Any CPU.Build.0 = Release|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.NativeRelease|x64.ActiveCfg = Release|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.NativeRelease|x86.ActiveCfg = Release|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.Release|Any CPU.Build.0 = Release|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.Release|x64.ActiveCfg = Release|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.Release|x64.Build.0 = Release|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.Release|x86.ActiveCfg = Release|Any CPU + {4106DB10-E09F-480E-9CE6-B39235512EE6}.Release|x86.Build.0 = Release|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.Debug|x64.ActiveCfg = Debug|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.Debug|x64.Build.0 = Debug|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.Debug|x86.ActiveCfg = Debug|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.Debug|x86.Build.0 = Debug|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.NativeDebug|Any CPU.ActiveCfg = Debug|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.NativeDebug|Any CPU.Build.0 = Debug|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.NativeDebug|x64.ActiveCfg = Debug|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.NativeDebug|x86.ActiveCfg = Debug|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.NativeRelease|Any CPU.ActiveCfg = Release|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.NativeRelease|Any CPU.Build.0 = Release|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.NativeRelease|x64.ActiveCfg = Release|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.NativeRelease|x86.ActiveCfg = Release|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.Release|Any CPU.Build.0 = Release|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.Release|x64.ActiveCfg = Release|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.Release|x64.Build.0 = Release|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.Release|x86.ActiveCfg = Release|Any CPU + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA}.Release|x86.Build.0 = Release|Any CPU + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.Debug|Any CPU.ActiveCfg = Debug|x64 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.Debug|Any CPU.Build.0 = Debug|x64 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.Debug|x64.ActiveCfg = Debug|x64 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.Debug|x64.Build.0 = Debug|x64 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.Debug|x86.ActiveCfg = Debug|x86 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.Debug|x86.Build.0 = Debug|x86 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.NativeDebug|Any CPU.ActiveCfg = Debug|x64 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.NativeDebug|Any CPU.Build.0 = Debug|x64 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.NativeDebug|x64.ActiveCfg = Debug|x64 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.NativeDebug|x86.ActiveCfg = Debug|x86 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.NativeRelease|Any CPU.ActiveCfg = Release|x64 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.NativeRelease|Any CPU.Build.0 = Release|x64 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.NativeRelease|x64.ActiveCfg = Release|x64 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.NativeRelease|x86.ActiveCfg = Release|x86 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.Release|Any CPU.ActiveCfg = Release|x64 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.Release|Any CPU.Build.0 = Release|x64 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.Release|x64.ActiveCfg = Release|x64 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.Release|x64.Build.0 = Release|x64 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.Release|x86.ActiveCfg = Release|x86 + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97}.Release|x86.Build.0 = Release|x86 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.Debug|Any CPU.ActiveCfg = Debug|x64 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.Debug|Any CPU.Build.0 = Debug|x64 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.Debug|x64.ActiveCfg = Debug|x64 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.Debug|x64.Build.0 = Debug|x64 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.Debug|x86.ActiveCfg = Debug|x86 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.Debug|x86.Build.0 = Debug|x86 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.NativeDebug|Any CPU.ActiveCfg = Debug|x64 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.NativeDebug|Any CPU.Build.0 = Debug|x64 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.NativeDebug|x64.ActiveCfg = Debug|x64 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.NativeDebug|x86.ActiveCfg = Debug|x86 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.NativeRelease|Any CPU.ActiveCfg = Release|x64 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.NativeRelease|Any CPU.Build.0 = Release|x64 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.NativeRelease|x64.ActiveCfg = Release|x64 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.NativeRelease|x86.ActiveCfg = Release|x86 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.Release|Any CPU.ActiveCfg = Release|x64 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.Release|Any CPU.Build.0 = Release|x64 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.Release|x64.ActiveCfg = Release|x64 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.Release|x64.Build.0 = Release|x64 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.Release|x86.ActiveCfg = Release|x86 + {679FA2A2-898B-4320-884E-C2D294A97CE1}.Release|x86.Build.0 = Release|x86 + {46A8612B-418B-4D70-B3A7-A21DD0627473}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.Debug|Any CPU.Build.0 = Debug|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.Debug|x64.ActiveCfg = Debug|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.Debug|x64.Build.0 = Debug|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.Debug|x86.ActiveCfg = Debug|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.Debug|x86.Build.0 = Debug|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.NativeDebug|Any CPU.ActiveCfg = Debug|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.NativeDebug|Any CPU.Build.0 = Debug|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.NativeDebug|x64.ActiveCfg = Debug|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.NativeDebug|x86.ActiveCfg = Debug|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.NativeRelease|Any CPU.ActiveCfg = Release|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.NativeRelease|Any CPU.Build.0 = Release|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.NativeRelease|x64.ActiveCfg = Release|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.NativeRelease|x86.ActiveCfg = Release|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.Release|Any CPU.ActiveCfg = Release|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.Release|Any CPU.Build.0 = Release|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.Release|x64.ActiveCfg = Release|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.Release|x64.Build.0 = Release|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.Release|x86.ActiveCfg = Release|Any CPU + {46A8612B-418B-4D70-B3A7-A21DD0627473}.Release|x86.Build.0 = Release|Any CPU + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Debug|Any CPU.ActiveCfg = Debug|x64 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Debug|Any CPU.Build.0 = Debug|x64 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Debug|x64.ActiveCfg = Debug|x64 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Debug|x64.Build.0 = Debug|x64 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Debug|x86.ActiveCfg = Debug|x86 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Debug|x86.Build.0 = Debug|x86 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.NativeDebug|Any CPU.ActiveCfg = Debug|x64 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.NativeDebug|Any CPU.Build.0 = Debug|x64 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.NativeDebug|x64.ActiveCfg = Debug|x64 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.NativeDebug|x86.ActiveCfg = Debug|x86 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.NativeRelease|Any CPU.ActiveCfg = Release|x64 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.NativeRelease|Any CPU.Build.0 = Release|x64 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.NativeRelease|x64.ActiveCfg = Release|x64 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.NativeRelease|x86.ActiveCfg = Release|x86 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Release|Any CPU.ActiveCfg = Release|x64 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Release|Any CPU.Build.0 = Release|x64 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Release|x64.ActiveCfg = Release|x64 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Release|x64.Build.0 = Release|x64 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Release|x86.ActiveCfg = Release|x86 + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F}.Release|x86.Build.0 = Release|x86 + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.Debug|x64.ActiveCfg = Debug|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.Debug|x64.Build.0 = Debug|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.Debug|x86.ActiveCfg = Debug|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.Debug|x86.Build.0 = Debug|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.NativeDebug|Any CPU.ActiveCfg = Debug|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.NativeDebug|Any CPU.Build.0 = Debug|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.NativeDebug|x64.ActiveCfg = Debug|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.NativeDebug|x86.ActiveCfg = Debug|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.NativeRelease|Any CPU.ActiveCfg = Release|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.NativeRelease|Any CPU.Build.0 = Release|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.NativeRelease|x64.ActiveCfg = Release|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.NativeRelease|x86.ActiveCfg = Release|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.Release|Any CPU.Build.0 = Release|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.Release|x64.ActiveCfg = Release|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.Release|x64.Build.0 = Release|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.Release|x86.ActiveCfg = Release|Any CPU + {064D860B-4D7C-4B1D-918F-E020F1B99E2A}.Release|x86.Build.0 = Release|Any CPU + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.Debug|x64.ActiveCfg = Debug|x64 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.Debug|x64.Build.0 = Debug|x64 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.Debug|x86.ActiveCfg = Debug|Win32 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.Debug|x86.Build.0 = Debug|Win32 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.NativeDebug|Any CPU.ActiveCfg = Debug|Win32 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.NativeDebug|x64.ActiveCfg = Debug|x64 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.NativeDebug|x64.Build.0 = Debug|x64 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.NativeDebug|x86.ActiveCfg = Debug|Win32 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.NativeDebug|x86.Build.0 = Debug|Win32 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.NativeRelease|Any CPU.ActiveCfg = Release|Win32 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.NativeRelease|x64.ActiveCfg = Release|x64 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.NativeRelease|x64.Build.0 = Release|x64 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.NativeRelease|x86.ActiveCfg = Release|Win32 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.NativeRelease|x86.Build.0 = Release|Win32 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.Release|Any CPU.ActiveCfg = Release|Win32 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.Release|x64.ActiveCfg = Release|x64 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.Release|x64.Build.0 = Release|x64 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.Release|x86.ActiveCfg = Release|Win32 + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1}.Release|x86.Build.0 = Release|Win32 + {4787A64F-9A3E-4867-A55A-70CB4B2B2FFE}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {4787A64F-9A3E-4867-A55A-70CB4B2B2FFE}.Debug|x64.ActiveCfg = Debug|x64 + {4787A64F-9A3E-4867-A55A-70CB4B2B2FFE}.Debug|x64.Build.0 = Debug|x64 + {4787A64F-9A3E-4867-A55A-70CB4B2B2FFE}.Debug|x86.ActiveCfg = Debug|Win32 + {4787A64F-9A3E-4867-A55A-70CB4B2B2FFE}.Debug|x86.Build.0 = Debug|Win32 + {4787A64F-9A3E-4867-A55A-70CB4B2B2FFE}.NativeDebug|Any CPU.ActiveCfg = Debug|Win32 + {4787A64F-9A3E-4867-A55A-70CB4B2B2FFE}.NativeDebug|x64.ActiveCfg = Debug|x64 + {4787A64F-9A3E-4867-A55A-70CB4B2B2FFE}.NativeDebug|x64.Build.0 = Debug|x64 + {4787A64F-9A3E-4867-A55A-70CB4B2B2FFE}.NativeDebug|x86.ActiveCfg = Debug|Win32 + {4787A64F-9A3E-4867-A55A-70CB4B2B2FFE}.NativeDebug|x86.Build.0 = Debug|Win32 + {4787A64F-9A3E-4867-A55A-70CB4B2B2FFE}.NativeRelease|Any CPU.ActiveCfg = Release|Win32 + {4787A64F-9A3E-4867-A55A-70CB4B2B2FFE}.NativeRelease|x64.ActiveCfg = Release|x64 + {4787A64F-9A3E-4867-A55A-70CB4B2B2FFE}.NativeRelease|x64.Build.0 = Release|x64 + {4787A64F-9A3E-4867-A55A-70CB4B2B2FFE}.NativeRelease|x86.ActiveCfg = Release|Win32 + {4787A64F-9A3E-4867-A55A-70CB4B2B2FFE}.NativeRelease|x86.Build.0 = Release|Win32 + {4787A64F-9A3E-4867-A55A-70CB4B2B2FFE}.Release|Any CPU.ActiveCfg = Release|Win32 + {4787A64F-9A3E-4867-A55A-70CB4B2B2FFE}.Release|x64.ActiveCfg = Release|x64 + {4787A64F-9A3E-4867-A55A-70CB4B2B2FFE}.Release|x64.Build.0 = Release|x64 + {4787A64F-9A3E-4867-A55A-70CB4B2B2FFE}.Release|x86.ActiveCfg = Release|Win32 + {4787A64F-9A3E-4867-A55A-70CB4B2B2FFE}.Release|x86.Build.0 = Release|Win32 + {439824F9-1455-4CC4-BD79-B44FA0A16552}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {439824F9-1455-4CC4-BD79-B44FA0A16552}.Debug|x64.ActiveCfg = Debug|x64 + {439824F9-1455-4CC4-BD79-B44FA0A16552}.Debug|x64.Build.0 = Debug|x64 + {439824F9-1455-4CC4-BD79-B44FA0A16552}.Debug|x86.ActiveCfg = Debug|Win32 + {439824F9-1455-4CC4-BD79-B44FA0A16552}.Debug|x86.Build.0 = Debug|Win32 + {439824F9-1455-4CC4-BD79-B44FA0A16552}.NativeDebug|Any CPU.ActiveCfg = Debug|Win32 + {439824F9-1455-4CC4-BD79-B44FA0A16552}.NativeDebug|x64.ActiveCfg = Debug|x64 + {439824F9-1455-4CC4-BD79-B44FA0A16552}.NativeDebug|x64.Build.0 = Debug|x64 + {439824F9-1455-4CC4-BD79-B44FA0A16552}.NativeDebug|x86.ActiveCfg = Debug|Win32 + {439824F9-1455-4CC4-BD79-B44FA0A16552}.NativeDebug|x86.Build.0 = Debug|Win32 + {439824F9-1455-4CC4-BD79-B44FA0A16552}.NativeRelease|Any CPU.ActiveCfg = Release|Win32 + {439824F9-1455-4CC4-BD79-B44FA0A16552}.NativeRelease|x64.ActiveCfg = Release|x64 + {439824F9-1455-4CC4-BD79-B44FA0A16552}.NativeRelease|x64.Build.0 = Release|x64 + {439824F9-1455-4CC4-BD79-B44FA0A16552}.NativeRelease|x86.ActiveCfg = Release|Win32 + {439824F9-1455-4CC4-BD79-B44FA0A16552}.NativeRelease|x86.Build.0 = Release|Win32 + {439824F9-1455-4CC4-BD79-B44FA0A16552}.Release|Any CPU.ActiveCfg = Release|Win32 + {439824F9-1455-4CC4-BD79-B44FA0A16552}.Release|x64.ActiveCfg = Release|x64 + {439824F9-1455-4CC4-BD79-B44FA0A16552}.Release|x64.Build.0 = Release|x64 + {439824F9-1455-4CC4-BD79-B44FA0A16552}.Release|x86.ActiveCfg = Release|Win32 + {439824F9-1455-4CC4-BD79-B44FA0A16552}.Release|x86.Build.0 = Release|Win32 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.Debug|x64.ActiveCfg = Debug|x64 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.Debug|x64.Build.0 = Debug|x64 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.Debug|x86.ActiveCfg = Debug|Win32 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.Debug|x86.Build.0 = Debug|Win32 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.NativeDebug|Any CPU.ActiveCfg = Debug|Win32 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.NativeDebug|x64.ActiveCfg = Debug|x64 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.NativeDebug|x64.Build.0 = Debug|x64 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.NativeDebug|x86.ActiveCfg = Debug|Win32 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.NativeDebug|x86.Build.0 = Debug|Win32 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.NativeRelease|Any CPU.ActiveCfg = Release|Win32 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.NativeRelease|x64.ActiveCfg = Release|x64 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.NativeRelease|x64.Build.0 = Release|x64 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.NativeRelease|x86.ActiveCfg = Release|Win32 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.NativeRelease|x86.Build.0 = Release|Win32 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.Release|Any CPU.ActiveCfg = Release|Win32 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.Release|x64.ActiveCfg = Release|x64 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.Release|x64.Build.0 = Release|x64 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.Release|x86.ActiveCfg = Release|Win32 + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B}.Release|x86.Build.0 = Release|Win32 + {55494E58-E061-4C4C-A0A8-837008E72F85}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {55494E58-E061-4C4C-A0A8-837008E72F85}.Debug|x64.ActiveCfg = Debug|x64 + {55494E58-E061-4C4C-A0A8-837008E72F85}.Debug|x64.Build.0 = Debug|x64 + {55494E58-E061-4C4C-A0A8-837008E72F85}.Debug|x86.ActiveCfg = Debug|Win32 + {55494E58-E061-4C4C-A0A8-837008E72F85}.Debug|x86.Build.0 = Debug|Win32 + {55494E58-E061-4C4C-A0A8-837008E72F85}.NativeDebug|Any CPU.ActiveCfg = Debug|Win32 + {55494E58-E061-4C4C-A0A8-837008E72F85}.NativeDebug|x64.ActiveCfg = Debug|x64 + {55494E58-E061-4C4C-A0A8-837008E72F85}.NativeDebug|x64.Build.0 = Debug|x64 + {55494E58-E061-4C4C-A0A8-837008E72F85}.NativeDebug|x86.ActiveCfg = Debug|Win32 + {55494E58-E061-4C4C-A0A8-837008E72F85}.NativeDebug|x86.Build.0 = Debug|Win32 + {55494E58-E061-4C4C-A0A8-837008E72F85}.NativeRelease|Any CPU.ActiveCfg = Release|Win32 + {55494E58-E061-4C4C-A0A8-837008E72F85}.NativeRelease|x64.ActiveCfg = Release|x64 + {55494E58-E061-4C4C-A0A8-837008E72F85}.NativeRelease|x64.Build.0 = Release|x64 + {55494E58-E061-4C4C-A0A8-837008E72F85}.NativeRelease|x86.ActiveCfg = Release|Win32 + {55494E58-E061-4C4C-A0A8-837008E72F85}.NativeRelease|x86.Build.0 = Release|Win32 + {55494E58-E061-4C4C-A0A8-837008E72F85}.Release|Any CPU.ActiveCfg = Release|Win32 + {55494E58-E061-4C4C-A0A8-837008E72F85}.Release|x64.ActiveCfg = Release|x64 + {55494E58-E061-4C4C-A0A8-837008E72F85}.Release|x64.Build.0 = Release|x64 + {55494E58-E061-4C4C-A0A8-837008E72F85}.Release|x86.ActiveCfg = Release|Win32 + {55494E58-E061-4C4C-A0A8-837008E72F85}.Release|x86.Build.0 = Release|Win32 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.Debug|x64.ActiveCfg = Debug|x64 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.Debug|x64.Build.0 = Debug|x64 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.Debug|x86.ActiveCfg = Debug|Win32 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.Debug|x86.Build.0 = Debug|Win32 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.NativeDebug|Any CPU.ActiveCfg = Debug|Win32 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.NativeDebug|x64.ActiveCfg = Debug|x64 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.NativeDebug|x64.Build.0 = Debug|x64 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.NativeDebug|x86.ActiveCfg = Debug|Win32 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.NativeDebug|x86.Build.0 = Debug|Win32 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.NativeRelease|Any CPU.ActiveCfg = Release|Win32 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.NativeRelease|x64.ActiveCfg = Release|x64 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.NativeRelease|x64.Build.0 = Release|x64 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.NativeRelease|x86.ActiveCfg = Release|Win32 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.NativeRelease|x86.Build.0 = Release|Win32 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.Release|Any CPU.ActiveCfg = Release|Win32 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.Release|x64.ActiveCfg = Release|x64 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.Release|x64.Build.0 = Release|x64 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.Release|x86.ActiveCfg = Release|Win32 + {09D9D1D6-2951-4E14-BC35-76A23CF9391A}.Release|x86.Build.0 = Release|Win32 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.Debug|Any CPU.ActiveCfg = Debug|x64 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.Debug|Any CPU.Build.0 = Debug|x64 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.Debug|x64.ActiveCfg = Debug|x64 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.Debug|x64.Build.0 = Debug|x64 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.Debug|x86.ActiveCfg = Debug|x86 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.Debug|x86.Build.0 = Debug|x86 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.NativeDebug|Any CPU.ActiveCfg = Debug|x64 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.NativeDebug|Any CPU.Build.0 = Debug|x64 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.NativeDebug|x64.ActiveCfg = Debug|x64 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.NativeDebug|x86.ActiveCfg = Debug|x86 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.NativeRelease|Any CPU.ActiveCfg = Release|x64 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.NativeRelease|Any CPU.Build.0 = Release|x64 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.NativeRelease|x64.ActiveCfg = Release|x64 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.NativeRelease|x86.ActiveCfg = Release|x86 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.Release|Any CPU.ActiveCfg = Release|x64 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.Release|Any CPU.Build.0 = Release|x64 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.Release|x64.ActiveCfg = Release|x64 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.Release|x64.Build.0 = Release|x64 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.Release|x86.ActiveCfg = Release|x86 + {42E60F88-E23F-417A-8143-0CCEC05E1D02}.Release|x86.Build.0 = Release|x86 + {48F46909-E76A-4788-BCE1-E543C0E140FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.Debug|x64.ActiveCfg = Debug|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.Debug|x64.Build.0 = Debug|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.Debug|x86.ActiveCfg = Debug|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.Debug|x86.Build.0 = Debug|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.NativeDebug|Any CPU.ActiveCfg = Debug|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.NativeDebug|Any CPU.Build.0 = Debug|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.NativeDebug|x64.ActiveCfg = Debug|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.NativeDebug|x86.ActiveCfg = Debug|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.NativeRelease|Any CPU.ActiveCfg = Release|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.NativeRelease|Any CPU.Build.0 = Release|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.NativeRelease|x64.ActiveCfg = Release|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.NativeRelease|x86.ActiveCfg = Release|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.Release|Any CPU.Build.0 = Release|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.Release|x64.ActiveCfg = Release|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.Release|x64.Build.0 = Release|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.Release|x86.ActiveCfg = Release|Any CPU + {48F46909-E76A-4788-BCE1-E543C0E140FE}.Release|x86.Build.0 = Release|Any CPU + {D57EA297-6DC2-4BC0-8C91-334863327863}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {D57EA297-6DC2-4BC0-8C91-334863327863}.Debug|x64.ActiveCfg = Debug|x64 + {D57EA297-6DC2-4BC0-8C91-334863327863}.Debug|x64.Build.0 = Debug|x64 + {D57EA297-6DC2-4BC0-8C91-334863327863}.Debug|x86.ActiveCfg = Debug|Win32 + {D57EA297-6DC2-4BC0-8C91-334863327863}.Debug|x86.Build.0 = Debug|Win32 + {D57EA297-6DC2-4BC0-8C91-334863327863}.NativeDebug|Any CPU.ActiveCfg = Debug|Win32 + {D57EA297-6DC2-4BC0-8C91-334863327863}.NativeDebug|x64.ActiveCfg = Debug|x64 + {D57EA297-6DC2-4BC0-8C91-334863327863}.NativeDebug|x64.Build.0 = Debug|x64 + {D57EA297-6DC2-4BC0-8C91-334863327863}.NativeDebug|x86.ActiveCfg = Debug|Win32 + {D57EA297-6DC2-4BC0-8C91-334863327863}.NativeDebug|x86.Build.0 = Debug|Win32 + {D57EA297-6DC2-4BC0-8C91-334863327863}.NativeRelease|Any CPU.ActiveCfg = Release|Win32 + {D57EA297-6DC2-4BC0-8C91-334863327863}.NativeRelease|x64.ActiveCfg = Release|x64 + {D57EA297-6DC2-4BC0-8C91-334863327863}.NativeRelease|x64.Build.0 = Release|x64 + {D57EA297-6DC2-4BC0-8C91-334863327863}.NativeRelease|x86.ActiveCfg = Release|Win32 + {D57EA297-6DC2-4BC0-8C91-334863327863}.NativeRelease|x86.Build.0 = Release|Win32 + {D57EA297-6DC2-4BC0-8C91-334863327863}.Release|Any CPU.ActiveCfg = Release|Win32 + {D57EA297-6DC2-4BC0-8C91-334863327863}.Release|x64.ActiveCfg = Release|x64 + {D57EA297-6DC2-4BC0-8C91-334863327863}.Release|x64.Build.0 = Release|x64 + {D57EA297-6DC2-4BC0-8C91-334863327863}.Release|x86.ActiveCfg = Release|Win32 + {D57EA297-6DC2-4BC0-8C91-334863327863}.Release|x86.Build.0 = Release|Win32 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.Debug|x64.ActiveCfg = Debug|x64 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.Debug|x64.Build.0 = Debug|x64 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.Debug|x86.ActiveCfg = Debug|Win32 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.Debug|x86.Build.0 = Debug|Win32 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.NativeDebug|Any CPU.ActiveCfg = Debug|Win32 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.NativeDebug|x64.ActiveCfg = Debug|x64 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.NativeDebug|x64.Build.0 = Debug|x64 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.NativeDebug|x86.ActiveCfg = Debug|Win32 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.NativeDebug|x86.Build.0 = Debug|Win32 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.NativeRelease|Any CPU.ActiveCfg = Release|Win32 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.NativeRelease|x64.ActiveCfg = Release|x64 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.NativeRelease|x64.Build.0 = Release|x64 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.NativeRelease|x86.ActiveCfg = Release|Win32 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.NativeRelease|x86.Build.0 = Release|Win32 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.Release|Any CPU.ActiveCfg = Release|Win32 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.Release|x64.ActiveCfg = Release|x64 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.Release|x64.Build.0 = Release|x64 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.Release|x86.ActiveCfg = Release|Win32 + {7F87406C-A3C8-4139-A68D-E4C344294A67}.Release|x86.Build.0 = Release|Win32 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.Debug|x64.ActiveCfg = Debug|x64 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.Debug|x64.Build.0 = Debug|x64 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.Debug|x86.ActiveCfg = Debug|Win32 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.Debug|x86.Build.0 = Debug|Win32 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.NativeDebug|Any CPU.ActiveCfg = Debug|Win32 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.NativeDebug|x64.ActiveCfg = Debug|x64 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.NativeDebug|x64.Build.0 = Debug|x64 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.NativeDebug|x86.ActiveCfg = Debug|Win32 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.NativeDebug|x86.Build.0 = Debug|Win32 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.NativeRelease|Any CPU.ActiveCfg = Release|Win32 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.NativeRelease|x64.ActiveCfg = Release|x64 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.NativeRelease|x64.Build.0 = Release|x64 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.NativeRelease|x86.ActiveCfg = Release|Win32 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.NativeRelease|x86.Build.0 = Release|Win32 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.Release|Any CPU.ActiveCfg = Release|Win32 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.Release|x64.ActiveCfg = Release|x64 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.Release|x64.Build.0 = Release|x64 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.Release|x86.ActiveCfg = Release|Win32 + {CAC1267B-8778-4257-AAC6-CAF481723B01}.Release|x86.Build.0 = Release|Win32 + {1533E271-F61B-441B-8B74-59FB61DF0552}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {1533E271-F61B-441B-8B74-59FB61DF0552}.Debug|x64.ActiveCfg = Debug|x64 + {1533E271-F61B-441B-8B74-59FB61DF0552}.Debug|x64.Build.0 = Debug|x64 + {1533E271-F61B-441B-8B74-59FB61DF0552}.Debug|x86.ActiveCfg = Debug|Win32 + {1533E271-F61B-441B-8B74-59FB61DF0552}.Debug|x86.Build.0 = Debug|Win32 + {1533E271-F61B-441B-8B74-59FB61DF0552}.NativeDebug|Any CPU.ActiveCfg = Debug|Win32 + {1533E271-F61B-441B-8B74-59FB61DF0552}.NativeDebug|x64.ActiveCfg = Debug|x64 + {1533E271-F61B-441B-8B74-59FB61DF0552}.NativeDebug|x64.Build.0 = Debug|x64 + {1533E271-F61B-441B-8B74-59FB61DF0552}.NativeDebug|x86.ActiveCfg = Debug|Win32 + {1533E271-F61B-441B-8B74-59FB61DF0552}.NativeDebug|x86.Build.0 = Debug|Win32 + {1533E271-F61B-441B-8B74-59FB61DF0552}.NativeRelease|Any CPU.ActiveCfg = Release|Win32 + {1533E271-F61B-441B-8B74-59FB61DF0552}.NativeRelease|x64.ActiveCfg = Release|x64 + {1533E271-F61B-441B-8B74-59FB61DF0552}.NativeRelease|x64.Build.0 = Release|x64 + {1533E271-F61B-441B-8B74-59FB61DF0552}.NativeRelease|x86.ActiveCfg = Release|Win32 + {1533E271-F61B-441B-8B74-59FB61DF0552}.NativeRelease|x86.Build.0 = Release|Win32 + {1533E271-F61B-441B-8B74-59FB61DF0552}.Release|Any CPU.ActiveCfg = Release|Win32 + {1533E271-F61B-441B-8B74-59FB61DF0552}.Release|x64.ActiveCfg = Release|x64 + {1533E271-F61B-441B-8B74-59FB61DF0552}.Release|x64.Build.0 = Release|x64 + {1533E271-F61B-441B-8B74-59FB61DF0552}.Release|x86.ActiveCfg = Release|Win32 + {1533E271-F61B-441B-8B74-59FB61DF0552}.Release|x86.Build.0 = Release|Win32 + {1F0C8D9B-F47B-41F3-9FC9-6954B6DC7712}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F0C8D9B-F47B-41F3-9FC9-6954B6DC7712}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F0C8D9B-F47B-41F3-9FC9-6954B6DC7712}.Debug|x64.ActiveCfg = Debug|Any CPU + {1F0C8D9B-F47B-41F3-9FC9-6954B6DC7712}.Debug|x64.Build.0 = Debug|Any CPU + {1F0C8D9B-F47B-41F3-9FC9-6954B6DC7712}.Debug|x86.ActiveCfg = Debug|Any CPU + {1F0C8D9B-F47B-41F3-9FC9-6954B6DC7712}.Debug|x86.Build.0 = Debug|Any CPU + {1F0C8D9B-F47B-41F3-9FC9-6954B6DC7712}.NativeDebug|Any CPU.ActiveCfg = Debug|Any CPU + {1F0C8D9B-F47B-41F3-9FC9-6954B6DC7712}.NativeDebug|Any CPU.Build.0 = Debug|Any CPU + {1F0C8D9B-F47B-41F3-9FC9-6954B6DC7712}.NativeDebug|x64.ActiveCfg = Debug|Any CPU + {1F0C8D9B-F47B-41F3-9FC9-6954B6DC7712}.NativeDebug|x86.ActiveCfg = Debug|Any CPU + {1F0C8D9B-F47B-41F3-9FC9-6954B6DC7712}.NativeRelease|Any CPU.ActiveCfg = Release|Any CPU + {1F0C8D9B-F47B-41F3-9FC9-6954B6DC7712}.NativeRelease|Any CPU.Build.0 = Release|Any CPU + {1F0C8D9B-F47B-41F3-9FC9-6954B6DC7712}.NativeRelease|x64.ActiveCfg = Release|Any CPU + {1F0C8D9B-F47B-41F3-9FC9-6954B6DC7712}.NativeRelease|x86.ActiveCfg = Release|Any CPU + {1F0C8D9B-F47B-41F3-9FC9-6954B6DC7712}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F0C8D9B-F47B-41F3-9FC9-6954B6DC7712}.Release|Any CPU.Build.0 = Release|Any CPU + {1F0C8D9B-F47B-41F3-9FC9-6954B6DC7712}.Release|x64.ActiveCfg = Release|Any CPU + {1F0C8D9B-F47B-41F3-9FC9-6954B6DC7712}.Release|x64.Build.0 = Release|Any CPU + {1F0C8D9B-F47B-41F3-9FC9-6954B6DC7712}.Release|x86.ActiveCfg = Release|Any CPU + {1F0C8D9B-F47B-41F3-9FC9-6954B6DC7712}.Release|x86.Build.0 = Release|Any CPU + {CE4FB142-91FB-4B34-BC96-A31120EF4009}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE4FB142-91FB-4B34-BC96-A31120EF4009}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE4FB142-91FB-4B34-BC96-A31120EF4009}.Debug|x64.ActiveCfg = Debug|Any CPU + {CE4FB142-91FB-4B34-BC96-A31120EF4009}.Debug|x64.Build.0 = Debug|Any CPU + {CE4FB142-91FB-4B34-BC96-A31120EF4009}.Debug|x86.ActiveCfg = Debug|Any CPU + {CE4FB142-91FB-4B34-BC96-A31120EF4009}.Debug|x86.Build.0 = Debug|Any CPU + {CE4FB142-91FB-4B34-BC96-A31120EF4009}.NativeDebug|Any CPU.ActiveCfg = Debug|Any CPU + {CE4FB142-91FB-4B34-BC96-A31120EF4009}.NativeDebug|Any CPU.Build.0 = Debug|Any CPU + {CE4FB142-91FB-4B34-BC96-A31120EF4009}.NativeDebug|x64.ActiveCfg = Debug|Any CPU + {CE4FB142-91FB-4B34-BC96-A31120EF4009}.NativeDebug|x86.ActiveCfg = Debug|Any CPU + {CE4FB142-91FB-4B34-BC96-A31120EF4009}.NativeRelease|Any CPU.ActiveCfg = Release|Any CPU + {CE4FB142-91FB-4B34-BC96-A31120EF4009}.NativeRelease|Any CPU.Build.0 = Release|Any CPU + {CE4FB142-91FB-4B34-BC96-A31120EF4009}.NativeRelease|x64.ActiveCfg = Release|Any CPU + {CE4FB142-91FB-4B34-BC96-A31120EF4009}.NativeRelease|x86.ActiveCfg = Release|Any CPU + {CE4FB142-91FB-4B34-BC96-A31120EF4009}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE4FB142-91FB-4B34-BC96-A31120EF4009}.Release|Any CPU.Build.0 = Release|Any CPU + {CE4FB142-91FB-4B34-BC96-A31120EF4009}.Release|x64.ActiveCfg = Release|Any CPU + {CE4FB142-91FB-4B34-BC96-A31120EF4009}.Release|x64.Build.0 = Release|Any CPU + {CE4FB142-91FB-4B34-BC96-A31120EF4009}.Release|x86.ActiveCfg = Release|Any CPU + {CE4FB142-91FB-4B34-BC96-A31120EF4009}.Release|x86.Build.0 = Release|Any CPU + {A091777D-66B3-42E1-B95C-85322DE40706}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A091777D-66B3-42E1-B95C-85322DE40706}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A091777D-66B3-42E1-B95C-85322DE40706}.Debug|x64.ActiveCfg = Debug|Any CPU + {A091777D-66B3-42E1-B95C-85322DE40706}.Debug|x64.Build.0 = Debug|Any CPU + {A091777D-66B3-42E1-B95C-85322DE40706}.Debug|x86.ActiveCfg = Debug|Any CPU + {A091777D-66B3-42E1-B95C-85322DE40706}.Debug|x86.Build.0 = Debug|Any CPU + {A091777D-66B3-42E1-B95C-85322DE40706}.NativeDebug|Any CPU.ActiveCfg = Debug|Any CPU + {A091777D-66B3-42E1-B95C-85322DE40706}.NativeDebug|Any CPU.Build.0 = Debug|Any CPU + {A091777D-66B3-42E1-B95C-85322DE40706}.NativeDebug|x64.ActiveCfg = Debug|Any CPU + {A091777D-66B3-42E1-B95C-85322DE40706}.NativeDebug|x86.ActiveCfg = Debug|Any CPU + {A091777D-66B3-42E1-B95C-85322DE40706}.NativeRelease|Any CPU.ActiveCfg = Release|Any CPU + {A091777D-66B3-42E1-B95C-85322DE40706}.NativeRelease|Any CPU.Build.0 = Release|Any CPU + {A091777D-66B3-42E1-B95C-85322DE40706}.NativeRelease|x64.ActiveCfg = Release|Any CPU + {A091777D-66B3-42E1-B95C-85322DE40706}.NativeRelease|x86.ActiveCfg = Release|Any CPU + {A091777D-66B3-42E1-B95C-85322DE40706}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A091777D-66B3-42E1-B95C-85322DE40706}.Release|Any CPU.Build.0 = Release|Any CPU + {A091777D-66B3-42E1-B95C-85322DE40706}.Release|x64.ActiveCfg = Release|Any CPU + {A091777D-66B3-42E1-B95C-85322DE40706}.Release|x64.Build.0 = Release|Any CPU + {A091777D-66B3-42E1-B95C-85322DE40706}.Release|x86.ActiveCfg = Release|Any CPU + {A091777D-66B3-42E1-B95C-85322DE40706}.Release|x86.Build.0 = Release|Any CPU + {A641A208-2974-4E48-BCFF-54E3AAFA4FB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A641A208-2974-4E48-BCFF-54E3AAFA4FB9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A641A208-2974-4E48-BCFF-54E3AAFA4FB9}.Debug|x64.ActiveCfg = Debug|Any CPU + {A641A208-2974-4E48-BCFF-54E3AAFA4FB9}.Debug|x64.Build.0 = Debug|Any CPU + {A641A208-2974-4E48-BCFF-54E3AAFA4FB9}.Debug|x86.ActiveCfg = Debug|Any CPU + {A641A208-2974-4E48-BCFF-54E3AAFA4FB9}.Debug|x86.Build.0 = Debug|Any CPU + {A641A208-2974-4E48-BCFF-54E3AAFA4FB9}.NativeDebug|Any CPU.ActiveCfg = Debug|Any CPU + {A641A208-2974-4E48-BCFF-54E3AAFA4FB9}.NativeDebug|Any CPU.Build.0 = Debug|Any CPU + {A641A208-2974-4E48-BCFF-54E3AAFA4FB9}.NativeDebug|x64.ActiveCfg = Debug|Any CPU + {A641A208-2974-4E48-BCFF-54E3AAFA4FB9}.NativeDebug|x86.ActiveCfg = Debug|Any CPU + {A641A208-2974-4E48-BCFF-54E3AAFA4FB9}.NativeRelease|Any CPU.ActiveCfg = Release|Any CPU + {A641A208-2974-4E48-BCFF-54E3AAFA4FB9}.NativeRelease|Any CPU.Build.0 = Release|Any CPU + {A641A208-2974-4E48-BCFF-54E3AAFA4FB9}.NativeRelease|x64.ActiveCfg = Release|Any CPU + {A641A208-2974-4E48-BCFF-54E3AAFA4FB9}.NativeRelease|x86.ActiveCfg = Release|Any CPU + {A641A208-2974-4E48-BCFF-54E3AAFA4FB9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A641A208-2974-4E48-BCFF-54E3AAFA4FB9}.Release|Any CPU.Build.0 = Release|Any CPU + {A641A208-2974-4E48-BCFF-54E3AAFA4FB9}.Release|x64.ActiveCfg = Release|Any CPU + {A641A208-2974-4E48-BCFF-54E3AAFA4FB9}.Release|x64.Build.0 = Release|Any CPU + {A641A208-2974-4E48-BCFF-54E3AAFA4FB9}.Release|x86.ActiveCfg = Release|Any CPU + {A641A208-2974-4E48-BCFF-54E3AAFA4FB9}.Release|x86.Build.0 = Release|Any CPU + {28055B05-25D4-4F17-9F36-A1D09FDDA607}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28055B05-25D4-4F17-9F36-A1D09FDDA607}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28055B05-25D4-4F17-9F36-A1D09FDDA607}.Debug|x64.ActiveCfg = Debug|Any CPU + {28055B05-25D4-4F17-9F36-A1D09FDDA607}.Debug|x64.Build.0 = Debug|Any CPU + {28055B05-25D4-4F17-9F36-A1D09FDDA607}.Debug|x86.ActiveCfg = Debug|Any CPU + {28055B05-25D4-4F17-9F36-A1D09FDDA607}.Debug|x86.Build.0 = Debug|Any CPU + {28055B05-25D4-4F17-9F36-A1D09FDDA607}.NativeDebug|Any CPU.ActiveCfg = Debug|Any CPU + {28055B05-25D4-4F17-9F36-A1D09FDDA607}.NativeDebug|Any CPU.Build.0 = Debug|Any CPU + {28055B05-25D4-4F17-9F36-A1D09FDDA607}.NativeDebug|x64.ActiveCfg = Debug|Any CPU + {28055B05-25D4-4F17-9F36-A1D09FDDA607}.NativeDebug|x86.ActiveCfg = Debug|Any CPU + {28055B05-25D4-4F17-9F36-A1D09FDDA607}.NativeRelease|Any CPU.ActiveCfg = Release|Any CPU + {28055B05-25D4-4F17-9F36-A1D09FDDA607}.NativeRelease|Any CPU.Build.0 = Release|Any CPU + {28055B05-25D4-4F17-9F36-A1D09FDDA607}.NativeRelease|x64.ActiveCfg = Release|Any CPU + {28055B05-25D4-4F17-9F36-A1D09FDDA607}.NativeRelease|x86.ActiveCfg = Release|Any CPU + {28055B05-25D4-4F17-9F36-A1D09FDDA607}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28055B05-25D4-4F17-9F36-A1D09FDDA607}.Release|Any CPU.Build.0 = Release|Any CPU + {28055B05-25D4-4F17-9F36-A1D09FDDA607}.Release|x64.ActiveCfg = Release|Any CPU + {28055B05-25D4-4F17-9F36-A1D09FDDA607}.Release|x64.Build.0 = Release|Any CPU + {28055B05-25D4-4F17-9F36-A1D09FDDA607}.Release|x86.ActiveCfg = Release|Any CPU + {28055B05-25D4-4F17-9F36-A1D09FDDA607}.Release|x86.Build.0 = Release|Any CPU + {F6FAA65F-AA29-4DDA-AA89-C16AF4A69F9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F6FAA65F-AA29-4DDA-AA89-C16AF4A69F9F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F6FAA65F-AA29-4DDA-AA89-C16AF4A69F9F}.Debug|x64.ActiveCfg = Debug|Any CPU + {F6FAA65F-AA29-4DDA-AA89-C16AF4A69F9F}.Debug|x86.ActiveCfg = Debug|Any CPU + {F6FAA65F-AA29-4DDA-AA89-C16AF4A69F9F}.NativeDebug|Any CPU.ActiveCfg = Debug|Any CPU + {F6FAA65F-AA29-4DDA-AA89-C16AF4A69F9F}.NativeDebug|Any CPU.Build.0 = Debug|Any CPU + {F6FAA65F-AA29-4DDA-AA89-C16AF4A69F9F}.NativeDebug|x64.ActiveCfg = Debug|Any CPU + {F6FAA65F-AA29-4DDA-AA89-C16AF4A69F9F}.NativeDebug|x86.ActiveCfg = Debug|Any CPU + {F6FAA65F-AA29-4DDA-AA89-C16AF4A69F9F}.NativeRelease|Any CPU.ActiveCfg = Release|Any CPU + {F6FAA65F-AA29-4DDA-AA89-C16AF4A69F9F}.NativeRelease|Any CPU.Build.0 = Release|Any CPU + {F6FAA65F-AA29-4DDA-AA89-C16AF4A69F9F}.NativeRelease|x64.ActiveCfg = Release|Any CPU + {F6FAA65F-AA29-4DDA-AA89-C16AF4A69F9F}.NativeRelease|x86.ActiveCfg = Release|Any CPU + {F6FAA65F-AA29-4DDA-AA89-C16AF4A69F9F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F6FAA65F-AA29-4DDA-AA89-C16AF4A69F9F}.Release|Any CPU.Build.0 = Release|Any CPU + {F6FAA65F-AA29-4DDA-AA89-C16AF4A69F9F}.Release|x64.ActiveCfg = Release|Any CPU + {F6FAA65F-AA29-4DDA-AA89-C16AF4A69F9F}.Release|x64.Build.0 = Release|Any CPU + {F6FAA65F-AA29-4DDA-AA89-C16AF4A69F9F}.Release|x86.ActiveCfg = Release|Any CPU + {F6FAA65F-AA29-4DDA-AA89-C16AF4A69F9F}.Release|x86.Build.0 = Release|Any CPU + {980DAB60-6471-46EC-82EE-B457D91C3789}.Debug|Any CPU.ActiveCfg = Debug|x86 + {980DAB60-6471-46EC-82EE-B457D91C3789}.Debug|x64.ActiveCfg = Debug|x64 + {980DAB60-6471-46EC-82EE-B457D91C3789}.Debug|x64.Build.0 = Debug|x64 + {980DAB60-6471-46EC-82EE-B457D91C3789}.Debug|x86.ActiveCfg = Debug|x86 + {980DAB60-6471-46EC-82EE-B457D91C3789}.Debug|x86.Build.0 = Debug|x86 + {980DAB60-6471-46EC-82EE-B457D91C3789}.NativeDebug|Any CPU.ActiveCfg = Release|x64 + {980DAB60-6471-46EC-82EE-B457D91C3789}.NativeDebug|Any CPU.Build.0 = Release|x64 + {980DAB60-6471-46EC-82EE-B457D91C3789}.NativeDebug|x64.ActiveCfg = Debug|x64 + {980DAB60-6471-46EC-82EE-B457D91C3789}.NativeDebug|x86.ActiveCfg = Debug|x86 + {980DAB60-6471-46EC-82EE-B457D91C3789}.NativeRelease|Any CPU.ActiveCfg = Release|x64 + {980DAB60-6471-46EC-82EE-B457D91C3789}.NativeRelease|Any CPU.Build.0 = Release|x64 + {980DAB60-6471-46EC-82EE-B457D91C3789}.NativeRelease|x64.ActiveCfg = Release|x64 + {980DAB60-6471-46EC-82EE-B457D91C3789}.NativeRelease|x86.ActiveCfg = Release|x86 + {980DAB60-6471-46EC-82EE-B457D91C3789}.Release|Any CPU.ActiveCfg = Release|x86 + {980DAB60-6471-46EC-82EE-B457D91C3789}.Release|x64.ActiveCfg = Release|x64 + {980DAB60-6471-46EC-82EE-B457D91C3789}.Release|x64.Build.0 = Release|x64 + {980DAB60-6471-46EC-82EE-B457D91C3789}.Release|x86.ActiveCfg = Release|x86 + {980DAB60-6471-46EC-82EE-B457D91C3789}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {E4E2BDC4-A9C6-4AE9-B429-032EC83EDE64} = {C74B8F36-FD2F-45C9-9B8A-00E7CF0126A9} + {8B3446E8-E6A8-4591-AA63-A95837C6E97C} = {04B1EDB6-E967-4D25-89B9-E6F8304038CD} + {4106DB10-E09F-480E-9CE6-B39235512EE6} = {EF30B533-D715-421A-92B7-92FEF460AC9C} + {4E3E1F5C-CD52-4CC0-A35F-D1FA1685D2FA} = {EF30B533-D715-421A-92B7-92FEF460AC9C} + {9BC4AFCB-325D-4C81-8228-8CF301CE2F97} = {C74B8F36-FD2F-45C9-9B8A-00E7CF0126A9} + {679FA2A2-898B-4320-884E-C2D294A97CE1} = {744ACDC6-F6A0-4FF9-9421-F25C5F2DC520} + {46A8612B-418B-4D70-B3A7-A21DD0627473} = {04B1EDB6-E967-4D25-89B9-E6F8304038CD} + {13FD8F12-FFBE-4D01-B4AC-444F2994B04F} = {744ACDC6-F6A0-4FF9-9421-F25C5F2DC520} + {064D860B-4D7C-4B1D-918F-E020F1B99E2A} = {EF30B533-D715-421A-92B7-92FEF460AC9C} + {744ACDC6-F6A0-4FF9-9421-F25C5F2DC520} = {EF30B533-D715-421A-92B7-92FEF460AC9C} + {1EAC8125-1765-4E2D-8CBE-56DC98A1C8C1} = {EF30B533-D715-421A-92B7-92FEF460AC9C} + {16E521CE-77F1-4B1C-A183-520A41C4F372} = {04B1EDB6-E967-4D25-89B9-E6F8304038CD} + {06CA2C2B-83B0-4D83-905A-E0C74790009E} = {04B1EDB6-E967-4D25-89B9-E6F8304038CD} + {4787A64F-9A3E-4867-A55A-70CB4B2B2FFE} = {16E521CE-77F1-4B1C-A183-520A41C4F372} + {439824F9-1455-4CC4-BD79-B44FA0A16552} = {16E521CE-77F1-4B1C-A183-520A41C4F372} + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B} = {06CA2C2B-83B0-4D83-905A-E0C74790009E} + {55494E58-E061-4C4C-A0A8-837008E72F85} = {06CA2C2B-83B0-4D83-905A-E0C74790009E} + {09D9D1D6-2951-4E14-BC35-76A23CF9391A} = {06CA2C2B-83B0-4D83-905A-E0C74790009E} + {42E60F88-E23F-417A-8143-0CCEC05E1D02} = {744ACDC6-F6A0-4FF9-9421-F25C5F2DC520} + {48F46909-E76A-4788-BCE1-E543C0E140FE} = {622D35C9-627B-466E-8D15-752968CC79AF} + {D57EA297-6DC2-4BC0-8C91-334863327863} = {06CA2C2B-83B0-4D83-905A-E0C74790009E} + {7F87406C-A3C8-4139-A68D-E4C344294A67} = {06CA2C2B-83B0-4D83-905A-E0C74790009E} + {CAC1267B-8778-4257-AAC6-CAF481723B01} = {EF30B533-D715-421A-92B7-92FEF460AC9C} + {1533E271-F61B-441B-8B74-59FB61DF0552} = {06CA2C2B-83B0-4D83-905A-E0C74790009E} + {1F0C8D9B-F47B-41F3-9FC9-6954B6DC7712} = {EF30B533-D715-421A-92B7-92FEF460AC9C} + {CE4FB142-91FB-4B34-BC96-A31120EF4009} = {04B1EDB6-E967-4D25-89B9-E6F8304038CD} + {A091777D-66B3-42E1-B95C-85322DE40706} = {EF30B533-D715-421A-92B7-92FEF460AC9C} + {A641A208-2974-4E48-BCFF-54E3AAFA4FB9} = {EF30B533-D715-421A-92B7-92FEF460AC9C} + {28055B05-25D4-4F17-9F36-A1D09FDDA607} = {EF30B533-D715-421A-92B7-92FEF460AC9C} + {F6FAA65F-AA29-4DDA-AA89-C16AF4A69F9F} = {EF30B533-D715-421A-92B7-92FEF460AC9C} + {980DAB60-6471-46EC-82EE-B457D91C3789} = {744ACDC6-F6A0-4FF9-9421-F25C5F2DC520} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {DB4F868D-E1AE-4FD7-9333-69FA15B268C5} + EndGlobalSection +EndGlobal diff --git a/src/IISIntegration/NuGetPackageVerifier.json b/src/IISIntegration/NuGetPackageVerifier.json new file mode 100644 index 0000000000..a20c45992c --- /dev/null +++ b/src/IISIntegration/NuGetPackageVerifier.json @@ -0,0 +1,14 @@ +{ + "adx-nonshipping": { + "rules": [], + "packages": { + "Microsoft.AspNetCore.AspNetCoreModule": {}, + "Microsoft.AspNetCore.AspNetCoreModuleV2": {} + } + }, + "Default": { + "rules": [ + "DefaultCompositeRule" + ] + } +} \ No newline at end of file diff --git a/src/IISIntegration/NuGetPackageVerifier.xplat.json b/src/IISIntegration/NuGetPackageVerifier.xplat.json new file mode 100644 index 0000000000..c5f5582998 --- /dev/null +++ b/src/IISIntegration/NuGetPackageVerifier.xplat.json @@ -0,0 +1,7 @@ +{ + "Default": { + "rules": [ + "DefaultCompositeRule" + ] + } +} diff --git a/src/IISIntegration/benchmarks/IIS.Performance/FirstRequestConfig.cs b/src/IISIntegration/benchmarks/IIS.Performance/FirstRequestConfig.cs new file mode 100644 index 0000000000..727746871f --- /dev/null +++ b/src/IISIntegration/benchmarks/IIS.Performance/FirstRequestConfig.cs @@ -0,0 +1,39 @@ +// 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 BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Toolchains.CsProj; +using BenchmarkDotNet.Toolchains.DotNetCli; +using BenchmarkDotNet.Validators; + +namespace BenchmarkDotNet.Attributes +{ + internal class FirstRequestConfig : ManualConfig + { + public FirstRequestConfig() + { + Add(ConsoleLogger.Default); + Add(MarkdownExporter.GitHub); + + Add(MemoryDiagnoser.Default); + Add(StatisticColumn.OperationsPerSecond); + Add(DefaultColumnProviders.Instance); + + Add(JitOptimizationsValidator.FailOnError); + + Add(Job.Core + .With(CsProjCoreToolchain.From(NetCoreAppSettings.NetCoreApp21)) + .With(new GcMode { Server = true }) + .WithTargetCount(10) + .WithInvocationCount(1) + .WithUnrollFactor(1) + .With(RunStrategy.ColdStart)); + } + } +} diff --git a/src/IISIntegration/benchmarks/IIS.Performance/IIS.Performance.csproj b/src/IISIntegration/benchmarks/IIS.Performance/IIS.Performance.csproj new file mode 100644 index 0000000000..6373cfdfa7 --- /dev/null +++ b/src/IISIntegration/benchmarks/IIS.Performance/IIS.Performance.csproj @@ -0,0 +1,46 @@ + + + + + IIS.Performance + Microsoft.AspNetCore.Server.IIS.Performance + netcoreapp2.2 + Exe + true + true + false + + + + + + + + + + + + + + + + + + + False + + + False + + + False + + + + + + + + + + \ No newline at end of file diff --git a/src/IISIntegration/benchmarks/IIS.Performance/PlaintextBenchmark.cs b/src/IISIntegration/benchmarks/IIS.Performance/PlaintextBenchmark.cs new file mode 100644 index 0000000000..8c6b4b2cd3 --- /dev/null +++ b/src/IISIntegration/benchmarks/IIS.Performance/PlaintextBenchmark.cs @@ -0,0 +1,73 @@ +// 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.Http; +using System.Text; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IIS.Performance +{ + [AspNetCoreBenchmark] + public class PlaintextBenchmark + { + private TestServer _server; + + private HttpClient _client; + + [GlobalSetup] + public void Setup() + { + _server = TestServer.Create(builder => builder.UseMiddleware(), new LoggerFactory()).GetAwaiter().GetResult(); + // Recreate client, TestServer.Client has additional logging that can hurt performance + _client = new HttpClient() + { + BaseAddress = _server.HttpClient.BaseAddress + }; + } + + [Benchmark] + public async Task Plaintext() + { + await _client.GetAsync("/plaintext"); + } + + // Copied from https://github.com/aspnet/benchmarks/blob/dev/src/Benchmarks/Middleware/PlaintextMiddleware.cs + public class PlaintextMiddleware + { + private static readonly PathString _path = new PathString("/plaintext"); + private static readonly byte[] _helloWorldPayload = Encoding.UTF8.GetBytes("Hello, World!"); + + private readonly RequestDelegate _next; + + public PlaintextMiddleware(RequestDelegate next) + { + _next = next; + } + + public Task Invoke(HttpContext httpContext) + { + if (httpContext.Request.Path.StartsWithSegments(_path, StringComparison.Ordinal)) + { + return WriteResponse(httpContext.Response); + } + + return _next(httpContext); + } + + public static Task WriteResponse(HttpResponse response) + { + var payloadLength = _helloWorldPayload.Length; + response.StatusCode = 200; + response.ContentType = "text/plain"; + response.ContentLength = payloadLength; + return response.Body.WriteAsync(_helloWorldPayload, 0, payloadLength); + } +} + } +} diff --git a/src/IISIntegration/benchmarks/IIS.Performance/StartupTimeBenchmark.cs b/src/IISIntegration/benchmarks/IIS.Performance/StartupTimeBenchmark.cs new file mode 100644 index 0000000000..ef66b6b671 --- /dev/null +++ b/src/IISIntegration/benchmarks/IIS.Performance/StartupTimeBenchmark.cs @@ -0,0 +1,50 @@ +// 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.IO; +using System.Net.Http; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.AspNetCore.Server.IIS.Performance +{ + [AspNetCoreBenchmark(typeof(FirstRequestConfig))] + public class StartupTimeBenchmark + { + private ApplicationDeployer _deployer; + public HttpClient _client; + + [IterationSetup] + public void Setup() + { + var deploymentParameters = new DeploymentParameters(Path.Combine(TestPathUtilities.GetSolutionRootDirectory("IISIntegration"), "test/Websites/InProcessWebSite"), + ServerType.IISExpress, + RuntimeFlavor.CoreClr, + RuntimeArchitecture.x64) + { + ServerConfigTemplateContent = File.ReadAllText("IISExpress.config"), + SiteName = "HttpTestSite", + TargetFramework = "netcoreapp2.1", + ApplicationType = ApplicationType.Portable, + AncmVersion = AncmVersion.AspNetCoreModuleV2 + }; + _deployer = ApplicationDeployerFactory.Create(deploymentParameters, NullLoggerFactory.Instance); + _client = _deployer.DeployAsync().Result.HttpClient; + } + + [IterationCleanup] + public void Cleanup() + { + _deployer.Dispose(); + } + + [Benchmark] + public async Task SendFirstRequest() + { + var response = await _client.GetAsync(""); + } + } +} diff --git a/src/IISIntegration/build/Build.Settings b/src/IISIntegration/build/Build.Settings new file mode 100644 index 0000000000..d60b07c269 --- /dev/null +++ b/src/IISIntegration/build/Build.Settings @@ -0,0 +1,98 @@ + + + + $(MSBuildThisFileDirectory)..\ + Debug + Win32 + v120 + v140 + v120 + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + $(OutputPath) + aspnetcore + + + + true + false + + + + false + true + ..\DefaultRules.ruleset + false + + true + $(RunCodeAnalysis) + + + + + Use + true + true + true + + + Windows + true + $(OutDir)$(TargetName).pdb + $(OutDir)$(TargetName).pub.pdb + true + true + true + + + + + + Disabled + + + + + + MaxSpeed + true + true + + + + + + WIN32;_DEBUG;%(PreprocessorDefinitions) + + + + + + _WIN64;_DEBUG;%(PreprocessorDefinitions) + + + + + + WIN32;NDEBUG;%(PreprocessorDefinitions) + + + true + + + + + + _WIN64;NDEBUG;%(PreprocessorDefinitions) + + + true + + + + + <_TwoDigitYear>$([MSBuild]::Subtract($([System.DateTime]::UtcNow.Year), 2000)) + <_ThreeDigitDayOfYear>$([System.DateTime]::UtcNow.DayOfYear.ToString().PadLeft(3, '0')) + $(_TwoDigitYear)$(_ThreeDigitDayOfYear) + + + \ No newline at end of file diff --git a/src/IISIntegration/build/Config.Definitions.Props b/src/IISIntegration/build/Config.Definitions.Props new file mode 100644 index 0000000000..974816b5d7 --- /dev/null +++ b/src/IISIntegration/build/Config.Definitions.Props @@ -0,0 +1,21 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + \ No newline at end of file diff --git a/src/IISIntegration/build/Key.snk b/src/IISIntegration/build/Key.snk new file mode 100644 index 0000000000..e10e4889c1 Binary files /dev/null and b/src/IISIntegration/build/Key.snk differ diff --git a/src/IISIntegration/build/applicationhost.config b/src/IISIntegration/build/applicationhost.config new file mode 100644 index 0000000000..ce00a887b4 --- /dev/null +++ b/src/IISIntegration/build/applicationhost.config @@ -0,0 +1,968 @@ + + + + + + +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/build/applicationhost.iis.config b/src/IISIntegration/build/applicationhost.iis.config new file mode 100644 index 0000000000..34a4da59e6 --- /dev/null +++ b/src/IISIntegration/build/applicationhost.iis.config @@ -0,0 +1,732 @@ + + + + + + + + +
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+ + +
+
+
+
+ +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/build/assets.props b/src/IISIntegration/build/assets.props new file mode 100644 index 0000000000..13b30556bd --- /dev/null +++ b/src/IISIntegration/build/assets.props @@ -0,0 +1,269 @@ + + + + true + x64 + $(Platform) + Win32 + $(NativePlatform) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(MSBuildThisFileDirectory)..\src\AspNetCoreModuleV1\AspNetCore\bin\$(Configuration)\$(NativeVCPlatform)\aspnetcore.dll + $(MSBuildThisFileDirectory)..\src\AspNetCoreModuleV2\AspNetCore\bin\$(Configuration)\$(NativeVCPlatform)\aspnetcorev2.dll + $(MSBuildThisFileDirectory)..\src\AspNetCoreModuleV2\InProcessRequestHandler\bin\$(Configuration)\$(NativeVCPlatform)\aspnetcorev2_inprocess.dll + $(MSBuildThisFileDirectory)..\src\AspNetCoreModuleV2\OutOfProcessRequestHandler\bin\$(Configuration)\$(NativeVCPlatform)\aspnetcorev2_outofprocess.dll + + diff --git a/src/IISIntegration/build/buildpipeline/pipeline.groovy b/src/IISIntegration/build/buildpipeline/pipeline.groovy new file mode 100644 index 0000000000..13fd8d9add --- /dev/null +++ b/src/IISIntegration/build/buildpipeline/pipeline.groovy @@ -0,0 +1,18 @@ +import org.dotnet.ci.pipelines.Pipeline + +def windowsPipeline = Pipeline.createPipeline(this, 'build/buildpipeline/windows.groovy') + +def configurations = [ + 'Debug', + 'Release' +] + +configurations.each { configuration -> + + def params = [ + 'Configuration': configuration + ] + + windowsPipeline.triggerPipelineOnEveryGithubPR("Windows ${configuration} x64 Build", params) + windowsPipeline.triggerPipelineOnGithubPush(params) +} diff --git a/src/IISIntegration/build/buildpipeline/windows-appverif.groovy b/src/IISIntegration/build/buildpipeline/windows-appverif.groovy new file mode 100644 index 0000000000..0c1a6affe8 --- /dev/null +++ b/src/IISIntegration/build/buildpipeline/windows-appverif.groovy @@ -0,0 +1,15 @@ +@Library('dotnet-ci') _ + +// 'node' indicates to Jenkins that the enclosed block runs on a node that matches +// the label 'windows-with-vs' +simpleNode('Windows.10.Amd64.EnterpriseRS3.ASPNET.Open') { + stage ('Checking out source') { + checkout scm + bat 'git submodule update --init --recursive' + } + stage ('Build') { + def logFolder = getLogFolder() + def environment = "\$env:ASPNETCORE_TEST_LOG_DIR='${WORKSPACE}\\${logFolder}'" + bat "powershell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command \"${environment};&.\\tools\\SetupTestEnvironment.ps1 Setup;&.\\tools\\update_schema.ps1;&.\\tools\\UpdateIISExpressCertificate.ps1;&.\\run.cmd -CI default-build /p:Configuration=${params.Configuration}\";" + } +} diff --git a/src/IISIntegration/build/buildpipeline/windows.groovy b/src/IISIntegration/build/buildpipeline/windows.groovy new file mode 100644 index 0000000000..c482e84079 --- /dev/null +++ b/src/IISIntegration/build/buildpipeline/windows.groovy @@ -0,0 +1,15 @@ +@Library('dotnet-ci') _ + +// 'node' indicates to Jenkins that the enclosed block runs on a node that matches +// the label 'windows-with-vs' +simpleNode('Windows.10.Amd64.EnterpriseRS3.ASPNET.Open') { + stage ('Checking out source') { + checkout scm + bat 'git submodule update --init --recursive' + } + stage ('Build') { + def logFolder = getLogFolder() + def environment = "\$env:ASPNETCORE_TEST_LOG_DIR='${WORKSPACE}\\${logFolder}'" + bat "powershell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command \"${environment};&.\\tools\\SetupTestEnvironment.ps1 SetupDumps;&.\\tools\\update_schema.ps1;&.\\tools\\UpdateIISExpressCertificate.ps1;&.\\run.cmd -CI default-build /p:SkipIISBackwardsCompatibilityTests=true /p:SkipIISForwardsCompatibilityTests=true /p:Configuration=${params.Configuration}\";" + } +} diff --git a/src/IISIntegration/build/dependencies.props b/src/IISIntegration/build/dependencies.props new file mode 100644 index 0000000000..2f9ebbca5b --- /dev/null +++ b/src/IISIntegration/build/dependencies.props @@ -0,0 +1,68 @@ + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + 0.10.13 + 2.2.0-preview2-20181019.5 + 2.2.0-rtm-35541 + 2.2.0-rtm-35541 + 2.2.0-rtm-35541 + 2.2.0-rtm-35541 + 2.2.0-rtm-35541 + 2.2.0-rtm-35541 + 2.2.0-rtm-35541 + 2.2.0-rtm-35541 + 2.2.0-rtm-35541 + 2.2.0-rtm-35541 + 2.2.0-rtm-35541 + 0.6.0-rtm-35541 + 2.2.0-rtm-35541 + 2.2.0-rtm-35541 + 2.2.0-rtm-35541 + 2.2.0-rtm-35541 + 15.6.82 + 15.6.82 + 2.2.0-rtm-35541 + 2.2.0-rtm-35541 + 2.2.0-rtm-35541 + 2.2.0-rtm-35541 + 2.2.0-rtm-35541 + 2.2.0-rtm-35541 + 2.2.0-rtm-35541 + 2.2.0-rtm-35541 + 2.2.0-rtm-35541 + 2.2.0-rtm-35541 + 2.1.3 + 2.2.0-rtm-27023-02 + 1.0.1 + 2.2.0-rtm-35541 + 15.6.1 + 11.1.0 + 2.0.3 + 4.5.0 + 4.5.0 + 4.5.2 + 4.5.1 + 4.5.1 + 4.5.0 + 4.5.1 + 4.5.0 + 4.5.0 + 9.0.1 + 2.3.1 + 2.4.0 + + + + 2.2.0-preview3-35497 + 2.2.0-preview3-35497 + 2.2.0-preview3-35497 + 2.1.0 + 2.1.0 + 2.1.0 + 2.1.0 + 4.5.2 + 4.5.0 + + diff --git a/src/IISIntegration/build/functional-test-assets.targets b/src/IISIntegration/build/functional-test-assets.targets new file mode 100644 index 0000000000..81fb82b4a3 --- /dev/null +++ b/src/IISIntegration/build/functional-test-assets.targets @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/IISIntegration/build/launchSettings.json b/src/IISIntegration/build/launchSettings.json new file mode 100644 index 0000000000..246b7a0b47 --- /dev/null +++ b/src/IISIntegration/build/launchSettings.json @@ -0,0 +1,42 @@ +{ + "iisSettings": { + "windowsAuthentication": true, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5762/", + "sslPort": 0 + } + }, + "profiles": { + "ANCM IIS Express": { + "commandName": "Executable", + "executablePath": "$(IISExpressPath)", + "commandLineArgs": "$(IISExpressArguments)", + "environmentVariables": { + "IIS_SITE_PATH": "$(MSBuildThisFileDirectory)", + "ANCM_PATH": "$(AspNetCoreModuleV1ShimDll)", + "ANCMV2_PATH": "$(AspNetCoreModuleV2ShimDll)", + "ANCM_OUTOFPROCESS_HANDLER": "$(AspNetCoreModuleV2OutOfProcessHandlerDll)", + "LAUNCHER_ARGS": "$(TargetPath)", + "ASPNETCORE_ENVIRONMENT": "Development", + "LAUNCHER_PATH": "$(DotNetPath)", + "ASPNETCORE_MODULE_DEBUG": "console" + } + }, + "ANCM IIS": { + "commandName": "Executable", + "executablePath": "$(IISPath)", + "commandLineArgs": "$(IISArguments)", + "environmentVariables": { + "IIS_SITE_PATH": "$(MSBuildThisFileDirectory)", + "ANCM_PATH": "$(AspNetCoreModuleV1ShimDll)", + "ANCMV2_PATH": "$(AspNetCoreModuleV2ShimDll)", + "ASPNETCORE_MODULE_OUTOFPROCESS_HANDLER": "$(AspNetCoreModuleV2OutOfProcessHandlerDll)", + "LAUNCHER_ARGS": "$(TargetPath)", + "ASPNETCORE_ENVIRONMENT": "Development", + "LAUNCHER_PATH": "$(DotNetPath)", + "ASPNETCORE_MODULE_DEBUG": "console" + } + } + } +} diff --git a/src/IISIntegration/build/native.targets b/src/IISIntegration/build/native.targets new file mode 100644 index 0000000000..1c5a981691 --- /dev/null +++ b/src/IISIntegration/build/native.targets @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/build/repo.props b/src/IISIntegration/build/repo.props new file mode 100644 index 0000000000..6a94ca900f --- /dev/null +++ b/src/IISIntegration/build/repo.props @@ -0,0 +1,44 @@ + + + + + $(BuildDir)AspNetCoreModule.zip + $(BuildDir)StressTestWebSite.zip + + + + + + + + + + + + + + + + + + + + + + + + + + + Internal.AspNetCore.Universe.Lineup + 2.2.0-* + https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json + + + + + + + + + diff --git a/src/IISIntegration/build/repo.targets b/src/IISIntegration/build/repo.targets new file mode 100644 index 0000000000..3e937d6bf9 --- /dev/null +++ b/src/IISIntegration/build/repo.targets @@ -0,0 +1,147 @@ + + + + $(PrepareDependsOn) + $(GetArtifactInfoDependsOn);GetNativeArtifactsInfo + BuildNativeAssets;$(CompileDependsOn) + $(PackageDependsOn);PackageNativeProjects;PackageStressTestApp + $(TestDependsOn);RunNativeTest + $(RepositoryRoot)NuGetPackageVerifier.xplat.json + $(RepositoryRoot)src\ + bin\$(Configuration)\ + + + + + + + + + + + -p:Configuration=Native$(Configuration) -v:m -nologo -clp:NoSummary -p:CommitHash=$(CommitHash) -m + + + + + + + + + + + + + + $(BuildDir)Microsoft.AspNetCore.AspNetCoreModule.$(PackageVersion).nupkg + $(BuildDir)Microsoft.AspNetCore.AspNetCoreModuleV2.$(PackageVersion).nupkg + + + + + NuGetPackage + Microsoft.AspNetCore.AspNetCoreModule + $(PackageVersion) + $(RepositoryRoot) + + + + + + NuGetPackage + Microsoft.AspNetCore.AspNetCoreModuleV2 + $(PackageVersion) + $(RepositoryRoot) + + + + + ZipArchive + $(RepositoryRoot) + shipoob + + + + ZipArchive + $(RepositoryRoot) + noship + + + + + + + + + + + $(MSBuildThisFileDirectory)..\nuget\Microsoft.AspNetCore.AspNetCoreModuleV2.props.in + $(MSBuildThisFileDirectory)..\artifacts\Microsoft.AspNetCore.AspNetCoreModuleV2.props + + + + + + + + + + + + + + + + + + + + + + + + -p:Configuration=$(Configuration) -v:m -nologo -clp:NoSummary + + + + + + + $(MSBuildThisFileDirectory)..\test\WebSites\StressTestWebSite\ + $(StressAppBasePath)bin\published\ + + + + + + + + + + + + diff --git a/src/IISIntegration/build/sources.props b/src/IISIntegration/build/sources.props new file mode 100644 index 0000000000..9215df9751 --- /dev/null +++ b/src/IISIntegration/build/sources.props @@ -0,0 +1,17 @@ + + + + + $(DotNetRestoreSources) + + $(RestoreSources); + https://dotnet.myget.org/F/dotnet-core/api/v3/index.json; + https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json; + https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json; + + + $(RestoreSources); + https://api.nuget.org/v3/index.json; + + + diff --git a/src/IISIntegration/build/testsite.props b/src/IISIntegration/build/testsite.props new file mode 100644 index 0000000000..74f205b4cf --- /dev/null +++ b/src/IISIntegration/build/testsite.props @@ -0,0 +1,81 @@ + + + + win7-x64;win7-x86 + x64;x86 + $(MSBuildThisFileDirectory)applicationhost.config + $(MSBuildThisFileDirectory)applicationhost.iis.config + false + True + + + + + + $(MSBuildProgramFiles32)\IIS Express\iisexpress.exe + $(SystemRoot)\SysWOW64\inetsrv\w3wp.exe + Win32 + + + + $(ProgramW6432)\IIS Express\iisexpress.exe + $(SystemRoot)\System32\inetsrv\w3wp.exe + x64 + + + + + $(NativePlatform)\ + + + + + /config:"$(IISExpressAppHostConfig)" /systray:false + -h "$(IISAppHostConfig)" + + aspnetcorev2_inprocess.dll + $(userprofile)\.dotnet\$(NativePlatform)\dotnet.exe + + + + + + + + + + + False + + + + + + $(MSBuildThisFileDirectory)..\test\TestTasks\bin\$(Configuration)\$(TargetFramework)\TestTasks + $(InjectDepsAssembly) + "win7-$(NativePlatform)" "$(AncmInProcessRHPath)" + + + + $(InjectDepsAssembly).exe + $(InjectDepsAssembly) + + + + $(InjectDepsAssembly).dll + dotnet + $(InjectDepsAssembly) $(InjectDepsArguments) + + + + + + + + + + + + diff --git a/src/IISIntegration/korebuild.json b/src/IISIntegration/korebuild.json new file mode 100644 index 0000000000..2624779e51 --- /dev/null +++ b/src/IISIntegration/korebuild.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/release/2.2/tools/korebuild.schema.json", + "channel": "release/2.2", + "toolsets": { + "visualstudio": { + "required": ["Windows"], + "includePrerelease": true, + "minVersion": "15.0.26730.03", + "requiredWorkloads": [ + "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + "Microsoft.VisualStudio.ComponentGroup.NativeDesktop.Win81", + "Microsoft.VisualStudio.Component.VC.ATL", + "Microsoft.VisualStudio.Component.Windows10SDK.15063.Desktop" + ] + } + } + } diff --git a/src/IISIntegration/nuget/Microsoft.AspNetCore.AspNetCoreModule.nuspec b/src/IISIntegration/nuget/Microsoft.AspNetCore.AspNetCoreModule.nuspec new file mode 100644 index 0000000000..f05fcbbdd8 --- /dev/null +++ b/src/IISIntegration/nuget/Microsoft.AspNetCore.AspNetCoreModule.nuspec @@ -0,0 +1,33 @@ + + + + Microsoft.AspNetCore.AspNetCoreModule + Microsoft ASP.NET Core Module + $VERSION$ + Microsoft + Microsoft + https://www.microsoft.com/web/webpi/eula/net_library_eula_ENU.htm + © .NET Foundation. All rights reserved. + https://www.asp.net/ + https://go.microsoft.com/fwlink/?LinkID=288859 + true + ASP.NET Core Module + en-US + Microsoft.AspNetCore.AspNetCoreModule + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/nuget/Microsoft.AspNetCore.AspNetCoreModule.props b/src/IISIntegration/nuget/Microsoft.AspNetCore.AspNetCoreModule.props new file mode 100644 index 0000000000..7a761813b4 --- /dev/null +++ b/src/IISIntegration/nuget/Microsoft.AspNetCore.AspNetCoreModule.props @@ -0,0 +1,8 @@ + + + + $(MSBuildThisFileDirectory)..\contentFiles\any\any\x64\aspnetcore.dll + $(MSBuildThisFileDirectory)..\contentFiles\any\any\x86\aspnetcore.dll + + + diff --git a/src/IISIntegration/nuget/Microsoft.AspNetCore.AspNetCoreModuleV2.nuspec b/src/IISIntegration/nuget/Microsoft.AspNetCore.AspNetCoreModuleV2.nuspec new file mode 100644 index 0000000000..bc33d6121c --- /dev/null +++ b/src/IISIntegration/nuget/Microsoft.AspNetCore.AspNetCoreModuleV2.nuspec @@ -0,0 +1,44 @@ + + + + Microsoft.AspNetCore.AspNetCoreModuleV2 + Microsoft ASP.NET Core Module + $VERSION$ + Microsoft + Microsoft + https://www.microsoft.com/web/webpi/eula/net_library_eula_ENU.htm + © .NET Foundation. All rights reserved. + https://www.asp.net/ + https://go.microsoft.com/fwlink/?LinkID=288859 + true + ASP.NET Core Module + en-US + Microsoft.AspNetCore.AspNetCoreModule + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/nuget/Microsoft.AspNetCore.AspNetCoreModuleV2.props.in b/src/IISIntegration/nuget/Microsoft.AspNetCore.AspNetCoreModuleV2.props.in new file mode 100644 index 0000000000..09af105513 --- /dev/null +++ b/src/IISIntegration/nuget/Microsoft.AspNetCore.AspNetCoreModuleV2.props.in @@ -0,0 +1,13 @@ + + + + ${AspNetCoreModuleOutOfProcessVersion} + $(MSBuildThisFileDirectory)..\contentFiles\any\any\x64\aspnetcorev2.dll + $(MSBuildThisFileDirectory)..\contentFiles\any\any\x86\aspnetcorev2.dll + $(MSBuildThisFileDirectory)..\contentFiles\any\any\x64\aspnetcorev2_inprocess.dll + $(MSBuildThisFileDirectory)..\contentFiles\any\any\x86\aspnetcorev2_inprocess.dll + $(MSBuildThisFileDirectory)..\contentFiles\any\any\x64\$(AspNetCoreModuleOutOfProcessVersion)\aspnetcorev2_outofprocess.dll + $(MSBuildThisFileDirectory)..\contentFiles\any\any\x86\$(AspNetCoreModuleOutOfProcessVersion)\aspnetcorev2_outofprocess.dll + + + diff --git a/src/IISIntegration/samples/IISSample/IISSample.csproj b/src/IISIntegration/samples/IISSample/IISSample.csproj new file mode 100644 index 0000000000..556945519e --- /dev/null +++ b/src/IISIntegration/samples/IISSample/IISSample.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp2.2;net461 + + + + + + + + + + + diff --git a/src/IISIntegration/samples/IISSample/Properties/launchSettings.json b/src/IISIntegration/samples/IISSample/Properties/launchSettings.json new file mode 100644 index 0000000000..009aa9ab89 --- /dev/null +++ b/src/IISIntegration/samples/IISSample/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:25334/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.Server.IISIntegration" + } + }, + "IISSample": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/src/IISIntegration/samples/IISSample/Startup.cs b/src/IISIntegration/samples/IISSample/Startup.cs new file mode 100644 index 0000000000..f99b6ad729 --- /dev/null +++ b/src/IISIntegration/samples/IISSample/Startup.cs @@ -0,0 +1,107 @@ +// 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.Linq; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.IISIntegration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace IISSample +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + // These two middleware are registered via an IStartupFilter in UseIISIntegration but you can configure them here. + services.Configure(options => + { + options.AuthenticationDisplayName = "Windows Auth"; + }); + services.Configure(options => + { + }); + } + + public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory, IAuthenticationSchemeProvider authSchemeProvider) + { + var logger = loggerfactory.CreateLogger("Requests"); + + app.Run(async (context) => + { + logger.LogDebug("Received request: " + context.Request.Method + " " + context.Request.Path); + + context.Response.ContentType = "text/plain"; + await context.Response.WriteAsync("Hello World - " + DateTimeOffset.Now + Environment.NewLine); + await context.Response.WriteAsync(Environment.NewLine); + + await context.Response.WriteAsync("Address:" + Environment.NewLine); + await context.Response.WriteAsync("Scheme: " + context.Request.Scheme + Environment.NewLine); + await context.Response.WriteAsync("Host: " + context.Request.Headers["Host"] + Environment.NewLine); + await context.Response.WriteAsync("PathBase: " + context.Request.PathBase.Value + Environment.NewLine); + await context.Response.WriteAsync("Path: " + context.Request.Path.Value + Environment.NewLine); + await context.Response.WriteAsync("Query: " + context.Request.QueryString.Value + Environment.NewLine); + await context.Response.WriteAsync(Environment.NewLine); + + await context.Response.WriteAsync("Connection:" + Environment.NewLine); + await context.Response.WriteAsync("RemoteIp: " + context.Connection.RemoteIpAddress + Environment.NewLine); + await context.Response.WriteAsync("RemotePort: " + context.Connection.RemotePort + Environment.NewLine); + await context.Response.WriteAsync("LocalIp: " + context.Connection.LocalIpAddress + Environment.NewLine); + await context.Response.WriteAsync("LocalPort: " + context.Connection.LocalPort + Environment.NewLine); + await context.Response.WriteAsync("ClientCert: " + context.Connection.ClientCertificate + Environment.NewLine); + await context.Response.WriteAsync(Environment.NewLine); + + await context.Response.WriteAsync("User: " + context.User.Identity.Name + Environment.NewLine); + var scheme = await authSchemeProvider.GetSchemeAsync(IISDefaults.AuthenticationScheme); + await context.Response.WriteAsync("DisplayName: " + scheme?.DisplayName + Environment.NewLine); + await context.Response.WriteAsync(Environment.NewLine); + + await context.Response.WriteAsync("Headers:" + Environment.NewLine); + foreach (var header in context.Request.Headers) + { + await context.Response.WriteAsync(header.Key + ": " + header.Value + Environment.NewLine); + } + await context.Response.WriteAsync(Environment.NewLine); + + await context.Response.WriteAsync("Environment Variables:" + Environment.NewLine); + var vars = Environment.GetEnvironmentVariables(); + foreach (var key in vars.Keys.Cast().OrderBy(key => key, StringComparer.OrdinalIgnoreCase)) + { + var value = vars[key]; + await context.Response.WriteAsync(key + ": " + value + Environment.NewLine); + } + + await context.Response.WriteAsync(Environment.NewLine); + if (context.Features.Get() != null) + { + await context.Response.WriteAsync("Websocket feature is enabled."); + } + else + { + await context.Response.WriteAsync("Websocket feature is disabled."); + } + }); + } + + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .ConfigureLogging((_, factory) => + { + factory.AddConsole(); + factory.AddFilter("Console", level => level >= LogLevel.Debug); + }) + .UseKestrel() + .UseStartup() + .Build(); + + host.Run(); + } + } +} + diff --git a/src/IISIntegration/samples/IISSample/web.config b/src/IISIntegration/samples/IISSample/web.config new file mode 100644 index 0000000000..ca998a82a4 --- /dev/null +++ b/src/IISIntegration/samples/IISSample/web.config @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/samples/NativeIISSample/NativeIISSample.csproj b/src/IISIntegration/samples/NativeIISSample/NativeIISSample.csproj new file mode 100644 index 0000000000..5fcf5c96a8 --- /dev/null +++ b/src/IISIntegration/samples/NativeIISSample/NativeIISSample.csproj @@ -0,0 +1,24 @@ + + + + + + netcoreapp2.2 + true + + + + + + + + + + + + + + + inprocess + + diff --git a/src/IISIntegration/samples/NativeIISSample/Properties/launchSettings.json b/src/IISIntegration/samples/NativeIISSample/Properties/launchSettings.json new file mode 100644 index 0000000000..246b7a0b47 --- /dev/null +++ b/src/IISIntegration/samples/NativeIISSample/Properties/launchSettings.json @@ -0,0 +1,42 @@ +{ + "iisSettings": { + "windowsAuthentication": true, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5762/", + "sslPort": 0 + } + }, + "profiles": { + "ANCM IIS Express": { + "commandName": "Executable", + "executablePath": "$(IISExpressPath)", + "commandLineArgs": "$(IISExpressArguments)", + "environmentVariables": { + "IIS_SITE_PATH": "$(MSBuildThisFileDirectory)", + "ANCM_PATH": "$(AspNetCoreModuleV1ShimDll)", + "ANCMV2_PATH": "$(AspNetCoreModuleV2ShimDll)", + "ANCM_OUTOFPROCESS_HANDLER": "$(AspNetCoreModuleV2OutOfProcessHandlerDll)", + "LAUNCHER_ARGS": "$(TargetPath)", + "ASPNETCORE_ENVIRONMENT": "Development", + "LAUNCHER_PATH": "$(DotNetPath)", + "ASPNETCORE_MODULE_DEBUG": "console" + } + }, + "ANCM IIS": { + "commandName": "Executable", + "executablePath": "$(IISPath)", + "commandLineArgs": "$(IISArguments)", + "environmentVariables": { + "IIS_SITE_PATH": "$(MSBuildThisFileDirectory)", + "ANCM_PATH": "$(AspNetCoreModuleV1ShimDll)", + "ANCMV2_PATH": "$(AspNetCoreModuleV2ShimDll)", + "ASPNETCORE_MODULE_OUTOFPROCESS_HANDLER": "$(AspNetCoreModuleV2OutOfProcessHandlerDll)", + "LAUNCHER_ARGS": "$(TargetPath)", + "ASPNETCORE_ENVIRONMENT": "Development", + "LAUNCHER_PATH": "$(DotNetPath)", + "ASPNETCORE_MODULE_DEBUG": "console" + } + } + } +} diff --git a/src/IISIntegration/samples/NativeIISSample/Startup.cs b/src/IISIntegration/samples/NativeIISSample/Startup.cs new file mode 100644 index 0000000000..2b18ff895c --- /dev/null +++ b/src/IISIntegration/samples/NativeIISSample/Startup.cs @@ -0,0 +1,120 @@ +// 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.Linq; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.IIS; + +namespace NativeIISSample +{ + public class Startup + { + private readonly IAuthenticationSchemeProvider _authSchemeProvider; + + public Startup(IAuthenticationSchemeProvider authSchemeProvider = null) + { + _authSchemeProvider = authSchemeProvider; + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + app.Run(async (context) => + { + context.Response.ContentType = "text/plain"; + + await context.Response.WriteAsync("Hello World - " + DateTimeOffset.Now + Environment.NewLine); + await context.Response.WriteAsync(Environment.NewLine); + + await context.Response.WriteAsync("Address:" + Environment.NewLine); + await context.Response.WriteAsync("Scheme: " + context.Request.Scheme + Environment.NewLine); + await context.Response.WriteAsync("Host: " + context.Request.Headers["Host"] + Environment.NewLine); + await context.Response.WriteAsync("PathBase: " + context.Request.PathBase.Value + Environment.NewLine); + await context.Response.WriteAsync("Path: " + context.Request.Path.Value + Environment.NewLine); + await context.Response.WriteAsync("Query: " + context.Request.QueryString.Value + Environment.NewLine); + await context.Response.WriteAsync(Environment.NewLine); + + await context.Response.WriteAsync("Connection:" + Environment.NewLine); + await context.Response.WriteAsync("RemoteIp: " + context.Connection.RemoteIpAddress + Environment.NewLine); + await context.Response.WriteAsync("RemotePort: " + context.Connection.RemotePort + Environment.NewLine); + await context.Response.WriteAsync("LocalIp: " + context.Connection.LocalIpAddress + Environment.NewLine); + await context.Response.WriteAsync("LocalPort: " + context.Connection.LocalPort + Environment.NewLine); + await context.Response.WriteAsync("ClientCert: " + context.Connection.ClientCertificate + Environment.NewLine); + await context.Response.WriteAsync(Environment.NewLine); + + await context.Response.WriteAsync("User: " + context.User.Identity.Name + Environment.NewLine); + if (_authSchemeProvider != null) + { + var scheme = await _authSchemeProvider.GetSchemeAsync(IISServerDefaults.AuthenticationScheme); + await context.Response.WriteAsync("DisplayName: " + scheme?.DisplayName + Environment.NewLine); + } + + await context.Response.WriteAsync(Environment.NewLine); + + await context.Response.WriteAsync("Headers:" + Environment.NewLine); + foreach (var header in context.Request.Headers) + { + await context.Response.WriteAsync(header.Key + ": " + header.Value + Environment.NewLine); + } + await context.Response.WriteAsync(Environment.NewLine); + + await context.Response.WriteAsync("Environment Variables:" + Environment.NewLine); + var vars = Environment.GetEnvironmentVariables(); + foreach (var key in vars.Keys.Cast().OrderBy(key => key, StringComparer.OrdinalIgnoreCase)) + { + var value = vars[key]; + await context.Response.WriteAsync(key + ": " + value + Environment.NewLine); + } + await context.Response.WriteAsync(Environment.NewLine); + + // accessing IIS server variables + await context.Response.WriteAsync("Server Variables:" + Environment.NewLine); + + foreach (var varName in IISServerVarNames) + { + await context.Response.WriteAsync(varName + ": " + context.GetIISServerVariable(varName) + Environment.NewLine); + } + + await context.Response.WriteAsync(Environment.NewLine); + if (context.Features.Get() != null) + { + await context.Response.WriteAsync("Websocket feature is enabled."); + } + else + { + await context.Response.WriteAsync("Websocket feature is disabled."); + } + }); + } + + private static readonly string[] IISServerVarNames = + { + "AUTH_TYPE", + "AUTH_USER", + "CONTENT_TYPE", + "HTTP_HOST", + "HTTPS", + "REMOTE_PORT", + "REMOTE_USER", + "REQUEST_METHOD", + "WEBSOCKET_VERSION" + }; + + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseKestrel() + .UseIIS() + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/src/IISIntegration/samples/NativeIISSample/web.config b/src/IISIntegration/samples/NativeIISSample/web.config new file mode 100644 index 0000000000..b366ebfd8c --- /dev/null +++ b/src/IISIntegration/samples/NativeIISSample/web.config @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/AspNetCore.vcxproj b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/AspNetCore.vcxproj new file mode 100644 index 0000000000..169c79e503 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/AspNetCore.vcxproj @@ -0,0 +1,276 @@ + + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {439824F9-1455-4CC4-BD79-B44FA0A16552} + Win32Proj + AspNetCoreModule + AspNetCore + aspnetcore + false + 10.0.15063.0 + + + + DynamicLibrary + true + v141 + Unicode + + + DynamicLibrary + true + v141 + Unicode + + + DynamicLibrary + false + v141 + true + Unicode + + + DynamicLibrary + false + v141 + true + Unicode + + + + + + + + + + + + + + + + + + + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + + NotUsing + Level4 + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;ASPNETCOREMODULE_EXPORTS;%(PreprocessorDefinitions) + precomp.hxx + $(IntDir)$(TargetName).pch + ..\IISLib;inc\ + ProgramDatabase + MultiThreadedDebug + true + true + true + false + SyncCThrow + 8Bytes + true + false + true + CompileAsCpp + true + + + Windows + true + kernel32.lib;user32.lib;advapi32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;ahadmin.lib;rpcrt4.lib;winhttp.lib;pdh.lib;ws2_32.lib;wbemuuid.lib;iphlpapi.lib;%(AdditionalDependencies) + Source.def + + + ..\Commonlib + + + + + NotUsing + Level4 + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;ASPNETCOREMODULE_EXPORTS;%(PreprocessorDefinitions) + precomp.hxx + $(IntDir)$(TargetName).pch + ..\IISLib;inc\ + ProgramDatabase + MultiThreadedDebug + true + true + true + false + SyncCThrow + 8Bytes + true + false + true + CompileAsCpp + true + + + Windows + true + kernel32.lib;user32.lib;advapi32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;ahadmin.lib;rpcrt4.lib;winhttp.lib;pdh.lib;ws2_32.lib;wbemuuid.lib;iphlpapi.lib;%(AdditionalDependencies) + Source.def + + + ..\Commonlib + + + + + Level4 + NotUsing + MaxSpeed + true + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;ASPNETCOREMODULE_EXPORTS;%(PreprocessorDefinitions) + ..\IISLib;inc\ + precomp.hxx + MultiThreaded + true + true + true + false + SyncCThrow + 8Bytes + true + false + true + CompileAsCpp + true + + + Windows + false + true + true + Source.def + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;ahadmin.lib;winhttp.lib;odbc32.lib;ws2_32.lib;odbccp32.lib;wbemuuid.lib;iphlpapi.lib;pdh.lib;rpcrt4.lib;%(AdditionalDependencies) + + + ..\Commonlib + + + + + Level4 + NotUsing + MaxSpeed + true + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;ASPNETCOREMODULE_EXPORTS;%(PreprocessorDefinitions) + precomp.hxx + ..\IISLib;inc\ + MultiThreaded + true + true + true + false + SyncCThrow + 8Bytes + true + false + true + CompileAsCpp + true + + + Windows + false + true + true + Source.def + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;ahadmin.lib;rpcrt4.lib;winhttp.lib;pdh.lib;ws2_32.lib;wbemuuid.lib;iphlpapi.lib;%(AdditionalDependencies) + + + ..\Commonlib + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {4787a64f-9a3e-4867-a55a-70cb4b2b2ffe} + + + + + + + + + + Document + mc %(FullPath) + Compiling Event Messages ... + %(Filename).rc;%(Filename).h;MSG0409.bin + + + \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/application.h b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/application.h new file mode 100644 index 0000000000..e0ef14ec9c --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/application.h @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +// +// The key used for hash-table lookups, consists of the port on which the http process is created. +// +class APPLICATION_KEY +{ +public: + + APPLICATION_KEY( + VOID + ) : INLINE_STRU_INIT(m_struKey) + { + } + + HRESULT + Initialize( + _In_ LPCWSTR pszKey + ) + { + return m_struKey.Copy(pszKey); + } + + BOOL + GetIsEqual( + const APPLICATION_KEY * key2 + ) const + { + return m_struKey.Equals(key2->m_struKey); + } + + DWORD CalcKeyHash() const + { + return Hash(m_struKey.QueryStr()); + } + +private: + + INLINE_STRU(m_struKey, 1024); +}; + +class APP_OFFLINE_HTM +{ +public: + APP_OFFLINE_HTM(LPCWSTR pszPath) : m_cRefs(1) + { + m_Path.Copy( pszPath ); + } + + VOID + ReferenceAppOfflineHtm() const + { + InterlockedIncrement(&m_cRefs); + } + + VOID + DereferenceAppOfflineHtm() const + { + if (InterlockedDecrement(&m_cRefs) == 0) + { + delete this; + } + } + + BOOL + Load( + VOID + ) + { + BOOL fResult = TRUE; + LARGE_INTEGER li = {0}; + CHAR *pszBuff = NULL; + HANDLE handle = INVALID_HANDLE_VALUE; + + handle = CreateFile( m_Path.QueryStr(), + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + NULL ); + + if( handle == INVALID_HANDLE_VALUE ) + { + if ( GetLastError() == ERROR_FILE_NOT_FOUND ) + { + fResult = FALSE; + } + + // This Load() member function is supposed be called only when the change notification event of file creation or file modification happens. + // If file is currenlty locked exclusively by other processes, we might get INVALID_HANDLE_VALUE even though the file exists. In that case, we should return TRUE here. + goto Finished; + } + + if(!GetFileSizeEx( handle, &li )) + { + goto Finished; + } + + if( li.HighPart != 0 ) + { + // > 4gb file size not supported + // todo: log a warning at event log + goto Finished; + } + + DWORD bytesRead = 0; + + if(li.LowPart > 0) + { + pszBuff = new CHAR[ li.LowPart + 1 ]; + + if( ReadFile( handle, pszBuff, li.LowPart, &bytesRead, NULL ) ) + { + m_Contents.Copy( pszBuff, bytesRead ); + } + } + +Finished: + if( handle != INVALID_HANDLE_VALUE ) + { + CloseHandle(handle); + handle = INVALID_HANDLE_VALUE; + } + + if( pszBuff != NULL ) + { + delete[] pszBuff; + pszBuff = NULL; + } + + return fResult; + } + + mutable LONG m_cRefs; + STRA m_Contents; + STRU m_Path; +}; + +class APPLICATION_MANAGER; + +class APPLICATION +{ +public: + + APPLICATION() : m_pProcessManager(NULL), m_pApplicationManager(NULL), m_cRefs(1), + m_fAppOfflineFound(FALSE), m_pAppOfflineHtm(NULL), m_pFileWatcherEntry(NULL) + { + } + + APPLICATION_KEY * + QueryApplicationKey() + { + return &m_applicationKey; + } + + VOID + SetAppOfflineFound( + BOOL found + ) + { + m_fAppOfflineFound = found; + } + + BOOL + AppOfflineFound() + { + return m_fAppOfflineFound; + } + + HRESULT + GetProcess( + _In_ IHttpContext *context, + _In_ ASPNETCORE_CONFIG *pConfig, + _Out_ SERVER_PROCESS **ppServerProcess + ) + { + return m_pProcessManager->GetProcess( context, pConfig, ppServerProcess ); + } + + HRESULT + Recycle() + { + HRESULT hr = S_OK; + m_pProcessManager->ShutdownAllProcesses(); + return hr; + } + + VOID + ReferenceApplication() const + { + InterlockedIncrement(&m_cRefs); + } + + VOID + DereferenceApplication() const + { + if (InterlockedDecrement(&m_cRefs) == 0) + { + delete this; + } + } + + APP_OFFLINE_HTM* QueryAppOfflineHtm() + { + return m_pAppOfflineHtm; + } + + ~APPLICATION(); + + HRESULT + Initialize( + _In_ APPLICATION_MANAGER *pApplicationManager, + _In_ LPCWSTR pszApplication, + _In_ LPCWSTR pszPhysicalPath + ); + + VOID + UpdateAppOfflineFileHandle(); + + HRESULT + StartMonitoringAppOffline(); + +private: + + STRU m_strAppPhysicalPath; + mutable LONG m_cRefs; + APPLICATION_KEY m_applicationKey; + PROCESS_MANAGER* m_pProcessManager; + APPLICATION_MANAGER *m_pApplicationManager; + BOOL m_fAppOfflineFound; + APP_OFFLINE_HTM *m_pAppOfflineHtm; + FILE_WATCHER_ENTRY *m_pFileWatcherEntry; +}; + +class APPLICATION_HASH : + public HASH_TABLE +{ + +public: + + APPLICATION_HASH() + {} + + APPLICATION_KEY * + ExtractKey( + APPLICATION *pApplication + ) + { + return pApplication->QueryApplicationKey(); + } + + DWORD + CalcKeyHash( + APPLICATION_KEY *key + ) + { + return key->CalcKeyHash(); + } + + BOOL + EqualKeys( + APPLICATION_KEY *key1, + APPLICATION_KEY *key2 + ) + { + return key1->GetIsEqual(key2); + } + + VOID + ReferenceRecord( + APPLICATION *pApplication + ) + { + pApplication->ReferenceApplication(); + } + + VOID + DereferenceRecord( + APPLICATION *pApplication + ) + { + pApplication->DereferenceApplication(); + } + +private: + + APPLICATION_HASH(const APPLICATION_HASH &); + void operator=(const APPLICATION_HASH &); +}; \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/applicationmanager.h b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/applicationmanager.h new file mode 100644 index 0000000000..d9e626262d --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/applicationmanager.h @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#define DEFAULT_HASH_BUCKETS 293 + +class APPLICATION_MANAGER +{ +public: + + static + APPLICATION_MANAGER* + GetInstance( + VOID + ) + { + if( sm_pApplicationManager == NULL ) + { + sm_pApplicationManager = new APPLICATION_MANAGER(); + } + + return sm_pApplicationManager; + } + + static + VOID + Cleanup( + VOID + ) + { + if(sm_pApplicationManager != NULL) + { + delete sm_pApplicationManager; + sm_pApplicationManager = NULL; + } + } + + HRESULT + GetApplication( + _In_ IHttpContext* pContext, + _Out_ APPLICATION ** ppApplication + ); + + HRESULT + RecycleApplication( + _In_ LPCWSTR pszApplication + ); + + HRESULT + Get502ErrorPage( + _Out_ HTTP_DATA_CHUNK** ppErrorPage + ); + + ~APPLICATION_MANAGER() + { + if(m_pApplicationHash != NULL) + { + m_pApplicationHash->Clear(); + delete m_pApplicationHash; + m_pApplicationHash = NULL; + } + + if( m_pFileWatcher!= NULL ) + { + delete m_pFileWatcher; + m_pFileWatcher = NULL; + } + + if(m_pHttp502ErrorPage != NULL) + { + delete m_pHttp502ErrorPage; + m_pHttp502ErrorPage = NULL; + } + + } + + FILE_WATCHER* + GetFileWatcher() + { + return m_pFileWatcher; + } + + HRESULT Initialize() + { + HRESULT hr = S_OK; + + if(m_pApplicationHash == NULL) + { + m_pApplicationHash = new APPLICATION_HASH(); + if(m_pApplicationHash == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + hr = m_pApplicationHash->Initialize(DEFAULT_HASH_BUCKETS); + if(FAILED(hr)) + { + goto Finished; + } + } + + if( m_pFileWatcher == NULL ) + { + m_pFileWatcher = new FILE_WATCHER; + if(m_pFileWatcher == NULL) + { + hr = HRESULT_FROM_WIN32( ERROR_NOT_ENOUGH_MEMORY ); + goto Finished; + } + + m_pFileWatcher->Create(); + } + + Finished: + return hr; + } + +private: + // + // we currently limit the size of m_pstrErrorInfo to 5000, be careful if you want to change its payload + // + APPLICATION_MANAGER() : m_pApplicationHash(NULL), m_pFileWatcher(NULL), m_pHttp502ErrorPage(NULL), m_pstrErrorInfo( + " \ + \ + \ + \ + IIS 502.5 Error \ +
\ +

HTTP Error 502.5 - Process Failure

\ +
\ +

Common causes of this issue:

\ +
  • The application process failed to start
  • \ +
  • The application process started but then stopped
  • \ +
  • The application process started but failed to listen on the configured port
\ +
\ +
\ +

Troubleshooting steps:

\ +
  • Check the system event log for error messages
  • \ +
  • Enable logging the application process' stdout messages
  • \ +
  • Attach a debugger to the application process and inspect
\ +

For more information visit: \ + https://go.microsoft.com/fwlink/?LinkID=808681

\ +
\ +
\ +
") + { + InitializeSRWLock(&m_srwLock); + } + + FILE_WATCHER *m_pFileWatcher; + APPLICATION_HASH *m_pApplicationHash; + static APPLICATION_MANAGER *sm_pApplicationManager; + SRWLOCK m_srwLock; + HTTP_DATA_CHUNK *m_pHttp502ErrorPage; + LPSTR m_pstrErrorInfo; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/aspnetcoreconfig.h b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/aspnetcoreconfig.h new file mode 100644 index 0000000000..95b4303cee --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/aspnetcoreconfig.h @@ -0,0 +1,207 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once +#define CS_ROOTWEB_CONFIG L"MACHINE/WEBROOT/APPHOST/" +#define CS_ROOTWEB_CONFIG_LEN _countof(CS_ROOTWEB_CONFIG)-1 +#define CS_ASPNETCORE_SECTION L"system.webServer/aspNetCore" +#define CS_WINDOWS_AUTHENTICATION_SECTION L"system.webServer/security/authentication/windowsAuthentication" +#define CS_BASIC_AUTHENTICATION_SECTION L"system.webServer/security/authentication/basicAuthentication" +#define CS_ANONYMOUS_AUTHENTICATION_SECTION L"system.webServer/security/authentication/anonymousAuthentication" +#define CS_AUTHENTICATION_ENABLED L"enabled" +#define CS_ASPNETCORE_PROCESS_EXE_PATH L"processPath" +#define CS_ASPNETCORE_PROCESS_ARGUMENTS L"arguments" +#define CS_ASPNETCORE_PROCESS_STARTUP_TIME_LIMIT L"startupTimeLimit" +#define CS_ASPNETCORE_PROCESS_SHUTDOWN_TIME_LIMIT L"shutdownTimeLimit" +#define CS_ASPNETCORE_WINHTTP_REQUEST_TIMEOUT L"requestTimeout" +#define CS_ASPNETCORE_RAPID_FAILS_PER_MINUTE L"rapidFailsPerMinute" +#define CS_ASPNETCORE_STDOUT_LOG_ENABLED L"stdoutLogEnabled" +#define CS_ASPNETCORE_STDOUT_LOG_FILE L"stdoutLogFile" +#define CS_ASPNETCORE_ENVIRONMENT_VARIABLES L"environmentVariables" +#define CS_ASPNETCORE_ENVIRONMENT_VARIABLE L"environmentVariable" +#define CS_ASPNETCORE_ENVIRONMENT_VARIABLE_NAME L"name" +#define CS_ASPNETCORE_ENVIRONMENT_VARIABLE_VALUE L"value" +#define CS_ASPNETCORE_PROCESSES_PER_APPLICATION L"processesPerApplication" +#define CS_ASPNETCORE_FORWARD_WINDOWS_AUTH_TOKEN L"forwardWindowsAuthToken" +#define CS_ASPNETCORE_DISABLE_START_UP_ERROR_PAGE L"disableStartUpErrorPage" +#define CS_ASPNETCORE_RECYCLE_ON_FILE_CHANGE L"recycleOnFileChange" +#define CS_ASPNETCORE_RECYCLE_ON_FILE_CHANGE_FILE L"file" +#define CS_ASPNETCORE_RECYCLE_ON_FILE_CHANGE_FILE_PATH L"path" + +#define MAX_RAPID_FAILS_PER_MINUTE 100 +#define MILLISECONDS_IN_ONE_SECOND 1000 +#define MIN_PORT 1025 +#define MAX_PORT 48000 + +#define HEX_TO_ASCII(c) ((CHAR)(((c) < 10) ? ((c) + '0') : ((c) + 'a' - 10))) + +extern HTTP_MODULE_ID g_pModuleId; +extern IHttpServer * g_pHttpServer; + +class ASPNETCORE_CONFIG : IHttpStoredContext +{ +public: + + virtual + ~ASPNETCORE_CONFIG(); + + VOID + CleanupStoredContext() + { + delete this; + } + + static + HRESULT + GetConfig( + _In_ IHttpContext *pHttpContext, + _Out_ ASPNETCORE_CONFIG **ppAspNetCoreConfig + ); + + ENVIRONMENT_VAR_HASH* + QueryEnvironmentVariables( + VOID + ) + { + return m_pEnvironmentVariables; + } + + DWORD + QueryRapidFailsPerMinute( + VOID + ) + { + return m_dwRapidFailsPerMinute; + } + + DWORD + QueryStartupTimeLimitInMS( + VOID + ) + { + return m_dwStartupTimeLimitInMS; + } + + DWORD + QueryShutdownTimeLimitInMS( + VOID + ) + { + return m_dwShutdownTimeLimitInMS; + } + + DWORD + QueryProcessesPerApplication( + VOID + ) + { + return m_dwProcessesPerApplication; + } + + DWORD + QueryRequestTimeoutInMS( + VOID + ) + { + return m_dwRequestTimeoutInMS; + } + + STRU* + QueryArguments( + VOID + ) + { + return &m_struArguments; + } + + STRU* + QueryApplicationPath( + VOID + ) + { + return &m_struApplication; + } + + STRU* + QueryProcessPath( + VOID + ) + { + return &m_struProcessPath; + } + + BOOL + QueryStdoutLogEnabled() + { + return m_fStdoutLogEnabled; + } + + BOOL + QueryForwardWindowsAuthToken() + { + return m_fForwardWindowsAuthToken; + } + + BOOL + QueryWindowsAuthEnabled() + { + return m_fWindowsAuthEnabled; + } + + BOOL + QueryBasicAuthEnabled() + { + return m_fBasicAuthEnabled; + } + + BOOL + QueryAnonymousAuthEnabled() + { + return m_fAnonymousAuthEnabled; + } + + BOOL + QueryDisableStartUpErrorPage() + { + return m_fDisableStartUpErrorPage; + } + + STRU* + QueryStdoutLogFile() + { + return &m_struStdoutLogFile; + } + +private: + + // + // private constructor + // + ASPNETCORE_CONFIG(): + m_fStdoutLogEnabled( FALSE ), + m_pEnvironmentVariables( NULL ) + { + } + + HRESULT + Populate( + IHttpContext *pHttpContext + ); + + DWORD m_dwRequestTimeoutInMS; + DWORD m_dwStartupTimeLimitInMS; + DWORD m_dwShutdownTimeLimitInMS; + DWORD m_dwRapidFailsPerMinute; + DWORD m_dwProcessesPerApplication; + STRU m_struApplication; + STRU m_struArguments; + STRU m_struProcessPath; + STRU m_struStdoutLogFile; + BOOL m_fStdoutLogEnabled; + BOOL m_fForwardWindowsAuthToken; + BOOL m_fDisableStartUpErrorPage; + BOOL m_fWindowsAuthEnabled; + BOOL m_fBasicAuthEnabled; + BOOL m_fAnonymousAuthEnabled; + ENVIRONMENT_VAR_HASH* m_pEnvironmentVariables; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/debugutil.h b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/debugutil.h new file mode 100644 index 0000000000..7378462efb --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/debugutil.h @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#define ASPNETCORE_DEBUG_FLAG_INFO 0x00000001 +#define ASPNETCORE_DEBUG_FLAG_WARNING 0x00000002 +#define ASPNETCORE_DEBUG_FLAG_ERROR 0x00000004 + +extern DWORD g_dwAspNetCoreDebugFlags; + +static +BOOL +IfDebug( + DWORD dwFlag + ) +{ + return ( dwFlag & g_dwAspNetCoreDebugFlags ); +} + +static +VOID +DebugPrint( + DWORD dwFlag, + LPCSTR szString + ) +{ + STACK_STRA (strOutput, 256); + HRESULT hr = S_OK; + + if ( IfDebug( dwFlag ) ) + { + hr = strOutput.SafeSnprintf( + "[aspnetcore.dll] %s\r\n", + szString ); + + if (FAILED (hr)) + { + goto Finished; + } + + OutputDebugStringA( strOutput.QueryStr() ); + } + +Finished: + + return; +} + +static +VOID +DebugPrintf( +DWORD dwFlag, +LPCSTR szFormat, +... +) +{ + STACK_STRA (strCooked,256); + + va_list args; + HRESULT hr = S_OK; + + if ( IfDebug( dwFlag ) ) + { + va_start( args, szFormat ); + + hr = strCooked.SafeVsnprintf(szFormat, args ); + + va_end( args ); + + if (FAILED (hr)) + { + goto Finished; + } + + DebugPrint( dwFlag, strCooked.QueryStr() ); + } + +Finished: + return; +} + diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/environmentvariablehash.h b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/environmentvariablehash.h new file mode 100644 index 0000000000..062090ac17 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/environmentvariablehash.h @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +// +// The key used for hash-table lookups, consists of the port on which the http process is created. +// + +class ENVIRONMENT_VAR_ENTRY +{ +public: + ENVIRONMENT_VAR_ENTRY(): + _cRefs(1) + { + } + + HRESULT + Initialize( + PCWSTR pszName, + PCWSTR pszValue + ) + { + HRESULT hr = S_OK; + if (FAILED(hr = _strName.Copy(pszName)) || + FAILED(hr = _strValue.Copy(pszValue))) + { + } + return hr; + } + + VOID + Reference() const + { + InterlockedIncrement(&_cRefs); + } + + VOID + Dereference() const + { + if (InterlockedDecrement(&_cRefs) == 0) + { + delete this; + } + } + + PWSTR const + QueryName() + { + return _strName.QueryStr(); + } + + PWSTR const + QueryValue() + { + return _strValue.QueryStr(); + } + +private: + ~ENVIRONMENT_VAR_ENTRY() + { + } + + STRU _strName; + STRU _strValue; + mutable LONG _cRefs; +}; + +class ENVIRONMENT_VAR_HASH : public HASH_TABLE +{ +public: + ENVIRONMENT_VAR_HASH() + {} + + PWSTR + ExtractKey( + ENVIRONMENT_VAR_ENTRY * pEntry + ) + { + return pEntry->QueryName(); + } + + DWORD + CalcKeyHash( + PWSTR pszName + ) + { + return HashStringNoCase(pszName); + } + + BOOL + EqualKeys( + PWSTR pszName1, + PWSTR pszName2 + ) + { + return (_wcsicmp(pszName1, pszName2) == 0); + } + + VOID + ReferenceRecord( + ENVIRONMENT_VAR_ENTRY * pEntry + ) + { + pEntry->Reference(); + } + + VOID + DereferenceRecord( + ENVIRONMENT_VAR_ENTRY * pEntry + ) + { + pEntry->Dereference(); + } + + static + VOID + CopyToMultiSz( + ENVIRONMENT_VAR_ENTRY * pEntry, + PVOID pvData + ) + { + STRU strTemp; + MULTISZ *pMultiSz = static_cast(pvData); + DBG_ASSERT(pMultiSz); + DBG_ASSERT(pEntry); + strTemp.Copy(pEntry->QueryName()); + strTemp.Append(pEntry->QueryValue()); + pMultiSz->Append(strTemp.QueryStr()); + } + + static + VOID + CopyToTable( + ENVIRONMENT_VAR_ENTRY * pEntry, + PVOID pvData + ) + { + // best effort copy, ignore the failure + ENVIRONMENT_VAR_ENTRY * pNewEntry = new ENVIRONMENT_VAR_ENTRY(); + if (pNewEntry != NULL) + { + pNewEntry->Initialize(pEntry->QueryName(), pEntry->QueryValue()); + ENVIRONMENT_VAR_HASH *pHash = static_cast(pvData); + DBG_ASSERT(pHash); + pHash->InsertRecord(pNewEntry); + // Need to dereference as InsertRecord references it now + pNewEntry->Dereference(); + } + } + +private: + ENVIRONMENT_VAR_HASH(const ENVIRONMENT_VAR_HASH &); + void operator=(const ENVIRONMENT_VAR_HASH &); +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/filewatcher.h b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/filewatcher.h new file mode 100644 index 0000000000..16d3942a2f --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/filewatcher.h @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#define FILE_WATCHER_SHUTDOWN_KEY (ULONG_PTR)(-1) +#define FILE_WATCHER_ENTRY_BUFFER_SIZE 4096 +#ifndef CONTAINING_RECORD +// +// Calculate the address of the base of the structure given its type, and an +// address of a field within the structure. +// + +#define CONTAINING_RECORD(address, type, field) \ + ((type *)((PCHAR)(address)-(ULONG_PTR)(&((type *)0)->field))) + +#endif // !CONTAINING_RECORD +#define FILE_NOTIFY_VALID_MASK 0x00000fff +#define FILE_WATCHER_ENTRY_SIGNATURE ((DWORD) 'FWES') +#define FILE_WATCHER_ENTRY_SIGNATURE_FREE ((DWORD) 'sewf') + +class APPLICATION; + +class FILE_WATCHER{ +public: + + FILE_WATCHER(); + + ~FILE_WATCHER(); + + HRESULT Create(); + + HANDLE + QueryCompletionPort( + VOID + ) const + { + return m_hCompletionPort; + } + + static + DWORD + WINAPI ChangeNotificationThread(LPVOID); + + static + void + WINAPI FileWatcherCompletionRoutine + ( + DWORD dwCompletionStatus, + DWORD cbCompletion, + OVERLAPPED * pOverlapped + ); + +private: + HANDLE m_hCompletionPort; + HANDLE m_hChangeNotificationThread; +}; + +class FILE_WATCHER_ENTRY +{ +public: + FILE_WATCHER_ENTRY(FILE_WATCHER * pFileMonitor); + + OVERLAPPED _overlapped; + + HRESULT + Create( + _In_ PCWSTR pszDirectoryToMonitor, + _In_ PCWSTR pszFileNameToMonitor, + _In_ APPLICATION* pApplication, + _In_ HANDLE hImpersonationToken + ); + + VOID + ReferenceFileWatcherEntry() const + { + InterlockedIncrement(&_cRefs); + } + + VOID + DereferenceFileWatcherEntry() const + { + if (InterlockedDecrement(&_cRefs) == 0) + { + delete this; + } + } + + BOOL + QueryIsValid() const + { + return _fIsValid; + } + + VOID + MarkEntryInValid() + { + _fIsValid = FALSE; + } + + HRESULT Monitor(); + + VOID StopMonitor(); + + HRESULT + HandleChangeCompletion( + _In_ DWORD dwCompletionStatus, + _In_ DWORD cbCompletion + ); + +private: + virtual ~FILE_WATCHER_ENTRY(); + + DWORD _dwSignature; + BUFFER _buffDirectoryChanges; + HANDLE _hImpersonationToken; + HANDLE _hDirectory; + FILE_WATCHER* _pFileMonitor; + APPLICATION* _pApplication; + STRU _strFileName; + STRU _strDirectoryName; + LONG _lStopMonitorCalled; + mutable LONG _cRefs; + BOOL _fIsValid; + SRWLOCK _srwLock; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/forwarderconnection.h b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/forwarderconnection.h new file mode 100644 index 0000000000..a3f5dfdabe --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/forwarderconnection.h @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +// +// The key used for hash-table lookups, consists of the port on which the http process is created. +// +class FORWARDER_CONNECTION_KEY +{ +public: + + FORWARDER_CONNECTION_KEY( + VOID + ) + { + } + + HRESULT + Initialize( + _In_ DWORD dwPort + ) + { + m_dwPort = dwPort; + return S_OK; + } + + BOOL + GetIsEqual( + const FORWARDER_CONNECTION_KEY * key2 + ) const + { + return m_dwPort == key2->m_dwPort; + } + + DWORD CalcKeyHash() const + { + // TODO: Review hash distribution. + return Hash(m_dwPort); + } + +private: + + DWORD m_dwPort; +}; + +class FORWARDER_CONNECTION +{ +public: + + FORWARDER_CONNECTION( + VOID + ); + + HRESULT + Initialize( + DWORD dwPort + ); + + HINTERNET + QueryHandle() const + { + return m_hConnection; + } + + VOID + ReferenceForwarderConnection() const + { + InterlockedIncrement(&m_cRefs); + } + + VOID + DereferenceForwarderConnection() const + { + if (InterlockedDecrement(&m_cRefs) == 0) + { + delete this; + } + } + + FORWARDER_CONNECTION_KEY * + QueryConnectionKey() + { + return &m_ConnectionKey; + } + +private: + + ~FORWARDER_CONNECTION() + { + if (m_hConnection != NULL) + { + WinHttpCloseHandle(m_hConnection); + m_hConnection = NULL; + } + } + + mutable LONG m_cRefs; + FORWARDER_CONNECTION_KEY m_ConnectionKey; + HINTERNET m_hConnection; +}; + +class FORWARDER_CONNECTION_HASH : + public HASH_TABLE +{ + +public: + + FORWARDER_CONNECTION_HASH() + {} + + FORWARDER_CONNECTION_KEY * + ExtractKey( + FORWARDER_CONNECTION *pConnection + ) + { + return pConnection->QueryConnectionKey(); + } + + DWORD + CalcKeyHash( + FORWARDER_CONNECTION_KEY *key + ) + { + return key->CalcKeyHash(); + } + + BOOL + EqualKeys( + FORWARDER_CONNECTION_KEY *key1, + FORWARDER_CONNECTION_KEY *key2 + ) + { + return key1->GetIsEqual(key2); + } + + VOID + ReferenceRecord( + FORWARDER_CONNECTION *pConnection + ) + { + pConnection->ReferenceForwarderConnection(); + } + + VOID + DereferenceRecord( + FORWARDER_CONNECTION *pConnection + ) + { + pConnection->DereferenceForwarderConnection(); + } + +private: + + FORWARDER_CONNECTION_HASH(const FORWARDER_CONNECTION_HASH &); + void operator=(const FORWARDER_CONNECTION_HASH &); +}; \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/forwardinghandler.h b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/forwardinghandler.h new file mode 100644 index 0000000000..dcd8531dac --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/forwardinghandler.h @@ -0,0 +1,468 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "forwarderconnection.h" +#include "protocolconfig.h" +#include "serverprocess.h" +#include "application.h" +#include "tracelog.h" +#include "websockethandler.h" + +#define ASPNETCORE_DEBUG_STRU_BUFFER_SIZE 100 +#define ASPNETCORE_DEBUG_STRU_ARRAY_SIZE 100 + +enum FORWARDING_REQUEST_STATUS +{ + FORWARDER_START, + FORWARDER_SENDING_REQUEST, + FORWARDER_RECEIVING_RESPONSE, + FORWARDER_RECEIVED_WEBSOCKET_RESPONSE, + FORWARDER_RESET_CONNECTION, + FORWARDER_DONE +}; + +extern HTTP_MODULE_ID g_pModuleId; +extern IHttpServer * g_pHttpServer; +extern BOOL g_fAsyncDisconnectAvailable; +extern PCWSTR g_pszModuleName; +extern HMODULE g_hModule; +extern HMODULE g_hWinHttpModule; +extern DWORD g_dwTlsIndex; +extern DWORD g_OptionalWinHttpFlags; + +#ifdef DEBUG +extern STRA g_strLogs[ASPNETCORE_DEBUG_STRU_ARRAY_SIZE]; +extern DWORD g_dwLogCounter; +#endif // DEBUG + +enum MULTI_PART_POSITION +{ + MULTI_PART_IN_BOUNDARY, + MULTI_PART_IN_HEADER, + MULTI_PART_IN_CHUNK, + MULTI_PART_IN_CHUNK_END +}; + +class ASYNC_DISCONNECT_CONTEXT; + +#define FORWARDING_HANDLER_SIGNATURE ((DWORD)'FHLR') +#define FORWARDING_HANDLER_SIGNATURE_FREE ((DWORD)'fhlr') + +class FORWARDING_HANDLER +{ +public: + + FORWARDING_HANDLER( + __in IHttpContext * pW3Context + ); + + static void * operator new(size_t size); + + static void operator delete(void * pMemory); + + VOID + ReferenceForwardingHandler( + VOID + ) const; + + VOID + DereferenceForwardingHandler( + VOID + ) const; + + REQUEST_NOTIFICATION_STATUS + OnExecuteRequestHandler(); + + REQUEST_NOTIFICATION_STATUS + OnAsyncCompletion( + DWORD cbCompletion, + HRESULT hrCompletionStatus + ); + + IHttpTraceContext * + QueryTraceContext() + { + return m_pW3Context->GetTraceContext(); + } + + IHttpContext * + QueryHttpContext( + VOID + ) + { + return m_pW3Context; + } + + static + VOID + CALLBACK + OnWinHttpCompletion( + HINTERNET hRequest, + DWORD_PTR dwContext, + DWORD dwInternetStatus, + LPVOID lpvStatusInformation, + DWORD dwStatusInformationLength + ) + { + + FORWARDING_HANDLER * pThis = static_cast(reinterpret_cast(dwContext)); + if (pThis == NULL) + { + //error happened, nothing can be done here + return; + } + DBG_ASSERT(pThis->m_Signature == FORWARDING_HANDLER_SIGNATURE); + pThis->OnWinHttpCompletionInternal(hRequest, + dwInternetStatus, + lpvStatusInformation, + dwStatusInformationLength); + } + + static + HRESULT + StaticInitialize( + BOOL fEnableReferenceCountTracing + ); + + static + VOID + StaticTerminate(); + + static + PCWSTR + QueryErrorFormat() + { + return sm_strErrorFormat.QueryStr(); + } + + static + HANDLE + QueryEventLog() + { + return sm_hEventLog; + } + + VOID + TerminateRequest( + BOOL fClientInitiated + ); + + static HINTERNET sm_hSession; + + HRESULT + SetStatusAndHeaders( + PCSTR pszHeaders, + DWORD cchHeaders + ); + + HRESULT + OnSharedRequestEntity( + ULONGLONG ulOffset, + LPCBYTE pvBuffer, + DWORD cbBuffer + ); + + VOID + SetStatus( + FORWARDING_REQUEST_STATUS status + ) + { + m_RequestStatus = status; + } + + virtual + ~FORWARDING_HANDLER( + VOID + ); + +private: + + // + // Begin OnMapRequestHandler phases. + // + + HRESULT + CreateWinHttpRequest( + __in const IHttpRequest * pRequest, + __in const PROTOCOL_CONFIG * pProtocol, + __in HINTERNET hConnect, + __inout STRU * pstrUrl, + ASPNETCORE_CONFIG* pAspNetCoreConfig, + SERVER_PROCESS* pServerProcess + ); + + // + // End OnMapRequestHandler phases. + // + + VOID + RemoveRequest(); + + HRESULT + GetHeaders( + const PROTOCOL_CONFIG * pProtocol, + PCWSTR * ppszHeaders, + DWORD * pcchHeaders, + ASPNETCORE_CONFIG* pAspNetCoreConfig, + SERVER_PROCESS* pServerProcess + ); + + HRESULT + DoReverseRewrite( + __in IHttpResponse *pResponse + ); + + BYTE * + GetNewResponseBuffer( + DWORD dwBufferSize + ); + + VOID + FreeResponseBuffers(); + + VOID + OnWinHttpCompletionInternal( + HINTERNET hRequest, + DWORD dwInternetStatus, + LPVOID lpvStatusInformation, + DWORD dwStatusInformationLength + ); + + HRESULT + OnWinHttpCompletionSendRequestOrWriteComplete( + HINTERNET hRequest, + DWORD dwInternetStatus, + __out BOOL * pfClientError, + __out BOOL * pfAnotherCompletionExpected + ); + + HRESULT + OnWinHttpCompletionStatusHeadersAvailable( + HINTERNET hRequest, + __out BOOL * pfAnotherCompletionExpected + ); + + HRESULT + OnWinHttpCompletionStatusDataAvailable( + HINTERNET hRequest, + DWORD dwBytes, + __out BOOL * pfAnotherCompletionExpected + ); + + HRESULT + OnWinHttpCompletionStatusReadComplete( + __in IHttpResponse * pResponse, + DWORD dwStatusInformationLength, + __out BOOL * pfAnotherCompletionExpected + ); + + HRESULT + OnSendingRequest( + DWORD cbCompletion, + HRESULT hrCompletionStatus, + __out BOOL * pfClientError + ); + + HRESULT + OnReceivingResponse(); + + HRESULT + OnWebSocketWinHttpSendComplete( + HINTERNET hRequest, + LPVOID pvStatus, + DWORD hrCompletion, + DWORD cbCompletion, + BOOL * pfAnotherCompletionExpected + ); + + HRESULT + OnWebSocketWinHttpReceiveComplete( + HINTERNET hRequest, + LPVOID pvStatus, + DWORD hrCompletion, + DWORD cbCompletion, + BOOL * pfAnotherCompletionExpected + ); + + HRESULT + OnWebSocketIisSendComplete( + DWORD hrCompletion, + DWORD cbCompletion + ); + + HRESULT + OnWebSocketIisReceiveComplete( + DWORD hrCompletion, + DWORD cbCompletion + ); + + HRESULT + DoIisWebSocketReceive( + VOID + ); + + VOID + TerminateWebsocket( + VOID + ); + + DWORD m_Signature; + mutable LONG m_cRefs; + + IHttpContext * m_pW3Context; + + // + // WinHTTP request handle is protected using a read-write lock. + // + SRWLOCK m_RequestLock; + HINTERNET m_hRequest; + + APP_OFFLINE_HTM *m_pAppOfflineHtm; + APPLICATION *m_pApplication; + + BOOL m_fResponseHeadersReceivedAndSet; + volatile BOOL m_fClientDisconnected; + // + // A safety guard flag indicating no more IIS PostCompletion is allowed + // + volatile BOOL m_fFinishRequest; + // + // A safety guard flag to prevent from unexpect callback which may signal IIS pipeline + // more than once with non-pending status + // + volatile BOOL m_fDoneAsyncCompletion; + volatile BOOL m_fHasError; + // + // WinHttp may hit AV under race if handle got closed more than once simultaneously + // Use two bool variables to guard + // + volatile BOOL m_fHttpHandleInClose; + volatile BOOL m_fWebSocketHandleInClose; + // + // Record the number of winhttp handles in use + // release IIS pipeline only after all handles got closed + // + volatile LONG m_dwHandlers; + + BOOL m_fDoReverseRewriteHeaders; + BOOL m_fServerResetConn; + DWORD m_msStartTime; + DWORD m_BytesToReceive; + DWORD m_BytesToSend; + + BYTE * m_pEntityBuffer; + DWORD m_cchLastSend; + + static const SIZE_T INLINE_ENTITY_BUFFERS = 8; + DWORD m_cEntityBuffers; + BUFFER_T m_buffEntityBuffers; + + DWORD m_cBytesBuffered; + DWORD m_cMinBufferLimit; + + PCSTR m_pszOriginalHostHeader; + + volatile FORWARDING_REQUEST_STATUS m_RequestStatus; + + ASYNC_DISCONNECT_CONTEXT * m_pDisconnect; + + PCWSTR m_pszHeaders; + DWORD m_cchHeaders; + + BOOL m_fWebSocketEnabled; + + STRU m_strFullUri; + + ULONGLONG m_cContentLength; + + WEBSOCKET_HANDLER * m_pWebSocket; + + static PROTOCOL_CONFIG sm_ProtocolConfig; + + static STRU sm_strErrorFormat; + + static HANDLE sm_hEventLog; + + static ALLOC_CACHE_HANDLER * sm_pAlloc; + + // + // Reference cout tracing for debugging purposes. + // + static TRACE_LOG * sm_pTraceLog; +}; + +class ASYNC_DISCONNECT_CONTEXT : public IHttpConnectionStoredContext +{ + public: + ASYNC_DISCONNECT_CONTEXT() + { + m_pHandler = NULL; + } + + VOID + CleanupStoredContext() + { + DBG_ASSERT(m_pHandler == NULL); + delete this; + } + + VOID + NotifyDisconnect() + { + FORWARDING_HANDLER *pInitialValue = (FORWARDING_HANDLER*) + InterlockedExchangePointer((PVOID*) &m_pHandler, NULL); + + if (pInitialValue != NULL) + { + pInitialValue->TerminateRequest(TRUE); + pInitialValue->DereferenceForwardingHandler(); + } + } + + VOID + SetHandler( + FORWARDING_HANDLER *pHandler + ) + { + // + // Take a reference on the forwarding handler. + // This reference will be released on either of two conditions: + // + // 1. When the request processing ends, in which case a ResetHandler() + // is called. + // + // 2. When a disconnect notification arrives. + // + // We need to make sure that only one of them ends up dereferencing + // the object. + // + + DBG_ASSERT (pHandler != NULL); + DBG_ASSERT (m_pHandler == NULL); + + pHandler->ReferenceForwardingHandler(); + InterlockedExchangePointer((PVOID*)&m_pHandler, pHandler); + } + + VOID + ResetHandler( + VOID + ) + { + FORWARDING_HANDLER *pInitialValue = (FORWARDING_HANDLER*) + InterlockedExchangePointer( (PVOID*)&m_pHandler, NULL); + + if (pInitialValue != NULL) + { + pInitialValue->DereferenceForwardingHandler(); + } + } + + private: + ~ASYNC_DISCONNECT_CONTEXT() + {} + + FORWARDING_HANDLER * m_pHandler; +}; \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/path.h b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/path.h new file mode 100644 index 0000000000..05545acfd5 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/path.h @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +class PATH +{ +public: + + static + HRESULT + SplitUrl( + PCWSTR pszDestinationUrl, + BOOL *pfSecure, + STRU *pstrDestination, + STRU *pstrUrl + ); + + static + HRESULT + UnEscapeUrl( + PCWSTR pszUrl, + DWORD cchUrl, + bool fCopyQuery, + STRA * pstrResult + ); + + static + HRESULT + UnEscapeUrl( + PCWSTR pszUrl, + DWORD cchUrl, + STRU * pstrResult + ); + + static HRESULT + EscapeAbsPath( + IHttpRequest * pRequest, + STRU * strEscapedUrl + ); + + static + bool + IsValidAttributeNameChar( + WCHAR ch + ); + + static + bool + IsValidQueryStringName( + PCWSTR pszName + ); + + static + bool + IsValidHeaderName( + PCWSTR pszName + ); + + static + bool + FindInMultiString( + PCWSTR pszMultiString, + PCWSTR pszStringToFind + ); + + static + HRESULT + IsPathUnc( + __in LPCWSTR pszPath, + __out BOOL * pfIsUnc + ); + + static + HRESULT + ConvertPathToFullPath( + _In_ LPCWSTR pszPath, + _In_ LPCWSTR pszRootPath, + _Out_ STRU* pStrFullPath + ); + +private: + + PATH() {} + ~PATH() {} + + static + CHAR + ToHexDigit( + UINT nDigit + ) + { + return static_cast(nDigit > 9 ? nDigit - 10 + 'A' : nDigit + '0'); + } +}; \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/processmanager.h b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/processmanager.h new file mode 100644 index 0000000000..b91e8e6bfb --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/processmanager.h @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#define ONE_MINUTE_IN_MILLISECONDS 60000 + +class PROCESS_MANAGER +{ +public: + + virtual + ~PROCESS_MANAGER(); + + VOID + ReferenceProcessManager() const + { + InterlockedIncrement(&m_cRefs); + } + + VOID + DereferenceProcessManager() const + { + if (InterlockedDecrement(&m_cRefs) == 0) + { + delete this; + } + } + + HRESULT + GetProcess( + _In_ IHttpContext *context, + _In_ ASPNETCORE_CONFIG *pConfig, + _Out_ SERVER_PROCESS **ppServerProcess + ); + + HANDLE + QueryNULHandle() + { + return m_hNULHandle; + } + + HRESULT + Initialize( + VOID + ); + + VOID + SendShutdownSignal() + { + AcquireSRWLockExclusive( &m_srwLock ); + + for(DWORD i = 0; i < m_dwProcessesPerApplication; ++i ) + { + if( m_ppServerProcessList != NULL && + m_ppServerProcessList[i] != NULL ) + { + m_ppServerProcessList[i]->SendSignal(); + m_ppServerProcessList[i]->DereferenceServerProcess(); + m_ppServerProcessList[i] = NULL; + } + } + + ReleaseSRWLockExclusive( &m_srwLock ); + } + + VOID + ShutdownProcess( + SERVER_PROCESS* pServerProcess + ) + { + AcquireSRWLockExclusive( &m_srwLock ); + + ShutdownProcessNoLock( pServerProcess ); + + ReleaseSRWLockExclusive( &m_srwLock ); + } + + VOID + ShutdownAllProcesses( + ) + { + AcquireSRWLockExclusive( &m_srwLock ); + + ShutdownAllProcessesNoLock(); + + ReleaseSRWLockExclusive( &m_srwLock ); + } + + VOID + IncrementRapidFailCount( + VOID + ) + { + InterlockedIncrement(&m_cRapidFailCount); + } + + PROCESS_MANAGER() : + m_ppServerProcessList( NULL ), + m_hNULHandle( NULL ), + m_cRapidFailCount( 0 ), + m_dwProcessesPerApplication( 1 ), + m_dwRouteToProcessIndex( 0 ), + m_fServerProcessListReady(FALSE), + m_cRefs( 1 ) + { + InitializeSRWLock( &m_srwLock ); + } + +private: + + BOOL + RapidFailsPerMinuteExceeded( + LONG dwRapidFailsPerMinute + ) + { + DWORD dwCurrentTickCount = GetTickCount(); + + if( (dwCurrentTickCount - m_dwRapidFailTickStart) + >= ONE_MINUTE_IN_MILLISECONDS ) + { + // + // reset counters every minute. + // + + InterlockedExchange(&m_cRapidFailCount, 0); + m_dwRapidFailTickStart = dwCurrentTickCount; + } + + return m_cRapidFailCount > dwRapidFailsPerMinute; + } + + VOID + ShutdownProcessNoLock( + SERVER_PROCESS* pServerProcess + ) + { + for(DWORD i = 0; i < m_dwProcessesPerApplication; ++i ) + { + if( m_ppServerProcessList != NULL && + m_ppServerProcessList[i] != NULL && + m_ppServerProcessList[i]->GetPort() == pServerProcess->GetPort() ) + { + // shutdown pServerProcess if not already shutdown. + m_ppServerProcessList[i]->StopProcess(); + m_ppServerProcessList[i]->DereferenceServerProcess(); + m_ppServerProcessList[i] = NULL; + } + } + } + + VOID + ShutdownAllProcessesNoLock( + VOID + ) + { + for(DWORD i = 0; i < m_dwProcessesPerApplication; ++i ) + { + if( m_ppServerProcessList != NULL && + m_ppServerProcessList[i] != NULL ) + { + // shutdown pServerProcess if not already shutdown. + m_ppServerProcessList[i]->SendSignal(); + m_ppServerProcessList[i]->DereferenceServerProcess(); + m_ppServerProcessList[i] = NULL; + } + } + } + + volatile LONG m_cRapidFailCount; + DWORD m_dwRapidFailTickStart; + DWORD m_dwProcessesPerApplication; + volatile DWORD m_dwRouteToProcessIndex; + + SRWLOCK m_srwLock; + SERVER_PROCESS **m_ppServerProcessList; + + // + // m_hNULHandle is used to redirect stdout/stderr to NUL. + // If Createprocess is called to launch a batch file for example, + // it tries to write to the console buffer by default. It fails to + // start if the console buffer is owned by the parent process i.e + // in our case w3wp.exe. So we have to redirect the stdout/stderr + // of the child process to NUL or to a file (anything other than + // the console buffer of the parent process). + // + + HANDLE m_hNULHandle; + mutable LONG m_cRefs; + + volatile static BOOL sm_fWSAStartupDone; + volatile BOOL m_fServerProcessListReady; +}; \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/protocolconfig.h b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/protocolconfig.h new file mode 100644 index 0000000000..d9d730c544 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/protocolconfig.h @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "aspnetcoreconfig.h" + +class PROTOCOL_CONFIG +{ + public: + + PROTOCOL_CONFIG() + { + } + + HRESULT + Initialize(); + + VOID + OverrideConfig( + ASPNETCORE_CONFIG *pAspNetCoreConfig + ); + + BOOL + QueryDoKeepAlive() const + { + return m_fKeepAlive; + } + + DWORD + QueryTimeout() const + { + return m_msTimeout; + } + + BOOL + QueryPreserveHostHeader() const + { + return m_fPreserveHostHeader; + } + + BOOL + QueryReverseRewriteHeaders() const + { + return m_fReverseRewriteHeaders; + } + + const STRA * + QueryXForwardedForName() const + { + return &m_strXForwardedForName; + } + + BOOL + QueryIncludePortInXForwardedFor() const + { + return m_fIncludePortInXForwardedFor; + } + + DWORD + QueryMinResponseBuffer() const + { + return m_dwMinResponseBuffer; + } + + DWORD + QueryResponseBufferLimit() const + { + return m_dwResponseBufferLimit; + } + + DWORD + QueryMaxResponseHeaderSize() const + { + return m_dwMaxResponseHeaderSize; + } + + const STRA* + QuerySslHeaderName() const + { + return &m_strSslHeaderName; + } + + const STRA * + QueryClientCertName() const + { + return &m_strClientCertName; + } + + private: + + BOOL m_fKeepAlive; + BOOL m_fPreserveHostHeader; + BOOL m_fReverseRewriteHeaders; + BOOL m_fIncludePortInXForwardedFor; + + DWORD m_msTimeout; + DWORD m_dwMinResponseBuffer; + DWORD m_dwResponseBufferLimit; + DWORD m_dwMaxResponseHeaderSize; + + STRA m_strXForwardedForName; + STRA m_strSslHeaderName; + STRA m_strClientCertName; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/proxymodule.h b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/proxymodule.h new file mode 100644 index 0000000000..1adbcffae8 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/proxymodule.h @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "forwardinghandler.h" + +class CProxyModule : public CHttpModule +{ +public: + + CProxyModule(); + + ~CProxyModule(); + + void * operator new(size_t size, IModuleAllocator * pPlacement) + { + return pPlacement->AllocateMemory(static_cast(size)); + } + + VOID + operator delete( + void * + ) + { + } + + __override + REQUEST_NOTIFICATION_STATUS + OnExecuteRequestHandler( + IHttpContext * pHttpContext, + IHttpEventProvider * pProvider + ); + + __override + REQUEST_NOTIFICATION_STATUS + OnAsyncCompletion( + IHttpContext * pHttpContext, + DWORD dwNotification, + BOOL fPostNotification, + IHttpEventProvider * pProvider, + IHttpCompletionInfo * pCompletionInfo + ); + +private: + + FORWARDING_HANDLER * m_pHandler; +}; + +class CProxyModuleFactory : public IHttpModuleFactory +{ +public: + HRESULT + GetHttpModule( + CHttpModule ** ppModule, + IModuleAllocator * pAllocator + ); + + VOID + Terminate(); +}; \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/resource.h b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/resource.h new file mode 100644 index 0000000000..6330be8879 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/resource.h @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#define IDS_INVALID_PROPERTY 1000 +#define IDS_SERVER_ERROR 1001 + +#define ASPNETCORE_EVENT_MSG_BUFFER_SIZE 256 +#define ASPNETCORE_EVENT_PROCESS_START_SUCCESS_MSG L"Application '%s' started process '%d' successfully and is listening on port '%d'." +#define ASPNETCORE_EVENT_RAPID_FAIL_COUNT_EXCEEDED_MSG L"Maximum rapid fail count per minute of '%d' exceeded." +#define ASPNETCORE_EVENT_PROCESS_START_INTERNAL_ERROR_MSG L"Application '%s' failed to parse processPath and arguments due to internal error, ErrorCode = '0x%x'." +#define ASPNETCORE_EVENT_PROCESS_START_POSTCREATE_ERROR_MSG L"Application '%s' with physical root '%s' created process with commandline '%s'but failed to get its status, ErrorCode = '0x%x'." +#define ASPNETCORE_EVENT_PROCESS_START_ERROR_MSG L"Application '%s' with physical root '%s' failed to start process with commandline '%s', ErrorCode = '0x%x' : %x." +#define ASPNETCORE_EVENT_PROCESS_START_WRONGPORT_ERROR_MSG L"Application '%s' with physical root '%s' created process with commandline '%s' but failed to listen on the given port '%d'" +#define ASPNETCORE_EVENT_PROCESS_START_NOTREADY_ERROR_MSG L"Application '%s' with physical root '%s' created process with commandline '%s' but either crashed or did not reponse or did not listen on the given port '%d', ErrorCode = '0x%x'" +#define ASPNETCORE_EVENT_INVALID_STDOUT_LOG_FILE_MSG L"Warning: Could not create stdoutLogFile %s, ErrorCode = %d." +#define ASPNETCORE_EVENT_GRACEFUL_SHUTDOWN_FAILURE_MSG L"Failed to gracefully shutdown process '%d'." +#define ASPNETCORE_EVENT_SENT_SHUTDOWN_HTTP_REQUEST_MSG L"Sent shutdown HTTP message to process '%d' and received http status '%d'." +#define ASPNETCORE_EVENT_RECYCLE_APPOFFLINE_MSG L"App_offline file '%s' was detected." +#define ASPNETCORE_EVENT_PROCESS_SHUTDOWN_MSG L"Application '%s' with physical root '%s' shut down process with Id '%d' listening on port '%d'" diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/responseheaderhash.h b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/responseheaderhash.h new file mode 100644 index 0000000000..7ef127366b --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/responseheaderhash.h @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +// +// *_HEADER_HASH maps strings to UlHeader* values +// + +#define UNKNOWN_INDEX (0xFFFFFFFF) + +struct HEADER_RECORD +{ + PCSTR _pszName; + ULONG _ulHeaderIndex; +}; + +class RESPONSE_HEADER_HASH: public HASH_TABLE +{ +public: + RESPONSE_HEADER_HASH() + {} + + VOID + ReferenceRecord( + HEADER_RECORD * + ) + {} + + VOID + DereferenceRecord( + HEADER_RECORD * + ) + {} + + PCSTR + ExtractKey( + HEADER_RECORD * pRecord + ) + { + return pRecord->_pszName; + } + + DWORD + CalcKeyHash( + PCSTR key + ) + { + return HashStringNoCase(key); + } + + BOOL + EqualKeys( + PCSTR key1, + PCSTR key2 + ) + { + return (_stricmp(key1, key2) == 0); + } + + HRESULT + Initialize( + VOID + ); + + VOID + Terminate( + VOID + ); + + DWORD + GetIndex( + PCSTR pszName + ) + { + HEADER_RECORD * pRecord = NULL; + + FindKey(pszName, &pRecord); + if (pRecord != NULL) + { + return pRecord->_ulHeaderIndex; + } + + return UNKNOWN_INDEX; + } + + static + PCSTR + GetString( + ULONG ulIndex + ) + { + if (ulIndex < HttpHeaderResponseMaximum) + { + DBG_ASSERT(sm_rgHeaders[ulIndex]._ulHeaderIndex == ulIndex); + return sm_rgHeaders[ulIndex]._pszName; + } + + return NULL; + } + +private: + + static HEADER_RECORD sm_rgHeaders[]; + + RESPONSE_HEADER_HASH(const RESPONSE_HEADER_HASH &); + void operator=(const RESPONSE_HEADER_HASH &); +}; + +extern RESPONSE_HEADER_HASH * g_pResponseHeaderHash; \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/serverprocess.h b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/serverprocess.h new file mode 100644 index 0000000000..94b65bad54 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/serverprocess.h @@ -0,0 +1,338 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include + +#define MIN_PORT 1025 +#define MAX_PORT 48000 +#define MAX_RETRY 10 +#define MAX_ACTIVE_CHILD_PROCESSES 16 +#define LOCALHOST "127.0.0.1" +#define ASPNETCORE_PORT_STR L"ASPNETCORE_PORT" +#define ASPNETCORE_PORT_ENV_STR L"ASPNETCORE_PORT=" +#define ASPNETCORE_APP_PATH_ENV_STR L"ASPNETCORE_APPL_PATH=" +#define ASPNETCORE_APP_TOKEN_ENV_STR L"ASPNETCORE_TOKEN=" +#define ASPNETCORE_APP_PATH_ENV_STR L"ASPNETCORE_APPL_PATH=" +#define HOSTING_STARTUP_ASSEMBLIES_ENV_STR L"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES" +#define HOSTING_STARTUP_ASSEMBLIES_NAME L"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES=" +#define HOSTING_STARTUP_ASSEMBLIES_VALUE L"Microsoft.AspNetCore.Server.IISIntegration" +#define ASPNETCORE_IIS_AUTH_ENV_STR L"ASPNETCORE_IIS_HTTPAUTH=" +#define ASPNETCORE_IIS_AUTH_WINDOWS L"windows;" +#define ASPNETCORE_IIS_AUTH_BASIC L"basic;" +#define ASPNETCORE_IIS_AUTH_ANONYMOUS L"anonymous;" +#define ASPNETCORE_IIS_AUTH_NONE L"none" + +class PROCESS_MANAGER; +class FORWARDER_CONNECTION; + +class SERVER_PROCESS +{ +public: + SERVER_PROCESS(); + + HRESULT + Initialize( + _In_ PROCESS_MANAGER *pProcessManager, + _In_ STRU *pszProcessExePath, + _In_ STRU *pszArguments, + _In_ DWORD dwStartupTimeLimitInMS, + _In_ DWORD dwShtudownTimeLimitInMS, + _In_ BOOL fWindowsAuthEnabled, + _In_ BOOL fBasicAuthEnabled, + _In_ BOOL fAnonymousAuthEnabled, + _In_ ENVIRONMENT_VAR_HASH* pEnvironmentVariables, + _In_ BOOL fStdoutLogEnabled, + _In_ STRU *pstruStdoutLogFile + ); + + + HRESULT + StartProcess( + _In_ IHttpContext *context + ); + + HRESULT + SetWindowsAuthToken( + _In_ HANDLE hToken, + _Out_ LPHANDLE pTargeTokenHandle + ); + + BOOL + IsReady( + VOID + ) + { + return m_fReady; + } + + BOOL + IsDebuggerAttached( + VOID + ) + { + return m_fDebuggerAttached; + } + + VOID + StopProcess( + VOID + ); + + DWORD + GetPort() + { + return m_dwPort; + } + + VOID + ReferenceServerProcess( + VOID + ) + { + InterlockedIncrement(&m_cRefs); + } + + VOID + DereferenceServerProcess( + VOID + ) + { + _ASSERT(m_cRefs != 0 ); + + if (InterlockedDecrement(&m_cRefs) == 0) + { + delete this; + } + } + + virtual + ~SERVER_PROCESS(); + + HRESULT + HandleProcessExit( + VOID + ); + + FORWARDER_CONNECTION* + QueryWinHttpConnection( + VOID + ) + { + return m_pForwarderConnection; + } + + static + VOID + CALLBACK + TimerCallback( + _In_ PTP_CALLBACK_INSTANCE Instance, + _In_ PVOID Context, + _In_ PTP_TIMER Timer + ); + + LPCWSTR + QueryPortStr() + { + return m_struPort.QueryStr(); + } + + LPCWSTR + QueryFullLogPath() + { + return m_struFullLogFile.QueryStr(); + } + + LPCSTR + QueryGuid() + { + return m_straGuid.QueryStr(); + } + + DWORD + QueryProcessGroupId() + { + return m_dwProcessId; + } + + VOID + SendSignal( + VOID + ); + +private: + + BOOL + IsDebuggerIsAttached( + VOID + ); + + HRESULT + StopAllProcessesInJobObject( + VOID + ); + + HRESULT + SetupStdHandles( + _In_ IHttpContext *context, + _In_ LPSTARTUPINFOW pStartupInfo + ); + + HRESULT + CheckIfServerIsUp( + _In_ DWORD dwPort, + _Out_ DWORD * pdwProcessId, + _Out_ BOOL * pfReady + ); + + HRESULT + RegisterProcessWait( + _In_ PHANDLE phWaitHandle, + _In_ HANDLE hProcessToWaitOn + ); + + HRESULT + GetChildProcessHandles( + ); + + HRESULT + SetupListenPort( + ENVIRONMENT_VAR_HASH *pEnvironmentVarTable + ); + + HRESULT + SetupAppPath( + IHttpContext* pContext, + ENVIRONMENT_VAR_HASH* pEnvironmentVarTable + ); + + HRESULT + SetupAppToken( + ENVIRONMENT_VAR_HASH* pEnvironmentVarTable + ); + + HRESULT + InitEnvironmentVariablesTable( + ENVIRONMENT_VAR_HASH** pEnvironmentVarTable + ); + + HRESULT + OutputEnvironmentVariables( + MULTISZ* pmszOutput, + ENVIRONMENT_VAR_HASH* pEnvironmentVarTable + ); + + HRESULT + SetupCommandLine( + STRU* pstrCommandLine + ); + + HRESULT + PostStartCheck( + const STRU* const pStruCommandline, + STRU* pStruErrorMessage + ); + + HRESULT + GetRandomPort( + DWORD* pdwPickedPort, + DWORD dwExcludedPort + ); + + DWORD + GetNumberOfDigits( + _In_ DWORD dwNumber + ) + { + DWORD digits = 0; + + if( dwNumber == 0 ) + { + digits = 1; + goto Finished; + } + + while( dwNumber > 0) + { + dwNumber = dwNumber / 10; + digits ++; + } + Finished: + return digits; + } + + static + VOID + SendShutDownSignal( + LPVOID lpParam + ); + + VOID + SendShutDownSignalInternal( + VOID + ); + + HRESULT + SendShutdownHttpMessage( + VOID + ); + + VOID + TerminateBackendProcess( + VOID + ); + + FORWARDER_CONNECTION *m_pForwarderConnection; + BOOL m_fStdoutLogEnabled; + BOOL m_fWindowsAuthEnabled; + BOOL m_fBasicAuthEnabled; + BOOL m_fAnonymousAuthEnabled; + BOOL m_fDebuggerAttached; + + STTIMER m_Timer; + SOCKET m_socket; + + STRU m_struLogFile; + STRU m_struFullLogFile; + STRU m_ProcessPath; + STRU m_Arguments; + STRU m_struAppPath; + STRU m_struAppFullPath; + STRU m_struPort; + STRU m_pszRootApplicationPath; + volatile LONG m_lStopping; + volatile BOOL m_fReady; + mutable LONG m_cRefs; + + std::mt19937 m_randomGenerator; + + DWORD m_dwPort; + DWORD m_dwStartupTimeLimitInMS; + DWORD m_dwShutdownTimeLimitInMS; + DWORD m_cChildProcess; + DWORD m_dwChildProcessIds[MAX_ACTIVE_CHILD_PROCESSES]; + DWORD m_dwProcessId; + DWORD m_dwListeningProcessId; + + STRA m_straGuid; + + HANDLE m_hJobObject; + HANDLE m_hStdoutHandle; + // + // m_hProcessHandle is the handle to process this object creates. + // + HANDLE m_hProcessHandle; + HANDLE m_hListeningProcessHandle; + HANDLE m_hProcessWaitHandle; + HANDLE m_hShutdownHandle; + // + // m_hChildProcessHandle is the handle to process created by + // m_hProcessHandle process if it does. + // + HANDLE m_hChildProcessHandles[MAX_ACTIVE_CHILD_PROCESSES]; + HANDLE m_hChildProcessWaitHandles[MAX_ACTIVE_CHILD_PROCESSES]; + + PROCESS_MANAGER *m_pProcessManager; + ENVIRONMENT_VAR_HASH *m_pEnvironmentVarTable ; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/sttimer.h b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/sttimer.h new file mode 100644 index 0000000000..dfb79e7a6a --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/sttimer.h @@ -0,0 +1,279 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#ifndef _STTIMER_H +#define _STTIMER_H + +class STTIMER +{ +public: + + STTIMER() + : _pTimer( NULL ) + { + fInCanel = FALSE; + } + + virtual + ~STTIMER() + { + if ( _pTimer ) + { + CancelTimer(); + CloseThreadpoolTimer( _pTimer ); + _pTimer = NULL; + } + } + + HRESULT + InitializeTimer( + PTP_TIMER_CALLBACK pfnCallback, + VOID * pContext, + DWORD dwInitialWait = 0, + DWORD dwPeriod = 0 + ) + { + _pTimer = CreateThreadpoolTimer( pfnCallback, + pContext, + NULL ); + + if ( !_pTimer ) + { + return HRESULT_FROM_WIN32( GetLastError() ); + } + + if ( dwInitialWait ) + { + SetTimer( dwInitialWait, + dwPeriod ); + } + + return S_OK; + } + + VOID + SetTimer( + DWORD dwInitialWait, + DWORD dwPeriod = 0 + ) + { + FILETIME ftInitialWait; + + if ( dwInitialWait == 0 && dwPeriod == 0 ) + { + // + // Special case. We are preventing new callbacks + // from being queued. Any existing callbacks in the + // queue will still run. + // + // This effectively disables the timer. It can be + // re-enabled by setting non-zero initial wait or + // period values. + // + if (_pTimer != NULL) + { + SetThreadpoolTimer(_pTimer, NULL, 0, 0); + } + + return; + } + + InitializeRelativeFileTime( &ftInitialWait, dwInitialWait ); + + SetThreadpoolTimer( _pTimer, + &ftInitialWait, + dwPeriod, + 0 ); + } + + VOID + CancelTimer() + { + // + // Disable the timer + // + if (fInCanel) + return; + + fInCanel = TRUE; + SetTimer( 0 ); + + // + // Wait until any callbacks queued prior to disabling + // have completed. + // + if (_pTimer != NULL) + { + WaitForThreadpoolTimerCallbacks(_pTimer, TRUE); + } + + fInCanel = FALSE; + } + + static + VOID + CALLBACK + TimerCallback( + _In_ PTP_CALLBACK_INSTANCE Instance, + _In_ PVOID Context, + _In_ PTP_TIMER Timer + ) + { + Instance; + Timer; + STRU* pstruLogFilePath = (STRU*)Context; + HANDLE hStdoutHandle = NULL; + SECURITY_ATTRIBUTES saAttr = { 0 }; + HRESULT hr = S_OK; + + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = TRUE; + saAttr.lpSecurityDescriptor = NULL; + + hStdoutHandle = CreateFileW(pstruLogFilePath->QueryStr(), + FILE_READ_DATA, + FILE_SHARE_WRITE, + &saAttr, + OPEN_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + NULL); + if (hStdoutHandle == INVALID_HANDLE_VALUE) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + } + + CloseHandle(hStdoutHandle); + } + +private: + + VOID + InitializeRelativeFileTime( + FILETIME * pft, + DWORD dwMilliseconds + ) + { + LARGE_INTEGER li; + + // + // The pftDueTime parameter expects the time to be + // expressed as the number of 100 nanosecond intervals + // times -1. + // + // To convert from milliseconds, we'll multiply by + // -10000 + // + + li.QuadPart = (LONGLONG)dwMilliseconds * -10000; + + pft->dwHighDateTime = li.HighPart; + pft->dwLowDateTime = li.LowPart; + }; + + TP_TIMER * _pTimer; + BOOL fInCanel; +}; + +class STELAPSED +{ +public: + + STELAPSED() + : _dwInitTime( 0 ), + _dwInitTickCount( 0 ), + _dwPerfCountsPerMillisecond( 0 ), + _fUsingHighResolution( FALSE ) + { + LARGE_INTEGER li; + BOOL fResult; + + _dwInitTickCount = GetTickCount64(); + + fResult = QueryPerformanceFrequency( &li ); + + if ( !fResult ) + { + goto Finished; + } + + _dwPerfCountsPerMillisecond = li.QuadPart / 1000; + + fResult = QueryPerformanceCounter( &li ); + + if ( !fResult ) + { + goto Finished; + } + + _dwInitTime = li.QuadPart / _dwPerfCountsPerMillisecond; + + _fUsingHighResolution = TRUE; + +Finished: + + return; + } + + virtual + ~STELAPSED() + { + } + + LONGLONG + QueryElapsedTime() + { + LARGE_INTEGER li; + + if ( _fUsingHighResolution && QueryPerformanceCounter( &li ) ) + { + DWORD64 dwCurrentTime = li.QuadPart / _dwPerfCountsPerMillisecond; + + if ( dwCurrentTime < _dwInitTime ) + { + // + // It's theoretically possible that QueryPerformanceCounter + // may return slightly different values on different CPUs. + // In this case, we don't want to return an unexpected value + // so we'll return zero. This is acceptable because + // presumably such a case would only happen for a very short + // time window. + // + // It would be possible to prevent this by ensuring processor + // affinity for all calls to QueryPerformanceCounter, but that + // would be undesirable in the general case because it could + // introduce unnecessary context switches and potentially a + // CPU bottleneck. + // + // Note that this issue also applies to callers doing rapid + // calls to this function. If a caller wants to mitigate + // that, they could enforce the affinitization, or they + // could implement a similar sanity check when comparing + // returned values from this function. + // + + return 0; + } + + return dwCurrentTime - _dwInitTime; + } + + return GetTickCount64() - _dwInitTickCount; + } + + BOOL + QueryUsingHighResolution() + { + return _fUsingHighResolution; + } + +private: + + DWORD64 _dwInitTime; + DWORD64 _dwInitTickCount; + DWORD64 _dwPerfCountsPerMillisecond; + BOOL _fUsingHighResolution; +}; + +#endif // _STTIMER_H \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/websockethandler.h b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/websockethandler.h new file mode 100644 index 0000000000..aadfcb6884 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/websockethandler.h @@ -0,0 +1,221 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +class FORWARDING_HANDLER; + +class WEBSOCKET_HANDLER +{ +public: + WEBSOCKET_HANDLER(); + + virtual + ~WEBSOCKET_HANDLER(); + + static + HRESULT + StaticInitialize( + BOOL fEnableReferenceTraceLogging + ); + + static + VOID + StaticTerminate( + VOID + ); + + VOID + Terminate( + VOID + ); + + VOID + TerminateRequest( + VOID + ) + { + Cleanup(ServerStateUnavailable); + } + + HRESULT + ProcessRequest( + FORWARDING_HANDLER *pHandler, + IHttpContext * pHttpContext, + HINTERNET hRequest, + BOOL* fHandleCreated + ); + + REQUEST_NOTIFICATION_STATUS + OnAsyncCompletion( + VOID + ); + + HRESULT + OnWinHttpSendComplete( + WINHTTP_WEB_SOCKET_STATUS * pCompletionStatus + ); + + HRESULT + OnWinHttpShutdownComplete( + VOID + ); + + HRESULT + OnWinHttpReceiveComplete( + WINHTTP_WEB_SOCKET_STATUS * pCompletionStatus + ); + + HRESULT + OnWinHttpIoError( + WINHTTP_WEB_SOCKET_ASYNC_RESULT *pCompletionStatus + ); + +private: + enum CleanupReason + { + CleanupReasonUnknown = 0, + IdleTimeout = 1, + ConnectFailed = 2, + ClientDisconnect = 3, + ServerDisconnect = 4, + ServerStateUnavailable = 5 + }; + + WEBSOCKET_HANDLER(const WEBSOCKET_HANDLER &); + void operator=(const WEBSOCKET_HANDLER &); + + VOID + InsertRequest( + VOID + ); + + VOID + RemoveRequest( + VOID + ); + + static + VOID + WINAPI + OnReadIoCompletion( + HRESULT hrError, + VOID * pvCompletionContext, + DWORD cbIO, + BOOL fUTF8Encoded, + BOOL fFinalFragment, + BOOL fClose + ); + + static + VOID + WINAPI + OnWriteIoCompletion( + HRESULT hrError, + VOID * pvCompletionContext, + DWORD cbIO, + BOOL fUTF8Encoded, + BOOL fFinalFragment, + BOOL fClose + ); + + VOID + Cleanup( + CleanupReason reason + ); + + HRESULT + DoIisWebSocketReceive( + VOID + ); + + HRESULT + DoWinHttpWebSocketReceive( + VOID + ); + + HRESULT + DoIisWebSocketSend( + DWORD cbData, + WINHTTP_WEB_SOCKET_BUFFER_TYPE eBufferType + ); + + HRESULT + DoWinHttpWebSocketSend( + DWORD cbData, + WINHTTP_WEB_SOCKET_BUFFER_TYPE eBufferType + ); + + HRESULT + OnIisSendComplete( + HRESULT hrError, + DWORD cbIO + ); + + HRESULT + OnIisReceiveComplete( + HRESULT hrError, + DWORD cbIO, + BOOL fUTF8Encoded, + BOOL fFinalFragment, + BOOL fClose + ); + + VOID + IncrementOutstandingIo( + VOID + ); + + VOID + DecrementOutstandingIo( + VOID + ); + + VOID + IndicateCompletionToIIS( + VOID + ); + +private: + static const + DWORD RECEIVE_BUFFER_SIZE = 4*1024; + + LIST_ENTRY _listEntry; + + IHttpContext3 * _pHttpContext; + + IWebSocketContext * _pWebSocketContext; + + FORWARDING_HANDLER *_pHandler; + + HINTERNET _hWebSocketRequest; + + BYTE _WinHttpReceiveBuffer[RECEIVE_BUFFER_SIZE]; + + BYTE _IisReceiveBuffer[RECEIVE_BUFFER_SIZE]; + + CRITICAL_SECTION _RequestLock; + + LONG _dwOutstandingIo; + + volatile + BOOL _fHandleClosed; + + volatile + BOOL _fCleanupInProgress; + + volatile + BOOL _fIndicateCompletionToIis; + + volatile + BOOL _fReceivedCloseMsg; + + static + LIST_ENTRY sm_RequestsListHead; + + static + SRWLOCK sm_RequestsListLock; + + static + TRACE_LOG * sm_pTraceLog; +}; \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/winhttphelper.h b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/winhttphelper.h new file mode 100644 index 0000000000..b301a76cf2 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Inc/winhttphelper.h @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +typedef +HINTERNET +(WINAPI * PFN_WINHTTP_WEBSOCKET_COMPLETE_UPGRADE)( + _In_ HINTERNET hRequest, + _In_opt_ DWORD_PTR pContext +); + + +typedef +DWORD +(WINAPI * PFN_WINHTTP_WEBSOCKET_SEND)( + _In_ HINTERNET hWebSocket, + _In_ WINHTTP_WEB_SOCKET_BUFFER_TYPE eBufferType, + _In_reads_opt_(dwBufferLength) PVOID pvBuffer, + _In_ DWORD dwBufferLength +); + +typedef +DWORD +(WINAPI * PFN_WINHTTP_WEBSOCKET_RECEIVE)( + _In_ HINTERNET hWebSocket, + _Out_writes_bytes_to_(dwBufferLength, *pdwBytesRead) PVOID pvBuffer, + _In_ DWORD dwBufferLength, + _Out_range_(0, dwBufferLength) DWORD *pdwBytesRead, + _Out_ WINHTTP_WEB_SOCKET_BUFFER_TYPE *peBufferType +); + +typedef +DWORD +(WINAPI * PFN_WINHTTP_WEBSOCKET_SHUTDOWN)( + _In_ HINTERNET hWebSocket, + _In_ USHORT usStatus, + _In_reads_bytes_opt_(dwReasonLength) PVOID pvReason, + _In_range_(0, WINHTTP_WEB_SOCKET_MAX_CLOSE_REASON_LENGTH) DWORD dwReasonLength +); + +typedef +DWORD +(WINAPI * PFN_WINHTTP_WEBSOCKET_QUERY_CLOSE_STATUS)( + _In_ HINTERNET hWebSocket, + _Out_ USHORT *pusStatus, + _Out_writes_bytes_to_opt_(dwReasonLength, *pdwReasonLengthConsumed) PVOID pvReason, + _In_range_(0, WINHTTP_WEB_SOCKET_MAX_CLOSE_REASON_LENGTH) DWORD dwReasonLength, + _Out_range_(0, WINHTTP_WEB_SOCKET_MAX_CLOSE_REASON_LENGTH) DWORD *pdwReasonLengthConsumed +); + +class WINHTTP_HELPER +{ +public: + static + HRESULT + StaticInitialize(); + + static + VOID + GetFlagsFromBufferType( + __in WINHTTP_WEB_SOCKET_BUFFER_TYPE BufferType, + __out BOOL * pfUtf8Encoded, + __out BOOL * pfFinalFragment, + __out BOOL * pfClose + ); + + static + VOID + GetBufferTypeFromFlags( + __in BOOL fUtf8Encoded, + __in BOOL fFinalFragment, + __in BOOL fClose, + __out WINHTTP_WEB_SOCKET_BUFFER_TYPE* pBufferType + ); + + static + PFN_WINHTTP_WEBSOCKET_COMPLETE_UPGRADE sm_pfnWinHttpWebSocketCompleteUpgrade; + + static + PFN_WINHTTP_WEBSOCKET_SEND sm_pfnWinHttpWebSocketSend; + + static + PFN_WINHTTP_WEBSOCKET_RECEIVE sm_pfnWinHttpWebSocketReceive; + + static + PFN_WINHTTP_WEBSOCKET_SHUTDOWN sm_pfnWinHttpWebSocketShutdown; + + static + PFN_WINHTTP_WEBSOCKET_QUERY_CLOSE_STATUS sm_pfnWinHttpWebSocketQueryCloseStatus; +}; \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Source.def b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Source.def new file mode 100644 index 0000000000..9aae10ab5d --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/Source.def @@ -0,0 +1,4 @@ +LIBRARY aspnetcore + +EXPORTS + RegisterModule diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/aspnetcore_msg.mc b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/aspnetcore_msg.mc new file mode 100644 index 0000000000..73eb713f1c --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/aspnetcore_msg.mc @@ -0,0 +1,85 @@ +;/*++ +; +; Copyright (c) .NET Foundation. All rights reserved. +; Licensed under the MIT License. See License.txt in the project root for license information. +; +;Module Name: +; +; aspnetcore_msg.mc +; +;Abstract: +; +; Asp.Net Core Module localizable messages. +; +;--*/ +; +; +;#ifndef _ASPNETCORE_MSG_H_ +;#define _ASPNETCORE_MSG_H_ +; + +SeverityNames=(Success=0x0 + Informational=0x1 + Warning=0x2 + Error=0x3 + ) + +MessageIdTypedef=DWORD + +Messageid=1000 +SymbolicName=ASPNETCORE_EVENT_PROCESS_START_ERROR +Language=English +%1 +. + +Messageid=1001 +SymbolicName=ASPNETCORE_EVENT_PROCESS_START_SUCCESS +Language=English +%1 +. + +Messageid=1002 +SymbolicName=ASPNETCORE_EVENT_PROCESS_CRASH +Language=English +%1 +. + +Messageid=1003 +SymbolicName=ASPNETCORE_EVENT_RAPID_FAIL_COUNT_EXCEEDED +Language=English +%1 +. + +Messageid=1004 +SymbolicName=ASPNETCORE_EVENT_CONFIG_ERROR +Language=English +%1 +. + +Messageid=1005 +SymbolicName=ASPNETCORE_EVENT_GRACEFUL_SHUTDOWN_FAILURE +Language=English +%1 +. + +Messageid=1006 +SymbolicName=ASPNETCORE_EVENT_SENT_SHUTDOWN_HTTP_REQUEST +Language=English +%1 +. + +Messageid=1012 +SymbolicName=ASPNETCORE_EVENT_RECYCLE_APPOFFLINE +Language=English +%1 +. + +Messageid=1030 +SymbolicName=ASPNETCORE_EVENT_PROCESS_SHUTDOWN +Language=English +%1 +. + +; +;#endif // _ASPNETCORE_MODULE_MSG_H_ +; diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/aspnetcoremodule.rc b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/aspnetcoremodule.rc new file mode 100644 index 0000000000..4bd467538f --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/aspnetcoremodule.rc @@ -0,0 +1,120 @@ +// Microsoft Visual C++ generated resource script. +// +#include +#include "version.h" +#include "resource.h" +#include "aspnetcore_msg.rc" +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#define FileDescription "IIS ASP.NET Core Module. Commit: " CommitHash + +///////////////////////////////////////////////////////////////////////////// +// +// 11 +// + +//1 11 +//BEGIN +// 0x0001, 0x0000, 0x03e8, 0x0000, 0x03ed, 0x0000, 0x0010, 0x0000, 0x0010, +// 0x0001, 0x0025, 0x0031, 0x000d, 0x000a, 0x0000, 0x0000, 0x0010, 0x0001, +// 0x0025, 0x0031, 0x000d, 0x000a, 0x0000, 0x0000, 0x0010, 0x0001, 0x0025, +// 0x0031, 0x000d, 0x000a, 0x0000, 0x0000, 0x0010, 0x0001, 0x0025, 0x0031, +// 0x000d, 0x000a, 0x0000, 0x0000, 0x0010, 0x0001, 0x0025, 0x0031, 0x000d, +// 0x000a, 0x0000, 0x0000, 0x0010, 0x0001, 0x0025, 0x0031, 0x000d, 0x000a, +// 0x0000, 0x0000 +//END + + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION FileVersion + PRODUCTVERSION ProductVersion + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x40004L + FILETYPE 0x2L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", "Microsoft Corporation" + VALUE "FileDescription", FileDescription + VALUE "FileVersion", FileVersionStr + VALUE "InternalName", "aspnetcore" + VALUE "LegalCopyright", "Copyright (C) Microsoft Corporation" + VALUE "OriginalFilename", "aspnetcore.dll" + VALUE "ProductName", "ASP.NET Core Module" + VALUE "ProductVersion", ProductVersionStr + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END + + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + +STRINGTABLE +BEGIN + IDS_INVALID_PROPERTY "Property name '%s' in system.webServer/aspNetCore section has invalid value '%s' which does not conform to the prescribed format" + IDS_SERVER_ERROR "There was a connection error while trying to route the request." +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/resource.h b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/resource.h new file mode 100644 index 0000000000..493c3e2797 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/resource.h @@ -0,0 +1,18 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by aspnetcoremodule.rc +// +#define ASPNETCORE_EVENT_MSG_BUFFER_SIZE 256 +#define IDS_INVALID_PROPERTY 1000 +#define IDS_SERVER_ERROR 1001 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 101 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1000 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/application.cxx b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/application.cxx new file mode 100644 index 0000000000..f80435cfbb --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/application.cxx @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "precomp.hxx" + +APPLICATION::~APPLICATION() +{ + if (m_pAppOfflineHtm != NULL) + { + m_pAppOfflineHtm->DereferenceAppOfflineHtm(); + m_pAppOfflineHtm = NULL; + } + + if (m_pFileWatcherEntry != NULL) + { + // Mark the entry as invalid, + // StopMonitor will close the file handle and trigger a FCN + // the entry will delete itself when processing this FCN + m_pFileWatcherEntry->MarkEntryInValid(); + m_pFileWatcherEntry->StopMonitor(); + m_pFileWatcherEntry = NULL; + } + + if (m_pProcessManager != NULL) + { + m_pProcessManager->ShutdownAllProcesses(); + m_pProcessManager->DereferenceProcessManager(); + m_pProcessManager = NULL; + } +} + +HRESULT +APPLICATION::Initialize( + _In_ APPLICATION_MANAGER* pApplicationManager, + _In_ LPCWSTR pszApplication, + _In_ LPCWSTR pszPhysicalPath +) +{ + HRESULT hr = S_OK; + + DBG_ASSERT(pszPhysicalPath != NULL); + DBG_ASSERT(pApplicationManager != NULL); + DBG_ASSERT(pszPhysicalPath != NULL); + m_strAppPhysicalPath.Copy(pszPhysicalPath); + + m_pApplicationManager = pApplicationManager; + + hr = m_applicationKey.Initialize(pszApplication); + if (FAILED(hr)) + { + goto Finished; + } + + if (m_pProcessManager == NULL) + { + m_pProcessManager = new PROCESS_MANAGER; + if (m_pProcessManager == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + hr = m_pProcessManager->Initialize(); + if (FAILED(hr)) + { + goto Finished; + } + } + + if (m_pFileWatcherEntry == NULL) + { + m_pFileWatcherEntry = new FILE_WATCHER_ENTRY(pApplicationManager->GetFileWatcher()); + if (m_pFileWatcherEntry == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + } + + UpdateAppOfflineFileHandle(); + +Finished: + + if (FAILED(hr)) + { + if (m_pFileWatcherEntry != NULL) + { + m_pFileWatcherEntry->DereferenceFileWatcherEntry(); + m_pFileWatcherEntry = NULL; + } + + if (m_pProcessManager != NULL) + { + m_pProcessManager->DereferenceProcessManager(); + m_pProcessManager = NULL; + } + } + + return hr; +} + +HRESULT +APPLICATION::StartMonitoringAppOffline() +{ + HRESULT hr = S_OK; + + hr = m_pFileWatcherEntry->Create(m_strAppPhysicalPath.QueryStr(), L"app_offline.htm", this, NULL); + + return hr; +} + +VOID +APPLICATION::UpdateAppOfflineFileHandle() +{ + STRU strFilePath; + PATH::ConvertPathToFullPath(L".\\app_offline.htm", m_strAppPhysicalPath.QueryStr(), &strFilePath); + APP_OFFLINE_HTM *pOldAppOfflineHtm = NULL; + APP_OFFLINE_HTM *pNewAppOfflineHtm = NULL; + + if (INVALID_FILE_ATTRIBUTES == GetFileAttributes(strFilePath.QueryStr()) && GetLastError() == ERROR_FILE_NOT_FOUND) + { + m_fAppOfflineFound = FALSE; + } + else + { + m_fAppOfflineFound = TRUE; + + // + // send shutdown signal + // + + // The reason why we send the shutdown signal before loading the new app_offline file is because we want to make some delay + // before reading the appoffline.htm so that the file change can be done on time. + if (m_pProcessManager != NULL) + { + m_pProcessManager->SendShutdownSignal(); + } + + pNewAppOfflineHtm = new APP_OFFLINE_HTM(strFilePath.QueryStr()); + + if ( pNewAppOfflineHtm != NULL ) + { + if (pNewAppOfflineHtm->Load()) + { + // + // loaded the new app_offline.htm + // + pOldAppOfflineHtm = (APP_OFFLINE_HTM *)InterlockedExchangePointer((VOID**)&m_pAppOfflineHtm, pNewAppOfflineHtm); + + if (pOldAppOfflineHtm != NULL) + { + pOldAppOfflineHtm->DereferenceAppOfflineHtm(); + pOldAppOfflineHtm = NULL; + } + } + else + { + // ignored the new app_offline file because the file does not exist. + pNewAppOfflineHtm->DereferenceAppOfflineHtm(); + pNewAppOfflineHtm = NULL; + } + } + } +} \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/applicationmanager.cxx b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/applicationmanager.cxx new file mode 100644 index 0000000000..41f20013b7 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/applicationmanager.cxx @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "precomp.hxx" + +APPLICATION_MANAGER* APPLICATION_MANAGER::sm_pApplicationManager = NULL; + +HRESULT +APPLICATION_MANAGER::GetApplication( + _In_ IHttpContext* pContext, + _Out_ APPLICATION ** ppApplication +) +{ + HRESULT hr = S_OK; + APPLICATION *pApplication = NULL; + APPLICATION_KEY key; + BOOL fExclusiveLock = FALSE; + PCWSTR pszApplicationId = NULL; + + *ppApplication = NULL; + + DBG_ASSERT(pContext != NULL); + DBG_ASSERT(pContext->GetApplication() != NULL); + pszApplicationId = pContext->GetApplication()->GetApplicationId(); + + hr = key.Initialize(pszApplicationId); + if (FAILED(hr)) + { + goto Finished; + } + + m_pApplicationHash->FindKey(&key, ppApplication); + + if (*ppApplication == NULL) + { + + pApplication = new APPLICATION(); + if (pApplication == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + AcquireSRWLockExclusive(&m_srwLock); + fExclusiveLock = TRUE; + m_pApplicationHash->FindKey(&key, ppApplication); + + if (*ppApplication != NULL) + { + // someone else created the application + delete pApplication; + pApplication = NULL; + goto Finished; + } + + hr = pApplication->Initialize(this, pszApplicationId, pContext->GetApplication()->GetApplicationPhysicalPath()); + if (FAILED(hr)) + { + goto Finished; + } + + hr = m_pApplicationHash->InsertRecord( pApplication ); + + if (FAILED(hr)) + { + goto Finished; + } + ReleaseSRWLockExclusive(&m_srwLock); + fExclusiveLock = FALSE; + + pApplication->StartMonitoringAppOffline(); + + *ppApplication = pApplication; + pApplication = NULL; + } + +Finished: + + if (fExclusiveLock == TRUE) + ReleaseSRWLockExclusive(&m_srwLock); + + if (FAILED(hr)) + { + if (pApplication != NULL) + { + pApplication->DereferenceApplication(); + pApplication = NULL; + } + } + + return hr; +} + + +HRESULT +APPLICATION_MANAGER::RecycleApplication( + _In_ LPCWSTR pszApplication +) +{ + HRESULT hr = S_OK; + APPLICATION_KEY key; + + hr = key.Initialize(pszApplication); + if (FAILED(hr)) + { + goto Finished; + } + AcquireSRWLockExclusive(&m_srwLock); + m_pApplicationHash->DeleteKey(&key); + ReleaseSRWLockExclusive(&m_srwLock); + +Finished: + + return hr; +} + +HRESULT +APPLICATION_MANAGER::Get502ErrorPage( + _Out_ HTTP_DATA_CHUNK** ppErrorPage +) +{ + HRESULT hr = S_OK; + BOOL fExclusiveLock = FALSE; + HTTP_DATA_CHUNK *pHttp502ErrorPage = NULL; + + DBG_ASSERT(ppErrorPage != NULL); + + //on-demand create the error page + if (m_pHttp502ErrorPage != NULL) + { + *ppErrorPage = m_pHttp502ErrorPage; + } + else + { + AcquireSRWLockExclusive(&m_srwLock); + fExclusiveLock = TRUE; + if (m_pHttp502ErrorPage != NULL) + { + *ppErrorPage = m_pHttp502ErrorPage; + } + else + { + size_t maxsize = 5000; + pHttp502ErrorPage = new HTTP_DATA_CHUNK(); + if (pHttp502ErrorPage == NULL) + { + hr = HRESULT_FROM_WIN32(ERROR_NOT_ENOUGH_MEMORY); + goto Finished; + } + pHttp502ErrorPage->DataChunkType = HttpDataChunkFromMemory; + pHttp502ErrorPage->FromMemory.pBuffer = (PVOID)m_pstrErrorInfo; + + pHttp502ErrorPage->FromMemory.BufferLength = (ULONG)strnlen(m_pstrErrorInfo, maxsize); //(ULONG)(wcslen(m_pstrErrorInfo)); // *sizeof(WCHAR); + if(m_pHttp502ErrorPage != NULL) + { + delete m_pHttp502ErrorPage; + } + m_pHttp502ErrorPage = pHttp502ErrorPage; + *ppErrorPage = m_pHttp502ErrorPage; + } + } + +Finished: + if (fExclusiveLock) + { + ReleaseSRWLockExclusive(&m_srwLock); + } + + if (FAILED(hr)) + { + if (pHttp502ErrorPage != NULL) + { + delete pHttp502ErrorPage; + } + } + + return hr; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/aspnetcoreconfig.cxx b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/aspnetcoreconfig.cxx new file mode 100644 index 0000000000..d8bcd8d777 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/aspnetcoreconfig.cxx @@ -0,0 +1,416 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "precomp.hxx" + +ASPNETCORE_CONFIG::~ASPNETCORE_CONFIG() +{ + // + // the destructor will be called once IIS decides to recycle the module context (i.e., application) + // + if (!m_struApplication.IsEmpty()) + { + APPLICATION_MANAGER::GetInstance()->RecycleApplication(m_struApplication.QueryStr()); + } + if(m_pEnvironmentVariables != NULL) + { + m_pEnvironmentVariables->Clear(); + delete m_pEnvironmentVariables; + m_pEnvironmentVariables = NULL; + } +} + +HRESULT +ASPNETCORE_CONFIG::GetConfig( + _In_ IHttpContext *pHttpContext, + _Out_ ASPNETCORE_CONFIG **ppAspNetCoreConfig +) +{ + HRESULT hr = S_OK; + IHttpApplication *pHttpApplication = pHttpContext->GetApplication(); + ASPNETCORE_CONFIG *pAspNetCoreConfig = NULL; + + if (ppAspNetCoreConfig == NULL) + { + hr = E_INVALIDARG; + goto Finished; + } + + *ppAspNetCoreConfig = NULL; + + // potential bug if user sepcific config at virtual dir level + pAspNetCoreConfig = (ASPNETCORE_CONFIG*) + pHttpApplication->GetModuleContextContainer()->GetModuleContext(g_pModuleId); + + if (pAspNetCoreConfig != NULL) + { + *ppAspNetCoreConfig = pAspNetCoreConfig; + pAspNetCoreConfig = NULL; + goto Finished; + } + + pAspNetCoreConfig = new ASPNETCORE_CONFIG; + if (pAspNetCoreConfig == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + hr = pAspNetCoreConfig->Populate(pHttpContext); + if (FAILED(hr)) + { + goto Finished; + } + + hr = pHttpApplication->GetModuleContextContainer()-> + SetModuleContext(pAspNetCoreConfig, g_pModuleId); + if (FAILED(hr)) + { + if (hr == HRESULT_FROM_WIN32(ERROR_ALREADY_ASSIGNED)) + { + delete pAspNetCoreConfig; + + pAspNetCoreConfig = (ASPNETCORE_CONFIG*)pHttpApplication-> + GetModuleContextContainer()-> + GetModuleContext(g_pModuleId); + + _ASSERT(pAspNetCoreConfig != NULL); + + hr = S_OK; + } + else + { + goto Finished; + } + } + else + { + // set appliction info here instead of inside Populate() + // as the destructor will delete the backend process + hr = pAspNetCoreConfig->QueryApplicationPath()->Copy(pHttpApplication->GetApplicationId()); + if (FAILED(hr)) + { + goto Finished; + } + } + + *ppAspNetCoreConfig = pAspNetCoreConfig; + pAspNetCoreConfig = NULL; + +Finished: + + if (pAspNetCoreConfig != NULL) + { + delete pAspNetCoreConfig; + pAspNetCoreConfig = NULL; + } + + return hr; +} + +HRESULT +ASPNETCORE_CONFIG::Populate( + IHttpContext *pHttpContext +) +{ + HRESULT hr = S_OK; + STACK_STRU(strSiteConfigPath, 256); + STRU strEnvName; + STRU strEnvValue; + STRU strExpandedEnvValue; + IAppHostAdminManager *pAdminManager = NULL; + IAppHostElement *pAspNetCoreElement = NULL; + IAppHostElement *pWindowsAuthenticationElement = NULL; + IAppHostElement *pBasicAuthenticationElement = NULL; + IAppHostElement *pAnonymousAuthenticationElement = NULL; + IAppHostElement *pEnvVarList = NULL; + IAppHostElement *pEnvVar = NULL; + IAppHostElementCollection *pEnvVarCollection = NULL; + ULONGLONG ullRawTimeSpan = 0; + ENUM_INDEX index; + ENVIRONMENT_VAR_ENTRY* pEntry = NULL; + + m_pEnvironmentVariables = new ENVIRONMENT_VAR_HASH(); + if (m_pEnvironmentVariables == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + if (FAILED(hr = m_pEnvironmentVariables->Initialize(37 /*prime*/))) + { + delete m_pEnvironmentVariables; + m_pEnvironmentVariables = NULL; + goto Finished; + } + + pAdminManager = g_pHttpServer->GetAdminManager(); + + hr = strSiteConfigPath.Copy(pHttpContext->GetApplication()->GetAppConfigPath()); + if (FAILED(hr)) + { + goto Finished; + } + + hr = pAdminManager->GetAdminSection(CS_WINDOWS_AUTHENTICATION_SECTION, + strSiteConfigPath.QueryStr(), + &pWindowsAuthenticationElement); + if (FAILED(hr)) + { + // assume the corresponding authen was not enabled + // as the section may get deleted by user in some HWC case + // ToDo: log a warning to event log + m_fWindowsAuthEnabled = FALSE; + } + else + { + hr = GetElementBoolProperty(pWindowsAuthenticationElement, + CS_AUTHENTICATION_ENABLED, + &m_fWindowsAuthEnabled); + if (FAILED(hr)) + { + goto Finished; + } + } + + hr = pAdminManager->GetAdminSection(CS_BASIC_AUTHENTICATION_SECTION, + strSiteConfigPath.QueryStr(), + &pBasicAuthenticationElement); + if (FAILED(hr)) + { + m_fBasicAuthEnabled = FALSE; + } + else + { + hr = GetElementBoolProperty(pBasicAuthenticationElement, + CS_AUTHENTICATION_ENABLED, + &m_fBasicAuthEnabled); + if (FAILED(hr)) + { + goto Finished; + } + } + + hr = pAdminManager->GetAdminSection(CS_ANONYMOUS_AUTHENTICATION_SECTION, + strSiteConfigPath.QueryStr(), + &pAnonymousAuthenticationElement); + if (FAILED(hr)) + { + m_fAnonymousAuthEnabled = FALSE; + } + else + { + hr = GetElementBoolProperty(pAnonymousAuthenticationElement, + CS_AUTHENTICATION_ENABLED, + &m_fAnonymousAuthEnabled); + if (FAILED(hr)) + { + goto Finished; + } + } + + hr = pAdminManager->GetAdminSection(CS_ASPNETCORE_SECTION, + strSiteConfigPath.QueryStr(), + &pAspNetCoreElement); + if (FAILED(hr)) + { + goto Finished; + } + + hr = GetElementStringProperty(pAspNetCoreElement, + CS_ASPNETCORE_PROCESS_EXE_PATH, + &m_struProcessPath); + if (FAILED(hr)) + { + goto Finished; + } + + hr = GetElementStringProperty(pAspNetCoreElement, + CS_ASPNETCORE_PROCESS_ARGUMENTS, + &m_struArguments); + if (FAILED(hr)) + { + goto Finished; + } + + hr = GetElementDWORDProperty(pAspNetCoreElement, + CS_ASPNETCORE_RAPID_FAILS_PER_MINUTE, + &m_dwRapidFailsPerMinute); + if (FAILED(hr)) + { + goto Finished; + } + + // + // rapidFailsPerMinute cannot be greater than 100. + // + if (m_dwRapidFailsPerMinute > MAX_RAPID_FAILS_PER_MINUTE) + { + m_dwRapidFailsPerMinute = MAX_RAPID_FAILS_PER_MINUTE; + } + + hr = GetElementDWORDProperty(pAspNetCoreElement, + CS_ASPNETCORE_PROCESSES_PER_APPLICATION, + &m_dwProcessesPerApplication); + if (FAILED(hr)) + { + goto Finished; + } + + hr = GetElementDWORDProperty( + pAspNetCoreElement, + CS_ASPNETCORE_PROCESS_STARTUP_TIME_LIMIT, + &m_dwStartupTimeLimitInMS + ); + if (FAILED(hr)) + { + goto Finished; + } + + m_dwStartupTimeLimitInMS *= MILLISECONDS_IN_ONE_SECOND; + + hr = GetElementDWORDProperty( + pAspNetCoreElement, + CS_ASPNETCORE_PROCESS_SHUTDOWN_TIME_LIMIT, + &m_dwShutdownTimeLimitInMS + ); + if (FAILED(hr)) + { + goto Finished; + } + m_dwShutdownTimeLimitInMS *= MILLISECONDS_IN_ONE_SECOND; + + hr = GetElementBoolProperty(pAspNetCoreElement, + CS_ASPNETCORE_FORWARD_WINDOWS_AUTH_TOKEN, + &m_fForwardWindowsAuthToken); + if (FAILED(hr)) + { + goto Finished; + } + + hr = GetElementBoolProperty(pAspNetCoreElement, + CS_ASPNETCORE_DISABLE_START_UP_ERROR_PAGE, + &m_fDisableStartUpErrorPage); + if (FAILED(hr)) + { + goto Finished; + } + + hr = GetElementRawTimeSpanProperty( + pAspNetCoreElement, + CS_ASPNETCORE_WINHTTP_REQUEST_TIMEOUT, + &ullRawTimeSpan + ); + if (FAILED(hr)) + { + goto Finished; + } + + m_dwRequestTimeoutInMS = (DWORD)TIMESPAN_IN_MILLISECONDS(ullRawTimeSpan); + + hr = GetElementBoolProperty(pAspNetCoreElement, + CS_ASPNETCORE_STDOUT_LOG_ENABLED, + &m_fStdoutLogEnabled); + if (FAILED(hr)) + { + goto Finished; + } + + hr = GetElementStringProperty(pAspNetCoreElement, + CS_ASPNETCORE_STDOUT_LOG_FILE, + &m_struStdoutLogFile); + if (FAILED(hr)) + { + goto Finished; + } + + hr = GetElementChildByName(pAspNetCoreElement, + CS_ASPNETCORE_ENVIRONMENT_VARIABLES, + &pEnvVarList); + if (FAILED(hr)) + { + goto Finished; + } + + hr = pEnvVarList->get_Collection(&pEnvVarCollection); + if (FAILED(hr)) + { + goto Finished; + } + + for (hr = FindFirstElement(pEnvVarCollection, &index, &pEnvVar); + SUCCEEDED(hr); + hr = FindNextElement(pEnvVarCollection, &index, &pEnvVar)) + { + if (hr == S_FALSE) + { + hr = S_OK; + break; + } + + if (FAILED(hr = GetElementStringProperty(pEnvVar, + CS_ASPNETCORE_ENVIRONMENT_VARIABLE_NAME, + &strEnvName)) || + FAILED(hr = GetElementStringProperty(pEnvVar, + CS_ASPNETCORE_ENVIRONMENT_VARIABLE_VALUE, + &strEnvValue)) || + FAILED(hr = strEnvName.Append(L"=")) || + FAILED(hr = STRU::ExpandEnvironmentVariables(strEnvValue.QueryStr(), &strExpandedEnvValue))) + { + goto Finished; + } + + pEntry = new ENVIRONMENT_VAR_ENTRY(); + if (pEntry == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + if (FAILED(hr = pEntry->Initialize(strEnvName.QueryStr(), strExpandedEnvValue.QueryStr())) || + FAILED(hr = m_pEnvironmentVariables->InsertRecord(pEntry))) + { + goto Finished; + } + strEnvName.Reset(); + strEnvValue.Reset(); + strExpandedEnvValue.Reset(); + pEnvVar->Release(); + pEnvVar = NULL; + pEntry->Dereference(); + pEntry = NULL; + } + +Finished: + + if (pAspNetCoreElement != NULL) + { + pAspNetCoreElement->Release(); + pAspNetCoreElement = NULL; + } + + if (pEnvVarList != NULL) + { + pEnvVarList->Release(); + pEnvVarList = NULL; + } + + if (pEnvVar != NULL) + { + pEnvVar->Release(); + pEnvVar = NULL; + } + + if (pEnvVarCollection != NULL) + { + pEnvVarCollection->Release(); + pEnvVarCollection = NULL; + } + + if (pEntry != NULL) + { + pEntry->Dereference(); + pEntry = NULL; + } + + return hr; +} \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/filewatcher.cxx b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/filewatcher.cxx new file mode 100644 index 0000000000..c3fd9f1882 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/filewatcher.cxx @@ -0,0 +1,481 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "precomp.hxx" + +FILE_WATCHER::FILE_WATCHER() : + m_hCompletionPort(NULL), + m_hChangeNotificationThread(NULL) +{ +} + +FILE_WATCHER::~FILE_WATCHER() +{ + if (m_hChangeNotificationThread != NULL) + { + CloseHandle(m_hChangeNotificationThread); + m_hChangeNotificationThread = NULL; + } +} + +HRESULT +FILE_WATCHER::Create( + VOID +) +{ + HRESULT hr = S_OK; + + m_hCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, + NULL, + 0, + 0); + + if (m_hCompletionPort == NULL) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + m_hChangeNotificationThread = CreateThread(NULL, + 0, + (LPTHREAD_START_ROUTINE)ChangeNotificationThread, + this, + 0, + NULL); + + if (m_hChangeNotificationThread == NULL) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + + CloseHandle(m_hCompletionPort); + m_hCompletionPort = NULL; + + goto Finished; + } + +Finished: + return hr; +} + +DWORD +WINAPI +FILE_WATCHER::ChangeNotificationThread( + LPVOID pvArg +) +/*++ + +Routine Description: + +IO completion thread + +Arguments: + +None + +Return Value: + +Win32 error + +--*/ +{ + FILE_WATCHER * pFileMonitor; + BOOL fSuccess = FALSE; + DWORD cbCompletion = 0; + OVERLAPPED * pOverlapped = NULL; + DWORD dwErrorStatus; + ULONG_PTR completionKey; + + pFileMonitor = (FILE_WATCHER*)pvArg; + DBG_ASSERT(pFileMonitor != NULL); + + while (TRUE) + { + fSuccess = GetQueuedCompletionStatus( + pFileMonitor->m_hCompletionPort, + &cbCompletion, + &completionKey, + &pOverlapped, + INFINITE); + + DBG_ASSERT(fSuccess); + DebugPrint(1, "FILE_WATCHER::ChangeNotificationThread"); + dwErrorStatus = fSuccess ? ERROR_SUCCESS : GetLastError(); + + if (completionKey == FILE_WATCHER_SHUTDOWN_KEY) + { + continue; + } + + DBG_ASSERT(pOverlapped != NULL); + if (pOverlapped != NULL) + { + FileWatcherCompletionRoutine( + dwErrorStatus, + cbCompletion, + pOverlapped); + } + pOverlapped = NULL; + cbCompletion = 0; + } +} + +VOID +WINAPI +FILE_WATCHER::FileWatcherCompletionRoutine( + DWORD dwCompletionStatus, + DWORD cbCompletion, + OVERLAPPED * pOverlapped +) +/*++ + +Routine Description: + +Called when ReadDirectoryChangesW() completes + +Arguments: + +dwCompletionStatus - Error of completion +cbCompletion - Bytes of completion +pOverlapped - State of completion + +Return Value: + +None + +--*/ +{ + FILE_WATCHER_ENTRY * pMonitorEntry; + pMonitorEntry = CONTAINING_RECORD(pOverlapped, FILE_WATCHER_ENTRY, _overlapped); + pMonitorEntry->DereferenceFileWatcherEntry(); + DBG_ASSERT(pMonitorEntry != NULL); + + pMonitorEntry->HandleChangeCompletion(dwCompletionStatus, cbCompletion); + + if (pMonitorEntry->QueryIsValid()) + { + // + // Continue monitoring + // + pMonitorEntry->Monitor(); + } + else + { + // + // Marked by application distructor + // Deference the entry to delete it + // + pMonitorEntry->DereferenceFileWatcherEntry(); + } +} + + +FILE_WATCHER_ENTRY::FILE_WATCHER_ENTRY(FILE_WATCHER * pFileMonitor) : + _pFileMonitor(pFileMonitor), + _hDirectory(INVALID_HANDLE_VALUE), + _hImpersonationToken(NULL), + _pApplication(NULL), + _lStopMonitorCalled(0), + _cRefs(1), + _fIsValid(TRUE) +{ + _dwSignature = FILE_WATCHER_ENTRY_SIGNATURE; + InitializeSRWLock(&_srwLock); +} + +FILE_WATCHER_ENTRY::~FILE_WATCHER_ENTRY() +{ + _dwSignature = FILE_WATCHER_ENTRY_SIGNATURE_FREE; + + if (_hDirectory != INVALID_HANDLE_VALUE) + { + CloseHandle(_hDirectory); + _hDirectory = INVALID_HANDLE_VALUE; + } + + if (_hImpersonationToken != NULL) + { + CloseHandle(_hImpersonationToken); + _hImpersonationToken = NULL; + } +} + +#pragma warning(disable:4100) + +HRESULT +FILE_WATCHER_ENTRY::HandleChangeCompletion( + _In_ DWORD dwCompletionStatus, + _In_ DWORD cbCompletion +) +/*++ + +Routine Description: + +Handle change notification (see if any of associated config files +need to be flushed) + +Arguments: + +dwCompletionStatus - Completion status +cbCompletion - Bytes of completion + +Return Value: + +HRESULT + +--*/ +{ + HRESULT hr = S_OK; + FILE_NOTIFY_INFORMATION * pNotificationInfo; + BOOL fFileChanged = FALSE; + STRU strEventMsg; + + AcquireSRWLockExclusive(&_srwLock); + if (!_fIsValid) + { + goto Finished; + } + + // When directory handle is closed then HandleChangeCompletion + // happens with cbCompletion = 0 and dwCompletionStatus = 0 + // From documentation it is not clear if that combination + // of return values is specific to closing handles or whether + // it could also mean an error condition. Hence we will maintain + // explicit flag that will help us determine if entry + // is being shutdown (StopMonitor() called) + // + if (_lStopMonitorCalled) + { + goto Finished; + } + + // + // There could be a FCN overflow + // Let assume the file got changed instead of checking files + // Othersie we have to cache the file info + // + if (cbCompletion == 0) + { + fFileChanged = TRUE; + } + else + { + pNotificationInfo = (FILE_NOTIFY_INFORMATION*)_buffDirectoryChanges.QueryPtr(); + DBG_ASSERT(pNotificationInfo != NULL); + + while (pNotificationInfo != NULL) + { + // + // check whether the monitored file got changed + // + if (_wcsnicmp(pNotificationInfo->FileName, + _strFileName.QueryStr(), + pNotificationInfo->FileNameLength/sizeof(WCHAR)) == 0) + { + fFileChanged = TRUE; + break; + } + // + // Advance to next notification + // + if (pNotificationInfo->NextEntryOffset == 0) + { + pNotificationInfo = NULL; + } + else + { + pNotificationInfo = (FILE_NOTIFY_INFORMATION*) + ((PBYTE)pNotificationInfo + + pNotificationInfo->NextEntryOffset); + } + } + } + + if (fFileChanged) + { + LPCWSTR apsz[1]; + if (SUCCEEDED(strEventMsg.SafeSnwprintf( + ASPNETCORE_EVENT_RECYCLE_APPOFFLINE_MSG, + _strFileName.QueryStr()))) + { + apsz[0] = strEventMsg.QueryStr(); + + // + // not checking return code because if ReportEvent + // fails, we cannot do anything. + // + if (FORWARDING_HANDLER::QueryEventLog() != NULL) + { + ReportEventW(FORWARDING_HANDLER::QueryEventLog(), + EVENTLOG_INFORMATION_TYPE, + 0, + ASPNETCORE_EVENT_RECYCLE_APPOFFLINE, + NULL, + 1, + 0, + apsz, + NULL); + } + } + // + // so far we only monitoring app_offline + // + _pApplication->UpdateAppOfflineFileHandle(); + } + +Finished: + ReleaseSRWLockExclusive(&_srwLock); + return hr; +} + +#pragma warning( error : 4100 ) + +HRESULT +FILE_WATCHER_ENTRY::Monitor(VOID) +{ + HRESULT hr = S_OK; + DWORD cbRead; + + AcquireSRWLockExclusive(&_srwLock); + ReferenceFileWatcherEntry(); + ZeroMemory(&_overlapped, sizeof(_overlapped)); + + if(!ReadDirectoryChangesW(_hDirectory, + _buffDirectoryChanges.QueryPtr(), + _buffDirectoryChanges.QuerySize(), + FALSE, // Watching sub dirs. Set to False now as only monitoring app_offline + FILE_NOTIFY_VALID_MASK & ~FILE_NOTIFY_CHANGE_LAST_ACCESS, + &cbRead, + &_overlapped, + NULL)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + } + + ReleaseSRWLockExclusive(&_srwLock); + return hr; +} + +VOID +FILE_WATCHER_ENTRY::StopMonitor(VOID) +{ + // + // Flag that monitoring is being stopped so that + // we know that HandleChangeCompletion() call + // can be ignored + // + InterlockedExchange(&_lStopMonitorCalled, 1); + + AcquireSRWLockExclusive(&_srwLock); + + if (_hDirectory != INVALID_HANDLE_VALUE) + { + CloseHandle(_hDirectory); + _hDirectory = INVALID_HANDLE_VALUE; + } + + ReleaseSRWLockExclusive(&_srwLock); +} + +HRESULT +FILE_WATCHER_ENTRY::Create( + _In_ PCWSTR pszDirectoryToMonitor, + _In_ PCWSTR pszFileNameToMonitor, + _In_ APPLICATION* pApplication, + _In_ HANDLE hImpersonationToken +) +{ + HRESULT hr = S_OK; + BOOL fRet = FALSE; + + if (pszDirectoryToMonitor == NULL || + pszFileNameToMonitor == NULL || + pApplication == NULL) + { + DBG_ASSERT(FALSE); + hr = HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER); + goto Finished; + } + + // + //remember the application + // + _pApplication = pApplication; + + if (FAILED(hr = _strFileName.Copy(pszFileNameToMonitor))) + { + goto Finished; + } + + if (FAILED(hr = _strDirectoryName.Copy(pszDirectoryToMonitor))) + { + goto Finished; + } + + // + // Resize change buffer to something "reasonable" + // + if (!_buffDirectoryChanges.Resize(FILE_WATCHER_ENTRY_BUFFER_SIZE)) + { + hr = HRESULT_FROM_WIN32(ERROR_NOT_ENOUGH_MEMORY); + goto Finished; + } + + if (hImpersonationToken != NULL) + { + fRet = DuplicateHandle(GetCurrentProcess(), + hImpersonationToken, + GetCurrentProcess(), + &_hImpersonationToken, + 0, + FALSE, + DUPLICATE_SAME_ACCESS); + + if (!fRet) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + } + else + { + if (_hImpersonationToken != NULL) + { + CloseHandle(_hImpersonationToken); + _hImpersonationToken = NULL; + } + } + + _hDirectory = CreateFileW( + _strDirectoryName.QueryStr(), + FILE_LIST_DIRECTORY, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, + NULL); + + if (_hDirectory == INVALID_HANDLE_VALUE) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + if (CreateIoCompletionPort( + _hDirectory, + _pFileMonitor->QueryCompletionPort(), + NULL, + 0) == NULL) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + // + // Start monitoring + // + hr = Monitor(); + +Finished: + + return hr; +} \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/forwarderconnection.cxx b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/forwarderconnection.cxx new file mode 100644 index 0000000000..ed1e29aadd --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/forwarderconnection.cxx @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "precomp.hxx" + +FORWARDER_CONNECTION::FORWARDER_CONNECTION( + VOID +) : m_cRefs (1), + m_hConnection (NULL) +{ +} + +HRESULT +FORWARDER_CONNECTION::Initialize( + DWORD dwPort +) +{ + HRESULT hr = S_OK; + + hr = m_ConnectionKey.Initialize( dwPort ); + if ( FAILED( hr ) ) + { + goto Finished; + } + + m_hConnection = WinHttpConnect(FORWARDING_HANDLER::sm_hSession, + L"127.0.0.1", + (USHORT) dwPort, + 0); + if (m_hConnection == NULL) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + // + // Since WinHttp will not emit WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING + // when closing WebSocket handle on Win8. Register callback at Connect level as a workaround + // + if (WinHttpSetStatusCallback(m_hConnection, + FORWARDING_HANDLER::OnWinHttpCompletion, + WINHTTP_CALLBACK_FLAG_HANDLES, + NULL) == WINHTTP_INVALID_STATUS_CALLBACK) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + +Finished: + + return hr; +} \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/forwardinghandler.cxx b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/forwardinghandler.cxx new file mode 100644 index 0000000000..1665539f18 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/forwardinghandler.cxx @@ -0,0 +1,3079 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "precomp.hxx" +#include + +// Just to be aware of the FORWARDING_HANDLER object size. +C_ASSERT(sizeof(FORWARDING_HANDLER) <= 632); + +#define DEF_MAX_FORWARDS 32 +#define HEX_TO_ASCII(c) ((CHAR)(((c) < 10) ? ((c) + '0') : ((c) + 'a' - 10))) + +#define BUFFER_SIZE (8192UL) +#define ENTITY_BUFFER_SIZE (6 + BUFFER_SIZE + 2) +#define STR_ANCM_CHILDREQUEST "ANCM_WasCreateProcessFailure" + +HINTERNET FORWARDING_HANDLER::sm_hSession = NULL; +STRU FORWARDING_HANDLER::sm_strErrorFormat; +HANDLE FORWARDING_HANDLER::sm_hEventLog = NULL; +ALLOC_CACHE_HANDLER * FORWARDING_HANDLER::sm_pAlloc = NULL; +TRACE_LOG * FORWARDING_HANDLER::sm_pTraceLog = NULL; +PROTOCOL_CONFIG FORWARDING_HANDLER::sm_ProtocolConfig; + +FORWARDING_HANDLER::FORWARDING_HANDLER( + __in IHttpContext * pW3Context +) : m_Signature(FORWARDING_HANDLER_SIGNATURE), +m_cRefs(1), +m_dwHandlers(1), +m_pW3Context(pW3Context), +m_hRequest(NULL), +m_fResponseHeadersReceivedAndSet(FALSE), +m_fDoReverseRewriteHeaders(FALSE), +m_msStartTime(0), +m_BytesToReceive(0), +m_BytesToSend(0), +m_pEntityBuffer(NULL), +m_cchLastSend(0), +m_cEntityBuffers(0), +m_cBytesBuffered(0), +m_cMinBufferLimit(0), +m_pszOriginalHostHeader(NULL), +m_RequestStatus(FORWARDER_START), +m_pDisconnect(NULL), +m_pszHeaders(NULL), +m_cchHeaders(0), +m_fWebSocketEnabled(FALSE), +m_cContentLength(0), +m_pWebSocket(NULL), +m_pApplication(NULL), +m_pAppOfflineHtm(NULL), +m_fFinishRequest(FALSE), +m_fClientDisconnected(FALSE), +m_fHasError(FALSE), +m_fServerResetConn(FALSE), +m_fDoneAsyncCompletion(FALSE), +m_fHttpHandleInClose(FALSE), +m_fWebSocketHandleInClose(FALSE) +{ + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, + "FORWARDING_HANDLER --%p\n", this); + + InitializeSRWLock(&m_RequestLock); +} + +FORWARDING_HANDLER::~FORWARDING_HANDLER( + VOID +) +{ + // + // Destructor has started. + // + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, + "~FORWARDING_HANDLER --%p\n", this); + + m_Signature = FORWARDING_HANDLER_SIGNATURE_FREE; + + // + // Disconnect notification cleanup would happen first, before + // the FORWARDING_HANDLER instance got removed from m_pSharedhandler list. + // The m_pServer cleanup would happen afterwards, since there may be a + // call pending from SHARED_HANDLER to FORWARDING_HANDLER::SetStatusAndHeaders() + // + DBG_ASSERT(m_pDisconnect == NULL); + + FreeResponseBuffers(); + + if (m_hRequest != NULL) + { + // m_hRequest should have already been closed and set to NULL + // if not, we cannot close it as it may callback and cause AV + // let's do our best job here + WinHttpSetStatusCallback(m_hRequest, + NULL, // Callback Function + WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS, + NULL); + WinHttpCloseHandle(m_hRequest); + m_hRequest = NULL; + } + + if(m_pApplication != NULL) + { + m_pApplication->DereferenceApplication(); + m_pApplication = NULL; + } + + if(m_pAppOfflineHtm != NULL) + { + m_pAppOfflineHtm->DereferenceAppOfflineHtm(); + m_pAppOfflineHtm = NULL; + } + + m_pW3Context = NULL; +} + +// static +void * FORWARDING_HANDLER::operator new(size_t) +{ + DBG_ASSERT(sm_pAlloc != NULL); + if (sm_pAlloc == NULL) + { + return NULL; + } + return sm_pAlloc->Alloc(); +} + +// static +void FORWARDING_HANDLER::operator delete(void * pMemory) +{ + DBG_ASSERT(sm_pAlloc != NULL); + if (sm_pAlloc != NULL) + { + sm_pAlloc->Free(pMemory); + } +} + +VOID +FORWARDING_HANDLER::ReferenceForwardingHandler( + VOID +) const +{ + LONG cRefs = InterlockedIncrement(&m_cRefs); + if (sm_pTraceLog != NULL) + { + WriteRefTraceLog(sm_pTraceLog, + cRefs, + this); + } +} + +VOID +FORWARDING_HANDLER::DereferenceForwardingHandler( + VOID +) const +{ + DBG_ASSERT(m_cRefs != 0); + + LONG cRefs = InterlockedDecrement(&m_cRefs); + if (cRefs == 0) + { + delete this; + } + + if (sm_pTraceLog != NULL) + { + WriteRefTraceLog(sm_pTraceLog, + cRefs, + this); + } +} + +HRESULT +FORWARDING_HANDLER::SetStatusAndHeaders( + PCSTR pszHeaders, + DWORD +) +{ + HRESULT hr; + IHttpResponse * pResponse = m_pW3Context->GetResponse(); + IHttpRequest * pRequest = m_pW3Context->GetRequest(); + STACK_STRA(strHeaderName, 128); + STACK_STRA(strHeaderValue, 2048); + DWORD index = 0; + PSTR pchNewline; + PCSTR pchEndofHeaderValue; + BOOL fServerHeaderPresent = FALSE; + + _ASSERT(pszHeaders != NULL); + + // + // The first line is the status line + // + PSTR pchStatus = const_cast(strchr(pszHeaders, ' ')); + if (pchStatus == NULL) + { + return HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER); + } + while (*pchStatus == ' ') + { + pchStatus++; + } + USHORT uStatus = static_cast(atoi(pchStatus)); + + if (m_fWebSocketEnabled && uStatus != 101) + { + // + // Expected 101 response. + // + + m_fWebSocketEnabled = FALSE; + } + + pchStatus = strchr(pchStatus, ' '); + if (pchStatus == NULL) + { + return HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER); + } + while (*pchStatus == ' ') + { + pchStatus++; + } + if (*pchStatus == '\r' || *pchStatus == '\n') + { + pchStatus--; + } + + pchNewline = strchr(pchStatus, '\n'); + if (pchNewline == NULL) + { + return HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER); + } + + if (uStatus != 200) + { + // + // Skip over any spaces before the '\n' + // + for (pchEndofHeaderValue = pchNewline - 1; + (pchEndofHeaderValue > pchStatus) && + ((*pchEndofHeaderValue == ' ') || + (*pchEndofHeaderValue == '\r')); + pchEndofHeaderValue--) + {} + + // + // Copy the status description + // + if (FAILED(hr = strHeaderValue.Copy( + pchStatus, + (DWORD)(pchEndofHeaderValue - pchStatus) + 1)) || + FAILED(hr = pResponse->SetStatus(uStatus, + strHeaderValue.QueryStr(), + 0, + S_OK, + NULL, + TRUE))) + { + return hr; + } + } + + for (index = static_cast(pchNewline - pszHeaders) + 1; + pszHeaders[index] != '\r' && pszHeaders[index] != '\n' && pszHeaders[index] != '\0'; + index = static_cast(pchNewline - pszHeaders) + 1) + { + // + // Find the ':' in Header : Value\r\n + // + PCSTR pchColon = strchr(pszHeaders + index, ':'); + + // + // Find the '\n' in Header : Value\r\n + // + pchNewline = const_cast(strchr(pszHeaders + index, '\n')); + + if (pchNewline == NULL) + { + return HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER); + } + + // + // Take care of header continuation + // + while (pchNewline[1] == ' ' || + pchNewline[1] == '\t') + { + pchNewline = strchr(pchNewline + 1, '\n'); + } + + DBG_ASSERT( + (pchColon != NULL) && (pchColon < pchNewline)); + if ((pchColon == NULL) || (pchColon >= pchNewline)) + { + return HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER); + } + + // + // Skip over any spaces before the ':' + // + PCSTR pchEndofHeaderName; + for (pchEndofHeaderName = pchColon - 1; + (pchEndofHeaderName >= pszHeaders + index) && + (*pchEndofHeaderName == ' '); + pchEndofHeaderName--) + {} + + pchEndofHeaderName++; + + // + // Copy the header name + // + if (FAILED(hr = strHeaderName.Copy( + pszHeaders + index, + (DWORD)(pchEndofHeaderName - pszHeaders) - index))) + { + return hr; + } + + // + // Skip over the ':' and any trailing spaces + // + for (index = static_cast(pchColon - pszHeaders) + 1; + pszHeaders[index] == ' '; + index++) + {} + + + // + // Skip over any spaces before the '\n' + // + for (pchEndofHeaderValue = pchNewline - 1; + (pchEndofHeaderValue >= pszHeaders + index) && + ((*pchEndofHeaderValue == ' ') || + (*pchEndofHeaderValue == '\r')); + pchEndofHeaderValue--) + {} + + pchEndofHeaderValue++; + + // + // Copy the header value + // + if (pchEndofHeaderValue == pszHeaders + index) + { + strHeaderValue.Reset(); + } + else if (FAILED(hr = strHeaderValue.Copy( + pszHeaders + index, + (DWORD)(pchEndofHeaderValue - pszHeaders) - index))) + { + return hr; + } + + // + // Do not pass the transfer-encoding:chunked, Connection, Date or + // Server headers along + // + DWORD headerIndex = g_pResponseHeaderHash->GetIndex(strHeaderName.QueryStr()); + if (headerIndex == UNKNOWN_INDEX) + { + hr = pResponse->SetHeader(strHeaderName.QueryStr(), + strHeaderValue.QueryStr(), + static_cast(strHeaderValue.QueryCCH()), + FALSE); // fReplace + } + else + { + switch (headerIndex) + { + case HttpHeaderTransferEncoding: + if (!strHeaderValue.Equals("chunked", TRUE)) + { + break; + } + __fallthrough; + case HttpHeaderConnection: + case HttpHeaderDate: + continue; + + case HttpHeaderServer: + fServerHeaderPresent = TRUE; + break; + + case HttpHeaderContentLength: + if (pRequest->GetRawHttpRequest()->Verb != HttpVerbHEAD) + { + m_cContentLength = _atoi64(strHeaderValue.QueryStr()); + } + break; + } + + hr = pResponse->SetHeader(static_cast(headerIndex), + strHeaderValue.QueryStr(), + static_cast(strHeaderValue.QueryCCH()), + TRUE); // fReplace + } + if (FAILED(hr)) + { + return hr; + } + } + + // + // Explicitly remove the Server header if the back-end didn't set one. + // + + if (!fServerHeaderPresent) + { + pResponse->DeleteHeader("Server"); + } + + if (m_fDoReverseRewriteHeaders) + { + hr = DoReverseRewrite(pResponse); + if (FAILED(hr)) + { + return hr; + } + } + + m_fResponseHeadersReceivedAndSet = TRUE; + + return S_OK; +} + +HRESULT +FORWARDING_HANDLER::DoReverseRewrite( + __in IHttpResponse *pResponse +) +{ + DBG_ASSERT(pResponse == m_pW3Context->GetResponse()); + BOOL fSecure = (m_pW3Context->GetRequest()->GetRawHttpRequest()->pSslInfo != NULL); + STRA strTemp; + PCSTR pszHeader; + PCSTR pszStartHost; + PCSTR pszEndHost; + HTTP_RESPONSE_HEADERS *pHeaders; + HRESULT hr; + + // + // Content-Location and Location are easy, one known header in + // http[s]://host/url format + // + pszHeader = pResponse->GetHeader(HttpHeaderContentLocation); + if (pszHeader != NULL) + { + if (_strnicmp(pszHeader, "http://", 7) == 0) + { + pszStartHost = pszHeader + 7; + } + else if (_strnicmp(pszHeader, "https://", 8) == 0) + { + pszStartHost = pszHeader + 8; + } + else + { + goto Location; + } + + pszEndHost = strchr(pszStartHost, '/'); + + if (FAILED(hr = strTemp.Copy(fSecure ? "https://" : "http://")) || + FAILED(hr = strTemp.Append(m_pszOriginalHostHeader))) + { + return hr; + } + if (pszEndHost != NULL && + FAILED(hr = strTemp.Append(pszEndHost))) + { + return hr; + } + if (FAILED(hr = pResponse->SetHeader(HttpHeaderContentLocation, + strTemp.QueryStr(), + static_cast(strTemp.QueryCCH()), + TRUE))) + { + return hr; + } + } + +Location: + + pszHeader = pResponse->GetHeader(HttpHeaderLocation); + if (pszHeader != NULL) + { + if (_strnicmp(pszHeader, "http://", 7) == 0) + { + pszStartHost = pszHeader + 7; + } + else if (_strnicmp(pszHeader, "https://", 8) == 0) + { + pszStartHost = pszHeader + 8; + } + else + { + goto SetCookie; + } + + pszEndHost = strchr(pszStartHost, '/'); + + if (FAILED(hr = strTemp.Copy(fSecure ? "https://" : "http://")) || + FAILED(hr = strTemp.Append(m_pszOriginalHostHeader))) + { + return hr; + } + if (pszEndHost != NULL && + FAILED(hr = strTemp.Append(pszEndHost))) + { + return hr; + } + if (FAILED(hr = pResponse->SetHeader(HttpHeaderLocation, + strTemp.QueryStr(), + static_cast(strTemp.QueryCCH()), + TRUE))) + { + return hr; + } + } + +SetCookie: + + // + // Set-Cookie is different - possibly multiple unknown headers with + // syntax name=value ; ... ; Domain=.host ; ... + // + pHeaders = &pResponse->GetRawHttpResponse()->Headers; + for (DWORD i = 0; iUnknownHeaderCount; i++) + { + if (_stricmp(pHeaders->pUnknownHeaders[i].pName, "Set-Cookie") != 0) + { + continue; + } + + pszHeader = pHeaders->pUnknownHeaders[i].pRawValue; + pszStartHost = strchr(pszHeader, ';'); + while (pszStartHost != NULL) + { + pszStartHost++; + while (IsSpace(*pszStartHost)) + { + pszStartHost++; + } + + if (_strnicmp(pszStartHost, "Domain", 6) != 0) + { + pszStartHost = strchr(pszStartHost, ';'); + continue; + } + pszStartHost += 6; + + while (IsSpace(*pszStartHost)) + { + pszStartHost++; + } + if (*pszStartHost != '=') + { + break; + } + pszStartHost++; + while (IsSpace(*pszStartHost)) + { + pszStartHost++; + } + if (*pszStartHost == '.') + { + pszStartHost++; + } + pszEndHost = pszStartHost; + while (!IsSpace(*pszEndHost) && + *pszEndHost != ';' && + *pszEndHost != '\0') + { + pszEndHost++; + } + + if (FAILED(hr = strTemp.Copy(pszHeader, static_cast(pszStartHost - pszHeader))) || + FAILED(hr = strTemp.Append(m_pszOriginalHostHeader)) || + FAILED(hr = strTemp.Append(pszEndHost))) + { + return hr; + } + + pszHeader = (PCSTR)m_pW3Context->AllocateRequestMemory(strTemp.QueryCCH() + 1); + if (pszHeader == NULL) + { + return E_OUTOFMEMORY; + } + StringCchCopyA(const_cast(pszHeader), strTemp.QueryCCH() + 1, strTemp.QueryStr()); + pHeaders->pUnknownHeaders[i].pRawValue = pszHeader; + pHeaders->pUnknownHeaders[i].RawValueLength = static_cast(strTemp.QueryCCH()); + + break; + } + } + + return S_OK; +} + +HRESULT +FORWARDING_HANDLER::GetHeaders( + const PROTOCOL_CONFIG * pProtocol, + PCWSTR * ppszHeaders, + DWORD * pcchHeaders, + ASPNETCORE_CONFIG* pAspNetCoreConfig, + SERVER_PROCESS* pServerProcess +) +{ + + HRESULT hr = S_OK; + PCSTR pszCurrentHeader; + PCSTR ppHeadersToBeRemoved; + PCSTR pszFinalHeader; + USHORT cchCurrentHeader; + DWORD cchFinalHeader; + BOOL fSecure = FALSE; // dummy. Used in SplitUrl. Value will not be used + // as ANCM always use http protocol to communicate with backend + STRU struDestination; + STRU struUrl; + STACK_STRA(strTemp, 64); + HTTP_REQUEST_HEADERS *pHeaders; + IHttpRequest *pRequest = m_pW3Context->GetRequest(); + MULTISZA mszMsAspNetCoreHeaders; + + // + // We historically set the host section in request url to the new host header + // this is wrong but Kestrel has dependency on it. + // should change it in the future + // + if (!pProtocol->QueryPreserveHostHeader()) + { + if (FAILED(hr = PATH::SplitUrl(pRequest->GetRawHttpRequest()->CookedUrl.pFullUrl, + &fSecure, + &struDestination, + &struUrl)) || + FAILED(hr = strTemp.CopyW(struDestination.QueryStr())) || + FAILED(hr = pRequest->SetHeader(HttpHeaderHost, + strTemp.QueryStr(), + static_cast(strTemp.QueryCCH()), + TRUE))) // fReplace + { + return hr; + } + } + // + // Strip all headers starting with MS-ASPNETCORE. + // These headers are generated by the asp.net core module and + // passed to the process it creates. + // + + pHeaders = &m_pW3Context->GetRequest()->GetRawHttpRequest()->Headers; + for (DWORD i = 0; iUnknownHeaderCount; i++) + { + if (_strnicmp(pHeaders->pUnknownHeaders[i].pName, "MS-ASPNETCORE", 13) == 0) + { + mszMsAspNetCoreHeaders.Append(pHeaders->pUnknownHeaders[i].pName, (DWORD)pHeaders->pUnknownHeaders[i].NameLength); + } + } + + ppHeadersToBeRemoved = mszMsAspNetCoreHeaders.First(); + + // + // iterate the list of headers to be removed and delete them from the request. + // + + while (ppHeadersToBeRemoved != NULL) + { + m_pW3Context->GetRequest()->DeleteHeader(ppHeadersToBeRemoved); + ppHeadersToBeRemoved = mszMsAspNetCoreHeaders.Next(ppHeadersToBeRemoved); + } + + if (pServerProcess->QueryGuid() != NULL) + { + hr = m_pW3Context->GetRequest()->SetHeader("MS-ASPNETCORE-TOKEN", + pServerProcess->QueryGuid(), + (USHORT)strlen(pServerProcess->QueryGuid()), + TRUE); + if (FAILED(hr)) + { + return hr; + } + } + + if (pAspNetCoreConfig->QueryForwardWindowsAuthToken() && + (_wcsicmp(m_pW3Context->GetUser()->GetAuthenticationType(), L"negotiate") == 0 || + _wcsicmp(m_pW3Context->GetUser()->GetAuthenticationType(), L"ntlm") == 0)) + { + if (m_pW3Context->GetUser()->GetPrimaryToken() != NULL && + m_pW3Context->GetUser()->GetPrimaryToken() != INVALID_HANDLE_VALUE) + { + HANDLE hTargetTokenHandle = NULL; + hr = pServerProcess->SetWindowsAuthToken(m_pW3Context->GetUser()->GetPrimaryToken(), + &hTargetTokenHandle); + if (FAILED(hr)) + { + return hr; + } + + // + // set request header with target token value + // + CHAR pszHandleStr[16] = { 0 }; + if (_ui64toa_s((UINT64)hTargetTokenHandle, pszHandleStr, 16, 16) != 0) + { + hr = HRESULT_FROM_WIN32(ERROR_INVALID_DATA); + return hr; + } + + hr = m_pW3Context->GetRequest()->SetHeader("MS-ASPNETCORE-WINAUTHTOKEN", + pszHandleStr, + (USHORT)strlen(pszHandleStr), + TRUE); + if (FAILED(hr)) + { + return hr; + } + } + } + + if (!pProtocol->QueryXForwardedForName()->IsEmpty()) + { + strTemp.Reset(); + + pszCurrentHeader = pRequest->GetHeader(pProtocol->QueryXForwardedForName()->QueryStr(), &cchCurrentHeader); + if (pszCurrentHeader != NULL) + { + if (FAILED(hr = strTemp.Copy(pszCurrentHeader, cchCurrentHeader)) || + FAILED(hr = strTemp.Append(", ", 2))) + { + return hr; + } + } + + if (FAILED(hr = m_pW3Context->GetServerVariable("REMOTE_ADDR", + &pszFinalHeader, + &cchFinalHeader))) + { + return hr; + } + + if (pRequest->GetRawHttpRequest()->Address.pRemoteAddress->sa_family == AF_INET6) + { + if (FAILED(hr = strTemp.Append("[", 1)) || + FAILED(hr = strTemp.Append(pszFinalHeader, cchFinalHeader)) || + FAILED(hr = strTemp.Append("]", 1))) + { + return hr; + } + } + else + { + if (FAILED(hr = strTemp.Append(pszFinalHeader, cchFinalHeader))) + { + return hr; + } + } + + if (pProtocol->QueryIncludePortInXForwardedFor()) + { + if (FAILED(hr = m_pW3Context->GetServerVariable("REMOTE_PORT", + &pszFinalHeader, + &cchFinalHeader))) + { + return hr; + } + + if (FAILED(hr = strTemp.Append(":", 1)) || + FAILED(hr = strTemp.Append(pszFinalHeader, cchFinalHeader))) + { + return hr; + } + } + + if (FAILED(hr = pRequest->SetHeader(pProtocol->QueryXForwardedForName()->QueryStr(), + strTemp.QueryStr(), + static_cast(strTemp.QueryCCH()), + TRUE))) // fReplace + { + return hr; + } + } + + if (!pProtocol->QuerySslHeaderName()->IsEmpty()) + { + const HTTP_SSL_INFO *pSslInfo = pRequest->GetRawHttpRequest()->pSslInfo; + LPSTR pszScheme = "http"; + if (pSslInfo != NULL) + { + pszScheme = "https"; + } + + strTemp.Reset(); + + pszCurrentHeader = pRequest->GetHeader(pProtocol->QuerySslHeaderName()->QueryStr(), &cchCurrentHeader); + if (pszCurrentHeader != NULL) + { + if (FAILED(hr = strTemp.Copy(pszCurrentHeader, cchCurrentHeader)) || + FAILED(hr = strTemp.Append(", ", 2))) + { + return hr; + } + } + + if (FAILED(hr = strTemp.Append(pszScheme))) + { + return hr; + } + + if (FAILED(pRequest->SetHeader(pProtocol->QuerySslHeaderName()->QueryStr(), + strTemp.QueryStr(), + (USHORT)strTemp.QueryCCH(), + TRUE))) + { + return hr; + } + } + + if (!pProtocol->QueryClientCertName()->IsEmpty()) + { + if (pRequest->GetRawHttpRequest()->pSslInfo == NULL || + pRequest->GetRawHttpRequest()->pSslInfo->pClientCertInfo == NULL) + { + pRequest->DeleteHeader(pProtocol->QueryClientCertName()->QueryStr()); + } + else + { + // Resize the buffer large enough to hold the encoded certificate info + if (FAILED(hr = strTemp.Resize( + 1 + (pRequest->GetRawHttpRequest()->pSslInfo->pClientCertInfo->CertEncodedSize + 2) / 3 * 4))) + { + return hr; + } + + Base64Encode( + pRequest->GetRawHttpRequest()->pSslInfo->pClientCertInfo->pCertEncoded, + pRequest->GetRawHttpRequest()->pSslInfo->pClientCertInfo->CertEncodedSize, + strTemp.QueryStr(), + strTemp.QuerySize(), + NULL); + strTemp.SyncWithBuffer(); + + if (FAILED(hr = pRequest->SetHeader( + pProtocol->QueryClientCertName()->QueryStr(), + strTemp.QueryStr(), + static_cast(strTemp.QueryCCH()), + TRUE))) // fReplace + { + return hr; + } + } + } + + // + // Remove the connection header + // + if (!m_fWebSocketEnabled) + { + pRequest->DeleteHeader(HttpHeaderConnection); + } + + // + // Get all the headers to send to the client + // + hr = m_pW3Context->GetServerVariable("ALL_RAW", + ppszHeaders, + pcchHeaders); + if (FAILED(hr)) + { + return hr; + } + + return S_OK; +} + +HRESULT +FORWARDING_HANDLER::CreateWinHttpRequest( + __in const IHttpRequest * pRequest, + __in const PROTOCOL_CONFIG * pProtocol, + __in HINTERNET hConnect, + __inout STRU * pstrUrl, + ASPNETCORE_CONFIG* pAspNetCoreConfig, + SERVER_PROCESS* pServerProcess +) +{ + HRESULT hr = S_OK; + PCWSTR pszVersion = NULL; + PCSTR pszVerb; + DWORD dwTimeout = INFINITE; + STACK_STRU(strVerb, 32); + + // + // Create the request handle for this request (leave some fields blank, + // we will fill them when sending the request) + // + pszVerb = pRequest->GetHttpMethod(); + if (FAILED(hr = strVerb.CopyA(pszVerb))) + { + goto Finished; + } + + //pszVersion = pProtocol->QueryVersion(); + if (pszVersion == NULL) + { + DWORD cchUnused; + hr = m_pW3Context->GetServerVariable( + "HTTP_VERSION", + &pszVersion, + &cchUnused); + if (FAILED(hr)) + { + goto Finished; + } + } + + m_hRequest = WinHttpOpenRequest(hConnect, + strVerb.QueryStr(), + pstrUrl->QueryStr(), + pszVersion, + WINHTTP_NO_REFERER, + WINHTTP_DEFAULT_ACCEPT_TYPES, + WINHTTP_FLAG_ESCAPE_DISABLE_QUERY + | g_OptionalWinHttpFlags); + if (m_hRequest == NULL) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + if (!pServerProcess->IsDebuggerAttached()) + { + dwTimeout = pProtocol->QueryTimeout(); + } + + if (!WinHttpSetTimeouts(m_hRequest, + dwTimeout, //resolve timeout + dwTimeout, // connect timeout + dwTimeout, // send timeout + dwTimeout)) // receive timeout + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + DWORD dwResponseBufferLimit = pProtocol->QueryResponseBufferLimit(); + if (!WinHttpSetOption(m_hRequest, + WINHTTP_OPTION_MAX_RESPONSE_DRAIN_SIZE, + &dwResponseBufferLimit, + sizeof(dwResponseBufferLimit))) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + DWORD dwMaxHeaderSize = pProtocol->QueryMaxResponseHeaderSize(); + if (!WinHttpSetOption(m_hRequest, + WINHTTP_OPTION_MAX_RESPONSE_HEADER_SIZE, + &dwMaxHeaderSize, + sizeof(dwMaxHeaderSize))) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + DWORD dwOption = WINHTTP_DISABLE_COOKIES; + + dwOption |= WINHTTP_DISABLE_AUTHENTICATION; + + if (!pProtocol->QueryDoKeepAlive()) + { + dwOption |= WINHTTP_DISABLE_KEEP_ALIVE; + } + if (!WinHttpSetOption(m_hRequest, + WINHTTP_OPTION_DISABLE_FEATURE, + &dwOption, + sizeof(dwOption))) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + if (WinHttpSetStatusCallback(m_hRequest, + FORWARDING_HANDLER::OnWinHttpCompletion, + (WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS | + WINHTTP_CALLBACK_FLAG_HANDLES | + WINHTTP_CALLBACK_STATUS_SENDING_REQUEST), + NULL) == WINHTTP_INVALID_STATUS_CALLBACK) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + hr = GetHeaders(pProtocol, + &m_pszHeaders, + &m_cchHeaders, + pAspNetCoreConfig, + pServerProcess); + if (FAILED(hr)) + { + goto Finished; + } + +Finished: + + return hr; +} + +REQUEST_NOTIFICATION_STATUS +FORWARDING_HANDLER::OnExecuteRequestHandler( + VOID +) +{ + REQUEST_NOTIFICATION_STATUS retVal = RQ_NOTIFICATION_CONTINUE; + HRESULT hr = S_OK; + ASPNETCORE_CONFIG *pAspNetCoreConfig = NULL; + FORWARDER_CONNECTION *pConnection = NULL; + STACK_STRU(strDestination, 32); + STACK_STRU(strUrl, 2048); + STACK_STRU(struEscapedUrl, 2048); + STACK_STRU(strDescription, 128); + HINTERNET hConnect = NULL; + IHttpRequest *pRequest = m_pW3Context->GetRequest(); + IHttpResponse *pResponse = m_pW3Context->GetResponse(); + PROTOCOL_CONFIG *pProtocol = &sm_ProtocolConfig; + APPLICATION_MANAGER *pApplicationManager = NULL; + SERVER_PROCESS *pServerProcess = NULL; + USHORT cchHostName = 0; + BOOL fSecure = FALSE; + BOOL fProcessStartFailure = FALSE; + HTTP_DATA_CHUNK *pDataChunk = NULL; + IHttpConnection *pClientConnection = NULL; + + DBG_ASSERT(m_RequestStatus == FORWARDER_START); + + // + // Take a reference so that object does not go away as a result of + // async completion. + // + ReferenceForwardingHandler(); + + m_pszOriginalHostHeader = pRequest->GetHeader(HttpHeaderHost, &cchHostName); + + // read per site aspNetCore configuration. + hr = ASPNETCORE_CONFIG::GetConfig(m_pW3Context, &pAspNetCoreConfig); + if (FAILED(hr)) + { + // configuration error. + goto Failure; + } + + // override Protocol related config from aspNetCore config + pProtocol->OverrideConfig(pAspNetCoreConfig); + + // + // parse original url + // + if (FAILED(hr = PATH::SplitUrl(pRequest->GetRawHttpRequest()->CookedUrl.pFullUrl, + &fSecure, + &strDestination, + &strUrl))) + { + goto Failure; + } + + if (FAILED(hr = PATH::EscapeAbsPath(pRequest, &struEscapedUrl))) + { + goto Failure; + } + + m_fDoReverseRewriteHeaders = pProtocol->QueryReverseRewriteHeaders(); + + m_cMinBufferLimit = pProtocol->QueryMinResponseBuffer(); + + pClientConnection = m_pW3Context->GetConnection(); + if (pClientConnection == NULL || + !pClientConnection->IsConnected()) + { + hr = HRESULT_FROM_WIN32(WSAECONNRESET); + goto Failure; + } + + // + // Find the application that is supposed to service this request. + // + pApplicationManager = APPLICATION_MANAGER::GetInstance(); + if (pApplicationManager == NULL) + { + hr = E_OUTOFMEMORY; + goto Failure; + } + + hr = pApplicationManager->GetApplication(m_pW3Context, + &m_pApplication); + if (FAILED(hr)) + { + goto Failure; + } + + m_pAppOfflineHtm = m_pApplication->QueryAppOfflineHtm(); + if (m_pAppOfflineHtm != NULL) + { + m_pAppOfflineHtm->ReferenceAppOfflineHtm(); + } + + if (m_pApplication->AppOfflineFound() && m_pAppOfflineHtm != NULL) + { + HTTP_DATA_CHUNK DataChunk; + + DataChunk.DataChunkType = HttpDataChunkFromMemory; + DataChunk.FromMemory.pBuffer = (PVOID)m_pAppOfflineHtm->m_Contents.QueryStr(); + DataChunk.FromMemory.BufferLength = m_pAppOfflineHtm->m_Contents.QueryCB(); + + pResponse->SetStatus(503, "Service Unavailable", 0, hr, NULL, TRUE); + pResponse->SetHeader("Content-Type", + "text/html", + (USHORT)strlen("text/html"), + FALSE + ); // no need to check return hresult + + pResponse->WriteEntityChunkByReference(&DataChunk); + goto Finished; + } + + hr = m_pApplication->GetProcess(m_pW3Context, + pAspNetCoreConfig, + &pServerProcess); + if (FAILED(hr)) + { + fProcessStartFailure = TRUE; + goto Failure; + } + + if (pServerProcess == NULL) + { + hr = HRESULT_FROM_WIN32(ERROR_CREATE_FAILED); + goto Failure; + } + + if (pServerProcess->QueryWinHttpConnection() == NULL) + { + hr = HRESULT_FROM_WIN32(ERROR_INVALID_HANDLE); + goto Failure; + } + + hConnect = pServerProcess->QueryWinHttpConnection()->QueryHandle(); + + // + // Mark request as websocket if upgrade header is present. + // + + if (g_fWebSocketSupported) + { + USHORT cchHeader = 0; + PCSTR pszWebSocketHeader = pRequest->GetHeader("Upgrade", &cchHeader); + + if (cchHeader == 9 && _stricmp(pszWebSocketHeader, "websocket") == 0) + { + m_fWebSocketEnabled = TRUE; + } + } + + hr = CreateWinHttpRequest(pRequest, + pProtocol, + hConnect, + &struEscapedUrl, + pAspNetCoreConfig, + pServerProcess); + + if (FAILED(hr)) + { + goto Failure; + } + + // + // Register for connection disconnect notification with http.sys. + // + if (g_fAsyncDisconnectAvailable) + { + m_pDisconnect = static_cast( + pClientConnection->GetModuleContextContainer()-> + GetConnectionModuleContext(g_pModuleId)); + if (m_pDisconnect == NULL) + { + m_pDisconnect = new ASYNC_DISCONNECT_CONTEXT; + if (m_pDisconnect == NULL) + { + hr = E_OUTOFMEMORY; + goto Failure; + } + + hr = pClientConnection->GetModuleContextContainer()-> + SetConnectionModuleContext(m_pDisconnect, + g_pModuleId); + DBG_ASSERT(hr != HRESULT_FROM_WIN32(ERROR_ALREADY_ASSIGNED)); + if (FAILED(hr)) + { + m_pDisconnect->CleanupStoredContext(); + m_pDisconnect = NULL; + goto Failure; + } + } + + // + // Issue: There is a window of opportunity to miss on the disconnect + // notification if it happens before the SetHandler() call is made. + // It is suboptimal for performance, but should functionally be OK. + // + + m_pDisconnect->SetHandler(this); + } + + if (m_hRequest == NULL) + { + hr = HRESULT_FROM_WIN32(WSAECONNRESET); + goto Failure; + } + + // + // Begins normal request handling. Send request to server. + // + + m_RequestStatus = FORWARDER_SENDING_REQUEST; + + // + // Calculate the bytes to receive from the content length. + // + + DWORD cbContentLength = 0; + PCSTR pszContentLength = pRequest->GetHeader(HttpHeaderContentLength); + if (pszContentLength != NULL) + { + cbContentLength = m_BytesToReceive = atol(pszContentLength); + if (m_BytesToReceive == INFINITE) + { + hr = HRESULT_FROM_WIN32(WSAECONNRESET); + goto Failure; + } + } + else if (pRequest->GetHeader(HttpHeaderTransferEncoding) != NULL) + { + m_BytesToReceive = INFINITE; + } + + if (m_fWebSocketEnabled) + { + // + // Set the upgrade flag for a websocket request. + // + + if (!WinHttpSetOption(m_hRequest, + WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET, + NULL, + 0)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + } + + m_cchLastSend = m_cchHeaders; + + if (!WinHttpSendRequest(m_hRequest, + m_pszHeaders, + m_cchHeaders, + NULL, + 0, + cbContentLength, + reinterpret_cast(static_cast(this)))) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, + "FORWARDING_HANDLER::OnExecuteRequestHandler, Send request failed"); + goto Failure; + } + + // + // Async WinHTTP operation is in progress. Release this thread meanwhile, + // OnWinHttpCompletion method should resume the work by posting an IIS completion. + // + retVal = RQ_NOTIFICATION_PENDING; + goto Finished; + +Failure: + + // + // Reset status for consistency. + // + m_RequestStatus = FORWARDER_DONE; + m_fHasError = TRUE; + pResponse->DisableKernelCache(); + pResponse->GetRawHttpResponse()->EntityChunkCount = 0; + // + // Finish the request on failure. + // + retVal = RQ_NOTIFICATION_FINISH_REQUEST; + + if (hr == HRESULT_FROM_WIN32(WSAECONNRESET)) + { + pResponse->SetStatus(400, "Bad Request", 0, hr); + goto Finished; + } + else if (fProcessStartFailure && !pAspNetCoreConfig->QueryDisableStartUpErrorPage()) + { + pResponse->SetStatus(502, "Bad Gateway", 5, hr, NULL, TRUE); + pResponse->SetHeader("Content-Type", + "text/html", + (USHORT)strlen("text/html"), + FALSE + ); + + if (SUCCEEDED(pApplicationManager->Get502ErrorPage(&pDataChunk))) + { + pResponse->WriteEntityChunkByReference(pDataChunk); + goto Finished; + } + } + + // + // default error behavior + // + pResponse->SetStatus(502, "Bad Gateway", 3, hr); + + if (hr > HRESULT_FROM_WIN32(WINHTTP_ERROR_BASE) && + hr <= HRESULT_FROM_WIN32(WINHTTP_ERROR_LAST)) + { +#pragma prefast (suppress : __WARNING_FUNCTION_NEEDS_REVIEW, "Function and parameters reviewed.") + FormatMessage( + FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_FROM_HMODULE, + g_hWinHttpModule, + HRESULT_CODE(hr), + 0, + strDescription.QueryStr(), + strDescription.QuerySizeCCH(), + NULL); + } + else + { + LoadString(g_hModule, + IDS_SERVER_ERROR, + strDescription.QueryStr(), + strDescription.QuerySizeCCH()); + } + + strDescription.SyncWithBuffer(); + if (strDescription.QueryCCH() != 0) + { + pResponse->SetErrorDescription( + strDescription.QueryStr(), + strDescription.QueryCCH(), + FALSE); + } + +Finished: + + if (pConnection != NULL) + { + pConnection->DereferenceForwarderConnection(); + pConnection = NULL; + } + + if (pServerProcess != NULL) + { + pServerProcess->DereferenceServerProcess(); + pServerProcess = NULL; + } + + if (retVal != RQ_NOTIFICATION_PENDING) + { + RemoveRequest(); + } + + DereferenceForwardingHandler(); + // + // Do not use this object after dereferencing it, it may be gone. + // + + return retVal; +} + +VOID +FORWARDING_HANDLER::RemoveRequest() +{ + ASYNC_DISCONNECT_CONTEXT * pDisconnect; + pDisconnect = (ASYNC_DISCONNECT_CONTEXT *) InterlockedExchangePointer((PVOID*)&m_pDisconnect, NULL); + if (pDisconnect != NULL) + { + pDisconnect->ResetHandler(); + pDisconnect = NULL; + } +} + +REQUEST_NOTIFICATION_STATUS +FORWARDING_HANDLER::OnAsyncCompletion( + DWORD cbCompletion, + HRESULT hrCompletionStatus +) +/*++ + +Routine Description: + +Handle the completion from IIS and continue the execution +of this request based on the current state. + +Arguments: + +cbCompletion - Number of bytes associated with this completion +dwCompletionStatus - the win32 status associated with this completion + +Return Value: + +REQUEST_NOTIFICATION_STATUS + +--*/ +{ + HRESULT hr = S_OK; + REQUEST_NOTIFICATION_STATUS retVal = RQ_NOTIFICATION_PENDING; + BOOL fLocked = FALSE; + BOOL fClientError = FALSE; + BOOL fClosed = FALSE; + BOOL fWebsocketUpgraded = FALSE; + +#ifdef DEBUG + FORWARDING_REQUEST_STATUS dwLocalStatus = m_RequestStatus; +#endif // DEBUG + + if (sm_pTraceLog != NULL) + { + WriteRefTraceLogEx(sm_pTraceLog, + m_cRefs, + this, + "FORWARDING_HANDLER::OnAsyncCompletion Enter", + reinterpret_cast(static_cast(cbCompletion)), + reinterpret_cast(static_cast(hrCompletionStatus))); + } + + DBG_ASSERT(m_pW3Context != NULL); + __analysis_assume(m_pW3Context != NULL); + + // + // Take a reference so that object does not go away as a result of + // async completion. + // + ReferenceForwardingHandler(); + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, + "FORWARDING_HANDLER::OnAsyncCompletion"); + + // + // OnAsyncCompletion can be called on a Winhttp io completion thread. + // Hence we need to check the TLS before we acquire the shared lock. + // + if (TlsGetValue(g_dwTlsIndex) != this) + { + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == NULL); + AcquireSRWLockExclusive(&m_RequestLock); + TlsSetValue(g_dwTlsIndex, this); + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == this); + + fLocked = TRUE; + } + + if (m_fClientDisconnected && (m_RequestStatus != FORWARDER_DONE) ) + { + hr = ERROR_CONNECTION_ABORTED; + goto Failure; + } + + if (m_RequestStatus == FORWARDER_RECEIVED_WEBSOCKET_RESPONSE) + { + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, + "FORWARDING_HANDLER::OnAsyncCompletion, Send completed for 101 response"); + // + // This should be the write completion of the 101 response. + // + m_pWebSocket = new WEBSOCKET_HANDLER(); + if (m_pWebSocket == NULL) + { + hr = E_OUTOFMEMORY; + goto Failure; + } + + hr = m_pWebSocket->ProcessRequest(this, m_pW3Context, m_hRequest, &fWebsocketUpgraded); + if (fWebsocketUpgraded) + { + // WinHttp WebSocket handle has been created, bump the counter so that remember to close it + // and prevent from premature postcomplation and unexpected callback from winhttp + InterlockedIncrement(&m_dwHandlers); + } + + if (FAILED(hr)) + { + // This failure could happen when client disconnect happens or backend server fails + // after websocket upgrade + goto Failure; + } + + // + // WebSocket upgrade is successful. Close the WinHttpRequest Handle + // + m_fHttpHandleInClose = TRUE; + fClosed = WinHttpCloseHandle(m_hRequest); + DBG_ASSERT(fClosed); + m_hRequest = NULL; + + if (!fClosed) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Failure; + } + retVal = RQ_NOTIFICATION_PENDING; + goto Finished; + } + + // + // Begins normal completion handling. There is already a shared acquired + // for protecting the WinHTTP request handle from being closed. + // + switch (m_RequestStatus) + { + case FORWARDER_RECEIVING_RESPONSE: + + // + // This is a completion of a write (send) to http.sys, abort in case of + // failure, if there is more data available from WinHTTP, read it + // or else ask if there is more. + // + if (FAILED(hrCompletionStatus)) + { + hr = hrCompletionStatus; + fClientError = TRUE; + goto Failure; + } + + hr = OnReceivingResponse(); + if (FAILED(hr)) + { + goto Failure; + } + break; + + case FORWARDER_SENDING_REQUEST: + + hr = OnSendingRequest(cbCompletion, + hrCompletionStatus, + &fClientError); + if (FAILED(hr)) + { + goto Failure; + } + break; + + default: + DBG_ASSERT(m_RequestStatus == FORWARDER_DONE); + if (m_hRequest == NULL && m_pWebSocket == NULL) + { + // Request must have been done + if (!m_fFinishRequest) + { + goto Failure; + } + + if (m_fHasError) + { + retVal = RQ_NOTIFICATION_FINISH_REQUEST; + } + else + { + retVal = RQ_NOTIFICATION_CONTINUE; + } + } + goto Finished; + } + + // + // Either OnReceivingResponse or OnSendingRequest initiated an + // async WinHTTP operation, release this thread meanwhile, + // OnWinHttpCompletion method should resume the work by posting an IIS completion. + // + retVal = RQ_NOTIFICATION_PENDING; + goto Finished; + +Failure: + + // + // Reset status for consistency. + // + m_RequestStatus = FORWARDER_DONE; + if (!m_fHasError) + { + m_fHasError = TRUE; + + // + // Do the right thing based on where the error originated from. + // + IHttpResponse *pResponse = m_pW3Context->GetResponse(); + pResponse->DisableKernelCache(); + pResponse->GetRawHttpResponse()->EntityChunkCount = 0; + + if (fClientError || m_fClientDisconnected) + { + if (!m_fResponseHeadersReceivedAndSet) + { + pResponse->SetStatus(400, "Bad Request", 0, HRESULT_FROM_WIN32(WSAECONNRESET)); + } + else + { + // + // Response headers from origin server were + // already received and set for the current response. + // Honor the response status. + // + } + } + else + { + STACK_STRU(strDescription, 128); + + pResponse->SetStatus(502, "Bad Gateway", 3, hr); + + if (hr > HRESULT_FROM_WIN32(WINHTTP_ERROR_BASE) && + hr <= HRESULT_FROM_WIN32(WINHTTP_ERROR_LAST)) + { +#pragma prefast (suppress : __WARNING_FUNCTION_NEEDS_REVIEW, "Function and parameters reviewed.") + FormatMessage( + FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_FROM_HMODULE, + g_hWinHttpModule, + HRESULT_CODE(hr), + 0, + strDescription.QueryStr(), + strDescription.QuerySizeCCH(), + NULL); + } + else + { + LoadString(g_hModule, + IDS_SERVER_ERROR, + strDescription.QueryStr(), + strDescription.QuerySizeCCH()); + } + (VOID)strDescription.SyncWithBuffer(); + +#ifdef DEBUG + // adding more info to error descrption to help us diagnose the issue + STACK_STRA(strMoreData, 128); + sprintf_s(strMoreData.QueryStr(), 100, "OnAsyncCompletion --%p--%p--%d--%d--%d--%d\n", this, m_pW3Context, GetCurrentThreadId(), (UINT)dwLocalStatus, m_fServerResetConn, m_fClientDisconnected); + strMoreData.SyncWithBuffer(); + strDescription.AppendA(strMoreData.QueryStr()); +#endif // DEBUG + + if (strDescription.QueryCCH() != 0) + { + pResponse->SetErrorDescription( + strDescription.QueryStr(), + strDescription.QueryCCH(), + FALSE); + } + + if (hr == HRESULT_FROM_WIN32(ERROR_WINHTTP_INVALID_SERVER_RESPONSE)) + { + if (!m_fServerResetConn) + { + RemoveRequest(); + pResponse->ResetConnection(); + m_fServerResetConn = TRUE; + } + } + } + } + + if (m_pWebSocket != NULL && !m_fWebSocketHandleInClose) + { + m_fWebSocketHandleInClose = TRUE; + m_pWebSocket->TerminateRequest(); + } + + if (m_hRequest != NULL && !m_fHttpHandleInClose) + { + m_fHttpHandleInClose = TRUE; + WinHttpCloseHandle(m_hRequest); + m_hRequest = NULL; + } + +Finished: + + if (fLocked) + { + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == this); + TlsSetValue(g_dwTlsIndex, NULL); + ReleaseSRWLockExclusive(&m_RequestLock); + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == NULL); + } + + if (retVal != RQ_NOTIFICATION_PENDING) + { +#ifdef DEBUG + DWORD counter = InterlockedIncrement(&g_dwLogCounter) % ASPNETCORE_DEBUG_STRU_ARRAY_SIZE; + g_strLogs[counter].Reset(); + sprintf_s(g_strLogs[counter].QueryStr(), ASPNETCORE_DEBUG_STRU_BUFFER_SIZE, + "OnAyncCompletion--%p--%d--%d--%d--%d\n", this, GetCurrentThreadId(), (UINT)dwLocalStatus, retVal, m_fDoneAsyncCompletion); + g_strLogs[counter].SyncWithBuffer(); +#endif // DEBUG + + DBG_ASSERT(m_dwHandlers == 0); + RemoveRequest(); + + // This is just a safety guard to prevent from returning non pending status no more once + // which should never happen + if (!m_fDoneAsyncCompletion) + { + m_fDoneAsyncCompletion = TRUE; + } + else + { + retVal = RQ_NOTIFICATION_PENDING; + } + } + + DereferenceForwardingHandler(); + // + // Do not use this object after dereferencing it, it may be gone. + // + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, + "FORWARDING_HANDLER::OnAsyncCompletion Done %d", retVal); + return retVal; +} + +HRESULT +FORWARDING_HANDLER::OnSendingRequest( + DWORD cbCompletion, + HRESULT hrCompletionStatus, + __out BOOL * pfClientError +) +{ + HRESULT hr = S_OK; + + // + // This is a completion for a read from http.sys, abort in case + // of failure, if we read anything write it out over WinHTTP, + // but we have already reached EOF, now read the response + // + if (hrCompletionStatus == HRESULT_FROM_WIN32(ERROR_HANDLE_EOF)) + { + DBG_ASSERT(m_BytesToReceive == 0 || m_BytesToReceive == INFINITE); + if (m_BytesToReceive == INFINITE) + { + m_BytesToReceive = 0; + m_cchLastSend = 5; // "0\r\n\r\n" + + // + // WinHttpWriteData can operate asynchronously. + // + // Take reference so that object does not go away as a result of + // async completion. + // + if (!WinHttpWriteData(m_hRequest, + "0\r\n\r\n", + 5, + NULL)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Failure; + } + } + else + { + m_RequestStatus = FORWARDER_RECEIVING_RESPONSE; + + // + // WinHttpReceiveResponse can operate asynchronously. + // + // Take reference so that object does not go away as a result of + // async completion. + // + if (!WinHttpReceiveResponse(m_hRequest, NULL)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Failure; + } + } + } + else if (SUCCEEDED(hrCompletionStatus)) + { + DWORD cbOffset; + + if (m_BytesToReceive != INFINITE) + { + m_BytesToReceive -= cbCompletion; + cbOffset = 6; + } + else + { + // + // For chunk-encoded requests, need to re-chunk the + // entity body + // + // Add the CRLF just before and after the chunk data + // + m_pEntityBuffer[4] = '\r'; + m_pEntityBuffer[5] = '\n'; + + m_pEntityBuffer[cbCompletion + 6] = '\r'; + m_pEntityBuffer[cbCompletion + 7] = '\n'; + + if (cbCompletion < 0x10) + { + cbOffset = 3; + m_pEntityBuffer[3] = HEX_TO_ASCII(cbCompletion); + cbCompletion += 5; + } + else if (cbCompletion < 0x100) + { + cbOffset = 2; + m_pEntityBuffer[2] = HEX_TO_ASCII(cbCompletion >> 4); + m_pEntityBuffer[3] = HEX_TO_ASCII(cbCompletion & 0xf); + cbCompletion += 6; + } + else if (cbCompletion < 0x1000) + { + cbOffset = 1; + m_pEntityBuffer[1] = HEX_TO_ASCII(cbCompletion >> 8); + m_pEntityBuffer[2] = HEX_TO_ASCII((cbCompletion >> 4) & 0xf); + m_pEntityBuffer[3] = HEX_TO_ASCII(cbCompletion & 0xf); + cbCompletion += 7; + } + else + { + DBG_ASSERT(cbCompletion < 0x10000); + + cbOffset = 0; + m_pEntityBuffer[0] = HEX_TO_ASCII(cbCompletion >> 12); + m_pEntityBuffer[1] = HEX_TO_ASCII((cbCompletion >> 8) & 0xf); + m_pEntityBuffer[2] = HEX_TO_ASCII((cbCompletion >> 4) & 0xf); + m_pEntityBuffer[3] = HEX_TO_ASCII(cbCompletion & 0xf); + cbCompletion += 8; + } + } + m_cchLastSend = cbCompletion; + + // + // WinHttpWriteData can operate asynchronously. + // + // Take reference so that object does not go away as a result of + // async completion. + // + if (!WinHttpWriteData(m_hRequest, + m_pEntityBuffer + cbOffset, + cbCompletion, + NULL)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Failure; + } + } + else + { + hr = hrCompletionStatus; + *pfClientError = TRUE; + goto Failure; + } + +Failure: + + return hr; +} + +HRESULT +FORWARDING_HANDLER::OnReceivingResponse( +) +{ + HRESULT hr = S_OK; + + if (m_cBytesBuffered >= m_cMinBufferLimit) + { + FreeResponseBuffers(); + } + + if (m_BytesToSend == 0) + { + // + // If response buffering is enabled, try to read large chunks + // at a time - also treat very small buffering limit as no + // buffering + // + m_BytesToSend = min(m_cMinBufferLimit, BUFFER_SIZE); + if (m_BytesToSend < BUFFER_SIZE / 2) + { + // + // Disable buffering. + // + m_BytesToSend = 0; + } + } + + if (m_BytesToSend == 0) + { + // + // No buffering enabled. + // + // WinHttpQueryDataAvailable can operate asynchronously. + // + if (!WinHttpQueryDataAvailable(m_hRequest, NULL)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Failure; + } + } + else + { + // + // Buffering enabled. + // + + if (m_pEntityBuffer == NULL) + { + m_pEntityBuffer = GetNewResponseBuffer(min(m_BytesToSend, BUFFER_SIZE)); + if (m_pEntityBuffer == NULL) + { + hr = E_OUTOFMEMORY; + goto Failure; + } + } + + // + // WinHttpReadData can operate asynchronously. + // + if (!WinHttpReadData(m_hRequest, + m_pEntityBuffer, + min(m_BytesToSend, BUFFER_SIZE), + NULL)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Failure; + } + } + +Failure: + + return hr; +} + +VOID +FORWARDING_HANDLER::OnWinHttpCompletionInternal( + HINTERNET hRequest, + DWORD dwInternetStatus, + LPVOID lpvStatusInformation, + DWORD dwStatusInformationLength +) +/*++ + +Routine Description: + +Completion call associated with a WinHTTP operation + +Arguments: + +hRequest - The winhttp request handle associated with this completion +dwInternetStatus - enum specifying what the completion is for +lpvStatusInformation - completion specific information +dwStatusInformationLength - length of the above information + +Return Value: + +None + +--*/ +{ + HRESULT hr = S_OK; + BOOL fExclusiveLock = FALSE; + BOOL fSharedLock = FALSE; + BOOL fDoPostCompletion = FALSE; + BOOL fClientError = FALSE; + BOOL fAnotherCompletionExpected = FALSE; + DWORD dwHandlers = 1; // defaullt for http handler + + DBG_ASSERT(m_pW3Context != NULL); + __analysis_assume(m_pW3Context != NULL); + IHttpResponse * pResponse = m_pW3Context->GetResponse(); + BOOL fEndRequest = FALSE; + + // Reference the request handler to prevent it from being released prematurely + ReferenceForwardingHandler(); + + UNREFERENCED_PARAMETER(dwStatusInformationLength); + + if (sm_pTraceLog != NULL) + { + WriteRefTraceLogEx(sm_pTraceLog, + m_cRefs, + this, + "FORWARDING_HANDLER::OnWinHttpCompletionInternal Enter", + reinterpret_cast(static_cast(dwInternetStatus)), + NULL); + } + + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, + "FORWARDING_HANDLER::OnWinHttpCompletionInternal %x --%p", + dwInternetStatus, this); + + // + // Exclusive lock on the winhttp handle to protect from a client disconnect/ + // server stop closing the handle while we are using it. + // + // WinHttp can call async completion on the same thread/stack, so + // we have to account for that and not try to take the lock again, + // otherwise, we could end up in a deadlock. + // + + fEndRequest = (dwInternetStatus == WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING); + + if (TlsGetValue(g_dwTlsIndex) != this) + { + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == NULL); + if (m_RequestStatus != FORWARDER_RECEIVED_WEBSOCKET_RESPONSE) + { + // Webscoket has already been guarded by critical section + // Only require exclisive lock for non-websocket scenario which has duplex channel + // Otherwise, there will be a deadlock + AcquireSRWLockExclusive(&m_RequestLock); + TlsSetValue(g_dwTlsIndex, this); + fExclusiveLock = TRUE; + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == this); + } + else + { + AcquireSRWLockShared(&m_RequestLock); + TlsSetValue(g_dwTlsIndex, this); + fSharedLock = TRUE; + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == this); + } + } + + if (fEndRequest) + { + dwHandlers = InterlockedDecrement(&m_dwHandlers); + } + + if (m_fFinishRequest) + { + // Request was done by another thread, skip + goto Finished; + } + + if(m_fClientDisconnected && (m_RequestStatus != FORWARDER_DONE)) + { + hr = ERROR_CONNECTION_ABORTED; + goto Failure; + } + + if (m_RequestStatus == FORWARDER_RECEIVED_WEBSOCKET_RESPONSE) + { + fAnotherCompletionExpected = TRUE; + + if(m_pWebSocket == NULL) + { + goto Finished; + } + + switch (dwInternetStatus) + { + case WINHTTP_CALLBACK_STATUS_SHUTDOWN_COMPLETE: + m_pWebSocket->OnWinHttpShutdownComplete(); + break; + + case WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE: + m_pWebSocket->OnWinHttpSendComplete( + (WINHTTP_WEB_SOCKET_STATUS*)lpvStatusInformation + ); + break; + + case WINHTTP_CALLBACK_STATUS_READ_COMPLETE: + m_pWebSocket->OnWinHttpReceiveComplete( + (WINHTTP_WEB_SOCKET_STATUS*)lpvStatusInformation + ); + break; + + case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR: + m_pWebSocket->OnWinHttpIoError( + (WINHTTP_WEB_SOCKET_ASYNC_RESULT*)lpvStatusInformation + ); + break; + } + goto Finished; + } + + switch (dwInternetStatus) + { + case WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE: + case WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE: + hr = OnWinHttpCompletionSendRequestOrWriteComplete(hRequest, + dwInternetStatus, + &fClientError, + &fAnotherCompletionExpected); + break; + + case WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE: + hr = OnWinHttpCompletionStatusHeadersAvailable(hRequest, + &fAnotherCompletionExpected); + break; + + case WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE: + hr = OnWinHttpCompletionStatusDataAvailable(hRequest, + *reinterpret_cast(lpvStatusInformation), // dwBytes + &fAnotherCompletionExpected); + break; + + case WINHTTP_CALLBACK_STATUS_READ_COMPLETE: + hr = OnWinHttpCompletionStatusReadComplete(pResponse, + dwStatusInformationLength, + &fAnotherCompletionExpected); + break; + + case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR: + hr = HRESULT_FROM_WIN32(static_cast(lpvStatusInformation)->dwError); + break; + + case WINHTTP_CALLBACK_STATUS_SENDING_REQUEST: + // + // This is a notification, not a completion. This notifiation happens + // during the Send Request operation. + // + fAnotherCompletionExpected = TRUE; + break; + + case WINHTTP_CALLBACK_STATUS_REQUEST_SENT: + // + // Need to ignore this event. We get it as a side-effect of registering + // for WINHTTP_CALLBACK_STATUS_SENDING_REQUEST (which we actually need). + // + hr = S_OK; + fAnotherCompletionExpected = TRUE; + break; + + case WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING: + + fAnotherCompletionExpected = FALSE; + + if (m_RequestStatus != FORWARDER_DONE) + { + hr = ERROR_CONNECTION_ABORTED; + fClientError = m_fClientDisconnected; + goto Failure; + } + break; + + case WINHTTP_CALLBACK_STATUS_CONNECTION_CLOSED: + hr = ERROR_CONNECTION_ABORTED; + break; + + default: + // + // E_UNEXPECTED is rarely used, if seen means that this condition may been occurred. + // + DBG_ASSERT(FALSE); + hr = E_UNEXPECTED; + if (sm_pTraceLog != NULL) + { + WriteRefTraceLogEx(sm_pTraceLog, + m_cRefs, + this, + "FORWARDING_HANDLER::OnWinHttpCompletionInternal Unexpected WinHTTP Status", + reinterpret_cast(static_cast(dwInternetStatus)), + NULL); + } + break; + } + + // + // Handle failure code for switch statement above. + // + if (FAILED(hr)) + { + goto Failure; + } + + // + // WinHTTP completion handled successfully. + // + goto Finished; + +Failure: + m_RequestStatus = FORWARDER_DONE; + if(!m_fHasError) + { + m_fHasError = TRUE; + pResponse->DisableKernelCache(); + pResponse->GetRawHttpResponse()->EntityChunkCount = 0; + + if (fClientError || m_fClientDisconnected) + { + if (!m_fResponseHeadersReceivedAndSet) + { + pResponse->SetStatus(400, "Bad Request", 0, HRESULT_FROM_WIN32(WSAECONNRESET)); + } + else + { + // + // Response headers from origin server were + // already received and set for the current response. + // Honor the response status. + // + } + } + else + { + STACK_STRU(strDescription, 128); + pResponse->SetStatus(502, "Bad Gateway", 3, hr); + + if (hr > HRESULT_FROM_WIN32(WINHTTP_ERROR_BASE) && + hr <= HRESULT_FROM_WIN32(WINHTTP_ERROR_LAST)) + { +#pragma prefast (suppress : __WARNING_FUNCTION_NEEDS_REVIEW, "Function and parameters reviewed.") + FormatMessage( + FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_FROM_HMODULE, + g_hWinHttpModule, + HRESULT_CODE(hr), + 0, + strDescription.QueryStr(), + strDescription.QuerySizeCCH(), + NULL); + } + else + { + LoadString(g_hModule, + IDS_SERVER_ERROR, + strDescription.QueryStr(), + strDescription.QuerySizeCCH()); + } + strDescription.SyncWithBuffer(); + +#ifdef DEBUG + // addingmore data to error description to help us diaganose the issue + STACK_STRA(strMoreData, 128); + sprintf_s(strMoreData.QueryStr(), 100, "OnWinHttpCompletionInternal --%p--%d--%d--%d\n", this, GetCurrentThreadId(), dwInternetStatus, m_fServerResetConn); + strMoreData.SyncWithBuffer(); + strDescription.AppendA(strMoreData.QueryStr()); +#endif // DEBUG + + if (strDescription.QueryCCH() != 0) + { + pResponse->SetErrorDescription( + strDescription.QueryStr(), + strDescription.QueryCCH(), + FALSE); + } + + if (hr == HRESULT_FROM_WIN32(ERROR_WINHTTP_INVALID_SERVER_RESPONSE)) + { + if (!m_fServerResetConn) + { + RemoveRequest(); + pResponse->ResetConnection(); + m_fServerResetConn = TRUE; + } + } + } + } + +Finished: + // + // Since we use TLS to guard WinHttp operation, call PostCompletion instead of + // IndicateCompletion to allow cleaning up the TLS before thread reuse. + // Never post after the request has been finished for whatever reason + // + // Only postCompletion after all WinHttp handle got closed, + // i.e., received WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING callback for all handles + // So that no further WinHttp callback will be called + // Never post completion again after that + // Otherwise, there will be a AV as the request already passed IIS pipeline + // + if (fEndRequest && !m_fFinishRequest && dwHandlers ==0) + { + // + // Happy path + // +#ifdef DEBUG + DWORD counter = InterlockedIncrement(&g_dwLogCounter) % ASPNETCORE_DEBUG_STRU_ARRAY_SIZE; + sprintf_s(g_strLogs[counter].QueryStr(), ASPNETCORE_DEBUG_STRU_BUFFER_SIZE, + "PostCompletion 0 --%p--%p--%d\n", this, m_pW3Context, GetCurrentThreadId()); + g_strLogs[counter].SyncWithBuffer(); +#endif // DEBUG + + // Marked the request is finished, no more PostCompletion is allowed + RemoveRequest(); + m_fFinishRequest = TRUE; + fDoPostCompletion = TRUE; + if (m_pWebSocket != NULL) + { + delete(m_pWebSocket); + m_pWebSocket = NULL; + } + } + + else if (m_RequestStatus == FORWARDER_DONE) + { + // + // Error path + // + RemoveRequest(); + if (m_hRequest != NULL && !m_fHttpHandleInClose) + { + m_fHttpHandleInClose = TRUE; + WinHttpCloseHandle(m_hRequest); + m_hRequest = NULL; + } + + if (m_pWebSocket != NULL && !m_fWebSocketHandleInClose) + { + m_fWebSocketHandleInClose = TRUE; + m_pWebSocket->TerminateRequest(); + } + + if (fEndRequest) + { + fDoPostCompletion = (dwHandlers == 0 && !m_fFinishRequest); + if (fDoPostCompletion) + { +#ifdef DEBUG + DWORD counter = InterlockedIncrement(&g_dwLogCounter) % ASPNETCORE_DEBUG_STRU_ARRAY_SIZE; + sprintf_s(g_strLogs[counter].QueryStr(), ASPNETCORE_DEBUG_STRU_BUFFER_SIZE, + "PostCompletion 1 --%p--%p--%d\n", this, m_pW3Context, GetCurrentThreadId()); + g_strLogs[counter].SyncWithBuffer(); +#endif // DEBUG + // Marked the request is finished, no more PostCompletion is allowed + m_fFinishRequest = TRUE; + } + } + } + else if (!fAnotherCompletionExpected) + { + // + // Regular async IO operation + // + fDoPostCompletion = !m_fFinishRequest; +#ifdef DEBUG + if (fDoPostCompletion) + { + DWORD counter = InterlockedIncrement(&g_dwLogCounter) % ASPNETCORE_DEBUG_STRU_ARRAY_SIZE; + sprintf_s(g_strLogs[counter].QueryStr(), ASPNETCORE_DEBUG_STRU_BUFFER_SIZE, + "PostCompletion 2 --%p--%p--%d\n", this, m_pW3Context, GetCurrentThreadId()); + g_strLogs[counter].SyncWithBuffer(); + } +#endif // DEBUG + } + + // + // No code should access IIS m_pW3Context after posting the completion. + // + if (fDoPostCompletion) + { + m_pW3Context->PostCompletion(0); + } + + if (fExclusiveLock) + { + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == this); + TlsSetValue(g_dwTlsIndex, NULL); + ReleaseSRWLockExclusive(&m_RequestLock); + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == NULL); + } + else if (fSharedLock) + { + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == this); + TlsSetValue(g_dwTlsIndex, NULL); + ReleaseSRWLockShared(&m_RequestLock); + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == NULL); + } + + DereferenceForwardingHandler(); + +} + +HRESULT +FORWARDING_HANDLER::OnWinHttpCompletionSendRequestOrWriteComplete( + HINTERNET hRequest, + DWORD, + __out BOOL * pfClientError, + __out BOOL * pfAnotherCompletionExpected +) +{ + HRESULT hr = S_OK; + IHttpRequest * pRequest = m_pW3Context->GetRequest(); + + // + // completion for sending the initial request or request entity to + // winhttp, get more request entity if available, else start receiving + // the response + // + if (m_BytesToReceive > 0) + { + if (m_pEntityBuffer == NULL) + { + m_pEntityBuffer = GetNewResponseBuffer( + ENTITY_BUFFER_SIZE); + if (m_pEntityBuffer == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + } + + if (sm_pTraceLog != NULL) + { + WriteRefTraceLogEx(sm_pTraceLog, + m_cRefs, + this, + "Calling ReadEntityBody", + NULL, + NULL); + } + hr = pRequest->ReadEntityBody( + m_pEntityBuffer + 6, + min(m_BytesToReceive, BUFFER_SIZE), + TRUE, // fAsync + NULL, // pcbBytesReceived + NULL); // pfCompletionPending + if (hr == HRESULT_FROM_WIN32(ERROR_HANDLE_EOF)) + { + DBG_ASSERT(m_BytesToReceive == 0 || + m_BytesToReceive == INFINITE); + + // + // ERROR_HANDLE_EOF is not an error. + // + hr = S_OK; + + if (m_BytesToReceive == INFINITE) + { + m_BytesToReceive = 0; + m_cchLastSend = 5; + + if (!WinHttpWriteData(m_hRequest, + "0\r\n\r\n", + 5, + NULL)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + *pfAnotherCompletionExpected = TRUE; + + goto Finished; + } + } + else if (FAILED(hr)) + { + *pfClientError = TRUE; + goto Finished; + } + else + { + // + // ReadEntityBody will post a completion to IIS. + // + *pfAnotherCompletionExpected = TRUE; + + goto Finished; + } + } + + m_RequestStatus = FORWARDER_RECEIVING_RESPONSE; + + if (!WinHttpReceiveResponse(hRequest, NULL)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + *pfAnotherCompletionExpected = TRUE; + +Finished: + + return hr; +} + +HRESULT +FORWARDING_HANDLER::OnWinHttpCompletionStatusHeadersAvailable( + HINTERNET hRequest, + __out BOOL * pfAnotherCompletionExpected +) +{ + HRESULT hr = S_OK; + STACK_BUFFER(bufHeaderBuffer, 2048); + STACK_STRA(strHeaders, 2048); + DWORD dwHeaderSize = bufHeaderBuffer.QuerySize(); + + UNREFERENCED_PARAMETER(pfAnotherCompletionExpected); + + // + // Headers are available, read the status line and headers and pass + // them on to the client + // + // WinHttpQueryHeaders operates synchronously, + // no need for taking reference. + // + dwHeaderSize = bufHeaderBuffer.QuerySize(); + if (!WinHttpQueryHeaders(hRequest, + WINHTTP_QUERY_RAW_HEADERS_CRLF, + WINHTTP_HEADER_NAME_BY_INDEX, + bufHeaderBuffer.QueryPtr(), + &dwHeaderSize, + WINHTTP_NO_HEADER_INDEX)) + { + if (!bufHeaderBuffer.Resize(dwHeaderSize)) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + // + // WinHttpQueryHeaders operates synchronously, + // no need for taking reference. + // + if (!WinHttpQueryHeaders(hRequest, + WINHTTP_QUERY_RAW_HEADERS_CRLF, + WINHTTP_HEADER_NAME_BY_INDEX, + bufHeaderBuffer.QueryPtr(), + &dwHeaderSize, + WINHTTP_NO_HEADER_INDEX)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + } + + if (FAILED(hr = strHeaders.CopyW( + reinterpret_cast(bufHeaderBuffer.QueryPtr())))) + { + goto Finished; + } + + // Issue: The reason we add trailing \r\n is to eliminate issues that have been observed + // in some configurations where status and headers would not have final \r\n nor \r\n\r\n + // (last header was null terminated).That caused crash within header parsing code that expected valid + // format. Parsing code was fized to return ERROR_INVALID_PARAMETER, but we still should make + // Example of a status+header string that was causing problems (note the missing \r\n at the end) + // HTTP/1.1 302 Moved Permanently\r\n....\r\nLocation:http://site\0 + // + + if (!strHeaders.IsEmpty() && strHeaders.QueryStr()[strHeaders.QueryCCH() - 1] != '\n') + { + hr = strHeaders.Append("\r\n"); + if (FAILED(hr)) + { + goto Finished; + } + } + + if (FAILED(hr = SetStatusAndHeaders( + strHeaders.QueryStr(), + strHeaders.QueryCCH()))) + { + goto Finished; + } + + FreeResponseBuffers(); + + // + // If the request was websocket, and response was 101, + // trigger a flush, so that IIS's websocket module + // can get a chance to initialize and complete the handshake. + // + + if (m_fWebSocketEnabled) + { + + hr = m_pW3Context->GetResponse()->Flush( + TRUE, + TRUE, + NULL, + NULL); + + if (FAILED(hr)) + { + *pfAnotherCompletionExpected = FALSE; + } + else + { + m_RequestStatus = FORWARDER_RECEIVED_WEBSOCKET_RESPONSE; + *pfAnotherCompletionExpected = TRUE; + } + } + +Finished: + + return hr; +} + +HRESULT +FORWARDING_HANDLER::OnWinHttpCompletionStatusDataAvailable( + HINTERNET hRequest, + DWORD dwBytes, + __out BOOL * pfAnotherCompletionExpected +) +{ + HRESULT hr = S_OK; + + // + // Response data is available from winhttp, read it + // + if (dwBytes == 0) + { + if (m_cContentLength != 0) + { + hr = HRESULT_FROM_WIN32(ERROR_WINHTTP_INVALID_SERVER_RESPONSE); + goto Finished; + } + + m_RequestStatus = FORWARDER_DONE; + + goto Finished; + } + + m_BytesToSend = dwBytes; + if (m_cContentLength != 0) + { + m_cContentLength -= dwBytes; + } + + m_pEntityBuffer = GetNewResponseBuffer( + min(m_BytesToSend, BUFFER_SIZE)); + if (m_pEntityBuffer == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + if (!WinHttpReadData(hRequest, + m_pEntityBuffer, + min(m_BytesToSend, BUFFER_SIZE), + NULL)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + *pfAnotherCompletionExpected = TRUE; + +Finished: + + return hr; +} + +HRESULT +FORWARDING_HANDLER::OnWinHttpCompletionStatusReadComplete( + __in IHttpResponse * pResponse, + DWORD dwStatusInformationLength, + __out BOOL * pfAnotherCompletionExpected +) +{ + HRESULT hr = S_OK; + + // + // Response data has been read from winhttp, send it to the client + // + m_BytesToSend -= dwStatusInformationLength; + + if (m_cMinBufferLimit >= BUFFER_SIZE / 2) + { + if (m_cContentLength != 0) + { + m_cContentLength -= dwStatusInformationLength; + } + + // + // If we were not using WinHttpQueryDataAvailable and winhttp + // did not fill our buffer, we must have reached the end of the + // response + // + if (dwStatusInformationLength == 0 || + m_BytesToSend != 0) + { + if (m_cContentLength != 0) + { + hr = HRESULT_FROM_WIN32(ERROR_WINHTTP_INVALID_SERVER_RESPONSE); + goto Finished; + } + + m_RequestStatus = FORWARDER_DONE; + } + } + else + { + DBG_ASSERT(dwStatusInformationLength != 0); + } + + if (dwStatusInformationLength == 0) + { + goto Finished; + } + else + { + m_cBytesBuffered += dwStatusInformationLength; + + HTTP_DATA_CHUNK Chunk; + Chunk.DataChunkType = HttpDataChunkFromMemory; + Chunk.FromMemory.pBuffer = m_pEntityBuffer; + Chunk.FromMemory.BufferLength = dwStatusInformationLength; + if (FAILED(hr = pResponse->WriteEntityChunkByReference(&Chunk))) + { + goto Finished; + } + } + + if (m_cBytesBuffered >= m_cMinBufferLimit) + { + // + // Always post a completion to resume the WinHTTP data pump. + // + hr = pResponse->Flush(TRUE, // fAsync + TRUE, // fMoreData + NULL); // pcbSent + if (FAILED(hr)) + { + goto Finished; + } + *pfAnotherCompletionExpected = TRUE; + } + else + { + *pfAnotherCompletionExpected = FALSE; + } + +Finished: + + return hr; +} + +// static +HRESULT +FORWARDING_HANDLER::StaticInitialize( + BOOL fEnableReferenceCountTracing +) +/*++ + +Routine Description: + +Global initialization routine for FORWARDING_HANDLERs + +Arguments: + +fEnableReferenceCountTracing - True if ref count tracing should be use. + +Return Value: + +HRESULT + +--*/ +{ + HRESULT hr = S_OK; + + sm_pAlloc = new ALLOC_CACHE_HANDLER; + if (sm_pAlloc == NULL) + { + hr = E_OUTOFMEMORY; + goto Failure; + } + + hr = sm_pAlloc->Initialize(sizeof(FORWARDING_HANDLER), + 128); // nThreshold + if (FAILED(hr)) + { + goto Failure; + } + + // + // Open the session handle, specify random user-agent that will be + // overwritten by the client + // + sm_hSession = WinHttpOpen(L"", + WINHTTP_ACCESS_TYPE_NO_PROXY, + WINHTTP_NO_PROXY_NAME, + WINHTTP_NO_PROXY_BYPASS, + WINHTTP_FLAG_ASYNC); + if (sm_hSession == NULL) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Failure; + } + + // + // Don't set non-blocking callbacks WINHTTP_OPTION_ASSURED_NON_BLOCKING_CALLBACKS, + // as we will call WinHttpQueryDataAvailable to get response on the same thread + // that we received callback from Winhttp on completing sending/forwarding the request + // + + // + // Setup the callback function + // + if (WinHttpSetStatusCallback(sm_hSession, + FORWARDING_HANDLER::OnWinHttpCompletion, + (WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS | + WINHTTP_CALLBACK_STATUS_SENDING_REQUEST), + NULL) == WINHTTP_INVALID_STATUS_CALLBACK) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Failure; + } + + // + // Make sure we see the redirects (rather than winhttp doing it + // automatically) + // + DWORD dwRedirectOption = WINHTTP_OPTION_REDIRECT_POLICY_NEVER; + if (!WinHttpSetOption(sm_hSession, + WINHTTP_OPTION_REDIRECT_POLICY, + &dwRedirectOption, + sizeof(dwRedirectOption))) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Failure; + } + + // Initialize Application Manager + APPLICATION_MANAGER *pApplicationManager = APPLICATION_MANAGER::GetInstance(); + if (pApplicationManager == NULL) + { + hr = E_OUTOFMEMORY; + goto Failure; + } + + hr = pApplicationManager->Initialize(); + if (FAILED(hr)) + { + goto Failure; + } + + // Initialize PROTOCOL_CONFIG + sm_ProtocolConfig.Initialize(); + + if (FAILED(hr = sm_strErrorFormat.Resize(256))) + { + goto Failure; + } + + if (LoadString(g_hModule, + IDS_INVALID_PROPERTY, + sm_strErrorFormat.QueryStr(), + sm_strErrorFormat.QuerySizeCCH()) == 0) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Failure; + } + sm_strErrorFormat.SyncWithBuffer(); + + // If RegisterEventSource failed, we cannot do any thing about it + // No need to check whether the returned handle is valid + + if (g_pHttpServer->IsCommandLineLaunch()) + { + sm_hEventLog = RegisterEventSource(NULL, ASPNETCORE_IISEXPRESS_EVENT_PROVIDER); + } + else + { + sm_hEventLog = RegisterEventSource(NULL, ASPNETCORE_EVENT_PROVIDER); + } + + g_dwTlsIndex = TlsAlloc(); + if (g_dwTlsIndex == TLS_OUT_OF_INDEXES) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Failure; + } + + if (fEnableReferenceCountTracing) + { + sm_pTraceLog = CreateRefTraceLog(10000, 0); + } + + return S_OK; + +Failure: + + StaticTerminate(); + + return hr; +} + +// static +VOID +FORWARDING_HANDLER::StaticTerminate( + VOID +) +/*++ + +Routine Description: + +Global termination routine for FORWARDING_HANDLERs + +Arguments: + +None + +Return Value: + +None + +--*/ +{ + // + // Delete all the statics + // + + APPLICATION_MANAGER::Cleanup(); + + // + // wait for all server processes to go away + // for a max of 10 seconds. + // + + DWORD tickCount = GetTickCount(); + + while (g_dwActiveServerProcesses > 0) + { + if ((GetTickCount() - tickCount) > 10000) + { + break; + } + Sleep(250); + } + + if (sm_hSession != NULL) + { + WinHttpCloseHandle(sm_hSession); + sm_hSession = NULL; + } + + if (sm_hEventLog != NULL) + { + DeregisterEventSource(sm_hEventLog); + sm_hEventLog = NULL; + } + + if (g_dwTlsIndex != TLS_OUT_OF_INDEXES) + { + DBG_REQUIRE(TlsFree(g_dwTlsIndex)); + g_dwTlsIndex = TLS_OUT_OF_INDEXES; + } + + sm_strErrorFormat.Reset(); + + if (sm_pTraceLog != NULL) + { + DestroyRefTraceLog(sm_pTraceLog); + sm_pTraceLog = NULL; + } + + if (sm_pAlloc != NULL) + { + delete sm_pAlloc; + sm_pAlloc = NULL; + } +} + +VOID +CopyMultiSzToOutput( + IGlobalRSCAQueryProvider * pProvider, + PCWSTR pszList, + DWORD * pcbData +) +{ + PBYTE pvData; + DWORD cbData = 0; + PCWSTR pszListCopy = pszList; + while (*pszList != L'\0') + { + cbData += (static_cast(wcslen(pszList)) + 1) * sizeof(WCHAR); + pszList += wcslen(pszList) + 1; + } + cbData += sizeof(WCHAR); + if (FAILED(pProvider->GetOutputBuffer(cbData, + &pvData))) + { + return; + } + memcpy(pvData, + pszListCopy, + cbData); + *pcbData = cbData; +} + +struct AFFINITY_LOOKUP_CONTEXT +{ + DWORD timeout; + PCWSTR pszServer; + BUFFER * pHostNames; + DWORD cbData; +}; + +struct CACHE_CONTEXT +{ + PCSTR pszHostName; + IGlobalRSCAQueryProvider *pProvider; + __field_bcount_part(cbBuffer, cbData) + PBYTE pvData; + DWORD cbData; + DWORD cbBuffer; +}; + +VOID +FORWARDING_HANDLER::TerminateRequest( + BOOL fClientInitiated +) +{ + BOOL fAcquiredLock = FALSE; + if (TlsGetValue(g_dwTlsIndex) != this) + { + AcquireSRWLockExclusive(&m_RequestLock); + TlsSetValue(g_dwTlsIndex, this); + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == this); + fAcquiredLock = TRUE; + } + + // Only set the disconnect flag as the disconnect happens, request most likely is in OnAsyncCompletion + // If we close the handle here, most likely WinHttp callback happens on the same threads. + // We will have two OnAyncCompletion calls and may have race on PostCompletion + // Need do more investigation + if (!m_fHttpHandleInClose) + { + m_fClientDisconnected = fClientInitiated; + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, + "FORWARDING_HANDLER::TerminateRequest"); + RemoveRequest(); + + if (m_RequestStatus == FORWARDER_RECEIVED_WEBSOCKET_RESPONSE) + { + // + // websocket client is gone, cannot finish closing handshake gracefully + // have to terminate the request + // + if (m_pWebSocket != NULL) + { + m_pWebSocket->TerminateRequest(); + } + } + } + if (fAcquiredLock) + { + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == this); + TlsSetValue(g_dwTlsIndex, NULL); + ReleaseSRWLockExclusive(&m_RequestLock); + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == NULL); + } +} + +BYTE * +FORWARDING_HANDLER::GetNewResponseBuffer( + DWORD dwBufferSize +) +{ + DWORD dwNeededSize = (m_cEntityBuffers + 1) * sizeof(BYTE *); + if (dwNeededSize > m_buffEntityBuffers.QuerySize() && + !m_buffEntityBuffers.Resize( + max(dwNeededSize, m_buffEntityBuffers.QuerySize() * 2))) + { + return NULL; + } + + BYTE *pBuffer = (BYTE *)HeapAlloc(GetProcessHeap(), + 0, // dwFlags + dwBufferSize); + if (pBuffer == NULL) + { + return NULL; + } + + m_buffEntityBuffers.QueryPtr()[m_cEntityBuffers] = pBuffer; + m_cEntityBuffers++; + + return pBuffer; +} + +VOID +FORWARDING_HANDLER::FreeResponseBuffers() +{ + BYTE **pBuffers = m_buffEntityBuffers.QueryPtr(); + for (DWORD i = 0; i + +HTTP_MODULE_ID g_pModuleId = NULL; +IHttpServer * g_pHttpServer = NULL; +BOOL g_fAsyncDisconnectAvailable = FALSE; +BOOL g_fWinHttpNonBlockingCallbackAvailable = FALSE; +PCWSTR g_pszModuleName = NULL; +HINSTANCE g_hModule; +HINSTANCE g_hWinHttpModule; +BOOL g_fWebSocketSupported = FALSE; +DWORD g_dwTlsIndex = TLS_OUT_OF_INDEXES; +BOOL g_fEnableReferenceCountTracing = FALSE; +DWORD g_dwAspNetCoreDebugFlags = 0; +BOOL g_fNsiApiNotSupported = FALSE; +DWORD g_dwActiveServerProcesses = 0; +DWORD g_OptionalWinHttpFlags = 0; //specify additional WinHTTP options when using WinHttpOpenRequest API. +DWORD g_dwDebugFlags = 0; +PCSTR g_szDebugLabel = "ASPNET_CORE_MODULE"; + +#ifdef DEBUG +STRA g_strLogs[ASPNETCORE_DEBUG_STRU_ARRAY_SIZE]; +DWORD g_dwLogCounter = 0; +#endif // DEBUG + + +BOOL WINAPI DllMain( + HMODULE hModule, + DWORD dwReason, + LPVOID + ) +{ + switch (dwReason) + { + case DLL_PROCESS_ATTACH: + g_hModule = hModule; + DisableThreadLibraryCalls(hModule); + break; + default: + break; + } + + return TRUE; +} + +VOID +LoadGlobalConfiguration( +VOID +) +{ + HKEY hKey; + + if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, + L"SOFTWARE\\Microsoft\\IIS Extensions\\IIS AspNetCore Module\\Parameters", + 0, + KEY_READ, + &hKey) == NO_ERROR) + { + DWORD dwType; + DWORD dwData; + DWORD cbData; + + cbData = sizeof(dwData); + if ((RegQueryValueEx(hKey, + L"OptionalWinHttpFlags", + NULL, + &dwType, + (LPBYTE)&dwData, + &cbData) == NO_ERROR) && + (dwType == REG_DWORD)) + { + g_OptionalWinHttpFlags = dwData; + } + + cbData = sizeof(dwData); + if ((RegQueryValueEx(hKey, + L"EnableReferenceCountTracing", + NULL, + &dwType, + (LPBYTE)&dwData, + &cbData) == NO_ERROR) && + (dwType == REG_DWORD) && (dwData == 1 || dwData == 0)) + { + g_fEnableReferenceCountTracing = !!dwData; + } + + cbData = sizeof(dwData); + if ((RegQueryValueEx(hKey, + L"DebugFlags", + NULL, + &dwType, + (LPBYTE)&dwData, + &cbData) == NO_ERROR) && + (dwType == REG_DWORD)) + { + g_dwAspNetCoreDebugFlags = dwData; + } + + RegCloseKey(hKey); + } + + DWORD dwSize = 0; + DWORD dwResult = GetExtendedTcpTable(NULL, + &dwSize, + FALSE, + AF_INET, + TCP_TABLE_OWNER_PID_LISTENER, + 0); + if (dwResult != NO_ERROR && dwResult != ERROR_INSUFFICIENT_BUFFER) + { + g_fNsiApiNotSupported = TRUE; + } +} + +HRESULT +__stdcall +RegisterModule( +DWORD dwServerVersion, +IHttpModuleRegistrationInfo * pModuleInfo, +IHttpServer * pHttpServer +) +/*++ + +Routine description: + +Function called by IIS immediately after loading the module, used to let +IIS know what notifications the module is interested in + +Arguments: + +dwServerVersion - IIS version the module is being loaded on +pModuleInfo - info regarding this module +pHttpServer - callback functions which can be used by the module at +any point + +Return value: + +HRESULT + +--*/ +{ + HRESULT hr = S_OK; + CProxyModuleFactory * pFactory = NULL; + +#ifdef DEBUG + CREATE_DEBUG_PRINT_OBJECT("Asp.Net Core Module"); + g_dwDebugFlags = DEBUG_FLAGS_ANY; +#endif // DEBUG + + LoadGlobalConfiguration(); + + // + // 7.0 is 0,7 + // + if (dwServerVersion > MAKELONG(0, 7)) + { + g_fAsyncDisconnectAvailable = TRUE; + } + + // + // 8.0 is 0,8 + // + if (dwServerVersion >= MAKELONG(0, 8)) + { + // IISOOB:36641 Enable back WINHTTP_OPTION_ASSURED_NON_BLOCKING_CALLBACKS for Win8. + // g_fWinHttpNonBlockingCallbackAvailable = TRUE; + g_fWebSocketSupported = TRUE; + } + + hr = WINHTTP_HELPER::StaticInitialize(); + if (FAILED(hr)) + { + if (hr == HRESULT_FROM_WIN32(ERROR_PROC_NOT_FOUND)) + { + g_fWebSocketSupported = FALSE; + } + else + { + goto Finished; + } + } + + g_pModuleId = pModuleInfo->GetId(); + g_pszModuleName = pModuleInfo->GetName(); + g_pHttpServer = pHttpServer; + +#ifdef DEBUG + for (int i = 0; i < ASPNETCORE_DEBUG_STRU_ARRAY_SIZE; i++) + { + g_strLogs[i].Resize(ASPNETCORE_DEBUG_STRU_BUFFER_SIZE + 1); + } +#endif // DEBUG + // + // WinHTTP does not create enough threads, ask it to create more. + // Starting in Windows 7, this setting is ignored because WinHTTP + // uses a thread pool. + // + SYSTEM_INFO si; + GetSystemInfo(&si); + DWORD dwThreadCount = (si.dwNumberOfProcessors * 3 + 1) / 2; + WinHttpSetOption(NULL, + WINHTTP_OPTION_WORKER_THREAD_COUNT, + &dwThreadCount, + sizeof(dwThreadCount)); + + // + // Create the factory before any static initialization. + // The CProxyModuleFactory::Terminate method will clean any + // static object initialized. + // + pFactory = new CProxyModuleFactory; + if (pFactory == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + hr = pModuleInfo->SetRequestNotifications( + pFactory, + RQ_EXECUTE_REQUEST_HANDLER, + 0); + if (FAILED(hr)) + { + goto Finished; + } + + pFactory = NULL; + g_pResponseHeaderHash = new RESPONSE_HEADER_HASH; + if (g_pResponseHeaderHash == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + hr = g_pResponseHeaderHash->Initialize(); + if (FAILED(hr)) + { + goto Finished; + } + + hr = ALLOC_CACHE_HANDLER::StaticInitialize(); + if (FAILED(hr)) + { + goto Finished; + } + + hr = FORWARDING_HANDLER::StaticInitialize(g_fEnableReferenceCountTracing); + if (FAILED(hr)) + { + goto Finished; + } + + hr = WEBSOCKET_HANDLER::StaticInitialize(g_fEnableReferenceCountTracing); + if (FAILED(hr)) + { + goto Finished; + } + +Finished: + + if (pFactory != NULL) + { + pFactory->Terminate(); + pFactory = NULL; + } + + return hr; +} + diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/path.cxx b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/path.cxx new file mode 100644 index 0000000000..cde0dd5312 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/path.cxx @@ -0,0 +1,442 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "precomp.hxx" + +// static +HRESULT +PATH::SplitUrl( + PCWSTR pszDestinationUrl, + BOOL *pfSecure, + STRU *pstrDestination, + STRU *pstrUrl +) +/*++ + +Routine Description: + + Split the URL specified for forwarding into its specific components + The format of the URL looks like + http[s]://destination[:port]/path + when port is omitted, the default port for that specific protocol is used + when host is omitted, it gets the same value as the destination + +Arguments: + + pszDestinationUrl - the url to be split up + pfSecure - SSL to be used in forwarding? + pstrDestination - destination + pDestinationPort - port + pstrUrl - URL + +Return Value: + + HRESULT + +--*/ +{ + HRESULT hr; + + // + // First determine if the target is secure + // + if (_wcsnicmp(pszDestinationUrl, L"http://", 7) == 0) + { + *pfSecure = FALSE; + pszDestinationUrl += 7; + } + else if (_wcsnicmp(pszDestinationUrl, L"https://", 8) == 0) + { + *pfSecure = TRUE; + pszDestinationUrl += 8; + } + else + { + return HRESULT_FROM_WIN32(ERROR_INVALID_DATA); + } + + if (*pszDestinationUrl == L'\0') + { + return HRESULT_FROM_WIN32(ERROR_INVALID_DATA); + } + + // + // Find the 3rd slash corresponding to the url + // + LPCWSTR pszSlash = wcschr(pszDestinationUrl, L'/'); + if (pszSlash == NULL) + { + if (FAILED(hr = pstrUrl->Copy(L"/", 1)) || + FAILED(hr = pstrDestination->Copy(pszDestinationUrl))) + { + return hr; + } + } + else + { + if (FAILED(hr = pstrUrl->Copy(pszSlash)) || + FAILED(hr = pstrDestination->Copy(pszDestinationUrl, + (DWORD)(pszSlash - pszDestinationUrl)))) + { + return hr; + } + } + + return S_OK; +} + +// Change a hexadecimal digit to its numerical equivalent +#define TOHEX( ch ) \ + ((ch) > L'9' ? \ + (ch) >= L'a' ? \ + (ch) - L'a' + 10 : \ + (ch) - L'A' + 10 \ + : (ch) - L'0') + +// static +HRESULT +PATH::UnEscapeUrl( + PCWSTR pszUrl, + DWORD cchUrl, + bool fCopyQuery, + STRA * pstrResult +) +{ + HRESULT hr; + CHAR pch[2]; + pch[1] = '\0'; + DWORD cchStart = 0; + DWORD index = 0; + + while (index < cchUrl && + (fCopyQuery || pszUrl[index] != L'?')) + { + switch (pszUrl[index]) + { + case L'%': + if (iswxdigit(pszUrl[index+1]) && iswxdigit(pszUrl[index+2])) + { + if (index > cchStart && + FAILED(hr = pstrResult->AppendW(pszUrl + cchStart, + index - cchStart))) + { + return hr; + } + cchStart = index+3; + + pch[0] = static_cast(TOHEX(pszUrl[index+1]) * 16 + + TOHEX(pszUrl[index+2])); + if (FAILED(hr = pstrResult->Append(pch, 1))) + { + return hr; + } + index += 3; + break; + } + + __fallthrough; + default: + index++; + } + } + + if (index > cchStart) + { + return pstrResult->AppendW(pszUrl + cchStart, + index - cchStart); + } + + return S_OK; +} + +// static +HRESULT +PATH::UnEscapeUrl( + PCWSTR pszUrl, + DWORD cchUrl, + STRU * pstrResult +) +{ + HRESULT hr; + WCHAR pch[2]; + pch[1] = L'\0'; + DWORD cchStart = 0; + DWORD index = 0; + bool fInQuery = FALSE; + + while (index < cchUrl) + { + switch (pszUrl[index]) + { + case L'%': + if (iswxdigit(pszUrl[index+1]) && iswxdigit(pszUrl[index+2])) + { + if (index > cchStart && + FAILED(hr = pstrResult->Append(pszUrl + cchStart, + index - cchStart))) + { + return hr; + } + cchStart = index+3; + + pch[0] = static_cast(TOHEX(pszUrl[index+1]) * 16 + + TOHEX(pszUrl[index+2])); + if (FAILED(hr = pstrResult->Append(pch, 1))) + { + return hr; + } + index += 3; + if (pch[0] == L'?') + { + fInQuery = TRUE; + } + break; + } + + index++; + break; + + case L'/': + if (fInQuery) + { + if (index > cchStart && + FAILED(hr = pstrResult->Append(pszUrl + cchStart, + index - cchStart))) + { + return hr; + } + cchStart = index+1; + + if (FAILED(hr = pstrResult->Append(L"\\", 1))) + { + return hr; + } + index += 1; + break; + } + + __fallthrough; + default: + index++; + } + } + + if (index > cchStart) + { + return pstrResult->Append(pszUrl + cchStart, + index - cchStart); + } + + return S_OK; +} + +HRESULT +PATH::EscapeAbsPath( + IHttpRequest * pRequest, + STRU * strEscapedUrl +) +{ + HRESULT hr = S_OK; + STRU strAbsPath; + LPCWSTR pszAbsPath = NULL; + LPCWSTR pszFindStr = NULL; + + hr = strAbsPath.Copy( pRequest->GetRawHttpRequest()->CookedUrl.pAbsPath, + pRequest->GetRawHttpRequest()->CookedUrl.AbsPathLength / sizeof(WCHAR) ); + if(FAILED(hr)) + { + goto Finished; + } + + pszAbsPath = strAbsPath.QueryStr(); + pszFindStr = wcschr(pszAbsPath, L'?'); + + while(pszFindStr != NULL) + { + strEscapedUrl->Append( pszAbsPath, pszFindStr - pszAbsPath); + strEscapedUrl->Append(L"%3F"); + pszAbsPath = pszFindStr + 1; + pszFindStr = wcschr(pszAbsPath, L'?'); + } + + strEscapedUrl->Append(pszAbsPath); + strEscapedUrl->Append(pRequest->GetRawHttpRequest()->CookedUrl.pQueryString, + pRequest->GetRawHttpRequest()->CookedUrl.QueryStringLength / sizeof(WCHAR)); + +Finished: + return hr; +} + +// static +bool +PATH::IsValidAttributeNameChar( + WCHAR ch +) +{ + // + // Values based on ASP.NET rendering for cookie names. RFC 2965 is not clear + // what the non-special characters are. + // + return ch == L'\t' || (ch > 31 && ch < 127); +} + +// static +bool +PATH::FindInMultiString( + PCWSTR pszMultiString, + PCWSTR pszStringToFind +) +{ + while (*pszMultiString != L'\0') + { + if (wcscmp(pszMultiString, pszStringToFind) == 0) + { + return TRUE; + } + pszMultiString += wcslen(pszMultiString) + 1; + } + + return FALSE; +} + +// static +bool +PATH::IsValidQueryStringName( + PCWSTR pszName +) +{ + while (*pszName != L'\0') + { + WCHAR c = *pszName; + if (c != L'-' && c != L'_' && c != L'+' && + c != L'.' && c != L'*' && c != L'$' && c != L'%' && c != L',' && + !iswalnum(c)) + { + return FALSE; + } + pszName++; + } + + return TRUE; +} + +// static +bool +PATH::IsValidHeaderName( + PCWSTR pszName +) +{ + while (*pszName != L'\0') + { + WCHAR c = *pszName; + if (c != L'-' && c != L'_' && c != L'+' && + c != L'.' && c != L'*' && c != L'$' && c != L'%' + && !iswalnum(c)) + { + return FALSE; + } + pszName++; + } + + return TRUE; +} + +HRESULT +PATH::IsPathUnc( + __in LPCWSTR pszPath, + __out BOOL * pfIsUnc +) +{ + HRESULT hr = S_OK; + STRU strTempPath; + + if ( pszPath == NULL || pfIsUnc == NULL ) + { + hr = E_INVALIDARG; + goto Finished; + } + + hr = MakePathCanonicalizationProof( (LPWSTR) pszPath, &strTempPath ); + if ( FAILED(hr) ) + { + goto Finished; + } + + // + // MakePathCanonicalizationProof will map \\?\UNC, \\.\UNC and \\ to \\?\UNC + // + (*pfIsUnc) = ( _wcsnicmp( strTempPath.QueryStr(), L"\\\\?\\UNC\\", 8 /* sizeof \\?\UNC\ */) == 0 ); + +Finished: + + return hr; +} + +HRESULT +PATH::ConvertPathToFullPath( + _In_ LPCWSTR pszPath, + _In_ LPCWSTR pszRootPath, + _Out_ STRU* pStruFullPath +) +{ + HRESULT hr = S_OK; + STRU strFileFullPath; + LPWSTR pszFullPath = NULL; + + // if relative path, prefix with root path and then convert to absolute path. + if( pszPath[0] == L'.' ) + { + hr = strFileFullPath.Copy(pszRootPath); + if(FAILED(hr)) + { + goto Finished; + } + + if(!strFileFullPath.EndsWith(L"\\")) + { + hr = strFileFullPath.Append(L"\\"); + if(FAILED(hr)) + { + goto Finished; + } + } + } + + hr = strFileFullPath.Append( pszPath ); + if(FAILED(hr)) + { + goto Finished; + } + + pszFullPath = new WCHAR[ strFileFullPath.QueryCCH() + 1]; + if( pszFullPath == NULL ) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + if(_wfullpath( pszFullPath, + strFileFullPath.QueryStr(), + strFileFullPath.QueryCCH() + 1 ) == NULL ) + { + hr = HRESULT_FROM_WIN32( ERROR_INVALID_PARAMETER ); + goto Finished; + } + + // convert to canonical path + hr = MakePathCanonicalizationProof( pszFullPath, pStruFullPath ); + if(FAILED(hr)) + { + goto Finished; + } + +Finished: + + if( pszFullPath != NULL ) + { + delete[] pszFullPath; + pszFullPath = NULL; + } + + return hr; +} \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/precomp.hxx b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/precomp.hxx new file mode 100644 index 0000000000..0aabd3b5f1 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/precomp.hxx @@ -0,0 +1,156 @@ +// Copyright(c).NET Foundation.All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once +#pragma warning( disable : 4091) + +// +// System related headers +// +#define _WINSOCKAPI_ + +#define NTDDI_VERSION 0x06010000 +#define WINVER 0x0601 +#define _WIN32_WINNT 0x0601 + +#include +#include +#include + +//#include +#include +#include + +// This should remove our issue of compiling for win7 without header files. +// We force the Windows 8 version check logic in iiswebsocket.h to succeed even though we're compiling for Windows 7. +// Then, we set the version defines back to Windows 7 to for the remainder of the compilation. +#undef NTDDI_VERSION +#undef WINVER +#undef _WIN32_WINNT +#define NTDDI_VERSION 0x06020000 +#define WINVER 0x0602 +#define _WIN32_WINNT 0x0602 +#include +#undef NTDDI_VERSION +#undef WINVER +#undef _WIN32_WINNT + +#define NTDDI_VERSION 0x06010000 +#define WINVER 0x0601 +#define _WIN32_WINNT 0x0601 + +#include +#include + +// +// Option available starting Windows 8. +// 111 is the value in SDK on May 15, 2012. +// +#ifndef WINHTTP_OPTION_ASSURED_NON_BLOCKING_CALLBACKS +#define WINHTTP_OPTION_ASSURED_NON_BLOCKING_CALLBACKS 111 +#endif + +#define ASPNETCORE_EVENT_PROVIDER L"IIS AspNetCore Module" +#define ASPNETCORE_IISEXPRESS_EVENT_PROVIDER L"IIS Express AspNetCore Module" + +#define TIMESPAN_IN_MILLISECONDS(x) ((x)/((LONGLONG)(10000))) +#define TIMESPAN_IN_SECONDS(x) ((TIMESPAN_IN_MILLISECONDS(x))/((LONGLONG)(1000))) +#define TIMESPAN_IN_MINUTES(x) ((TIMESPAN_IN_SECONDS(x))/((LONGLONG)(60))) + +#ifdef max +#undef max +template inline T max(T a, T b) +{ + return a > b ? a : b; +} +#endif + +#ifdef min +#undef min +template inline T min(T a, T b) +{ + return a < b ? a : b; +} +#endif + +inline bool IsSpace(char ch) +{ + switch(ch) + { + case 32: // ' ' + case 9: // '\t' + case 10: // '\n' + case 13: // '\r' + case 11: // '\v' + case 12: // '\f' + return true; + default: + return false; + } +} + +#include +#include +#include +#include +#include + +#include +#include "ahutil.h" +#include "multisz.h" +#include "multisza.h" +#include "sttimer.h" +#include +#include +#include +#include +#include +#include + +#include "filewatcher.h" +#include "environmentvariablehash.h" +#include "..\aspnetcore_msg.h" +#include "aspnetcoreconfig.h" +#include "serverprocess.h" +#include "processmanager.h" +#include "application.h" +#include "applicationmanager.h" +#include "resource.h" +#include "path.h" +#include "debugutil.h" +#include "protocolconfig.h" +#include "responseheaderhash.h" +#include "forwarderconnection.h" +#include "winhttphelper.h" +#include "websockethandler.h" +#include "forwardinghandler.h" +#include "proxymodule.h" + +FORCEINLINE +DWORD +WIN32_FROM_HRESULT( + HRESULT hr +) +{ + if ((FAILED(hr)) && + (HRESULT_FACILITY(hr) == FACILITY_WIN32)) + { + return HRESULT_CODE(hr); + } + return hr; +} + +FORCEINLINE +HRESULT +HRESULT_FROM_GETLASTERROR() +{ + return ( GetLastError() != NO_ERROR ) + ? HRESULT_FROM_WIN32( GetLastError() ) + : E_FAIL; +} + +extern BOOL g_fAsyncDisconnectAvailable; +extern PVOID g_pModuleId; +extern BOOL g_fWebSocketSupported; +extern BOOL g_fEnableReferenceCountTracing; +extern DWORD g_dwActiveServerProcesses; diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/processmanager.cxx b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/processmanager.cxx new file mode 100644 index 0000000000..5029a52670 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/processmanager.cxx @@ -0,0 +1,294 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "precomp.hxx" + +volatile BOOL PROCESS_MANAGER::sm_fWSAStartupDone = FALSE; + +HRESULT +PROCESS_MANAGER::Initialize( + VOID +) +{ + HRESULT hr = S_OK; + WSADATA wsaData; + int result; + BOOL fLocked = FALSE; + + if( !sm_fWSAStartupDone ) + { + AcquireSRWLockExclusive( &m_srwLock ); + fLocked = TRUE; + + if( !sm_fWSAStartupDone ) + { + if( (result = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0 ) + { + hr = HRESULT_FROM_WIN32( result ); + goto Finished; + } + sm_fWSAStartupDone = TRUE; + } + + ReleaseSRWLockExclusive( &m_srwLock ); + fLocked = FALSE; + } + + m_dwRapidFailTickStart = GetTickCount(); + + if( m_hNULHandle == NULL ) + { + SECURITY_ATTRIBUTES saAttr; + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = TRUE; + saAttr.lpSecurityDescriptor = NULL; + + m_hNULHandle = CreateFileW( L"NUL", + FILE_WRITE_DATA, + FILE_SHARE_READ, + &saAttr, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + NULL ); + if( m_hNULHandle == INVALID_HANDLE_VALUE ) + { + hr = HRESULT_FROM_GETLASTERROR(); + goto Finished; + } + } + +Finished: + + if(fLocked) + { + ReleaseSRWLockExclusive( &m_srwLock ); + } + + return hr; +} + +PROCESS_MANAGER::~PROCESS_MANAGER() +{ + AcquireSRWLockExclusive(&m_srwLock); + + if( m_ppServerProcessList != NULL ) + { + for( DWORD i = 0; i < m_dwProcessesPerApplication; ++i ) + { + if( m_ppServerProcessList[i] != NULL ) + { + m_ppServerProcessList[i]->DereferenceServerProcess(); + m_ppServerProcessList[i] = NULL; + } + } + + delete[] m_ppServerProcessList; + m_ppServerProcessList = NULL; + } + + if( m_hNULHandle != NULL ) + { + CloseHandle( m_hNULHandle ); + m_hNULHandle = NULL; + } + + if( sm_fWSAStartupDone ) + { + WSACleanup(); + sm_fWSAStartupDone = FALSE; + } + + ReleaseSRWLockExclusive(&m_srwLock); +} + +HRESULT +PROCESS_MANAGER::GetProcess( + _In_ IHttpContext *context, + _In_ ASPNETCORE_CONFIG *pConfig, + _Out_ SERVER_PROCESS **ppServerProcess +) +{ + HRESULT hr = S_OK; + BOOL fSharedLock = FALSE; + BOOL fExclusiveLock = FALSE; + PCWSTR apsz[1]; + STACK_STRU( strEventMsg, 256 ); + DWORD dwProcessIndex = 0; + SERVER_PROCESS **ppSelectedServerProcess = NULL; + + if (!m_fServerProcessListReady) + { + AcquireSRWLockExclusive( &m_srwLock ); + fExclusiveLock = TRUE; + + if (!m_fServerProcessListReady) + { + m_dwProcessesPerApplication = pConfig->QueryProcessesPerApplication(); + m_ppServerProcessList = new SERVER_PROCESS*[m_dwProcessesPerApplication]; + if(m_ppServerProcessList == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + for(DWORD i=0;iIsReady() ) + { + m_ppServerProcessList[dwProcessIndex]->ReferenceServerProcess(); + *ppServerProcess = m_ppServerProcessList[dwProcessIndex]; + goto Finished; + } + + ReleaseSRWLockShared( &m_srwLock ); + fSharedLock = FALSE; + // should make the lock per process so that we can start processes simultaneously ? + + if(m_ppServerProcessList[dwProcessIndex] == NULL || !m_ppServerProcessList[dwProcessIndex]->IsReady()) + { + AcquireSRWLockExclusive( &m_srwLock ); + fExclusiveLock = TRUE; + + if( m_ppServerProcessList[dwProcessIndex] != NULL ) + { + if( !m_ppServerProcessList[dwProcessIndex]->IsReady() ) + { + // + // terminate existing process that is not ready + // before creating new one. + // + + ShutdownProcessNoLock( m_ppServerProcessList[dwProcessIndex] ); + } + else + { + // server is already up and ready to serve requests. + m_ppServerProcessList[dwProcessIndex]->ReferenceServerProcess(); + *ppServerProcess = m_ppServerProcessList[dwProcessIndex]; + goto Finished; + } + } + + if( RapidFailsPerMinuteExceeded(pConfig->QueryRapidFailsPerMinute()) ) + { + // + // rapid fails per minute exceeded, do not create new process. + // + + if( SUCCEEDED( strEventMsg.SafeSnwprintf( + ASPNETCORE_EVENT_RAPID_FAIL_COUNT_EXCEEDED_MSG, + pConfig->QueryRapidFailsPerMinute() ) ) ) + { + apsz[0] = strEventMsg.QueryStr(); + + // + // not checking return code because if ReportEvent + // fails, we cannot do anything. + // + if (FORWARDING_HANDLER::QueryEventLog() != NULL) + { + ReportEventW(FORWARDING_HANDLER::QueryEventLog(), + EVENTLOG_INFORMATION_TYPE, + 0, + ASPNETCORE_EVENT_RAPID_FAIL_COUNT_EXCEEDED, + NULL, + 1, + 0, + apsz, + NULL); + } + } + + hr = HRESULT_FROM_WIN32(ERROR_SERVER_DISABLED); + goto Finished; + } + + if( m_ppServerProcessList[dwProcessIndex] == NULL ) + { + m_ppServerProcessList[dwProcessIndex] = new SERVER_PROCESS(); + if( m_ppServerProcessList[dwProcessIndex] == NULL ) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + hr = m_ppServerProcessList[dwProcessIndex]->Initialize( + this, + pConfig->QueryProcessPath(), + pConfig->QueryArguments(), + pConfig->QueryStartupTimeLimitInMS(), + pConfig->QueryShutdownTimeLimitInMS(), + pConfig->QueryWindowsAuthEnabled(), + pConfig->QueryBasicAuthEnabled(), + pConfig->QueryAnonymousAuthEnabled(), + pConfig->QueryEnvironmentVariables(), + pConfig->QueryStdoutLogEnabled(), + pConfig->QueryStdoutLogFile() + ); + if( FAILED( hr ) ) + { + goto Finished; + } + + hr = m_ppServerProcessList[dwProcessIndex]->StartProcess(context); + if( FAILED( hr ) ) + { + goto Finished; + } + } + + if( !m_ppServerProcessList[dwProcessIndex]->IsReady() ) + { + hr = HRESULT_FROM_WIN32( ERROR_CREATE_FAILED ); + goto Finished; + } + + m_ppServerProcessList[dwProcessIndex]->ReferenceServerProcess(); + *ppServerProcess = m_ppServerProcessList[dwProcessIndex]; + } + +Finished: + + if( FAILED(hr) ) + { + if(m_ppServerProcessList[dwProcessIndex] != NULL ) + { + m_ppServerProcessList[dwProcessIndex]->DereferenceServerProcess(); + m_ppServerProcessList[dwProcessIndex] = NULL; + } + } + + if( fSharedLock ) + { + ReleaseSRWLockShared( &m_srwLock ); + fSharedLock = FALSE; + } + + if( fExclusiveLock ) + { + ReleaseSRWLockExclusive( &m_srwLock ); + fExclusiveLock = FALSE; + } + + return hr; +} \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/protocolconfig.cxx b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/protocolconfig.cxx new file mode 100644 index 0000000000..83e2648925 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/protocolconfig.cxx @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "precomp.hxx" + +HRESULT +PROTOCOL_CONFIG::Initialize() +{ + HRESULT hr; + STRU strTemp; + + m_fKeepAlive = TRUE; + m_msTimeout = 120000; + m_fPreserveHostHeader = TRUE; + m_fReverseRewriteHeaders = FALSE; + + if (FAILED(hr = m_strXForwardedForName.CopyW(L"X-Forwarded-For"))) + { + goto Finished; + } + + if (FAILED(hr = m_strSslHeaderName.CopyW(L"X-Forwarded-Proto"))) + { + goto Finished; + } + + if (FAILED(hr = m_strClientCertName.CopyW(L"MS-ASPNETCORE-CLIENTCERT"))) + { + goto Finished; + } + + m_fIncludePortInXForwardedFor = TRUE; + m_dwMinResponseBuffer = 0; // no response buffering + m_dwResponseBufferLimit = 4096*1024; + m_dwMaxResponseHeaderSize = 65536; + +Finished: + + return hr; +} + +VOID +PROTOCOL_CONFIG::OverrideConfig( + ASPNETCORE_CONFIG *pAspNetCoreConfig +) +{ + m_msTimeout = pAspNetCoreConfig->QueryRequestTimeoutInMS(); +} \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/proxymodule.cxx b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/proxymodule.cxx new file mode 100644 index 0000000000..4a4e8fb6bd --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/proxymodule.cxx @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "precomp.hxx" + + +__override +HRESULT +CProxyModuleFactory::GetHttpModule( + CHttpModule ** ppModule, + IModuleAllocator * pAllocator +) +{ + CProxyModule *pModule = new (pAllocator) CProxyModule(); + if (pModule == NULL) + { + return E_OUTOFMEMORY; + } + + *ppModule = pModule; + return S_OK; +} + +__override +VOID +CProxyModuleFactory::Terminate( + VOID +) +/*++ + +Routine description: + +Function called by IIS for global (non-request-specific) notifications + +Arguments: + +None. + +Return value: + +None + +--*/ +{ + FORWARDING_HANDLER::StaticTerminate(); + + WEBSOCKET_HANDLER::StaticTerminate(); + + if (g_pResponseHeaderHash != NULL) + { + g_pResponseHeaderHash->Clear(); + delete g_pResponseHeaderHash; + g_pResponseHeaderHash = NULL; + } + + ALLOC_CACHE_HANDLER::StaticTerminate(); + + delete this; +} + +CProxyModule::CProxyModule( +) : m_pHandler(NULL) +{ +} + +CProxyModule::~CProxyModule() +{ + if (m_pHandler != NULL) + { + // + // This will be called when the main notification is cleaned up + // i.e., the request is done with IIS pipeline + // + m_pHandler->DereferenceForwardingHandler(); + m_pHandler = NULL; + } +} + +__override +REQUEST_NOTIFICATION_STATUS +CProxyModule::OnExecuteRequestHandler( + IHttpContext * pHttpContext, + IHttpEventProvider * +) +{ + m_pHandler = new FORWARDING_HANDLER(pHttpContext); + if (m_pHandler == NULL) + { + pHttpContext->GetResponse()->SetStatus(500, "Internal Server Error", 0, E_OUTOFMEMORY); + return RQ_NOTIFICATION_FINISH_REQUEST; + } + + return m_pHandler->OnExecuteRequestHandler(); +} + +__override +REQUEST_NOTIFICATION_STATUS +CProxyModule::OnAsyncCompletion( + IHttpContext *, + DWORD dwNotification, + BOOL fPostNotification, + IHttpEventProvider *, + IHttpCompletionInfo * pCompletionInfo +) +{ + UNREFERENCED_PARAMETER(dwNotification); + UNREFERENCED_PARAMETER(fPostNotification); + DBG_ASSERT(dwNotification == RQ_EXECUTE_REQUEST_HANDLER); + DBG_ASSERT(fPostNotification == FALSE); + + return m_pHandler->OnAsyncCompletion( + pCompletionInfo->GetCompletionBytes(), + pCompletionInfo->GetCompletionStatus()); +} \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/responseheaderhash.cxx b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/responseheaderhash.cxx new file mode 100644 index 0000000000..161095042c --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/responseheaderhash.cxx @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "precomp.hxx" + +RESPONSE_HEADER_HASH * g_pResponseHeaderHash = NULL; + +HEADER_RECORD RESPONSE_HEADER_HASH::sm_rgHeaders[] = +{ + { "Cache-Control", HttpHeaderCacheControl }, + { "Connection", HttpHeaderConnection }, + { "Date", HttpHeaderDate }, + { "Keep-Alive", HttpHeaderKeepAlive }, + { "Pragma", HttpHeaderPragma }, + { "Trailer", HttpHeaderTrailer }, + { "Transfer-Encoding", HttpHeaderTransferEncoding }, + { "Upgrade", HttpHeaderUpgrade }, + { "Via", HttpHeaderVia }, + { "Warning", HttpHeaderWarning }, + { "Allow", HttpHeaderAllow }, + { "Content-Length", HttpHeaderContentLength }, + { "Content-Type", HttpHeaderContentType }, + { "Content-Encoding", HttpHeaderContentEncoding }, + { "Content-Language", HttpHeaderContentLanguage }, + { "Content-Location", HttpHeaderContentLocation }, + { "Content-MD5", HttpHeaderContentMd5 }, + { "Content-Range", HttpHeaderContentRange }, + { "Expires", HttpHeaderExpires }, + { "Last-Modified", HttpHeaderLastModified }, + { "Accept-Ranges", HttpHeaderAcceptRanges }, + { "Age", HttpHeaderAge }, + { "ETag", HttpHeaderEtag }, + { "Location", HttpHeaderLocation }, + { "Proxy-Authenticate", HttpHeaderProxyAuthenticate }, + { "Retry-After", HttpHeaderRetryAfter }, + { "Server", HttpHeaderServer }, + // Set it to something which cannot be a header name, in effect + // making Server an unknown header. w:w is used to avoid collision with Keep-Alive. + { "w:w\r\n", HttpHeaderServer }, + // Set it to something which cannot be a header name, in effect + // making Set-Cookie an unknown header + { "y:y\r\n", HttpHeaderSetCookie }, + { "Vary", HttpHeaderVary }, + // Set it to something which cannot be a header name, in effect + // making WWW-Authenticate an unknown header + { "z:z\r\n", HttpHeaderWwwAuthenticate } + +}; + +HRESULT +RESPONSE_HEADER_HASH::Initialize( + VOID +) +/*++ + +Routine Description: + + Initialize global header hash table + +Arguments: + + None + +Return Value: + + HRESULT + +--*/ +{ + HRESULT hr; + + // + // 31 response headers. + // Make sure to update the number of buckets it new headers + // are added. Test it to avoid collisions. + // + C_ASSERT(_countof(sm_rgHeaders) == 31); + + // + // 79 buckets will have less collisions for the 31 response headers. + // Known collisions are "Age" colliding with "Expire" and "Location" + // colliding with both "Expire" and "Age". + // + hr = HASH_TABLE::Initialize(79); + if (FAILED(hr)) + { + return hr; + } + + for ( DWORD Index = 0; Index < _countof(sm_rgHeaders); ++Index ) + { + if (FAILED(hr = InsertRecord(&sm_rgHeaders[Index]))) + { + return hr; + } + } + + return S_OK; +} + diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/serverprocess.cxx b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/serverprocess.cxx new file mode 100644 index 0000000000..b3fd02432a --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/serverprocess.cxx @@ -0,0 +1,2387 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "precomp.hxx" +#include +#include + +extern BOOL g_fNsiApiNotSupported; + +#define STARTUP_TIME_LIMIT_INCREMENT_IN_MILLISECONDS 5000 + +HRESULT +SERVER_PROCESS::Initialize( + PROCESS_MANAGER *pProcessManager, + STRU *pszProcessExePath, + STRU *pszArguments, + DWORD dwStartupTimeLimitInMS, + DWORD dwShtudownTimeLimitInMS, + BOOL fWindowsAuthEnabled, + BOOL fBasicAuthEnabled, + BOOL fAnonymousAuthEnabled, + ENVIRONMENT_VAR_HASH *pEnvironmentVariables, + BOOL fStdoutLogEnabled, + STRU *pstruStdoutLogFile +) +{ + HRESULT hr = S_OK; + JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo = { 0 }; + + m_pProcessManager = pProcessManager; + m_dwStartupTimeLimitInMS = dwStartupTimeLimitInMS; + m_dwShutdownTimeLimitInMS = dwShtudownTimeLimitInMS; + m_fStdoutLogEnabled = fStdoutLogEnabled; + m_fWindowsAuthEnabled = fWindowsAuthEnabled; + m_fBasicAuthEnabled = fBasicAuthEnabled; + m_fAnonymousAuthEnabled = fAnonymousAuthEnabled; + m_pProcessManager->ReferenceProcessManager(); + m_fDebuggerAttached = FALSE; + + if (FAILED (hr = m_ProcessPath.Copy(*pszProcessExePath)) || + FAILED (hr = m_struLogFile.Copy(*pstruStdoutLogFile))|| + FAILED (hr = m_Arguments.Copy(*pszArguments))) + { + goto Finished; + } + + if (m_hJobObject == NULL) + { + m_hJobObject = CreateJobObject(NULL, // LPSECURITY_ATTRIBUTES + NULL); // LPCTSTR lpName +#pragma warning( disable : 4312) + // 0xdeadbeef is used by Antares + if (m_hJobObject == NULL || m_hJobObject == (HANDLE)0xdeadbeef) + { + m_hJobObject = NULL; + // ignore job object creation error. + } +#pragma warning( error : 4312) + if (m_hJobObject != NULL) + { + jobInfo.BasicLimitInformation.LimitFlags = + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + + if (!SetInformationJobObject(m_hJobObject, + JobObjectExtendedLimitInformation, + &jobInfo, + sizeof jobInfo)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + } + + m_pEnvironmentVarTable = pEnvironmentVariables; + } + +Finished: + return hr; +} + +HRESULT +SERVER_PROCESS::GetRandomPort +( + DWORD* pdwPickedPort, + DWORD dwExcludedPort = 0 +) +{ + HRESULT hr = S_OK; + BOOL fPortInUse = FALSE; + DWORD dwActualProcessId = 0; + + std::uniform_int_distribution<> dist(MIN_PORT, MAX_PORT); + + if (g_fNsiApiNotSupported) + { + // + // the default value for optional parameter dwExcludedPort is 0 which is reserved + // a random number between MIN_PORT and MAX_PORT + // + while ((*pdwPickedPort = dist(m_randomGenerator)) == dwExcludedPort); + } + else + { + DWORD cRetry = 0; + do + { + // + // ignore dwActualProcessId because here we are + // determing whether the randomly generated port is + // in use by any other process. + // + while ((*pdwPickedPort = dist(m_randomGenerator)) == dwExcludedPort); + hr = CheckIfServerIsUp(*pdwPickedPort, &dwActualProcessId, &fPortInUse); + } while (fPortInUse && ++cRetry < MAX_RETRY); + + if (cRetry >= MAX_RETRY) + { + hr = HRESULT_FROM_WIN32(ERROR_PORT_NOT_SET); + } + } + + return hr; +} + +HRESULT +SERVER_PROCESS::SetupListenPort( + ENVIRONMENT_VAR_HASH *pEnvironmentVarTable +) +{ + HRESULT hr = S_OK; + ENVIRONMENT_VAR_ENTRY *pEntry = NULL; + pEnvironmentVarTable->FindKey(ASPNETCORE_PORT_ENV_STR, &pEntry); + if (pEntry != NULL) + { + if (pEntry->QueryValue() != NULL && pEntry->QueryValue()[0] != L'\0') + { + m_dwPort = (DWORD)_wtoi(pEntry->QueryValue()); + if(m_dwPort >MAX_PORT || m_dwPort < MIN_PORT) + { + hr = E_INVALIDARG; + goto Finished; + // need add log for this one + } + hr = m_struPort.Copy(pEntry->QueryValue()); + goto Finished; + } + else + { + // + // user set the env variable but did not give value, let's set it up + // + pEnvironmentVarTable->DeleteKey(ASPNETCORE_PORT_ENV_STR); + pEntry->Dereference(); + pEntry = NULL; + } + } + + WCHAR buffer[15]; + if (FAILED(hr = GetRandomPort(&m_dwPort))) + { + goto Finished; + } + + if (swprintf_s(buffer, 15, L"%d", m_dwPort) <= 0) + { + hr = E_INVALIDARG; + goto Finished; + } + + pEntry = new ENVIRONMENT_VAR_ENTRY(); + if (pEntry == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + if (FAILED(hr = pEntry->Initialize(ASPNETCORE_PORT_ENV_STR, buffer)) || + FAILED(hr = pEnvironmentVarTable->InsertRecord(pEntry)) || + FAILED(hr = m_struPort.Copy(buffer))) + { + goto Finished; + } + +Finished: + if (pEntry != NULL) + { + pEntry->Dereference(); + pEntry = NULL; + } + return hr; +} + +HRESULT +SERVER_PROCESS::SetupAppPath( + IHttpContext* pContext, + ENVIRONMENT_VAR_HASH* pEnvironmentVarTable +) +{ + HRESULT hr = S_OK; + DWORD dwCounter = 0; + DWORD dwPosition = 0; + WCHAR* pszPath = NULL; + ENVIRONMENT_VAR_ENTRY* pEntry = NULL; + + pEnvironmentVarTable->FindKey(ASPNETCORE_APP_PATH_ENV_STR, &pEntry); + if (pEntry != NULL) + { + // user should not set this environment variable in configuration + pEnvironmentVarTable->DeleteKey(ASPNETCORE_APP_PATH_ENV_STR); + pEntry->Dereference(); + pEntry = NULL; + } + + if (m_struAppPath.IsEmpty()) + { + if (FAILED(hr = m_pszRootApplicationPath.Copy(pContext->GetApplication()->GetApplicationPhysicalPath())) || + FAILED(hr = m_struAppFullPath.Copy(pContext->GetApplication()->GetAppConfigPath()))) + { + goto Finished; + } + } + + // let's find the app path. IIS does not support nested sites + // we can seek for the fourth '/' if it exits + // MACHINE/WEBROOT/APPHOST//. + pszPath = m_struAppFullPath.QueryStr(); + while (pszPath[dwPosition] != NULL) + { + if (pszPath[dwPosition] == '/') + { + dwCounter++; + if (dwCounter == 4) + break; + } + dwPosition++; + } + + if (dwCounter == 4) + { + hr = m_struAppPath.Copy(pszPath + dwPosition); + } + else + { + hr = m_struAppPath.Copy(L"/"); + } + + if (FAILED(hr)) + { + goto Finished; + } + + pEntry = new ENVIRONMENT_VAR_ENTRY(); + if (pEntry == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + if (FAILED (hr = pEntry->Initialize(ASPNETCORE_APP_PATH_ENV_STR, m_struAppPath.QueryStr())) || + FAILED (hr = pEnvironmentVarTable->InsertRecord(pEntry))) + { + goto Finished; + } + +Finished: + if (pEntry!= NULL) + { + pEntry->Dereference(); + pEntry = NULL; + } + return hr; +} + +HRESULT +SERVER_PROCESS::SetupAppToken( + ENVIRONMENT_VAR_HASH *pEnvironmentVarTable +) +{ + HRESULT hr = S_OK; + UUID logUuid; + PSTR pszLogUuid = NULL; + BOOL fRpcStringAllocd = FALSE; + RPC_STATUS rpcStatus; + STRU strAppToken; + ENVIRONMENT_VAR_ENTRY* pEntry = NULL; + + pEnvironmentVarTable->FindKey(ASPNETCORE_APP_TOKEN_ENV_STR, &pEntry); + if (pEntry != NULL) + { + // user sets the environment variable + m_straGuid.Reset(); + hr = m_straGuid.CopyW(pEntry->QueryValue()); + pEntry->Dereference(); + pEntry = NULL; + goto Finished; + } + else + { + if (m_straGuid.IsEmpty()) + { + // the GUID has not been set yet + rpcStatus = UuidCreate(&logUuid); + if (rpcStatus != RPC_S_OK) + { + hr = rpcStatus; + goto Finished; + } + + rpcStatus = UuidToStringA(&logUuid, (BYTE **)&pszLogUuid); + if (rpcStatus != RPC_S_OK) + { + hr = rpcStatus; + goto Finished; + } + + fRpcStringAllocd = TRUE; + + if (FAILED (hr = m_straGuid.Copy(pszLogUuid))) + { + goto Finished; + } + } + + pEntry = new ENVIRONMENT_VAR_ENTRY(); + if (pEntry == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + if (FAILED(strAppToken.CopyA(m_straGuid.QueryStr())) || + FAILED(hr = pEntry->Initialize(ASPNETCORE_APP_TOKEN_ENV_STR, strAppToken.QueryStr())) || + FAILED(hr = pEnvironmentVarTable->InsertRecord(pEntry))) + { + goto Finished; + } + } + +Finished: + + if (fRpcStringAllocd) + { + RpcStringFreeA((BYTE **)&pszLogUuid); + pszLogUuid = NULL; + } + if (pEntry != NULL) + { + pEntry->Dereference(); + pEntry = NULL; + } + return hr; +} + + +HRESULT +SERVER_PROCESS::InitEnvironmentVariablesTable( + ENVIRONMENT_VAR_HASH** ppEnvironmentVarTable +) +{ + HRESULT hr = S_OK; + BOOL fFound = FALSE; + DWORD dwResult, dwError; + STRU strIisAuthEnvValue; + STACK_STRU(strStartupAssemblyEnv, 1024); + ENVIRONMENT_VAR_ENTRY* pHostingEntry = NULL; + ENVIRONMENT_VAR_ENTRY* pIISAuthEntry = NULL; + ENVIRONMENT_VAR_HASH* pEnvironmentVarTable = NULL; + + pEnvironmentVarTable = new ENVIRONMENT_VAR_HASH(); + if (pEnvironmentVarTable == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + // + // few environment variables expected, small bucket size for hash table + // + if (FAILED(hr = pEnvironmentVarTable->Initialize(37 /*prime*/))) + { + goto Finished; + } + + // copy the envirable hash table (from configuration) to a temp one as we may need to remove elements + m_pEnvironmentVarTable->Apply(ENVIRONMENT_VAR_HASH::CopyToTable, pEnvironmentVarTable); + if (pEnvironmentVarTable->Count() != m_pEnvironmentVarTable->Count()) + { + // hash table copy failed + hr = E_UNEXPECTED; + goto Finished; + } + + pEnvironmentVarTable->FindKey(ASPNETCORE_IIS_AUTH_ENV_STR, &pIISAuthEntry); + if (pIISAuthEntry != NULL) + { + // user defined ASPNETCORE_IIS_HTTPAUTH in configuration, wipe it off + pIISAuthEntry->Dereference(); + pEnvironmentVarTable->DeleteKey(ASPNETCORE_IIS_AUTH_ENV_STR); + } + + if (m_fWindowsAuthEnabled) + { + strIisAuthEnvValue.Copy(ASPNETCORE_IIS_AUTH_WINDOWS); + } + + if (m_fBasicAuthEnabled) + { + strIisAuthEnvValue.Append(ASPNETCORE_IIS_AUTH_BASIC); + } + + if (m_fAnonymousAuthEnabled) + { + strIisAuthEnvValue.Append(ASPNETCORE_IIS_AUTH_ANONYMOUS); + } + + if (strIisAuthEnvValue.IsEmpty()) + { + strIisAuthEnvValue.Copy(ASPNETCORE_IIS_AUTH_NONE); + } + + pIISAuthEntry = new ENVIRONMENT_VAR_ENTRY(); + if (pIISAuthEntry == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + if (FAILED(hr = pIISAuthEntry->Initialize(ASPNETCORE_IIS_AUTH_ENV_STR, strIisAuthEnvValue.QueryStr())) || + FAILED(hr = pEnvironmentVarTable->InsertRecord(pIISAuthEntry))) + { + goto Finished; + } + + + pEnvironmentVarTable->FindKey(HOSTING_STARTUP_ASSEMBLIES_NAME, &pHostingEntry); + if (pHostingEntry !=NULL ) + { + // user defined ASPNETCORE_HOSTINGSTARTUPASSEMBLIES in configuration + // the value will be used in OutputEnvironmentVariables. Do nothing here + pHostingEntry->Dereference(); + pHostingEntry = NULL; + goto Skipped; + } + + //check whether ASPNETCORE_HOSTINGSTARTUPASSEMBLIES is defined in system + dwResult = GetEnvironmentVariable(HOSTING_STARTUP_ASSEMBLIES_ENV_STR, + strStartupAssemblyEnv.QueryStr(), + strStartupAssemblyEnv.QuerySizeCCH()); + if (dwResult == 0) + { + dwError = GetLastError(); + + // Windows API (e.g., CreateProcess) allows variable with empty string value + // in such case dwResult will be 0 and dwError will also be 0 + // As UI and CMD does not allow empty value, ignore this environment var + if (dwError != ERROR_ENVVAR_NOT_FOUND && dwError != ERROR_SUCCESS) + { + hr = HRESULT_FROM_WIN32(dwError); + goto Finished; + } + } + else if (dwResult > strStartupAssemblyEnv.QuerySizeCCH()) + { + // have to increase the buffer and try get environment var again + strStartupAssemblyEnv.Reset(); + strStartupAssemblyEnv.Resize(dwResult + (DWORD)wcslen(HOSTING_STARTUP_ASSEMBLIES_VALUE) +1); + dwResult = GetEnvironmentVariable(HOSTING_STARTUP_ASSEMBLIES_ENV_STR, + strStartupAssemblyEnv.QueryStr(), + strStartupAssemblyEnv.QuerySizeCCH()); + if (strStartupAssemblyEnv.IsEmpty()) + { + hr = E_UNEXPECTED; + goto Finished; + } + fFound = TRUE; + } + else + { + fFound = TRUE; + } + + strStartupAssemblyEnv.SyncWithBuffer(); + if (fFound) + { + strStartupAssemblyEnv.Append(L";"); + } + strStartupAssemblyEnv.Append(HOSTING_STARTUP_ASSEMBLIES_VALUE); + + // the environment variable was not defined, create it and add to hashtable + pHostingEntry = new ENVIRONMENT_VAR_ENTRY(); + if (pHostingEntry == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + if (FAILED(hr = pHostingEntry->Initialize(HOSTING_STARTUP_ASSEMBLIES_NAME, strStartupAssemblyEnv.QueryStr())) || + FAILED(hr = pEnvironmentVarTable->InsertRecord(pHostingEntry))) + { + goto Finished; + } + +Skipped: + *ppEnvironmentVarTable = pEnvironmentVarTable; + pEnvironmentVarTable = NULL; + +Finished: + if (pHostingEntry != NULL) + { + pHostingEntry->Dereference(); + pHostingEntry = NULL; + } + + if (pIISAuthEntry != NULL) + { + pIISAuthEntry->Dereference(); + pIISAuthEntry = NULL; + } + + if (pEnvironmentVarTable != NULL) + { + pEnvironmentVarTable->Clear(); + delete pEnvironmentVarTable; + pEnvironmentVarTable = NULL; + } + return hr; +} + +HRESULT +SERVER_PROCESS::OutputEnvironmentVariables +( + MULTISZ* pmszOutput, + ENVIRONMENT_VAR_HASH* pEnvironmentVarTable +) +{ + HRESULT hr = S_OK; + LPWSTR pszEnvironmentVariables = NULL; + LPWSTR pszCurrentVariable = NULL; + LPWSTR pszNextVariable = NULL; + LPWSTR pszEqualChar = NULL; + STRU strEnvVar; + ENVIRONMENT_VAR_ENTRY* pEntry = NULL; + + DBG_ASSERT(pmszOutput); + DBG_ASSERT(pEnvironmentVarTable); // We added some startup variables + DBG_ASSERT(pEnvironmentVarTable->Count() >0); + + pszEnvironmentVariables = GetEnvironmentStringsW(); + if (pszEnvironmentVariables == NULL) + { + hr = HRESULT_FROM_WIN32(ERROR_INVALID_ENVIRONMENT); + goto Finished; + } + pszCurrentVariable = pszEnvironmentVariables; + while (*pszCurrentVariable != L'\0') + { + pszNextVariable = pszCurrentVariable + wcslen(pszCurrentVariable) + 1; + pszEqualChar = wcschr(pszCurrentVariable, L'='); + if (pszEqualChar != NULL) + { + if (FAILED(hr = strEnvVar.Copy(pszCurrentVariable, (DWORD)(pszEqualChar - pszCurrentVariable) + 1))) + { + goto Finished; + } + pEnvironmentVarTable->FindKey(strEnvVar.QueryStr(), &pEntry); + if (pEntry != NULL) + { + // same env variable is defined in configuration, use it + if (FAILED(hr = strEnvVar.Append(pEntry->QueryValue()))) + { + goto Finished; + } + pmszOutput->Append(strEnvVar); //should we check the returned bool + // remove the record from hash table as we already output it + pEntry->Dereference(); + pEnvironmentVarTable->DeleteKey(pEntry->QueryName()); + strEnvVar.Reset(); + pEntry = NULL; + } + else + { + pmszOutput->Append(pszCurrentVariable); + } + } + else + { + // env varaible is not well formated + hr = HRESULT_FROM_WIN32(ERROR_INVALID_ENVIRONMENT); + goto Finished; + } + // move to next env variable + pszCurrentVariable = pszNextVariable; + } + // append the remaining env variable in hash table + pEnvironmentVarTable->Apply(ENVIRONMENT_VAR_HASH::CopyToMultiSz, pmszOutput); + +Finished: + if (pszEnvironmentVariables != NULL) + { + FreeEnvironmentStringsW(pszEnvironmentVariables); + pszEnvironmentVariables = NULL; + } + return hr; +} + +HRESULT +SERVER_PROCESS::SetupCommandLine( + STRU* pstrCommandLine +) +{ + HRESULT hr = S_OK; + LPWSTR pszPath = NULL; + LPWSTR pszFullPath = NULL; + STRU strRelativePath; + DWORD dwBufferSize = 0; + FILE *file = NULL; + + DBG_ASSERT(pstrCommandLine); + + pszPath = m_ProcessPath.QueryStr(); + + if ((wcsstr(pszPath, L":") == NULL) && (wcsstr(pszPath, L"%") == NULL)) + { + // let's check whether it is a relative path + if (FAILED(hr = strRelativePath.Copy(m_pszRootApplicationPath.QueryStr())) || + FAILED(hr = strRelativePath.Append(L"\\")) || + FAILED(hr = strRelativePath.Append(pszPath))) + { + goto Finished; + } + + dwBufferSize = strRelativePath.QueryCCH() + 1; + pszFullPath = new WCHAR[dwBufferSize]; + if (pszFullPath == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + if (_wfullpath(pszFullPath, + strRelativePath.QueryStr(), + dwBufferSize) == NULL) + { + hr = HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER); + goto Finished; + } + + if ((file = _wfsopen(pszFullPath, L"r", _SH_DENYNO)) != NULL) + { + fclose(file); + pszPath = pszFullPath; + } + } + if (FAILED(hr = pstrCommandLine->Copy(pszPath)) || + FAILED(hr = pstrCommandLine->Append(L" ")) || + FAILED(hr = pstrCommandLine->Append(m_Arguments.QueryStr()))) + { + goto Finished; + } + +Finished: + if (pszFullPath != NULL) + { + delete pszFullPath; + } + return hr; +} + + +HRESULT +SERVER_PROCESS::PostStartCheck( + const STRU* const pStruCommandline, + STRU* pStruErrorMessage) +{ + HRESULT hr = S_OK; + + BOOL fReady = FALSE; + BOOL fProcessMatch = FALSE; + BOOL fDebuggerAttached = FALSE; + DWORD dwTickCount = 0; + DWORD dwTimeDifference = 0; + DWORD dwActualProcessId = 0; + INT iChildProcessIndex = -1; + + if (CheckRemoteDebuggerPresent(m_hProcessHandle, &fDebuggerAttached) == 0) + { + // some error occurred - assume debugger is not attached; + fDebuggerAttached = FALSE; + } + + dwTickCount = GetTickCount(); + do + { + DWORD processStatus; + if (GetExitCodeProcess(m_hProcessHandle, &processStatus)) + { + // make sure the process is still running + if (processStatus != STILL_ACTIVE) + { + hr = E_FAIL; + pStruErrorMessage->SafeSnwprintf( + ASPNETCORE_EVENT_PROCESS_START_ERROR_MSG, + m_struAppFullPath.QueryStr(), + m_pszRootApplicationPath.QueryStr(), + pStruCommandline->QueryStr(), + hr, + processStatus); + goto Finished; + } + } + // + // dwActualProcessId will be set only when NsiAPI(GetExtendedTcpTable) is supported + // + hr = CheckIfServerIsUp(m_dwPort, &dwActualProcessId, &fReady); + fDebuggerAttached = IsDebuggerIsAttached(); + + if (!fReady) + { + Sleep(250); + } + + dwTimeDifference = (GetTickCount() - dwTickCount); + } while (fReady == FALSE && + ((dwTimeDifference < m_dwStartupTimeLimitInMS) || fDebuggerAttached)); + + // register call back with the created process + if (FAILED(hr = RegisterProcessWait(&m_hProcessWaitHandle, m_hProcessHandle))) + { + goto Finished; + } + + // + // check if debugger is attached after startupTimeout. + // + if (!fDebuggerAttached && + CheckRemoteDebuggerPresent(m_hProcessHandle, &fDebuggerAttached) == 0) + { + // some error occurred - assume debugger is not attached; + fDebuggerAttached = FALSE; + } + if (!g_fNsiApiNotSupported) + { + // + // NsiAPI(GetExtendedTcpTable) is supported. we should check whether processIds matche + // + if (dwActualProcessId == m_dwProcessId) + { + m_dwListeningProcessId = m_dwProcessId; + fProcessMatch = TRUE; + } + + if (!fProcessMatch) + { + // could be the scenario that backend creates child process + if (FAILED(hr = GetChildProcessHandles())) + { + goto Finished; + } + + for (DWORD i = 0; i < m_cChildProcess; ++i) + { + // a child process listen on the assigned port + if (dwActualProcessId == m_dwChildProcessIds[i]) + { + m_dwListeningProcessId = m_dwChildProcessIds[i]; + fProcessMatch = TRUE; + + if (m_hChildProcessHandles[i] != NULL) + { + if (fDebuggerAttached == FALSE && + CheckRemoteDebuggerPresent(m_hChildProcessHandles[i], &fDebuggerAttached) == 0) + { + // some error occurred - assume debugger is not attached; + fDebuggerAttached = FALSE; + } + + if (FAILED(hr = RegisterProcessWait(&m_hChildProcessWaitHandles[i], + m_hChildProcessHandles[i]))) + { + goto Finished; + } + iChildProcessIndex = i; + } + break; + } + } + } + + if(!fProcessMatch) + { + // + // process that we created is not listening + // on the port we specified. + // + fReady = FALSE; + pStruErrorMessage->SafeSnwprintf( + ASPNETCORE_EVENT_PROCESS_START_WRONGPORT_ERROR_MSG, + m_struAppFullPath.QueryStr(), + m_pszRootApplicationPath.QueryStr(), + pStruCommandline->QueryStr(), + m_dwPort, + hr); + hr = HRESULT_FROM_WIN32(ERROR_CREATE_FAILED); + goto Finished; + } + } + + if (!fReady) + { + // + // hr is already set by CheckIfServerIsUp + // + if (dwTimeDifference >= m_dwStartupTimeLimitInMS) + { + hr = HRESULT_FROM_WIN32(ERROR_TIMEOUT); + pStruErrorMessage->SafeSnwprintf( + ASPNETCORE_EVENT_PROCESS_START_NOTREADY_ERROR_MSG, + m_struAppFullPath.QueryStr(), + m_pszRootApplicationPath.QueryStr(), + pStruCommandline->QueryStr(), + m_dwPort, + hr); + } + goto Finished; + } + + if (iChildProcessIndex >= 0) + { + // + // final check to make sure child process listening on HTTP is still UP + // This is needed because, the child process might have crashed/exited between + // the previous call to checkIfServerIsUp and RegisterProcessWait + // and we would not know about it. + // + + hr = CheckIfServerIsUp(m_dwPort, &dwActualProcessId, &fReady); + + if ((FAILED(hr) || fReady == FALSE)) + { + pStruErrorMessage->SafeSnwprintf( + ASPNETCORE_EVENT_PROCESS_START_NOTREADY_ERROR_MSG, + m_struAppFullPath.QueryStr(), + m_pszRootApplicationPath.QueryStr(), + pStruCommandline->QueryStr(), + m_dwPort, + hr); + goto Finished; + } + } + + // + // ready to mark the server process ready but before this, + // create and initialize the FORWARDER_CONNECTION + // + if (m_pForwarderConnection != NULL) + { + m_pForwarderConnection->DereferenceForwarderConnection(); + m_pForwarderConnection = NULL; + } + + if (m_pForwarderConnection == NULL) + { + m_pForwarderConnection = new FORWARDER_CONNECTION(); + if (m_pForwarderConnection == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + hr = m_pForwarderConnection->Initialize(m_dwPort); + if (FAILED(hr)) + { + goto Finished; + } + } + + if (!g_fNsiApiNotSupported) + { + m_hListeningProcessHandle = OpenProcess(SYNCHRONIZE | PROCESS_TERMINATE | PROCESS_DUP_HANDLE, + FALSE, + m_dwListeningProcessId); + } + + // + // mark server process as Ready + // + m_fReady = TRUE; + +Finished: + m_fDebuggerAttached = fDebuggerAttached; + return hr; +} + +HRESULT +SERVER_PROCESS::StartProcess( + IHttpContext *context +) +{ + HRESULT hr = S_OK; + PROCESS_INFORMATION processInformation = {0}; + STARTUPINFOW startupInfo = {0}; + BOOL fDonePrepareCommandLine = FALSE; + DWORD dwCreationFlags = 0; + + STACK_STRU( strEventMsg, 256); + STRU strFullProcessPath; + STRU struRelativePath; + STRU struApplicationId; + STRU struCommandLine; + + LPCWSTR apsz[1]; + + MULTISZ mszNewEnvironment; + ENVIRONMENT_VAR_HASH *pHashTable = NULL; + + GetStartupInfoW(&startupInfo); + + // + // setup stdout and stderr handles to our stdout handle only if + // the handle is valid. + // + SetupStdHandles(context, &startupInfo); + + if (FAILED(hr = InitEnvironmentVariablesTable(&pHashTable))) + { + goto Finished; + } + + // + // setup the the port that the backend process will listen on + // + if (FAILED (hr= SetupListenPort(pHashTable))) + { + goto Finished; + } + + // + // get app path + // + if (FAILED(hr = SetupAppPath(context, pHashTable))) + { + goto Finished; + } + + // + // generate new guid for each process + // + if (FAILED(hr = SetupAppToken(pHashTable))) + { + goto Finished; + } + + // + // setup environment variables for new process + // + if (FAILED(hr = OutputEnvironmentVariables(&mszNewEnvironment, pHashTable))) + { + goto Finished; + } + + // + // generate process command line. + // + if (FAILED(hr = SetupCommandLine(&struCommandLine))) + { + goto Finished; + } + + fDonePrepareCommandLine = TRUE; + + dwCreationFlags = CREATE_NO_WINDOW | + CREATE_UNICODE_ENVIRONMENT | + CREATE_SUSPENDED | + CREATE_NEW_PROCESS_GROUP; + + if (!CreateProcessW( + NULL, // applicationName + struCommandLine.QueryStr(), + NULL, // processAttr + NULL, // threadAttr + TRUE, // inheritHandles + dwCreationFlags, + mszNewEnvironment.QueryStr(), + m_pszRootApplicationPath.QueryStr(), // currentDir + &startupInfo, + &processInformation) ) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + // don't the check return code as we already in error report + strEventMsg.SafeSnwprintf( + ASPNETCORE_EVENT_PROCESS_START_ERROR_MSG, + m_struAppFullPath.QueryStr(), + m_pszRootApplicationPath.QueryStr(), + struCommandLine.QueryStr(), + hr, + 0); + goto Finished; + } + + m_hProcessHandle = processInformation.hProcess; + m_dwProcessId = processInformation.dwProcessId; + + if (m_hJobObject != NULL) + { + if (!AssignProcessToJobObject(m_hJobObject, m_hProcessHandle)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + if (hr != HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)) + { + goto Finished; + } + } + } + + if (ResumeThread( processInformation.hThread ) == -1) + { + hr = HRESULT_FROM_WIN32( GetLastError() ); + goto Finished; + } + + // + // need to make sure the server is up and listening on the port specified. + // + if (FAILED(hr = PostStartCheck(&struCommandLine, &strEventMsg))) + { + goto Finished; + } + + + if (SUCCEEDED(strEventMsg.SafeSnwprintf( + ASPNETCORE_EVENT_PROCESS_START_SUCCESS_MSG, + m_struAppFullPath.QueryStr(), + m_dwProcessId, + m_dwPort))) + { + apsz[0] = strEventMsg.QueryStr(); + + // + // not checking return code because if ReportEvent + // fails, we cannot do anything. + // + if (FORWARDING_HANDLER::QueryEventLog() != NULL) + { + ReportEventW(FORWARDING_HANDLER::QueryEventLog(), + EVENTLOG_INFORMATION_TYPE, + 0, + ASPNETCORE_EVENT_PROCESS_START_SUCCESS, + NULL, + 1, + 0, + apsz, + NULL); + } + } + +Finished: + if (processInformation.hThread != NULL) + { + CloseHandle(processInformation.hThread); + processInformation.hThread = NULL; + } + + if (pHashTable != NULL) + { + pHashTable->Clear(); + delete pHashTable; + pHashTable = NULL; + } + + if ( FAILED(hr) ) + { + if (strEventMsg.IsEmpty()) + { + if (!fDonePrepareCommandLine) + strEventMsg.SafeSnwprintf( + ASPNETCORE_EVENT_PROCESS_START_INTERNAL_ERROR_MSG, + m_struAppFullPath.QueryStr(), + hr); + else + strEventMsg.SafeSnwprintf( + ASPNETCORE_EVENT_PROCESS_START_POSTCREATE_ERROR_MSG, + m_struAppFullPath.QueryStr(), + m_pszRootApplicationPath.QueryStr(), + struCommandLine.QueryStr(), + hr); + } + + apsz[0] = strEventMsg.QueryStr(); + + // not checking return code because if ReportEvent + // fails, we cannot do anything. + // + if (FORWARDING_HANDLER::QueryEventLog() != NULL) + { + ReportEventW(FORWARDING_HANDLER::QueryEventLog(), + EVENTLOG_ERROR_TYPE, + 0, + ASPNETCORE_EVENT_PROCESS_START_ERROR, + NULL, + 1, + 0, + apsz, + NULL); + } + } + + if ( FAILED( hr ) || m_fReady == FALSE) + { + if (m_hStdoutHandle != NULL) + { + if ( m_hStdoutHandle != INVALID_HANDLE_VALUE ) + { + CloseHandle( m_hStdoutHandle ); + } + m_hStdoutHandle = NULL; + } + + if ( m_fStdoutLogEnabled ) + { + m_Timer.CancelTimer(); + } + + if (m_hListeningProcessHandle != NULL) + { + if( m_hListeningProcessHandle != INVALID_HANDLE_VALUE ) + { + CloseHandle( m_hListeningProcessHandle ); + } + m_hListeningProcessHandle = NULL; + } + + if ( m_hProcessWaitHandle != NULL ) + { + UnregisterWait( m_hProcessWaitHandle ); + m_hProcessWaitHandle = NULL; + } + + StopProcess(); + + StopAllProcessesInJobObject(); + } + return hr; +} + +HRESULT +SERVER_PROCESS::SetWindowsAuthToken( + HANDLE hToken, + LPHANDLE pTargetTokenHandle +) +{ + HRESULT hr = S_OK; + + if ( m_hListeningProcessHandle != NULL && m_hListeningProcessHandle != INVALID_HANDLE_VALUE ) + { + if (!DuplicateHandle( GetCurrentProcess(), + hToken, + m_hListeningProcessHandle, + pTargetTokenHandle, + 0, + FALSE, + DUPLICATE_SAME_ACCESS )) + { + hr = HRESULT_FROM_GETLASTERROR(); + goto Finished; + } + } + +Finished: + + return hr; +} + +HRESULT +SERVER_PROCESS::SetupStdHandles( + IHttpContext *context, + LPSTARTUPINFOW pStartupInfo +) +{ + SECURITY_ATTRIBUTES saAttr = {0}; + HRESULT hr = S_OK; + SYSTEMTIME systemTime; + STRU struLogFileName; + BOOL fStdoutLoggingFailed = FALSE; + STRU strEventMsg; + LPCWSTR apsz[1]; + STRU struAbsLogFilePath; + + DBG_ASSERT(pStartupInfo); + + if ( m_fStdoutLogEnabled ) + { + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = TRUE; + saAttr.lpSecurityDescriptor = NULL; + + if (m_hStdoutHandle != NULL) + { + if(!CloseHandle( m_hStdoutHandle )) + { + hr = HRESULT_FROM_GETLASTERROR(); + goto Finished; + } + + m_hStdoutHandle = NULL; + } + + hr = PATH::ConvertPathToFullPath( m_struLogFile.QueryStr(), + context->GetApplication()->GetApplicationPhysicalPath(), + &struAbsLogFilePath ); + if (FAILED(hr)) + { + goto Finished; + } + + GetSystemTime(&systemTime); + hr = struLogFileName.SafeSnwprintf( L"%s_%d_%d%d%d%d%d%d.log", + struAbsLogFilePath.QueryStr(), + GetCurrentProcessId(), + systemTime.wYear, + systemTime.wMonth, + systemTime.wDay, + systemTime.wHour, + systemTime.wMinute, + systemTime.wSecond ); + if (FAILED(hr)) + { + goto Finished; + } + + m_hStdoutHandle = CreateFileW( struLogFileName.QueryStr(), + FILE_WRITE_DATA, + FILE_SHARE_READ, + &saAttr, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + NULL ); + if ( m_hStdoutHandle == INVALID_HANDLE_VALUE ) + { + fStdoutLoggingFailed = TRUE; + m_hStdoutHandle = NULL; + + if( SUCCEEDED( strEventMsg.SafeSnwprintf( + ASPNETCORE_EVENT_INVALID_STDOUT_LOG_FILE_MSG, + struLogFileName.QueryStr(), + HRESULT_FROM_GETLASTERROR() ) ) ) + { + apsz[0] = strEventMsg.QueryStr(); + + // + // not checking return code because if ReportEvent + // fails, we cannot do anything. + // + if (FORWARDING_HANDLER::QueryEventLog() != NULL) + { + ReportEventW(FORWARDING_HANDLER::QueryEventLog(), + EVENTLOG_WARNING_TYPE, + 0, + ASPNETCORE_EVENT_CONFIG_ERROR, + NULL, + 1, + 0, + apsz, + NULL); + } + } + } + + if( !fStdoutLoggingFailed ) + { + pStartupInfo->dwFlags = STARTF_USESTDHANDLES; + pStartupInfo->hStdInput = INVALID_HANDLE_VALUE; + pStartupInfo->hStdError = m_hStdoutHandle; + pStartupInfo->hStdOutput = m_hStdoutHandle; + + m_struFullLogFile.Copy( struLogFileName ); + + // start timer to open and close handles regularly. + m_Timer.InitializeTimer(SERVER_PROCESS::TimerCallback, this, 3000, 3000); + } + } + + if( (!m_fStdoutLogEnabled || fStdoutLoggingFailed) && + m_pProcessManager->QueryNULHandle() != NULL && + m_pProcessManager->QueryNULHandle() != INVALID_HANDLE_VALUE ) + { + pStartupInfo->dwFlags = STARTF_USESTDHANDLES; + pStartupInfo->hStdInput = INVALID_HANDLE_VALUE; + pStartupInfo->hStdError = m_pProcessManager->QueryNULHandle(); + pStartupInfo->hStdOutput = m_pProcessManager->QueryNULHandle(); + } + +Finished: + + return hr; +} + +VOID +CALLBACK +SERVER_PROCESS::TimerCallback( + IN PTP_CALLBACK_INSTANCE Instance, + IN PVOID Context, + IN PTP_TIMER Timer +) +{ + Instance; + Timer; + SERVER_PROCESS* pServerProcess = (SERVER_PROCESS*) Context; + HANDLE hStdoutHandle = NULL; + SECURITY_ATTRIBUTES saAttr = {0}; + HRESULT hr = S_OK; + + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = TRUE; + saAttr.lpSecurityDescriptor = NULL; + + hStdoutHandle = CreateFileW( pServerProcess->QueryFullLogPath(), + FILE_READ_DATA, + FILE_SHARE_WRITE, + &saAttr, + OPEN_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + NULL ); + if( hStdoutHandle == INVALID_HANDLE_VALUE ) + { + hr = HRESULT_FROM_GETLASTERROR(); + } + + CloseHandle( hStdoutHandle ); +} + +HRESULT +SERVER_PROCESS::CheckIfServerIsUp( + _In_ DWORD dwPort, + _Out_ DWORD * pdwProcessId, + _Out_ BOOL * pfReady +) +{ + HRESULT hr = S_OK; + DWORD dwResult = ERROR_INSUFFICIENT_BUFFER; + MIB_TCPTABLE_OWNER_PID *pTCPInfo = NULL; + MIB_TCPROW_OWNER_PID *pOwner = NULL; + DWORD dwSize = 1000; + int iResult = 0; + SOCKADDR_IN sockAddr; + SOCKET socketCheck = INVALID_SOCKET; + + DBG_ASSERT(pfReady); + DBG_ASSERT(pdwProcessId); + + *pfReady = FALSE; + // + // it's OK for us to return processID 0 in case we cannot detect the real one + // + *pdwProcessId = 0; + + if (!g_fNsiApiNotSupported) + { + while (dwResult == ERROR_INSUFFICIENT_BUFFER) + { + // Increase the buffer size with additional space, MIB_TCPROW 20 bytes + // New entries may be added by other processes before calling GetExtendedTcpTable + dwSize += 200; + + if (pTCPInfo != NULL) + { + HeapFree(GetProcessHeap(), 0, pTCPInfo); + } + + pTCPInfo = (MIB_TCPTABLE_OWNER_PID*)HeapAlloc(GetProcessHeap(), 0, dwSize); + if (pTCPInfo == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + dwResult = GetExtendedTcpTable(pTCPInfo, + &dwSize, + FALSE, + AF_INET, + TCP_TABLE_OWNER_PID_LISTENER, + 0); + + if (dwResult != NO_ERROR && dwResult != ERROR_INSUFFICIENT_BUFFER) + { + hr = HRESULT_FROM_WIN32(dwResult); + goto Finished; + } + } + + // iterate pTcpInfo struct to find PID/PORT entry + for (DWORD dwLoop = 0; dwLoop < pTCPInfo->dwNumEntries; dwLoop++) + { + pOwner = &pTCPInfo->table[dwLoop]; + if (ntohs((USHORT)pOwner->dwLocalPort) == dwPort) + { + *pdwProcessId = pOwner->dwOwningPid; + *pfReady = TRUE; + break; + } + } + } + else + { + // + // We have to open socket to ping the service + // + socketCheck = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + + if (socketCheck == INVALID_SOCKET) + { + hr = HRESULT_FROM_WIN32(WSAGetLastError()); + goto Finished; + } + + sockAddr.sin_family = AF_INET; + if (!inet_pton(AF_INET, LOCALHOST, &(sockAddr.sin_addr))) + { + hr = HRESULT_FROM_WIN32(WSAGetLastError()); + goto Finished; + } + + //sockAddr.sin_addr.s_addr = inet_addr( LOCALHOST ); + sockAddr.sin_port = htons((u_short)dwPort); + + // + // Connect to server. + // + iResult = connect(socketCheck, (SOCKADDR *)&sockAddr, sizeof(sockAddr)); + if (iResult == SOCKET_ERROR) + { + hr = HRESULT_FROM_WIN32(WSAGetLastError()); + if (hr == HRESULT_FROM_WIN32(WSAECONNREFUSED)) + { + // WSAECONNREFUSED means no application listen on the given port. + // This is not a failure. Reset the hresult to S_OK and return fReady to false + hr = S_OK; + } + goto Finished; + } + + *pfReady = TRUE; + } + +Finished: + + if (socketCheck != INVALID_SOCKET) + { + iResult = closesocket(socketCheck); + if (iResult == SOCKET_ERROR) + { + hr = HRESULT_FROM_WIN32(WSAGetLastError()); + } + socketCheck = INVALID_SOCKET; + } + + if( pTCPInfo != NULL ) + { + HeapFree( GetProcessHeap(), 0, pTCPInfo ); + pTCPInfo = NULL; + } + + return hr; +} + +// send signal to the process to let it gracefully shutdown +// if the process cannot shutdown within given time, terminate it +VOID +SERVER_PROCESS::SendSignal( + VOID +) +{ + HRESULT hr = S_OK; + HANDLE hThread = NULL; + + ReferenceServerProcess(); + + m_hShutdownHandle = OpenProcess(SYNCHRONIZE | PROCESS_TERMINATE, FALSE, m_dwProcessId); + + if (m_hShutdownHandle == NULL) + { + // since we cannot open the process. let's terminate the process + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + hThread = CreateThread( + NULL, // default security attributes + 0, // default stack size + (LPTHREAD_START_ROUTINE)SendShutDownSignal, + this, // thread function arguments + 0, // default creation flags + NULL); // receive thread identifier + + if (hThread == NULL) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + if (WaitForSingleObject(m_hShutdownHandle, m_fDebuggerAttached ? INFINITE : m_dwShutdownTimeLimitInMS) != WAIT_OBJECT_0) + { + hr = HRESULT_FROM_WIN32(ERROR_TIMEOUT); + goto Finished; + } + + +Finished: + if (hThread != NULL) + { + // if the send shutdown message thread is still running, terminate it + DWORD dwThreadStatus = 0; + if (GetExitCodeThread(hThread, &dwThreadStatus)!= 0 && dwThreadStatus == STILL_ACTIVE) + { + TerminateThread(hThread, STATUS_CONTROL_C_EXIT); + } + CloseHandle(hThread); + hThread = NULL; + } + + if (FAILED(hr)) + { + TerminateBackendProcess(); + } + + if (m_hShutdownHandle != NULL && m_hShutdownHandle != INVALID_HANDLE_VALUE) + { + CloseHandle(m_hShutdownHandle); + m_hShutdownHandle = NULL; + } + + DereferenceServerProcess(); +} + +// +// StopProcess is only called if process crashes OR if the process +// creation failed and calling this counts towards RapidFailCounts. +// + +VOID +SERVER_PROCESS::StopProcess( + VOID +) +{ + m_fReady = FALSE; + + m_pProcessManager->IncrementRapidFailCount(); + + for(INT i=0;iNumberOfAssignedProcesses > processList->NumberOfProcessIdsInList || + processList->NumberOfProcessIdsInList == 0 ) ); + + if( dwError == ERROR_MORE_DATA ) + { + hr = E_OUTOFMEMORY; + // some error + goto Finished; + } + + if( processList == NULL || + ( processList->NumberOfAssignedProcesses > processList->NumberOfProcessIdsInList || + processList->NumberOfProcessIdsInList == 0 ) ) + { + hr = HRESULT_FROM_WIN32(ERROR_PROCESS_ABORTED); + // some error + goto Finished; + } + + if( processList->NumberOfProcessIdsInList > MAX_ACTIVE_CHILD_PROCESSES ) + { + hr = HRESULT_FROM_WIN32( ERROR_CREATE_FAILED ); + goto Finished; + } + + for( DWORD i=0; iNumberOfProcessIdsInList; i++ ) + { + dwPid = (DWORD)processList->ProcessIdList[i]; + if( dwPid != dwWorkerProcessPid ) + { + HANDLE hProcess = OpenProcess( + PROCESS_QUERY_INFORMATION | SYNCHRONIZE | PROCESS_TERMINATE | PROCESS_DUP_HANDLE, + FALSE, + dwPid + ); + + BOOL returnValue = CheckRemoteDebuggerPresent( hProcess, &fDebuggerPresent ); + if (hProcess != NULL) + { + CloseHandle(hProcess); + hProcess = NULL; + } + + if( ! returnValue ) + { + goto Finished; + } + + if( fDebuggerPresent ) + { + break; + } + } + } + +Finished: + + if( processList != NULL ) + { + HeapFree(GetProcessHeap(), 0, processList); + } + + return fDebuggerPresent; +} + +HRESULT +SERVER_PROCESS::GetChildProcessHandles( + VOID +) +{ + HRESULT hr = S_OK; + PJOBOBJECT_BASIC_PROCESS_ID_LIST processList = NULL; + DWORD dwPid = 0; + DWORD dwWorkerProcessPid = 0; + DWORD cbNumBytes = 1024; + DWORD dwRetries = 0; + DWORD dwError = NO_ERROR; + + dwWorkerProcessPid = GetCurrentProcessId(); + + do + { + dwError = NO_ERROR; + + if( processList != NULL ) + { + HeapFree(GetProcessHeap(), 0, processList); + processList = NULL; + + // resize + cbNumBytes = cbNumBytes * 2; + } + + processList = (PJOBOBJECT_BASIC_PROCESS_ID_LIST) HeapAlloc( + GetProcessHeap(), + 0, + cbNumBytes + ); + if( processList == NULL ) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + RtlZeroMemory( processList, cbNumBytes ); + + if( !QueryInformationJobObject( + m_hJobObject, + JobObjectBasicProcessIdList, + processList, + cbNumBytes, + NULL) ) + { + dwError = GetLastError(); + if( dwError != ERROR_MORE_DATA ) + { + hr = HRESULT_FROM_WIN32(dwError); + goto Finished; + } + } + + } while( dwRetries++ < 5 && + processList != NULL && + ( processList->NumberOfAssignedProcesses > processList->NumberOfProcessIdsInList || processList->NumberOfProcessIdsInList == 0 ) ); + + if( dwError == ERROR_MORE_DATA ) + { + hr = E_OUTOFMEMORY; + // some error + goto Finished; + } + + if( processList == NULL || ( processList->NumberOfAssignedProcesses > processList->NumberOfProcessIdsInList || processList->NumberOfProcessIdsInList == 0 ) ) + { + hr = HRESULT_FROM_WIN32(ERROR_PROCESS_ABORTED); + // some error + goto Finished; + } + + if( processList->NumberOfProcessIdsInList > MAX_ACTIVE_CHILD_PROCESSES ) + { + hr = HRESULT_FROM_WIN32( ERROR_CREATE_FAILED ); + goto Finished; + } + + for( DWORD i=0; iNumberOfProcessIdsInList; i++ ) + { + dwPid = (DWORD)processList->ProcessIdList[i]; + if( dwPid != m_dwProcessId && + dwPid != dwWorkerProcessPid ) + { + m_hChildProcessHandles[m_cChildProcess] = OpenProcess( + PROCESS_QUERY_INFORMATION | SYNCHRONIZE | PROCESS_TERMINATE | PROCESS_DUP_HANDLE, + FALSE, + dwPid + ); + m_dwChildProcessIds[m_cChildProcess] = dwPid; + m_cChildProcess ++; + } + } + +Finished: + + if( processList != NULL ) + { + HeapFree(GetProcessHeap(), 0, processList); + } + + return hr; +} + +HRESULT +SERVER_PROCESS::StopAllProcessesInJobObject( + VOID +) +{ + HRESULT hr = S_OK; + PJOBOBJECT_BASIC_PROCESS_ID_LIST processList = NULL; + HANDLE hProcess = NULL; + DWORD dwWorkerProcessPid = 0; + DWORD cbNumBytes = 1024; + DWORD dwRetries = 0; + + dwWorkerProcessPid = GetCurrentProcessId(); + + do + { + if( processList != NULL ) + { + HeapFree(GetProcessHeap(), 0, processList); + processList = NULL; + + // resize + cbNumBytes = cbNumBytes * 2; + } + + processList = (PJOBOBJECT_BASIC_PROCESS_ID_LIST) HeapAlloc( + GetProcessHeap(), + 0, + cbNumBytes + ); + if( processList == NULL ) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + RtlZeroMemory( processList, cbNumBytes ); + + if( !QueryInformationJobObject( + m_hJobObject, + JobObjectBasicProcessIdList, + processList, + cbNumBytes, + NULL) ) + { + DWORD dwError = GetLastError(); + if( dwError != ERROR_MORE_DATA ) + { + hr = HRESULT_FROM_WIN32(dwError); + goto Finished; + } + } + + } while( dwRetries++ < 5 && + processList != NULL && + ( processList->NumberOfAssignedProcesses > processList->NumberOfProcessIdsInList || processList->NumberOfProcessIdsInList == 0 ) ); + + if( processList == NULL || ( processList->NumberOfAssignedProcesses > processList->NumberOfProcessIdsInList || processList->NumberOfProcessIdsInList == 0 ) ) + { + hr = HRESULT_FROM_WIN32(ERROR_NOT_ENOUGH_MEMORY); + // some error + goto Finished; + } + + for( DWORD i=0; iNumberOfProcessIdsInList; i++ ) + { + if( dwWorkerProcessPid != (DWORD)processList->ProcessIdList[i] ) + { + hProcess = OpenProcess( PROCESS_TERMINATE, + FALSE, + (DWORD)processList->ProcessIdList[i] ); + if( hProcess != NULL ) + { + if( !TerminateProcess(hProcess, 1) ) + { + hr = HRESULT_FROM_GETLASTERROR(); + } + else + { + WaitForSingleObject( hProcess, INFINITE ); + } + + if( hProcess != NULL ) + { + CloseHandle( hProcess ); + hProcess = NULL; + } + } + } + } + +Finished: + + if( processList != NULL ) + { + HeapFree(GetProcessHeap(), 0, processList); + } + + return hr; +} + +SERVER_PROCESS::SERVER_PROCESS() : + m_cRefs( 1 ), + m_hProcessHandle( NULL ), + m_hProcessWaitHandle( NULL ), + m_dwProcessId( 0 ), + m_cChildProcess( 0 ), + m_fReady( FALSE ), + m_lStopping( 0L ), + m_hStdoutHandle( NULL ), + m_fStdoutLogEnabled( FALSE ), + m_hJobObject( NULL ), + m_pForwarderConnection( NULL ), + m_dwListeningProcessId( 0 ), + m_hListeningProcessHandle( NULL ), + m_hShutdownHandle( NULL ), + m_randomGenerator( std::random_device()() ) +{ + InterlockedIncrement(&g_dwActiveServerProcesses); + + for(INT i=0;iDereferenceProcessManager(); + m_pProcessManager = NULL; + } + + if(m_pForwarderConnection != NULL) + { + m_pForwarderConnection->DereferenceForwarderConnection(); + m_pForwarderConnection = NULL; + } + + m_pEnvironmentVarTable = NULL; + // no need to free m_pEnvironmentVarTable, as it references to + // the same hash table hold by configuration. + // the hashtable memory will be freed once onfiguration got recycled + + InterlockedDecrement(&g_dwActiveServerProcesses); +} + +static +VOID +CALLBACK +ProcessHandleCallback( + _In_ PVOID pContext, + _In_ BOOL +) +{ + SERVER_PROCESS *pServerProcess = (SERVER_PROCESS*) pContext; + pServerProcess->HandleProcessExit(); +} + +HRESULT +SERVER_PROCESS::RegisterProcessWait( + PHANDLE phWaitHandle, + HANDLE hProcessToWaitOn +) +{ + HRESULT hr = S_OK; + NTSTATUS status = 0; + + _ASSERT( phWaitHandle != NULL && *phWaitHandle == NULL ); + + *phWaitHandle = NULL; + + // wait thread will dereference. + ReferenceServerProcess(); + + status = RegisterWaitForSingleObject( + phWaitHandle, + hProcessToWaitOn, + (WAITORTIMERCALLBACK)&ProcessHandleCallback, + this, + INFINITE, + WT_EXECUTEONLYONCE | WT_EXECUTEINWAITTHREAD + ); + + if( status < 0 ) + { + hr = HRESULT_FROM_NT( status ); + goto Finished; + } + +Finished: + + if( FAILED( hr ) ) + { + *phWaitHandle = NULL; + DereferenceServerProcess(); + } + + return hr; +} + +HRESULT +SERVER_PROCESS::HandleProcessExit() +{ + HRESULT hr = S_OK; + BOOL fReady = FALSE; + DWORD dwProcessId = 0; + LPCWSTR apsz[1]; + STACK_STRU(strEventMsg, 256); + + if (InterlockedCompareExchange(&m_lStopping, 1L, 0L) == 0L) + { + CheckIfServerIsUp(m_dwPort, &dwProcessId, &fReady); + + if (!fReady) + { + if (SUCCEEDED(strEventMsg.SafeSnwprintf( + ASPNETCORE_EVENT_PROCESS_SHUTDOWN_MSG, + m_struAppFullPath.QueryStr(), + m_pszRootApplicationPath.QueryStr(), + m_dwProcessId, + m_dwPort))) + { + apsz[0] = strEventMsg.QueryStr(); + if (FORWARDING_HANDLER::QueryEventLog() != NULL) + { + ReportEventW(FORWARDING_HANDLER::QueryEventLog(), + EVENTLOG_INFORMATION_TYPE, + 0, + ASPNETCORE_EVENT_PROCESS_SHUTDOWN, + NULL, + 1, + 0, + apsz, + NULL); + } + } + m_pProcessManager->ShutdownProcess(this); + } + + DereferenceServerProcess(); + } + + return hr; +} + +HRESULT +SERVER_PROCESS::SendShutdownHttpMessage() +{ + HRESULT hr = S_OK; + HINTERNET hSession = NULL, + hConnect = NULL, + hRequest = NULL; + + STACK_STRU(strHeaders, 256); + STRU strAppToken; + STRU strUrl; + DWORD dwStatusCode = 0; + DWORD dwSize = sizeof(dwStatusCode); + + LPCWSTR apsz[1]; + STACK_STRU(strEventMsg, 256); + + hSession = WinHttpOpen(L"", + WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, + WINHTTP_NO_PROXY_NAME, + WINHTTP_NO_PROXY_BYPASS, + 0); + + if (hSession == NULL) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + hConnect = WinHttpConnect(hSession, + L"127.0.0.1", + (USHORT)m_dwPort, + 0); + + if (hConnect == NULL) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + if (m_struAppPath.QueryCCH() > 1) + { + // app path size is 1 means site root, i.e., "/" + // we don't want to add duplicated '/' to the request url + // otherwise the request will fail + strUrl.Copy(m_struAppPath); + } + strUrl.Append(L"/iisintegration"); + + hRequest = WinHttpOpenRequest(hConnect, + L"POST", + strUrl.QueryStr(), + NULL, + WINHTTP_NO_REFERER, + NULL, + 0); + + if (hRequest == NULL) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + // set timeout + if (!WinHttpSetTimeouts(hRequest, + m_dwShutdownTimeLimitInMS, // dwResolveTimeout + m_dwShutdownTimeLimitInMS, // dwConnectTimeout + m_dwShutdownTimeLimitInMS, // dwSendTimeout + m_dwShutdownTimeLimitInMS)) // dwReceiveTimeout + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + // set up the shutdown headers + if (FAILED(hr = strHeaders.Append(L"MS-ASPNETCORE-EVENT:shutdown \r\n")) || + FAILED(hr = strAppToken.Append(L"MS-ASPNETCORE-TOKEN:")) || + FAILED(hr = strAppToken.AppendA(m_straGuid.QueryStr())) || + FAILED(hr = strHeaders.Append(strAppToken.QueryStr()))) + { + goto Finished; + } + + if (!WinHttpSendRequest(hRequest, + strHeaders.QueryStr(), // pwszHeaders + strHeaders.QueryCCH(), // dwHeadersLength + WINHTTP_NO_REQUEST_DATA, + 0, // dwOptionalLength + 0, // dwTotalLength + 0)) // dwContext + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + if (!WinHttpReceiveResponse(hRequest , NULL)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + if (!WinHttpQueryHeaders(hRequest, + WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, + WINHTTP_HEADER_NAME_BY_INDEX, + &dwStatusCode, + &dwSize, + WINHTTP_NO_HEADER_INDEX)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + if (dwStatusCode != 202) + { + // not expected http status + hr = E_FAIL; + } + + // log + if (SUCCEEDED(strEventMsg.SafeSnwprintf( + ASPNETCORE_EVENT_SENT_SHUTDOWN_HTTP_REQUEST_MSG, + m_dwProcessId, + dwStatusCode))) + { + apsz[0] = strEventMsg.QueryStr(); + if (FORWARDING_HANDLER::QueryEventLog() != NULL) + { + ReportEventW(FORWARDING_HANDLER::QueryEventLog(), + EVENTLOG_INFORMATION_TYPE, + 0, + ASPNETCORE_EVENT_SENT_SHUTDOWN_HTTP_REQUEST, + NULL, + 1, + 0, + apsz, + NULL); + } + } + +Finished: + if (hRequest) + { + WinHttpCloseHandle(hRequest); + hRequest = NULL; + } + if (hConnect) + { + WinHttpCloseHandle(hConnect); + hConnect = NULL; + } + if (hSession) + { + WinHttpCloseHandle(hSession); + hSession = NULL; + } + return hr; +} + +//static +VOID +SERVER_PROCESS::SendShutDownSignal( + LPVOID lpParam +) +{ + SERVER_PROCESS* pThis = static_cast(lpParam); + DBG_ASSERT(pThis); + pThis->SendShutDownSignalInternal(); +} + +// +// send shutdown message first, if fail then send +// ctrl-c to the backend process to let it gracefully shutdown +// +VOID +SERVER_PROCESS::SendShutDownSignalInternal( + VOID +) +{ + ReferenceServerProcess(); + + if (FAILED(SendShutdownHttpMessage())) + { + // + // failed to send shutdown http message + // try send ctrl signal + // + HWND hCurrentConsole = NULL; + BOOL fFreeConsole = FALSE; + hCurrentConsole = GetConsoleWindow(); + if (hCurrentConsole) + { + // free current console first, as we may have one, e.g., hostedwebcore case + fFreeConsole = FreeConsole(); + } + + if (AttachConsole(m_dwProcessId)) + { + // call ctrl-break instead of ctrl-c as child process may ignore ctrl-c + if (!GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, m_dwProcessId)) + { + // failed to send the ctrl signal. terminate the backend process immediately instead of waiting for timeout + TerminateBackendProcess(); + } + FreeConsole(); + + if (fFreeConsole) + { + // IISExpress and hostedwebcore w3wp run as background process + // have to attach console back to ensure post app_offline scenario still works + AttachConsole(ATTACH_PARENT_PROCESS); + } + } + else + { + // terminate the backend process immediately instead of waiting for timeout + TerminateBackendProcess(); + } + } + + DereferenceServerProcess(); +} + +VOID +SERVER_PROCESS::TerminateBackendProcess( + VOID +) +{ + LPCWSTR apsz[1]; + STACK_STRU(strEventMsg, 256); + + if (InterlockedCompareExchange(&m_lStopping, 1L, 0L) == 0L) + { + // backend process will be terminated, remove the waitcallback + if (m_hProcessWaitHandle != NULL) + { + UnregisterWait(m_hProcessWaitHandle); + m_hProcessWaitHandle = NULL; + } + + // cannot gracefully shutdown or timeout, terminate the process + if (m_hProcessHandle != NULL && m_hProcessHandle != INVALID_HANDLE_VALUE) + { + TerminateProcess(m_hProcessHandle, 0); + m_hProcessHandle = NULL; + } + + // as we skipped process exit callback (ProcessHandleCallback), + // need to dereference the object otherwise memory leak + DereferenceServerProcess(); + + // log a warning for ungraceful shutdown + if (SUCCEEDED(strEventMsg.SafeSnwprintf( + ASPNETCORE_EVENT_GRACEFUL_SHUTDOWN_FAILURE_MSG, + m_dwProcessId))) + { + apsz[0] = strEventMsg.QueryStr(); + if (FORWARDING_HANDLER::QueryEventLog() != NULL) + { + ReportEventW(FORWARDING_HANDLER::QueryEventLog(), + EVENTLOG_WARNING_TYPE, + 0, + ASPNETCORE_EVENT_GRACEFUL_SHUTDOWN_FAILURE, + NULL, + 1, + 0, + apsz, + NULL); + } + } + } +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/websockethandler.cxx b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/websockethandler.cxx new file mode 100644 index 0000000000..7adc14c915 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/websockethandler.cxx @@ -0,0 +1,1169 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +/*++ + +Abstract: + + Main Handler for websocket requests. + + Initiates websocket connection to backend. + Uses WinHttp API's for backend connections, + and IIS Websocket API's for sending/receiving + websocket traffic. + + Transfers data between the two IO endpoints. + +----------------- +Read Loop Design +----------------- +When a read IO completes successfully on any endpoints, Asp.Net Core Module doesn't +immediately issue the next read. The next read is initiated only after +the read data is sent to the other endpoint. As soon as this send completes, +we initiate the next IO. It should be noted that the send complete merely +indicates the API completion from HTTP, and not necessarily over the network. + +This prevents the need for data buffering at the Asp.Net Core Module level. + +--*/ + +#include "precomp.hxx" + +SRWLOCK WEBSOCKET_HANDLER::sm_RequestsListLock; + +LIST_ENTRY WEBSOCKET_HANDLER::sm_RequestsListHead; + +TRACE_LOG * WEBSOCKET_HANDLER::sm_pTraceLog; + +WEBSOCKET_HANDLER::WEBSOCKET_HANDLER(): + _pHttpContext ( NULL ), + _pWebSocketContext ( NULL ), + _hWebSocketRequest( NULL ), + _pHandler ( NULL ), + _dwOutstandingIo ( 0 ), + _fCleanupInProgress ( FALSE ), + _fIndicateCompletionToIis ( FALSE ), + _fHandleClosed( FALSE ), + _fReceivedCloseMsg ( FALSE ) +{ + DebugPrintf (ASPNETCORE_DEBUG_FLAG_INFO, "WEBSOCKET_HANDLER::WEBSOCKET_HANDLER"); + + InitializeCriticalSectionAndSpinCount(&_RequestLock, 1000); + + InsertRequest(); +} + +WEBSOCKET_HANDLER::~WEBSOCKET_HANDLER() +{ + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, "WEBSOCKET_HANDLER::~WEBSOCKET_HANDLER"); + RemoveRequest(); + DeleteCriticalSection(&_RequestLock); +} + +VOID +WEBSOCKET_HANDLER::Terminate( + VOID + ) +{ + DebugPrintf (ASPNETCORE_DEBUG_FLAG_INFO, "WEBSOCKET_HANDLER::Terminate"); + if (!_fHandleClosed) + { + EnterCriticalSection(&_RequestLock); + _fCleanupInProgress = TRUE; + if (_pHttpContext != NULL) + { + _pHttpContext->CancelIo(); + _pHttpContext = NULL; + } + + if (_hWebSocketRequest != NULL) + { + WinHttpCloseHandle(_hWebSocketRequest); + _hWebSocketRequest = NULL; + } + + _fHandleClosed = TRUE; + LeaveCriticalSection(&_RequestLock); + } +} + +//static +HRESULT +WEBSOCKET_HANDLER::StaticInitialize( + BOOL fEnableReferenceCountTracing + ) +/*++ + + Routine Description: + + Initialize structures required for idle connection cleanup. + +--*/ +{ + if (!g_fWebSocketSupported) + { + return S_OK; + } + + if (fEnableReferenceCountTracing) + { + // + // If tracing is enabled, keep track of all websocket requests + // for debugging purposes. + // + + InitializeListHead (&sm_RequestsListHead); + sm_pTraceLog = CreateRefTraceLog( 10000, 0 ); + } + + return S_OK; +} + +//static +VOID +WEBSOCKET_HANDLER::StaticTerminate( + VOID + ) +{ + if (!g_fWebSocketSupported) + { + return; + } + + if (sm_pTraceLog) + { + DestroyRefTraceLog(sm_pTraceLog); + sm_pTraceLog = NULL; + } +} + +VOID +WEBSOCKET_HANDLER::InsertRequest( + VOID + ) +{ + if (g_fEnableReferenceCountTracing) + { + AcquireSRWLockExclusive(&sm_RequestsListLock); + + InsertTailList(&sm_RequestsListHead, &_listEntry); + + ReleaseSRWLockExclusive( &sm_RequestsListLock); + } +} + +//static +VOID +WEBSOCKET_HANDLER::RemoveRequest( + VOID + ) +{ + if (g_fEnableReferenceCountTracing) + { + AcquireSRWLockExclusive(&sm_RequestsListLock); + + RemoveEntryList(&_listEntry); + + ReleaseSRWLockExclusive( &sm_RequestsListLock); + } +} + +VOID +WEBSOCKET_HANDLER::IncrementOutstandingIo( + VOID + ) +{ + LONG dwOutstandingIo = InterlockedIncrement(&_dwOutstandingIo); + + if (sm_pTraceLog) + { + WriteRefTraceLog(sm_pTraceLog, dwOutstandingIo, this); + } +} + +VOID +WEBSOCKET_HANDLER::DecrementOutstandingIo( + VOID + ) +/*++ + Routine Description: + Decrements outstanding IO count. + + This indicates completion to IIS if all outstanding IO + has been completed, and a Cleanup was triggered for this + connection (denoted by _fIndicateCompletionToIis). + +--*/ +{ + LONG dwOutstandingIo = InterlockedDecrement (&_dwOutstandingIo); + + if (sm_pTraceLog) + { + WriteRefTraceLog(sm_pTraceLog, dwOutstandingIo, this); + } + + if (dwOutstandingIo == 0 && _fIndicateCompletionToIis) + { + IndicateCompletionToIIS(); + } +} + +VOID +WEBSOCKET_HANDLER::IndicateCompletionToIIS( + VOID + ) +/*++ + Routine Description: + Indicates completion to IIS. + + This returns a Pending Status, so that forwarding handler has a chance + to do book keeping when request is finally done. + +--*/ +{ + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, + "WEBSOCKET_HANDLER::IndicateCompletionToIIS called %d", _dwOutstandingIo); + // + // close Websocket handle. This will triger a WinHttp callback + // on handle close, then let IIS pipeline continue. + // Make sure no pending IO as there is no IIS websocket cancelation, + // any unexpected callback will lead to AV. Revisit it once CanelOutGoingIO works + // + if (_hWebSocketRequest != NULL && _dwOutstandingIo == 0) + { + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, + "WEBSOCKET_HANDLER::IndicateCompletionToIIS"); + + _pHandler->SetStatus(FORWARDER_DONE); + _fHandleClosed = TRUE; + WinHttpCloseHandle(_hWebSocketRequest); + _hWebSocketRequest = NULL; + } +} + +HRESULT +WEBSOCKET_HANDLER::ProcessRequest( + FORWARDING_HANDLER *pHandler, + IHttpContext *pHttpContext, + HINTERNET hRequest, + BOOL* fHandleCreated +) +/*++ + +Routine Description: + + Entry point to WebSocket Handler: + + This routine is called after the 101 response was successfully sent to + the client. + This routine get's a websocket handle to winhttp, + websocket handle to IIS's websocket context, and initiates IO + in these two endpoints. + + +--*/ +{ + HRESULT hr = S_OK; + //DWORD dwBuffSize = RECEIVE_BUFFER_SIZE; + + _pHandler = pHandler; + + EnterCriticalSection(&_RequestLock); + + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, + "WEBSOCKET_HANDLER::ProcessRequest"); + + // + // Cache the points to IHttpContext3 + // + + hr = HttpGetExtendedInterface(g_pHttpServer, + pHttpContext, + &_pHttpContext); + if (FAILED (hr)) + { + goto Finished; + } + + // + // Get pointer to IWebSocketContext for IIS websocket IO. + // + + _pWebSocketContext = (IWebSocketContext *) _pHttpContext-> + GetNamedContextContainer()->GetNamedContext(IIS_WEBSOCKET); + if ( _pWebSocketContext == NULL ) + { + hr = HRESULT_FROM_WIN32( ERROR_FILE_NOT_FOUND ); + goto Finished; + } + + // + // Get Handle to Winhttp's websocket context. + // + + _hWebSocketRequest = WINHTTP_HELPER::sm_pfnWinHttpWebSocketCompleteUpgrade( + hRequest, + (DWORD_PTR) pHandler); + + if (_hWebSocketRequest == NULL) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + *fHandleCreated = TRUE; + // + // Resize the send & receive buffers to be more conservative (and avoid DoS attacks). + // NOTE: The two WinHTTP options below were added for WinBlue, so we can't + // rely on their existence. + // + + //if (!WinHttpSetOption(_hWebSocketRequest, + // WINHTTP_OPTION_WEB_SOCKET_RECEIVE_BUFFER_SIZE, + // &dwBuffSize, + // sizeof(dwBuffSize))) + //{ + // DWORD dwRet = GetLastError(); + // if ( dwRet != ERROR_WINHTTP_INVALID_OPTION ) + // { + // hr = HRESULT_FROM_WIN32(dwRet); + // goto Finished; + // } + //} + + //if (!WinHttpSetOption(_hWebSocketRequest, + // WINHTTP_OPTION_WEB_SOCKET_SEND_BUFFER_SIZE, + // &dwBuffSize, + // sizeof(dwBuffSize))) + //{ + // DWORD dwRet = GetLastError(); + // if ( dwRet != ERROR_WINHTTP_INVALID_OPTION ) + // { + // hr = HRESULT_FROM_WIN32(dwRet); + // goto Finished; + // } + //} + + // + // Initiate Read on IIS + // + + hr = DoIisWebSocketReceive(); + if (FAILED(hr)) + { + goto Finished; + } + + // + // Initiate Read on WinHttp + // + + hr = DoWinHttpWebSocketReceive(); + if (FAILED(hr)) + { + goto Finished; + } + +Finished: + LeaveCriticalSection(&_RequestLock); + + if (FAILED (hr)) + { + DebugPrintf (ASPNETCORE_DEBUG_FLAG_ERROR, + "Process Request Failed with HR=%08x", hr); + } + + return hr; +} + +HRESULT +WEBSOCKET_HANDLER::DoIisWebSocketReceive( + VOID +) +/*++ + +Routine Description: + + Initiates a websocket receive on the IIS Websocket Context. + + +--*/ +{ + HRESULT hr = S_OK; + + DWORD dwBufferSize = RECEIVE_BUFFER_SIZE; + BOOL fUtf8Encoded; + BOOL fFinalFragment; + BOOL fClose; + + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, + "WEBSOCKET_HANDLER::DoIisWebSocketReceive"); + + IncrementOutstandingIo(); + + hr = _pWebSocketContext->ReadFragment( + &_IisReceiveBuffer, + &dwBufferSize, + TRUE, + &fUtf8Encoded, + &fFinalFragment, + &fClose, + OnReadIoCompletion, + this, + NULL); + if (FAILED(hr)) + { + DecrementOutstandingIo(); + + DebugPrintf(ASPNETCORE_DEBUG_FLAG_ERROR, + "WEBSOCKET_HANDLER::DoIisWebSocketSend failed with %08x", hr); + + } + + return hr; +} + +HRESULT +WEBSOCKET_HANDLER::DoWinHttpWebSocketReceive( + VOID +) +/*++ + +Routine Description: + + Initiates a websocket receive on WinHttp + + +--*/ +{ + HRESULT hr = S_OK; + DWORD dwError = NO_ERROR; + + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, + "WEBSOCKET_HANDLER::DoWinHttpWebSocketReceive"); + + IncrementOutstandingIo(); + + dwError = WINHTTP_HELPER::sm_pfnWinHttpWebSocketReceive( + _hWebSocketRequest, + &_WinHttpReceiveBuffer, + RECEIVE_BUFFER_SIZE, + NULL, + NULL); + + if (dwError != NO_ERROR) + { + DecrementOutstandingIo(); + + hr = HRESULT_FROM_WIN32(dwError); + + DebugPrintf(ASPNETCORE_DEBUG_FLAG_ERROR, + "WEBSOCKET_HANDLER::DoWinHttpWebSocketReceive failed with %08x", hr); + + } + + return hr; +} + +HRESULT +WEBSOCKET_HANDLER::DoIisWebSocketSend( + DWORD cbData, + WINHTTP_WEB_SOCKET_BUFFER_TYPE eBufferType +) +/*++ + +Routine Description: + + Initiates a websocket send on IIS + +--*/ +{ + HRESULT hr = S_OK; + + BOOL fUtf8Encoded = FALSE; + BOOL fFinalFragment = FALSE; + BOOL fClose = FALSE; + + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, + "WEBSOCKET_HANDLER::DoIisWebSocketSend %d", eBufferType); + + if (eBufferType == WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE) + { + // + // Query Close Status from WinHttp + // + + DWORD dwError = NO_ERROR; + USHORT uStatus; + DWORD dwReceived = 0; + STACK_STRU(strCloseReason, 128); + + dwError = WINHTTP_HELPER::sm_pfnWinHttpWebSocketQueryCloseStatus( + _hWebSocketRequest, + &uStatus, + &_WinHttpReceiveBuffer, + RECEIVE_BUFFER_SIZE, + &dwReceived); + + if (dwError != NO_ERROR) + { + hr = HRESULT_FROM_WIN32(dwError); + goto Finished; + } + + // + // Convert close reason to WCHAR + // + + hr = strCloseReason.CopyA((PCSTR)&_WinHttpReceiveBuffer, + dwReceived); + if (FAILED(hr)) + { + goto Finished; + } + + IncrementOutstandingIo(); + + // + // Backend end may start close hand shake first + // Need to inidcate no more receive should be called on WinHttp connection + // + _fReceivedCloseMsg = TRUE; + _fIndicateCompletionToIis = TRUE; + + // + // Send close to IIS. + // + + hr = _pWebSocketContext->SendConnectionClose( + TRUE, + uStatus, + uStatus == 1005 ? NULL : strCloseReason.QueryStr(), + OnWriteIoCompletion, + this, + NULL); + } + else + { + // + // Get equivalant flags for IIS API from buffer type. + // + + WINHTTP_HELPER::GetFlagsFromBufferType(eBufferType, + &fUtf8Encoded, + &fFinalFragment, + &fClose); + + IncrementOutstandingIo(); + + // + // Do the Send. + // + + hr = _pWebSocketContext->WriteFragment( + &_WinHttpReceiveBuffer, + &cbData, + TRUE, + fUtf8Encoded, + fFinalFragment, + OnWriteIoCompletion, + this, + NULL); + + } + + if (FAILED(hr)) + { + DecrementOutstandingIo(); + } + +Finished: + if (FAILED(hr)) + { + DebugPrintf(ASPNETCORE_DEBUG_FLAG_ERROR, + "WEBSOCKET_HANDLER::DoIisWebSocketSend failed with %08x", hr); + } + + return hr; +} + +HRESULT +WEBSOCKET_HANDLER::DoWinHttpWebSocketSend( + DWORD cbData, + WINHTTP_WEB_SOCKET_BUFFER_TYPE eBufferType +) +/*++ + +Routine Description: + + Initiates a websocket send on WinHttp + +--*/ +{ + DWORD dwError = NO_ERROR; + HRESULT hr = S_OK; + + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, + "WEBSOCKET_HANDLER::DoWinHttpWebSocketSend, %d", eBufferType); + + if (eBufferType == WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE) + { + USHORT uStatus; + LPCWSTR pszReason; + STACK_STRA(strCloseReason, 128); + + // + // Get Close status from IIS. + // + + hr = _pWebSocketContext->GetCloseStatus(&uStatus, + &pszReason); + + if (FAILED(hr)) + { + goto Finished; + } + + // + // Convert status to UTF8 + // + + hr = strCloseReason.CopyWToUTF8Unescaped(pszReason); + if (FAILED(hr)) + { + goto Finished; + } + + IncrementOutstandingIo(); + + // + // Send Close. + // + + dwError = WINHTTP_HELPER::sm_pfnWinHttpWebSocketShutdown( + _hWebSocketRequest, + uStatus, + strCloseReason.QueryCCH() == 0 ? NULL : (PVOID) strCloseReason.QueryStr(), + strCloseReason.QueryCCH()); + + if (dwError == ERROR_IO_PENDING) + { + // + // Call will complete asynchronously, return. + // ignore error. + // + + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, + "WEBSOCKET_HANDLER::DoWinhttpWebSocketSend IO_PENDING"); + + dwError = NO_ERROR; + } + else + { + if (dwError == NO_ERROR) + { + // + // Call completed synchronously. + // + + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, + "WEBSOCKET_HANDLER::DoWinhttpWebSocketSend Shutdown successful."); + } + } + } + else + { + IncrementOutstandingIo(); + + dwError = WINHTTP_HELPER::sm_pfnWinHttpWebSocketSend( + _hWebSocketRequest, + eBufferType, + cbData == 0 ? NULL : &_IisReceiveBuffer, + cbData + ); + } + + if (dwError != NO_ERROR) + { + hr = HRESULT_FROM_WIN32(dwError); + DecrementOutstandingIo(); + goto Finished; + } + +Finished: + if (FAILED(hr)) + { + DebugPrintf(ASPNETCORE_DEBUG_FLAG_ERROR, + "WEBSOCKET_HANDLER::DoWinHttpWebSocketSend failed with %08x", hr); + } + + return hr; +} + +//static +VOID +WINAPI +WEBSOCKET_HANDLER::OnReadIoCompletion( + HRESULT hrError, + VOID * pvCompletionContext, + DWORD cbIO, + BOOL fUTF8Encoded, + BOOL fFinalFragment, + BOOL fClose + ) +/*++ + + Routine Description: + + Completion routine for Read's from IIS pipeline. + +--*/ +{ + WEBSOCKET_HANDLER * pHandler = (WEBSOCKET_HANDLER *) + pvCompletionContext; + + pHandler->OnIisReceiveComplete( + hrError, + cbIO, + fUTF8Encoded, + fFinalFragment, + fClose + ); +} + +//static +VOID +WINAPI +WEBSOCKET_HANDLER::OnWriteIoCompletion( + HRESULT hrError, + VOID * pvCompletionContext, + DWORD cbIO, + BOOL fUTF8Encoded, + BOOL fFinalFragment, + BOOL fClose + ) +/*++ + Routine Description: + + Completion routine for Write's from IIS pipeline. + +--*/ +{ + WEBSOCKET_HANDLER * pHandler = (WEBSOCKET_HANDLER *) + pvCompletionContext; + + UNREFERENCED_PARAMETER(fUTF8Encoded); + UNREFERENCED_PARAMETER(fFinalFragment); + UNREFERENCED_PARAMETER(fClose); + + pHandler->OnIisSendComplete( + hrError, + cbIO + ); +} + + +HRESULT +WEBSOCKET_HANDLER::OnWinHttpSendComplete( + WINHTTP_WEB_SOCKET_STATUS * + ) +/*++ + +Routine Description: + Completion callback executed when a send to backend + server completes. + + If the send was successful, issue the next read + on the client's endpoint. + +++*/ +{ + HRESULT hr = S_OK; + BOOL fLocked = FALSE; + CleanupReason cleanupReason = CleanupReasonUnknown; + + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, + "WEBSOCKET_HANDLER::OnWinHttpSendComplete"); + + if (_fCleanupInProgress) + { + goto Finished; + } + + EnterCriticalSection (&_RequestLock); + + fLocked = TRUE; + + if (_fCleanupInProgress) + { + goto Finished; + } + // + // Data was successfully sent to backend. + // Initiate next receive from IIS. + // + + hr = DoIisWebSocketReceive(); + if (FAILED(hr)) + { + goto Finished; + } + +Finished: + if (fLocked) + { + LeaveCriticalSection(&_RequestLock); + } + + if (FAILED (hr)) + { + Cleanup (cleanupReason); + + DebugPrintf (ASPNETCORE_DEBUG_FLAG_ERROR, + "WEBSOCKET_HANDLER::OnWinsockSendComplete failed with HR=%08x", hr); + } + + // + // The handler object can be gone after this call. + // do not reference it after this statement. + // + + DecrementOutstandingIo(); + + return hr; +} + +HRESULT +WEBSOCKET_HANDLER::OnWinHttpShutdownComplete( + VOID + ) +{ + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, + "WEBSOCKET_HANDLER::OnWinHttpShutdownComplete --%p", _pHandler); + + DecrementOutstandingIo(); + + return S_OK; +} + +HRESULT +WEBSOCKET_HANDLER::OnWinHttpIoError( + WINHTTP_WEB_SOCKET_ASYNC_RESULT *pCompletionStatus +) +{ + HRESULT hr = HRESULT_FROM_WIN32(pCompletionStatus->AsyncResult.dwError); + + DebugPrintf(ASPNETCORE_DEBUG_FLAG_ERROR, + "WEBSOCKET_HANDLER::OnWinHttpIoError HR = %08x, Operation = %d", + hr, pCompletionStatus->AsyncResult.dwResult); + + Cleanup(ServerDisconnect); + + DecrementOutstandingIo(); + + return hr; +} + +HRESULT +WEBSOCKET_HANDLER::OnWinHttpReceiveComplete( + WINHTTP_WEB_SOCKET_STATUS * pCompletionStatus + ) +/*++ + +Routine Description: + + Completion callback executed when a receive completes + on the backend server winhttp endpoint. + + Issue send on the Client(IIS) if the receive was + successful. + + If the receive completed with zero bytes, that + indicates that the server has disconnected the connection. + Issue cleanup for the websocket handler. +--*/ +{ + HRESULT hr = S_OK; + BOOL fLocked = FALSE; + CleanupReason cleanupReason = CleanupReasonUnknown; + + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, + "WEBSOCKET_HANDLER::OnWinHttpReceiveComplete, %d", _fCleanupInProgress); + + if (_fCleanupInProgress) + { + goto Finished; + } + + EnterCriticalSection(&_RequestLock); + + fLocked = TRUE; + if (_fCleanupInProgress) + { + goto Finished; + } + hr = DoIisWebSocketSend( + pCompletionStatus->dwBytesTransferred, + pCompletionStatus->eBufferType + ); + + if (FAILED (hr)) + { + cleanupReason = ClientDisconnect; + goto Finished; + } + +Finished: + if (fLocked) + { + LeaveCriticalSection(&_RequestLock); + } + if (FAILED (hr)) + { + Cleanup (cleanupReason); + + DebugPrintf (ASPNETCORE_DEBUG_FLAG_ERROR, + "WEBSOCKET_HANDLER::OnWinsockReceiveComplete failed with HR=%08x", hr); + } + + // + // The handler object can be gone after this call. + // do not reference it after this statement. + // + + DecrementOutstandingIo(); + + return hr; +} + +HRESULT +WEBSOCKET_HANDLER::OnIisSendComplete( + HRESULT hrCompletion, + DWORD cbIo + ) +/*++ +Routine Description: + + Completion callback executed when a send + completes from the client. + + If send was successful,issue read on the + server endpoint, to continue the readloop. + +--*/ +{ + HRESULT hr = S_OK; + BOOL fLocked = FALSE; + CleanupReason cleanupReason = CleanupReasonUnknown; + + UNREFERENCED_PARAMETER(cbIo); + + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, "WEBSOCKET_HANDLER::OnIisSendComplete"); + + if (FAILED(hrCompletion)) + { + hr = hrCompletion; + cleanupReason = ClientDisconnect; + goto Finished; + } + + if (_fCleanupInProgress) + { + goto Finished; + } + EnterCriticalSection(&_RequestLock); + fLocked = TRUE; + if (_fCleanupInProgress) + { + goto Finished; + } + + // + // Only call read if no close hand shake was received from backend + // + //if (!_fReceivedCloseMsg) + //{ + // + // Write Completed, initiate next read from backend server. + // + hr = DoWinHttpWebSocketReceive(); + if (FAILED(hr)) + { + cleanupReason = ServerDisconnect; + goto Finished; + } + //} + +Finished: + if (fLocked) + { + LeaveCriticalSection(&_RequestLock); + } + if (FAILED (hr)) + { + Cleanup (cleanupReason); + + DebugPrintf (ASPNETCORE_DEBUG_FLAG_ERROR, + "WEBSOCKET_HANDLER::OnIisSendComplete failed with HR=%08x", hr); + } + + // + // The handler object can be gone after this call. + // do not reference it after this statement. + // + + DecrementOutstandingIo(); + + return hr; +} + +HRESULT +WEBSOCKET_HANDLER::OnIisReceiveComplete( + HRESULT hrCompletion, + DWORD cbIO, + BOOL fUTF8Encoded, + BOOL fFinalFragment, + BOOL fClose + ) +/*++ +Routine Description: + + Completion routine executed when a receive completes + from the client (IIS endpoint). + + If the receive was successful, initiate a send on + the backend server (winhttp) endpoint. + + If the receive failed, initiate cleanup. + +--*/ +{ + HRESULT hr = S_OK; + BOOL fLocked = FALSE; + CleanupReason cleanupReason = CleanupReasonUnknown; + WINHTTP_WEB_SOCKET_BUFFER_TYPE BufferType; + + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, + "WEBSOCKET_HANDLER::OnIisReceiveComplete"); + + if (FAILED(hrCompletion)) + { + cleanupReason = ClientDisconnect; + hr = hrCompletion; + goto Finished; + } + + if (_fCleanupInProgress) + { + goto Finished; + } + + EnterCriticalSection(&_RequestLock); + fLocked = TRUE; + + if (_fCleanupInProgress) + { + goto Finished; + } + // + // Get Buffer Type from flags. + // + + WINHTTP_HELPER::GetBufferTypeFromFlags(fUTF8Encoded, + fFinalFragment, + fClose, + &BufferType); + + // + // Initiate Send. + // + + hr = DoWinHttpWebSocketSend(cbIO, BufferType); + if (FAILED (hr)) + { + cleanupReason = ServerDisconnect; + goto Finished; + } + +Finished: + if (fLocked) + { + LeaveCriticalSection(&_RequestLock); + } + if (FAILED (hr)) + { + Cleanup (cleanupReason); + + DebugPrintf (ASPNETCORE_DEBUG_FLAG_ERROR, + "WEBSOCKET_HANDLER::OnIisReceiveComplete failed with HR=%08x", hr); + } + + // + // The handler object can be gone after this call. + // do not reference it after this statement. + // + + DecrementOutstandingIo(); + + return hr; +} + +VOID +WEBSOCKET_HANDLER::Cleanup( + CleanupReason reason +) +/*++ + +Routine Description: + + Cleanup function for the websocket handler. + + Initiates cancelIo on the two IO endpoints: + IIS, WinHttp client. + +Arguments: + CleanupReason +--*/ +{ + BOOL fLocked = FALSE; + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, + "WEBSOCKET_HANDLER::Cleanup Initiated with reason %d", reason); + + if (_fCleanupInProgress) + { + goto Finished; + } + + EnterCriticalSection(&_RequestLock); + fLocked = TRUE; + + if (_fCleanupInProgress) + { + goto Finished; + } + + _fCleanupInProgress = TRUE; + + _fIndicateCompletionToIis = TRUE; + // + // We need cancel IO for fast error handling + // Reivist the code once CanelOutstandingIO api is available + // + /*if (_pWebSocketContext != NULL) + { + _pWebSocketContext->CancelOutstandingIO(); + }*/ + _pHttpContext->CancelIo(); + + // + // Don't close the handle here, + // as it trigger a WinHttp callback and let IIS pipeline continue + // Handle should be closed only in IndicateCompletionToIIS + IndicateCompletionToIIS(); +Finished: + if (fLocked) + { + LeaveCriticalSection(&_RequestLock); + } + +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/winhttphelper.cxx b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/winhttphelper.cxx new file mode 100644 index 0000000000..8407c45830 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/AspNetCore/src/winhttphelper.cxx @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "precomp.hxx" + +PFN_WINHTTP_WEBSOCKET_COMPLETE_UPGRADE +WINHTTP_HELPER::sm_pfnWinHttpWebSocketCompleteUpgrade; + +PFN_WINHTTP_WEBSOCKET_SEND +WINHTTP_HELPER::sm_pfnWinHttpWebSocketSend; + +PFN_WINHTTP_WEBSOCKET_RECEIVE +WINHTTP_HELPER::sm_pfnWinHttpWebSocketReceive; + +PFN_WINHTTP_WEBSOCKET_SHUTDOWN +WINHTTP_HELPER::sm_pfnWinHttpWebSocketShutdown; + +PFN_WINHTTP_WEBSOCKET_QUERY_CLOSE_STATUS +WINHTTP_HELPER::sm_pfnWinHttpWebSocketQueryCloseStatus; + +//static +HRESULT +WINHTTP_HELPER::StaticInitialize( + VOID +) +{ + HRESULT hr = S_OK; + + if (!g_fWebSocketSupported) + { + return S_OK; + } + + // + // Initialize the function pointers for WinHttp Websocket API's. + // + + HMODULE hWinHttp = GetModuleHandleA("winhttp.dll"); + if (hWinHttp == NULL) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + sm_pfnWinHttpWebSocketCompleteUpgrade = (PFN_WINHTTP_WEBSOCKET_COMPLETE_UPGRADE) + GetProcAddress(hWinHttp, "WinHttpWebSocketCompleteUpgrade"); + if (sm_pfnWinHttpWebSocketCompleteUpgrade == NULL) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + sm_pfnWinHttpWebSocketQueryCloseStatus = (PFN_WINHTTP_WEBSOCKET_QUERY_CLOSE_STATUS) + GetProcAddress(hWinHttp, "WinHttpWebSocketQueryCloseStatus"); + if (sm_pfnWinHttpWebSocketQueryCloseStatus == NULL) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + sm_pfnWinHttpWebSocketReceive = (PFN_WINHTTP_WEBSOCKET_RECEIVE) + GetProcAddress(hWinHttp, "WinHttpWebSocketReceive"); + if (sm_pfnWinHttpWebSocketReceive == NULL) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + sm_pfnWinHttpWebSocketSend = (PFN_WINHTTP_WEBSOCKET_SEND) + GetProcAddress(hWinHttp, "WinHttpWebSocketSend"); + if (sm_pfnWinHttpWebSocketSend == NULL) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + sm_pfnWinHttpWebSocketShutdown = (PFN_WINHTTP_WEBSOCKET_SHUTDOWN) + GetProcAddress(hWinHttp, "WinHttpWebSocketShutdown"); + if (sm_pfnWinHttpWebSocketShutdown == NULL) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + +Finished: + return hr; +} + + +//static +VOID +WINHTTP_HELPER::GetFlagsFromBufferType( + __in WINHTTP_WEB_SOCKET_BUFFER_TYPE BufferType, + __out BOOL * pfUtf8Encoded, + __out BOOL * pfFinalFragment, + __out BOOL * pfClose +) +{ + switch (BufferType) + { + case WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE: + *pfUtf8Encoded = FALSE; + *pfFinalFragment = TRUE; + *pfClose = FALSE; + + break; + + case WINHTTP_WEB_SOCKET_BINARY_FRAGMENT_BUFFER_TYPE: + *pfUtf8Encoded = FALSE; + *pfFinalFragment = FALSE; + *pfClose = FALSE; + + break; + + case WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE: + *pfUtf8Encoded = TRUE; + *pfFinalFragment = TRUE; + *pfClose = FALSE; + + break; + + case WINHTTP_WEB_SOCKET_UTF8_FRAGMENT_BUFFER_TYPE: + *pfUtf8Encoded = TRUE; + *pfFinalFragment = FALSE; + *pfClose = FALSE; + + break; + + case WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE: + *pfUtf8Encoded = FALSE; + *pfFinalFragment = FALSE; + *pfClose = TRUE; + + break; + } +} + +//static +VOID +WINHTTP_HELPER::GetBufferTypeFromFlags( + __in BOOL fUtf8Encoded, + __in BOOL fFinalFragment, + __in BOOL fClose, + __out WINHTTP_WEB_SOCKET_BUFFER_TYPE* pBufferType +) +{ + if (fClose) + { + *pBufferType = WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE; + } + else + if (fUtf8Encoded) + { + if (fFinalFragment) + { + *pBufferType = WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE; + } + else + { + *pBufferType = WINHTTP_WEB_SOCKET_UTF8_FRAGMENT_BUFFER_TYPE; + } + } + else + { + if (fFinalFragment) + { + *pBufferType = WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE; + } + else + { + *pBufferType = WINHTTP_WEB_SOCKET_BINARY_FRAGMENT_BUFFER_TYPE; + } + } + + return; +} \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/IISLib.vcxproj b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/IISLib.vcxproj new file mode 100644 index 0000000000..bb8795992b --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/IISLib.vcxproj @@ -0,0 +1,201 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {4787A64F-9A3E-4867-A55A-70CB4B2B2FFE} + Win32Proj + IISLib + IISLib + 10.0.15063.0 + + + + StaticLibrary + true + v141 + Unicode + + + StaticLibrary + true + v141 + Unicode + + + StaticLibrary + false + v141 + true + Unicode + + + StaticLibrary + false + v141 + true + Unicode + + + + + + + + + + + + + + + + + + + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + + + + Level3 + Disabled + WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + true + ProgramDatabase + MultiThreadedDebug + false + true + + + Windows + true + + + + + + + Level3 + Disabled + WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + true + ProgramDatabase + MultiThreadedDebug + false + true + + + Windows + true + + + + + Level3 + + + MaxSpeed + true + true + WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + true + MultiThreaded + false + true + + + Windows + true + true + true + + + + + Level3 + + + MaxSpeed + true + true + WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + true + MultiThreaded + false + true + + + Windows + true + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/acache.cxx b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/acache.cxx new file mode 100644 index 0000000000..d68813edbc --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/acache.cxx @@ -0,0 +1,443 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "precomp.h" + +LONG ALLOC_CACHE_HANDLER::sm_nFillPattern = 0xACA50000; +HANDLE ALLOC_CACHE_HANDLER::sm_hHeap; + +// +// This class is used to implement the free list. We cast the free'd +// memory block to a FREE_LIST_HEADER*. The signature is used to guard against +// double deletion. We also fill memory with a pattern. +// +class FREE_LIST_HEADER +{ +public: + SLIST_ENTRY ListEntry; + DWORD dwSignature; + + enum + { + FREE_SIGNATURE = (('A') | ('C' << 8) | ('a' << 16) | (('$' << 24) | 0x80)), + }; +}; + +ALLOC_CACHE_HANDLER::ALLOC_CACHE_HANDLER( + VOID +) : m_nThreshold(0), + m_cbSize(0), + m_pFreeLists(NULL), + m_nTotal(0) +{ +} + +ALLOC_CACHE_HANDLER::~ALLOC_CACHE_HANDLER( + VOID +) +{ + if (m_pFreeLists != NULL) + { + CleanupLookaside(); + m_pFreeLists->Dispose(); + m_pFreeLists = NULL; + } +} + +HRESULT +ALLOC_CACHE_HANDLER::Initialize( + DWORD cbSize, + LONG nThreshold +) +{ + HRESULT hr = S_OK; + + m_nThreshold = nThreshold; + if ( m_nThreshold > 0xffff) + { + // + // This will be compared against QueryDepthSList return value (USHORT). + // + m_nThreshold = 0xffff; + } + + if ( IsPageheapEnabled() ) + { + // + // Disable acache. + // + m_nThreshold = 0; + } + + // + // Make sure the block is big enough to hold a FREE_LIST_HEADER. + // + m_cbSize = cbSize; + m_cbSize = max(m_cbSize, sizeof(FREE_LIST_HEADER)); + + // + // Round up the block size to a multiple of the size of a LONG (for + // the fill pattern in Free()). + // + m_cbSize = (m_cbSize + sizeof(LONG) - 1) & ~(sizeof(LONG) - 1); + +#if defined(_MSC_VER) && _MSC_VER >= 1600 // VC10 + auto Init = [] (SLIST_HEADER* pHead) + { + InitializeSListHead(pHead); + }; +#else + class Functor + { + public: + void operator()(SLIST_HEADER* pHead) + { + InitializeSListHead(pHead); + } + } Init; +#endif + + hr = PER_CPU::Create(Init, + &m_pFreeLists ); + if (FAILED(hr)) + { + goto Finished; + } + + m_nFillPattern = InterlockedIncrement(&sm_nFillPattern); + +Finished: + + return hr; +} + +// static +HRESULT +ALLOC_CACHE_HANDLER::StaticInitialize( + VOID +) +{ + // + // Since the memory allocated is fixed size, + // a heap is not really needed, allocations can be done + // using VirtualAllocEx[Numa]. For now use Windows Heap. + // + // Be aware that creating one private heap consumes more + // virtual address space for the worker process. + // + sm_hHeap = GetProcessHeap(); + return S_OK; +} + + +// static +VOID +ALLOC_CACHE_HANDLER::StaticTerminate( + VOID +) +{ + sm_hHeap = NULL; +} + +VOID +ALLOC_CACHE_HANDLER::CleanupLookaside( + VOID +) +/*++ + Description: + This function cleans up the lookaside list by removing storage space. + + Arguments: + None. + + Returns: + None +--*/ +{ + // + // Free up all the entries in the list. + // Don't use InterlockedFlushSList, in order to work + // memory must be 16 bytes aligned and currently it is 64. + // + +#if defined(_MSC_VER) && _MSC_VER >= 1600 // VC10 + auto Predicate = [=] (SLIST_HEADER * pListHeader) + { + PSLIST_ENTRY pl; + LONG NodesToDelete = QueryDepthSList( pListHeader ); + + pl = InterlockedPopEntrySList( pListHeader ); + while ( pl != NULL && --NodesToDelete >= 0 ) + { + InterlockedDecrement( &m_nTotal); + + ::HeapFree( sm_hHeap, 0, pl ); + + pl = InterlockedPopEntrySList(pListHeader); + } + }; +#else + class Functor + { + public: + explicit Functor(ALLOC_CACHE_HANDLER * pThis) : _pThis(pThis) + { + } + void operator()(SLIST_HEADER * pListHeader) + { + PSLIST_ENTRY pl; + LONG NodesToDelete = QueryDepthSList( pListHeader ); + + pl = InterlockedPopEntrySList( pListHeader ); + while ( pl != NULL && --NodesToDelete >= 0 ) + { + InterlockedDecrement( &_pThis->m_nTotal); + + ::HeapFree( sm_hHeap, 0, pl ); + + pl = InterlockedPopEntrySList(pListHeader); + } + } + private: + ALLOC_CACHE_HANDLER * _pThis; + } Predicate(this); +#endif + + m_pFreeLists ->ForEach(Predicate); +} + +LPVOID +ALLOC_CACHE_HANDLER::Alloc( + VOID +) +{ + LPVOID pMemory = NULL; + + if ( m_nThreshold > 0 ) + { + SLIST_HEADER * pListHeader = m_pFreeLists ->GetLocal(); + pMemory = (LPVOID) InterlockedPopEntrySList(pListHeader); // get the real object + + if (pMemory != NULL) + { + FREE_LIST_HEADER* pfl = (FREE_LIST_HEADER*) pMemory; + // + // If the signature is wrong then somebody's been scribbling + // on memory that they've freed. + // + DBG_ASSERT(pfl->dwSignature == FREE_LIST_HEADER::FREE_SIGNATURE); + } + } + + if ( pMemory == NULL ) + { + // + // No free entry. Need to alloc a new object. + // + pMemory = (LPVOID) ::HeapAlloc( sm_hHeap, + 0, + m_cbSize ); + + if ( pMemory != NULL ) + { + // + // Update counters. + // + m_nTotal++; + } + } + + if ( pMemory == NULL ) + { + SetLastError( ERROR_NOT_ENOUGH_MEMORY ); + } + else + { + FREE_LIST_HEADER* pfl = (FREE_LIST_HEADER*) pMemory; + pfl->dwSignature = 0; // clear; just in case caller never overwrites + } + + return pMemory; +} + +VOID +ALLOC_CACHE_HANDLER::Free( + __in LPVOID pMemory +) +{ + // + // Assume that this is allocated using the Alloc() function. + // + DBG_ASSERT(NULL != pMemory); + + // + // Use a signature to check against double deletions. + // + FREE_LIST_HEADER* pfl = (FREE_LIST_HEADER*) pMemory; + DBG_ASSERT(pfl->dwSignature != FREE_LIST_HEADER::FREE_SIGNATURE); + + // + // Start filling the space beyond the portion overlaid by the initial + // FREE_LIST_HEADER. Fill at most 6 DWORDS. + // + LONG* pl = (LONG*) (pfl+1); + + for (LONG cb = (LONG)min(6 * sizeof(LONG),m_cbSize) - sizeof(FREE_LIST_HEADER); + cb > 0; + cb -= sizeof(LONG)) + { + *pl++ = m_nFillPattern; + } + + // + // Now, set the signature. + // + pfl->dwSignature = FREE_LIST_HEADER::FREE_SIGNATURE; + + // + // Store the items in the alloc cache. + // + SLIST_HEADER * pListHeader = m_pFreeLists ->GetLocal(); + + if ( QueryDepthSList(pListHeader) >= m_nThreshold ) + { + // + // Threshold for free entries is exceeded. Free the object to + // process pool. + // + ::HeapFree( sm_hHeap, 0, pMemory ); + } + else + { + // + // Store the given pointer in the single linear list + // + InterlockedPushEntrySList(pListHeader, &pfl->ListEntry); + } +} + +DWORD +ALLOC_CACHE_HANDLER::QueryDepthForAllSLists( + VOID +) +/*++ + +Description: + + Aggregates the total count of elements in all lists. + +Arguments: + + None. + +Return Value: + + Total count (snapshot). + +--*/ +{ + DWORD Count = 0; + + if (m_pFreeLists != NULL) + { +#if defined(_MSC_VER) && _MSC_VER >= 1600 // VC10 + auto Predicate = [&Count] (SLIST_HEADER * pListHeader) + { + Count += QueryDepthSList(pListHeader); + }; +#else + class Functor + { + public: + explicit Functor(DWORD& Count) : _Count(Count) + { + } + void operator()(SLIST_HEADER * pListHeader) + { + _Count += QueryDepthSList(pListHeader); + } + private: + DWORD& _Count; + } Predicate(Count); +#endif + // + // [&Count] means that the method can modify local variable Count. + // + m_pFreeLists ->ForEach(Predicate); + } + + return Count; +} + +// static +BOOL +ALLOC_CACHE_HANDLER::IsPageheapEnabled( + VOID +) +{ + BOOL fRet = FALSE; + BOOL fLockedHeap = FALSE; + HMODULE hModule = NULL; + HANDLE hHeap = NULL; + PROCESS_HEAP_ENTRY heapEntry = {0}; + + // + // If verifier.dll is loaded - we are running under app verifier == pageheap is enabled + // + hModule = GetModuleHandle( L"verifier.dll" ); + if ( hModule != NULL ) + { + hModule = NULL; + fRet = TRUE; + goto Finished; + } + + // + // Create a heap for calling heapwalk + // otherwise HeapWalk turns off lookasides for a useful heap + // + hHeap = ::HeapCreate( 0, 0, 0 ); + if ( hHeap == NULL ) + { + fRet = FALSE; + goto Finished; + } + + fRet = ::HeapLock( hHeap ); + if ( !fRet ) + { + goto Finished; + } + fLockedHeap = TRUE; + + // + // If HeapWalk is unsupported -> then running page heap + // + fRet = ::HeapWalk( hHeap, &heapEntry ); + if ( !fRet ) + { + if ( GetLastError() == ERROR_INVALID_FUNCTION ) + { + fRet = TRUE; + goto Finished; + } + } + + fRet = FALSE; + +Finished: + + if ( fLockedHeap ) + { + fLockedHeap = FALSE; + DBG_REQUIRE( ::HeapUnlock( hHeap ) ); + } + + if ( hHeap ) + { + DBG_REQUIRE( ::HeapDestroy( hHeap ) ); + hHeap = NULL; + } + + return fRet; +} \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/acache.h b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/acache.h new file mode 100644 index 0000000000..048df2b507 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/acache.h @@ -0,0 +1,115 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "percpu.h" + +class ALLOC_CACHE_HANDLER +{ +public: + + ALLOC_CACHE_HANDLER( + VOID + ); + + ~ALLOC_CACHE_HANDLER( + VOID + ); + + HRESULT + Initialize( + DWORD cbSize, + LONG nThreshold + ); + + LPVOID + Alloc( + VOID + ); + + VOID + Free( + __in LPVOID pMemory + ); + + +private: + + VOID + CleanupLookaside( + VOID + ); + + DWORD + QueryDepthForAllSLists( + VOID + ); + + LONG m_nThreshold; + DWORD m_cbSize; + + PER_CPU * m_pFreeLists; + + // + // Total heap allocations done over the lifetime. + // Note that this is not interlocked, it is just a hint for debugging. + // + volatile LONG m_nTotal; + + LONG m_nFillPattern; + +public: + + static + HRESULT + StaticInitialize( + VOID + ); + + static + VOID + StaticTerminate( + VOID + ); + + static + BOOL + IsPageheapEnabled(); + +private: + + static LONG sm_nFillPattern; + static HANDLE sm_hHeap; +}; + + +// You can use ALLOC_CACHE_HANDLER as a per-class allocator +// in your C++ classes. Add the following to your class definition: +// +// protected: +// static ALLOC_CACHE_HANDLER* sm_palloc; +// public: +// static void* operator new(size_t s) +// { +// IRTLASSERT(s == sizeof(C)); +// IRTLASSERT(sm_palloc != NULL); +// return sm_palloc->Alloc(); +// } +// static void operator delete(void* pv) +// { +// IRTLASSERT(pv != NULL); +// if (sm_palloc != NULL) +// sm_palloc->Free(pv); +// } +// +// Obviously, you must initialize sm_palloc before you can allocate +// any objects of this class. +// +// Note that if you derive a class from this base class, the derived class +// must also provide its own operator new and operator delete. If not, the +// base class's allocator will be called, but the size of the derived +// object will almost certainly be larger than that of the base object. +// Furthermore, the allocator will not be used for arrays of objects +// (override operator new[] and operator delete[]), but this is a +// harder problem since the allocator works with one fixed size. diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/ahutil.cpp b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/ahutil.cpp new file mode 100644 index 0000000000..dae2027f33 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/ahutil.cpp @@ -0,0 +1,1671 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "precomp.h" + +HRESULT +SetElementProperty( + IN IAppHostElement * pElement, + IN CONST WCHAR * szPropName, + IN CONST VARIANT * varPropValue + ) +{ + HRESULT hr = NOERROR; + + CComPtr pPropElement; + + BSTR bstrPropName = SysAllocString( szPropName ); + + if( !bstrPropName ) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR( hr ); + goto exit; + } + + hr = pElement->GetPropertyByName( bstrPropName, + &pPropElement ); + if( FAILED(hr) ) + { + DBGERROR_HR( hr ); + goto exit; + } + + hr = pPropElement->put_Value( *varPropValue ); + if( FAILED(hr) ) + { + DBGERROR_HR( hr ); + goto exit; + } + +exit: + + if( bstrPropName ) + { + SysFreeString( bstrPropName ); + bstrPropName = NULL; + } + + return hr; +} + +HRESULT +SetElementStringProperty( + IN IAppHostElement * pElement, + IN CONST WCHAR * szPropName, + IN CONST WCHAR * szPropValue + ) +{ + HRESULT hr; + VARIANT varPropValue; + VariantInit(&varPropValue); + + hr = VariantAssign(&varPropValue, szPropValue); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + hr = SetElementProperty(pElement, szPropName, &varPropValue); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + +exit: + + VariantClear(&varPropValue); + return hr; +} + +HRESULT +GetElementStringProperty( + IN IAppHostElement * pElement, + IN CONST WCHAR * szPropName, + OUT BSTR * pbstrPropValue + ) +{ + HRESULT hr = S_OK; + BSTR bstrPropName = SysAllocString( szPropName ); + IAppHostProperty* pProperty = NULL; + + *pbstrPropValue = NULL; + + if (!bstrPropName) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR( hr ); + goto exit; + } + + hr = pElement->GetPropertyByName( bstrPropName, &pProperty ); + if (FAILED(hr)) + { + DBGERROR_HR( hr ); + goto exit; + } + + hr = pProperty->get_StringValue( pbstrPropValue ); + if (FAILED(hr)) + { + DBGERROR_HR( hr ); + goto exit; + } + +exit: + + if (pProperty) + { + pProperty->Release(); + } + + if (bstrPropName) + { + SysFreeString( bstrPropName ); + } + + return hr; +} + + +HRESULT +GetElementStringProperty( + IN IAppHostElement * pElement, + IN CONST WCHAR * szPropName, + OUT STRU * pstrPropValue + ) +{ + HRESULT hr = S_OK; + BSTR bstrPropName = SysAllocString( szPropName ); + IAppHostProperty* pProperty = NULL; + BSTR bstrPropValue = NULL; + + if (!bstrPropName) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR( hr ); + goto exit; + } + + hr = pElement->GetPropertyByName( bstrPropName, &pProperty ); + if (FAILED(hr)) + { + DBGERROR_HR( hr ); + goto exit; + } + + hr = pProperty->get_StringValue( &bstrPropValue ); + if (FAILED(hr)) + { + DBGERROR_HR( hr ); + goto exit; + } + + hr = pstrPropValue->Copy(bstrPropValue); + if (FAILED(hr)) + { + DBGERROR_HR( hr ); + goto exit; + } + +exit: + + if (pProperty) + { + pProperty->Release(); + } + + if (bstrPropValue) + { + SysFreeString( bstrPropValue ); + } + + if (bstrPropName) + { + SysFreeString( bstrPropName ); + } + + return hr; +} + +HRESULT +GetElementChildByName( + IN IAppHostElement * pElement, + IN LPCWSTR pszElementName, + OUT IAppHostElement ** ppChildElement +) +{ + BSTR bstrElementName = SysAllocString(pszElementName); + if (bstrElementName == NULL) + { + return E_OUTOFMEMORY; + } + HRESULT hr = pElement->GetElementByName(bstrElementName, + ppChildElement); + SysFreeString(bstrElementName); + return hr; +} + +HRESULT +GetElementBoolProperty( + IN IAppHostElement * pElement, + IN LPCWSTR pszPropertyName, + OUT bool * pBool +) +{ + BOOL fValue; + HRESULT hr = GetElementBoolProperty(pElement, + pszPropertyName, + &fValue); + if (SUCCEEDED(hr)) + { + *pBool = !!fValue; + } + return hr; +} + +HRESULT +GetElementBoolProperty( + IN IAppHostElement * pElement, + IN LPCWSTR pszPropertyName, + OUT BOOL * pBool +) +{ + HRESULT hr = S_OK; + BSTR bstrPropertyName = NULL; + IAppHostProperty * pProperty = NULL; + VARIANT varValue; + + VariantInit( &varValue ); + + bstrPropertyName = SysAllocString( pszPropertyName ); + if ( bstrPropertyName == NULL ) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR(hr); + goto exit; + } + + // Now ask for the property and if it succeeds it is returned directly back. + hr = pElement->GetPropertyByName( bstrPropertyName, &pProperty ); + if ( FAILED ( hr ) ) + { + goto exit; + } + + // Now let's get the property and then extract it from the Variant. + hr = pProperty->get_Value( &varValue ); + if ( FAILED ( hr ) ) + { + goto exit; + } + + hr = VariantChangeType( &varValue, &varValue, 0, VT_BOOL ); + if ( FAILED ( hr ) ) + { + goto exit; + } + + // extract the value + *pBool = ( V_BOOL( &varValue ) == VARIANT_TRUE ); + +exit: + + VariantClear( &varValue ); + + if ( bstrPropertyName != NULL ) + { + SysFreeString( bstrPropertyName ); + bstrPropertyName = NULL; + } + + if ( pProperty != NULL ) + { + pProperty->Release(); + pProperty = NULL; + } + + return hr; + +} + +HRESULT +GetElementDWORDProperty( + IN IAppHostElement * pSitesCollectionEntry, + IN LPCWSTR pwszName, + OUT DWORD * pdwValue +) +{ + HRESULT hr = S_OK; + IAppHostProperty * pProperty = NULL; + BSTR bstrName = NULL; + VARIANT varValue; + + VariantInit( &varValue ); + + bstrName = SysAllocString( pwszName ); + if ( bstrName == NULL ) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR(hr); + goto error; + } + + hr = pSitesCollectionEntry->GetPropertyByName( bstrName, + &pProperty ); + if ( FAILED ( hr ) ) + { + goto error; + } + + hr = pProperty->get_Value( &varValue ); + if ( FAILED ( hr ) ) + { + goto error; + } + + hr = VariantChangeType( &varValue, &varValue, 0, VT_UI4 ); + if ( FAILED ( hr ) ) + { + goto error; + } + + // extract the value + *pdwValue = varValue.ulVal; + +error: + + VariantClear( &varValue ); + + if ( pProperty != NULL ) + { + pProperty->Release(); + pProperty = NULL; + } + + if ( bstrName != NULL ) + { + SysFreeString( bstrName ); + bstrName = NULL; + } + + return hr; +} + +HRESULT +GetElementLONGLONGProperty( + IN IAppHostElement * pSitesCollectionEntry, + IN LPCWSTR pwszName, + OUT LONGLONG * pllValue +) +{ + HRESULT hr = S_OK; + IAppHostProperty * pProperty = NULL; + BSTR bstrName = NULL; + VARIANT varValue; + + VariantInit( &varValue ); + + bstrName = SysAllocString( pwszName ); + if ( bstrName == NULL ) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR(hr); + goto error; + } + + hr = pSitesCollectionEntry->GetPropertyByName( bstrName, + &pProperty ); + if ( FAILED ( hr ) ) + { + goto error; + } + + hr = pProperty->get_Value( &varValue ); + if ( FAILED ( hr ) ) + { + goto error; + } + + hr = VariantChangeType( &varValue, &varValue, 0, VT_I8 ); + if ( FAILED ( hr ) ) + { + goto error; + } + + // extract the value + *pllValue = varValue.ulVal; + +error: + + VariantClear( &varValue ); + + if ( pProperty != NULL ) + { + pProperty->Release(); + pProperty = NULL; + } + + if ( bstrName != NULL ) + { + SysFreeString( bstrName ); + bstrName = NULL; + } + + return hr; +} + +HRESULT +GetElementRawTimeSpanProperty( + IN IAppHostElement * pElement, + IN LPCWSTR pszPropertyName, + OUT ULONGLONG * pulonglong +) +{ + HRESULT hr = S_OK; + BSTR bstrPropertyName = NULL; + IAppHostProperty * pProperty = NULL; + VARIANT varValue; + + VariantInit( &varValue ); + + bstrPropertyName = SysAllocString( pszPropertyName ); + if ( bstrPropertyName == NULL ) + { + hr = HRESULT_FROM_WIN32( ERROR_NOT_ENOUGH_MEMORY ); + goto Finished; + } + + // Now ask for the property and if it succeeds it is returned directly back + hr = pElement->GetPropertyByName( bstrPropertyName, &pProperty ); + if ( FAILED ( hr ) ) + { + goto Finished; + } + + // Now let's get the property and then extract it from the Variant. + hr = pProperty->get_Value( &varValue ); + if ( FAILED ( hr ) ) + { + goto Finished; + } + + hr = VariantChangeType( &varValue, &varValue, 0, VT_UI8 ); + if ( FAILED ( hr ) ) + { + goto Finished; + } + + // extract the value + *pulonglong = varValue.ullVal; + + +Finished: + + VariantClear( &varValue ); + + if ( bstrPropertyName != NULL ) + { + SysFreeString( bstrPropertyName ); + bstrPropertyName = NULL; + } + + if ( pProperty != NULL ) + { + pProperty->Release(); + pProperty = NULL; + } + + return hr; + +} // end of Config_GetRawTimeSpanProperty + +HRESULT +DeleteElementFromCollection( + IAppHostElementCollection *pCollection, + CONST WCHAR * szKeyName, + CONST WCHAR * szKeyValue, + ULONG BehaviorFlags, + BOOL * pfDeleted + ) +{ + HRESULT hr = NOERROR; + ULONG index; + + VARIANT varIndex; + VariantInit( &varIndex ); + + *pfDeleted = FALSE; + + hr = FindElementInCollection( + pCollection, + szKeyName, + szKeyValue, + BehaviorFlags, + &index + ); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + if (hr == S_FALSE) + { + // + // Not found. + // + + goto exit; + } + + varIndex.vt = VT_UI4; + varIndex.ulVal = index; + + hr = pCollection->DeleteElement( varIndex ); + + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto exit; + } + + *pfDeleted = TRUE; + +exit: + + return hr; +} + +HRESULT +DeleteAllElementsFromCollection( + IAppHostElementCollection *pCollection, + CONST WCHAR * szKeyName, + CONST WCHAR * szKeyValue, + ULONG BehaviorFlags, + UINT * pNumDeleted + ) +{ + HRESULT hr = S_OK; + UINT numDeleted = 0; + BOOL fDeleted = TRUE; + + while (fDeleted) + { + hr = DeleteElementFromCollection( + pCollection, + szKeyName, + szKeyValue, + BehaviorFlags, + &fDeleted + ); + + if (hr == S_FALSE) + { + hr = S_OK; + break; + } + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + break; + } + + if (fDeleted) + { + numDeleted++; + } + } + + *pNumDeleted = numDeleted; + return hr; +} + +BOOL +FindCompareCaseSensitive( + CONST WCHAR * szLookupValue, + CONST WCHAR * szKeyValue + ) +{ + return !wcscmp(szLookupValue, szKeyValue); +} + +BOOL +FindCompareCaseInsensitive( + CONST WCHAR * szLookupValue, + CONST WCHAR * szKeyValue + ) +{ + return !_wcsicmp(szLookupValue, szKeyValue); +} + +typedef +BOOL +(*PFN_FIND_COMPARE_PROC)( + CONST WCHAR *szLookupValue, + CONST WCHAR *szKeyValue + ); + +HRESULT +FindElementInCollection( + IAppHostElementCollection *pCollection, + CONST WCHAR * szKeyName, + CONST WCHAR * szKeyValue, + ULONG BehaviorFlags, + OUT ULONG * pIndex + ) +{ + HRESULT hr = NOERROR; + + CComPtr pElement; + CComPtr pKeyProperty; + + VARIANT varIndex; + VariantInit( &varIndex ); + + VARIANT varKeyValue; + VariantInit( &varKeyValue ); + + DWORD count; + DWORD i; + + BSTR bstrKeyName = NULL; + PFN_FIND_COMPARE_PROC compareProc; + + compareProc = (BehaviorFlags & FIND_ELEMENT_CASE_INSENSITIVE) + ? &FindCompareCaseInsensitive + : &FindCompareCaseSensitive; + + bstrKeyName = SysAllocString( szKeyName ); + if( !bstrKeyName ) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR(hr); + goto exit; + } + + hr = pCollection->get_Count( &count ); + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto exit; + } + + for( i = 0; i < count; i++ ) + { + varIndex.vt = VT_UI4; + varIndex.ulVal = i; + + hr = pCollection->get_Item( varIndex, + &pElement ); + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto tryNext; + } + + hr = pElement->GetPropertyByName( bstrKeyName, + &pKeyProperty ); + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto tryNext; + } + + hr = pKeyProperty->get_Value( &varKeyValue ); + + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto tryNext; + } + + if ((compareProc)(szKeyValue, varKeyValue.bstrVal)) + { + *pIndex = i; + break; + } + +tryNext: + + pElement.Release(); + pKeyProperty.Release(); + + VariantClear( &varKeyValue ); + } + + if (i >= count) + { + hr = S_FALSE; + } + +exit: + + SysFreeString( bstrKeyName ); + VariantClear( &varKeyValue ); + + return hr; +} + +HRESULT +VariantAssign( + IN OUT VARIANT * pv, + IN CONST WCHAR * sz + ) +{ + if( !pv || !sz ) + { + return E_INVALIDARG; + } + + HRESULT hr = NOERROR; + + BSTR bstr = SysAllocString( sz ); + if( !bstr ) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR( hr ); + goto exit; + } + + hr = VariantClear( pv ); + if( FAILED(hr) ) + { + DBGERROR_HR( hr ); + goto exit; + } + + pv->vt = VT_BSTR; + pv->bstrVal = bstr; + bstr = NULL; + +exit: + + SysFreeString( bstr ); + + return hr; +} + +HRESULT +GetLocationFromFile( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + IN CONST WCHAR * szLocationPath, + OUT IAppHostConfigLocation ** ppLocation, + OUT BOOL * pFound + ) +{ + HRESULT hr = NOERROR; + + CComPtr pLocationCollection; + CComPtr pLocation; + + BSTR bstrLocationPath = NULL; + + *ppLocation = NULL; + *pFound = FALSE; + + hr = GetLocationCollection( pAdminMgr, + szConfigPath, + &pLocationCollection ); + + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto exit; + } + + DWORD count; + DWORD i; + VARIANT varIndex; + VariantInit( &varIndex ); + + hr = pLocationCollection->get_Count( &count ); + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto exit; + } + + for( i = 0; i < count; i++ ) + { + varIndex.vt = VT_UI4; + varIndex.ulVal = i; + + hr = pLocationCollection->get_Item( varIndex, + &pLocation ); + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto exit; + } + + hr = pLocation->get_Path( &bstrLocationPath ); + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto exit; + } + + if( 0 == wcscmp ( szLocationPath, bstrLocationPath ) ) + { + *pFound = TRUE; + *ppLocation = pLocation.Detach(); + break; + } + + + pLocation.Release(); + + SysFreeString( bstrLocationPath ); + bstrLocationPath = NULL; + } + +exit: + + SysFreeString( bstrLocationPath ); + + return hr; +} + +HRESULT +GetSectionFromLocation( + IN IAppHostConfigLocation * pLocation, + IN CONST WCHAR * szSectionName, + OUT IAppHostElement ** ppSectionElement, + OUT BOOL * pFound + ) +{ + HRESULT hr = NOERROR; + + CComPtr pSectionElement; + + DWORD count; + DWORD i; + + VARIANT varIndex; + VariantInit( &varIndex ); + + BSTR bstrSectionName = NULL; + + *pFound = FALSE; + *ppSectionElement = NULL; + + hr = pLocation->get_Count( &count ); + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto exit; + } + + for( i = 0; i < count; i++ ) + { + varIndex.vt = VT_UI4; + varIndex.ulVal = i; + + + hr = pLocation->get_Item( varIndex, + &pSectionElement ); + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto exit; + } + + hr = pSectionElement->get_Name( &bstrSectionName ); + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto exit; + } + + if( 0 == wcscmp ( szSectionName, bstrSectionName ) ) + { + *pFound = TRUE; + *ppSectionElement = pSectionElement.Detach(); + break; + } + + pSectionElement.Release(); + + SysFreeString( bstrSectionName ); + bstrSectionName = NULL; + } + +exit: + + SysFreeString( bstrSectionName ); + + return hr; +} + + +HRESULT +GetAdminElement( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + IN CONST WCHAR * szElementName, + OUT IAppHostElement ** pElement +) +{ + HRESULT hr = S_OK; + BSTR bstrConfigPath = NULL; + BSTR bstrElementName = NULL; + + bstrConfigPath = SysAllocString(szConfigPath); + bstrElementName = SysAllocString(szElementName); + + if (bstrConfigPath == NULL || bstrElementName == NULL) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR(hr); + goto exit; + } + + hr = pAdminMgr->GetAdminSection( bstrElementName, + bstrConfigPath, + pElement ); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + +exit: + + if ( bstrElementName != NULL ) + { + SysFreeString(bstrElementName); + bstrElementName = NULL; + } + if ( bstrConfigPath != NULL ) + { + SysFreeString(bstrConfigPath); + bstrConfigPath = NULL; + } + + return hr; +} + + +HRESULT +ClearAdminElement( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + IN CONST WCHAR * szElementName + ) +{ + HRESULT hr; + CComPtr pElement; + + hr = GetAdminElement( + pAdminMgr, + szConfigPath, + szElementName, + &pElement + ); + + if (FAILED(hr)) + { + if (hr == HRESULT_FROM_WIN32(ERROR_NOT_FOUND)) + { + hr = S_OK; + } + else + { + DBGERROR_HR(hr); + } + + goto exit; + } + + hr = pElement->Clear(); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + +exit: + + return hr; +} + + +HRESULT +ClearElementFromAllSites( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + IN CONST WCHAR * szElementName + ) +{ + HRESULT hr; + CComPtr pSitesCollection; + CComPtr pSiteElement; + CComPtr pChildCollection; + ENUM_INDEX index; + BOOL found; + + // + // Enumerate the sites, remove the specified elements. + // + + hr = GetSitesCollection( + pAdminMgr, + szConfigPath, + &pSitesCollection + ); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + for (hr = FindFirstElement(pSitesCollection, &index, &pSiteElement) ; + SUCCEEDED(hr) ; + hr = FindNextElement(pSitesCollection, &index, &pSiteElement)) + { + if (hr == S_FALSE) + { + hr = S_OK; + break; + } + + hr = pSiteElement->get_ChildElements(&pChildCollection); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + if (pChildCollection) + { + hr = ClearChildElementsByName( + pChildCollection, + szElementName, + &found + ); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + } + + pSiteElement.Release(); + } + +exit: + + return hr; + +} + + +HRESULT +ClearElementFromAllLocations( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + IN CONST WCHAR * szElementName + ) +{ + HRESULT hr; + CComPtr pLocationCollection; + CComPtr pLocation; + CComPtr pChildCollection; + ENUM_INDEX index; + + // + // Enum the tags, remove the specified elements. + // + + hr = GetLocationCollection( + pAdminMgr, + szConfigPath, + &pLocationCollection + ); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + for (hr = FindFirstLocation(pLocationCollection, &index, &pLocation) ; + SUCCEEDED(hr) ; + hr = FindNextLocation(pLocationCollection, &index, &pLocation)) + { + if (hr == S_FALSE) + { + hr = S_OK; + break; + } + + hr = ClearLocationElements(pLocation, szElementName); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + pLocation.Release(); + } + +exit: + + return hr; + +} + +HRESULT +ClearLocationElements( + IN IAppHostConfigLocation * pLocation, + IN CONST WCHAR * szElementName + ) +{ + HRESULT hr; + CComPtr pElement; + ENUM_INDEX index; + BOOL matched; + + for (hr = FindFirstLocationElement(pLocation, &index, &pElement) ; + SUCCEEDED(hr) ; + hr = FindNextLocationElement(pLocation, &index, &pElement)) + { + if (hr == S_FALSE) + { + hr = S_OK; + break; + } + + hr = CompareElementName(pElement, szElementName, &matched); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + if (matched) + { + pElement->Clear(); + } + + pElement.Release(); + } + +exit: + + return hr; +} + +HRESULT +CompareElementName( + IN IAppHostElement * pElement, + IN CONST WCHAR * szNameToMatch, + OUT BOOL * pMatched + ) +{ + HRESULT hr; + BSTR bstrElementName = NULL; + + *pMatched = FALSE; // until proven otherwise + + hr = pElement->get_Name(&bstrElementName); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + if( 0 == wcscmp ( szNameToMatch, bstrElementName ) ) + { + *pMatched = TRUE; + } + +exit: + + SysFreeString(bstrElementName); + return hr; +} + + +HRESULT +ClearChildElementsByName( + IN IAppHostChildElementCollection * pCollection, + IN CONST WCHAR * szElementName, + OUT BOOL * pFound + ) +{ + HRESULT hr; + CComPtr pElement; + ENUM_INDEX index; + BOOL matched; + + *pFound = FALSE; + + for (hr = FindFirstChildElement(pCollection, &index, &pElement) ; + SUCCEEDED(hr) ; + hr = FindNextChildElement(pCollection, &index, &pElement)) + { + if (hr == S_FALSE) + { + hr = S_OK; + break; + } + + hr = CompareElementName(pElement, szElementName, &matched); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + if (matched) + { + hr = pElement->Clear(); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + *pFound = TRUE; + } + + pElement.Release(); + } + +exit: + + return hr; +} + + +HRESULT +GetSitesCollection( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + OUT IAppHostElementCollection ** pSitesCollection + ) +{ + HRESULT hr; + CComPtr pSitesElement; + BSTR bstrConfigPath; + BSTR bstrSitesSectionName; + + bstrConfigPath = SysAllocString(szConfigPath); + bstrSitesSectionName = SysAllocString(L"system.applicationHost/sites"); + *pSitesCollection = NULL; + + if (bstrConfigPath == NULL || bstrSitesSectionName == NULL) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR(hr); + goto exit; + } + + // + // Chase down the sites collection. + // + + hr = pAdminMgr->GetAdminSection( bstrSitesSectionName, + bstrConfigPath, + &pSitesElement ); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + hr = pSitesElement->get_Collection(pSitesCollection); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + +exit: + + SysFreeString(bstrSitesSectionName); + SysFreeString(bstrConfigPath); + return hr; +} + + +HRESULT +GetLocationCollection( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + OUT IAppHostConfigLocationCollection ** pLocationCollection + ) +{ + HRESULT hr; + BSTR bstrConfigPath; + CComPtr pConfigMgr; + CComPtr pConfigFile; + + bstrConfigPath = SysAllocString(szConfigPath); + *pLocationCollection = NULL; + + if (bstrConfigPath == NULL) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR(hr); + goto exit; + } + + hr = pAdminMgr->get_ConfigManager(&pConfigMgr); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + hr = pConfigMgr->GetConfigFile(bstrConfigPath, &pConfigFile); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + hr = pConfigFile->get_Locations(pLocationCollection); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + +exit: + + SysFreeString(bstrConfigPath); + return hr; +} + + +HRESULT +FindFirstElement( + IN IAppHostElementCollection * pCollection, + OUT ENUM_INDEX * pIndex, + OUT IAppHostElement ** pElement + ) +{ + HRESULT hr; + + hr = pCollection->get_Count(&pIndex->Count); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + return hr; + } + + VariantInit(&pIndex->Index); + pIndex->Index.vt = VT_UI4; + pIndex->Index.ulVal = 0; + + return FindNextElement(pCollection, pIndex, pElement); +} + +HRESULT +FindNextElement( + IN IAppHostElementCollection * pCollection, + IN OUT ENUM_INDEX * pIndex, + OUT IAppHostElement ** pElement + ) +{ + HRESULT hr; + + *pElement = NULL; + + if (pIndex->Index.ulVal >= pIndex->Count) + { + return S_FALSE; + } + + hr = pCollection->get_Item(pIndex->Index, pElement); + + if (SUCCEEDED(hr)) + { + pIndex->Index.ulVal++; + } + + return hr; +} + +HRESULT +FindFirstChildElement( + IN IAppHostChildElementCollection * pCollection, + OUT ENUM_INDEX * pIndex, + OUT IAppHostElement ** pElement + ) +{ + HRESULT hr; + + hr = pCollection->get_Count(&pIndex->Count); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + return hr; + } + + VariantInit(&pIndex->Index); + pIndex->Index.vt = VT_UI4; + pIndex->Index.ulVal = 0; + + return FindNextChildElement(pCollection, pIndex, pElement); +} + +HRESULT +FindNextChildElement( + IN IAppHostChildElementCollection * pCollection, + IN OUT ENUM_INDEX * pIndex, + OUT IAppHostElement ** pElement + ) +{ + HRESULT hr; + + *pElement = NULL; + + if (pIndex->Index.ulVal >= pIndex->Count) + { + return S_FALSE; + } + + hr = pCollection->get_Item(pIndex->Index, pElement); + + if (SUCCEEDED(hr)) + { + pIndex->Index.ulVal++; + } + + return hr; +} + +HRESULT +FindFirstLocation( + IN IAppHostConfigLocationCollection * pCollection, + OUT ENUM_INDEX * pIndex, + OUT IAppHostConfigLocation ** pLocation + ) +{ + HRESULT hr; + + hr = pCollection->get_Count(&pIndex->Count); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + return hr; + } + + VariantInit(&pIndex->Index); + pIndex->Index.vt = VT_UI4; + pIndex->Index.ulVal = 0; + + return FindNextLocation(pCollection, pIndex, pLocation); +} + +HRESULT +FindNextLocation( + IN IAppHostConfigLocationCollection * pCollection, + IN OUT ENUM_INDEX * pIndex, + OUT IAppHostConfigLocation ** pLocation + ) +{ + HRESULT hr; + + *pLocation = NULL; + + if (pIndex->Index.ulVal >= pIndex->Count) + { + return S_FALSE; + } + + hr = pCollection->get_Item(pIndex->Index, pLocation); + + if (SUCCEEDED(hr)) + { + pIndex->Index.ulVal++; + } + + return hr; +} + +HRESULT +FindFirstLocationElement( + IN IAppHostConfigLocation * pLocation, + OUT ENUM_INDEX * pIndex, + OUT IAppHostElement ** pElement + ) +{ + HRESULT hr; + + hr = pLocation->get_Count(&pIndex->Count); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + return hr; + } + + VariantInit(&pIndex->Index); + pIndex->Index.vt = VT_UI4; + pIndex->Index.ulVal = 0; + + return FindNextLocationElement(pLocation, pIndex, pElement); +} + +HRESULT +FindNextLocationElement( + IN IAppHostConfigLocation * pLocation, + IN OUT ENUM_INDEX * pIndex, + OUT IAppHostElement ** pElement + ) +{ + HRESULT hr; + + *pElement = NULL; + + if (pIndex->Index.ulVal >= pIndex->Count) + { + return S_FALSE; + } + + hr = pLocation->get_Item(pIndex->Index, pElement); + + if (SUCCEEDED(hr)) + { + pIndex->Index.ulVal++; + } + + return hr; +} + +HRESULT +GetSharedConfigEnabled( + BOOL * pfIsSharedConfig +) +/*++ + +Routine Description: + Search the configuration for the shared configuration property. + +Arguments: + + pfIsSharedConfig - true if shared configuration is enabled + +Return Value: + HRESULT + +--*/ +{ + HRESULT hr = S_OK; + IAppHostAdminManager *pAdminManager = NULL; + + BSTR bstrSectionName = NULL; + BSTR bstrConfigPath = NULL; + + IAppHostElement * pConfigRedirSection = NULL; + + + bstrSectionName = SysAllocString( L"configurationRedirection" ); + + if ( bstrSectionName == NULL ) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR(hr); + goto exit; + } + + bstrConfigPath = SysAllocString( L"MACHINE/REDIRECTION" ); + if ( bstrConfigPath == NULL ) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR(hr); + goto exit; + } + + hr = CoCreateInstance( CLSID_AppHostAdminManager, + NULL, + CLSCTX_INPROC_SERVER, + IID_IAppHostAdminManager, + (VOID **)&pAdminManager ); + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto exit; + } + + hr = pAdminManager->GetAdminSection( bstrSectionName, + bstrConfigPath, + &pConfigRedirSection ); + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto exit; + } + + hr = GetElementBoolProperty( pConfigRedirSection, + L"enabled", + pfIsSharedConfig ); + + if ( FAILED( hr ) ) + { + DBGERROR_HR(hr); + goto exit; + } + + pConfigRedirSection->Release(); + pConfigRedirSection = NULL; + + +exit: + + // + // dump config exception to setup log file (if available) + // + + if ( pConfigRedirSection != NULL ) + { + pConfigRedirSection->Release(); + } + + if ( pAdminManager != NULL ) + { + pAdminManager->Release(); + } + + if ( bstrConfigPath != NULL ) + { + SysFreeString( bstrConfigPath ); + } + + if ( bstrSectionName != NULL ) + { + SysFreeString( bstrSectionName ); + } + + return hr; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/ahutil.h b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/ahutil.h new file mode 100644 index 0000000000..5694b21b7e --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/ahutil.h @@ -0,0 +1,258 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once +#include + +HRESULT +SetElementProperty( + IN IAppHostElement * pElement, + IN CONST WCHAR * szPropName, + IN CONST VARIANT * varPropValue + ); + +HRESULT +SetElementStringProperty( + IN IAppHostElement * pElement, + IN CONST WCHAR * szPropName, + IN CONST WCHAR * szPropValue + ); + +HRESULT +GetElementStringProperty( + IN IAppHostElement * pElement, + IN CONST WCHAR * szPropName, + OUT BSTR * pbstrPropValue + ); + +HRESULT +GetElementStringProperty( + IN IAppHostElement * pElement, + IN CONST WCHAR * szPropName, + OUT STRU * pstrPropValue + ); + +HRESULT +GetElementBoolProperty( + IN IAppHostElement * pElement, + IN LPCWSTR pszPropertyName, + OUT BOOL * pBool + ); + +HRESULT +GetElementBoolProperty( + IN IAppHostElement * pElement, + IN LPCWSTR pszPropertyName, + OUT bool * pBool + ); + +HRESULT +GetElementChildByName( + IN IAppHostElement * pElement, + IN LPCWSTR pszElementName, + OUT IAppHostElement ** ppChildElement + ); + +HRESULT +GetElementDWORDProperty( + IN IAppHostElement * pElement, + IN LPCWSTR pszPropertyName, + OUT DWORD * pdwValue + ); + +HRESULT +GetElementLONGLONGProperty( + IN IAppHostElement * pElement, + IN LPCWSTR pszPropertyName, + OUT LONGLONG * pllValue +); + + +HRESULT +GetElementRawTimeSpanProperty( + IN IAppHostElement * pElement, + IN LPCWSTR pszPropertyName, + OUT ULONGLONG * pulonglong + ); + +#define FIND_ELEMENT_CASE_SENSITIVE 0x00000000 +#define FIND_ELEMENT_CASE_INSENSITIVE 0x00000001 + +HRESULT +DeleteElementFromCollection( + IAppHostElementCollection *pCollection, + CONST WCHAR * szKeyName, + CONST WCHAR * szKeyValue, + ULONG BehaviorFlags, + BOOL * pfDeleted + ); + +HRESULT +DeleteAllElementsFromCollection( + IAppHostElementCollection *pCollection, + CONST WCHAR * szKeyName, + CONST WCHAR * szKeyValue, + ULONG BehaviorFlags, + UINT * pNumDeleted + ); + +HRESULT +FindElementInCollection( + IAppHostElementCollection *pCollection, + CONST WCHAR * szKeyName, + CONST WCHAR * szKeyValue, + ULONG BehaviorFlags, + OUT ULONG * pIndex + ); + +HRESULT +VariantAssign( + IN OUT VARIANT * pv, + IN CONST WCHAR * sz + ); + +HRESULT +GetLocationFromFile( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + IN CONST WCHAR * szLocationPath, + OUT IAppHostConfigLocation ** ppLocation, + OUT BOOL * pFound + ); + +HRESULT +GetSectionFromLocation( + IN IAppHostConfigLocation * pLocation, + IN CONST WCHAR * szSectionName, + OUT IAppHostElement ** ppSectionElement, + OUT BOOL * pFound + ); + +HRESULT +GetAdminElement( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + IN CONST WCHAR * szElementName, + OUT IAppHostElement ** pElement + ); + +HRESULT +ClearAdminElement( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + IN CONST WCHAR * szElementName + ); + +HRESULT +ClearElementFromAllSites( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + IN CONST WCHAR * szElementName + ); + +HRESULT +ClearElementFromAllLocations( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + IN CONST WCHAR * szElementName + ); + +HRESULT +ClearLocationElements( + IN IAppHostConfigLocation * pLocation, + IN CONST WCHAR * szElementName + ); + +HRESULT +CompareElementName( + IN IAppHostElement * pElement, + IN CONST WCHAR * szNameToMatch, + OUT BOOL * pMatched + ); + +HRESULT +ClearChildElementsByName( + IN IAppHostChildElementCollection * pCollection, + IN CONST WCHAR * szElementName, + OUT BOOL * pFound + ); + +HRESULT +GetSitesCollection( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + OUT IAppHostElementCollection ** pSitesCollection + ); + +HRESULT +GetLocationCollection( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + OUT IAppHostConfigLocationCollection ** pLocationCollection + ); + +struct ENUM_INDEX +{ + VARIANT Index; + ULONG Count; +}; + +HRESULT +FindFirstElement( + IN IAppHostElementCollection * pCollection, + OUT ENUM_INDEX * pIndex, + OUT IAppHostElement ** pElement + ); + +HRESULT +FindNextElement( + IN IAppHostElementCollection * pCollection, + IN OUT ENUM_INDEX * pIndex, + OUT IAppHostElement ** pElement + ); + +HRESULT +FindFirstChildElement( + IN IAppHostChildElementCollection * pCollection, + OUT ENUM_INDEX * pIndex, + OUT IAppHostElement ** pElement + ); + +HRESULT +FindNextChildElement( + IN IAppHostChildElementCollection * pCollection, + IN OUT ENUM_INDEX * pIndex, + OUT IAppHostElement ** pElement + ); + +HRESULT +FindFirstLocation( + IN IAppHostConfigLocationCollection * pCollection, + OUT ENUM_INDEX * pIndex, + OUT IAppHostConfigLocation ** pLocation + ); + +HRESULT +FindNextLocation( + IN IAppHostConfigLocationCollection * pCollection, + IN OUT ENUM_INDEX * pIndex, + OUT IAppHostConfigLocation ** pLocation + ); + +HRESULT +FindFirstLocationElement( + IN IAppHostConfigLocation * pLocation, + OUT ENUM_INDEX * pIndex, + OUT IAppHostElement ** pElement + ); + +HRESULT +FindNextLocationElement( + IN IAppHostConfigLocation * pLocation, + IN OUT ENUM_INDEX * pIndex, + OUT IAppHostElement ** pElement + ); + +HRESULT GetSharedConfigEnabled( + BOOL * pfIsSharedConfig +); \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/base64.cpp b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/base64.cpp new file mode 100644 index 0000000000..b8b6a0bf74 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/base64.cpp @@ -0,0 +1,482 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "precomp.h" + +DWORD +Base64Encode( + __in_bcount(cbDecodedBufferSize) VOID * pDecodedBuffer, + IN DWORD cbDecodedBufferSize, + __out_ecount_opt(cchEncodedStringSize) PWSTR pszEncodedString, + IN DWORD cchEncodedStringSize, + __out_opt DWORD * pcchEncoded + ) +/*++ + +Routine Description: + + Decode a base64-encoded string. + +Arguments: + + pDecodedBuffer (IN) - buffer to encode. + cbDecodedBufferSize (IN) - size of buffer to encode. + cchEncodedStringSize (IN) - size of the buffer for the encoded string. + pszEncodedString (OUT) = the encoded string. + pcchEncoded (OUT) - size in characters of the encoded string. + +Return Values: + + 0 - success. + E_OUTOFMEMORY + +--*/ +{ + static WCHAR rgchEncodeTable[64] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' + }; + + DWORD ib; + DWORD ich; + DWORD cchEncoded; + BYTE b0, b1, b2; + BYTE * pbDecodedBuffer = (BYTE *) pDecodedBuffer; + + // Calculate encoded string size. + cchEncoded = 1 + (cbDecodedBufferSize + 2) / 3 * 4; + + if (NULL != pcchEncoded) { + *pcchEncoded = cchEncoded; + } + + if (cchEncodedStringSize == 0 && pszEncodedString == NULL) { + return ERROR_SUCCESS; + } + + if (cchEncodedStringSize < cchEncoded) { + // Given buffer is too small to hold encoded string. + return ERROR_INSUFFICIENT_BUFFER; + } + + // Encode data byte triplets into four-byte clusters. + ib = ich = 0; + while (ib < cbDecodedBufferSize) { + b0 = pbDecodedBuffer[ib++]; + b1 = (ib < cbDecodedBufferSize) ? pbDecodedBuffer[ib++] : 0; + b2 = (ib < cbDecodedBufferSize) ? pbDecodedBuffer[ib++] : 0; + + // + // The checks below for buffer overflow seems redundant to me. + // But it's the only way I can find to keep OACR quiet so it + // will have to do. + // + + pszEncodedString[ich++] = rgchEncodeTable[b0 >> 2]; + if ( ich >= cchEncodedStringSize ) + { + DBG_ASSERT( FALSE ); + return ERROR_BUFFER_OVERFLOW; + } + + pszEncodedString[ich++] = rgchEncodeTable[((b0 << 4) & 0x30) | ((b1 >> 4) & 0x0f)]; + if ( ich >= cchEncodedStringSize ) + { + DBG_ASSERT( FALSE ); + return ERROR_BUFFER_OVERFLOW; + } + + pszEncodedString[ich++] = rgchEncodeTable[((b1 << 2) & 0x3c) | ((b2 >> 6) & 0x03)]; + if ( ich >= cchEncodedStringSize ) + { + DBG_ASSERT( FALSE ); + return ERROR_BUFFER_OVERFLOW; + } + + pszEncodedString[ich++] = rgchEncodeTable[b2 & 0x3f]; + if ( ich >= cchEncodedStringSize ) + { + DBG_ASSERT( FALSE ); + return ERROR_BUFFER_OVERFLOW; + } + } + + // Pad the last cluster as necessary to indicate the number of data bytes + // it represents. + switch (cbDecodedBufferSize % 3) { + case 0: + break; + case 1: + pszEncodedString[ich - 2] = '='; + __fallthrough; + case 2: + pszEncodedString[ich - 1] = '='; + break; + } + + // Null-terminate the encoded string. + pszEncodedString[ich++] = '\0'; + + DBG_ASSERT(ich == cchEncoded); + + return ERROR_SUCCESS; +} + + +DWORD +Base64Decode( + __in PCWSTR pszEncodedString, + __out_opt VOID * pDecodeBuffer, + __in DWORD cbDecodeBufferSize, + __out_opt DWORD * pcbDecoded + ) +/*++ + +Routine Description: + + Decode a base64-encoded string. + +Arguments: + + pszEncodedString (IN) - base64-encoded string to decode. + cbDecodeBufferSize (IN) - size in bytes of the decode buffer. + pbDecodeBuffer (OUT) - holds the decoded data. + pcbDecoded (OUT) - number of data bytes in the decoded data (if success or + STATUS_BUFFER_TOO_SMALL). + +Return Values: + + 0 - success. + E_OUTOFMEMORY + E_INVALIDARG + +--*/ +{ +#define NA (255) +#define DECODE(x) (((ULONG)(x) < sizeof(rgbDecodeTable)) ? rgbDecodeTable[x] : NA) + + static BYTE rgbDecodeTable[128] = { + NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, // 0-15 + NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, // 16-31 + NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 62, NA, NA, NA, 63, // 32-47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, NA, NA, NA, 0, NA, NA, // 48-63 + NA, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 64-79 + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, NA, NA, NA, NA, NA, // 80-95 + NA, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 96-111 + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, NA, NA, NA, NA, NA, // 112-127 + }; + + DWORD cbDecoded; + DWORD cchEncodedSize; + DWORD ich; + DWORD ib; + BYTE b0, b1, b2, b3; + BYTE * pbDecodeBuffer = (BYTE *) pDecodeBuffer; + + cchEncodedSize = (DWORD)wcslen(pszEncodedString); + if (NULL != pcbDecoded) { + *pcbDecoded = 0; + } + + if ((0 == cchEncodedSize) || (0 != (cchEncodedSize % 4))) { + // Input string is not sized correctly to be base64. + return ERROR_INVALID_PARAMETER; + } + + // Calculate decoded buffer size. + cbDecoded = (cchEncodedSize + 3) / 4 * 3; + if (pszEncodedString[cchEncodedSize-1] == '=') { + if (pszEncodedString[cchEncodedSize-2] == '=') { + // Only one data byte is encoded in the last cluster. + cbDecoded -= 2; + } + else { + // Only two data bytes are encoded in the last cluster. + cbDecoded -= 1; + } + } + + if (NULL != pcbDecoded) { + *pcbDecoded = cbDecoded; + } + + if (cbDecodeBufferSize == 0 && pDecodeBuffer == NULL) { + return ERROR_SUCCESS; + } + + if (cbDecoded > cbDecodeBufferSize) { + // Supplied buffer is too small. + return ERROR_INSUFFICIENT_BUFFER; + } + + // Decode each four-byte cluster into the corresponding three data bytes. + ich = ib = 0; + while (ich < cchEncodedSize) { + b0 = DECODE(pszEncodedString[ich]); ich++; + b1 = DECODE(pszEncodedString[ich]); ich++; + b2 = DECODE(pszEncodedString[ich]); ich++; + b3 = DECODE(pszEncodedString[ich]); ich++; + + if ((NA == b0) || (NA == b1) || (NA == b2) || (NA == b3)) { + // Contents of input string are not base64. + return ERROR_INVALID_PARAMETER; + } + + pbDecodeBuffer[ib++] = (b0 << 2) | (b1 >> 4); + + if (ib < cbDecoded) { + pbDecodeBuffer[ib++] = (b1 << 4) | (b2 >> 2); + + if (ib < cbDecoded) { + pbDecodeBuffer[ib++] = (b2 << 6) | b3; + } + } + } + + DBG_ASSERT(ib == cbDecoded); + + return ERROR_SUCCESS; +} + + +DWORD +Base64Encode( + __in_bcount(cbDecodedBufferSize) VOID * pDecodedBuffer, + IN DWORD cbDecodedBufferSize, + __out_ecount_opt(cchEncodedStringSize) PSTR pszEncodedString, + IN DWORD cchEncodedStringSize, + __out_opt DWORD * pcchEncoded + ) +/*++ + +Routine Description: + + Decode a base64-encoded string. + +Arguments: + + pDecodedBuffer (IN) - buffer to encode. + cbDecodedBufferSize (IN) - size of buffer to encode. + cchEncodedStringSize (IN) - size of the buffer for the encoded string. + pszEncodedString (OUT) = the encoded string. + pcchEncoded (OUT) - size in characters of the encoded string. + +Return Values: + + 0 - success. + E_OUTOFMEMORY + +--*/ +{ + static CHAR rgchEncodeTable[64] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' + }; + + DWORD ib; + DWORD ich; + DWORD cchEncoded; + BYTE b0, b1, b2; + BYTE * pbDecodedBuffer = (BYTE *) pDecodedBuffer; + + // Calculate encoded string size. + cchEncoded = 1 + (cbDecodedBufferSize + 2) / 3 * 4; + + if (NULL != pcchEncoded) { + *pcchEncoded = cchEncoded; + } + + if (cchEncodedStringSize == 0 && pszEncodedString == NULL) { + return ERROR_SUCCESS; + } + + if (cchEncodedStringSize < cchEncoded) { + // Given buffer is too small to hold encoded string. + return ERROR_INSUFFICIENT_BUFFER; + } + + // Encode data byte triplets into four-byte clusters. + ib = ich = 0; + while (ib < cbDecodedBufferSize) { + b0 = pbDecodedBuffer[ib++]; + b1 = (ib < cbDecodedBufferSize) ? pbDecodedBuffer[ib++] : 0; + b2 = (ib < cbDecodedBufferSize) ? pbDecodedBuffer[ib++] : 0; + + // + // The checks below for buffer overflow seems redundant to me. + // But it's the only way I can find to keep OACR quiet so it + // will have to do. + // + + pszEncodedString[ich++] = rgchEncodeTable[b0 >> 2]; + if ( ich >= cchEncodedStringSize ) + { + DBG_ASSERT( FALSE ); + return ERROR_BUFFER_OVERFLOW; + } + + pszEncodedString[ich++] = rgchEncodeTable[((b0 << 4) & 0x30) | ((b1 >> 4) & 0x0f)]; + if ( ich >= cchEncodedStringSize ) + { + DBG_ASSERT( FALSE ); + return ERROR_BUFFER_OVERFLOW; + } + + pszEncodedString[ich++] = rgchEncodeTable[((b1 << 2) & 0x3c) | ((b2 >> 6) & 0x03)]; + if ( ich >= cchEncodedStringSize ) + { + DBG_ASSERT( FALSE ); + return ERROR_BUFFER_OVERFLOW; + } + + pszEncodedString[ich++] = rgchEncodeTable[b2 & 0x3f]; + if ( ich >= cchEncodedStringSize ) + { + DBG_ASSERT( FALSE ); + return ERROR_BUFFER_OVERFLOW; + } + } + + // Pad the last cluster as necessary to indicate the number of data bytes + // it represents. + switch (cbDecodedBufferSize % 3) { + case 0: + break; + case 1: + pszEncodedString[ich - 2] = '='; + __fallthrough; + case 2: + pszEncodedString[ich - 1] = '='; + break; + } + + // Null-terminate the encoded string. + pszEncodedString[ich++] = '\0'; + + DBG_ASSERT(ich == cchEncoded); + + return ERROR_SUCCESS; +} + + +DWORD +Base64Decode( + __in PCSTR pszEncodedString, + __out_opt VOID * pDecodeBuffer, + __in DWORD cbDecodeBufferSize, + __out_opt DWORD * pcbDecoded + ) +/*++ + +Routine Description: + + Decode a base64-encoded string. + +Arguments: + + pszEncodedString (IN) - base64-encoded string to decode. + cbDecodeBufferSize (IN) - size in bytes of the decode buffer. + pbDecodeBuffer (OUT) - holds the decoded data. + pcbDecoded (OUT) - number of data bytes in the decoded data (if success or + STATUS_BUFFER_TOO_SMALL). + +Return Values: + + 0 - success. + E_OUTOFMEMORY + E_INVALIDARG + +--*/ +{ +#define NA (255) +#define DECODE(x) (((ULONG)(x) < sizeof(rgbDecodeTable)) ? rgbDecodeTable[x] : NA) + + static BYTE rgbDecodeTable[128] = { + NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, // 0-15 + NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, // 16-31 + NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 62, NA, NA, NA, 63, // 32-47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, NA, NA, NA, 0, NA, NA, // 48-63 + NA, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 64-79 + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, NA, NA, NA, NA, NA, // 80-95 + NA, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 96-111 + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, NA, NA, NA, NA, NA, // 112-127 + }; + + DWORD cbDecoded; + DWORD cchEncodedSize; + DWORD ich; + DWORD ib; + BYTE b0, b1, b2, b3; + BYTE * pbDecodeBuffer = (BYTE *) pDecodeBuffer; + + cchEncodedSize = (DWORD)strlen(pszEncodedString); + if (NULL != pcbDecoded) { + *pcbDecoded = 0; + } + + if ((0 == cchEncodedSize) || (0 != (cchEncodedSize % 4))) { + // Input string is not sized correctly to be base64. + return ERROR_INVALID_PARAMETER; + } + + // Calculate decoded buffer size. + cbDecoded = (cchEncodedSize + 3) / 4 * 3; + if (pszEncodedString[cchEncodedSize-1] == '=') { + if (pszEncodedString[cchEncodedSize-2] == '=') { + // Only one data byte is encoded in the last cluster. + cbDecoded -= 2; + } + else { + // Only two data bytes are encoded in the last cluster. + cbDecoded -= 1; + } + } + + if (NULL != pcbDecoded) { + *pcbDecoded = cbDecoded; + } + + if (cbDecodeBufferSize == 0 && pDecodeBuffer == NULL) { + return ERROR_SUCCESS; + } + + if (cbDecoded > cbDecodeBufferSize) { + // Supplied buffer is too small. + return ERROR_INSUFFICIENT_BUFFER; + } + + // Decode each four-byte cluster into the corresponding three data bytes. + ich = ib = 0; + while (ich < cchEncodedSize) { + b0 = DECODE(pszEncodedString[ich]); ich++; + b1 = DECODE(pszEncodedString[ich]); ich++; + b2 = DECODE(pszEncodedString[ich]); ich++; + b3 = DECODE(pszEncodedString[ich]); ich++; + + if ((NA == b0) || (NA == b1) || (NA == b2) || (NA == b3)) { + // Contents of input string are not base64. + return ERROR_INVALID_PARAMETER; + } + + pbDecodeBuffer[ib++] = (b0 << 2) | (b1 >> 4); + + if (ib < cbDecoded) { + pbDecodeBuffer[ib++] = (b1 << 4) | (b2 >> 2); + + if (ib < cbDecoded) { + pbDecodeBuffer[ib++] = (b2 << 6) | b3; + } + } + } + + DBG_ASSERT(ib == cbDecoded); + + return ERROR_SUCCESS; +} + diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/base64.h b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/base64.h new file mode 100644 index 0000000000..469b074d73 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/base64.h @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#ifndef _BASE64_H_ +#define _BASE64_H_ + +DWORD +Base64Encode( + __in_bcount( cbDecodedBufferSize ) VOID * pDecodedBuffer, + IN DWORD cbDecodedBufferSize, + __out_ecount_opt( cchEncodedStringSize ) PWSTR pszEncodedString, + IN DWORD cchEncodedStringSize, + __out_opt DWORD * pcchEncoded + ); + +DWORD +Base64Decode( + __in PCWSTR pszEncodedString, + __out_opt VOID * pDecodeBuffer, + __in DWORD cbDecodeBufferSize, + __out_opt DWORD * pcbDecoded + ); + +DWORD +Base64Encode( + __in_bcount( cbDecodedBufferSize ) VOID * pDecodedBuffer, + IN DWORD cbDecodedBufferSize, + __out_ecount_opt( cchEncodedStringSize ) PSTR pszEncodedString, + IN DWORD cchEncodedStringSize, + __out_opt DWORD * pcchEncoded + ); + +DWORD +Base64Decode( + __in PCSTR pszEncodedString, + __out_opt VOID * pDecodeBuffer, + __in DWORD cbDecodeBufferSize, + __out_opt DWORD * pcbDecoded + ); + +#endif // _BASE64_HXX_ + diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/buffer.h b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/buffer.h new file mode 100644 index 0000000000..1d68155387 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/buffer.h @@ -0,0 +1,271 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include + + +// +// BUFFER_T class shouldn't be used directly. Use BUFFER specialization class instead. +// The only BUFFER_T partners are STRU and STRA classes. +// BUFFER_T cannot hold other but primitive types since it doesn't call +// constructor and destructor. +// +// Note: Size is in bytes. +// +template +class BUFFER_T +{ +public: + + BUFFER_T() + : m_cbBuffer( sizeof(m_rgBuffer) ), + m_fHeapAllocated( false ), + m_pBuffer(m_rgBuffer) + /*++ + Description: + + Default constructor where the inline buffer is used. + + Arguments: + + None. + + Returns: + + None. + + --*/ + { + } + + BUFFER_T( + __inout_bcount(cbInit) T* pbInit, + __in DWORD cbInit + ) : m_pBuffer( pbInit ), + m_cbBuffer( cbInit ), + m_fHeapAllocated( false ) + /*++ + Description: + + Instantiate BUFFER, initially using pbInit as buffer + This is useful for stack-buffers and inline-buffer class members + (see STACK_BUFFER and INLINE_BUFFER_INIT below) + + BUFFER does not free pbInit. + + Arguments: + + pbInit - Initial buffer to use. + cbInit - Size of pbInit in bytes (not in elements). + + Returns: + + None. + + --*/ + { + _ASSERTE( NULL != pbInit ); + _ASSERTE( cbInit > 0 ); + } + + ~BUFFER_T() + { + if( IsHeapAllocated() ) + { + _ASSERTE( NULL != m_pBuffer ); + HeapFree( GetProcessHeap(), 0, m_pBuffer ); + m_pBuffer = NULL; + m_cbBuffer = 0; + m_fHeapAllocated = false; + } + } + + T* + QueryPtr( + VOID + ) const + { + // + // Return pointer to data buffer. + // + return m_pBuffer; + } + + DWORD + QuerySize( + VOID + ) const + { + // + // Return number of bytes. + // + return m_cbBuffer; + } + + __success(return == true) + bool + Resize( + const SIZE_T cbNewSize, + const bool fZeroMemoryBeyondOldSize = false + ) + /*++ + Description: + + Resizes the buffer. + + Arguments: + + cbNewSize - Size in bytes to grow to. + fZeroMemoryBeyondOldSize + - Whether to zero the region of memory of the + new buffer beyond the original size. + + Returns: + + TRUE on success, FALSE on failure. + + --*/ + { + PVOID pNewMem; + + if ( cbNewSize <= m_cbBuffer ) + { + return true; + } + + if ( cbNewSize > MAXDWORD ) + { + SetLastError( ERROR_INVALID_PARAMETER ); + return false; + } + + DWORD dwHeapAllocFlags = fZeroMemoryBeyondOldSize ? HEAP_ZERO_MEMORY : 0; + + if( IsHeapAllocated() ) + { + pNewMem = HeapReAlloc( GetProcessHeap(), dwHeapAllocFlags, m_pBuffer, cbNewSize ); + } + else + { + pNewMem = HeapAlloc( GetProcessHeap(), dwHeapAllocFlags, cbNewSize ); + } + + if( pNewMem == NULL ) + { + SetLastError( ERROR_NOT_ENOUGH_MEMORY ); + return false; + } + + if( !IsHeapAllocated() ) + { + // + // First time this block is allocated. Copy over old contents. + // + memcpy_s( pNewMem, static_cast(cbNewSize), m_pBuffer, m_cbBuffer ); + m_fHeapAllocated = true; + } + + m_pBuffer = reinterpret_cast(pNewMem); + m_cbBuffer = static_cast(cbNewSize); + + _ASSERTE( m_pBuffer != NULL ); + + return true; + } + +private: + + bool + IsHeapAllocated( + VOID + ) const + { + return m_fHeapAllocated; + } + + // + // The default inline buffer. + // This member should be at the beginning for alignment purposes. + // + T m_rgBuffer[LENGTH]; + + // + // Is m_pBuffer dynamically allocated? + // + bool m_fHeapAllocated; + + // + // Size of the buffer as requested by client in bytes. + // + DWORD m_cbBuffer; + + // + // Pointer to buffer. + // + __field_bcount_full(m_cbBuffer) + T* m_pBuffer; +}; + +// +// Resizes the buffer by 2 if the ideal size is bigger +// than the buffer length. That give us lg(n) allocations. +// +// Use template inferring like: +// +// BUFFER buff; +// hr = ResizeBufferByTwo(buff, 100); +// +template +HRESULT +ResizeBufferByTwo( + BUFFER_T& Buffer, + SIZE_T cbIdealSize, + bool fZeroMemoryBeyondOldSize = false +) +{ + if (cbIdealSize > Buffer.QuerySize()) + { + if (!Buffer.Resize(max(cbIdealSize, static_cast(Buffer.QuerySize() * 2)), + fZeroMemoryBeyondOldSize)) + { + return E_OUTOFMEMORY; + } + } + return S_OK; +} + + +// +// +// Lots of code uses BUFFER class to store a bunch of different +// structures, so m_rgBuffer needs to be 8 byte aligned when it is used +// as an opaque buffer. +// +#define INLINED_BUFFER_LEN 32 +typedef BUFFER_T BUFFER; + +// +// Assumption of macros below for pointer alignment purposes +// +C_ASSERT( sizeof(VOID*) <= sizeof(ULONGLONG) ); + +// +// Declare a BUFFER that will use stack memory of +// bytes. If the buffer overflows then a heap buffer will be allocated. +// +#define STACK_BUFFER( _name, _size ) \ + ULONGLONG __aqw##_name[ ( ( (_size) + sizeof(ULONGLONG) - 1 ) / sizeof(ULONGLONG) ) ]; \ + BUFFER _name( (BYTE*)__aqw##_name, sizeof(__aqw##_name) ) + +// +// Macros for declaring and initializing a BUFFER that will use inline memory +// of bytes as a member of an object. +// +#define INLINE_BUFFER( _name, _size ) \ + ULONGLONG __aqw##_name[ ( ( (_size) + sizeof(ULONGLONG) - 1 ) / sizeof(ULONGLONG) ) ]; \ + BUFFER _name; + +#define INLINE_BUFFER_INIT( _name ) \ + _name( (BYTE*)__aqw##_name, sizeof( __aqw##_name ) ) diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/datetime.h b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/datetime.h new file mode 100644 index 0000000000..fd09b7a6a0 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/datetime.h @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#ifndef _DATETIME_H_ +#define _DATETIME_H_ + +BOOL +StringTimeToFileTime( + PCSTR pszTime, + ULONGLONG * pulTime +); + +#endif + diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/dbgutil.h b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/dbgutil.h new file mode 100644 index 0000000000..45c26777a9 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/dbgutil.h @@ -0,0 +1,102 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#ifndef _DBGUTIL_H_ +#define _DBGUTIL_H_ + +#include + +// +// TODO +// Using _CrtDbg implementation. If hooking is desired +// wrappers should be provided here so that we can reimplement +// if neecessary. +// +// IF_DEBUG/DEBUG FLAGS +// +// registry configuration +// + +// +// Debug error levels for DEBUG_FLAGS_VAR. +// + +#define DEBUG_FLAG_INFO 0x00000001 +#define DEBUG_FLAG_WARN 0x00000002 +#define DEBUG_FLAG_ERROR 0x00000004 + +// +// Predefined error level values. These are backwards from the +// windows definitions. +// + +#define DEBUG_FLAGS_INFO (DEBUG_FLAG_ERROR | DEBUG_FLAG_WARN | DEBUG_FLAG_INFO) +#define DEBUG_FLAGS_WARN (DEBUG_FLAG_ERROR | DEBUG_FLAG_WARN) +#define DEBUG_FLAGS_ERROR (DEBUG_FLAG_ERROR) +#define DEBUG_FLAGS_ANY (DEBUG_FLAG_INFO | DEBUG_FLAG_WARN | DEBUG_FLAG_ERROR) + +// +// Global variables to control tracing. Generally per module +// + +#ifndef DEBUG_FLAGS_VAR +#define DEBUG_FLAGS_VAR g_dwDebugFlags +#endif + +#ifndef DEBUG_LABEL_VAR +#define DEBUG_LABEL_VAR g_szDebugLabel +#endif + +extern PCSTR DEBUG_LABEL_VAR; +extern DWORD DEBUG_FLAGS_VAR; + +// +// Module should make this declaration globally. +// + +#define DECLARE_DEBUG_PRINT_OBJECT( _pszLabel_ ) \ + PCSTR DEBUG_LABEL_VAR = _pszLabel_; \ + DWORD DEBUG_FLAGS_VAR = DEBUG_FLAGS_ANY; \ + +#define DECLARE_DEBUG_PRINT_OBJECT2( _pszLabel_, _dwLevel_ ) \ + PCSTR DEBUG_LABEL_VAR = _pszLabel_; \ + DWORD DEBUG_FLAGS_VAR = _dwLevel_; \ + +// +// This doesn't do anything now. Should be safe to call in dll main. +// + +#define CREATE_DEBUG_PRINT_OBJECT + +// +// Trace macros +// + +#define DBG_CONTEXT _CRT_WARN, __FILE__, __LINE__, DEBUG_LABEL_VAR + +#ifdef DEBUG +#define DBGINFO(args) \ +{if (DEBUG_FLAGS_VAR & DEBUG_FLAG_INFO) { _CrtDbgReport args; }} +#define DBGWARN(args) \ +{if (DEBUG_FLAGS_VAR & DEBUG_FLAG_WARN) { _CrtDbgReport args; }} +#define DBGERROR(args) \ +{if (DEBUG_FLAGS_VAR & DEBUG_FLAG_ERROR) { _CrtDbgReport args; }} +#else +#define DBGINFO +#define DBGWARN +#define DBGERROR +#endif + +#define DBGPRINTF DBGINFO + +// +// Simple error traces +// + +#define DBGERROR_HR( _hr_ ) \ + DBGERROR((DBG_CONTEXT, "hr=0x%x\n", _hr_)) + +#define DBGERROR_STATUS( _status_ ) \ + DBGERROR((DBG_CONTEXT, "status=%d\n", _status_)) + +#endif diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/hashfn.h b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/hashfn.h new file mode 100644 index 0000000000..a7bfeda2cf --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/hashfn.h @@ -0,0 +1,325 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#ifndef __HASHFN_H__ +#define __HASHFN_H__ + + +// Produce a scrambled, randomish number in the range 0 to RANDOM_PRIME-1. +// Applying this to the results of the other hash functions is likely to +// produce a much better distribution, especially for the identity hash +// functions such as Hash(char c), where records will tend to cluster at +// the low end of the hashtable otherwise. LKRhash applies this internally +// to all hash signatures for exactly this reason. + +inline DWORD +HashScramble(DWORD dwHash) +{ + // Here are 10 primes slightly greater than 10^9 + // 1000000007, 1000000009, 1000000021, 1000000033, 1000000087, + // 1000000093, 1000000097, 1000000103, 1000000123, 1000000181. + + // default value for "scrambling constant" + const DWORD RANDOM_CONSTANT = 314159269UL; + // large prime number, also used for scrambling + const DWORD RANDOM_PRIME = 1000000007UL; + + return (RANDOM_CONSTANT * dwHash) % RANDOM_PRIME ; +} + + +// Faster scrambling function suggested by Eric Jacobsen + +inline DWORD +HashRandomizeBits(DWORD dw) +{ + return (((dw * 1103515245 + 12345) >> 16) + | ((dw * 69069 + 1) & 0xffff0000)); +} + + +// Small prime number used as a multiplier in the supplied hash functions +const DWORD HASH_MULTIPLIER = 101; + +#undef HASH_SHIFT_MULTIPLY + +#ifdef HASH_SHIFT_MULTIPLY +# define HASH_MULTIPLY(dw) (((dw) << 7) - (dw)) +#else +# define HASH_MULTIPLY(dw) ((dw) * HASH_MULTIPLIER) +#endif + +// Fast, simple hash function that tends to give a good distribution. +// Apply HashScramble to the result if you're using this for something +// other than LKRhash. + +inline DWORD +HashString( + const char* psz, + DWORD dwHash = 0) +{ + // force compiler to use unsigned arithmetic + const unsigned char* upsz = (const unsigned char*) psz; + + for ( ; *upsz; ++upsz) + dwHash = HASH_MULTIPLY(dwHash) + *upsz; + + return dwHash; +} + +inline DWORD +HashString( + __in_ecount(cch) const char* psz, + __in DWORD cch, + __in DWORD dwHash +) +{ + // force compiler to use unsigned arithmetic + const unsigned char* upsz = (const unsigned char*) psz; + + for (DWORD Index = 0; + Index < cch; + ++Index, ++upsz) + { + dwHash = HASH_MULTIPLY(dwHash) + *upsz; + } + + return dwHash; +} + + +// Unicode version of above + +inline DWORD +HashString( + const wchar_t* pwsz, + DWORD dwHash = 0) +{ + for ( ; *pwsz; ++pwsz) + dwHash = HASH_MULTIPLY(dwHash) + *pwsz; + + return dwHash; +} + +// Based on length of the string instead of null-terminating character + +inline DWORD +HashString( + __in_ecount(cch) const wchar_t* pwsz, + __in DWORD cch, + __in DWORD dwHash +) +{ + for (DWORD Index = 0; + Index < cch; + ++Index, ++pwsz) + { + dwHash = HASH_MULTIPLY(dwHash) + *pwsz; + } + + return dwHash; +} + + +// Quick-'n'-dirty case-insensitive string hash function. +// Make sure that you follow up with _stricmp or _mbsicmp. You should +// also cache the length of strings and check those first. Caching +// an uppercase version of a string can help too. +// Again, apply HashScramble to the result if using with something other +// than LKRhash. +// Note: this is not really adequate for MBCS strings. + +inline DWORD +HashStringNoCase( + const char* psz, + DWORD dwHash = 0) +{ + const unsigned char* upsz = (const unsigned char*) psz; + + for ( ; *upsz; ++upsz) + dwHash = HASH_MULTIPLY(dwHash) + + (*upsz & 0xDF); // strip off lowercase bit + + return dwHash; +} + +inline DWORD +HashStringNoCase( + __in_ecount(cch) + const char* psz, + SIZE_T cch, + DWORD dwHash) +{ + const unsigned char* upsz = (const unsigned char*) psz; + + for (SIZE_T Index = 0; + Index < cch; + ++Index, ++upsz) + { + dwHash = HASH_MULTIPLY(dwHash) + + (*upsz & 0xDF); // strip off lowercase bit + } + return dwHash; +} + + +// Unicode version of above + +inline DWORD +HashStringNoCase( + const wchar_t* pwsz, + DWORD dwHash = 0) +{ + for ( ; *pwsz; ++pwsz) + dwHash = HASH_MULTIPLY(dwHash) + (*pwsz & 0xFFDF); + + return dwHash; +} + +// Unicode version of above with length + +inline DWORD +HashStringNoCase( + __in_ecount(cch) + const wchar_t* pwsz, + SIZE_T cch, + DWORD dwHash) +{ + for (SIZE_T Index = 0; + Index < cch; + ++Index, ++pwsz) + { + dwHash = HASH_MULTIPLY(dwHash) + (*pwsz & 0xFFDF); + } + return dwHash; +} + + +// HashBlob returns the hash of a blob of arbitrary binary data. +// +// Warning: HashBlob is generally not the right way to hash a class object. +// Consider: +// class CFoo { +// public: +// char m_ch; +// double m_d; +// char* m_psz; +// }; +// +// inline DWORD Hash(const CFoo& rFoo) +// { return HashBlob(&rFoo, sizeof(CFoo)); } +// +// This is the wrong way to hash a CFoo for two reasons: (a) there will be +// a 7-byte gap between m_ch and m_d imposed by the alignment restrictions +// of doubles, which will be filled with random data (usually non-zero for +// stack variables), and (b) it hashes the address (rather than the +// contents) of the string m_psz. Similarly, +// +// bool operator==(const CFoo& rFoo1, const CFoo& rFoo2) +// { return memcmp(&rFoo1, &rFoo2, sizeof(CFoo)) == 0; } +// +// does the wrong thing. Much better to do this: +// +// DWORD Hash(const CFoo& rFoo) +// { +// return HashString(rFoo.m_psz, +// HASH_MULTIPLIER * Hash(rFoo.m_ch) +// + Hash(rFoo.m_d)); +// } +// +// Again, apply HashScramble if using with something other than LKRhash. + +inline DWORD +HashBlob( + const void* pv, + size_t cb, + DWORD dwHash = 0) +{ + const BYTE * pb = static_cast(pv); + + while (cb-- > 0) + dwHash = HASH_MULTIPLY(dwHash) + *pb++; + + return dwHash; +} + + + +// +// Overloaded hash functions for all the major builtin types. +// Again, apply HashScramble to result if using with something other than +// LKRhash. +// + +inline DWORD Hash(const char* psz) +{ return HashString(psz); } + +inline DWORD Hash(const unsigned char* pusz) +{ return HashString(reinterpret_cast(pusz)); } + +inline DWORD Hash(const signed char* pssz) +{ return HashString(reinterpret_cast(pssz)); } + +inline DWORD Hash(const wchar_t* pwsz) +{ return HashString(pwsz); } + +inline DWORD +Hash( + const GUID* pguid, + DWORD dwHash = 0) +{ + + return * reinterpret_cast(const_cast(pguid)) + dwHash; +} + +// Identity hash functions: scalar values map to themselves +inline DWORD Hash(char c) +{ return c; } + +inline DWORD Hash(unsigned char uc) +{ return uc; } + +inline DWORD Hash(signed char sc) +{ return sc; } + +inline DWORD Hash(short sh) +{ return sh; } + +inline DWORD Hash(unsigned short ush) +{ return ush; } + +inline DWORD Hash(int i) +{ return i; } + +inline DWORD Hash(unsigned int u) +{ return u; } + +inline DWORD Hash(long l) +{ return l; } + +inline DWORD Hash(unsigned long ul) +{ return ul; } + +inline DWORD Hash(float f) +{ + // be careful of rounding errors when computing keys + union { + float f; + DWORD dw; + } u; + u.f = f; + return u.dw; +} + +inline DWORD Hash(double dbl) +{ + // be careful of rounding errors when computing keys + union { + double dbl; + DWORD dw[2]; + } u; + u.dbl = dbl; + return u.dw[0] * HASH_MULTIPLIER + u.dw[1]; +} + +#endif // __HASHFN_H__ diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/hashtable.h b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/hashtable.h new file mode 100644 index 0000000000..9319e5643d --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/hashtable.h @@ -0,0 +1,666 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include +#include "rwlock.h" +#include "prime.h" + +template +class HASH_NODE +{ + template + friend class HASH_TABLE; + + HASH_NODE( + _Record * pRecord, + DWORD dwHash + ) : _pNext (NULL), + _pRecord (pRecord), + _dwHash (dwHash) + {} + + ~HASH_NODE() + { + _ASSERTE(_pRecord == NULL); + } + + private: + // Next node in the hash table look-aside + HASH_NODE<_Record> *_pNext; + + // actual record + _Record * _pRecord; + + // hash value + DWORD _dwHash; +}; + +template +class HASH_TABLE +{ +protected: + typedef BOOL + (PFN_DELETE_IF)( + _Record * pRecord, + PVOID pvContext + ); + + typedef VOID + (PFN_APPLY)( + _Record * pRecord, + PVOID pvContext + ); + +public: + HASH_TABLE( + VOID + ) + : _ppBuckets( NULL ), + _nBuckets( 0 ), + _nItems( 0 ) + { + } + + virtual + ~HASH_TABLE(); + + virtual + VOID + ReferenceRecord( + _Record * pRecord + ) = 0; + + virtual + VOID + DereferenceRecord( + _Record * pRecord + ) = 0; + + virtual + _Key + ExtractKey( + _Record * pRecord + ) = 0; + + virtual + DWORD + CalcKeyHash( + _Key key + ) = 0; + + virtual + BOOL + EqualKeys( + _Key key1, + _Key key2 + ) = 0; + + DWORD + Count( + VOID + ) const; + + bool + IsInitialized( + VOID + ) const; + + virtual + VOID + Clear(); + + HRESULT + Initialize( + DWORD nBucketSize + ); + + virtual + VOID + FindKey( + _Key key, + _Record ** ppRecord + ); + + virtual + HRESULT + InsertRecord( + _Record * pRecord + ); + + virtual + VOID + DeleteKey( + _Key key + ); + + virtual + VOID + DeleteIf( + PFN_DELETE_IF pfnDeleteIf, + PVOID pvContext + ); + + VOID + Apply( + PFN_APPLY pfnApply, + PVOID pvContext + ); + +private: + + __success(*ppNode != NULL && return != FALSE) + BOOL + FindNodeInternal( + _Key key, + DWORD dwHash, + __deref_out + HASH_NODE<_Record> ** ppNode, + __deref_opt_out + HASH_NODE<_Record> *** pppPreviousNodeNextPointer = NULL + ); + + VOID + DeleteNode( + HASH_NODE<_Record> * pNode + ) + { + if (pNode->_pRecord != NULL) + { + DereferenceRecord(pNode->_pRecord); + pNode->_pRecord = NULL; + } + + delete pNode; + } + + VOID + RehashTableIfNeeded( + VOID + ); + + HASH_NODE<_Record> ** _ppBuckets; + DWORD _nBuckets; + DWORD _nItems; + // + // Allow to use lock object in const methods. + // + mutable + CWSDRWLock _tableLock; +}; + +template +HRESULT +HASH_TABLE<_Record,_Key>::Initialize( + DWORD nBuckets +) +{ + HRESULT hr = S_OK; + + if ( nBuckets == 0 ) + { + hr = E_INVALIDARG; + goto Failed; + } + + if (nBuckets >= MAXDWORD/sizeof(HASH_NODE<_Record> *)) + { + hr = E_INVALIDARG; + goto Failed; + } + + _ASSERTE(_ppBuckets == NULL ); + if ( _ppBuckets != NULL ) + { + hr = E_INVALIDARG; + goto Failed; + } + + hr = _tableLock.Init(); + if ( FAILED( hr ) ) + { + goto Failed; + } + + _ppBuckets = (HASH_NODE<_Record> **)HeapAlloc( + GetProcessHeap(), + HEAP_ZERO_MEMORY, + nBuckets*sizeof(HASH_NODE<_Record> *)); + if (_ppBuckets == NULL) + { + hr = HRESULT_FROM_WIN32(ERROR_NOT_ENOUGH_MEMORY); + goto Failed; + } + _nBuckets = nBuckets; + + return S_OK; + +Failed: + + if (_ppBuckets) + { + HeapFree(GetProcessHeap(), + 0, + _ppBuckets); + _ppBuckets = NULL; + } + + return hr; +} + + +template +HASH_TABLE<_Record,_Key>::~HASH_TABLE() +{ + if (_ppBuckets == NULL) + { + return; + } + + _ASSERTE(_nItems == 0); + + HeapFree(GetProcessHeap(), + 0, + _ppBuckets); + _ppBuckets = NULL; + _nBuckets = 0; +} + +template< class _Record, class _Key> +DWORD +HASH_TABLE<_Record,_Key>::Count() const +{ + return _nItems; +} + +template< class _Record, class _Key> +bool +HASH_TABLE<_Record,_Key>::IsInitialized( + VOID +) const +{ + return _ppBuckets != NULL; +} + + +template +VOID +HASH_TABLE<_Record,_Key>::Clear() +{ + HASH_NODE<_Record> *pCurrent; + HASH_NODE<_Record> *pNext; + + // This is here in the off cases where someone instantiates a hashtable + // and then does an automatic "clear" before its destruction WITHOUT + // ever initializing it. + if ( ! _tableLock.QueryInited() ) + { + return; + } + + _tableLock.ExclusiveAcquire(); + + for (DWORD i=0; i<_nBuckets; i++) + { + pCurrent = _ppBuckets[i]; + _ppBuckets[i] = NULL; + while (pCurrent != NULL) + { + pNext = pCurrent->_pNext; + DeleteNode(pCurrent); + pCurrent = pNext; + } + } + + _nItems = 0; + _tableLock.ExclusiveRelease(); +} + +template +__success(*ppNode != NULL && return != FALSE) +BOOL +HASH_TABLE<_Record,_Key>::FindNodeInternal( + _Key key, + DWORD dwHash, + __deref_out + HASH_NODE<_Record> ** ppNode, + __deref_opt_out + HASH_NODE<_Record> *** pppPreviousNodeNextPointer +) +/*++ + Return value indicates whether the item is found + key, dwHash - key and hash for the node to find + ppNode - on successful return, the node found, on failed return, the first + node with hash value greater than the node to be found + pppPreviousNodeNextPointer - the pointer to previous node's _pNext + + This routine may be called under either read or write lock +--*/ +{ + HASH_NODE<_Record> **ppPreviousNodeNextPointer; + HASH_NODE<_Record> *pNode; + BOOL fFound = FALSE; + + ppPreviousNodeNextPointer = _ppBuckets + (dwHash % _nBuckets); + pNode = *ppPreviousNodeNextPointer; + while (pNode != NULL) + { + if (pNode->_dwHash == dwHash) + { + if (EqualKeys(key, + ExtractKey(pNode->_pRecord))) + { + fFound = TRUE; + break; + } + } + else if (pNode->_dwHash > dwHash) + { + break; + } + + ppPreviousNodeNextPointer = &(pNode->_pNext); + pNode = *ppPreviousNodeNextPointer; + } + + __analysis_assume( (pNode == NULL && fFound == FALSE) || + (pNode != NULL && fFound == TRUE ) ); + *ppNode = pNode; + if (pppPreviousNodeNextPointer != NULL) + { + *pppPreviousNodeNextPointer = ppPreviousNodeNextPointer; + } + return fFound; +} + +template +VOID +HASH_TABLE<_Record,_Key>::FindKey( + _Key key, + _Record ** ppRecord +) +{ + HASH_NODE<_Record> *pNode; + + *ppRecord = NULL; + + DWORD dwHash = CalcKeyHash(key); + + _tableLock.SharedAcquire(); + + if (FindNodeInternal(key, dwHash, &pNode) && + pNode->_pRecord != NULL) + { + ReferenceRecord(pNode->_pRecord); + *ppRecord = pNode->_pRecord; + } + + _tableLock.SharedRelease(); +} + +template +HRESULT +HASH_TABLE<_Record,_Key>::InsertRecord( + _Record * pRecord +) +/*++ + This method inserts a node for this record and also empty nodes for paths + in the heirarchy leading upto this path + + The insert is done under only a read-lock - this is possible by keeping + the hashes in a bucket in increasing order and using interlocked operations + to actually insert the item in the hash-bucket lookaside list and the parent + children list + + Returns HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS) if the record already exists. + Never leak this error to the end user because "*file* already exists" may be confusing. +--*/ +{ + BOOL fLocked = FALSE; + _Key key = ExtractKey(pRecord); + DWORD dwHash = CalcKeyHash(key); + HRESULT hr = S_OK; + HASH_NODE<_Record> * pNewNode; + HASH_NODE<_Record> * pNextNode; + HASH_NODE<_Record> ** ppPreviousNodeNextPointer; + + // + // Ownership of pRecord is not transferred to pNewNode yet, so remember + // to either set it to null before deleting pNewNode or add an extra + // reference later - this is to make sure we do not do an extra ref/deref + // which users may view as getting flushed out of the hash-table + // + pNewNode = new HASH_NODE<_Record>(pRecord, dwHash); + if (pNewNode == NULL) + { + hr = HRESULT_FROM_WIN32(ERROR_NOT_ENOUGH_MEMORY); + goto Finished; + } + + _tableLock.SharedAcquire(); + fLocked = TRUE; + + do + { + // + // Find the right place to add this node + // + if (FindNodeInternal(key, dwHash, &pNextNode, &ppPreviousNodeNextPointer)) + { + // + // If node already there, return error + // + pNewNode->_pRecord = NULL; + DeleteNode(pNewNode); + + // + // We should never leak this error to the end user + // because "file already exists" may be confusing. + // + hr = HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS); + goto Finished; + } + + // + // If another node got inserted in between, we will have to retry + // + pNewNode->_pNext = pNextNode; + } while (InterlockedCompareExchangePointer((PVOID *)ppPreviousNodeNextPointer, + pNewNode, + pNextNode) != pNextNode); + // pass ownership of pRecord now + if (pRecord != NULL) + { + ReferenceRecord(pRecord); + pRecord = NULL; + } + InterlockedIncrement((LONG *)&_nItems); + +Finished: + + if (fLocked) + { + _tableLock.SharedRelease(); + } + + if (SUCCEEDED(hr)) + { + RehashTableIfNeeded(); + } + + return hr; +} + +template +VOID +HASH_TABLE<_Record,_Key>::DeleteKey( + _Key key +) +{ + HASH_NODE<_Record> *pNode; + HASH_NODE<_Record> **ppPreviousNodeNextPointer; + + DWORD dwHash = CalcKeyHash(key); + + _tableLock.ExclusiveAcquire(); + + if (FindNodeInternal(key, dwHash, &pNode, &ppPreviousNodeNextPointer)) + { + *ppPreviousNodeNextPointer = pNode->_pNext; + DeleteNode(pNode); + _nItems--; + } + + _tableLock.ExclusiveRelease(); +} + +template +VOID +HASH_TABLE<_Record,_Key>::DeleteIf( + PFN_DELETE_IF pfnDeleteIf, + PVOID pvContext +) +{ + HASH_NODE<_Record> *pNode; + HASH_NODE<_Record> **ppPreviousNodeNextPointer; + + _tableLock.ExclusiveAcquire(); + + for (DWORD i=0; i<_nBuckets; i++) + { + ppPreviousNodeNextPointer = _ppBuckets + i; + pNode = *ppPreviousNodeNextPointer; + while (pNode != NULL) + { + // + // Non empty nodes deleted based on DeleteIf, empty nodes deleted + // if they have no children + // + if (pfnDeleteIf(pNode->_pRecord, pvContext)) + { + *ppPreviousNodeNextPointer = pNode->_pNext; + DeleteNode(pNode); + _nItems--; + } + else + { + ppPreviousNodeNextPointer = &pNode->_pNext; + } + + pNode = *ppPreviousNodeNextPointer; + } + } + + _tableLock.ExclusiveRelease(); +} + +template +VOID +HASH_TABLE<_Record,_Key>::Apply( + PFN_APPLY pfnApply, + PVOID pvContext +) +{ + HASH_NODE<_Record> *pNode; + + _tableLock.SharedAcquire(); + + for (DWORD i=0; i<_nBuckets; i++) + { + pNode = _ppBuckets[i]; + while (pNode != NULL) + { + if (pNode->_pRecord != NULL) + { + pfnApply(pNode->_pRecord, pvContext); + } + + pNode = pNode->_pNext; + } + } + + _tableLock.SharedRelease(); +} + +template +VOID +HASH_TABLE<_Record,_Key>::RehashTableIfNeeded( + VOID +) +{ + HASH_NODE<_Record> **ppBuckets; + DWORD nBuckets; + HASH_NODE<_Record> *pNode; + HASH_NODE<_Record> *pNextNode; + HASH_NODE<_Record> **ppNextPointer; + HASH_NODE<_Record> *pNewNextNode; + DWORD nNewBuckets; + + // + // If number of items has become too many, we will double the hash table + // size (we never reduce it however) + // + if (_nItems <= PRIME::GetPrime(2*_nBuckets)) + { + return; + } + + _tableLock.ExclusiveAcquire(); + + nNewBuckets = PRIME::GetPrime(2*_nBuckets); + + if (_nItems <= nNewBuckets) + { + goto Finished; + } + + nBuckets = nNewBuckets; + if (nBuckets >= 0xffffffff/sizeof(HASH_NODE<_Record> *)) + { + goto Finished; + } + ppBuckets = (HASH_NODE<_Record> **)HeapAlloc( + GetProcessHeap(), + HEAP_ZERO_MEMORY, + nBuckets*sizeof(HASH_NODE<_Record> *)); + if (ppBuckets == NULL) + { + goto Finished; + } + + // + // Take out nodes from the old hash table and insert in the new one, make + // sure to keep the hashes in increasing order + // + for (DWORD i=0; i<_nBuckets; i++) + { + pNode = _ppBuckets[i]; + while (pNode != NULL) + { + pNextNode = pNode->_pNext; + + ppNextPointer = ppBuckets + (pNode->_dwHash % nBuckets); + pNewNextNode = *ppNextPointer; + while (pNewNextNode != NULL && + pNewNextNode->_dwHash <= pNode->_dwHash) + { + ppNextPointer = &pNewNextNode->_pNext; + pNewNextNode = pNewNextNode->_pNext; + } + pNode->_pNext = pNewNextNode; + *ppNextPointer = pNode; + + pNode = pNextNode; + } + } + + HeapFree(GetProcessHeap(), 0, _ppBuckets); + _ppBuckets = ppBuckets; + _nBuckets = nBuckets; + ppBuckets = NULL; + +Finished: + + _tableLock.ExclusiveRelease(); +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/listentry.h b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/listentry.h new file mode 100644 index 0000000000..80b70e97a9 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/listentry.h @@ -0,0 +1,163 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#ifndef _LIST_ENTRY_H +#define _LIST_ENTRY_H + +// +// Doubly-linked list manipulation routines. +// + + +#define InitializeListHead32(ListHead) (\ + (ListHead)->Flink = (ListHead)->Blink = PtrToUlong((ListHead))) + + +FORCEINLINE +VOID +InitializeListHead( + IN PLIST_ENTRY ListHead + ) +{ + ListHead->Flink = ListHead->Blink = ListHead; +} + +FORCEINLINE +BOOLEAN +IsListEmpty( + IN const LIST_ENTRY * ListHead + ) +{ + return (BOOLEAN)(ListHead->Flink == ListHead); +} + +FORCEINLINE +BOOLEAN +RemoveEntryList( + IN PLIST_ENTRY Entry + ) +{ + PLIST_ENTRY Blink; + PLIST_ENTRY Flink; + + Flink = Entry->Flink; + Blink = Entry->Blink; + Blink->Flink = Flink; + Flink->Blink = Blink; + return (BOOLEAN)(Flink == Blink); +} + +FORCEINLINE +PLIST_ENTRY +RemoveHeadList( + IN PLIST_ENTRY ListHead + ) +{ + PLIST_ENTRY Flink; + PLIST_ENTRY Entry; + + Entry = ListHead->Flink; + Flink = Entry->Flink; + ListHead->Flink = Flink; + Flink->Blink = ListHead; + return Entry; +} + + + +FORCEINLINE +PLIST_ENTRY +RemoveTailList( + IN PLIST_ENTRY ListHead + ) +{ + PLIST_ENTRY Blink; + PLIST_ENTRY Entry; + + Entry = ListHead->Blink; + Blink = Entry->Blink; + ListHead->Blink = Blink; + Blink->Flink = ListHead; + return Entry; +} + + +FORCEINLINE +VOID +InsertTailList( + IN PLIST_ENTRY ListHead, + IN PLIST_ENTRY Entry + ) +{ + PLIST_ENTRY Blink; + + Blink = ListHead->Blink; + Entry->Flink = ListHead; + Entry->Blink = Blink; + Blink->Flink = Entry; + ListHead->Blink = Entry; +} + + +FORCEINLINE +VOID +InsertHeadList( + IN PLIST_ENTRY ListHead, + IN PLIST_ENTRY Entry + ) +{ + PLIST_ENTRY Flink; + + Flink = ListHead->Flink; + Entry->Flink = Flink; + Entry->Blink = ListHead; + Flink->Blink = Entry; + ListHead->Flink = Entry; +} + +FORCEINLINE +VOID +AppendTailList( + IN PLIST_ENTRY ListHead, + IN PLIST_ENTRY ListToAppend + ) +{ + PLIST_ENTRY ListEnd = ListHead->Blink; + + ListHead->Blink->Flink = ListToAppend; + ListHead->Blink = ListToAppend->Blink; + ListToAppend->Blink->Flink = ListHead; + ListToAppend->Blink = ListEnd; +} + +FORCEINLINE +PSINGLE_LIST_ENTRY +PopEntryList( + PSINGLE_LIST_ENTRY ListHead + ) +{ + PSINGLE_LIST_ENTRY FirstEntry; + FirstEntry = ListHead->Next; + if (FirstEntry != NULL) { + ListHead->Next = FirstEntry->Next; + } + + return FirstEntry; +} + + +FORCEINLINE +VOID +PushEntryList( + PSINGLE_LIST_ENTRY ListHead, + PSINGLE_LIST_ENTRY Entry + ) +{ + Entry->Next = ListHead->Next; + ListHead->Next = Entry; +} + + +#endif diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/macros.h b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/macros.h new file mode 100644 index 0000000000..960f663a98 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/macros.h @@ -0,0 +1,63 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#ifndef _MACROS_H +#define _MACROS_H + +// +// The DIFF macro should be used around an expression involving pointer +// subtraction. The expression passed to DIFF is cast to a size_t type, +// allowing the result to be easily assigned to any 32-bit variable or +// passed to a function expecting a 32-bit argument. +// + +#define DIFF(x) ((size_t)(x)) + +// Change a hexadecimal digit to its numerical equivalent +#define TOHEX( ch ) \ + ((ch) > L'9' ? \ + (ch) >= L'a' ? \ + (ch) - L'a' + 10 : \ + (ch) - L'A' + 10 \ + : (ch) - L'0') + + +// Change a number to its Hexadecimal equivalent + +#define TODIGIT( nDigit ) \ + (CHAR)((nDigit) > 9 ? \ + (nDigit) - 10 + 'A' \ + : (nDigit) + '0') + + +inline int +SAFEIsSpace(UCHAR c) +{ + return isspace( c ); +} + +inline int +SAFEIsAlNum(UCHAR c) +{ + return isalnum( c ); +} + +inline int +SAFEIsAlpha(UCHAR c) +{ + return isalpha( c ); +} + +inline int +SAFEIsXDigit(UCHAR c) +{ + return isxdigit( c ); +} + +inline int +SAFEIsDigit(UCHAR c) +{ + return isdigit( c ); +} + +#endif // _MACROS_H diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/multisz.cpp b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/multisz.cpp new file mode 100644 index 0000000000..775ec4cd0c --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/multisz.cpp @@ -0,0 +1,474 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + + +#pragma warning (disable : 4267) + +#include "precomp.h" +#include "multisz.h" +#include + +// +// Private Definitions +// + +#define MAXULONG 4294967295 +#define ISWHITE( ch ) ((ch) == L' ' || (ch) == L'\t' || (ch) == L'\r') + +// +// When appending data, this is the extra amount we request to avoid +// reallocations +// +#define STR_SLOP 128 + + +DWORD +MULTISZ::CalcLength( const WCHAR * str, + LPDWORD pcStrings ) +{ + DWORD count = 0; + DWORD total = 1; + DWORD len; + + while( *str ) { + len = ::wcslen( str ) + 1; + total += len; + str += len; + count++; + } + + if( pcStrings != NULL ) { + *pcStrings = count; + } + + return total; + +} // MULTISZ::CalcLength + + +BOOL +MULTISZ::FindString( const WCHAR * str ) +{ + + WCHAR * multisz; + + // + // Sanity check. + // + + DBG_ASSERT( QueryStr() != NULL ); + DBG_ASSERT( str != NULL ); + DBG_ASSERT( *str != '\0' ); + + // + // Scan it. + // + + multisz = QueryStr(); + + while( *multisz != '\0' ) { + + if( !::wcscmp( multisz, str ) ) { + + return TRUE; + + } + + multisz += ::wcslen( multisz ) + 1; + + } + + return FALSE; + +} // MULTISZ::FindString + + +BOOL +MULTISZ::FindStringNoCase( const WCHAR * str ) +{ + + WCHAR * multisz; + + // + // Sanity check. + // + + DBG_ASSERT( QueryStr() != NULL ); + DBG_ASSERT( str != NULL ); + DBG_ASSERT( *str != '\0' ); + + // + // Scan it. + // + + multisz = QueryStr(); + + while( *multisz != '\0' ) { + + if( !_wcsicmp( multisz, str ) ) { + + return TRUE; + + } + + multisz += wcslen( multisz ) + 1; + + } + + return FALSE; + +} // MULTISZ::FindStringNoCase + + +VOID +MULTISZ::AuxInit( const WCHAR * pInit ) +{ + BOOL fRet; + + if ( pInit ) + { + DWORD cStrings; + int cbCopy = CalcLength( pInit, &cStrings ) * sizeof(WCHAR); + fRet = Resize( cbCopy ); + + if ( fRet ) { + CopyMemory( QueryPtr(), pInit, cbCopy ); + m_cchLen = (cbCopy)/sizeof(WCHAR); + m_cStrings = cStrings; + } else { +// BUFFER::SetValid( FALSE); + } + + } else { + + Reset(); + + } + +} // MULTISZ::AuxInit() + + +/******************************************************************* + + NAME: MULTISZ::AuxAppend + + SYNOPSIS: Appends the string onto the multisz. + + ENTRY: Object to append +********************************************************************/ + +BOOL MULTISZ::AuxAppend( const WCHAR * pStr, UINT cbStr, BOOL fAddSlop ) +{ + DBG_ASSERT( pStr != NULL ); + + UINT cbThis = QueryCB(); + + DBG_ASSERT( cbThis >= 2 ); + + if( cbThis == 4 ) { + + // + // It's empty, so start at the beginning. + // + + cbThis = 0; + + } else { + + // + // It's not empty, so back up over the final terminating NULL. + // + + cbThis -= sizeof(WCHAR); + + } + + // + // Only resize when we have to. When we do resize, we tack on + // some extra space to avoid extra reallocations. + // + // Note: QuerySize returns the requested size of the string buffer, + // *not* the strlen of the buffer + // + + //AcIncrement( CacMultiszAppend); + + // + // Check for the arithmetic overflow + // + // ( 2 * sizeof( WCHAR ) ) is for the double terminator + // + ULONGLONG cb64Required = (ULONGLONG)cbThis + cbStr + 2 * sizeof(WCHAR); + if ( cb64Required > MAXULONG ) + { + SetLastError( ERROR_ARITHMETIC_OVERFLOW ); + return FALSE; + } + if ( QuerySize() < (DWORD) cb64Required ) + { + ULONGLONG cb64AllocSize = cb64Required + (fAddSlop ? STR_SLOP : 0 ); + // + // Check for the arithmetic overflow + // + if ( cb64AllocSize > MAXULONG ) + { + SetLastError( ERROR_ARITHMETIC_OVERFLOW ); + return FALSE; + } + if ( !Resize( (DWORD) cb64AllocSize ) ) + return FALSE; + } + + // copy the exact string and tack on the double terminator + memcpy( (BYTE *) QueryPtr() + cbThis, + pStr, + cbStr); + *(WCHAR *)((BYTE *)QueryPtr() + cbThis + cbStr) = L'\0'; + *(WCHAR *)((BYTE *)QueryPtr() + cbThis + cbStr + sizeof(WCHAR) ) = L'\0'; + + m_cchLen = CalcLength( (const WCHAR *)QueryPtr(), &m_cStrings ); + return TRUE; + +} // MULTISZ::AuxAppend() + + +#if 0 + +BOOL +MULTISZ::CopyToBuffer( WCHAR * lpszBuffer, LPDWORD lpcch) const +/*++ + Description: + Copies the string into the WCHAR buffer passed in if the buffer + is sufficient to hold the translated string. + If the buffer is small, the function returns small and sets *lpcch + to contain the required number of characters. + + Arguments: + lpszBuffer pointer to WCHAR buffer which on return contains + the UNICODE version of string on success. + lpcch pointer to DWORD containing the length of the buffer. + If *lpcch == 0 then the function returns TRUE with + the count of characters required stored in *lpcch. + Also in this case lpszBuffer is not affected. + Returns: + TRUE on success. + FALSE on failure. Use GetLastError() for further details. +--*/ +{ + BOOL fReturn = TRUE; + + if ( lpcch == NULL) { + SetLastError( ERROR_INVALID_PARAMETER); + return ( FALSE); + } + + if ( *lpcch == 0) { + + // + // Inquiring the size of buffer alone + // + *lpcch = QueryCCH() + 1; // add one character for terminating null + } else { + + // + // Copy after conversion from ANSI to Unicode + // + int iRet; + iRet = MultiByteToWideChar( CP_ACP, + MB_PRECOMPOSED | MB_ERR_INVALID_CHARS, + QueryStrA(), QueryCCH() + 1, + lpszBuffer, (int )*lpcch); + + if ( iRet == 0 || iRet != (int ) *lpcch) { + + // + // Error in conversion. + // + fReturn = FALSE; + } + } + + return ( fReturn); +} // MULTISZ::CopyToBuffer() +#endif + +BOOL +MULTISZ::CopyToBuffer( __out_ecount_opt(*lpcch) WCHAR * lpszBuffer, LPDWORD lpcch) const +/*++ + Description: + Copies the string into the WCHAR buffer passed in if the buffer + is sufficient to hold the translated string. + If the buffer is small, the function returns small and sets *lpcch + to contain the required number of characters. + + Arguments: + lpszBuffer pointer to WCHAR buffer which on return contains + the string on success. + lpcch pointer to DWORD containing the length of the buffer. + If *lpcch == 0 then the function returns TRUE with + the count of characters required stored in lpcch. + Also in this case lpszBuffer is not affected. + Returns: + TRUE on success. + FALSE on failure. Use GetLastError() for further details. +--*/ +{ + BOOL fReturn = TRUE; + + if ( lpcch == NULL) { + SetLastError( ERROR_INVALID_PARAMETER); + return ( FALSE); + } + + register DWORD cch = QueryCCH(); + + if ( *lpcch >= cch) { + + DBG_ASSERT( lpszBuffer); + memcpy( lpszBuffer, QueryStr(), cch * sizeof(WCHAR)); + } else { + DBG_ASSERT( *lpcch < cch); + SetLastError( ERROR_INSUFFICIENT_BUFFER); + fReturn = FALSE; + } + + *lpcch = cch; + + return ( fReturn); +} // MULTISZ::CopyToBuffer() + +BOOL +MULTISZ::Equals( + MULTISZ* pmszRhs +) +// +// Compares this to pmszRhs, returns TRUE if equal +// +{ + DBG_ASSERT( NULL != pmszRhs ); + + PCWSTR pszLhs = First( ); + PCWSTR pszRhs = pmszRhs->First( ); + + if( m_cStrings != pmszRhs->m_cStrings ) + { + return FALSE; + } + + while( NULL != pszLhs ) + { + DBG_ASSERT( NULL != pszRhs ); + + if( 0 != wcscmp( pszLhs, pszRhs ) ) + { + return FALSE; + } + + pszLhs = Next( pszLhs ); + pszRhs = pmszRhs->Next( pszRhs ); + } + + return TRUE; +} + +HRESULT +SplitCommaDelimitedString( + PCWSTR pszList, + BOOL fTrimEntries, + BOOL fRemoveEmptyEntries, + MULTISZ * pmszList +) +/*++ + +Routine Description: + + Split comma delimited string into a multisz. Additional leading empty + entries after the first are discarded. + +Arguments: + + pszList - List to split up + fTrimEntries - Whether each entry should be trimmed before added to multisz + fRemoveEmptyEntries - Whether empty entires should be discarded + pmszList - Filled with MULTISZ list + +Return Value: + + HRESULT + +--*/ +{ + HRESULT hr = S_OK; + + if ( pszList == NULL || + pmszList == NULL ) + { + DBG_ASSERT( FALSE ); + hr = HRESULT_FROM_WIN32( ERROR_INVALID_PARAMETER ); + goto Finished; + } + + pmszList->Reset(); + + /* + pszCurrent: start of the current entry which may be the comma that + precedes the next entry if the entry is empty + + pszNext: the comma that precedes the next entry. If + pszCurrent == pszNext, then the entry is empty + + pszEnd: just past the end of the current entry + */ + + for ( PCWSTR pszCurrent = pszList, + pszNext = wcschr( pszCurrent, L',' ) + ; + ; + pszCurrent = pszNext + 1, + pszNext = wcschr( pszCurrent, L',' ) ) + { + PCWSTR pszEnd = NULL; + + if ( pszNext != NULL ) + { + pszEnd = pszNext; + } + else + { + pszEnd = pszCurrent + wcslen( pszCurrent ); + } + + if ( fTrimEntries ) + { + while ( pszCurrent < pszEnd && ISWHITE( pszCurrent[ 0 ] ) ) + { + pszCurrent++; + } + + while ( pszEnd > pszCurrent && ISWHITE( pszEnd[ -1 ] ) ) + { + pszEnd--; + } + } + + if ( pszCurrent != pszEnd || !fRemoveEmptyEntries ) + { + if ( !pmszList->Append( pszCurrent, (DWORD) ( pszEnd - pszCurrent ) ) ) + { + hr = HRESULT_FROM_WIN32( GetLastError() ); + goto Finished; + } + } + + if ( pszNext == NULL ) + { + break; + } + } + +Finished: + + return hr; +} + +#pragma warning(default:4267) \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/multisz.h b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/multisz.h new file mode 100644 index 0000000000..f65c151d4f --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/multisz.h @@ -0,0 +1,225 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#ifndef _MULTISZ_H_ +#define _MULTISZ_H_ + +#include "stringu.h" +#include "ntassert.h" + +/*++ + class MULTISZ: + + Intention: + A light-weight multi-string class supporting encapsulated string class. + + This object is derived from BUFFER class. + It maintains following state: + + m_fValid - whether this object is valid - + used only by MULTISZ() init functions + * NYI: I need to kill this someday * + m_cchLen - string length cached when we update the string. + m_cStrings - number of strings. + + Member Functions: + There are two categories of functions: + 1) Safe Functions - which do integrity checking of state + 2) UnSafe Functions - which do not do integrity checking, but + enable writing to the data stream freely. + (someday this will be enabled as Safe versions without + problem for users) + +--*/ +class MULTISZ : public BUFFER +{ +public: + + MULTISZ() + : BUFFER (), + m_cchLen ( 0), + m_cStrings(0) + { Reset(); } + + // creates a stack version of the MULTISZ object - uses passed in stack buffer + // MULTISZ does not free this pbInit on its own. + MULTISZ( __in_bcount(cbInit) WCHAR * pbInit, DWORD cbInit) + : BUFFER( (BYTE *) pbInit, cbInit), + m_cchLen (0), + m_cStrings(0) + {} + + MULTISZ( const WCHAR * pchInit ) + : BUFFER (), + m_cchLen ( 0), + m_cStrings(0) + { AuxInit(pchInit); } + + MULTISZ( const MULTISZ & str ) + : BUFFER (), + m_cchLen ( 0), + m_cStrings(0) + { AuxInit( str.QueryStr()); } + +// BOOL IsValid(VOID) const { return ( BUFFER::IsValid()) ; } + // + // Checks and returns TRUE if this string has no valid data else FALSE + // + BOOL IsEmpty( VOID) const { return ( *QueryStr() == L'\0'); } + + BOOL Append( const WCHAR * pchInit ) { + return ((pchInit != NULL) ? (AuxAppend( pchInit, + (DWORD) (::wcslen(pchInit)) * sizeof(WCHAR) + )) : + TRUE); + } + + + BOOL Append( const WCHAR * pchInit, DWORD cchLen ) { + return ((pchInit != NULL) ? (AuxAppend( pchInit, + cchLen * sizeof(WCHAR))) : + TRUE); + } + + BOOL Append( STRU & str ) + { return AuxAppend( str.QueryStr(), + (str.QueryCCH()) * sizeof(WCHAR)); } + + // Resets the internal string to be NULL string. Buffer remains cached. + VOID Reset( VOID) + { DBG_ASSERT( QueryPtr() != NULL); + QueryStr()[0] = L'\0'; + QueryStr()[1] = L'\0'; + m_cchLen = 2; + m_cStrings = 0; + } + + BOOL Copy( const WCHAR * pchInit, IN DWORD cbLen ) { + if ( QueryPtr() ) { Reset(); } + return ( (pchInit != NULL) ? + AuxAppend( pchInit, cbLen, FALSE ): + TRUE); + } + + BOOL Copy( const MULTISZ & str ) + { return ( Copy(str.QueryStr(), str.QueryCB())); } + + // + // Returns the number of bytes in the string including the terminating + // NULLs + // + UINT QueryCB( VOID ) const + { return ( m_cchLen * sizeof(WCHAR)); } + + // + // Returns # of characters in the string including the terminating NULLs + // + UINT QueryCCH( VOID ) const { return (m_cchLen); } + + // + // Returns # of strings in the multisz. + // + + DWORD QueryStringCount( VOID ) const { return m_cStrings; } + + // + // Makes a copy of the stored string in given buffer + // + BOOL CopyToBuffer( __out_ecount_opt(*lpcch) WCHAR * lpszBuffer, LPDWORD lpcch) const; + + // + // Return the string buffer + // + WCHAR * QueryStrA( VOID ) const { return ( QueryStr()); } + WCHAR * QueryStr( VOID ) const { return ((WCHAR *) QueryPtr()); } + + // + // Makes a clone of the current string in the string pointer passed in. + // + BOOL + Clone( OUT MULTISZ * pstrClone) const + { + return ((pstrClone == NULL) ? + (SetLastError(ERROR_INVALID_PARAMETER), FALSE) : + (pstrClone->Copy( *this)) + ); + } // MULTISZ::Clone() + + // + // Recalculates the length of *this because we've modified the buffers + // directly + // + + VOID RecalcLen( VOID ) + { m_cchLen = MULTISZ::CalcLength( QueryStr(), &m_cStrings ); } + + // + // Calculate total character length of a MULTI_SZ, including the + // terminating NULLs. + // + + static DWORD CalcLength( const WCHAR * str, + LPDWORD pcStrings = NULL ); + + // + // Determine if the MULTISZ contains a specific string. + // + + BOOL FindString( const WCHAR * str ); + + BOOL FindString( STRU & str ) + { return FindString( str.QueryStr() ); } + + // + // Determine if the MULTISZ contains a specific string - case-insensitive + // + + BOOL FindStringNoCase( const WCHAR * str ); + + BOOL FindStringNoCase( STRU & str ) + { return FindStringNoCase( str.QueryStr() ); } + + // + // Used for scanning a multisz. + // + + const WCHAR * First( VOID ) const + { return *QueryStr() == L'\0' ? NULL : QueryStr(); } + + const WCHAR * Next( const WCHAR * Current ) const + { Current += ::wcslen( Current ) + 1; + return *Current == L'\0' ? NULL : Current; } + + BOOL + Equals( + MULTISZ* pmszRhs + ); + +private: + + DWORD m_cchLen; + DWORD m_cStrings; + VOID AuxInit( const WCHAR * pInit ); + BOOL AuxAppend( const WCHAR * pInit, + UINT cbStr, BOOL fAddSlop = TRUE ); + +}; + +// +// Quick macro for declaring a MULTISZ that will use stack memory of +// bytes. If the buffer overflows then a heap buffer will be allocated +// + +#define STACK_MULTISZ( name, size ) WCHAR __ach##name[size]; \ + MULTISZ name( __ach##name, sizeof( __ach##name )) + +HRESULT +SplitCommaDelimitedString( + PCWSTR pszList, + BOOL fTrimEntries, + BOOL fRemoveEmptyEntries, + MULTISZ * pmszList +); + +#endif // !_MULTISZ_HXX_ + diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/multisza.cpp b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/multisza.cpp new file mode 100644 index 0000000000..54717edf05 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/multisza.cpp @@ -0,0 +1,408 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma warning (disable : 4267) +#include "precomp.h" +#include "multisza.h" +#include + +// +// Private Definitions +// + +#define MAXULONG 4294967295 +#define ISWHITE( ch ) ((ch) == L' ' || (ch) == L'\t' || (ch) == L'\r') + +// +// When appending data, this is the extra amount we request to avoid +// reallocations +// +#define STR_SLOP 128 + + +DWORD +MULTISZA::CalcLength( const CHAR * str, + LPDWORD pcStrings ) +{ + DWORD count = 0; + DWORD total = 1; + DWORD len; + + while( *str ) { + len = ::strlen( str ) + 1; + total += len; + str += len; + count++; + } + + if( pcStrings != NULL ) { + *pcStrings = count; + } + + return total; + +} // MULTISZA::CalcLength + + +BOOL +MULTISZA::FindString( const CHAR * str ) +{ + + CHAR * multisz; + + // + // Sanity check. + // + + DBG_ASSERT( QueryStr() != NULL ); + DBG_ASSERT( str != NULL ); + DBG_ASSERT( *str != '\0' ); + + // + // Scan it. + // + + multisz = QueryStr(); + + while( *multisz != '\0' ) { + + if( !::strcmp( multisz, str ) ) { + + return TRUE; + + } + + multisz += ::strlen( multisz ) + 1; + + } + + return FALSE; + +} // MULTISZA::FindString + + +BOOL +MULTISZA::FindStringNoCase( const CHAR * str ) +{ + + CHAR * multisz; + + // + // Sanity check. + // + + DBG_ASSERT( QueryStr() != NULL ); + DBG_ASSERT( str != NULL ); + DBG_ASSERT( *str != '\0' ); + + // + // Scan it. + // + + multisz = QueryStr(); + + while( *multisz != '\0' ) { + + if( !_stricmp( multisz, str ) ) { + + return TRUE; + + } + + multisz += strlen( multisz ) + 1; + + } + + return FALSE; + +} // MULTISZA::FindStringNoCase + + +VOID +MULTISZA::AuxInit( const CHAR * pInit ) +{ + BOOL fRet; + + if ( pInit ) + { + DWORD cStrings; + int cbCopy = CalcLength( pInit, &cStrings ) * sizeof(CHAR); + fRet = Resize( cbCopy ); + + if ( fRet ) { + CopyMemory( QueryPtr(), pInit, cbCopy ); + m_cchLen = (cbCopy)/sizeof(CHAR); + m_cStrings = cStrings; + } else { +// BUFFER::SetValid( FALSE); + } + + } else { + + Reset(); + + } + +} // MULTISZA::AuxInit() + + +/******************************************************************* + + NAME: MULTISZA::AuxAppend + + SYNOPSIS: Appends the string onto the MULTISZA. + + ENTRY: Object to append +********************************************************************/ + +BOOL MULTISZA::AuxAppend( const CHAR * pStr, UINT cbStr, BOOL fAddSlop ) +{ + DBG_ASSERT( pStr != NULL ); + + UINT cbThis = QueryCB(); + + if( cbThis == 2 ) { + + // + // It's empty, so start at the beginning. + // + + cbThis = 0; + + } else { + + // + // It's not empty, so back up over the final terminating NULL. + // + + cbThis -= sizeof(CHAR); + + } + + // + // Only resize when we have to. When we do resize, we tack on + // some extra space to avoid extra reallocations. + // + // Note: QuerySize returns the requested size of the string buffer, + // *not* the strlen of the buffer + // + + //AcIncrement( CacMultiszAppend); + + // + // Check for the arithmetic overflow + // + // ( 2 * sizeof( CHAR ) ) is for the double terminator + // + ULONGLONG cb64Required = (ULONGLONG)cbThis + cbStr + 2 * sizeof(CHAR); + if ( cb64Required > MAXULONG ) + { + SetLastError( ERROR_ARITHMETIC_OVERFLOW ); + return FALSE; + } + if ( QuerySize() < (DWORD) cb64Required ) + { + ULONGLONG cb64AllocSize = cb64Required + (fAddSlop ? STR_SLOP : 0 ); + // + // Check for the arithmetic overflow + // + if ( cb64AllocSize > MAXULONG ) + { + SetLastError( ERROR_ARITHMETIC_OVERFLOW ); + return FALSE; + } + if ( !Resize( (DWORD) cb64AllocSize ) ) + return FALSE; + } + + // copy the exact string and tack on the double terminator + memcpy( (BYTE *) QueryPtr() + cbThis, + pStr, + cbStr); + *(CHAR *)((BYTE *)QueryPtr() + cbThis + cbStr) = L'\0'; + *(CHAR *)((BYTE *)QueryPtr() + cbThis + cbStr + sizeof(CHAR) ) = L'\0'; + + m_cchLen = CalcLength( (const CHAR *)QueryPtr(), &m_cStrings ); + return TRUE; + +} // MULTISZA::AuxAppend() + +BOOL +MULTISZA::CopyToBuffer( __out_ecount_opt(*lpcch) CHAR * lpszBuffer, LPDWORD lpcch) const +/*++ + Description: + Copies the string into the CHAR buffer passed in if the buffer + is sufficient to hold the translated string. + If the buffer is small, the function returns small and sets *lpcch + to contain the required number of characters. + + Arguments: + lpszBuffer pointer to CHAR buffer which on return contains + the string on success. + lpcch pointer to DWORD containing the length of the buffer. + If *lpcch == 0 then the function returns TRUE with + the count of characters required stored in lpcch. + Also in this case lpszBuffer is not affected. + Returns: + TRUE on success. + FALSE on failure. Use GetLastError() for further details. +--*/ +{ + BOOL fReturn = TRUE; + + if ( lpcch == NULL) { + SetLastError( ERROR_INVALID_PARAMETER); + return ( FALSE); + } + + register DWORD cch = QueryCCH(); + + if ( *lpcch >= cch) { + + DBG_ASSERT( lpszBuffer); + memcpy( lpszBuffer, QueryStr(), cch * sizeof(CHAR)); + } else { + DBG_ASSERT( *lpcch < cch); + SetLastError( ERROR_INSUFFICIENT_BUFFER); + fReturn = FALSE; + } + + *lpcch = cch; + + return ( fReturn); +} // MULTISZA::CopyToBuffer() + +BOOL +MULTISZA::Equals( + MULTISZA* pmszRhs +) +// +// Compares this to pmszRhs, returns TRUE if equal +// +{ + DBG_ASSERT( NULL != pmszRhs ); + + PCSTR pszLhs = First( ); + PCSTR pszRhs = pmszRhs->First( ); + + if( m_cStrings != pmszRhs->m_cStrings ) + { + return FALSE; + } + + while( NULL != pszLhs ) + { + DBG_ASSERT( NULL != pszRhs ); + + if( 0 != strcmp( pszLhs, pszRhs ) ) + { + return FALSE; + } + + pszLhs = Next( pszLhs ); + pszRhs = pmszRhs->Next( pszRhs ); + } + + return TRUE; +} + +HRESULT +SplitCommaDelimitedString( + PCSTR pszList, + BOOL fTrimEntries, + BOOL fRemoveEmptyEntries, + MULTISZA * pmszList +) +/*++ + +Routine Description: + + Split comma delimited string into a MULTISZA. Additional leading empty + entries after the first are discarded. + +Arguments: + + pszList - List to split up + fTrimEntries - Whether each entry should be trimmed before added to MULTISZA + fRemoveEmptyEntries - Whether empty entires should be discarded + pmszList - Filled with MULTISZA list + +Return Value: + + HRESULT + +--*/ +{ + HRESULT hr = S_OK; + + if ( pszList == NULL || + pmszList == NULL ) + { + DBG_ASSERT( FALSE ); + hr = HRESULT_FROM_WIN32( ERROR_INVALID_PARAMETER ); + goto Finished; + } + + pmszList->Reset(); + + /* + pszCurrent: start of the current entry which may be the comma that + precedes the next entry if the entry is empty + + pszNext: the comma that precedes the next entry. If + pszCurrent == pszNext, then the entry is empty + + pszEnd: just past the end of the current entry + */ + + for ( PCSTR pszCurrent = pszList, + pszNext = strchr( pszCurrent, L',' ) + ; + ; + pszCurrent = pszNext + 1, + pszNext = strchr( pszCurrent, L',' ) ) + { + PCSTR pszEnd = NULL; + + if ( pszNext != NULL ) + { + pszEnd = pszNext; + } + else + { + pszEnd = pszCurrent + strlen( pszCurrent ); + } + + if ( fTrimEntries ) + { + while ( pszCurrent < pszEnd && ISWHITE( pszCurrent[ 0 ] ) ) + { + pszCurrent++; + } + + while ( pszEnd > pszCurrent && ISWHITE( pszEnd[ -1 ] ) ) + { + pszEnd--; + } + } + + if ( pszCurrent != pszEnd || !fRemoveEmptyEntries ) + { + if ( !pmszList->Append( pszCurrent, (DWORD) ( pszEnd - pszCurrent ) ) ) + { + hr = HRESULT_FROM_WIN32( GetLastError() ); + goto Finished; + } + } + + if ( pszNext == NULL ) + { + break; + } + } + +Finished: + + return hr; +} +#pragma warning(default:4267) \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/multisza.h b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/multisza.h new file mode 100644 index 0000000000..d575ec9423 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/multisza.h @@ -0,0 +1,226 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#ifndef _MULTISZA_H_ +#define _MULTISZA_H_ + +#include +#include "stringa.h" + + +/*++ + class MULTISZ: + + Intention: + A light-weight multi-string class supporting encapsulated string class. + + This object is derived from BUFFER class. + It maintains following state: + + m_fValid - whether this object is valid - + used only by MULTISZ() init functions + * NYI: I need to kill this someday * + m_cchLen - string length cached when we update the string. + m_cStrings - number of strings. + + Member Functions: + There are two categories of functions: + 1) Safe Functions - which do integrity checking of state + 2) UnSafe Functions - which do not do integrity checking, but + enable writing to the data stream freely. + (someday this will be enabled as Safe versions without + problem for users) + +--*/ +class MULTISZA : public BUFFER +{ +public: + + MULTISZA() + : BUFFER (), + m_cchLen ( 0), + m_cStrings(0) + { Reset(); } + + // creates a stack version of the MULTISZA object - uses passed in stack buffer + // MULTISZA does not free this pbInit on its own. + MULTISZA( __in_bcount(cbInit) CHAR * pbInit, DWORD cbInit) + : BUFFER( (BYTE *) pbInit, cbInit), + m_cchLen (0), + m_cStrings(0) + {} + + MULTISZA( const CHAR * pchInit ) + : BUFFER (), + m_cchLen ( 0), + m_cStrings(0) + { AuxInit(pchInit); } + + MULTISZA( const MULTISZA & str ) + : BUFFER (), + m_cchLen ( 0), + m_cStrings(0) + { AuxInit( str.QueryStr()); } + +// BOOL IsValid(VOID) const { return ( BUFFER::IsValid()) ; } + // + // Checks and returns TRUE if this string has no valid data else FALSE + // + BOOL IsEmpty( VOID) const { return ( *QueryStr() == L'\0'); } + + BOOL Append( const CHAR * pchInit ) { + return ((pchInit != NULL) ? (AuxAppend( pchInit, + (DWORD) (::strlen(pchInit)) * sizeof(CHAR) + )) : + TRUE); + } + + + BOOL Append( const CHAR * pchInit, DWORD cchLen ) { + return ((pchInit != NULL) ? (AuxAppend( pchInit, + cchLen * sizeof(CHAR))) : + TRUE); + } + + BOOL Append( STRA & str ) + { return AuxAppend( str.QueryStr(), + (str.QueryCCH()) * sizeof(CHAR)); } + + // Resets the internal string to be NULL string. Buffer remains cached. + VOID Reset( VOID) + { DBG_ASSERT( QueryPtr() != NULL); + QueryStr()[0] = L'\0'; + QueryStr()[1] = L'\0'; + m_cchLen = 2; + m_cStrings = 0; + } + + BOOL Copy( const CHAR * pchInit, IN DWORD cbLen ) { + if ( QueryPtr() ) { Reset(); } + return ( (pchInit != NULL) ? + AuxAppend( pchInit, cbLen, FALSE ): + TRUE); + } + + BOOL Copy( const MULTISZA & str ) + { return ( Copy(str.QueryStr(), str.QueryCB())); } + + // + // Returns the number of bytes in the string including the terminating + // NULLs + // + UINT QueryCB( VOID ) const + { return ( m_cchLen * sizeof(CHAR)); } + + // + // Returns # of characters in the string including the terminating NULLs + // + UINT QueryCCH( VOID ) const { return (m_cchLen); } + + // + // Returns # of strings in the MULTISZA. + // + + DWORD QueryStringCount( VOID ) const { return m_cStrings; } + + // + // Makes a copy of the stored string in given buffer + // + BOOL CopyToBuffer( __out_ecount_opt(*lpcch) CHAR * lpszBuffer, LPDWORD lpcch) const; + + // + // Return the string buffer + // + CHAR * QueryStrA( VOID ) const { return ( QueryStr()); } + CHAR * QueryStr( VOID ) const { return ((CHAR *) QueryPtr()); } + + // + // Makes a clone of the current string in the string pointer passed in. + // + BOOL + Clone( OUT MULTISZA * pstrClone) const + { + return ((pstrClone == NULL) ? + (SetLastError(ERROR_INVALID_PARAMETER), FALSE) : + (pstrClone->Copy( *this)) + ); + } // MULTISZA::Clone() + + // + // Recalculates the length of *this because we've modified the buffers + // directly + // + + VOID RecalcLen( VOID ) + { m_cchLen = MULTISZA::CalcLength( QueryStr(), &m_cStrings ); } + + // + // Calculate total character length of a MULTI_SZ, including the + // terminating NULLs. + // + + static DWORD CalcLength( const CHAR * str, + LPDWORD pcStrings = NULL ); + + // + // Determine if the MULTISZA contains a specific string. + // + + BOOL FindString( const CHAR * str ); + + BOOL FindString( STRA & str ) + { return FindString( str.QueryStr() ); } + + // + // Determine if the MULTISZA contains a specific string - case-insensitive + // + + BOOL FindStringNoCase( const CHAR * str ); + + BOOL FindStringNoCase( STRA & str ) + { return FindStringNoCase( str.QueryStr() ); } + + // + // Used for scanning a MULTISZA. + // + + const CHAR * First( VOID ) const + { return *QueryStr() == L'\0' ? NULL : QueryStr(); } + + const CHAR * Next( const CHAR * Current ) const + { Current += ::strlen( Current ) + 1; + return *Current == L'\0' ? NULL : Current; } + + BOOL + Equals( + MULTISZA* pmszRhs + ); + +private: + + DWORD m_cchLen; + DWORD m_cStrings; + VOID AuxInit( const CHAR * pInit ); + BOOL AuxAppend( const CHAR * pInit, + UINT cbStr, BOOL fAddSlop = TRUE ); + +}; + +// +// Quick macro for declaring a MULTISZA that will use stack memory of +// bytes. If the buffer overflows then a heap buffer will be allocated +// + +#define STACK_MULTISZA( name, size ) CHAR __ach##name[size]; \ + MULTISZA name( __ach##name, sizeof( __ach##name )) + +HRESULT +SplitCommaDelimitedString( + PCSTR pszList, + BOOL fTrimEntries, + BOOL fRemoveEmptyEntries, + MULTISZA * pmszList +); + +#endif // !_MULTISZA_HXX_ + diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/ntassert.h b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/ntassert.h new file mode 100644 index 0000000000..6d2f3b9a30 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/ntassert.h @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#ifdef _ASSERTE + #undef _ASSERTE +#endif + +#ifdef ASSERT + #undef ASSERT +#endif + +#if defined( DBG ) && DBG + #define SX_ASSERT( _x ) ( (VOID)( ( ( _x ) ) ? TRUE : ( __annotation( L"Debug", L"AssertFail", L#_x ), DbgRaiseAssertionFailure(), FALSE ) ) ) + #define SX_ASSERTMSG( _m, _x ) ( (VOID)( ( ( _x ) ) ? TRUE : ( __annotation( L"Debug", L"AssertFail", L##_m ), DbgRaiseAssertionFailure(), FALSE ) ) ) + #define SX_VERIFY( _x ) SX_ASSERT( _x ) + #define _ASSERTE( _x ) SX_ASSERT( _x ) + #define ASSERT( _x ) SX_ASSERT( _x ) + #define assert( _x ) SX_ASSERT( _x ) + #define DBG_ASSERT( _x ) SX_ASSERT( _x ) + #define DBG_REQUIRE( _x ) SX_ASSERT( _x ) +#else + #define SX_ASSERT( _x ) ( (VOID)0 ) + #define SX_ASSERTMSG( _m, _x ) ( (VOID)0 ) + #define SX_VERIFY( _x ) ( (VOID)( ( _x ) ? TRUE : FALSE ) ) + #define _ASSERTE( _x ) ( (VOID)0 ) + #define assert( _x ) ( (VOID)0 ) + #define DBG_ASSERT( _x ) ( (VOID)0 ) + #define DBG_REQUIRE( _x ) ((VOID)(_x)) +#endif + diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/percpu.h b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/percpu.h new file mode 100644 index 0000000000..5d3c563935 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/percpu.h @@ -0,0 +1,305 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +template +class PER_CPU +{ +public: + + template + inline + static + HRESULT + Create( + FunctionInitializer Initializer, + __deref_out PER_CPU ** ppInstance + ); + + inline + T * + GetLocal( + VOID + ); + + template + inline + VOID + ForEach( + FunctionForEach Function + ); + + inline + VOID + Dispose( + VOID + ); + +private: + + PER_CPU( + VOID + ) + { + // + // Don't perform any operation during constructor. + // Constructor will never be called. + // + } + + ~PER_CPU( + VOID + ) + { + // + // Don't perform any operation during destructor. + // Constructor will never be called. + // + } + + template + HRESULT + Initialize( + FunctionInitializer Initializer, + DWORD NumberOfVariables, + DWORD Alignment + ); + + T * + GetObject( + DWORD Index + ); + + static + HRESULT + GetProcessorInformation( + __out DWORD * pCacheLineSize, + __out DWORD * pNumberOfProcessors + ); + + // + // Pointer to the begining of the inlined array. + // + PVOID m_pVariables; + SIZE_T m_Alignment; + SIZE_T m_VariablesCount; +}; + +template +template +inline +// static +HRESULT +PER_CPU::Create( + FunctionInitializer Initializer, + __deref_out PER_CPU ** ppInstance +) +{ + HRESULT hr = S_OK; + DWORD CacheLineSize = 0; + DWORD ObjectCacheLineSize = 0; + DWORD NumberOfProcessors = 0; + PER_CPU * pInstance = NULL; + + hr = GetProcessorInformation(&CacheLineSize, + &NumberOfProcessors); + if (FAILED(hr)) + { + goto Finished; + } + + if (sizeof(T) > CacheLineSize) + { + // + // Round to the next multiple of the cache line size. + // + ObjectCacheLineSize = (sizeof(T) + CacheLineSize-1) & (CacheLineSize-1); + } + else + { + ObjectCacheLineSize = CacheLineSize; + } + + // + // Calculate the size of the PER_CPU object, including the array. + // The first cache line is for the member variables and the array + // starts in the next cache line. + // + SIZE_T Size = CacheLineSize + NumberOfProcessors * ObjectCacheLineSize; + + pInstance = (PER_CPU*) _aligned_malloc(Size, CacheLineSize); + if (pInstance == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + ZeroMemory(pInstance, Size); + + // + // The array start in the 2nd cache line. + // + pInstance->m_pVariables = reinterpret_cast(pInstance) + CacheLineSize; + + // + // Pass a disposer for disposing initialized items in case of failure. + // + hr = pInstance->Initialize(Initializer, + NumberOfProcessors, + ObjectCacheLineSize); + if (FAILED(hr)) + { + goto Finished; + } + + *ppInstance = pInstance; + pInstance = NULL; + +Finished: + + if (pInstance != NULL) + { + // + // Free the instance without disposing it. + // + pInstance->Dispose(); + pInstance = NULL; + } + + return hr; +} + +template +inline +T * +PER_CPU::GetLocal( + VOID +) +{ + // Use GetCurrentProcessorNumber (up to 64 logical processors) instead of + // GetCurrentProcessorNumberEx (more than 64 logical processors) because + // the number of processors are not densely packed per group. + // The idea of distributing variables per CPU is to have + // a scalability multiplier (could be NUMA node instead). + // + // Make sure the index don't go beyond the array size, if that happens, + // there won't be even distribution, but still better + // than one single variable. + // + return GetObject(GetCurrentProcessorNumber()); +} + +template +inline +T * +PER_CPU::GetObject( + DWORD Index +) +{ + return reinterpret_cast(static_cast(m_pVariables) + Index * m_Alignment); +} + +template +template +inline +VOID +PER_CPU::ForEach( + FunctionForEach Function +) +{ + for(DWORD Index = 0; Index < m_VariablesCount; ++Index) + { + T * pObject = GetObject(Index); + Function(pObject); + } +} + +template +VOID +PER_CPU::Dispose( + VOID +) +{ + _aligned_free(this); +} + +template +template +inline +HRESULT +PER_CPU::Initialize( + FunctionInitializer Initializer, + DWORD NumberOfVariables, + DWORD Alignment +) +/*++ + +Routine Description: + + Initialize each object using the initializer function. + If initialization for any object fails, it dispose the + objects that were successfully initialized. + +Arguments: + + Initializer - Function for initialize one object. + Signature: HRESULT Func(T*) + Dispose - Function for disposing initialized objects in case of failure. + Signature: void Func(T*) + NumberOfVariables - The length of the array of variables. + Alignment - Alignment to use for avoiding false sharing. + +Return: + + HRESULT - E_OUTOFMEMORY + +--*/ +{ + HRESULT hr = S_OK; + DWORD Index = 0; + + m_VariablesCount = NumberOfVariables; + m_Alignment = Alignment; + + for (; Index < m_VariablesCount; ++Index) + { + T * pObject = GetObject(Index); + Initializer(pObject); + } + + return hr; +} + +template +// static +HRESULT +PER_CPU::GetProcessorInformation( + __out DWORD * pCacheLineSize, + __out DWORD * pNumberOfProcessors +) +/*++ + +Routine Description: + + Gets the CPU cache-line size for the current system. + This information is used for avoiding CPU false sharing. + +Arguments: + + pCacheLineSize - The processor cache-line size. + pNumberOfProcessors - Maximum number of processors per group. + +Return: + + HRESULT - E_OUTOFMEMORY + +--*/ +{ + SYSTEM_INFO SystemInfo = { }; + + GetSystemInfo(&SystemInfo); + *pNumberOfProcessors = SystemInfo.dwNumberOfProcessors; + *pCacheLineSize = SYSTEM_CACHE_ALIGNMENT_SIZE; + + return S_OK; +} \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/precomp.h b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/precomp.h new file mode 100644 index 0000000000..9cccea4045 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/precomp.h @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include +#include +#pragma warning( disable:4127 ) +#include +#include +#include +#include +#include +#include + +#include "macros.h" +#include "stringu.h" +#include "stringa.h" +#include "dbgutil.h" +#include "ntassert.h" +#include "ahutil.h" +#include "acache.h" +//#include "base64.hxx" + diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/prime.h b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/prime.h new file mode 100644 index 0000000000..6a6a88ed78 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/prime.h @@ -0,0 +1,85 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include +#include + +// +// Pre-calculated prime numbers (up to 10,049,369). +// +extern __declspec(selectany) const DWORD g_Primes [] = { + 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, + 761, 919, 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, + 12143, 14591, 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, + 130363, 156437, 187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, + 968897, 1162687, 1395263, 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, + 5999471, 7199369, 7849369, 8649369, 9249369, 10049369 +}; + +class PRIME +{ +public: + + static + DWORD + GetPrime( + DWORD dwMinimum + ) + { + // + // Try to use the precalculated numbers. + // + for ( DWORD Index = 0; Index < _countof( g_Primes ); Index++ ) + { + DWORD dwCandidate = g_Primes[Index]; + if ( dwCandidate >= dwMinimum ) + { + return dwCandidate; + } + } + + // + // Do calculation. + // + for ( DWORD dwCandidate = dwMinimum | 1; + dwCandidate < MAXDWORD; + dwCandidate += 2 ) + { + if ( IsPrime( dwCandidate ) ) + { + return dwCandidate; + } + } + return dwMinimum; + } + +private: + + static + BOOL + IsPrime( + DWORD dwCandidate + ) + { + if ((dwCandidate & 1) == 0) + { + return ( dwCandidate == 2 ); + } + + DWORD dwMax = static_cast(sqrt(static_cast(dwCandidate))); + + for ( DWORD Index = 3; Index <= dwMax; Index += 2 ) + { + if ( (dwCandidate % Index) == 0 ) + { + return FALSE; + } + } + return TRUE; + } + + PRIME() {} + ~PRIME() {} +}; \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/pudebug.h b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/pudebug.h new file mode 100644 index 0000000000..7b0e35da0f --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/pudebug.h @@ -0,0 +1,736 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +# ifndef _PUDEBUG_H_ +# define _PUDEBUG_H_ + +#ifndef _NO_TRACING_ +# define _NO_TRACING_ +#endif // _NO_TRACING_ + +/************************************************************ + * Include Headers + ************************************************************/ + +# ifdef __cplusplus +extern "C" { +# endif // __cplusplus + +# include + +# ifndef dllexp +# define dllexp __declspec( dllexport) +# endif // dllexp + +#include + +#ifndef IN_OUT +#define IN_OUT __inout +#endif + +/*********************************************************** + * Macros + ************************************************************/ + +enum PRINT_REASONS { + PrintNone = 0x0, // Nothing to be printed + PrintError = 0x1, // An error message + PrintWarning = 0x2, // A warning message + PrintLog = 0x3, // Just logging. Indicates a trace of where ... + PrintMsg = 0x4, // Echo input message + PrintCritical = 0x5, // Print and Exit + PrintAssertion= 0x6 // Printing for an assertion failure + }; + + +enum DEBUG_OUTPUT_FLAGS { + DbgOutputNone = 0x0, // None + DbgOutputKdb = 0x1, // Output to Kernel Debugger + DbgOutputLogFile = 0x2, // Output to LogFile + DbgOutputTruncate = 0x4, // Truncate Log File if necessary + DbgOutputStderr = 0x8, // Send output to std error + DbgOutputBackup = 0x10, // Make backup of debug file ? + DbgOutputMemory = 0x20, // Dump to memory buffer + DbgOutputAll = 0xFFFFFFFF // All the bits set. + }; + + +# define MAX_LABEL_LENGTH ( 100) + + +// The following flags are used internally to track what level of tracing we +// are currently using. Bitmapped for extensibility. +#define DEBUG_FLAG_ODS 0x00000001 +//#define DEBUG_FLAG_INFO 0x00000002 +//#define DEBUG_FLAG_WARN 0x00000004 +//#define DEBUG_FLAG_ERROR 0x00000008 +// The following are used internally to determine whether to log or not based +// on what the current state is +//#define DEBUG_FLAGS_INFO (DEBUG_FLAG_ODS | DEBUG_FLAG_INFO) +//#define DEBUG_FLAGS_WARN (DEBUG_FLAG_ODS | DEBUG_FLAG_INFO | DEBUG_FLAG_WARN) +//#define DEBUG_FLAGS_ERROR (DEBUG_FLAG_ODS | DEBUG_FLAG_INFO | DEBUG_FLAG_WARN | DEBUG_FLAG_ERROR) + +#define DEBUG_FLAGS_ANY (DEBUG_FLAG_INFO | DEBUG_FLAG_WARN | DEBUG_FLAG_ERROR) + +// +// user of DEBUG infrastructure may choose unique variable name for DEBUG_FLAGS +// that's specially useful for cases where DEBUG infrastructure is used within +// static library (static library may prefer to maintain it's own DebugFlags independent +// on the main program it links to +// +#ifndef DEBUG_FLAGS_VAR +#define DEBUG_FLAGS_VAR g_dwDebugFlags +#endif + +extern +#ifdef __cplusplus +"C" +# endif // _cplusplus + DWORD DEBUG_FLAGS_VAR ; // Debugging Flags + +# define DECLARE_DEBUG_VARIABLE() + +# define SET_DEBUG_FLAGS( dwFlags) DEBUG_FLAGS_VAR = dwFlags +# define GET_DEBUG_FLAGS() ( DEBUG_FLAGS_VAR ) + +# define LOAD_DEBUG_FLAGS_FROM_REG(hkey, dwDefault) \ + DEBUG_FLAGS_VAR = PuLoadDebugFlagsFromReg((hkey), (dwDefault)) + +# define LOAD_DEBUG_FLAGS_FROM_REG_STR(pszRegKey, dwDefault) \ + DEBUG_FLAGS_VAR = PuLoadDebugFlagsFromRegStr((pszRegKey), (dwDefault)) + +# define SAVE_DEBUG_FLAGS_IN_REG(hkey, dwDbg) \ + PuSaveDebugFlagsInReg((hkey), (dwDbg)) + +# define DEBUG_IF( arg, s) if ( DEBUG_ ## arg & GET_DEBUG_FLAGS()) { \ + s \ + } else {} + +# define IF_DEBUG( arg) if ( DEBUG_## arg & GET_DEBUG_FLAGS()) + + +/*++ + class DEBUG_PRINTS + + This class is responsible for printing messages to log file / kernel debugger + + Currently the class supports only member functions for char. + ( not unicode-strings). + +--*/ + + +typedef struct _DEBUG_PRINTS { + + CHAR m_rgchLabel[MAX_LABEL_LENGTH]; + CHAR m_rgchLogFilePath[MAX_PATH]; + CHAR m_rgchLogFileName[MAX_PATH]; + HANDLE m_LogFileHandle; + HANDLE m_StdErrHandle; + BOOL m_fInitialized; + BOOL m_fBreakOnAssert; + DWORD m_dwOutputFlags; + VOID *m_pMemoryLog; +} DEBUG_PRINTS, FAR * LPDEBUG_PRINTS; + + +LPDEBUG_PRINTS +PuCreateDebugPrintsObject( + IN const char * pszPrintLabel, + IN DWORD dwOutputFlags); + +// +// frees the debug prints object and closes any file if necessary. +// Returns NULL on success or returns pDebugPrints on failure. +// +LPDEBUG_PRINTS +PuDeleteDebugPrintsObject( + IN_OUT LPDEBUG_PRINTS pDebugPrints); + + +VOID +PuDbgPrint( + IN_OUT LPDEBUG_PRINTS pDebugPrints, + IN const char * pszFilePath, + IN int nLineNum, + IN const char * pszFunctionName, + IN const char * pszFormat, + ...); + // arglist +VOID +PuDbgPrintW( + IN_OUT LPDEBUG_PRINTS pDebugPrints, + IN const char * pszFilePath, + IN int nLineNum, + IN const char * pszFunctionName, + IN const WCHAR * pszFormat, + ...); // arglist + +// PuDbgPrintError is similar to PuDbgPrint() but allows +// one to print error code in friendly manner +VOID +PuDbgPrintError( + IN_OUT LPDEBUG_PRINTS pDebugPrints, + IN const char * pszFilePath, + IN int nLineNum, + IN const char * pszFunctionName, + IN DWORD dwError, + IN const char * pszFormat, + ...); // arglist + +/*++ + PuDbgDump() does not do any formatting of output. + It just dumps the given message onto the debug destinations. +--*/ +VOID +PuDbgDump( + IN_OUT LPDEBUG_PRINTS pDebugPrints, + IN const char * pszFilePath, + IN int nLineNum, + IN const char * pszFunctionName, + IN const char * pszDump + ); + +// +// PuDbgAssertFailed() *must* be __cdecl to properly capture the +// thread context at the time of the failure. +// + +INT +__cdecl +PuDbgAssertFailed( + IN_OUT LPDEBUG_PRINTS pDebugPrints, + IN const char * pszFilePath, + IN int nLineNum, + IN const char * pszFunctionName, + IN const char * pszExpression, + IN const char * pszMessage); + +INT +WINAPI +PuDbgPrintAssertFailed( + IN_OUT LPDEBUG_PRINTS pDebugPrints, + IN const char * pszFilePath, + IN int nLineNum, + IN const char * pszFunctionName, + IN const char * pszExpression, + IN const char * pszMessage); + +VOID +PuDbgCaptureContext ( + OUT PCONTEXT ContextRecord + ); + +VOID +PuDbgPrintCurrentTime( + IN_OUT LPDEBUG_PRINTS pDebugPrints, + IN const char * pszFilePath, + IN int nLineNum, + IN const char * pszFunctionName + ); + +VOID +PuSetDbgOutputFlags( + IN_OUT LPDEBUG_PRINTS pDebugPrints, + IN DWORD dwFlags); + +DWORD +PuGetDbgOutputFlags( + IN const LPDEBUG_PRINTS pDebugPrints); + + +// +// Following functions return Win32 error codes. +// NO_ERROR if success +// + +DWORD +PuOpenDbgPrintFile( + IN_OUT LPDEBUG_PRINTS pDebugPrints, + IN const char * pszFileName, + IN const char * pszPathForFile); + +DWORD +PuReOpenDbgPrintFile( + IN_OUT LPDEBUG_PRINTS pDebugPrints); + +DWORD +PuCloseDbgPrintFile( + IN_OUT LPDEBUG_PRINTS pDebugPrints); + +DWORD +PuOpenDbgMemoryLog( + IN_OUT LPDEBUG_PRINTS pDebugPrints); + +DWORD +PuCloseDbgMemoryLog( + IN_OUT LPDEBUG_PRINTS pDebugPrints); + +DWORD +PuLoadDebugFlagsFromReg(IN HKEY hkey, IN DWORD dwDefault); + +DWORD +PuLoadDebugFlagsFromRegStr(IN LPCSTR pszRegKey, IN DWORD dwDefault); + +DWORD +PuSaveDebugFlagsInReg(IN HKEY hkey, IN DWORD dwDbg); + + +# define PuPrintToKdb( pszOutput) \ + if ( pszOutput != NULL) { \ + OutputDebugString( pszOutput); \ + } else {} + + + +# ifdef __cplusplus +}; +# endif // __cplusplus + +// begin_user_unmodifiable + + + +/*********************************************************** + * Macros + ************************************************************/ + + +extern +#ifdef __cplusplus +"C" +# endif // _cplusplus +DEBUG_PRINTS * g_pDebug; // define a global debug variable + +# if DBG + +// For the CHK build we want ODS enabled. For an explanation of these flags see +// the comment just after the definition of DBG_CONTEXT +# define DECLARE_DEBUG_PRINTS_OBJECT() \ + DEBUG_PRINTS * g_pDebug = NULL; \ + DWORD DEBUG_FLAGS_VAR = DEBUG_FLAG_ERROR; + +#else // !DBG + +# define DECLARE_DEBUG_PRINTS_OBJECT() \ + DEBUG_PRINTS * g_pDebug = NULL; \ + DWORD DEBUG_FLAGS_VAR = 0; + +#endif // !DBG + + +// +// Call the following macro as part of your initialization for program +// planning to use the debugging class. +// +/** DEBUGDEBUG +# define CREATE_DEBUG_PRINT_OBJECT( pszLabel) \ + g_pDebug = PuCreateDebugPrintsObject( pszLabel, DEFAULT_OUTPUT_FLAGS);\ + if ( g_pDebug == NULL) { \ + OutputDebugStringA( "Unable to Create Debug Print Object \n"); \ + } +*/ + +// +// Call the following macro once as part of the termination of program +// which uses the debugging class. +// +# define DELETE_DEBUG_PRINT_OBJECT( ) \ + g_pDebug = PuDeleteDebugPrintsObject( g_pDebug); + + +# define VALID_DEBUG_PRINT_OBJECT() \ + ( ( g_pDebug != NULL) && g_pDebug->m_fInitialized) + + +// +// Use the DBG_CONTEXT without any surrounding braces. +// This is used to pass the values for global DebugPrintObject +// and File/Line information +// +//# define DBG_CONTEXT g_pDebug, __FILE__, __LINE__, __FUNCTION__ + +// The 3 main tracing macros, each one corresponds to a different level of +// tracing + +// The 3 main tracing macros, each one corresponds to a different level of +// tracing +//# define DBGINFO(args) {if (DEBUG_FLAGS_VAR & DEBUG_FLAGS_INFO) { PuDbgPrint args; }} +//# define DBGWARN(args) {if (DEBUG_FLAGS_VAR & DEBUG_FLAGS_WARN) { PuDbgPrint args; }} +//# define DBGERROR(args) {if (DEBUG_FLAGS_VAR & DEBUG_FLAGS_ERROR) { PuDbgPrint args; }} + +# define DBGINFOW(args) {if (DEBUG_FLAGS_VAR & DEBUG_FLAGS_INFO) { PuDbgPrintW args; }} +# define DBGWARNW(args) {if (DEBUG_FLAGS_VAR & DEBUG_FLAGS_WARN) { PuDbgPrintW args; }} +# define DBGERRORW(args) {if (DEBUG_FLAGS_VAR & DEBUG_FLAGS_ERROR) { PuDbgPrintW args; }} + + +// +// DBGPRINTF() is printing function ( much like printf) but always called +// with the DBG_CONTEXT as follows +// DBGPRINTF( ( DBG_CONTEXT, format-string, arguments for format list)); +// +# define DBGPRINTF DBGINFO + +// +// DPERROR() is printing function ( much like printf) but always called +// with the DBG_CONTEXT as follows +// DPERROR( ( DBG_CONTEXT, error, format-string, +// arguments for format list)); +// +# define DPERROR( args) {if (DEBUG_FLAGS_VAR & DEBUG_FLAGS_ERROR) { PuDbgPrintError args; }} + +# if DBG + +# define DBG_CODE(s) s /* echoes code in debugging mode */ + +// The same 3 main tracing macros however in this case the macros are only compiled +// into the CHK build. This is necessary because some tracing info used functions or +// variables which are not compiled into the FRE build. +# define CHKINFO(args) { PuDbgPrint args; } +# define CHKWARN(args) { PuDbgPrint args; } +# define CHKERROR(args) { PuDbgPrint args; } + +# define CHKINFOW(args) { PuDbgPrintW args; } +# define CHKWARNW(args) { PuDbgPrintW args; } +# define CHKERRORW(args) { PuDbgPrintW args; } + + +#ifndef DBG_ASSERT +# ifdef _PREFAST_ +# define DBG_ASSERT(exp) ((void)0) /* Do Nothing */ +# define DBG_ASSERT_MSG(exp, pszMsg) ((void)0) /* Do Nothing */ +# define DBG_REQUIRE( exp) ((void) (exp)) +# else // !_PREFAST_ +# define DBG_ASSERT( exp ) \ + ( (VOID)( ( exp ) || ( DebugBreak(), \ + PuDbgPrintAssertFailed( DBG_CONTEXT, #exp, "" ) ) ) ) + +# define DBG_ASSERT_MSG( exp, pszMsg) \ + ( (VOID)( ( exp ) || ( DebugBreak(), \ + PuDbgPrintAssertFailed( DBG_CONTEXT, #exp, pszMsg ) ) ) ) + +# define DBG_REQUIRE( exp ) \ + DBG_ASSERT( exp ) +# endif // !_PREFAST_ +#endif + + +# define DBG_LOG() PuDbgPrint( DBG_CONTEXT, "\n" ) + +# define DBG_OPEN_LOG_FILE( pszFile, pszPath ) \ + PuOpenDbgPrintFile( g_pDebug, (pszFile), (pszPath) ) + +# define DBG_CLOSE_LOG_FILE( ) \ + PuCloseDbgPrintFile( g_pDebug ) + +# define DBG_OPEN_MEMORY_LOG( ) \ + PuOpenDbgMemoryLog( g_pDebug ) + + +# define DBGDUMP( args ) PuDbgDump args + +# define DBGPRINT_CURRENT_TIME() PuDbgPrintCurrentTime( DBG_CONTEXT ) + +# else // !DBG + +# define DBG_CODE(s) ((void)0) /* Do Nothing */ + +# define CHKINFO(args) ((void)0) /* Do Nothing */ +# define CHKWARN(args) ((void)0) /* Do Nothing */ +# define CHKERROR(args) ((void)0) /* Do Nothing */ + +# define CHKINFOW(args) ((void)0) /* Do Nothing */ +# define CHKWARNW(args) ((void)0) /* Do Nothing */ +# define CHKERRORW(args) ((void)0) /* Do Nothing */ + +#ifndef DBG_ASSERT +# define DBG_ASSERT(exp) ((void)0) /* Do Nothing */ + +# define DBG_ASSERT_MSG(exp, pszMsg) ((void)0) /* Do Nothing */ + +# define DBG_REQUIRE( exp) ((void) (exp)) +#endif // !DBG_ASSERT + +# define DBGDUMP( args) ((void)0) /* Do nothing */ + +# define DBG_LOG() ((void)0) /* Do Nothing */ + +# define DBG_OPEN_LOG_FILE( pszFile, pszPath) ((void)0) /* Do Nothing */ + +# define DBG_OPEN_MEMORY_LOG() ((void)0) /* Do Nothing */ + +# define DBG_CLOSE_LOG_FILE() ((void)0) /* Do Nothing */ + +# define DBGPRINT_CURRENT_TIME() ((void)0) /* Do Nothing */ + +# endif // !DBG + + +// end_user_unmodifiable + +// begin_user_unmodifiable + + +#ifdef ASSERT +# undef ASSERT +#endif + + +# define ASSERT( exp) DBG_ASSERT( exp) + + +// end_user_unmodifiable + +// begin_user_modifiable + +// +// Debugging constants consist of two pieces. +// All constants in the range 0x0 to 0x8000 are reserved +// User extensions may include additional constants (bit flags) +// + +# define DEBUG_API_ENTRY 0x00000001L +# define DEBUG_API_EXIT 0x00000002L +# define DEBUG_INIT_CLEAN 0x00000004L +# define DEBUG_ERROR 0x00000008L + + // End of Reserved Range +# define DEBUG_RESERVED 0x00000FFFL + +// end_user_modifiable + + + +/*********************************************************** + * Platform Type related variables and macros + ************************************************************/ + +// +// Enum for product types +// + +typedef enum _PLATFORM_TYPE { + + PtInvalid = 0, // Invalid + PtNtWorkstation = 1, // NT Workstation + PtNtServer = 2, // NT Server + +} PLATFORM_TYPE; + +// +// IISGetPlatformType is the function used to the platform type +// + +extern +#ifdef __cplusplus +"C" +# endif // _cplusplus +PLATFORM_TYPE +IISGetPlatformType( + VOID + ); + +// +// External Macros +// + +#define InetIsNtServer( _pt ) ((_pt) == PtNtServer) +#define InetIsNtWksta( _pt ) ((_pt) == PtNtWorkstation) +#define InetIsValidPT(_pt) ((_pt) != PtInvalid) + +extern +#ifdef __cplusplus +"C" +# endif // _cplusplus +PLATFORM_TYPE g_PlatformType; + + +// Use the DECLARE_PLATFORM_TYPE macro to declare the platform type +#define DECLARE_PLATFORM_TYPE() \ + PLATFORM_TYPE g_PlatformType = PtInvalid; + +// Use the INITIALIZE_PLATFORM_TYPE to init the platform type +// This should typically go inside the DLLInit or equivalent place. +#define INITIALIZE_PLATFORM_TYPE() \ + g_PlatformType = IISGetPlatformType(); + +// +// Additional Macros to use the Platform Type +// + +#define TsIsNtServer( ) InetIsNtServer(g_PlatformType) +#define TsIsNtWksta( ) InetIsNtWksta(g_PlatformType) +#define IISIsValidPlatform() InetIsValidPT(g_PlatformType) +#define IISPlatformType() (g_PlatformType) + + +/*********************************************************** + * Some utility functions for Critical Sections + ************************************************************/ + +// +// IISSetCriticalSectionSpinCount() provides a thunk for the +// original NT4.0sp3 API SetCriticalSectionSpinCount() for CS with Spin counts +// Users of this function should definitely dynlink with kernel32.dll, +// Otherwise errors will surface to a large extent +// +extern +# ifdef __cplusplus +"C" +# endif // _cplusplus +DWORD +IISSetCriticalSectionSpinCount( + LPCRITICAL_SECTION lpCriticalSection, + DWORD dwSpinCount +); + + +// +// Macro for the calls to SetCriticalSectionSpinCount() +// +# define SET_CRITICAL_SECTION_SPIN_COUNT( lpCS, dwSpins) \ + IISSetCriticalSectionSpinCount( (lpCS), (dwSpins)) + +// +// IIS_DEFAULT_CS_SPIN_COUNT is the default value of spins used by +// Critical sections defined within IIS. +// NYI: We should have to switch the individual values based on experiments! +// Current value is an arbitrary choice +// +# define IIS_DEFAULT_CS_SPIN_COUNT (1000) + +// +// Initializes a critical section and sets its spin count +// to IIS_DEFAULT_CS_SPIN_COUNT. Equivalent to +// InitializeCriticalSectionAndSpinCount(lpCS, IIS_DEFAULT_CS_SPIN_COUNT), +// but provides a safe thunking layer for older systems that don't provide +// this API. +// +extern +# ifdef __cplusplus +"C" +# endif // _cplusplus +BOOL +IISInitializeCriticalSection( + LPCRITICAL_SECTION lpCriticalSection +); + +// +// Macro for the calls to InitializeCriticalSection() +// +# define INITIALIZE_CRITICAL_SECTION(lpCS) IISInitializeCriticalSection(lpCS) + +# endif /* _DEBUG_HXX_ */ + +// +// The following macros allow the automatic naming of certain Win32 objects. +// See IIS\SVCS\IISRTL\WIN32OBJ.C for details on the naming convention. +// +// Set IIS_NAMED_WIN32_OBJECTS to a non-zero value to enable named events, +// semaphores, and mutexes. +// + +#if DBG +#define IIS_NAMED_WIN32_OBJECTS 1 +#else +#define IIS_NAMED_WIN32_OBJECTS 0 +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +HANDLE +PuDbgCreateEvent( + __in LPSTR FileName, + IN ULONG LineNumber, + __in LPSTR MemberName, + IN PVOID Address, + IN BOOL ManualReset, + IN BOOL InitialState + ); + +HANDLE +PuDbgCreateSemaphore( + __in LPSTR FileName, + IN ULONG LineNumber, + __in LPSTR MemberName, + IN PVOID Address, + IN LONG InitialCount, + IN LONG MaximumCount + ); + +HANDLE +PuDbgCreateMutex( + __in LPSTR FileName, + IN ULONG LineNumber, + __in LPSTR MemberName, + IN PVOID Address, + IN BOOL InitialOwner + ); + +#ifdef __cplusplus +} // extern "C" +#endif + +#if IIS_NAMED_WIN32_OBJECTS + +#define IIS_CREATE_EVENT( membername, address, manual, state ) \ + PuDbgCreateEvent( \ + (LPSTR)__FILE__, \ + (ULONG)__LINE__, \ + (membername), \ + (PVOID)(address), \ + (manual), \ + (state) \ + ) + +#define IIS_CREATE_SEMAPHORE( membername, address, initial, maximum ) \ + PuDbgCreateSemaphore( \ + (LPSTR)__FILE__, \ + (ULONG)__LINE__, \ + (membername), \ + (PVOID)(address), \ + (initial), \ + (maximum) \ + ) + +#define IIS_CREATE_MUTEX( membername, address, initial ) \ + PuDbgCreateMutex( \ + (LPSTR)__FILE__, \ + (ULONG)__LINE__, \ + (membername), \ + (PVOID)(address), \ + (initial) \ + ) + +#else // !IIS_NAMED_WIN32_OBJECTS + +#define IIS_CREATE_EVENT( membername, address, manual, state ) \ + CreateEventA( \ + NULL, \ + (manual), \ + (state), \ + NULL \ + ) + +#define IIS_CREATE_SEMAPHORE( membername, address, initial, maximum ) \ + CreateSemaphoreA( \ + NULL, \ + (initial), \ + (maximum), \ + NULL \ + ) + +#define IIS_CREATE_MUTEX( membername, address, initial ) \ + CreateMutexA( \ + NULL, \ + (initial), \ + NULL \ + ) + +#endif // IIS_NAMED_WIN32_OBJECTS + + +/************************ End of File ***********************/ + diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/reftrace.c b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/reftrace.c new file mode 100644 index 0000000000..c1b2e13a6e --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/reftrace.c @@ -0,0 +1,229 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include +#include "dbgutil.h" +#include "pudebug.h" +#include "reftrace.h" + + +PTRACE_LOG +CreateRefTraceLog( + IN LONG LogSize, + IN LONG ExtraBytesInHeader + ) +/*++ + +Routine Description: + + Creates a new (empty) ref count trace log buffer. + +Arguments: + + LogSize - The number of entries in the log. + + ExtraBytesInHeader - The number of extra bytes to include in the + log header. This is useful for adding application-specific + data to the log. + +Return Value: + + PTRACE_LOG - Pointer to the newly created log if successful, + NULL otherwise. + +--*/ +{ + + return CreateTraceLog( + LogSize, + ExtraBytesInHeader, + sizeof(REF_TRACE_LOG_ENTRY) + ); + +} // CreateRefTraceLog + + +VOID +DestroyRefTraceLog( + IN PTRACE_LOG Log + ) +/*++ + +Routine Description: + + Destroys a ref count trace log buffer created with CreateRefTraceLog(). + +Arguments: + + Log - The ref count trace log buffer to destroy. + +Return Value: + + None. + +--*/ +{ + + DestroyTraceLog( Log ); + +} // DestroyRefTraceLog + + +// +// N.B. For RtlCaptureBacktrace() to work properly, the calling function +// *must* be __cdecl, and must have a "normal" stack frame. So, we decorate +// WriteRefTraceLog[Ex]() with the __cdecl modifier and disable the frame +// pointer omission (FPO) optimization. +// + +//#pragma optimize( "y", off ) // disable frame pointer omission (FPO) +#pragma optimize( "", off ) // disable frame pointer omission (FPO) + +LONG +__cdecl +WriteRefTraceLog( + IN PTRACE_LOG Log, + IN LONG NewRefCount, + IN CONST VOID * Context + ) +/*++ + +Routine Description: + + Writes a new entry to the specified ref count trace log. The entry + written contains the updated reference count and a stack backtrace + leading up to the current caller. + +Arguments: + + Log - The log to write to. + + NewRefCount - The updated reference count. + + Context - An uninterpreted context to associate with the log entry. + +Return Value: + + Index of entry in log. + +--*/ +{ + + return WriteRefTraceLogEx( + Log, + NewRefCount, + Context, + REF_TRACE_EMPTY_CONTEXT, // suppress use of optional extra contexts + REF_TRACE_EMPTY_CONTEXT, + REF_TRACE_EMPTY_CONTEXT + ); + +} // WriteRefTraceLog + + + + +LONG +__cdecl +WriteRefTraceLogEx( + IN PTRACE_LOG Log, + IN LONG NewRefCount, + IN CONST VOID * Context, + IN CONST VOID * Context1, // optional extra context + IN CONST VOID * Context2, // optional extra context + IN CONST VOID * Context3 // optional extra context + ) +/*++ + +Routine Description: + + Writes a new "extended" entry to the specified ref count trace log. + The entry written contains the updated reference count, stack backtrace + leading up to the current caller and extra context information. + +Arguments: + + Log - The log to write to. + + NewRefCount - The updated reference count. + + Context - An uninterpreted context to associate with the log entry. + Context1 - An uninterpreted context to associate with the log entry. + Context2 - An uninterpreted context to associate with the log entry. + Context3 - An uninterpreted context to associate with the log entry. + + NOTE Context1/2/3 are "optional" in that the caller may suppress + debug display of these values by passing REF_TRACE_EMPTY_CONTEXT + for each of them. + +Return Value: + + Index of entry in log. + +--*/ +{ + + REF_TRACE_LOG_ENTRY entry; + ULONG hash; + DWORD cStackFramesSkipped; + + // + // Initialize the entry. + // + + RtlZeroMemory( + &entry, + sizeof(entry) + ); + + // + // Set log entry members. + // + + entry.NewRefCount = NewRefCount; + entry.Context = Context; + entry.Thread = GetCurrentThreadId(); + entry.Context1 = Context1; + entry.Context2 = Context2; + entry.Context3 = Context3; + + // + // Capture the stack backtrace. Normally, we skip two stack frames: + // one for this routine, and one for RtlCaptureBacktrace() itself. + // For non-Ex callers who come in via WriteRefTraceLog, + // we skip three stack frames. + // + + if ( entry.Context1 == REF_TRACE_EMPTY_CONTEXT + && entry.Context2 == REF_TRACE_EMPTY_CONTEXT + && entry.Context3 == REF_TRACE_EMPTY_CONTEXT + ) { + + cStackFramesSkipped = 2; + + } else { + + cStackFramesSkipped = 1; + + } + + RtlCaptureStackBackTrace( + cStackFramesSkipped, + REF_TRACE_LOG_STACK_DEPTH, + entry.Stack, + &hash + ); + + // + // Write it to the log. + // + + return WriteTraceLog( + Log, + &entry + ); + +} // WriteRefTraceLogEx + +#pragma optimize( "", on ) // restore frame pointer omission (FPO) + diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/reftrace.h b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/reftrace.h new file mode 100644 index 0000000000..e90ca0444a --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/reftrace.h @@ -0,0 +1,87 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#ifndef _REFTRACE_H_ +#define _REFTRACE_H_ + + +#if defined(__cplusplus) +extern "C" { +#endif // __cplusplus + +#include +#include "tracelog.h" + +// +// This is the number of stack backtrace values captured in each +// trace log entry. This value is chosen to make the log entry +// exactly twelve dwords long, making it a bit easier to interpret +// from within the debugger without the debugger extension. +// + +#define REF_TRACE_LOG_STACK_DEPTH 9 + +// No-op value for the Context1,2,3 parameters of WriteRefTraceLogEx +//#define REF_TRACE_EMPTY_CONTEXT ((PVOID) -1) +#define REF_TRACE_EMPTY_CONTEXT NULL + + +// +// This defines the entry written to the trace log. +// + +typedef struct _REF_TRACE_LOG_ENTRY { + + LONG NewRefCount; + CONST VOID * Context; + CONST VOID * Context1; + CONST VOID * Context2; + CONST VOID * Context3; + DWORD Thread; + PVOID Stack[REF_TRACE_LOG_STACK_DEPTH]; + +} REF_TRACE_LOG_ENTRY, *PREF_TRACE_LOG_ENTRY; + + +// +// Manipulators. +// + +PTRACE_LOG +CreateRefTraceLog( + IN LONG LogSize, + IN LONG ExtraBytesInHeader + ); + +VOID +DestroyRefTraceLog( + IN PTRACE_LOG Log + ); + +LONG +__cdecl +WriteRefTraceLog( + IN PTRACE_LOG Log, + IN LONG NewRefCount, + IN CONST VOID * Context + ); + +LONG +__cdecl +WriteRefTraceLogEx( + IN PTRACE_LOG Log, + IN LONG NewRefCount, + IN CONST VOID * Context, + IN CONST VOID * Context1, + IN CONST VOID * Context2, + IN CONST VOID * Context3 + ); + + +#if defined(__cplusplus) +} // extern "C" +#endif // __cplusplus + + +#endif // _REFTRACE_H_ + diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/rwlock.h b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/rwlock.h new file mode 100644 index 0000000000..dc7ccf834b --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/rwlock.h @@ -0,0 +1,193 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#if (_WIN32_WINNT < 0x600) + +// +// XP implementation. +// +class CWSDRWLock +{ +public: + + CWSDRWLock() + : m_bInited(FALSE) + { + } + + ~CWSDRWLock() + { + if (m_bInited) + { + DeleteCriticalSection(&m_rwLock.critsec); + CloseHandle(m_rwLock.ReadersDoneEvent); + } + } + + BOOL QueryInited() const + { + return m_bInited; + } + + HRESULT Init() + { + HRESULT hr = S_OK; + + if (FALSE == m_bInited) + { + m_rwLock.fWriterWaiting = FALSE; + m_rwLock.LockCount = 0; + if ( !InitializeCriticalSectionAndSpinCount( &m_rwLock.critsec, 0 )) + { + DWORD dwError = GetLastError(); + hr = HRESULT_FROM_WIN32(dwError); + return hr; + } + + m_rwLock.ReadersDoneEvent = CreateEvent(NULL, FALSE, FALSE, NULL); + if( NULL == m_rwLock.ReadersDoneEvent ) + { + DWORD dwError = GetLastError(); + hr = HRESULT_FROM_WIN32(dwError); + DeleteCriticalSection(&m_rwLock.critsec); + return hr; + } + m_bInited = TRUE; + } + + return hr; + } + + void SharedAcquire() + { + EnterCriticalSection(&m_rwLock.critsec); + InterlockedIncrement(&m_rwLock.LockCount); + LeaveCriticalSection(&m_rwLock.critsec); + } + + void SharedRelease() + { + ReleaseRWLock(); + } + + void ExclusiveAcquire() + { + EnterCriticalSection( &m_rwLock.critsec ); + + m_rwLock.fWriterWaiting = TRUE; + + // check if there are any readers active + if ( InterlockedExchangeAdd( &m_rwLock.LockCount, 0 ) > 0 ) + { + // + // Wait for all the readers to get done.. + // + WaitForSingleObject( m_rwLock.ReadersDoneEvent, INFINITE ); + } + m_rwLock.LockCount = -1; + } + + void ExclusiveRelease() + { + ReleaseRWLock(); + } + +private: + + BOOL m_bInited; + + typedef struct _RW_LOCK + { + BOOL fWriterWaiting; // Is a writer waiting on the lock? + LONG LockCount; + CRITICAL_SECTION critsec; + HANDLE ReadersDoneEvent; + } RW_LOCK, *PRW_LOCK; + + RW_LOCK m_rwLock; + +private: + + void ReleaseRWLock() + { + LONG Count = InterlockedDecrement( &m_rwLock.LockCount ); + + if ( 0 <= Count ) + { + // releasing a read lock + if (( m_rwLock.fWriterWaiting ) && ( 0 == Count )) + { + SetEvent( m_rwLock.ReadersDoneEvent ); + } + } + else + { + // Releasing a write lock + m_rwLock.LockCount = 0; + m_rwLock.fWriterWaiting = FALSE; + LeaveCriticalSection(&m_rwLock.critsec); + } + } +}; + +#else + +// +// Implementation for Windows Vista or greater. +// +class CWSDRWLock +{ +public: + + CWSDRWLock() + { + InitializeSRWLock(&m_rwLock); + } + + BOOL QueryInited() + { + return TRUE; + } + + + HRESULT Init() + { + // + // Method defined to keep compatibility with CWSDRWLock class for XP. + // + return S_OK; + } + + void SharedAcquire() + { + AcquireSRWLockShared(&m_rwLock); + } + + void SharedRelease() + { + ReleaseSRWLockShared(&m_rwLock); + } + + void ExclusiveAcquire() + { + AcquireSRWLockExclusive(&m_rwLock); + } + + void ExclusiveRelease() + { + ReleaseSRWLockExclusive(&m_rwLock); + } + +private: + + SRWLOCK m_rwLock; +}; + +#endif + +// +// Rename the lock class to a more clear name. +// +typedef CWSDRWLock READ_WRITE_LOCK; \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/stringa.cpp b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/stringa.cpp new file mode 100644 index 0000000000..29da773bca --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/stringa.cpp @@ -0,0 +1,1767 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "precomp.h" + +STRA::STRA( + VOID +) : m_cchLen( 0 ) +{ + *( QueryStr() ) = '\0'; +} + +STRA::STRA( + __inout_ecount(cchInit) CHAR* pbInit, + __in DWORD cchInit +) : m_Buff( pbInit, cchInit * sizeof( CHAR ) ), + m_cchLen(0) +/*++ + Description: + + Used by STACK_STRA. Initially populates underlying buffer with pbInit. + + pbInit is not freed. + + Arguments: + + pbInit - initial memory to use + cchInit - count, in characters, of pbInit + + Returns: + + None. + +--*/ +{ + _ASSERTE( NULL != pbInit ); + _ASSERTE( cchInit > 0 ); + _ASSERTE( pbInit[0] == '\0' ); +} + +BOOL +STRA::IsEmpty( + VOID +) const +{ + return ( m_cchLen == 0 ); +} + +BOOL +STRA::Equals( + __in PCSTR pszRhs, + __in BOOL fIgnoreCase /*= FALSE*/ +) const +{ + _ASSERTE( NULL != pszRhs ); + + if( fIgnoreCase ) + { + return ( 0 == _stricmp( QueryStr(), pszRhs ) ); + } + + return ( 0 == strcmp( QueryStr(), pszRhs ) ); +} + +BOOL +STRA::Equals( + __in const STRA * pstrRhs, + __in BOOL fIgnoreCase /*= FALSE*/ +) const +{ + _ASSERTE( NULL != pstrRhs ); + return Equals( pstrRhs->QueryStr(), fIgnoreCase ); +} + +BOOL +STRA::Equals( + __in const STRA & strRhs, + __in BOOL fIgnoreCase /*= FALSE*/ +) const +{ + return Equals( strRhs.QueryStr(), fIgnoreCase ); +} + +DWORD +STRA::QueryCB( + VOID +) const +// +// Returns the number of bytes in the string excluding the terminating NULL +// +{ + return m_cchLen * sizeof( CHAR ); +} + +DWORD +STRA::QueryCCH( + VOID +) const +// +// Returns the number of characters in the string excluding the terminating NULL +// +{ + return m_cchLen; +} + +DWORD +STRA::QuerySizeCCH( + VOID +) const +// +// Returns size of the underlying storage buffer, in characters +// +{ + return m_Buff.QuerySize() / sizeof( CHAR ); +} + +DWORD +STRA::QuerySize( + VOID +) const +// +// Returns the size of the storage buffer in bytes +// +{ + return m_Buff.QuerySize(); +} + +__nullterminated +__bcount(this->m_cchLen) +CHAR * +STRA::QueryStr( + VOID +) const +// +// Return the string buffer +// +{ + return m_Buff.QueryPtr(); +} + +VOID +STRA::Reset( + VOID +) +// +// Resets the internal string to be NULL string. Buffer remains cached. +// +{ + _ASSERTE( QueryStr() != NULL ); + *(QueryStr()) = '\0'; + m_cchLen = 0; +} + +HRESULT +STRA::Resize( + __in DWORD cchSize +) +{ + if( !m_Buff.Resize( cchSize * sizeof( CHAR ) ) ) + { + return E_OUTOFMEMORY; + } + + return S_OK; +} + +HRESULT +STRA::SyncWithBuffer( + VOID +) +// +// Recalculate the length of the string, etc. because we've modified +// the buffer directly. +// +{ + HRESULT hr; + size_t size; + hr = StringCchLengthA( QueryStr(), + QuerySizeCCH(), + &size ); + if ( SUCCEEDED( hr ) ) + { + m_cchLen = static_cast(size); + } + return hr; +} + +HRESULT +STRA::Copy( + __in PCSTR pszCopy +) +{ + HRESULT hr; + size_t cbLen; + hr = StringCbLengthA( pszCopy, + STRSAFE_MAX_CCH, + &cbLen ); + if ( FAILED( hr ) ) + { + return hr; + } + return Copy( pszCopy, cbLen ); +} + + +HRESULT +STRA::Copy( + __in_ecount(cchLen) + PCSTR pszCopy, + __in SIZE_T cbLen +) +// +// Copy the contents of another string to this one +// +{ + _ASSERTE( cbLen <= MAXDWORD ); + + return AuxAppend( + pszCopy, + static_cast(cbLen), + 0 + ); +} + +HRESULT +STRA::Copy( + __in const STRA * pstrRhs +) +{ + _ASSERTE( pstrRhs != NULL ); + return Copy( pstrRhs->QueryStr(), pstrRhs->QueryCCH() ); +} + +HRESULT +STRA::Copy( + __in const STRA & strRhs +) +{ + return Copy( strRhs.QueryStr(), strRhs.QueryCCH() ); +} + +HRESULT +STRA::CopyW( + __in PCWSTR pszCopyW +) +{ + HRESULT hr; + size_t cchLen; + hr = StringCchLengthW( pszCopyW, + STRSAFE_MAX_CCH, + &cchLen ); + if ( FAILED( hr ) ) + { + return hr; + } + return CopyW( pszCopyW, cchLen ); +} + +HRESULT +STRA::CopyWTruncate( + __in PCWSTR pszCopyWTruncate +) +{ + HRESULT hr; + size_t cchLen; + hr = StringCchLengthW( pszCopyWTruncate, + STRSAFE_MAX_CCH, + &cchLen ); + if ( FAILED( hr ) ) + { + return hr; + } + return CopyWTruncate( pszCopyWTruncate, cchLen ); +} + +HRESULT +STRA::CopyWTruncate( + __in_ecount(cchLen) + PCWSTR pszCopyWTruncate, + __in SIZE_T cchLen +) +// +// The "Truncate" methods do not do proper conversion. They do a (CHAR) caste +// +{ + _ASSERTE( cchLen <= MAXDWORD ); + + return AuxAppendWTruncate( + pszCopyWTruncate, + static_cast(cchLen), + 0 + ); +} + +HRESULT +STRA::Append( + __in PCSTR pszAppend +) +{ + HRESULT hr; + size_t cbLen; + hr = StringCbLengthA( pszAppend, + STRSAFE_MAX_CCH, + &cbLen ); + if ( FAILED( hr ) ) + { + return hr; + } + return Append( pszAppend, cbLen ); +} + +HRESULT +STRA::Append( + __in_ecount(cchLen) + PCSTR pszAppend, + __in SIZE_T cbLen +) +{ + _ASSERTE( cbLen <= MAXDWORD ); + if ( cbLen == 0 ) + { + return S_OK; + } + return AuxAppend( + pszAppend, + static_cast(cbLen), + QueryCB() + ); +} + +HRESULT +STRA::Append( + __in const STRA * pstrRhs +) +{ + _ASSERTE( pstrRhs != NULL ); + return Append( pstrRhs->QueryStr(), pstrRhs->QueryCCH() ); +} + +HRESULT +STRA::Append( + __in const STRA & strRhs +) +{ + return Append( strRhs.QueryStr(), strRhs.QueryCCH() ); +} + +HRESULT +STRA::AppendWTruncate( + __in PCWSTR pszAppendWTruncate +) +{ + HRESULT hr; + size_t cchLen; + hr = StringCchLengthW( pszAppendWTruncate, + STRSAFE_MAX_CCH, + &cchLen ); + if ( FAILED( hr ) ) + { + return hr; + } + return AppendWTruncate( pszAppendWTruncate, cchLen ); +} + +HRESULT +STRA::AppendWTruncate( + __in_ecount(cchLen) + PCWSTR pszAppendWTruncate, + __in SIZE_T cchLen +) +// +// The "Truncate" methods do not do proper conversion. They do a (CHAR) caste +// +{ + _ASSERTE( cchLen <= MAXDWORD ); + if ( cchLen == 0 ) + { + return S_OK; + } + return AuxAppendWTruncate( + pszAppendWTruncate, + static_cast(cchLen), + QueryCB() + ); +} + +HRESULT +STRA::CopyToBuffer( + __out_bcount(*pcb) CHAR* pszBuffer, + __inout DWORD * pcb +) const +// +// Makes a copy of the stored string into the given buffer +// +{ + _ASSERTE( NULL != pszBuffer ); + _ASSERTE( NULL != pcb ); + + HRESULT hr = S_OK; + DWORD cbNeeded = QueryCB() + sizeof( CHAR ); + + if( *pcb < cbNeeded ) + { + hr = HRESULT_FROM_WIN32( ERROR_INSUFFICIENT_BUFFER ); + goto Finished; + } + + memcpy( pszBuffer, QueryStr(), cbNeeded ); + +Finished: + + *pcb = cbNeeded; + + return hr; +} + +HRESULT +STRA::SetLen( + __in DWORD cchLen +) +/*++ + * +Routine Description: + + Set the length of the string and null terminate, if there + is sufficient buffer already allocated. Will not reallocate. + +Arguments: + + cchLen - The number of characters in the new string. + +Return Value: + + HRESULT + +--*/ +{ + if( cchLen >= QuerySizeCCH() ) + { + return HRESULT_FROM_WIN32( ERROR_INVALID_PARAMETER ); + } + + *( QueryStr() + cchLen ) = '\0'; + m_cchLen = cchLen; + + return S_OK; +} + + +HRESULT +STRA::SafeSnprintf( + __in __format_string + PCSTR pszFormatString, + ... +) +/*++ + +Routine Description: + + Writes to a STRA, growing it as needed. It arbitrarily caps growth at 64k chars. + +Arguments: + + pszFormatString - printf format + ... - printf args + +Return Value: + + HRESULT + +--*/ +{ + HRESULT hr = S_OK; + va_list argsList; + va_start( argsList, pszFormatString ); + + hr = SafeVsnprintf(pszFormatString, argsList); + + va_end( argsList ); + return hr; +} + +HRESULT +STRA::SafeVsnprintf( + __in __format_string + PCSTR pszFormatString, + va_list argsList +) +/*++ + +Routine Description: + + Writes to a STRA, growing it as needed. It arbitrarily caps growth at 64k chars. + +Arguments: + + pszFormatString - printf format + argsList - printf va_list + +Return Value: + + HRESULT + +--*/ +{ + HRESULT hr = S_OK; + int cchOutput; + int cchNeeded; + + // + // Format the incoming message using vsnprintf() + // so that the overflows are captured + // + cchOutput = _vsnprintf_s( + QueryStr(), + QuerySizeCCH(), + QuerySizeCCH() - 1, + pszFormatString, + argsList + ); + + if( cchOutput == -1 ) + { + // + // Couldn't fit this in the original STRU size. + // + cchNeeded = _vscprintf( pszFormatString, argsList ); + if( cchNeeded > 64 * 1024 ) + { + // + // If we're trying to produce a string > 64k chars, then + // there is probably a problem + // + hr = HRESULT_FROM_WIN32( ERROR_INVALID_DATA ); + goto Finished; + } + + // + // _vscprintf doesn't include terminating null character + // + cchNeeded++; + + hr = Resize( cchNeeded ); + if( FAILED( hr ) ) + { + goto Finished; + } + + cchOutput = _vsnprintf_s( + QueryStr(), + QuerySizeCCH(), + QuerySizeCCH() - 1, + pszFormatString, + argsList + ); + if( -1 == cchOutput ) + { + // + // This should never happen, cause we should already have correctly sized memory + // + _ASSERTE( FALSE ); + + hr = HRESULT_FROM_WIN32( ERROR_INVALID_DATA ); + goto Finished; + } + } + + // + // always null terminate at the last WCHAR + // + QueryStr()[ QuerySizeCCH() - 1 ] = L'\0'; + + // + // we directly touched the buffer - therefore: + // + hr = SyncWithBuffer(); + if( FAILED( hr ) ) + { + goto Finished; + } + +Finished: + + if( FAILED( hr ) ) + { + Reset(); + } + + return hr; +} + +bool +FShouldEscapeUtf8( + BYTE ch + ) +{ + if ( ( ch >= 128 ) ) + { + return true; + } + + return false; +} + +bool +FShouldEscapeUrl( + BYTE ch + ) +{ + if ( ( ch >= 128 || + ch <= 32 || + ch == '<' || + ch == '>' || + ch == '%' || + ch == '?' || + ch == '#' ) && + !( ch == '\n' || ch == '\r' ) ) + { + return true; + } + + return false; +} + +HRESULT +STRA::Escape( + VOID +) +/*++ + +Routine Description: + + Escapes a STRA + +Arguments: + + None + +Return Value: + + None + +--*/ +{ + return EscapeInternal( FShouldEscapeUrl ); +} + +HRESULT +STRA::EscapeUtf8( + VOID +) +/*++ + +Routine Description: + + Escapes the high-bit chars in a STRA. LWS, CR, LF & controls are untouched. + +Arguments: + + None + +Return Value: + + None + +--*/ +{ + return EscapeInternal( FShouldEscapeUtf8 ); +} + + +HRESULT +STRA::EscapeInternal( + PFN_F_SHOULD_ESCAPE pfnFShouldEscape +) +/*++ + +Routine Description: + + Escapes a STRA according to the predicate function passed in + +Arguments: + + None + +Return Value: + + None + +--*/ +{ + LPCSTR pch = QueryStr(); + __analysis_assume( pch != NULL ); + int i = 0; + BYTE ch; + HRESULT hr = S_OK; + BOOL fRet = FALSE; + SIZE_T NewSize = 0; + + // Set to true if any % escaping occurs + BOOL fEscapingDone = FALSE; + + // + // If there are any characters that need to be escaped we copy the entire string + // character by character into straTemp, escaping as we go, then at the end + // copy all of straTemp over. Don't modify InlineBuffer directly. + // + CHAR InlineBuffer[512]; + InlineBuffer[0] = '\0'; + STRA straTemp(InlineBuffer, sizeof(InlineBuffer)/sizeof(*InlineBuffer)); + + _ASSERTE( pch ); + + while (ch = pch[i]) + { + // + // Escape characters that are in the non-printable range + // but ignore CR and LF + // + + if ( pfnFShouldEscape( ch ) ) + { + if (FALSE == fEscapingDone) + { + // first character in the string that needed escaping + fEscapingDone = TRUE; + + // guess that the size needs to be larger than + // what we used to have times two + NewSize = QueryCCH() * 2; + if ( NewSize > MAXDWORD ) + { + hr = HRESULT_FROM_WIN32( ERROR_ARITHMETIC_OVERFLOW ); + return hr; + } + + hr = straTemp.Resize( static_cast(NewSize) ); + + if (FAILED(hr)) + { + return hr; + } + + // Copy all of the previous buffer into buffTemp, only if it is not the first character: + + if ( i > 0) + { + hr = straTemp.Copy(QueryStr(), + i * sizeof(CHAR)); + if (FAILED(hr)) + { + return hr; + } + } + } + + // resize the temporary (if needed) with the slop of the entire buffer length + // this fixes constant reallocation if the entire string needs to be escaped + NewSize = QueryCCH() + 2 * sizeof(CHAR) + 1 * sizeof(CHAR); + if ( NewSize > MAXDWORD ) + { + hr = HRESULT_FROM_WIN32( ERROR_ARITHMETIC_OVERFLOW ); + return hr; + } + + fRet = straTemp.m_Buff.Resize( NewSize ); + if ( !fRet ) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + return hr; + } + + // + // Create the string to append for the current character + // + + CHAR chHex[3]; + chHex[0] = '%'; + + // + // Convert the low then the high character to hex + // + + UINT nLowDigit = (UINT)(ch % 16); + chHex[2] = TODIGIT( nLowDigit ); + + ch /= 16; + + UINT nHighDigit = (UINT)(ch % 16); + + chHex[1] = TODIGIT( nHighDigit ); + + // + // Actually append the converted character to the end of the temporary + // + hr = straTemp.Append(chHex, 3); + if (FAILED(hr)) + { + return hr; + } + } + else + { + // if no escaping done, no need to copy + if (fEscapingDone) + { + // if ANY escaping done, copy current character into new buffer + straTemp.Append(&pch[i], 1); + } + } + + // inspect the next character in the string + i++; + } + + if (fEscapingDone) + { + // the escaped string is now in straTemp + hr = Copy(straTemp); + } + + return hr; + +} // EscapeInternal() + +VOID +STRA::Unescape( + VOID +) +/*++ + +Routine Description: + + Unescapes a STRA + + Supported escape sequences are: + %uxxxx unescapes Unicode character xxxx into system codepage + %xx unescapes character xx + % without following hex digits is ignored + +Arguments: + + None + +Return Value: + + None + +--*/ +{ + CHAR *pScan; + CHAR *pDest; + CHAR *pNextScan; + WCHAR wch; + DWORD dwLen; + BOOL fChanged = FALSE; + + // + // Now take care of any escape characters + // + pDest = pScan = strchr(QueryStr(), '%'); + + while (pScan) + { + if ((pScan[1] == 'u' || pScan[1] == 'U') && + SAFEIsXDigit(pScan[2]) && + SAFEIsXDigit(pScan[3]) && + SAFEIsXDigit(pScan[4]) && + SAFEIsXDigit(pScan[5])) + { + wch = TOHEX(pScan[2]) * 4096 + TOHEX(pScan[3]) * 256 + + TOHEX(pScan[4]) * 16 + TOHEX(pScan[5]); + + dwLen = WideCharToMultiByte(CP_ACP, + WC_NO_BEST_FIT_CHARS, + &wch, + 1, + (LPSTR) pDest, + 6, + NULL, + NULL); + + pDest += dwLen; + pScan += 6; + fChanged = TRUE; + } + else if (SAFEIsXDigit(pScan[1]) && SAFEIsXDigit(pScan[2])) + { + *pDest = TOHEX(pScan[1]) * 16 + TOHEX(pScan[2]); + + pDest ++; + pScan += 3; + fChanged = TRUE; + } + else // Not an escaped char, just a '%' + { + if (fChanged) + { + *pDest = *pScan; + } + + pDest++; + pScan++; + } + + // + // Copy all the information between this and the next escaped char + // + pNextScan = strchr(pScan, '%'); + + if (fChanged) // pScan!=pDest, so we have to copy the char's + { + if (!pNextScan) // That was the last '%' in the string + { + memmove(pDest, + pScan, + QueryCCH() - DIFF(pScan - QueryStr()) + 1); + } + else + { + // There is another '%', move intermediate chars + if ((dwLen = (DWORD)DIFF(pNextScan - pScan)) != 0) + { + memmove(pDest, + pScan, + dwLen); + pDest += dwLen; + } + } + } + + pScan = pNextScan; + } + + if (fChanged) + { + m_cchLen = (DWORD)strlen(QueryStr()); // for safety recalc the length + } + + return; +} + +HRESULT +STRA::CopyWToUTF8Unescaped( + __in LPCWSTR cpchStr +) +{ + return STRA::CopyWToUTF8Unescaped(cpchStr, (DWORD) wcslen(cpchStr)); +} + +HRESULT +STRA::CopyWToUTF8Unescaped( + __in_ecount(cch) + LPCWSTR cpchStr, + __in DWORD cch +) +{ + HRESULT hr = S_OK; + int iRet; + + if (cch == 0) + { + Reset(); + return S_OK; + } + + iRet = ConvertUnicodeToUTF8(cpchStr, + &m_Buff, + cch); + if (-1 == iRet) + { + // could not convert + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + m_cchLen = iRet; + + _ASSERTE(strlen(m_Buff.QueryPtr()) == m_cchLen); +Finished: + return hr; +} + +HRESULT +STRA::CopyWToUTF8Escaped( + __in LPCWSTR cpchStr +) +{ + return STRA::CopyWToUTF8Escaped(cpchStr, (DWORD) wcslen(cpchStr)); +} + +HRESULT +STRA::CopyWToUTF8Escaped( + __in_ecount(cch) + LPCWSTR cpchStr, + __in DWORD cch +) +{ + HRESULT hr = S_OK; + + hr = CopyWToUTF8Unescaped(cpchStr, cch); + if (FAILED(hr)) + { + goto Finished; + } + + hr = Escape(); + if (FAILED(hr)) + { + goto Finished; + } + + hr = S_OK; +Finished: + return hr; +} + +HRESULT +STRA::AuxAppend( + __in_ecount(cbLen) + LPCSTR pStr, + __in DWORD cbLen, + __in DWORD cbOffset +) +{ + _ASSERTE( NULL != pStr ); + _ASSERTE( cbOffset <= QueryCB() ); + + ULONGLONG cb64NewSize = (ULONGLONG)cbOffset + cbLen + sizeof( CHAR ); + if( cb64NewSize > MAXDWORD ) + { + return HRESULT_FROM_WIN32( ERROR_ARITHMETIC_OVERFLOW ); + } + + if( m_Buff.QuerySize() < cb64NewSize ) + { + if( !m_Buff.Resize( static_cast(cb64NewSize) ) ) + { + return E_OUTOFMEMORY; + } + } + + memcpy( reinterpret_cast(m_Buff.QueryPtr()) + cbOffset, pStr, cbLen ); + + m_cchLen = cbLen + cbOffset; + + *( QueryStr() + m_cchLen ) = '\0'; + + return S_OK; +} + +HRESULT +STRA::AuxAppendW( + __in_ecount(cchAppendW) + PCWSTR pszAppendW, + __in DWORD cchAppendW, + __in DWORD cbOffset, + __in UINT CodePage, + __in BOOL fFailIfNoTranslation, + __in DWORD dwFlags +) +{ + HRESULT hr = S_OK; + DWORD cbAvailable = 0; + DWORD cbRet = 0; + + // + // There are only two expect places to append + // + _ASSERTE( 0 == cbOffset || QueryCB() == cbOffset ); + + if ( cchAppendW == 0 ) + { + goto Finished; + } + + // + // start by assuming 1 char to 1 char will be enough space + // + if( !m_Buff.Resize( cbOffset + cchAppendW + sizeof( CHAR ) ) ) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + cbAvailable = m_Buff.QuerySize() - cbOffset; + + cbRet = WideCharToMultiByte( + CodePage, + dwFlags, + pszAppendW, + cchAppendW, + QueryStr() + cbOffset, + cbAvailable, + NULL, + NULL + ); + if( 0 != cbRet ) + { + if(!m_Buff.Resize(cbOffset + cbRet + 1)) + { + hr = E_OUTOFMEMORY; + } + + // + // not zero --> success, so we're done + // + goto Finished; + } + + // + // We only know how to handle ERROR_INSUFFICIENT_BUFFER + // + hr = HRESULT_FROM_WIN32( GetLastError() ); + if( hr != HRESULT_FROM_WIN32( ERROR_INSUFFICIENT_BUFFER ) ) + { + goto Finished; + } + + // + // Reset HResult because we need to get the number of bytes needed + // + hr = S_OK; + cbRet = WideCharToMultiByte( + CodePage, + dwFlags, + pszAppendW, + cchAppendW, + NULL, + 0, + NULL, + NULL + ); + if( 0 == cbRet ) + { + // + // no idea how we could ever reach here + // + hr = HRESULT_FROM_WIN32( GetLastError() ); + goto Finished; + } + + if( !m_Buff.Resize( cbOffset + cbRet + 1) ) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + cbAvailable = m_Buff.QuerySize() - cbOffset; + + cbRet = WideCharToMultiByte( + CodePage, + dwFlags, + pszAppendW, + cchAppendW, + QueryStr() + cbOffset, + cbAvailable, + NULL, + NULL + ); + if( 0 == cbRet ) + { + hr = HRESULT_FROM_WIN32( GetLastError() ); + goto Finished; + } + +Finished: + + if( SUCCEEDED( hr ) && 0 != cbRet ) + { + m_cchLen = cbRet + cbOffset; + } + + // + // ensure we're still NULL terminated in the right spot + // (regardless of success or failure) + // + QueryStr()[m_cchLen] = '\0'; + + return hr; +} + +HRESULT +STRA::AuxAppendWTruncate( + __in_ecount(cchAppendW) + __in PCWSTR pszAppendW, + __in DWORD cchAppendW, + __in DWORD cbOffset +) +// +// Cheesey WCHAR --> CHAR conversion +// +{ + HRESULT hr = S_OK; + CHAR* pszBuffer; + + _ASSERTE( NULL != pszAppendW ); + _ASSERTE( 0 == cbOffset || cbOffset == QueryCB() ); + + if( !pszAppendW ) + { + hr = HRESULT_FROM_WIN32( ERROR_INVALID_PARAMETER ); + goto Finished; + } + + ULONGLONG cbNeeded = (ULONGLONG)cbOffset + cchAppendW + sizeof( CHAR ); + if( cbNeeded > MAXDWORD ) + { + hr = HRESULT_FROM_WIN32( ERROR_ARITHMETIC_OVERFLOW ); + goto Finished; + } + + if( !m_Buff.Resize( static_cast(cbNeeded) ) ) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + // + // Copy/convert the UNICODE string over (by making two bytes into one) + // + pszBuffer = QueryStr() + cbOffset; + for( DWORD i = 0; i < cchAppendW; i++ ) + { + pszBuffer[i] = static_cast(pszAppendW[i]); + } + + m_cchLen = cchAppendW + cbOffset; + *( QueryStr() + m_cchLen ) = '\0'; + +Finished: + + return hr; +} + +// static +int +STRA::ConvertUnicodeToCodePage( + __in_ecount(dwStringLen) + LPCWSTR pszSrcUnicodeString, + __inout BUFFER_T * pbufDstAnsiString, + __in DWORD dwStringLen, + __in UINT uCodePage +) +{ + _ASSERTE(NULL != pszSrcUnicodeString); + _ASSERTE(NULL != pbufDstAnsiString); + + BOOL bTemp; + int iStrLen = 0; + DWORD dwFlags; + + if (uCodePage == CP_ACP) + { + dwFlags = WC_NO_BEST_FIT_CHARS; + } + else + { + dwFlags = 0; + } + + iStrLen = WideCharToMultiByte(uCodePage, + dwFlags, + pszSrcUnicodeString, + dwStringLen, + (LPSTR)pbufDstAnsiString->QueryPtr(), + (int)pbufDstAnsiString->QuerySize(), + NULL, + NULL); + if ((iStrLen == 0) && (GetLastError() == ERROR_INSUFFICIENT_BUFFER)) { + iStrLen = WideCharToMultiByte(uCodePage, + dwFlags, + pszSrcUnicodeString, + dwStringLen, + NULL, + 0, + NULL, + NULL); + if (iStrLen != 0) { + // add one just for the extra NULL + bTemp = pbufDstAnsiString->Resize(iStrLen + 1); + if (!bTemp) + { + iStrLen = 0; + } + else + { + iStrLen = WideCharToMultiByte(uCodePage, + dwFlags, + pszSrcUnicodeString, + dwStringLen, + (LPSTR)pbufDstAnsiString->QueryPtr(), + (int)pbufDstAnsiString->QuerySize(), + NULL, + NULL); + } + + } + } + + if (0 != iStrLen && + pbufDstAnsiString->Resize(iStrLen + 1)) + { + // insert a terminating NULL into buffer for the dwStringLen+1 in the case that the dwStringLen+1 was not a NULL. + ((CHAR*)pbufDstAnsiString->QueryPtr())[iStrLen] = '\0'; + } + else + { + iStrLen = -1; + } + + return iStrLen; +} + +// static +HRESULT +STRA::ConvertUnicodeToMultiByte( + __in_ecount(dwStringLen) + LPCWSTR pszSrcUnicodeString, + __in BUFFER_T * pbufDstAnsiString, + __in DWORD dwStringLen +) +{ + return ConvertUnicodeToCodePage( pszSrcUnicodeString, + pbufDstAnsiString, + dwStringLen, + CP_ACP ); +} + +// static +HRESULT +STRA::ConvertUnicodeToUTF8( + __in_ecount(dwStringLen) + LPCWSTR pszSrcUnicodeString, + __in BUFFER_T * pbufDstAnsiString, + __in DWORD dwStringLen +) +{ + return ConvertUnicodeToCodePage( pszSrcUnicodeString, + pbufDstAnsiString, + dwStringLen, + CP_UTF8 ); +} + +/*++ + +Routine Description: + + Removes leading and trailing whitespace + +--*/ + +VOID +STRA::Trim() +{ + PSTR pszString = QueryStr(); + DWORD cchNewLength = m_cchLen; + DWORD cchLeadingWhitespace = 0; + DWORD cchTempLength = 0; + + for (LONG ixString = m_cchLen - 1; ixString >= 0; ixString--) + { + if (isspace((unsigned char) pszString[ixString]) != 0) + { + pszString[ixString] = '\0'; + cchNewLength--; + } + else + { + break; + } + } + + cchTempLength = cchNewLength; + for (DWORD ixString = 0; ixString < cchTempLength; ixString++) + { + if (isspace((unsigned char) pszString[ixString]) != 0) + { + cchLeadingWhitespace++; + cchNewLength--; + } + else + { + break; + } + } + + if (cchNewLength == 0) + { + + Reset(); + } + else if (cchLeadingWhitespace > 0) + { + memmove(pszString, pszString + cchLeadingWhitespace, cchNewLength * sizeof(CHAR)); + pszString[cchNewLength] = '\0'; + } + + SyncWithBuffer(); +} + +/*++ + +Routine Description: + + Compares the string to the provided prefix to check for equality + +Arguments: + + pStraPrefix - string to compare with + fIgnoreCase - indicates whether the string comparison should be case-sensitive + +Return Value: + + TRUE if prefix string matches with internal string, FALSE otherwise + +--*/ +BOOL +STRA::StartsWith( + __in const STRA * pStraPrefix, + __in bool fIgnoreCase) const +{ + _ASSERTE( pStraPrefix != NULL ); + return StartsWith(pStraPrefix->QueryStr(), fIgnoreCase); +} + +/*++ + +Routine Description: + + Compares the string to the provided prefix to check for equality + +Arguments: + + straPrefix - string to compare with + fIgnoreCase - indicates whether the string comparison should be case-sensitive + +Return Value: + + TRUE if prefix string matches with internal string, FALSE otherwise + +--*/ +BOOL +STRA::StartsWith( + __in const STRA & straPrefix, + __in bool fIgnoreCase) const +{ + return StartsWith(straPrefix.QueryStr(), fIgnoreCase); +} + +/*++ + +Routine Description: + + Compares the string to the provided prefix to check for equality + +Arguments: + + pszPrefix - string to compare with + fIgnoreCase - indicates whether the string comparison should be case-sensitive + +Return Value: + + TRUE if prefix string matches with internal string, FALSE otherwise + +--*/ +BOOL +STRA::StartsWith( + __in PCSTR pszPrefix, + __in bool fIgnoreCase) const +{ + HRESULT hr = S_OK; + BOOL fMatch = FALSE; + size_t cchPrefix = 0; + + if (pszPrefix == NULL) + { + goto Finished; + } + + hr = StringCchLengthA( pszPrefix, + STRSAFE_MAX_CCH, + &cchPrefix ); + if (FAILED(hr)) + { + goto Finished; + } + + _ASSERTE( cchPrefix <= MAXDWORD ); + + if (cchPrefix > m_cchLen) + { + goto Finished; + } + + if( fIgnoreCase ) + { + fMatch = ( 0 == _strnicmp( QueryStr(), pszPrefix, cchPrefix ) ); + } + else + { + fMatch = ( 0 == strncmp( QueryStr(), pszPrefix, cchPrefix ) ); + } + + +Finished: + + return fMatch; +} + +/*++ + +Routine Description: + + Compares the string to the provided suffix to check for equality + +Arguments: + + pStraSuffix - string to compare with + fIgnoreCase - indicates whether the string comparison should be case-sensitive + +Return Value: + + TRUE if suffix string matches with internal string, FALSE otherwise + +--*/ +BOOL +STRA::EndsWith( + __in const STRA * pStraSuffix, + __in bool fIgnoreCase) const +{ + _ASSERTE( pStraSuffix != NULL ); + return EndsWith(pStraSuffix->QueryStr(), fIgnoreCase); +} + + +/*++ + +Routine Description: + + Compares the string to the provided suffix to check for equality + +Arguments: + + straSuffix - string to compare with + fIgnoreCase - indicates whether the string comparison should be case-sensitive + +Return Value: + + TRUE if suffix string matches with internal string, FALSE otherwise + +--*/ +BOOL +STRA::EndsWith( + __in const STRA & straSuffix, + __in bool fIgnoreCase) const +{ + return EndsWith(straSuffix.QueryStr(), fIgnoreCase); +} + + +/*++ + +Routine Description: + + Compares the string to the provided suffix to check for equality + +Arguments: + + pszSuffix - string to compare with + fIgnoreCase - indicates whether the string comparison should be case-sensitive + +Return Value: + + TRUE if suffix string matches with internal string, FALSE otherwise + +--*/ +BOOL +STRA::EndsWith( + __in PCSTR pszSuffix, + __in bool fIgnoreCase) const +{ + HRESULT hr = S_OK; + PSTR pszString = QueryStr(); + BOOL fMatch = FALSE; + size_t cchSuffix = 0; + ptrdiff_t ixOffset = 0; + + if (pszSuffix == NULL) + { + goto Finished; + } + + hr = StringCchLengthA( pszSuffix, + STRSAFE_MAX_CCH, + &cchSuffix ); + if (FAILED(hr)) + { + goto Finished; + } + + _ASSERTE( cchSuffix <= MAXDWORD ); + + if (cchSuffix > m_cchLen) + { + goto Finished; + } + + ixOffset = m_cchLen - cchSuffix; + _ASSERTE(ixOffset >= 0 && ixOffset <= MAXDWORD); + + if( fIgnoreCase ) + { + fMatch = ( 0 == _strnicmp( pszString + ixOffset, pszSuffix, cchSuffix ) ); + } + else + { + fMatch = ( 0 == strncmp( pszString + ixOffset, pszSuffix, cchSuffix ) ); + } + +Finished: + + return fMatch; +} + + +/*++ + +Routine Description: + + Searches the string for the first occurrence of the specified character. + +Arguments: + + charValue - character to find + dwStartIndex - the initial index. + +Return Value: + + The index for the first character occurence in the string. + + -1 if not found. + +--*/ +INT +STRA::IndexOf( + __in CHAR charValue, + __in DWORD dwStartIndex + ) const +{ + INT nIndex = -1; + + // Make sure that there are no buffer overruns. + if( dwStartIndex >= QueryCCH() ) + { + goto Finished; + } + + const CHAR* pChar = strchr( QueryStr() + dwStartIndex, charValue ); + + // Determine the index if found + if( pChar ) + { + // nIndex will be set to -1 on failure. + (VOID)SizeTToInt( pChar - QueryStr(), &nIndex ); + } + +Finished: + + return nIndex; +} + + +/*++ + +Routine Description: + + Searches the string for the first occurrence of the specified substring. + +Arguments: + + pszValue - substring to find + dwStartIndex - initial index. + +Return Value: + + The index for the first character occurence in the string. + + -1 if not found. + +--*/ +INT +STRA::IndexOf( + __in PCSTR pszValue, + __in DWORD dwStartIndex + ) const +{ + HRESULT hr = S_OK; + INT nIndex = -1; + SIZE_T cchValue = 0; + + // Validate input parameters + if( dwStartIndex >= QueryCCH() || !pszValue ) + { + goto Finished; + } + + const CHAR* pChar = strstr( QueryStr() + dwStartIndex, pszValue ); + + // Determine the index if found + if( pChar ) + { + // nIndex will be set to -1 on failure. + (VOID)SizeTToInt( pChar - QueryStr(), &nIndex ); + } + +Finished: + + return nIndex; +} + + +/*++ + +Routine Description: + + Searches the string for the last occurrence of the specified character. + +Arguments: + + charValue - character to find + dwStartIndex - initial index. + +Return Value: + + The index for the last character occurence in the string. + + -1 if not found. + +--*/ +INT +STRA::LastIndexOf( + __in CHAR charValue, + __in DWORD dwStartIndex + ) const +{ + INT nIndex = -1; + + // Make sure that there are no buffer overruns. + if( dwStartIndex >= QueryCCH() ) + { + goto Finished; + } + + const CHAR* pChar = strrchr( QueryStr() + dwStartIndex, charValue ); + + // Determine the index if found + if( pChar ) + { + // nIndex will be set to -1 on failure. + (VOID)SizeTToInt( pChar - QueryStr(), &nIndex ); + } + +Finished: + + return nIndex; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/stringa.h b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/stringa.h new file mode 100644 index 0000000000..39737f4a69 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/stringa.h @@ -0,0 +1,515 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "buffer.h" +#include "macros.h" +#include + +class STRA +{ + +public: + + STRA( + VOID + ); + + STRA( + __inout_ecount(cchInit) CHAR* pbInit, + __in DWORD cchInit + ); + + BOOL + IsEmpty( + VOID + ) const; + + BOOL + Equals( + __in PCSTR pszRhs, + __in BOOL fIgnoreCase = FALSE + ) const; + + BOOL + Equals( + __in const STRA * pstrRhs, + __in BOOL fIgnoreCase = FALSE + ) const; + + BOOL + Equals( + __in const STRA & strRhs, + __in BOOL fIgnoreCase = FALSE + ) const; + + static + BOOL + Equals( + __in PCSTR pszLhs, + __in PCSTR pszRhs, + __in bool fIgnoreCase = false + ) + { + // Return FALSE if either or both strings are NULL. + if (!pszLhs || !pszRhs) return FALSE; + + if( fIgnoreCase ) + { + return ( 0 == _stricmp( pszLhs, pszRhs ) ); + } + + return ( 0 == strcmp( pszLhs, pszRhs ) ); + } + + VOID + Trim(); + + BOOL + StartsWith( + __in const STRA * pStraPrefix, + __in bool fIgnoreCase = FALSE + ) const; + + BOOL + StartsWith( + __in const STRA & straPrefix, + __in bool fIgnoreCase = FALSE + ) const; + + BOOL + StartsWith( + __in PCSTR pszPrefix, + __in bool fIgnoreCase = FALSE + ) const; + + BOOL + EndsWith( + __in const STRA * pStraSuffix, + __in bool fIgnoreCase = FALSE + ) const; + + BOOL + EndsWith( + __in const STRA & straSuffix, + __in bool fIgnoreCase = FALSE + ) const; + + BOOL + EndsWith( + __in PCSTR pszSuffix, + __in bool fIgnoreCase = FALSE + ) const; + + INT + IndexOf( + __in CHAR charValue, + __in DWORD dwStartIndex = 0 + ) const; + + INT + IndexOf( + __in PCSTR pszValue, + __in DWORD dwStartIndex = 0 + ) const; + + INT + LastIndexOf( + __in CHAR charValue, + __in DWORD dwStartIndex = 0 + ) const; + + DWORD + QueryCB( + VOID + ) const; + + DWORD + QueryCCH( + VOID + ) const; + + DWORD + QuerySizeCCH( + VOID + ) const; + + DWORD + QuerySize( + VOID + ) const; + + __nullterminated + __bcount(this->m_cchLen) + CHAR * + QueryStr( + VOID + ) const; + + VOID + Reset( + VOID + ); + + HRESULT + Resize( + __in DWORD cchSize + ); + + HRESULT + SyncWithBuffer( + VOID + ); + + HRESULT + Copy( + __in PCSTR pszCopy + ); + + HRESULT + Copy( + __in_ecount(cbLen) + PCSTR pszCopy, + __in SIZE_T cbLen + ); + + HRESULT + Copy( + __in const STRA * pstrRhs + ); + + HRESULT + Copy( + __in const STRA & strRhs + ); + + HRESULT + CopyW( + __in PCWSTR pszCopyW + ); + + HRESULT + CopyW( + __in_ecount(cchLen) + PCWSTR pszCopyW, + __in SIZE_T cchLen, + __in UINT CodePage = CP_UTF8, + __in BOOL fFailIfNoTranslation = FALSE + ) + { + _ASSERTE( cchLen <= MAXDWORD ); + + return AuxAppendW( + pszCopyW, + static_cast(cchLen), + 0, + CodePage, + fFailIfNoTranslation + ); + } + + HRESULT + CopyWTruncate( + __in PCWSTR pszCopyWTruncate + ); + + HRESULT + CopyWTruncate( + __in_ecount(cchLen) + PCWSTR pszCopyWTruncate, + __in SIZE_T cchLen + ); + + HRESULT + Append( + __in PCSTR pszAppend + ); + + HRESULT + Append( + __in_ecount(cbLen) + PCSTR pszAppend, + __in SIZE_T cbLen + ); + + HRESULT + Append( + __in const STRA * pstrRhs + ); + + HRESULT + Append( + __in const STRA & strRhs + ); + + HRESULT + AppendW( + __in PCWSTR pszAppendW + ) + { + HRESULT hr; + size_t cchLen; + hr = StringCchLengthW( pszAppendW, + STRSAFE_MAX_CCH, + &cchLen ); + if ( FAILED( hr ) ) + { + return hr; + } + return AppendW( pszAppendW, cchLen ); + } + + HRESULT + AppendW( + __in_ecount(cchLen) + PCWSTR pszAppendW, + __in SIZE_T cchLen, + __in UINT CodePage = CP_UTF8, + __in BOOL fFailIfNoTranslation = FALSE + ) + { + _ASSERTE( cchLen <= MAXDWORD ); + if ( cchLen == 0 ) + { + return S_OK; + } + return AuxAppendW( + pszAppendW, + static_cast(cchLen), + QueryCB(), + CodePage, + fFailIfNoTranslation + ); + } + + HRESULT + AppendWTruncate( + __in PCWSTR pszAppendWTruncate + ); + + HRESULT + AppendWTruncate( + __in_ecount(cchLen) + PCWSTR pszAppendWTruncate, + __in SIZE_T cchLen + ); + + HRESULT + CopyToBuffer( + __out_bcount(*pcb) CHAR* pszBuffer, + __inout DWORD * pcb + ) const; + + HRESULT + SetLen( + __in DWORD cchLen + ); + + HRESULT + SafeSnprintf( + __in __format_string + PCSTR pszFormatString, + ... + ); + + HRESULT + SafeVsnprintf( + __in __format_string + PCSTR pszFormatString, + va_list argsList + ); + + HRESULT + Escape( + VOID + ); + + HRESULT + EscapeUtf8( + VOID + ); + + VOID + Unescape( + VOID + ); + + HRESULT + CopyWToUTF8Unescaped( + __in LPCWSTR cpchStr + ); + + HRESULT + CopyWToUTF8Unescaped( + __in_ecount(cch) + LPCWSTR cpchStr, + __in DWORD cch + ); + + HRESULT + CopyWToUTF8Escaped( + __in LPCWSTR cpchStr + ); + + HRESULT + CopyWToUTF8Escaped( + __in_ecount(cch) + LPCWSTR cpchStr, + __in DWORD cch + ); + +private: + + // + // Avoid C++ errors. This object should never go through a copy + // constructor, unintended cast or assignment. + // + STRA( const STRA &); + STRA & operator = (const STRA &); + + HRESULT + AuxAppend( + __in_ecount(cbLen) + LPCSTR pStr, + __in DWORD cbLen, + __in DWORD cbOffset + ); + + HRESULT + AuxAppendW( + __in_ecount(cchAppendW) + PCWSTR pszAppendW, + __in DWORD cchAppendW, + __in DWORD cbOffset, + __in UINT CodePage, + __in BOOL fFailIfNoTranslation + ) + { + DWORD dwFlags = 0; + + if( CP_ACP == CodePage ) + { + dwFlags = WC_NO_BEST_FIT_CHARS; + } + else if( fFailIfNoTranslation && CodePage == CP_UTF8 ) + { + // + // WC_ERR_INVALID_CHARS is only supported in Longhorn or greater. + // +#if defined( NTDDI_VERSION ) && NTDDI_VERSION >= NTDDI_LONGHORN + dwFlags |= WC_ERR_INVALID_CHARS; +#else + UNREFERENCED_PARAMETER(fFailIfNoTranslation); +#endif + } + + return AuxAppendW( pszAppendW, + cchAppendW, + cbOffset, + CodePage, + fFailIfNoTranslation, + dwFlags ); + } + + HRESULT + AuxAppendW( + __in_ecount(cchAppendW) + PCWSTR pszAppendW, + __in DWORD cchAppendW, + __in DWORD cbOffset, + __in UINT CodePage, + __in BOOL fFailIfNoTranslation, + __in DWORD dwFlags + ); + + HRESULT + AuxAppendWTruncate( + __in_ecount(cchAppendW) + __in PCWSTR pszAppendW, + __in DWORD cchAppendW, + __in DWORD cbOffset + ); + + static + int + ConvertUnicodeToCodePage( + __in_ecount(dwStringLen) + LPCWSTR pszSrcUnicodeString, + __inout BUFFER_T * pbufDstAnsiString, + __in DWORD dwStringLen, + __in UINT uCodePage + ); + + static + HRESULT + ConvertUnicodeToMultiByte( + __in_ecount(dwStringLen) + LPCWSTR pszSrcUnicodeString, + __in BUFFER_T * pbufDstAnsiString, + __in DWORD dwStringLen + ); + + static + HRESULT + ConvertUnicodeToUTF8( + __in_ecount(dwStringLen) + LPCWSTR pszSrcUnicodeString, + __in BUFFER_T * pbufDstAnsiString, + __in DWORD dwStringLen + ); + + typedef bool (* PFN_F_SHOULD_ESCAPE)(BYTE ch); + + HRESULT + EscapeInternal( + PFN_F_SHOULD_ESCAPE pfnFShouldEscape + ); + + // + // Buffer with an inline buffer of 1, + // enough to hold null-terminating character. + // + BUFFER_T m_Buff; + DWORD m_cchLen; +}; + +inline +HRESULT +AppendToString( + ULONGLONG Number, + STRA & String +) +{ + // prefast complains Append requires input + // to be null terminated, so zero initialize + // and pass the size of the buffer minus one + // to _ui64toa_s + CHAR chNumber[32] = {0}; + if (_ui64toa_s(Number, + chNumber, + sizeof(chNumber) - sizeof(CHAR), + 10) != 0) + { + return E_INVALIDARG; + } + return String.Append(chNumber); +} + +template +CHAR* InitHelper(__out CHAR (&psz)[size]) +{ + psz[0] = '\0'; + return psz; +} + +// +// Heap operation reduction macros +// +#define STACK_STRA(name, size) CHAR __ach##name[size];\ + STRA name(InitHelper(__ach##name), sizeof(__ach##name)) + +#define INLINE_STRA(name, size) CHAR __ach##name[size];\ + STRA name; + +#define INLINE_STRA_INIT(name) name(InitHelper(__ach##name), sizeof(__ach##name)) diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/stringu.cpp b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/stringu.cpp new file mode 100644 index 0000000000..15da79a7fe --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/stringu.cpp @@ -0,0 +1,1271 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma warning (disable : 4267) + +#include "precomp.h" + +STRU::STRU( + VOID +) : m_cchLen( 0 ) +{ + *(QueryStr()) = L'\0'; +} + +STRU::STRU( + __inout_ecount(cchInit) WCHAR* pbInit, + __in DWORD cchInit +) : m_Buff( pbInit, cchInit * sizeof( WCHAR ) ), + m_cchLen( 0 ) +/*++ + Description: + + Used by STACK_STRU. Initially populates underlying buffer with pbInit. + + pbInit is not freed. + + Arguments: + + pbInit - initial memory to use + cchInit - count, in characters, of pbInit + + Returns: + + None. + +--*/ +{ + _ASSERTE( cchInit <= (MAXDWORD / sizeof( WCHAR )) ); + _ASSERTE( NULL != pbInit ); + _ASSERTE(cchInit > 0 ); + _ASSERTE(pbInit[0] == L'\0'); +} + +BOOL +STRU::IsEmpty( + VOID +) const +{ + return ( m_cchLen == 0 ); +} + +DWORD +STRU::QueryCB( + VOID +) const +// +// Returns the number of bytes in the string excluding the terminating NULL +// +{ + return m_cchLen * sizeof( WCHAR ); +} + +DWORD +STRU::QueryCCH( + VOID +) const +// +// Returns the number of characters in the string excluding the terminating NULL +// +{ + return m_cchLen; +} + +DWORD +STRU::QuerySizeCCH( + VOID +) const +// +// Returns size of the underlying storage buffer, in characters +// +{ + return m_Buff.QuerySize() / sizeof( WCHAR ); +} + +__nullterminated +__ecount(this->m_cchLen) +WCHAR* +STRU::QueryStr( + VOID +) const +// +// Return the string buffer +// +{ + return m_Buff.QueryPtr(); +} + +VOID +STRU::Reset( + VOID +) +// +// Resets the internal string to be NULL string. Buffer remains cached. +// +{ + _ASSERTE( QueryStr() != NULL ); + *(QueryStr()) = L'\0'; + m_cchLen = 0; +} + +HRESULT +STRU::Resize( + DWORD cchSize +) +{ + SIZE_T cbSize = cchSize * sizeof( WCHAR ); + if ( cbSize > MAXDWORD ) + { + return HRESULT_FROM_WIN32( ERROR_ARITHMETIC_OVERFLOW ); + } + if( !m_Buff.Resize( cbSize ) ) + { + return E_OUTOFMEMORY; + } + + return S_OK; +} + +HRESULT +STRU::SyncWithBuffer( + VOID +) +// +// Recalculate the length of the string, etc. because we've modified +// the buffer directly. +// +{ + HRESULT hr; + size_t size; + hr = StringCchLengthW( QueryStr(), + QuerySizeCCH(), + &size ); + if ( SUCCEEDED( hr ) ) + { + m_cchLen = static_cast(size); + } + return hr; +} + +HRESULT +STRU::Copy( + __in PCWSTR pszCopy +) +{ + HRESULT hr; + size_t cbStr; + + hr = StringCchLengthW( pszCopy, + STRSAFE_MAX_CCH, + &cbStr ); + if ( FAILED( hr ) ) + { + return hr; + } + + _ASSERTE( cbStr <= MAXDWORD ); + return Copy( pszCopy, + cbStr ); +} + +HRESULT +STRU::Copy( + __in_ecount(cchLen) + PCWSTR pszCopy, + SIZE_T cchLen +) +// +// Copy the contents of another string to this one +// +{ + return AuxAppend( pszCopy, + cchLen * sizeof(WCHAR), + 0); +} + +HRESULT +STRU::Copy( + __in const STRU * pstrRhs +) +{ + _ASSERTE( NULL != pstrRhs ); + return Copy( pstrRhs->QueryStr(), pstrRhs->QueryCCH() ); +} + +HRESULT +STRU::Copy( + __in const STRU & str +) +{ + return Copy( str.QueryStr(), str.QueryCCH() ); +} + +HRESULT +STRU::CopyAndExpandEnvironmentStrings( + __in PCWSTR pszSource +) +{ + HRESULT hr = S_OK; + DWORD cchDestReqBuff = 0; + + Reset(); + + cchDestReqBuff = ExpandEnvironmentStringsW( pszSource, + QueryStr(), + QuerySizeCCH() ); + if ( cchDestReqBuff == 0 ) + { + hr = HRESULT_FROM_WIN32( GetLastError() ); + goto Finished; + } + else if ( cchDestReqBuff > QuerySizeCCH() ) + { + hr = Resize( cchDestReqBuff ); + if ( FAILED( hr ) ) + { + goto Finished; + } + + cchDestReqBuff = ExpandEnvironmentStringsW( pszSource, + QueryStr(), + QuerySizeCCH() ); + + if ( cchDestReqBuff == 0 || cchDestReqBuff > QuerySizeCCH() ) + { + _ASSERTE( FALSE ); + hr = HRESULT_FROM_WIN32( GetLastError() ); + goto Finished; + } + } + + hr = SyncWithBuffer(); + if ( FAILED( hr ) ) + { + goto Finished; + } + +Finished: + + return hr; + +} + +HRESULT +STRU::CopyA( + __in PCSTR pszCopyA +) +{ + HRESULT hr; + size_t cbStr; + + hr = StringCbLengthA( pszCopyA, + STRSAFE_MAX_CCH, + &cbStr ); + if ( FAILED( hr ) ) + { + return hr; + } + + _ASSERTE( cbStr <= MAXDWORD ); + return CopyA( pszCopyA, + cbStr ); +} + +HRESULT +STRU::CopyA( + __in_bcount(cchLen) + PCSTR pszCopyA, + SIZE_T cchLen, + UINT CodePage /*= CP_UTF8*/ +) +{ + return AuxAppendA( + pszCopyA, + cchLen, + 0, + CodePage + ); +} + +HRESULT +STRU::Append( + __in PCWSTR pszAppend +) +{ + HRESULT hr; + size_t cbStr; + + hr = StringCchLengthW( pszAppend, + STRSAFE_MAX_CCH, + &cbStr ); + if ( FAILED( hr ) ) + { + return hr; + } + + _ASSERTE( cbStr <= MAXDWORD ); + return Append( pszAppend, + cbStr ); +} + +HRESULT +STRU::Append( + __in_ecount(cchLen) + PCWSTR pszAppend, + SIZE_T cchLen +) +// +// Append something to the end of the string +// +{ + if ( cchLen == 0 ) + { + return S_OK; + } + return AuxAppend( pszAppend, + cchLen * sizeof(WCHAR), + QueryCB() ); +} + +HRESULT +STRU::Append( + __in const STRU * pstrRhs +) +{ + _ASSERTE( NULL != pstrRhs ); + return Append( pstrRhs->QueryStr(), pstrRhs->QueryCCH() ); +} + +HRESULT +STRU::Append( + __in const STRU & strRhs +) +{ + return Append( strRhs.QueryStr(), strRhs.QueryCCH() ); +} + +HRESULT +STRU::AppendA( + __in PCSTR pszAppendA +) +{ + HRESULT hr; + size_t cbStr; + + hr = StringCbLengthA( pszAppendA, + STRSAFE_MAX_CCH, + &cbStr ); + if ( FAILED( hr ) ) + { + return hr; + } + + _ASSERTE( cbStr <= MAXDWORD ); + return AppendA( pszAppendA, + cbStr ); +} + +HRESULT +STRU::AppendA( + __in_bcount(cchLen) + PCSTR pszAppendA, + SIZE_T cchLen, + UINT CodePage /*= CP_UTF8*/ +) +{ + if ( cchLen == 0 ) + { + return S_OK; + } + return AuxAppendA( + pszAppendA, + cchLen, + QueryCB(), + CodePage + ); +} + +HRESULT +STRU::CopyToBuffer( + __out_bcount(*pcb) WCHAR* pszBuffer, + PDWORD pcb +) const +// +// Makes a copy of the stored string into the given buffer +// +{ + _ASSERTE( NULL != pszBuffer ); + _ASSERTE( NULL != pcb ); + + HRESULT hr = S_OK; + DWORD cbNeeded = QueryCB() + sizeof( WCHAR ); + + if( *pcb < cbNeeded ) + { + hr = HRESULT_FROM_WIN32( ERROR_INSUFFICIENT_BUFFER ); + goto Finished; + } + + // + // BUGBUG: StringCchCopy? + // + memcpy( pszBuffer, QueryStr(), cbNeeded ); + +Finished: + + *pcb = cbNeeded; + + return hr; +} + +HRESULT +STRU::SetLen( + __in DWORD cchLen +) +/*++ + * +Routine Description: + + Set the length of the string and null terminate, if there + is sufficient buffer already allocated. Will not reallocate. + +Arguments: + + cchLen - The number of characters in the new string. + +Return Value: + + HRESULT + +--*/ +{ + if( cchLen >= QuerySizeCCH() ) + { + return HRESULT_FROM_WIN32( ERROR_INVALID_PARAMETER ); + } + + *( QueryStr() + cchLen ) = L'\0'; + m_cchLen = cchLen; + + return S_OK; +} + +HRESULT +STRU::SafeSnwprintf( + __in PCWSTR pwszFormatString, + ... +) +/*++ + +Routine Description: + + Writes to a STRU, growing it as needed. It arbitrarily caps growth at 64k chars. + +Arguments: + + pwszFormatString - printf format + ... - printf args + +Return Value: + + HRESULT + +--*/ +{ + HRESULT hr = S_OK; + va_list argsList; + va_start( argsList, pwszFormatString ); + + hr = SafeVsnwprintf(pwszFormatString, argsList); + + va_end( argsList ); + return hr; +} + +HRESULT +STRU::SafeVsnwprintf( + __in PCWSTR pwszFormatString, + va_list argsList +) +/*++ + +Routine Description: + + Writes to a STRU, growing it as needed. It arbitrarily caps growth at 64k chars. + +Arguments: + + pwszFormatString - printf format + argsList - printf va_list + +Return Value: + + HRESULT + +--*/ +{ + HRESULT hr = S_OK; + int cchOutput; + int cchNeeded; + + // + // Format the incoming message using vsnprintf() + // so that the overflows are captured + // + cchOutput = _vsnwprintf_s( + QueryStr(), + QuerySizeCCH(), + QuerySizeCCH() - 1, + pwszFormatString, + argsList + ); + + if( cchOutput == -1 ) + { + // + // Couldn't fit this in the original STRU size. + // + cchNeeded = _vscwprintf( pwszFormatString, argsList ); + if( cchNeeded > 64 * 1024 ) + { + // + // If we're trying to produce a string > 64k chars, then + // there is probably a problem + // + hr = HRESULT_FROM_WIN32( ERROR_INVALID_DATA ); + goto Finished; + } + + // + // _vscprintf doesn't include terminating null character + // + cchNeeded++; + + hr = Resize( cchNeeded ); + if( FAILED( hr ) ) + { + goto Finished; + } + + cchOutput = _vsnwprintf_s( + QueryStr(), + QuerySizeCCH(), + QuerySizeCCH() - 1, + pwszFormatString, + argsList + ); + if( -1 == cchOutput ) + { + // + // This should never happen, cause we should already have correctly sized memory + // + _ASSERTE( FALSE ); + + hr = HRESULT_FROM_WIN32( ERROR_INVALID_DATA ); + goto Finished; + } + } + + // + // always null terminate at the last WCHAR + // + QueryStr()[ QuerySizeCCH() - 1 ] = L'\0'; + + // + // we directly touched the buffer - therefore: + // + hr = SyncWithBuffer(); + if ( FAILED( hr ) ) + { + goto Finished; + } + +Finished: + + if( FAILED( hr ) ) + { + Reset(); + } + + return hr; +} + +HRESULT +STRU::AuxAppend( + __in_ecount(cNumStrings) + PCWSTR const rgpszStrings[], + SIZE_T cNumStrings +) +/*++ + +Routine Description: + + Appends an array of strings of length cNumStrings + +Arguments: + + rgStrings - The array of strings to be appened + cNumStrings - The count of String + +Return Value: + + HRESULT + +--*/ +{ + HRESULT hr = S_OK; + size_t cbStringsTotal = sizeof( WCHAR ); // Account for null-terminator + + // + // Compute total size of the string. + // Resize internal buffer + // Copy each array element one by one to backing buffer + // Update backing buffer string length + // + for ( SIZE_T i = 0; i < cNumStrings; i++ ) + { + _ASSERTE( rgpszStrings[ i ] != NULL ); + if ( NULL == rgpszStrings[ i ] ) + { + return E_INVALIDARG; + } + + size_t cbString = 0; + + hr = StringCbLengthW( rgpszStrings[ i ], + STRSAFE_MAX_CCH * sizeof( WCHAR ), + &cbString ); + if ( FAILED( hr ) ) + { + return hr; + } + + cbStringsTotal += cbString; + + if ( cbStringsTotal > MAXDWORD ) + { + return HRESULT_FROM_WIN32( ERROR_ARITHMETIC_OVERFLOW ); + } + } + + size_t cbBufSizeRequired = QueryCB() + cbStringsTotal; + if ( cbBufSizeRequired > MAXDWORD ) + { + return HRESULT_FROM_WIN32( ERROR_ARITHMETIC_OVERFLOW ); + } + + if( m_Buff.QuerySize() < cbBufSizeRequired ) + { + if( !m_Buff.Resize( cbBufSizeRequired ) ) + { + return E_OUTOFMEMORY; + } + } + + STRSAFE_LPWSTR pszStringEnd = QueryStr() + QueryCCH(); + size_t cchRemaining = QuerySizeCCH() - QueryCCH(); + for ( SIZE_T i = 0; i < cNumStrings; i++ ) + { + hr = StringCchCopyExW( pszStringEnd, // pszDest + cchRemaining, // cchDest + rgpszStrings[ i ], // pszSrc + &pszStringEnd, // ppszDestEnd + &cchRemaining, // pcchRemaining + 0 ); // dwFlags + if ( FAILED( hr ) ) + { + _ASSERTE( FALSE ); + HRESULT hr2 = SyncWithBuffer(); + if ( FAILED( hr2 ) ) + { + return hr2; + } + return hr; + } + } + + m_cchLen = static_cast< DWORD >( cbBufSizeRequired ) / sizeof( WCHAR ) - 1; + + return S_OK; +} + +HRESULT +STRU::AuxAppend( + __in_bcount(cbStr) + const WCHAR* pStr, + SIZE_T cbStr, + DWORD cbOffset +) +/*++ + +Routine Description: + + Appends to the string starting at the (byte) offset cbOffset. + +Arguments: + + pStr - A unicode string to be appended + cbStr - Length, in bytes, of pStr + cbOffset - Offset, in bytes, at which to begin the append + +Return Value: + + HRESULT + +--*/ +{ + _ASSERTE( NULL != pStr ); + _ASSERTE( 0 == cbStr % sizeof( WCHAR ) ); + _ASSERTE( cbOffset <= QueryCB() ); + _ASSERTE( 0 == cbOffset % sizeof( WCHAR ) ); + + ULONGLONG cb64NewSize = (ULONGLONG)cbOffset + cbStr + sizeof( WCHAR ); + if( cb64NewSize > MAXDWORD ) + { + return HRESULT_FROM_WIN32( ERROR_ARITHMETIC_OVERFLOW ); + } + + if( m_Buff.QuerySize() < cb64NewSize ) + { + if( !m_Buff.Resize( static_cast(cb64NewSize) ) ) + { + return E_OUTOFMEMORY; + } + } + + memcpy( reinterpret_cast(m_Buff.QueryPtr()) + cbOffset, pStr, cbStr ); + + m_cchLen = (static_cast(cbStr) + cbOffset) / sizeof(WCHAR); + + *( QueryStr() + m_cchLen ) = L'\0'; + + return S_OK; +} + +HRESULT +STRU::AuxAppendA( + __in_bcount(cbStr) + const CHAR* pStr, + SIZE_T cbStr, + DWORD cbOffset, + UINT CodePage +) +/*++ + +Routine Description: + + Convert and append an ANSI string to the string starting at + the (byte) offset cbOffset + +Arguments: + + pStr - An ANSI string to be appended + cbStr - Length, in bytes, of pStr + cbOffset - Offset, in bytes, at which to begin the append + CodePage - code page to use for conversion + +Return Value: + + HRESULT + +--*/ +{ + WCHAR* pszBuffer; + DWORD cchBuffer; + DWORD cchCharsCopied = 0; + + _ASSERTE( NULL != pStr ); + _ASSERTE( cbOffset <= QueryCB() ); + _ASSERTE( 0 == cbOffset % sizeof( WCHAR ) ); + + if ( NULL == pStr ) + { + return E_INVALIDARG; + } + + if( 0 == cbStr ) + { + return S_OK; + } + + // + // Only resize when we have to. When we do resize, we tack on + // some extra space to avoid extra reallocations. + // + if( m_Buff.QuerySize() < (ULONGLONG)cbOffset + (cbStr * sizeof( WCHAR )) + sizeof(WCHAR) ) + { + ULONGLONG cb64NewSize = (ULONGLONG)( cbOffset + cbStr * sizeof(WCHAR) + sizeof( WCHAR ) ); + + // + // Check for the arithmetic overflow + // + if( cb64NewSize > MAXDWORD ) + { + return HRESULT_FROM_WIN32( ERROR_ARITHMETIC_OVERFLOW ); + } + + if( !m_Buff.Resize( static_cast(cb64NewSize) ) ) + { + return E_OUTOFMEMORY; + } + } + + pszBuffer = reinterpret_cast(reinterpret_cast(m_Buff.QueryPtr()) + cbOffset); + cchBuffer = ( m_Buff.QuerySize() - cbOffset - sizeof( WCHAR ) ) / sizeof( WCHAR ); + + cchCharsCopied = MultiByteToWideChar( + CodePage, + MB_ERR_INVALID_CHARS, + pStr, + static_cast(cbStr), + pszBuffer, + cchBuffer + ); + if( 0 == cchCharsCopied ) + { + return HRESULT_FROM_WIN32( GetLastError() ); + } + + // + // set the new length + // + m_cchLen = cchCharsCopied + cbOffset/sizeof(WCHAR); + + // + // Must be less than, cause still need to add NULL + // + _ASSERTE( m_cchLen < QuerySizeCCH() ); + + // + // append NULL character + // + *(QueryStr() + m_cchLen) = L'\0'; + + return S_OK; +} + + +/*++ + +Routine Description: + + Removes leading and trailing whitespace + +--*/ + +VOID +STRU::Trim() +{ + PWSTR pwszString = QueryStr(); + DWORD cchNewLength = m_cchLen; + DWORD cchLeadingWhitespace = 0; + DWORD cchTempLength = 0; + + for (LONG ixString = m_cchLen - 1; ixString >= 0; ixString--) + { + if (iswspace(pwszString[ixString]) != 0) + { + pwszString[ixString] = L'\0'; + cchNewLength--; + } + else + { + break; + } + } + + cchTempLength = cchNewLength; + for (DWORD ixString = 0; ixString < cchTempLength; ixString++) + { + if (iswspace(pwszString[ixString]) != 0) + { + cchLeadingWhitespace++; + cchNewLength--; + } + else + { + break; + } + } + + if (cchNewLength == 0) + { + + Reset(); + } + else if (cchLeadingWhitespace > 0) + { + memmove(pwszString, pwszString + cchLeadingWhitespace, cchNewLength * sizeof(WCHAR)); + pwszString[cchNewLength] = L'\0'; + } + + SyncWithBuffer(); +} + +/*++ + +Routine Description: + + Compares the string to the provided prefix to check for equality + +Arguments: + + pwszPrefix - wide char string to compare with + fIgnoreCase - indicates whether the string comparison should be case-sensitive + +Return Value: + + TRUE if prefix string matches with internal string, FALSE otherwise + +--*/ + +BOOL +STRU::StartsWith( + __in PCWSTR pwszPrefix, + __in bool fIgnoreCase) const +{ + HRESULT hr = S_OK; + BOOL fMatch = FALSE; + size_t cchPrefix = 0; + + if (pwszPrefix == NULL) + { + goto Finished; + } + + hr = StringCchLengthW( pwszPrefix, + STRSAFE_MAX_CCH, + &cchPrefix ); + if (FAILED(hr)) + { + goto Finished; + } + + _ASSERTE( cchPrefix <= MAXDWORD ); + + if (cchPrefix > m_cchLen) + { + goto Finished; + } + + #if defined( NTDDI_VERSION ) && NTDDI_VERSION >= NTDDI_LONGHORN + + fMatch = ( CSTR_EQUAL == CompareStringOrdinal( QueryStr(), + cchPrefix, + pwszPrefix, + cchPrefix, + fIgnoreCase ) ); + #else + + if( fIgnoreCase ) + { + fMatch = ( 0 == _wcsnicmp( QueryStr(), pwszPrefix, cchPrefix ) ); + } + else + { + fMatch = ( 0 == wcsncmp( QueryStr(), pwszPrefix, cchPrefix ) ); + } + + #endif + +Finished: + + return fMatch; +} + +/*++ + +Routine Description: + + Compares the string to the provided suffix to check for equality + +Arguments: + + pwszSuffix - wide char string to compare with + fIgnoreCase - indicates whether the string comparison should be case-sensitive + +Return Value: + + TRUE if suffix string matches with internal string, FALSE otherwise + +--*/ + + +BOOL +STRU::EndsWith( + __in PCWSTR pwszSuffix, + __in bool fIgnoreCase) const +{ + HRESULT hr = S_OK; + PWSTR pwszString = QueryStr(); + BOOL fMatch = FALSE; + size_t cchSuffix = 0; + ptrdiff_t ixOffset = 0; + + if (pwszSuffix == NULL) + { + goto Finished; + } + + hr = StringCchLengthW( pwszSuffix, + STRSAFE_MAX_CCH, + &cchSuffix ); + if (FAILED(hr)) + { + goto Finished; + } + + _ASSERTE( cchSuffix <= MAXDWORD ); + + if (cchSuffix > m_cchLen) + { + goto Finished; + } + + ixOffset = m_cchLen - cchSuffix; + _ASSERTE(ixOffset >= 0 && ixOffset <= MAXDWORD); + + #if defined( NTDDI_VERSION ) && NTDDI_VERSION >= NTDDI_LONGHORN + + fMatch = ( CSTR_EQUAL == CompareStringOrdinal( pwszString + ixOffset, + cchSuffix, + pwszSuffix, + cchSuffix, + fIgnoreCase ) ); + #else + + if( fIgnoreCase ) + { + fMatch = ( 0 == _wcsnicmp( pwszString + ixOffset, pwszSuffix, cchSuffix ) ); + } + else + { + fMatch = ( 0 == wcsncmp( pwszString + ixOffset, pwszSuffix, cchSuffix ) ); + } + + #endif + +Finished: + + return fMatch; +} + +/*++ + +Routine Description: + + Searches the string for the first occurrence of the specified character. + +Arguments: + + charValue - character to find + dwStartIndex - the initial index. + +Return Value: + + The index for the first character occurence in the string. + + -1 if not found. + +--*/ +INT +STRU::IndexOf( + __in WCHAR charValue, + __in DWORD dwStartIndex + ) const +{ + INT nIndex = -1; + + // Make sure that there are no buffer overruns. + if( dwStartIndex >= QueryCCH() ) + { + goto Finished; + } + + const WCHAR* pwChar = wcschr( QueryStr() + dwStartIndex, charValue ); + + // Determine the index if found + if( pwChar ) + { + // nIndex will be set to -1 on failure. + (VOID)SizeTToInt( pwChar - QueryStr(), &nIndex ); + } + +Finished: + + return nIndex; +} + + +/*++ + +Routine Description: + + Searches the string for the first occurrence of the specified substring. + +Arguments: + + pwszValue - substring to find + dwStartIndex - initial index. + +Return Value: + + The index for the first character occurence in the string. + + -1 if not found. + +--*/ +INT +STRU::IndexOf( + __in PCWSTR pwszValue, + __in DWORD dwStartIndex + ) const +{ + HRESULT hr = S_OK; + INT nIndex = -1; + SIZE_T cchValue = 0; + + // Validate input parameters + if( dwStartIndex >= QueryCCH() || !pwszValue ) + { + goto Finished; + } + + const WCHAR* pwChar = wcsstr( QueryStr() + dwStartIndex, pwszValue ); + + // Determine the index if found + if( pwChar ) + { + // nIndex will be set to -1 on failure. + (VOID)SizeTToInt( pwChar - QueryStr(), &nIndex ); + } + +Finished: + + return nIndex; +} + + +/*++ + +Routine Description: + + Searches the string for the last occurrence of the specified character. + +Arguments: + + charValue - character to find + dwStartIndex - initial index. + +Return Value: + + The index for the last character occurence in the string. + + -1 if not found. + +--*/ +INT +STRU::LastIndexOf( + __in WCHAR charValue, + __in DWORD dwStartIndex + ) const +{ + INT nIndex = -1; + + // Make sure that there are no buffer overruns. + if( dwStartIndex >= QueryCCH() ) + { + goto Finished; + } + + const WCHAR* pwChar = wcsrchr( QueryStr() + dwStartIndex, charValue ); + + // Determine the index if found + if( pwChar ) + { + // nIndex will be set to -1 on failure. + (VOID)SizeTToInt( pwChar - QueryStr(), &nIndex ); + } + +Finished: + + return nIndex; +} + +//static +HRESULT +STRU::ExpandEnvironmentVariables( + __in PCWSTR pszString, + __out STRU * pstrExpandedString + ) +/*++ + +Routine Description: + + Expand the environment variables in a string + +Arguments: + + pszString - String with environment variables to expand + pstrExpandedString - Receives expanded string on success + +Return Value: + + HRESULT + +--*/ +{ + HRESULT hr = S_OK; + DWORD cchNewSize = 0; + + if ( pszString == NULL || + pstrExpandedString == NULL ) + { + DBG_ASSERT( FALSE ); + hr = HRESULT_FROM_WIN32( ERROR_INVALID_PARAMETER ); + goto Exit; + } + + cchNewSize = ExpandEnvironmentStrings( pszString, + pstrExpandedString->QueryStr(), + pstrExpandedString->QuerySizeCCH() ); + if ( cchNewSize == 0 ) + { + hr = HRESULT_FROM_WIN32( GetLastError() ); + goto Exit; + } + + if ( cchNewSize > pstrExpandedString->QuerySizeCCH() ) + { + hr = pstrExpandedString->Resize( + ( cchNewSize + 1 ) * sizeof( WCHAR ) + ); + if ( FAILED( hr ) ) + { + goto Exit; + } + + cchNewSize = ExpandEnvironmentStrings( + pszString, + pstrExpandedString->QueryStr(), + pstrExpandedString->QuerySizeCCH() + ); + + if ( cchNewSize == 0 || + cchNewSize > pstrExpandedString->QuerySizeCCH() ) + { + hr = HRESULT_FROM_WIN32( GetLastError() ); + goto Exit; + } + } + + pstrExpandedString->SyncWithBuffer(); + + hr = S_OK; + +Exit: + + return hr; +} + +#pragma warning(default:4267) diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/stringu.h b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/stringu.h new file mode 100644 index 0000000000..6f27c5421d --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/stringu.h @@ -0,0 +1,427 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "buffer.h" +#include + +class STRU +{ + +public: + + STRU( + VOID + ); + + STRU( + __inout_ecount(cchInit) WCHAR* pbInit, + __in DWORD cchInit + ); + + BOOL + IsEmpty( + VOID + ) const; + + BOOL + Equals( + __in const STRU * pstrRhs, + __in BOOL fIgnoreCase = FALSE + ) const + { + _ASSERTE( pstrRhs != NULL ); + return Equals( pstrRhs->QueryStr(), fIgnoreCase ); + } + + BOOL + Equals( + __in const STRU & strRhs, + __in BOOL fIgnoreCase = FALSE + ) const + { + return Equals( strRhs.QueryStr(), fIgnoreCase ); + } + + BOOL + Equals( + __in PCWSTR pszRhs, + __in BOOL fIgnoreCase = FALSE + ) const + { + _ASSERTE( NULL != pszRhs ); + if ( NULL == pszRhs ) + { + return FALSE; + } + + #if defined( NTDDI_VERSION ) && NTDDI_VERSION >= NTDDI_LONGHORN + + return ( CSTR_EQUAL == CompareStringOrdinal( QueryStr(), + QueryCCH(), + pszRhs, + -1, + fIgnoreCase ) ); + #else + + if( fIgnoreCase ) + { + return ( 0 == _wcsicmp( QueryStr(), pszRhs ) ); + } + return ( 0 == wcscmp( QueryStr(), pszRhs ) ); + + #endif + } + + + static + BOOL + Equals( + __in PCWSTR pwszLhs, + __in PCWSTR pwszRhs, + __in bool fIgnoreCase = false + ) + { + // Return FALSE if either or both strings are NULL. + if (!pwszLhs || !pwszRhs) return FALSE; + + // + // This method performs a ordinal string comparison when OS is Vista or + // greater and a culture sensitive comparison if not (XP). This is + // consistent with the existing Equals implementation (see above). + // +#if defined( NTDDI_VERSION ) && NTDDI_VERSION >= NTDDI_LONGHORN + + return ( CSTR_EQUAL == CompareStringOrdinal( pwszLhs, + -1, + pwszRhs, + -1, + fIgnoreCase ) ); +#else + + if( fIgnoreCase ) + { + return ( 0 == _wcsicmp( pwszLhs, pwszRhs ) ); + } + else + { + return ( 0 == wcscmp( pwszLhs, pwszRhs ) ); + } + +#endif + } + + VOID + Trim(); + + BOOL + StartsWith( + __in const STRU * pStruPrefix, + __in bool fIgnoreCase = FALSE + ) const + { + _ASSERTE( pStruPrefix != NULL ); + return StartsWith( pStruPrefix->QueryStr(), fIgnoreCase ); + } + + BOOL + StartsWith( + __in const STRU & struPrefix, + __in bool fIgnoreCase = FALSE + ) const + { + return StartsWith( struPrefix.QueryStr(), fIgnoreCase ); + } + + BOOL + StartsWith( + __in PCWSTR pwszPrefix, + __in bool fIgnoreCase = FALSE + ) const; + + BOOL + EndsWith( + __in const STRU * pStruSuffix, + __in bool fIgnoreCase = FALSE + ) const + { + _ASSERTE( pStruSuffix != NULL ); + return EndsWith( pStruSuffix->QueryStr(), fIgnoreCase ); + } + + BOOL + EndsWith( + __in const STRU & struSuffix, + __in bool fIgnoreCase = FALSE + ) const + { + return EndsWith( struSuffix.QueryStr(), fIgnoreCase ); + } + + BOOL + EndsWith( + __in PCWSTR pwszSuffix, + __in bool fIgnoreCase = FALSE + ) const; + + INT + IndexOf( + __in WCHAR charValue, + __in DWORD dwStartIndex = 0 + ) const; + + INT + IndexOf( + __in PCWSTR pwszValue, + __in DWORD dwStartIndex = 0 + ) const; + + INT + LastIndexOf( + __in WCHAR charValue, + __in DWORD dwStartIndex = 0 + ) const; + + DWORD + QueryCB( + VOID + ) const; + + DWORD + QueryCCH( + VOID + ) const; + + DWORD + QuerySizeCCH( + VOID + ) const; + + __nullterminated + __ecount(this->m_cchLen) + WCHAR* + QueryStr( + VOID + ) const; + + VOID + Reset( + VOID + ); + + HRESULT + Resize( + DWORD cchSize + ); + + HRESULT + SyncWithBuffer( + VOID + ); + + template + HRESULT + Copy( + __in PCWSTR const (&rgpszStrings)[size] + ) + // + // Copies an array of strings declared as stack array. For example: + // + // LPCWSTR rgExample[] { L"one", L"two" }; + // hr = str.Copy( rgExample ); + // + { + Reset(); + + return AuxAppend( rgpszStrings, _countof( rgpszStrings ) ); + } + + HRESULT + Copy( + __in PCWSTR pszCopy + ); + + HRESULT + Copy( + __in_ecount(cchLen) + PCWSTR pszCopy, + SIZE_T cchLen + ); + + HRESULT + Copy( + __in const STRU * pstrRhs + ); + + HRESULT + Copy( + __in const STRU & str + ); + + HRESULT + CopyAndExpandEnvironmentStrings( + __in PCWSTR pszSource + ); + + HRESULT + CopyA( + __in PCSTR pszCopyA + ); + + HRESULT + CopyA( + __in_bcount(cchLen) + PCSTR pszCopyA, + SIZE_T cchLen, + UINT CodePage = CP_UTF8 + ); + + template + HRESULT + Append( + __in PCWSTR const (&rgpszStrings)[size] + ) + // + // Appends an array of strings declared as stack array. For example: + // + // LPCWSTR rgExample[] { L"one", L"two" }; + // hr = str.Append( rgExample ); + // + { + return AuxAppend( rgpszStrings, _countof( rgpszStrings ) ); + } + + HRESULT + Append( + __in PCWSTR pszAppend + ); + + HRESULT + Append( + __in_ecount(cchLen) + PCWSTR pszAppend, + SIZE_T cchLen + ); + + HRESULT + Append( + __in const STRU * pstrRhs + ); + + HRESULT + Append( + __in const STRU & strRhs + ); + + HRESULT + AppendA( + __in PCSTR pszAppendA + ); + + HRESULT + AppendA( + __in_bcount(cchLen) + PCSTR pszAppendA, + SIZE_T cchLen, + UINT CodePage = CP_UTF8 + ); + + HRESULT + CopyToBuffer( + __out_bcount(*pcb) WCHAR* pszBuffer, + PDWORD pcb + ) const; + + HRESULT + SetLen( + __in DWORD cchLen + ); + + HRESULT + SafeSnwprintf( + __in PCWSTR pwszFormatString, + ... + ); + + HRESULT + SafeVsnwprintf( + __in PCWSTR pwszFormatString, + va_list argsList + ); + + static + HRESULT ExpandEnvironmentVariables( + __in PCWSTR pszString, + __out STRU * pstrExpandedString + ); + +private: + + // + // Avoid C++ errors. This object should never go through a copy + // constructor, unintended cast or assignment. + // + STRU( const STRU & ); + STRU & operator = ( const STRU & ); + + HRESULT + AuxAppend( + __in_ecount(cNumStrings) + PCWSTR const rgpszStrings[], + SIZE_T cNumStrings + ); + + HRESULT + AuxAppend( + __in_bcount(cbStr) + const WCHAR* pStr, + SIZE_T cbStr, + DWORD cbOffset + ); + + HRESULT + AuxAppendA( + __in_bcount(cbStr) + const CHAR* pStr, + SIZE_T cbStr, + DWORD cbOffset, + UINT CodePage + ); + + // + // Buffer with an inline buffer of 1, + // enough to hold null-terminating character. + // + BUFFER_T m_Buff; + DWORD m_cchLen; +}; + +// +// Helps to initialize an external buffer before +// constructing the STRU object. +// +template +WCHAR* InitHelper(__out WCHAR (&psz)[size]) +{ + psz[0] = L'\0'; + return psz; +} + +// +// Heap operation reduction macros +// +#define STACK_STRU(name, size) WCHAR __ach##name[size];\ + STRU name(InitHelper(__ach##name), sizeof(__ach##name)/sizeof(*__ach##name)) + +#define INLINE_STRU(name, size) WCHAR __ach##name[size];\ + STRU name; + +#define INLINE_STRU_INIT(name) name(InitHelper(__ach##name), sizeof(__ach##name)/sizeof(*__ach##name)) + + +HRESULT +MakePathCanonicalizationProof( + IN PCWSTR pszName, + OUT STRU * pstrPath +); diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/tracelog.c b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/tracelog.c new file mode 100644 index 0000000000..f7b2da5e43 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/tracelog.c @@ -0,0 +1,235 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include +#include "pudebug.h" +#include "tracelog.h" +#include + + +#define ALLOC_MEM(cb) (PVOID)LocalAlloc( LPTR, (cb) ) +#define FREE_MEM(ptr) (VOID)LocalFree( (HLOCAL)(ptr) ) + + + +PTRACE_LOG +CreateTraceLog( + IN LONG LogSize, + IN LONG ExtraBytesInHeader, + IN LONG EntrySize + ) +/*++ + +Routine Description: + + Creates a new (empty) trace log buffer. + +Arguments: + + LogSize - The number of entries in the log. + + ExtraBytesInHeader - The number of extra bytes to include in the + log header. This is useful for adding application-specific + data to the log. + + EntrySize - The size (in bytes) of each entry. + +Return Value: + + PTRACE_LOG - Pointer to the newly created log if successful, + NULL otherwise. + +--*/ +{ + + ULONG ulTotalSize = 0; + ULONG ulLogSize = 0; + ULONG ulEntrySize = 0; + ULONG ulTmpResult = 0; + ULONG ulExtraBytesInHeader = 0; + PTRACE_LOG log = NULL; + HRESULT hr = S_OK; + + // + // Sanity check the parameters. + // + + //DBG_ASSERT( LogSize > 0 ); + //DBG_ASSERT( EntrySize > 0 ); + //DBG_ASSERT( ExtraBytesInHeader >= 0 ); + //DBG_ASSERT( ( EntrySize & 3 ) == 0 ); + + // + // converting to unsigned long. Since all these values are positive + // so its safe to cast them to their unsigned equivalent directly. + // + ulLogSize = (ULONG) LogSize; + ulEntrySize = (ULONG) EntrySize; + ulExtraBytesInHeader = (ULONG) ExtraBytesInHeader; + + // + // Check if the multiplication operation will overflow a LONG + // ulTotalSize = LogSize * EntrySize; + // + hr = ULongMult( ulLogSize, ulEntrySize, &ulTotalSize ); + if ( FAILED(hr) ) + { + SetLastError( ERROR_ARITHMETIC_OVERFLOW ); + return NULL; + } + + // + // check for overflow in addition operation. + // ulTmpResult = sizeof(TRACE_LOG) + ulExtraBytesInHeader + // + hr = ULongAdd( (ULONG) sizeof(TRACE_LOG), ulExtraBytesInHeader, &ulTmpResult ); + if ( FAILED(hr) ) + { + SetLastError( ERROR_ARITHMETIC_OVERFLOW ); + return NULL; + } + + // + // check for overflow in addition operation. + // ulTotalSize = ulTotalSize + ulTmpResult; + // + hr = ULongAdd( ulTmpResult, ulTotalSize, &ulTotalSize ); + if ( FAILED(hr) ) + { + SetLastError( ERROR_ARITHMETIC_OVERFLOW ); + return NULL; + } + + if ( ulTotalSize > (ULONG) 0x7FFFFFFF ) + { + SetLastError( ERROR_ARITHMETIC_OVERFLOW ); + return NULL; + } + + // + // Allocate & initialize the log structure. + // + + log = (PTRACE_LOG)ALLOC_MEM( ulTotalSize ); + + // + // Initialize it. + // + + if( log != NULL ) { + + RtlZeroMemory( log, ulTotalSize ); + + log->Signature = TRACE_LOG_SIGNATURE; + log->LogSize = LogSize; + log->NextEntry = -1; + log->EntrySize = EntrySize; + log->LogBuffer = (PUCHAR)( log + 1 ) + ExtraBytesInHeader; + } + + return log; + +} // CreateTraceLog + + +VOID +DestroyTraceLog( + IN PTRACE_LOG Log + ) +/*++ + +Routine Description: + + Destroys a trace log buffer created with CreateTraceLog(). + +Arguments: + + Log - The trace log buffer to destroy. + +Return Value: + + None. + +--*/ +{ + if ( Log != NULL ) { + //DBG_ASSERT( Log->Signature == TRACE_LOG_SIGNATURE ); + + Log->Signature = TRACE_LOG_SIGNATURE_X; + FREE_MEM( Log ); + } + +} // DestroyTraceLog + + +LONG +WriteTraceLog( + IN PTRACE_LOG Log, + IN PVOID Entry + ) +/*++ + +Routine Description: + + Writes a new entry to the specified trace log. + +Arguments: + + Log - The log to write to. + + Entry - Pointer to the data to write. This buffer is assumed to be + Log->EntrySize bytes long. + +Return Value: + + Index of entry in log. This is useful for correlating the output + of !inetdbg.ref to a particular point in the output debug stream + +--*/ +{ + + PUCHAR target; + ULONG index; + + //DBG_ASSERT( Log != NULL ); + //DBG_ASSERT( Log->Signature == TRACE_LOG_SIGNATURE ); + //DBG_ASSERT( Entry != NULL ); + + // + // Find the next slot, copy the entry to the slot. + // + + index = ( (ULONG) InterlockedIncrement( &Log->NextEntry ) ) % (ULONG) Log->LogSize; + + //DBG_ASSERT( index < (ULONG) Log->LogSize ); + + target = Log->LogBuffer + ( index * Log->EntrySize ); + + RtlCopyMemory( + target, + Entry, + Log->EntrySize + ); + + return index; +} // WriteTraceLog + + +VOID +ResetTraceLog( + IN PTRACE_LOG Log + ) +{ + + //DBG_ASSERT( Log != NULL ); + //DBG_ASSERT( Log->Signature == TRACE_LOG_SIGNATURE ); + + RtlZeroMemory( + ( Log + 1 ), + Log->LogSize * Log->EntrySize + ); + + Log->NextEntry = -1; + +} // ResetTraceLog + diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/tracelog.h b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/tracelog.h new file mode 100644 index 0000000000..ed34bcffc9 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/tracelog.h @@ -0,0 +1,105 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#ifndef _TRACELOG_H_ +#define _TRACELOG_H_ + + +#if defined(__cplusplus) +extern "C" { +#endif // __cplusplus + + +typedef struct _TRACE_LOG { + + // + // Signature. + // + + LONG Signature; + + // + // The total number of entries available in the log. + // + + LONG LogSize; + + // + // The index of the next entry to use. + // + + LONG NextEntry; + + // + // The byte size of each entry. + // + + LONG EntrySize; + + // + // Pointer to the start of the circular buffer. + // + + PUCHAR LogBuffer; + + // + // The extra header bytes and actual log entries go here. + // + // BYTE ExtraHeaderBytes[ExtraBytesInHeader]; + // BYTE Entries[LogSize][EntrySize]; + // + +} TRACE_LOG, *PTRACE_LOG; + + +// +// Log header signature. +// + +#define TRACE_LOG_SIGNATURE ((DWORD)'gOlT') +#define TRACE_LOG_SIGNATURE_X ((DWORD)'golX') + + +// +// This macro maps a TRACE_LOG pointer to a pointer to the 'extra' +// data associated with the log. +// + +#define TRACE_LOG_TO_EXTRA_DATA(log) (PVOID)( (log) + 1 ) + + +// +// Manipulators. +// + +PTRACE_LOG +CreateTraceLog( + IN LONG LogSize, + IN LONG ExtraBytesInHeader, + IN LONG EntrySize + ); + +VOID +DestroyTraceLog( + IN PTRACE_LOG Log + ); + +LONG +WriteTraceLog( + IN PTRACE_LOG Log, + IN PVOID Entry + ); + +VOID +ResetTraceLog( + IN PTRACE_LOG Log + ); + + +#if defined(__cplusplus) +} // extern "C" +#endif // __cplusplus + + +#endif // _TRACELOG_H_ + diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/treehash.h b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/treehash.h new file mode 100644 index 0000000000..baa50726ce --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/treehash.h @@ -0,0 +1,850 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include +#include "rwlock.h" +#include "prime.h" + +template +class TREE_HASH_NODE +{ + template + friend class TREE_HASH_TABLE; + + private: + // Next node in the hash table look-aside + TREE_HASH_NODE<_Record> *_pNext; + + // links in the tree structure + TREE_HASH_NODE * _pParentNode; + TREE_HASH_NODE * _pFirstChild; + TREE_HASH_NODE * _pNextSibling; + + // actual record + _Record * _pRecord; + + // hash value + PCWSTR _pszPath; + DWORD _dwHash; +}; + +template +class TREE_HASH_TABLE +{ +protected: + typedef BOOL + (PFN_DELETE_IF)( + _Record * pRecord, + PVOID pvContext + ); + + typedef VOID + (PFN_APPLY)( + _Record * pRecord, + PVOID pvContext + ); + +public: + TREE_HASH_TABLE( + BOOL fCaseSensitive + ) : _ppBuckets( NULL ), + _nBuckets( 0 ), + _nItems( 0 ), + _fCaseSensitive( fCaseSensitive ) + { + } + + virtual + ~TREE_HASH_TABLE(); + + virtual + VOID + ReferenceRecord( + _Record * pRecord + ) = 0; + + virtual + VOID + DereferenceRecord( + _Record * pRecord + ) = 0; + + virtual + PCWSTR + GetKey( + _Record * pRecord + ) = 0; + + DWORD + Count() + { + return _nItems; + } + + virtual + VOID + Clear(); + + HRESULT + Initialize( + DWORD nBucketSize + ); + + DWORD + CalcHash( + PCWSTR pszKey + ) + { + return _fCaseSensitive ? HashString(pszKey) : HashStringNoCase(pszKey); + } + + virtual + VOID + FindKey( + PCWSTR pszKey, + _Record ** ppRecord + ); + + virtual + HRESULT + InsertRecord( + _Record * pRecord + ); + + virtual + VOID + DeleteKey( + PCWSTR pszKey + ); + + virtual + VOID + DeleteIf( + PFN_DELETE_IF pfnDeleteIf, + PVOID pvContext + ); + + VOID + Apply( + PFN_APPLY pfnApply, + PVOID pvContext + ); + +private: + + BOOL + FindNodeInternal( + PCWSTR pszKey, + DWORD dwHash, + TREE_HASH_NODE<_Record> ** ppNode, + TREE_HASH_NODE<_Record> *** pppPreviousNodeNextPointer = NULL + ); + + HRESULT + AddNodeInternal( + PCWSTR pszPath, + DWORD dwHash, + _Record * pRecord, + TREE_HASH_NODE<_Record> * pParentNode, + TREE_HASH_NODE<_Record> ** ppNewNode + ); + + HRESULT + AllocateNode( + PCWSTR pszPath, + DWORD dwHash, + _Record * pRecord, + TREE_HASH_NODE<_Record> * pParentNode, + TREE_HASH_NODE<_Record> ** ppNewNode + ); + + VOID + DeleteNode( + TREE_HASH_NODE<_Record> * pNode + ) + { + if (pNode->_pRecord != NULL) + { + DereferenceRecord(pNode->_pRecord); + pNode->_pRecord = NULL; + } + + HeapFree(GetProcessHeap(), + 0, + pNode); + } + + VOID + DeleteNodeInternal( + TREE_HASH_NODE<_Record> ** ppPreviousNodeNextPointer, + TREE_HASH_NODE<_Record> * pNode + ); + + VOID + RehashTableIfNeeded( + VOID + ); + + TREE_HASH_NODE<_Record> ** _ppBuckets; + DWORD _nBuckets; + DWORD _nItems; + BOOL _fCaseSensitive; + CWSDRWLock _tableLock; +}; + +template +HRESULT +TREE_HASH_TABLE<_Record>::AllocateNode( + PCWSTR pszPath, + DWORD dwHash, + _Record * pRecord, + TREE_HASH_NODE<_Record> * pParentNode, + TREE_HASH_NODE<_Record> ** ppNewNode +) +{ + // + // Allocate enough extra space for pszPath + // + DWORD cchPath = (DWORD) wcslen(pszPath); + if (cchPath >= ((0xffffffff - sizeof(TREE_HASH_NODE<_Record>))/sizeof(WCHAR) - 1)) + { + return HRESULT_FROM_WIN32(ERROR_NOT_ENOUGH_MEMORY); + } + TREE_HASH_NODE<_Record> *pNode = (TREE_HASH_NODE<_Record> *)HeapAlloc( + GetProcessHeap(), + HEAP_ZERO_MEMORY, + sizeof(TREE_HASH_NODE<_Record>) + (cchPath+1)*sizeof(WCHAR)); + if (pNode == NULL) + { + return HRESULT_FROM_WIN32(ERROR_NOT_ENOUGH_MEMORY); + } + + memcpy(pNode+1, pszPath, (cchPath+1)*sizeof(WCHAR)); + pNode->_pszPath = (PCWSTR)(pNode+1); + pNode->_dwHash = dwHash; + pNode->_pNext = pNode->_pNextSibling = pNode->_pFirstChild = NULL; + pNode->_pParentNode = pParentNode; + pNode->_pRecord = pRecord; + + *ppNewNode = pNode; + return S_OK; +} + +template +HRESULT +TREE_HASH_TABLE<_Record>::Initialize( + DWORD nBuckets +) +{ + HRESULT hr = S_OK; + + if ( nBuckets == 0 ) + { + hr = E_INVALIDARG; + goto Failed; + } + + hr = _tableLock.Init(); + if ( FAILED( hr ) ) + { + goto Failed; + } + + if (nBuckets >= 0xffffffff/sizeof(TREE_HASH_NODE<_Record> *)) + { + hr = E_INVALIDARG; + goto Failed; + } + + _ppBuckets = (TREE_HASH_NODE<_Record> **)HeapAlloc( + GetProcessHeap(), + HEAP_ZERO_MEMORY, + nBuckets*sizeof(TREE_HASH_NODE<_Record> *)); + if (_ppBuckets == NULL) + { + hr = HRESULT_FROM_WIN32(ERROR_NOT_ENOUGH_MEMORY); + goto Failed; + } + _nBuckets = nBuckets; + + return S_OK; + +Failed: + + if (_ppBuckets) + { + HeapFree(GetProcessHeap(), + 0, + _ppBuckets); + _ppBuckets = NULL; + } + + return hr; +} + + +template +TREE_HASH_TABLE<_Record>::~TREE_HASH_TABLE() +{ + if (_ppBuckets == NULL) + { + return; + } + + _ASSERTE(_nItems == 0); + + HeapFree(GetProcessHeap(), + 0, + _ppBuckets); + _ppBuckets = NULL; + _nBuckets = 0; +} + +template +VOID +TREE_HASH_TABLE<_Record>::Clear() +{ + TREE_HASH_NODE<_Record> *pCurrent; + TREE_HASH_NODE<_Record> *pNext; + + _tableLock.ExclusiveAcquire(); + + for (DWORD i=0; i<_nBuckets; i++) + { + pCurrent = _ppBuckets[i]; + _ppBuckets[i] = NULL; + while (pCurrent != NULL) + { + pNext = pCurrent->_pNext; + DeleteNode(pCurrent); + pCurrent = pNext; + } + } + + _nItems = 0; + _tableLock.ExclusiveRelease(); +} + +template +BOOL +TREE_HASH_TABLE<_Record>::FindNodeInternal( + PCWSTR pszKey, + DWORD dwHash, + TREE_HASH_NODE<_Record> ** ppNode, + TREE_HASH_NODE<_Record> *** pppPreviousNodeNextPointer +) +/*++ + Return value indicates whether the item is found + key, dwHash - key and hash for the node to find + ppNode - on successful return, the node found, on failed return, the first + node with hash value greater than the node to be found + pppPreviousNodeNextPointer - the pointer to previous node's _pNext + + This routine may be called under either read or write lock +--*/ +{ + TREE_HASH_NODE<_Record> **ppPreviousNodeNextPointer; + TREE_HASH_NODE<_Record> *pNode; + BOOL fFound = FALSE; + + ppPreviousNodeNextPointer = _ppBuckets + (dwHash % _nBuckets); + pNode = *ppPreviousNodeNextPointer; + while (pNode != NULL) + { + if (pNode->_dwHash == dwHash) + { + if (CompareStringOrdinal(pszKey, + -1, + pNode->_pszPath, + -1, + !_fCaseSensitive) == CSTR_EQUAL) + { + fFound = TRUE; + break; + } + } + else if (pNode->_dwHash > dwHash) + { + break; + } + + ppPreviousNodeNextPointer = &(pNode->_pNext); + pNode = *ppPreviousNodeNextPointer; + } + + *ppNode = pNode; + if (pppPreviousNodeNextPointer != NULL) + { + *pppPreviousNodeNextPointer = ppPreviousNodeNextPointer; + } + return fFound; +} + +template +VOID +TREE_HASH_TABLE<_Record>::FindKey( + PCWSTR pszKey, + _Record ** ppRecord +) +{ + TREE_HASH_NODE<_Record> *pNode; + + *ppRecord = NULL; + + DWORD dwHash = CalcHash(pszKey); + + _tableLock.SharedAcquire(); + + if (FindNodeInternal(pszKey, dwHash, &pNode) && + pNode->_pRecord != NULL) + { + ReferenceRecord(pNode->_pRecord); + *ppRecord = pNode->_pRecord; + } + + _tableLock.SharedRelease(); +} + +template +HRESULT +TREE_HASH_TABLE<_Record>::AddNodeInternal( + PCWSTR pszPath, + DWORD dwHash, + _Record * pRecord, + TREE_HASH_NODE<_Record> * pParentNode, + TREE_HASH_NODE<_Record> ** ppNewNode +) +/*++ + Return value is HRESULT indicating sucess or failure + pszPath, dwHash, pRecord - path, hash value and record to be inserted + pParentNode - this will be the parent of the node being inserted + ppNewNode - on successful return, the new node created and inserted + + This function may be called under a read or write lock +--*/ +{ + TREE_HASH_NODE<_Record> *pNewNode; + TREE_HASH_NODE<_Record> *pNextNode; + TREE_HASH_NODE<_Record> **ppNextPointer; + HRESULT hr; + + // + // Ownership of pRecord is not transferred to pNewNode yet, so remember + // to either set it to null before deleting pNewNode or add an extra + // reference later - this is to make sure we do not do an extra ref/deref + // which users may view as getting flushed out of the hash-table + // + hr = AllocateNode(pszPath, + dwHash, + pRecord, + pParentNode, + &pNewNode); + if (FAILED(hr)) + { + return hr; + } + + do + { + // + // Find the right place to add this node + // + + if (FindNodeInternal(pszPath, dwHash, &pNextNode, &ppNextPointer)) + { + // + // If node already there, record may still need updating + // + if (pRecord != NULL && + InterlockedCompareExchangePointer((PVOID *)&pNextNode->_pRecord, + pRecord, + NULL) == NULL) + { + ReferenceRecord(pRecord); + hr = S_OK; + } + else + { + hr = HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS); + } + + // ownership of pRecord has either passed to existing record or + // not to anyone at all + pNewNode->_pRecord = NULL; + DeleteNode(pNewNode); + *ppNewNode = pNextNode; + return hr; + } + + // + // If another node got inserted in betwen, we will have to retry + // + pNewNode->_pNext = pNextNode; + } while (InterlockedCompareExchangePointer((PVOID *)ppNextPointer, + pNewNode, + pNextNode) != pNextNode); + // pass ownership of pRecord now + if (pRecord != NULL) + { + ReferenceRecord(pRecord); + pRecord = NULL; + } + InterlockedIncrement((LONG *)&_nItems); + + // + // update the parent + // + if (pParentNode != NULL) + { + ppNextPointer = &pParentNode->_pFirstChild; + do + { + pNextNode = *ppNextPointer; + pNewNode->_pNextSibling = pNextNode; + } while (InterlockedCompareExchangePointer((PVOID *)ppNextPointer, + pNewNode, + pNextNode) != pNextNode); + } + + *ppNewNode = pNewNode; + return S_OK; +} + +template +HRESULT +TREE_HASH_TABLE<_Record>::InsertRecord( + _Record * pRecord +) +/*++ + This method inserts a node for this record and also empty nodes for paths + in the heirarchy leading upto this path + + The insert is done under only a read-lock - this is possible by keeping + the hashes in a bucket in increasing order and using interlocked operations + to actually insert the item in the hash-bucket lookaside list and the parent + children list + + Returns HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS) if the record already exists. + Never leak this error to the end user because "*file* already exists" may be confusing. +--*/ +{ + PCWSTR pszKey = GetKey(pRecord); + STACK_STRU( strPartialPath, 256); + PWSTR pszPartialPath; + DWORD dwHash; + DWORD cchEnd; + HRESULT hr; + TREE_HASH_NODE<_Record> *pParentNode = NULL; + + hr = strPartialPath.Copy(pszKey); + if (FAILED(hr)) + { + goto Finished; + } + pszPartialPath = strPartialPath.QueryStr(); + + _tableLock.SharedAcquire(); + + // + // First find the lowest parent node present + // + for (cchEnd = strPartialPath.QueryCCH() - 1; cchEnd > 0; cchEnd--) + { + if (pszPartialPath[cchEnd] == L'/' || pszPartialPath[cchEnd] == L'\\') + { + pszPartialPath[cchEnd] = L'\0'; + + dwHash = CalcHash(pszPartialPath); + if (FindNodeInternal(pszPartialPath, dwHash, &pParentNode)) + { + pszPartialPath[cchEnd] = pszKey[cchEnd]; + break; + } + pParentNode = NULL; + } + } + + // + // Now go ahead and add the rest of the tree (including our record) + // + for (; cchEnd <= strPartialPath.QueryCCH(); cchEnd++) + { + if (pszPartialPath[cchEnd] == L'\0') + { + dwHash = CalcHash(pszPartialPath); + hr = AddNodeInternal( + pszPartialPath, + dwHash, + (cchEnd == strPartialPath.QueryCCH()) ? pRecord : NULL, + pParentNode, + &pParentNode); + if (FAILED(hr) && + hr != HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS)) + { + goto Finished; + } + + pszPartialPath[cchEnd] = pszKey[cchEnd]; + } + } + +Finished: + _tableLock.SharedRelease(); + + if (SUCCEEDED(hr)) + { + RehashTableIfNeeded(); + } + + return hr; +} + +template +VOID +TREE_HASH_TABLE<_Record>::DeleteNodeInternal( + TREE_HASH_NODE<_Record> ** ppNextPointer, + TREE_HASH_NODE<_Record> * pNode +) +/*++ + pNode is the node to be deleted + ppNextPointer is the pointer to the previous node's next pointer pointing + to this node + + This function should be called under write-lock +--*/ +{ + // + // First remove this node from hash table + // + *ppNextPointer = pNode->_pNext; + + // + // Now fixup parent + // + if (pNode->_pParentNode != NULL) + { + ppNextPointer = &pNode->_pParentNode->_pFirstChild; + while (*ppNextPointer != pNode) + { + ppNextPointer = &(*ppNextPointer)->_pNextSibling; + } + *ppNextPointer = pNode->_pNextSibling; + } + + // + // Now remove all children recursively + // + TREE_HASH_NODE<_Record> *pChild = pNode->_pFirstChild; + TREE_HASH_NODE<_Record> *pNextChild; + while (pChild != NULL) + { + pNextChild = pChild->_pNextSibling; + + ppNextPointer = _ppBuckets + (pChild->_dwHash % _nBuckets); + while (*ppNextPointer != pChild) + { + ppNextPointer = &(*ppNextPointer)->_pNext; + } + pChild->_pParentNode = NULL; + DeleteNodeInternal(ppNextPointer, pChild); + + pChild = pNextChild; + } + + DeleteNode(pNode); + _nItems--; +} + +template +VOID +TREE_HASH_TABLE<_Record>::DeleteKey( + PCWSTR pszKey +) +{ + TREE_HASH_NODE<_Record> *pNode; + TREE_HASH_NODE<_Record> **ppPreviousNodeNextPointer; + + DWORD dwHash = CalcHash(pszKey); + + _tableLock.ExclusiveAcquire(); + + if (FindNodeInternal(pszKey, dwHash, &pNode, &ppPreviousNodeNextPointer)) + { + DeleteNodeInternal(ppPreviousNodeNextPointer, pNode); + } + + _tableLock.ExclusiveRelease(); +} + +template +VOID +TREE_HASH_TABLE<_Record>::DeleteIf( + PFN_DELETE_IF pfnDeleteIf, + PVOID pvContext +) +{ + TREE_HASH_NODE<_Record> *pNode; + TREE_HASH_NODE<_Record> **ppPreviousNodeNextPointer; + BOOL fDelete; + + _tableLock.ExclusiveAcquire(); + + for (DWORD i=0; i<_nBuckets; i++) + { + ppPreviousNodeNextPointer = _ppBuckets + i; + pNode = *ppPreviousNodeNextPointer; + while (pNode != NULL) + { + // + // Non empty nodes deleted based on DeleteIf, empty nodes deleted + // if they have no children + // + fDelete = FALSE; + if (pNode->_pRecord != NULL) + { + if (pfnDeleteIf(pNode->_pRecord, pvContext)) + { + fDelete = TRUE; + } + } + else if (pNode->_pFirstChild == NULL) + { + fDelete = TRUE; + } + + if (fDelete) + { + if (pNode->_pFirstChild == NULL) + { + DeleteNodeInternal(ppPreviousNodeNextPointer, pNode); + } + else + { + DereferenceRecord(pNode->_pRecord); + pNode->_pRecord = NULL; + } + } + else + { + ppPreviousNodeNextPointer = &pNode->_pNext; + } + + pNode = *ppPreviousNodeNextPointer; + } + } + + _tableLock.ExclusiveRelease(); +} + +template +VOID +TREE_HASH_TABLE<_Record>::Apply( + PFN_APPLY pfnApply, + PVOID pvContext +) +{ + TREE_HASH_NODE<_Record> *pNode; + + _tableLock.SharedAcquire(); + + for (DWORD i=0; i<_nBuckets; i++) + { + pNode = _ppBuckets[i]; + while (pNode != NULL) + { + if (pNode->_pRecord != NULL) + { + pfnApply(pNode->_pRecord, pvContext); + } + + pNode = pNode->_pNext; + } + } + + _tableLock.SharedRelease(); +} + +template +VOID +TREE_HASH_TABLE<_Record>::RehashTableIfNeeded( + VOID +) +{ + TREE_HASH_NODE<_Record> **ppBuckets; + DWORD nBuckets; + TREE_HASH_NODE<_Record> *pNode; + TREE_HASH_NODE<_Record> *pNextNode; + TREE_HASH_NODE<_Record> **ppNextPointer; + TREE_HASH_NODE<_Record> *pNewNextNode; + DWORD nNewBuckets; + + // + // If number of items has become too many, we will double the hash table + // size (we never reduce it however) + // + if (_nItems <= PRIME::GetPrime(2*_nBuckets)) + { + return; + } + + _tableLock.ExclusiveAcquire(); + + nNewBuckets = PRIME::GetPrime(2*_nBuckets); + + if (_nItems <= nNewBuckets) + { + goto Finished; + } + + nBuckets = nNewBuckets; + if (nBuckets >= 0xffffffff/sizeof(TREE_HASH_NODE<_Record> *)) + { + goto Finished; + } + ppBuckets = (TREE_HASH_NODE<_Record> **)HeapAlloc( + GetProcessHeap(), + HEAP_ZERO_MEMORY, + nBuckets*sizeof(TREE_HASH_NODE<_Record> *)); + if (ppBuckets == NULL) + { + goto Finished; + } + + // + // Take out nodes from the old hash table and insert in the new one, make + // sure to keep the hashes in increasing order + // + for (DWORD i=0; i<_nBuckets; i++) + { + pNode = _ppBuckets[i]; + while (pNode != NULL) + { + pNextNode = pNode->_pNext; + + ppNextPointer = ppBuckets + (pNode->_dwHash % nBuckets); + pNewNextNode = *ppNextPointer; + while (pNewNextNode != NULL && + pNewNextNode->_dwHash <= pNode->_dwHash) + { + ppNextPointer = &pNewNextNode->_pNext; + pNewNextNode = pNewNextNode->_pNext; + } + pNode->_pNext = pNewNextNode; + *ppNextPointer = pNode; + + pNode = pNextNode; + } + } + + HeapFree(GetProcessHeap(), 0, _ppBuckets); + _ppBuckets = ppBuckets; + _nBuckets = nBuckets; + ppBuckets = NULL; + +Finished: + + _tableLock.ExclusiveRelease(); +} + diff --git a/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/util.cxx b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/util.cxx new file mode 100644 index 0000000000..214ee65abf --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV1/IISLib/util.cxx @@ -0,0 +1,78 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "precomp.h" + +HRESULT +MakePathCanonicalizationProof( + IN PCWSTR pszName, + OUT STRU * pstrPath +) +/*++ + +Routine Description: + + This functions adds a prefix + to the string, which is "\\?\UNC\" for a UNC path, and "\\?\" for + other paths. This prefix tells Windows not to parse the path. + +Arguments: + + IN pszName - The path to be converted + OUT pstrPath - Output path created + +Return Values: + + HRESULT + +--*/ +{ + HRESULT hr; + + if (pszName[0] == L'\\' && pszName[1] == L'\\') + { + // + // If the path is already canonicalized, just return + // + + if ((pszName[2] == '?' || pszName[2] == '.') && + pszName[3] == '\\') + { + hr = pstrPath->Copy(pszName); + + if (SUCCEEDED(hr)) + { + // + // If the path was in DOS form ("\\.\"), + // we need to change it to Win32 from ("\\?\") + // + + pstrPath->QueryStr()[2] = L'?'; + } + + return hr; + } + + pszName += 2; + + + if (FAILED(hr = pstrPath->Copy(L"\\\\?\\UNC\\"))) + { + return hr; + } + } + else if (wcslen(pszName) > MAX_PATH) + { + if (FAILED(hr = pstrPath->Copy(L"\\\\?\\"))) + { + return hr; + } + } + else + { + pstrPath->Reset(); + } + + return pstrPath->Append(pszName); +} + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/AppOfflineApplication.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/AppOfflineApplication.cpp new file mode 100644 index 0000000000..abae1963d5 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/AppOfflineApplication.cpp @@ -0,0 +1,62 @@ +// 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. + +#include "AppOfflineApplication.h" + +#include "HandleWrapper.h" +#include "AppOfflineHandler.h" +#include "exceptions.h" + +HRESULT AppOfflineApplication::CreateHandler(IHttpContext* pHttpContext, IREQUEST_HANDLER** pRequestHandler) +{ + try + { + auto handler = std::make_unique(*pHttpContext, m_strAppOfflineContent); + *pRequestHandler = handler.release(); + } + CATCH_RETURN(); + + return S_OK; +} + +HRESULT AppOfflineApplication::OnAppOfflineFound() +{ + LARGE_INTEGER li = {}; + + HandleWrapper handle = CreateFile(m_appOfflineLocation.c_str(), + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + nullptr); + + RETURN_LAST_ERROR_IF(handle == INVALID_HANDLE_VALUE); + + RETURN_LAST_ERROR_IF(!GetFileSizeEx(handle, &li)); + + if (li.HighPart != 0) + { + // > 4gb file size not supported + // todo: log a warning at event log + return E_INVALIDARG; + } + + if (li.LowPart > 0) + { + DWORD bytesRead = 0; + std::string pszBuff(static_cast(li.LowPart) + 1, '\0'); + + RETURN_LAST_ERROR_IF(!ReadFile(handle, pszBuff.data(), li.LowPart, &bytesRead, nullptr)); + pszBuff.resize(bytesRead); + + m_strAppOfflineContent = pszBuff; + } + + return S_OK; +} + +bool AppOfflineApplication::ShouldBeStarted(const IHttpApplication& pApplication) +{ + return FileExists(GetAppOfflineLocation(pApplication)); +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/AppOfflineApplication.h b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/AppOfflineApplication.h new file mode 100644 index 0000000000..2c81ae98b1 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/AppOfflineApplication.h @@ -0,0 +1,28 @@ +// 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. + +#pragma once + +#include "application.h" +#include "requesthandler.h" +#include "PollingAppOfflineApplication.h" + +class AppOfflineApplication: public PollingAppOfflineApplication +{ +public: + AppOfflineApplication(const IHttpApplication& pApplication) + : PollingAppOfflineApplication(pApplication, PollingAppOfflineApplicationMode::StopWhenRemoved) + { + CheckAppOffline(); + } + + HRESULT CreateHandler(IHttpContext* pHttpContext, IREQUEST_HANDLER** pRequestHandler) override; + + HRESULT OnAppOfflineFound() override; + + static bool ShouldBeStarted(const IHttpApplication& pApplication); + +private: + std::string m_strAppOfflineContent; +}; + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/AppOfflineHandler.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/AppOfflineHandler.cpp new file mode 100644 index 0000000000..e3574d88e8 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/AppOfflineHandler.cpp @@ -0,0 +1,30 @@ +// 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. + +#include "AppOfflineHandler.h" + +#include "HandleWrapper.h" + +REQUEST_NOTIFICATION_STATUS AppOfflineHandler::ExecuteRequestHandler() +{ + HTTP_DATA_CHUNK DataChunk {}; + auto pResponse = m_pContext.GetResponse(); + + DBG_ASSERT(pResponse); + + // Ignore failure hresults as nothing we can do + // Set fTrySkipCustomErrors to true as we want client see the offline content + pResponse->SetStatus(503, "Service Unavailable", 0, S_OK, nullptr, TRUE); + pResponse->SetHeader("Content-Type", + "text/html", + static_cast(strlen("text/html")), + FALSE + ); + + DataChunk.DataChunkType = HttpDataChunkFromMemory; + DataChunk.FromMemory.pBuffer = m_strAppOfflineContent.data(); + DataChunk.FromMemory.BufferLength = static_cast(m_strAppOfflineContent.size()); + pResponse->WriteEntityChunkByReference(&DataChunk); + + return REQUEST_NOTIFICATION_STATUS::RQ_NOTIFICATION_FINISH_REQUEST; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/AppOfflineHandler.h b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/AppOfflineHandler.h new file mode 100644 index 0000000000..87f729bf82 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/AppOfflineHandler.h @@ -0,0 +1,24 @@ +// 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. + +#pragma once + +#include +#include "requesthandler.h" + +class AppOfflineHandler: public REQUEST_HANDLER +{ +public: + AppOfflineHandler(IHttpContext& pContext, const std::string appOfflineContent) + : REQUEST_HANDLER(pContext), + m_pContext(pContext), + m_strAppOfflineContent(appOfflineContent) + { + } + + REQUEST_NOTIFICATION_STATUS ExecuteRequestHandler() override; + +private: + IHttpContext& m_pContext; + std::string m_strAppOfflineContent; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/ApplicationFactory.h b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/ApplicationFactory.h new file mode 100644 index 0000000000..ed9733b68a --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/ApplicationFactory.h @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include +#include +#include +#include "iapplication.h" +#include "HandleWrapper.h" + +typedef +HRESULT +(WINAPI * PFN_ASPNETCORE_CREATE_APPLICATION)( + _In_ IHttpServer *pServer, + _In_ const IHttpApplication * pHttpApplication, + _In_ APPLICATION_PARAMETER *pParameters, + _In_ DWORD nParameters, + _Out_ IAPPLICATION **pApplication + ); + +class ApplicationFactory +{ +public: + ApplicationFactory(HMODULE hRequestHandlerDll, std::wstring location, PFN_ASPNETCORE_CREATE_APPLICATION pfnAspNetCoreCreateApplication) noexcept: + m_pfnAspNetCoreCreateApplication(pfnAspNetCoreCreateApplication), + m_location(std::move(location)), + m_hRequestHandlerDll(hRequestHandlerDll) + { + } + + HRESULT Execute( + _In_ IHttpServer *pServer, + _In_ IHttpContext *pHttpContext, + _Outptr_ IAPPLICATION **pApplication) const + { + // m_location.data() is const ptr copy to local to get mutable pointer + auto location = m_location; + std::array parameters { + { + {"InProcessExeLocation", location.data()}, + {"TraceContext", pHttpContext->GetTraceContext()} + } + }; + + return m_pfnAspNetCoreCreateApplication(pServer, pHttpContext->GetApplication(), parameters.data(), static_cast(parameters.size()), pApplication); + } + +private: + PFN_ASPNETCORE_CREATE_APPLICATION m_pfnAspNetCoreCreateApplication; + std::wstring m_location; + HandleWrapper m_hRequestHandlerDll; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/AspNetCore.vcxproj b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/AspNetCore.vcxproj new file mode 100644 index 0000000000..98c5920ed4 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/AspNetCore.vcxproj @@ -0,0 +1,305 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {EC82302F-D2F0-4727-99D1-EABC0DD9DC3B} + Win32Proj + AspNetCoreModule + AspNetCore + aspnetcorev2 + false + 10.0.15063.0 + + + + DynamicLibrary + true + v141 + Unicode + + + DynamicLibrary + true + v141 + Unicode + + + DynamicLibrary + false + v141 + true + Unicode + + + DynamicLibrary + false + v141 + true + Unicode + + + + + + + + + + + + + + + + + + + + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + + Level4 + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;ASPNETCOREMODULE_EXPORTS;%(PreprocessorDefinitions) + stdafx.h + $(IntDir)$(TargetName).pch + ..\IISLib;.\Inc;..\CommonLib + ProgramDatabase + MultiThreadedDebug + true + true + true + false + SyncCThrow + 8Bytes + true + false + true + CompileAsCpp + true + true + stdcpp17 + stdafx.h + + + Windows + true + kernel32.lib;user32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;ahadmin.lib;ws2_32.lib;iphlpapi.lib;version.lib + Source.def + UseLinkTimeCodeGeneration + + + ..\Commonlib + + + + + Level4 + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;ASPNETCOREMODULE_EXPORTS;%(PreprocessorDefinitions) + stdafx.h + $(IntDir)$(TargetName).pch + ..\IISLib;.\Inc;..\CommonLib + ProgramDatabase + MultiThreadedDebug + true + true + true + false + SyncCThrow + 8Bytes + true + false + true + CompileAsCpp + true + true + stdcpp17 + stdafx.h + + + Windows + true + kernel32.lib;user32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;ahadmin.lib;ws2_32.lib;iphlpapi.lib;version.lib + Source.def + UseLinkTimeCodeGeneration + + + ..\Commonlib + + + + + Level4 + NotUsing + MaxSpeed + true + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;ASPNETCOREMODULE_EXPORTS;%(PreprocessorDefinitions) + ..\IISLib;.\Inc;..\CommonLib + stdafx.h + MultiThreaded + true + true + true + false + SyncCThrow + 8Bytes + true + false + true + CompileAsCpp + true + true + stdcpp17 + stdafx.h + + + Windows + false + true + /NODEFAULTLIB:libucrt.lib /DEFAULTLIB:ucrt.lib %(AdditionalOptions) + true + Source.def + kernel32.lib;user32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;ahadmin.lib;ws2_32.lib;iphlpapi.lib;version.lib + UseLinkTimeCodeGeneration + + + ..\Commonlib + + + + + Level4 + NotUsing + MaxSpeed + true + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;ASPNETCOREMODULE_EXPORTS;%(PreprocessorDefinitions) + stdafx.h + ..\IISLib;.\Inc;..\CommonLib + MultiThreaded + true + true + true + false + SyncCThrow + 8Bytes + true + false + true + CompileAsCpp + true + true + stdcpp17 + stdafx.h + + + Windows + false + true + /NODEFAULTLIB:libucrt.lib /DEFAULTLIB:ucrt.lib %(AdditionalOptions) + true + Source.def + kernel32.lib;user32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;ahadmin.lib;ws2_32.lib;iphlpapi.lib;version.lib + UseLinkTimeCodeGeneration + + + ..\Commonlib + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create + Create + Create + Create + + + + + + {55494e58-e061-4c4c-a0a8-837008e72f85} + + + {09d9d1d6-2951-4e14-bc35-76a23cf9391a} + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + + + + true + + + true + + + + + + + + + + \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/DisconnectHandler.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/DisconnectHandler.cpp new file mode 100644 index 0000000000..ad2ff17b47 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/DisconnectHandler.cpp @@ -0,0 +1,57 @@ +// 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. + +#include "DisconnectHandler.h" +#include "exceptions.h" +#include "proxymodule.h" +#include "SRWExclusiveLock.h" + +void DisconnectHandler::NotifyDisconnect() +{ + try + { + std::unique_ptr pHandler; + { + SRWExclusiveLock lock(m_handlerLock); + m_pHandler.swap(pHandler); + m_disconnectFired = true; + } + + if (pHandler != nullptr) + { + pHandler->NotifyDisconnect(); + } + } + catch (...) + { + OBSERVE_CAUGHT_EXCEPTION(); + } +} + +void DisconnectHandler::CleanupStoredContext() noexcept +{ + delete this; +} + +void DisconnectHandler::SetHandler(std::unique_ptr handler) +{ + IREQUEST_HANDLER* pHandler = nullptr; + { + SRWExclusiveLock lock(m_handlerLock); + + handler.swap(m_pHandler); + pHandler = m_pHandler.get(); + } + + assert(pHandler != nullptr); + + if (pHandler != nullptr && (m_disconnectFired || m_pHttpConnection != nullptr && !m_pHttpConnection->IsConnected())) + { + pHandler->NotifyDisconnect(); + } +} + +void DisconnectHandler::RemoveHandler() noexcept +{ + m_pHandler = nullptr; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/DisconnectHandler.h b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/DisconnectHandler.h new file mode 100644 index 0000000000..36d404edd6 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/DisconnectHandler.h @@ -0,0 +1,43 @@ +// 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. + +#pragma once + +#include +#include "irequesthandler.h" + +class ASPNET_CORE_PROXY_MODULE; + +class DisconnectHandler final: public IHttpConnectionStoredContext +{ +public: + DisconnectHandler(IHttpConnection* pHttpConnection) + : m_pHandler(nullptr), m_pHttpConnection(pHttpConnection), m_disconnectFired(false) + { + InitializeSRWLock(&m_handlerLock); + } + + virtual + ~DisconnectHandler() + { + RemoveHandler(); + } + + void + NotifyDisconnect() override; + + void + CleanupStoredContext() noexcept override; + + void + SetHandler(std::unique_ptr handler); + + void RemoveHandler() noexcept; + +private: + SRWLOCK m_handlerLock {}; + std::unique_ptr m_pHandler; + IHttpConnection* m_pHttpConnection; + bool m_disconnectFired; +}; + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/HandlerResolver.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/HandlerResolver.cpp new file mode 100644 index 0000000000..44210d8b68 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/HandlerResolver.cpp @@ -0,0 +1,326 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "HandlerResolver.h" +#include "exceptions.h" +#include "SRWExclusiveLock.h" +#include "applicationinfo.h" +#include "EventLog.h" +#include "hostfxr_utility.h" +#include "GlobalVersionUtility.h" +#include "HandleWrapper.h" +#include "file_utility.h" +#include "LoggingHelpers.h" +#include "resources.h" +#include "ConfigurationLoadException.h" +#include "WebConfigConfigurationSource.h" +#include "ModuleHelpers.h" +#include "BaseOutputManager.h" +#include "Environment.h" + +const PCWSTR HandlerResolver::s_pwzAspnetcoreInProcessRequestHandlerName = L"aspnetcorev2_inprocess.dll"; +const PCWSTR HandlerResolver::s_pwzAspnetcoreOutOfProcessRequestHandlerName = L"aspnetcorev2_outofprocess.dll"; + +HandlerResolver::HandlerResolver(HMODULE hModule, const IHttpServer &pServer) + : m_hModule(hModule), + m_pServer(pServer), + m_loadedApplicationHostingModel(HOSTING_UNKNOWN) +{ + InitializeSRWLock(&m_requestHandlerLoadLock); +} + +HRESULT +HandlerResolver::LoadRequestHandlerAssembly(const IHttpApplication &pApplication, const ShimOptions& pConfiguration, std::unique_ptr& pApplicationFactory) +{ + HRESULT hr = S_OK; + PCWSTR pstrHandlerDllName = nullptr; + bool preventUnload = false; + if (pConfiguration.QueryHostingModel() == APP_HOSTING_MODEL::HOSTING_IN_PROCESS) + { + preventUnload = false; + pstrHandlerDllName = s_pwzAspnetcoreInProcessRequestHandlerName; + } + else + { + // OutOfProcess handler is not able to handle unload correctly + // It has code running after application.Stop exits + preventUnload = true; + pstrHandlerDllName = s_pwzAspnetcoreOutOfProcessRequestHandlerName; + } + HandleWrapper hRequestHandlerDll; + std::wstring location; + std::wstring handlerDllPath; + // Try to see if RH is already loaded, use GetModuleHandleEx to increment ref count + if (!GetModuleHandleEx(0, pstrHandlerDllName, &hRequestHandlerDll)) + { + if (pConfiguration.QueryHostingModel() == APP_HOSTING_MODEL::HOSTING_IN_PROCESS) + { + std::unique_ptr options; + std::unique_ptr outputManager; + + RETURN_IF_FAILED(HOSTFXR_OPTIONS::Create( + L"", + pConfiguration.QueryProcessPath(), + pApplication.GetApplicationPhysicalPath(), + pConfiguration.QueryArguments(), + options)); + + location = options->GetDotnetExeLocation(); + + RETURN_IF_FAILED(LoggingHelpers::CreateLoggingProvider( + pConfiguration.QueryStdoutLogEnabled(), + !m_pServer.IsCommandLineLaunch(), + pConfiguration.QueryStdoutLogFile().c_str(), + pApplication.GetApplicationPhysicalPath(), + outputManager)); + + + hr = FindNativeAssemblyFromHostfxr(*options.get(), pstrHandlerDllName, handlerDllPath, outputManager.get()); + + if (FAILED_LOG(hr)) + { + auto output = outputManager->GetStdOutContent(); + + EventLog::Error( + ASPNETCORE_EVENT_GENERAL_ERROR, + ASPNETCORE_EVENT_INPROCESS_RH_ERROR_MSG, + output.c_str()); + return hr; + } + } + else + { + if (FAILED_LOG(hr = FindNativeAssemblyFromGlobalLocation(pConfiguration, pstrHandlerDllName, handlerDllPath))) + { + EventLog::Error( + ASPNETCORE_EVENT_OUT_OF_PROCESS_RH_MISSING, + ASPNETCORE_EVENT_OUT_OF_PROCESS_RH_MISSING_MSG, + handlerDllPath.empty() ? s_pwzAspnetcoreOutOfProcessRequestHandlerName : handlerDllPath.c_str()); + + return hr; + } + } + + LOG_INFOF(L"Loading request handler: '%ls'", handlerDllPath.c_str()); + + hRequestHandlerDll = LoadLibrary(handlerDllPath.c_str()); + RETURN_LAST_ERROR_IF_NULL(hRequestHandlerDll); + if (preventUnload) + { + // Pin module in memory + GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_PIN, handlerDllPath.c_str(), &hRequestHandlerDll); + } + } + + auto pfnAspNetCoreCreateApplication = ModuleHelpers::GetKnownProcAddress(hRequestHandlerDll, "CreateApplication"); + RETURN_LAST_ERROR_IF_NULL(pfnAspNetCoreCreateApplication); + + pApplicationFactory = std::make_unique(hRequestHandlerDll.release(), location, pfnAspNetCoreCreateApplication); + return S_OK; +} + +HRESULT +HandlerResolver::GetApplicationFactory(const IHttpApplication &pApplication, std::unique_ptr& pApplicationFactory, const ShimOptions& options) +{ + SRWExclusiveLock lock(m_requestHandlerLoadLock); + if (m_loadedApplicationHostingModel != HOSTING_UNKNOWN) + { + // Mixed hosting models + if (m_loadedApplicationHostingModel != options.QueryHostingModel()) + { + EventLog::Error( + ASPNETCORE_EVENT_MIXED_HOSTING_MODEL_ERROR, + ASPNETCORE_EVENT_MIXED_HOSTING_MODEL_ERROR_MSG, + pApplication.GetApplicationId(), + options.QueryHostingModel()); + + return E_FAIL; + } + // Multiple in-process apps + if (m_loadedApplicationHostingModel == HOSTING_IN_PROCESS && m_loadedApplicationId != pApplication.GetApplicationId()) + { + EventLog::Error( + ASPNETCORE_EVENT_DUPLICATED_INPROCESS_APP, + ASPNETCORE_EVENT_DUPLICATED_INPROCESS_APP_MSG, + pApplication.GetApplicationId()); + + return E_FAIL; + } + } + + m_loadedApplicationHostingModel = options.QueryHostingModel(); + m_loadedApplicationId = pApplication.GetApplicationId(); + RETURN_IF_FAILED(LoadRequestHandlerAssembly(pApplication, options, pApplicationFactory)); + + return S_OK; +} + +void HandlerResolver::ResetHostingModel() +{ + SRWExclusiveLock lock(m_requestHandlerLoadLock); + + m_loadedApplicationHostingModel = APP_HOSTING_MODEL::HOSTING_UNKNOWN; + m_loadedApplicationId.resize(0); +} + +HRESULT +HandlerResolver::FindNativeAssemblyFromGlobalLocation( + const ShimOptions& pConfiguration, + PCWSTR pstrHandlerDllName, + std::wstring& handlerDllPath +) +{ + try + { + auto handlerPath = Environment::GetEnvironmentVariableValue(L"ASPNETCORE_MODULE_OUTOFPROCESS_HANDLER"); + if (handlerPath.has_value() && std::filesystem::is_regular_file(handlerPath.value())) + { + handlerDllPath = handlerPath.value(); + return S_OK; + } + + std::wstring modulePath = GlobalVersionUtility::GetModuleName(m_hModule); + + modulePath = GlobalVersionUtility::RemoveFileNameFromFolderPath(modulePath); + + handlerDllPath = GlobalVersionUtility::GetGlobalRequestHandlerPath(modulePath.c_str(), + pConfiguration.QueryHandlerVersion().c_str(), + pstrHandlerDllName + ); + } + catch (...) + { + EventLog::Info( + ASPNETCORE_EVENT_OUT_OF_PROCESS_RH_MISSING, + ASPNETCORE_EVENT_OUT_OF_PROCESS_RH_MISSING_MSG, + pstrHandlerDllName); + + return OBSERVE_CAUGHT_EXCEPTION(); + } + + return S_OK; +} + +// +// Tries to find aspnetcorerh.dll from the application +// Calls into hostfxr.dll to find it. +// Will leave hostfxr.dll loaded as it will be used again to call hostfxr_main. +// +HRESULT +HandlerResolver::FindNativeAssemblyFromHostfxr( + const HOSTFXR_OPTIONS& hostfxrOptions, + PCWSTR libraryName, + std::wstring& handlerDllPath, + BaseOutputManager* outputManager +) +{ + std::wstring struNativeSearchPaths; + size_t intIndex = 0; + size_t intPrevIndex = 0; + DWORD dwBufferSize = s_initialGetNativeSearchDirectoriesBufferSize; + DWORD dwRequiredBufferSize = 0; + hostfxr_get_native_search_directories_fn pFnHostFxrSearchDirectories = nullptr; + + RETURN_LAST_ERROR_IF_NULL(m_hHostFxrDll = LoadLibraryW(hostfxrOptions.GetHostFxrLocation().c_str())); + + try + { + pFnHostFxrSearchDirectories = ModuleHelpers::GetKnownProcAddress(m_hHostFxrDll, "hostfxr_get_native_search_directories"); + } + catch (...) + { + EventLog::Error( + ASPNETCORE_EVENT_GENERAL_ERROR, + ASPNETCORE_EVENT_HOSTFXR_DLL_INVALID_VERSION_MSG, + hostfxrOptions.GetHostFxrLocation().c_str() + ); + return OBSERVE_CAUGHT_EXCEPTION(); + } + + RETURN_LAST_ERROR_IF_NULL(pFnHostFxrSearchDirectories); + struNativeSearchPaths.resize(dwBufferSize); + + outputManager->TryStartRedirection(); + + while (TRUE) + { + DWORD hostfxrArgc; + std::unique_ptr hostfxrArgv; + + hostfxrOptions.GetArguments(hostfxrArgc, hostfxrArgv); + const auto intHostFxrExitCode = pFnHostFxrSearchDirectories( + hostfxrArgc, + hostfxrArgv.get(), + struNativeSearchPaths.data(), + dwBufferSize, + &dwRequiredBufferSize + ); + + if (intHostFxrExitCode == 0) + { + break; + } + else if (dwRequiredBufferSize > dwBufferSize) + { + dwBufferSize = dwRequiredBufferSize + 1; // for null terminator + + struNativeSearchPaths.resize(dwBufferSize); + } + else + { + // Stop redirecting before logging to event log to avoid logging debug logs + // twice. + outputManager->TryStopRedirection(); + + // If hostfxr didn't set the required buffer size, something in the app is misconfigured + // Ex: Framework not found. + EventLog::Error( + ASPNETCORE_EVENT_GENERAL_ERROR, + ASPNETCORE_EVENT_HOSTFXR_FAILURE_MSG + ); + + return E_UNEXPECTED; + } + } + + outputManager->TryStopRedirection(); + + struNativeSearchPaths.resize(struNativeSearchPaths.find(L'\0')); + + auto fFound = FALSE; + + // The native search directories are semicolon delimited. + // Split on semicolons, append aspnetcorerh.dll, and check if the file exists. + while ((intIndex = struNativeSearchPaths.find(L';', intPrevIndex)) != std::wstring::npos) + { + auto path = struNativeSearchPaths.substr(intPrevIndex, intIndex - intPrevIndex); + + if (!path.empty() && !(path[path.length() - 1] == L'\\')) + { + path.append(L"\\"); + } + + path.append(libraryName); + + if (std::filesystem::is_regular_file(path)) + { + handlerDllPath = path; + fFound = TRUE; + break; + } + + intPrevIndex = intIndex + 1; + } + + if (!fFound) + { + EventLog::Error( + ASPNETCORE_EVENT_GENERAL_ERROR, + ASPNETCORE_EVENT_INPROCESS_RH_REFERENCE_MSG, + handlerDllPath.empty() ? s_pwzAspnetcoreInProcessRequestHandlerName : handlerDllPath.c_str()); + return HRESULT_FROM_WIN32(ERROR_DLL_NOT_FOUND); + } + + return S_OK; +} + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/HandlerResolver.h b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/HandlerResolver.h new file mode 100644 index 0000000000..1cd9029347 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/HandlerResolver.h @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include +#include +#include "ShimOptions.h" +#include "hostfxroptions.h" +#include "HandleWrapper.h" +#include "ApplicationFactory.h" +#include "BaseOutputManager.h" + +class HandlerResolver +{ +public: + HandlerResolver(HMODULE hModule, const IHttpServer &pServer); + HRESULT GetApplicationFactory(const IHttpApplication &pApplication, std::unique_ptr& pApplicationFactory, const ShimOptions& options); + void ResetHostingModel(); + +private: + HRESULT LoadRequestHandlerAssembly(const IHttpApplication &pApplication, const ShimOptions& pConfiguration, std::unique_ptr& pApplicationFactory); + HRESULT FindNativeAssemblyFromGlobalLocation(const ShimOptions& pConfiguration, PCWSTR libraryName, std::wstring& handlerDllPath); + HRESULT FindNativeAssemblyFromHostfxr(const HOSTFXR_OPTIONS& hostfxrOptions, PCWSTR libraryName, std::wstring& handlerDllPath, BaseOutputManager* outputManager); + + HMODULE m_hModule; + const IHttpServer &m_pServer; + + SRWLOCK m_requestHandlerLoadLock {}; + std::wstring m_loadedApplicationId; + APP_HOSTING_MODEL m_loadedApplicationHostingModel; + HandleWrapper m_hHostFxrDll; + + static const PCWSTR s_pwzAspnetcoreInProcessRequestHandlerName; + static const PCWSTR s_pwzAspnetcoreOutOfProcessRequestHandlerName; + static const DWORD s_initialGetNativeSearchDirectoriesBufferSize = MAX_PATH * 4; +}; + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/HtmlResponses.rc b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/HtmlResponses.rc new file mode 100644 index 0000000000..d923a2121c Binary files /dev/null and b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/HtmlResponses.rc differ diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/InProcessShimStaticHtml.htm b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/InProcessShimStaticHtml.htm new file mode 100644 index 0000000000..a8c9d7fe1f --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/InProcessShimStaticHtml.htm @@ -0,0 +1,84 @@ + + + + + HTTP Error 500.0 - ANCM In-Process Handler Load Failure + + + +

HTTP Error 500.0 - ANCM In-Process Handler Load Failure

+ +

Common causes of this issue:

+ +
    +
  • The specified version of Microsoft.NetCore.App or Microsoft.AspNetCore.App was not found.
  • +
  • The in process request handler, Microsoft.AspNetCore.Server.IIS, was not referenced in the application.
  • +
  • ANCM could not find dotnet.
  • +
+ +

Troubleshooting steps:

+ +
    +
  • Check the system event log for error messages
  • +
  • Enable logging the application process' stdout messages
  • +
  • Attach a debugger to the application process and inspect
  • +
+ +

+ For more information visit: + %s https://go.microsoft.com/fwlink/?LinkID=2028526 +

+ + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/OutOfProcessShimStaticHtml.htm b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/OutOfProcessShimStaticHtml.htm new file mode 100644 index 0000000000..1194cf4025 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/OutOfProcessShimStaticHtml.htm @@ -0,0 +1,81 @@ + + + + + HTTP Error 500.0 - ANCM Out-Of-Process Handler Load Failure + + + +

HTTP Error 500.0 - ANCM Out-Of-Process Handler Load Failure

+ +

Common causes of this issue:

+
    +
  • The out of process request handler, aspnetcorev2_outofprocess.dll, could not be found next to the aspnetcorev2.dll.
  • +
  • Could not read configuration correctly. Check the application's associated web.config.
  • +
+ +

Troubleshooting steps:

+
    +
  • Check the system event log for error messages
  • +
  • Enable logging the application process' stdout messages
  • +
  • Attach a debugger to the application process and inspect
  • +
+ +

+ For more information visit: + %s https://go.microsoft.com/fwlink/?LinkID=2028526 +

+ + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/PollingAppOfflineApplication.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/PollingAppOfflineApplication.cpp new file mode 100644 index 0000000000..cf2395e7b5 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/PollingAppOfflineApplication.cpp @@ -0,0 +1,61 @@ +// 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. + +#include "PollingAppOfflineApplication.h" + +#include +#include "SRWExclusiveLock.h" +#include "HandleWrapper.h" +#include "exceptions.h" + +HRESULT PollingAppOfflineApplication::TryCreateHandler(_In_ IHttpContext* pHttpContext, _Outptr_result_maybenull_ IREQUEST_HANDLER** pRequestHandler) +{ + CheckAppOffline(); + return LOG_IF_FAILED(APPLICATION::TryCreateHandler(pHttpContext, pRequestHandler)); +} + +void +PollingAppOfflineApplication::CheckAppOffline() +{ + if (m_fStopCalled) + { + return; + } + + const auto ulCurrentTime = GetTickCount64(); + // + // we only care about app offline presented. If not, it means the application has started + // and is monitoring the app offline file + // we cache the file exist check result for 200 ms + // + if (ulCurrentTime - m_ulLastCheckTime > c_appOfflineRefreshIntervalMS) + { + SRWExclusiveLock lock(m_statusLock); + if (ulCurrentTime - m_ulLastCheckTime > c_appOfflineRefreshIntervalMS) + { + m_fAppOfflineFound = FileExists(m_appOfflineLocation); + if(m_fAppOfflineFound) + { + LOG_IF_FAILED(OnAppOfflineFound()); + } + m_ulLastCheckTime = ulCurrentTime; + } + } + + if (m_fAppOfflineFound != (m_mode == StopWhenRemoved)) + { + Stop(/* fServerInitiated */ false); + } +} + + +std::filesystem::path PollingAppOfflineApplication::GetAppOfflineLocation(const IHttpApplication& pApplication) +{ + return std::filesystem::path(pApplication.GetApplicationPhysicalPath()) / "app_offline.htm"; +} + +bool PollingAppOfflineApplication::FileExists(const std::filesystem::path& path) noexcept +{ + std::error_code ec; + return is_regular_file(path, ec) || ec.value() == ERROR_SHARING_VIOLATION; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/PollingAppOfflineApplication.h b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/PollingAppOfflineApplication.h new file mode 100644 index 0000000000..0f0eb58c8c --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/PollingAppOfflineApplication.h @@ -0,0 +1,47 @@ +// 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. + +#pragma once +#include +#include "application.h" + +enum PollingAppOfflineApplicationMode +{ + StopWhenAdded, + StopWhenRemoved +}; + +class PollingAppOfflineApplication: public APPLICATION +{ +public: + PollingAppOfflineApplication(const IHttpApplication& pApplication, PollingAppOfflineApplicationMode mode) + : APPLICATION(pApplication), + m_ulLastCheckTime(0), + m_appOfflineLocation(GetAppOfflineLocation(pApplication)), + m_fAppOfflineFound(false), + m_mode(mode) + { + InitializeSRWLock(&m_statusLock); + } + + HRESULT + TryCreateHandler( + _In_ IHttpContext *pHttpContext, + _Outptr_result_maybenull_ IREQUEST_HANDLER **pRequestHandler) override; + + void CheckAppOffline(); + virtual HRESULT OnAppOfflineFound() = 0; + void StopInternal(bool fServerInitiated) override { UNREFERENCED_PARAMETER(fServerInitiated); } + +protected: + std::filesystem::path m_appOfflineLocation; + static std::filesystem::path GetAppOfflineLocation(const IHttpApplication& pApplication); + static bool FileExists(const std::filesystem::path& path) noexcept; +private: + static const int c_appOfflineRefreshIntervalMS = 200; + std::string m_strAppOfflineContent; + ULONGLONG m_ulLastCheckTime; + bool m_fAppOfflineFound; + SRWLOCK m_statusLock {}; + PollingAppOfflineApplicationMode m_mode; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/ServerErrorApplication.h b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/ServerErrorApplication.h new file mode 100644 index 0000000000..b5b86aa20c --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/ServerErrorApplication.h @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once +#include "PollingAppOfflineApplication.h" +#include "requesthandler.h" +#include "ServerErrorHandler.h" + +class ServerErrorApplication : public PollingAppOfflineApplication +{ +public: + ServerErrorApplication(const IHttpApplication& pApplication, HRESULT hr, HINSTANCE moduleInstance) + : ServerErrorApplication(pApplication, hr, moduleInstance, true /* disableStartupPage*/, 0 /* page */) + { + } + + ServerErrorApplication(const IHttpApplication& pApplication, HRESULT hr, HINSTANCE moduleInstance, bool disableStartupPage, int page) + : m_HR(hr), + m_disableStartupPage(disableStartupPage), + m_page(page), + m_moduleInstance(moduleInstance), + PollingAppOfflineApplication(pApplication, PollingAppOfflineApplicationMode::StopWhenAdded) + { + } + + ~ServerErrorApplication() = default; + + HRESULT CreateHandler(IHttpContext *pHttpContext, IREQUEST_HANDLER ** pRequestHandler) override + { + auto handler = std::make_unique(*pHttpContext, 500ui16, 0ui16, "Internal Server Error", m_HR, m_moduleInstance, m_disableStartupPage, m_page); + *pRequestHandler = handler.release(); + return S_OK; + } + + HRESULT OnAppOfflineFound() noexcept override { return S_OK; } +private: + HRESULT m_HR; + bool m_disableStartupPage; + int m_page; + HINSTANCE m_moduleInstance; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/ShimOptions.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/ShimOptions.cpp new file mode 100644 index 0000000000..cb7f1488d7 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/ShimOptions.cpp @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "ShimOptions.h" + +#include "StringHelpers.h" +#include "ConfigurationLoadException.h" + +#define CS_ASPNETCORE_HANDLER_VERSION L"handlerVersion" + +ShimOptions::ShimOptions(const ConfigurationSource &configurationSource) : + m_hostingModel(HOSTING_UNKNOWN), + m_fStdoutLogEnabled(false) +{ + auto const section = configurationSource.GetRequiredSection(CS_ASPNETCORE_SECTION); + auto hostingModel = section->GetString(CS_ASPNETCORE_HOSTING_MODEL).value_or(L""); + + if (hostingModel.empty() || equals_ignore_case(hostingModel, CS_ASPNETCORE_HOSTING_MODEL_OUTOFPROCESS)) + { + m_hostingModel = HOSTING_OUT_PROCESS; + } + else if (equals_ignore_case(hostingModel, CS_ASPNETCORE_HOSTING_MODEL_INPROCESS)) + { + m_hostingModel = HOSTING_IN_PROCESS; + } + else + { + throw ConfigurationLoadException(format( + L"Unknown hosting model '%s'. Please specify either hostingModel=\"inprocess\" " + "or hostingModel=\"outofprocess\" in the web.config file.", hostingModel.c_str())); + } + + if (m_hostingModel == HOSTING_OUT_PROCESS) + { + const auto handlerSettings = section->GetKeyValuePairs(CS_ASPNETCORE_HANDLER_SETTINGS); + m_strHandlerVersion = find_element(handlerSettings, CS_ASPNETCORE_HANDLER_VERSION).value_or(std::wstring()); + } + + m_strProcessPath = section->GetRequiredString(CS_ASPNETCORE_PROCESS_EXE_PATH); + m_strArguments = section->GetString(CS_ASPNETCORE_PROCESS_ARGUMENTS).value_or(CS_ASPNETCORE_PROCESS_ARGUMENTS_DEFAULT); + m_fStdoutLogEnabled = section->GetRequiredBool(CS_ASPNETCORE_STDOUT_LOG_ENABLED); + m_struStdoutLogFile = section->GetRequiredString(CS_ASPNETCORE_STDOUT_LOG_FILE); + m_fDisableStartupPage = section->GetRequiredBool(CS_ASPNETCORE_DISABLE_START_UP_ERROR_PAGE); +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/ShimOptions.h b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/ShimOptions.h new file mode 100644 index 0000000000..3cfb47169e --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/ShimOptions.h @@ -0,0 +1,72 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include +#include "ConfigurationSource.h" +#include "exceptions.h" + +enum APP_HOSTING_MODEL +{ + HOSTING_UNKNOWN = 0, + HOSTING_IN_PROCESS, + HOSTING_OUT_PROCESS +}; + +class ShimOptions: NonCopyable +{ +public: + const std::wstring& + QueryProcessPath() const noexcept + { + return m_strProcessPath; + } + + const std::wstring& + QueryArguments() const noexcept + { + return m_strArguments; + } + + APP_HOSTING_MODEL + QueryHostingModel() const noexcept + { + return m_hostingModel; + } + + const std::wstring& + QueryHandlerVersion() const noexcept + { + return m_strHandlerVersion; + } + + BOOL + QueryStdoutLogEnabled() const noexcept + { + return m_fStdoutLogEnabled; + } + + const std::wstring& + QueryStdoutLogFile() const noexcept + { + return m_struStdoutLogFile; + } + + bool + QueryDisableStartupPage() const noexcept + { + return m_fDisableStartupPage; + } + + ShimOptions(const ConfigurationSource &configurationSource); + +private: + std::wstring m_strArguments; + std::wstring m_strProcessPath; + APP_HOSTING_MODEL m_hostingModel; + std::wstring m_strHandlerVersion; + std::wstring m_struStdoutLogFile; + bool m_fStdoutLogEnabled; + bool m_fDisableStartupPage; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/Source.def b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/Source.def new file mode 100644 index 0000000000..74135f4835 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/Source.def @@ -0,0 +1,4 @@ +LIBRARY aspnetcorev2 + +EXPORTS + RegisterModule diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/ancm.mof b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/ancm.mof new file mode 100644 index 0000000000..359806f8a2 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/ancm.mof @@ -0,0 +1,371 @@ +#pragma classflags("forceupdate") +#pragma namespace ("\\\\.\\Root\\WMI") +#pragma autorecover + +/* + * AspNetCore module trace events layout + * Uncomment the following class to run mof2trace to generate header file + * comment it back before checking it in +[Dynamic, + Description("IIS: WWW Server"), + Guid("{3a2a4e84-4c21-4981-ae10-3fda0d9b0f83}"), + locale("MS\\0x409")] +class IIS_Trace:EventTrace +{ + [Description ("Enable Flags") : amended, + ValueDescriptions{ + "AspNetCore module events" } : amended, + DefineValues{ + "ETW_IIS_ANCM" }, + Values{ + "ANCM" }, + ValueMap{ + "0x10000" } + ] + uint32 Flags; + + [Description ("Levels") : amended, + ValueDescriptions{ + "Abnormal exit or termination", + "Severe errors", + "Warnings", + "Information", + "Detailed information" } : amended, + DefineValues{ + "TRACE_LEVEL_FATAL", + "TRACE_LEVEL_ERROR", + "TRACE_LEVEL_WARNING", + "TRACE_LEVEL_INFORMATION", + "TRACE_LEVEL_VERBOSE" }, + Values{ + "Fatal", + "Error", + "Warning", + "Information", + "Verbose" }, + ValueMap{ + "0x1", + "0x2", + "0x3", + "0x4", + "0x5" }, + ValueType("index") + ] + uint32 Level; +}; +*/ + +[Dynamic, + Description("ANCM runtime events") : amended, + Guid("{82ADEAD7-12B2-4781-BDCA-5A4B6C757191}"), + EventVersion(1), + DisplayName("ANCM"), + EventANCMRuntime, + locale("MS\\0x409") +] +class ANCM_Events:IIS_Trace +{ +}; + +[Dynamic, + Description("Start application success") : amended, + EventType(1), + EventLevel(4), + EventTypeName("ANCM_START_APPLICATION_SUCCESS") : amended +] +class ANCMAppStart:ANCM_Events +{ + [WmiDataId(1), + Description("Context ID") : amended, + extension("Guid"), + ActivityID, + read] + object ContextId; + [WmiDataId(2), + Description("Application Description") : amended, + StringTermination("NullTerminated"), + format("w"), + read] + string AppDescription; +}; + +[Dynamic, + Description("Start application failed") : amended, + EventType(2), + EventLevel(2), + EventTypeName("ANCM_START_APPLICATION_FAIL") : amended +] +class ANCMAppStartFail:ANCM_Events +{ + [WmiDataId(1), + Description("Context ID") : amended, + extension("Guid"), + ActivityID, + read] + object ContextId; + [WmiDataId(2), + Description("Application Start Failure Description") : amended, + StringTermination("NullTerminated"), + format("w"), + read] + string FailureDescription; +}; + +[Dynamic, + Description("Start forwarding request") : amended, + EventType(3), + EventLevel(4), + EventTypeName("ANCM_REQUEST_FORWARD_START") : amended +] +class ANCMForwardStart:ANCM_Events +{ + [WmiDataId(1), + Description("Context ID") : amended, + extension("Guid"), + ActivityID, + read] + object ContextId; +}; + +[Dynamic, + Description("Finish forwarding request") : amended, + EventType(4), + EventLevel(4), + EventTypeName("ANCM_REQUEST_FORWARD_END") : amended +] +class ANCMForwardEnd:ANCM_Events +{ + [WmiDataId(1), + Description("Context ID") : amended, + extension("Guid"), + ActivityID, + read] + object ContextId; +}; + +[Dynamic, + Description("Forwarding request failure") : amended, + EventType(5), + EventLevel(2), + EventTypeName("ANCM_REQUEST_FORWARD_FAIL") : amended +] +class ANCMForwardFail:ANCM_Events +{ + [WmiDataId(1), + Description("Context ID") : amended, + extension("Guid"), + ActivityID, + read] + object ContextId; + [WmiDataId(2), + Description("Error code") : amended, + format("x"), + read] + uint32 ErrorCode; +}; + +[Dynamic, + Description("Receiving callback from WinHttp") : amended, + EventType(6), + EventLevel(4), + EventTypeName("ANCM_WINHTTP_CALLBACK") : amended +] +class ANCMWinHttpCallBack:ANCM_Events +{ + [WmiDataId(1), + Description("Context ID") : amended, + extension("Guid"), + ActivityID, + read] + object ContextId; + [WmiDataId(2), + Description("InternetStatus") : amended, + format("x"), + read] + uint32 InternetStatus; +}; + +[Dynamic, + Description("Starting inprocess execute request") : amended, + EventType(7), + EventLevel(4), + EventTypeName("ANCM_INPROC_EXECUTE_REQUEST_START") : amended +] +class ANCMExecuteStart:ANCM_Events +{ + [WmiDataId(1), + Description("Context ID") : amended, + extension("Guid"), + ActivityID, + read] + object ContextId; +}; + +[Dynamic, + Description("Ending inprocess execute request") : amended, + EventType(8), + EventLevel(5), + EventTypeName("ANCM_INPROC_EXECUTE_REQUEST_COMPLETION") : amended +] +class ANCMExecuteEnd:ANCM_Events +{ + [WmiDataId(1), + Description("Context ID") : amended, + extension("Guid"), + ActivityID, + read] + object ContextId; + [WmiDataId(2), + Description("Notification status") : amended, + format("d"), + read] + uint32 requestStatus; +}; + +[Dynamic, + Description("Starting inprocess async completion") : amended, + EventType(9), + EventLevel(5), + EventTypeName("ANCM_INPROC_ASYNC_COMPLETION_START") : amended +] +class ANCMAsyncStart:ANCM_Events +{ + [WmiDataId(1), + Description("Context ID") : amended, + extension("Guid"), + ActivityID, + read] + object ContextId; +}; + +[Dynamic, + Description("Ending inprocess async completion") : amended, + EventType(10), + EventLevel(5), + EventTypeName("ANCM_INPROC_ASYNC_COMPLETION_COMPLETION") : amended +] +class ANCMAsyncEnd:ANCM_Events +{ + [WmiDataId(1), + Description("Context ID") : amended, + extension("Guid"), + ActivityID, + read] + object ContextId; + [WmiDataId(2), + Description("Notification status") : amended, + format("d"), + read] + uint32 requestStatus; +}; + +[Dynamic, + Description("Inprocess app shutdown") : amended, + EventType(11), + EventLevel(4), + EventTypeName("ANCM_INPROC_REQUEST_SHUTDOWN") : amended +] +class ANCMRequestShutdown:ANCM_Events +{ + [WmiDataId(1), + Description("Context ID") : amended, + extension("Guid"), + ActivityID, + read] + object ContextId; +}; + +[Dynamic, + Description("Inprocess request disconnect") : amended, + EventType(12), + EventLevel(4), + EventTypeName("ANCM_INPROC_REQUEST_DISCONNECT") : amended +] +class ANCMRequestDisconnect:ANCM_Events +{ + [WmiDataId(1), + Description("Context ID") : amended, + extension("Guid"), + ActivityID, + read] + object ContextId; +}; + +[Dynamic, + Description("Indicate managed request complete") : amended, + EventType(13), + EventLevel(4), + EventTypeName("ANCM_INPROC_MANAGED_REQUEST_COMPLETION") : amended +] +class ANCMManagedRequestCompletion:ANCM_Events +{ + [WmiDataId(1), + Description("Context ID") : amended, + extension("Guid"), + ActivityID, + read] + object ContextId; +}; + +[Dynamic, + Description("Failed HRESULT") : amended, + EventType(14), + EventLevel(3), + EventTypeName("ANCM_HRESULT_FAILED") : amended +] +class ANCMHRESULTFailed:ANCM_Events +{ + [WmiDataId(1), + Description("Context ID") : amended, + extension("Guid"), + ActivityID, + read] + object ContextId; + [WmiDataId(2), + Description("Failed source file location") : amended, + StringTermination("NullTerminated"), + read] + string File; + [WmiDataId(3), + Description("Failed line number") : amended, + format("d"), + read] + uint32 Line; + [WmiDataId(4), + Description("HResult") : amended, + format("x"), + read] + uint32 HResult; +}; + +[Dynamic, + Description("Caught exception") : amended, + EventType(15), + EventLevel(3), + EventTypeName("ANCM_EXCEPTION_CAUGHT") : amended +] +class ANCMExceptionCaughFailed:ANCM_Events +{ + [WmiDataId(1), + Description("Context ID") : amended, + extension("Guid"), + ActivityID, + read] + object ContextId; + [WmiDataId(2), + Description("Exception catch file location") : amended, + StringTermination("NullTerminated"), + read] + string File; + [WmiDataId(3), + Description("Exception catch line number") : amended, + format("d"), + read] + uint32 Line; + [WmiDataId(2), + Description("Exception description") : amended, + StringTermination("NullTerminated"), + read] + string Description; +}; + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/applicationinfo.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/applicationinfo.cpp new file mode 100644 index 0000000000..36b0c2d902 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/applicationinfo.cpp @@ -0,0 +1,182 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "applicationinfo.h" + +#include "proxymodule.h" +#include "hostfxr_utility.h" +#include "debugutil.h" +#include "resources.h" +#include "SRWExclusiveLock.h" +#include "exceptions.h" +#include "EventLog.h" +#include "ServerErrorApplication.h" +#include "AppOfflineApplication.h" +#include "WebConfigConfigurationSource.h" +#include "ConfigurationLoadException.h" +#include "resource.h" + +extern HINSTANCE g_hServerModule; + +HRESULT +APPLICATION_INFO::CreateHandler( + IHttpContext& pHttpContext, + std::unique_ptr& pHandler +) +{ + HRESULT hr = S_OK; + + { + SRWSharedLock lock(m_applicationLock); + + RETURN_IF_FAILED(hr = TryCreateHandler(pHttpContext, pHandler)); + + if (hr == S_OK) + { + return S_OK; + } + } + + { + SRWExclusiveLock lock(m_applicationLock); + + // check if other thread created application + RETURN_IF_FAILED(hr = TryCreateHandler(pHttpContext, pHandler)); + + // In some cases (adding and removing app_offline quickly) application might start and stop immediately + // so retry until we get valid handler or error + while (hr != S_OK) + { + // At this point application is either null or shutdown and is returning S_FALSE + + if (m_pApplication != nullptr) + { + LOG_INFO(L"Application went offline"); + + // Call to wait for application to complete stopping + m_pApplication->Stop(/* fServerInitiated */ false); + m_pApplication = nullptr; + m_pApplicationFactory = nullptr; + } + + RETURN_IF_FAILED(CreateApplication(pHttpContext)); + + RETURN_IF_FAILED(hr = TryCreateHandler(pHttpContext, pHandler)); + } + } + + return S_OK; +} + +HRESULT +APPLICATION_INFO::CreateApplication(IHttpContext& pHttpContext) +{ + auto& pHttpApplication = *pHttpContext.GetApplication(); + if (AppOfflineApplication::ShouldBeStarted(pHttpApplication)) + { + LOG_INFO(L"Detected app_offline file, creating polling application"); + m_pApplication = make_application(pHttpApplication); + + return S_OK; + } + else + { + try + { + const WebConfigConfigurationSource configurationSource(m_pServer.GetAdminManager(), pHttpApplication); + ShimOptions options(configurationSource); + + const auto hr = TryCreateApplication(pHttpContext, options); + + if (FAILED_LOG(hr)) + { + // Log the failure and update application info to not try again + EventLog::Error( + ASPNETCORE_EVENT_ADD_APPLICATION_ERROR, + ASPNETCORE_EVENT_ADD_APPLICATION_ERROR_MSG, + pHttpApplication.GetApplicationId(), + hr); + + m_pApplication = make_application( + pHttpApplication, + hr, + g_hServerModule, + options.QueryDisableStartupPage(), + options.QueryHostingModel() == APP_HOSTING_MODEL::HOSTING_IN_PROCESS ? IN_PROCESS_SHIM_STATIC_HTML : OUT_OF_PROCESS_SHIM_STATIC_HTML); + } + return S_OK; + } + catch (ConfigurationLoadException &ex) + { + EventLog::Error( + ASPNETCORE_CONFIGURATION_LOAD_ERROR, + ASPNETCORE_CONFIGURATION_LOAD_ERROR_MSG, + ex.get_message().c_str()); + } + catch (...) + { + EventLog::Error( + ASPNETCORE_CONFIGURATION_LOAD_ERROR, + ASPNETCORE_CONFIGURATION_LOAD_ERROR_MSG, + L""); + } + + m_pApplication = make_application( + pHttpApplication, + E_FAIL, + g_hServerModule); + + return S_OK; + } +} + +HRESULT +APPLICATION_INFO::TryCreateApplication(IHttpContext& pHttpContext, const ShimOptions& options) +{ + RETURN_IF_FAILED(m_handlerResolver.GetApplicationFactory(*pHttpContext.GetApplication(), m_pApplicationFactory, options)); + LOG_INFO(L"Creating handler application"); + + IAPPLICATION * newApplication; + RETURN_IF_FAILED(m_pApplicationFactory->Execute( + &m_pServer, + &pHttpContext, + &newApplication)); + + m_pApplication.reset(newApplication); + return S_OK; +} + +HRESULT +APPLICATION_INFO::TryCreateHandler( + IHttpContext& pHttpContext, + std::unique_ptr& pHandler) +{ + if (m_pApplication != nullptr) + { + IREQUEST_HANDLER * newHandler; + const auto result = m_pApplication->TryCreateHandler(&pHttpContext, &newHandler); + RETURN_IF_FAILED(result); + + if (result == S_OK) + { + pHandler.reset(newHandler); + // another thread created the application + return S_OK; + } + } + return S_FALSE; +} + +VOID +APPLICATION_INFO::ShutDownApplication(bool fServerInitiated) +{ + SRWExclusiveLock lock(m_applicationLock); + + if (m_pApplication) + { + LOG_INFOF(L"Stopping application '%ls'", QueryApplicationInfoKey().c_str()); + m_pApplication->Stop(fServerInitiated); + m_pApplication = nullptr; + m_pApplicationFactory = nullptr; + } +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/applicationinfo.h b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/applicationinfo.h new file mode 100644 index 0000000000..b40e0a8ba9 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/applicationinfo.h @@ -0,0 +1,92 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "hostfxroptions.h" +#include "iapplication.h" +#include "SRWSharedLock.h" +#include "HandlerResolver.h" + +#define API_BUFFER_TOO_SMALL 0x80008098 + +extern BOOL g_fRecycleProcessCalled; + +class APPLICATION_INFO: NonCopyable +{ +public: + + APPLICATION_INFO( + IHttpServer &pServer, + IHttpApplication &pApplication, + HandlerResolver &pHandlerResolver + ) : + m_pServer(pServer), + m_handlerResolver(pHandlerResolver), + m_strConfigPath(pApplication.GetAppConfigPath()), + m_strInfoKey(pApplication.GetApplicationId()) + { + InitializeSRWLock(&m_applicationLock); + } + + ~APPLICATION_INFO() = default; + + const std::wstring& + QueryApplicationInfoKey() noexcept + { + return m_strInfoKey; + } + + const std::wstring& + QueryConfigPath() noexcept + { + return m_strConfigPath; + } + + VOID + ShutDownApplication(bool fServerInitiated); + + HRESULT + CreateHandler( + IHttpContext& pHttpContext, + std::unique_ptr& pHandler); + + bool ConfigurationPathApplies(const std::wstring& path) + { + // We need to check that the last character of the config path + // is either a null terminator or a slash. + // This checks the case where the config path was + // MACHINE/WEBROOT/site and your site path is MACHINE/WEBROOT/siteTest + auto const changed = m_strConfigPath._Starts_with(path); + if (changed) + { + const auto lastChar = m_strConfigPath[m_strConfigPath.length()]; + return lastChar == L'\0' || lastChar == L'/'; + } + return false; + } + +private: + + HRESULT + TryCreateHandler( + IHttpContext& pHttpContext, + std::unique_ptr& pHandler); + + HRESULT + CreateApplication(IHttpContext& pHttpContext); + + HRESULT + TryCreateApplication(IHttpContext& pHttpContext, const ShimOptions& options); + + IHttpServer &m_pServer; + HandlerResolver &m_handlerResolver; + + std::wstring m_strConfigPath; + std::wstring m_strInfoKey; + SRWLOCK m_applicationLock {}; + + std::unique_ptr m_pApplicationFactory; + std::unique_ptr m_pApplication; +}; + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/applicationmanager.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/applicationmanager.cpp new file mode 100644 index 0000000000..d1e2b12375 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/applicationmanager.cpp @@ -0,0 +1,183 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "applicationmanager.h" + +#include "proxymodule.h" +#include "resources.h" +#include "SRWExclusiveLock.h" +#include "exceptions.h" +#include "EventLog.h" + +extern BOOL g_fInShutdown; + +// +// Retrieves the application info from the application manager +// Will create the application info if it isn't initialized +// +HRESULT +APPLICATION_MANAGER::GetOrCreateApplicationInfo( + _In_ IHttpContext& pHttpContext, + _Out_ std::shared_ptr& ppApplicationInfo +) +{ + auto &pApplication = *pHttpContext.GetApplication(); + + // The configuration path is unique for each application and is used for the + // key in the applicationInfoHash. + std::wstring pszApplicationId = pApplication.GetApplicationId(); + + { + // When accessing the m_pApplicationInfoHash, we need to acquire the application manager + // lock to avoid races on setting state. + SRWSharedLock readLock(m_srwLock); + + if (g_fInShutdown) + { + return HRESULT_FROM_WIN32(ERROR_SERVER_SHUTDOWN_IN_PROGRESS); + } + + const auto pair = m_pApplicationInfoHash.find(pszApplicationId); + if (pair != m_pApplicationInfoHash.end()) + { + ppApplicationInfo = pair->second; + return S_OK; + } + + // It's important to release read lock here so exclusive lock + // can be reacquired later as SRW lock doesn't allow upgrades + } + + // Take exclusive lock before creating the application + SRWExclusiveLock writeLock(m_srwLock); + + if (!m_fDebugInitialize) + { + DebugInitializeFromConfig(m_pHttpServer, pApplication); + m_fDebugInitialize = TRUE; + } + + // Check if other thread created the application + const auto pair = m_pApplicationInfoHash.find(pszApplicationId); + if (pair != m_pApplicationInfoHash.end()) + { + ppApplicationInfo = pair->second; + return S_OK; + } + + ppApplicationInfo = std::make_shared(m_pHttpServer, pApplication, m_handlerResolver); + m_pApplicationInfoHash.emplace(pszApplicationId, ppApplicationInfo); + + return S_OK; +} + +// +// Finds any applications affected by a configuration change and calls Recycle on them +// InProcess: Triggers g_httpServer->RecycleProcess() and keep the application inside of the manager. +// This will cause a shutdown event to occur through the global stop listening event. +// OutOfProcess: Removes all applications in the application manager and calls Recycle, which will call Shutdown, +// on each application. +// +HRESULT +APPLICATION_MANAGER::RecycleApplicationFromManager( + _In_ LPCWSTR pszApplicationId +) +{ + try + { + std::vector> applicationsToRecycle; + + if (g_fInShutdown) + { + // We are already shutting down, ignore this event as a global configuration change event + // can occur after global stop listening for some reason. + return S_OK; + } + + { + SRWExclusiveLock lock(m_srwLock); + if (g_fInShutdown) + { + return S_OK; + } + const std::wstring configurationPath = pszApplicationId; + + auto itr = m_pApplicationInfoHash.begin(); + while (itr != m_pApplicationInfoHash.end()) + { + if (itr->second->ConfigurationPathApplies(configurationPath)) + { + applicationsToRecycle.emplace_back(itr->second); + itr = m_pApplicationInfoHash.erase(itr); + } + else + { + ++itr; + } + } + + // All applications were unloaded reset handler resolver validation logic + if (m_pApplicationInfoHash.empty()) + { + m_handlerResolver.ResetHostingModel(); + } + } + + // If we receive a request at this point. + // OutOfProcess: we will create a new application with new configuration + // InProcess: the request would have to be rejected, as we are about to call g_HttpServer->RecycleProcess + // on the worker proocess + + if (!applicationsToRecycle.empty()) + { + for (auto& application : applicationsToRecycle) + { + try + { + application->ShutDownApplication(/* fServerInitiated */ false); + } + catch (...) + { + LOG_ERRORF(L"Failed to stop application '%ls'", application->QueryApplicationInfoKey().c_str()); + OBSERVE_CAUGHT_EXCEPTION(); + + // Failed to recycle an application. Log an event + EventLog::Error( + ASPNETCORE_EVENT_RECYCLE_APP_FAILURE, + ASPNETCORE_EVENT_RECYCLE_FAILURE_CONFIGURATION_MSG, + pszApplicationId); + // Need to recycle the process as we cannot recycle the application + if (!g_fRecycleProcessCalled) + { + g_fRecycleProcessCalled = TRUE; + m_pHttpServer.RecycleProcess(L"AspNetCore Recycle Process on Demand Due Application Recycle Error"); + } + } + } + } + } + CATCH_RETURN(); + + return S_OK; +} + +// +// Shutsdown all applications in the application hashtable +// Only called by OnGlobalStopListening. +// +VOID +APPLICATION_MANAGER::ShutDown() +{ + // We are guaranteed to only have one outstanding OnGlobalStopListening event at a time + // However, it is possible to receive multiple OnGlobalStopListening events + // Protect against this by checking if we already shut down. + g_fInShutdown = TRUE; + + // During shutdown we lock until we delete the application + SRWExclusiveLock lock(m_srwLock); + for (auto &pair : m_pApplicationInfoHash) + { + pair.second->ShutDownApplication(/* fServerInitiated */ true); + pair.second = nullptr; + } +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/applicationmanager.h b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/applicationmanager.h new file mode 100644 index 0000000000..a9fdde6a5f --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/applicationmanager.h @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "applicationinfo.h" +#include "multisz.h" +#include "exceptions.h" +#include + +// +// This class will manage the lifecycle of all Asp.Net Core applciation +// It should be global singleton. +// Should always call GetInstance to get the object instance +// + + +class APPLICATION_MANAGER +{ +public: + + HRESULT + GetOrCreateApplicationInfo( + _In_ IHttpContext& pHttpContext, + _Out_ std::shared_ptr& ppApplicationInfo + ); + + HRESULT + RecycleApplicationFromManager( + _In_ LPCWSTR pszApplicationId + ); + + VOID + ShutDown(); + + APPLICATION_MANAGER(HMODULE hModule, IHttpServer& pHttpServer) : + m_pApplicationInfoHash(NULL), + m_fDebugInitialize(FALSE), + m_pHttpServer(pHttpServer), + m_handlerResolver(hModule, pHttpServer) + { + InitializeSRWLock(&m_srwLock); + } + +private: + + std::unordered_map> m_pApplicationInfoHash; + SRWLOCK m_srwLock {}; + BOOL m_fDebugInitialize; + IHttpServer &m_pHttpServer; + HandlerResolver m_handlerResolver; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/aspnetcore_schema_v2.xml b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/aspnetcore_schema_v2.xml new file mode 100644 index 0000000000..d65be07195 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/aspnetcore_schema_v2.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/aspnetcoremodule.rc b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/aspnetcoremodule.rc new file mode 100644 index 0000000000..9a110e2920 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/aspnetcoremodule.rc @@ -0,0 +1,120 @@ +// Microsoft Visual C++ generated resource script. +// +#include +#include "version.h" +#include "..\CommonLib\Aspnetcore_msg.rc" +#include "..\CommonLib\resources.h" +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#define FileDescription "IIS ASP.NET Core Module V2. Commit: " CommitHash + +///////////////////////////////////////////////////////////////////////////// +// +// 11 +// + +//1 11 +//BEGIN +// 0x0001, 0x0000, 0x03e8, 0x0000, 0x03ed, 0x0000, 0x0010, 0x0000, 0x0010, +// 0x0001, 0x0025, 0x0031, 0x000d, 0x000a, 0x0000, 0x0000, 0x0010, 0x0001, +// 0x0025, 0x0031, 0x000d, 0x000a, 0x0000, 0x0000, 0x0010, 0x0001, 0x0025, +// 0x0031, 0x000d, 0x000a, 0x0000, 0x0000, 0x0010, 0x0001, 0x0025, 0x0031, +// 0x000d, 0x000a, 0x0000, 0x0000, 0x0010, 0x0001, 0x0025, 0x0031, 0x000d, +// 0x000a, 0x0000, 0x0000, 0x0010, 0x0001, 0x0025, 0x0031, 0x000d, 0x000a, +// 0x0000, 0x0000 +//END + + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "..\CommonLib\resources.h\0" +END + +2 TEXTINCLUDE +BEGIN + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION FileVersion + PRODUCTVERSION ProductVersion + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x40004L + FILETYPE 0x2L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", "Microsoft" + VALUE "FileDescription", FileDescription + VALUE "FileVersion", FileVersionStr + VALUE "InternalName", "aspnetcore" + VALUE "LegalCopyright", "Copyright (C) Microsoft Corporation" + VALUE "OriginalFilename", "aspnetcore.dll" + VALUE "ProductName", "ASP.NET Core Module" + VALUE "ProductVersion", ProductVersionStr + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END + + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + +STRINGTABLE +BEGIN + IDS_INVALID_PROPERTY "Property name '%s' in system.webServer/aspNetCore section has invalid value '%s' which does not conform to the prescribed format" + IDS_SERVER_ERROR "There was a connection error while trying to route the request." +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/dllmain.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/dllmain.cpp new file mode 100644 index 0000000000..3fce85afdd --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/dllmain.cpp @@ -0,0 +1,165 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "applicationinfo.h" +#include "applicationmanager.h" +#include "proxymodule.h" +#include "globalmodule.h" +#include "acache.h" +#include "debugutil.h" +#include "resources.h" +#include "exceptions.h" +#include "EventLog.h" + +DECLARE_DEBUG_PRINT_OBJECT("aspnetcorev2.dll"); + +HANDLE g_hEventLog = nullptr; +BOOL g_fRecycleProcessCalled = FALSE; +BOOL g_fInShutdown = FALSE; +HINSTANCE g_hServerModule; + +VOID +StaticCleanup() +{ + if (g_hEventLog != nullptr) + { + DeregisterEventSource(g_hEventLog); + g_hEventLog = nullptr; + } + + DebugStop(); + ALLOC_CACHE_HANDLER::StaticTerminate(); +} + +BOOL WINAPI DllMain(HMODULE hModule, + DWORD ul_reason_for_call, + LPVOID lpReserved + ) +{ + UNREFERENCED_PARAMETER(lpReserved); + + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + + ALLOC_CACHE_HANDLER::StaticInitialize(); + g_hServerModule = hModule; + DisableThreadLibraryCalls(hModule); + DebugInitialize(hModule); + break; + case DLL_PROCESS_DETACH: + + // IIS can cause dll detach to occur before we receive global notifications + // For example, when we switch the bitness of the worker process, + // this is a bug in IIS. To try to avoid AVs, we will set a global flag + g_fInShutdown = TRUE; + StaticCleanup(); + default: + break; + } + + return TRUE; +} + +HRESULT +__stdcall +RegisterModule( +DWORD dwServerVersion, +IHttpModuleRegistrationInfo * pModuleInfo, +IHttpServer * pHttpServer +) try +/*++ + +Routine description: + +Function called by IIS immediately after loading the module, used to let +IIS know what notifications the module is interested in + +Arguments: + +dwServerVersion - IIS version the module is being loaded on +pModuleInfo - info regarding this module +pHttpServer - callback functions which can be used by the module at +any point + +Return value: + +HRESULT + +--*/ +{ + HKEY hKey {}; + BOOL fDisableANCM = FALSE; + + UNREFERENCED_PARAMETER(dwServerVersion); + + if (pHttpServer->IsCommandLineLaunch()) + { + g_hEventLog = RegisterEventSource(nullptr, ASPNETCORE_IISEXPRESS_EVENT_PROVIDER); + } + else + { + g_hEventLog = RegisterEventSource(nullptr, ASPNETCORE_EVENT_PROVIDER); + } + + // check whether the feature is disabled due to security reason + if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, + L"SOFTWARE\\Microsoft\\IIS Extensions\\IIS AspNetCore Module V2\\Parameters", + 0, + KEY_READ, + &hKey) == NO_ERROR) + { + DWORD dwType = 0; + DWORD dwData = 0; + DWORD cbData; + + cbData = sizeof(dwData); + if ((RegQueryValueEx(hKey, + L"DisableANCM", + nullptr, + &dwType, + (LPBYTE)&dwData, + &cbData) == NO_ERROR) && + (dwType == REG_DWORD)) + { + fDisableANCM = (dwData != 0); + } + + RegCloseKey(hKey); + } + + if (fDisableANCM) + { + // Logging + EventLog::Warn( + ASPNETCORE_EVENT_MODULE_DISABLED, + ASPNETCORE_EVENT_MODULE_DISABLED_MSG); + // this will return 500 error to client + // as we did not register the module + return S_OK; + } + + // + // Create the factory before any static initialization. + // The ASPNET_CORE_PROXY_MODULE_FACTORY::Terminate method will clean any + // static object initialized. + // + + auto applicationManager = std::make_shared(g_hServerModule, *pHttpServer); + auto moduleFactory = std::make_unique(pModuleInfo->GetId(), applicationManager); + + RETURN_IF_FAILED(pModuleInfo->SetRequestNotifications( + moduleFactory.release(), + RQ_EXECUTE_REQUEST_HANDLER, + 0)); +; + auto pGlobalModule = std::make_unique(std::move(applicationManager)); + + RETURN_IF_FAILED(pModuleInfo->SetGlobalNotifications( + pGlobalModule.release(), + GL_CONFIGURATION_CHANGE | // Configuration change trigers IIS application stop + GL_STOP_LISTENING)); // worker process stop or recycle + + return S_OK; +} +CATCH_RETURN() diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/globalmodule.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/globalmodule.cpp new file mode 100644 index 0000000000..abe08964cd --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/globalmodule.cpp @@ -0,0 +1,70 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "globalmodule.h" + +extern BOOL g_fInShutdown; + +ASPNET_CORE_GLOBAL_MODULE::ASPNET_CORE_GLOBAL_MODULE(std::shared_ptr pApplicationManager) noexcept + :m_pApplicationManager(std::move(pApplicationManager)) +{ +} + +// +// Is called when IIS decided to terminate worker process +// Shut down all core apps +// +GLOBAL_NOTIFICATION_STATUS +ASPNET_CORE_GLOBAL_MODULE::OnGlobalStopListening( + _In_ IGlobalStopListeningProvider * pProvider +) +{ + UNREFERENCED_PARAMETER(pProvider); + + LOG_INFO(L"ASPNET_CORE_GLOBAL_MODULE::OnGlobalStopListening"); + + if (g_fInShutdown) + { + // Avoid receiving two shutudown notifications. + return GL_NOTIFICATION_CONTINUE; + } + + m_pApplicationManager->ShutDown(); + m_pApplicationManager = nullptr; + + // Return processing to the pipeline. + return GL_NOTIFICATION_CONTINUE; +} + +// +// Is called when configuration changed +// Recycled the corresponding core app if its configuration changed +// +GLOBAL_NOTIFICATION_STATUS +ASPNET_CORE_GLOBAL_MODULE::OnGlobalConfigurationChange( + _In_ IGlobalConfigurationChangeProvider * pProvider +) +{ + if (g_fInShutdown) + { + return GL_NOTIFICATION_CONTINUE; + } + // Retrieve the path that has changed. + PCWSTR pwszChangePath = pProvider->GetChangePath(); + + LOG_INFOF(L"ASPNET_CORE_GLOBAL_MODULE::OnGlobalConfigurationChange '%ls'", pwszChangePath); + + // Test for an error. + if (nullptr != pwszChangePath && + _wcsicmp(pwszChangePath, L"MACHINE") != 0 && + _wcsicmp(pwszChangePath, L"MACHINE/WEBROOT") != 0) + { + if (m_pApplicationManager) + { + m_pApplicationManager->RecycleApplicationFromManager(pwszChangePath); + } + } + + // Return processing to the pipeline. + return GL_NOTIFICATION_CONTINUE; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/globalmodule.h b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/globalmodule.h new file mode 100644 index 0000000000..80f047e08d --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/globalmodule.h @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "applicationmanager.h" + +class ASPNET_CORE_GLOBAL_MODULE : NonCopyable, public CGlobalModule +{ + +public: + + ASPNET_CORE_GLOBAL_MODULE( + std::shared_ptr pApplicationManager + ) noexcept; + + virtual ~ASPNET_CORE_GLOBAL_MODULE() = default; + + VOID Terminate() override + { + LOG_INFO(L"ASPNET_CORE_GLOBAL_MODULE::Terminate"); + // Remove the class from memory. + delete this; + } + + GLOBAL_NOTIFICATION_STATUS + OnGlobalStopListening( + _In_ IGlobalStopListeningProvider * pProvider + ) override; + + GLOBAL_NOTIFICATION_STATUS + OnGlobalConfigurationChange( + _In_ IGlobalConfigurationChangeProvider * pProvider + ) override; + +private: + std::shared_ptr m_pApplicationManager; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/proxymodule.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/proxymodule.cpp new file mode 100644 index 0000000000..01707ffa9e --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/proxymodule.cpp @@ -0,0 +1,213 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "proxymodule.h" + +#include "applicationmanager.h" +#include "applicationinfo.h" +#include "exceptions.h" +#include "DisconnectHandler.h" +#include "SRWExclusiveLock.h" + +extern BOOL g_fInShutdown; + +__override + +ASPNET_CORE_PROXY_MODULE_FACTORY::ASPNET_CORE_PROXY_MODULE_FACTORY(HTTP_MODULE_ID moduleId, std::shared_ptr applicationManager) noexcept + : m_pApplicationManager(std::move(applicationManager)), + m_moduleId(moduleId) +{ +} + +HRESULT +ASPNET_CORE_PROXY_MODULE_FACTORY::GetHttpModule( + CHttpModule ** ppModule, + IModuleAllocator * pAllocator +) +{ + + #pragma warning( push ) + #pragma warning ( disable : 26409 ) // Disable "Avoid using new" + *ppModule = new (pAllocator) ASPNET_CORE_PROXY_MODULE(m_moduleId, m_pApplicationManager); + #pragma warning( push ) + if (*ppModule == nullptr) + { + return E_OUTOFMEMORY; + } + return S_OK; +} + +__override +VOID +ASPNET_CORE_PROXY_MODULE_FACTORY::Terminate( + VOID +) noexcept +/*++ + +Routine description: + + Function called by IIS for global (non-request-specific) notifications + +Arguments: + + None. + +Return value: + + None + +--*/ +{ + delete this; +} + +ASPNET_CORE_PROXY_MODULE::ASPNET_CORE_PROXY_MODULE(HTTP_MODULE_ID moduleId, std::shared_ptr applicationManager) noexcept + : m_pApplicationManager(std::move(applicationManager)), + m_pApplicationInfo(nullptr), + m_pHandler(nullptr), + m_moduleId(moduleId), + m_pDisconnectHandler(nullptr) +{ + InitializeSRWLock(&m_requestLock); +} + +ASPNET_CORE_PROXY_MODULE::~ASPNET_CORE_PROXY_MODULE() +{ + RemoveDisconnectHandler(); +} + +__override +REQUEST_NOTIFICATION_STATUS +ASPNET_CORE_PROXY_MODULE::OnExecuteRequestHandler( + IHttpContext * pHttpContext, + IHttpEventProvider * +) +{ + HRESULT hr = S_OK; + REQUEST_NOTIFICATION_STATUS retVal = RQ_NOTIFICATION_CONTINUE; + + TraceContextScope traceScope(pHttpContext->GetTraceContext()); + // We don't want OnAsyncCompletion to complete request before OnExecuteRequestHandler exits + auto lock = SRWExclusiveLock(m_requestLock); + + try + { + if (g_fInShutdown) + { + FINISHED(HRESULT_FROM_WIN32(ERROR_SERVER_SHUTDOWN_IN_PROGRESS)); + } + + FINISHED_IF_FAILED(m_pApplicationManager->GetOrCreateApplicationInfo( + *pHttpContext, + m_pApplicationInfo)); + + FINISHED_IF_FAILED(m_pApplicationInfo->CreateHandler(*pHttpContext, m_pHandler)); + + SetupDisconnectHandler(pHttpContext); + + retVal = m_pHandler->OnExecuteRequestHandler(); + } + catch (...) + { + hr = OBSERVE_CAUGHT_EXCEPTION(); + } + +Finished: + if (FAILED(LOG_IF_FAILED(hr))) + { + retVal = RQ_NOTIFICATION_FINISH_REQUEST; + if (hr == HRESULT_FROM_WIN32(ERROR_SERVER_SHUTDOWN_IN_PROGRESS)) + { + pHttpContext->GetResponse()->SetStatus(503, "Service Unavailable", 0, hr); + } + else + { + pHttpContext->GetResponse()->SetStatus(500, "Internal Server Error", 0, hr); + } + } + + return HandleNotificationStatus(retVal); +} + +__override +REQUEST_NOTIFICATION_STATUS +ASPNET_CORE_PROXY_MODULE::OnAsyncCompletion( + IHttpContext * pHttpContext, + DWORD, + BOOL, + IHttpEventProvider *, + IHttpCompletionInfo * pCompletionInfo +) +{ + TraceContextScope traceScope(pHttpContext->GetTraceContext()); + // We don't want OnAsyncCompletion to complete request before OnExecuteRequestHandler exits + auto lock = SRWExclusiveLock(m_requestLock); + + try + { + return HandleNotificationStatus(m_pHandler->OnAsyncCompletion( + pCompletionInfo->GetCompletionBytes(), + pCompletionInfo->GetCompletionStatus())); + } + catch (...) + { + OBSERVE_CAUGHT_EXCEPTION(); + return HandleNotificationStatus(RQ_NOTIFICATION_FINISH_REQUEST); + } +} + +REQUEST_NOTIFICATION_STATUS ASPNET_CORE_PROXY_MODULE::HandleNotificationStatus(REQUEST_NOTIFICATION_STATUS status) noexcept +{ + if (status != RQ_NOTIFICATION_PENDING) + { + RemoveDisconnectHandler(); + } + + return status; +} + +void ASPNET_CORE_PROXY_MODULE::SetupDisconnectHandler(IHttpContext * pHttpContext) +{ + auto connection = pHttpContext + ->GetConnection(); + + // connection might be null in when applicationInitialization is running + if (connection == nullptr) + { + return; + } + + auto moduleContainer = connection->GetModuleContextContainer(); + + #pragma warning( push ) + #pragma warning ( disable : 26466 ) // Disable "Don't use static_cast downcasts". We build without RTTI support so dynamic_cast is not available + auto pDisconnectHandler = static_cast(moduleContainer->GetConnectionModuleContext(m_moduleId)); + #pragma warning( push ) + + if (pDisconnectHandler == nullptr) + { + auto newHandler = std::make_unique(pHttpContext->GetConnection()); + pDisconnectHandler = newHandler.get(); + // ModuleContextContainer takes ownership of disconnectHandler + // we are trusting that it would not release it before deleting the context + LOG_IF_FAILED(moduleContainer->SetConnectionModuleContext(static_cast(newHandler.release()), m_moduleId)); + } + + // make code analysis happy + if (pDisconnectHandler != nullptr) + { + pDisconnectHandler->SetHandler(::ReferenceRequestHandler(m_pHandler.get())); + m_pDisconnectHandler = pDisconnectHandler; + } +} + +void ASPNET_CORE_PROXY_MODULE::RemoveDisconnectHandler() noexcept +{ + auto handler = m_pDisconnectHandler; + m_pDisconnectHandler = nullptr; + + if (handler != nullptr) + { + handler->RemoveHandler(); + } +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/proxymodule.h b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/proxymodule.h new file mode 100644 index 0000000000..8f5b680e36 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/proxymodule.h @@ -0,0 +1,84 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include +#include "applicationinfo.h" +#include "irequesthandler.h" +#include "applicationmanager.h" +#include "DisconnectHandler.h" + +extern HTTP_MODULE_ID g_pModuleId; + +class ASPNET_CORE_PROXY_MODULE : NonCopyable, public CHttpModule +{ + public: + + ASPNET_CORE_PROXY_MODULE(HTTP_MODULE_ID moduleId, std::shared_ptr applicationManager) noexcept; + + ~ASPNET_CORE_PROXY_MODULE(); + + void * operator new(size_t size, IModuleAllocator * pPlacement) + { + return pPlacement->AllocateMemory(static_cast(size)); + } + + VOID + operator delete( + void * + ) + {} + + __override + REQUEST_NOTIFICATION_STATUS + OnExecuteRequestHandler( + IHttpContext * pHttpContext, + IHttpEventProvider * pProvider + ) override; + + __override + REQUEST_NOTIFICATION_STATUS + OnAsyncCompletion( + IHttpContext * pHttpContext, + DWORD dwNotification, + BOOL fPostNotification, + IHttpEventProvider * pProvider, + IHttpCompletionInfo * pCompletionInfo + ) override; + + + private: + REQUEST_NOTIFICATION_STATUS + HandleNotificationStatus(REQUEST_NOTIFICATION_STATUS status) noexcept; + + void SetupDisconnectHandler(IHttpContext * pHttpContext); + void RemoveDisconnectHandler() noexcept; + + std::shared_ptr m_pApplicationManager; + std::shared_ptr m_pApplicationInfo; + std::unique_ptr m_pHandler; + HTTP_MODULE_ID m_moduleId; + DisconnectHandler * m_pDisconnectHandler; + SRWLOCK m_requestLock {}; +}; + +class ASPNET_CORE_PROXY_MODULE_FACTORY : NonCopyable, public IHttpModuleFactory +{ + public: + ASPNET_CORE_PROXY_MODULE_FACTORY(HTTP_MODULE_ID moduleId, std::shared_ptr applicationManager) noexcept; + virtual ~ASPNET_CORE_PROXY_MODULE_FACTORY() = default; + + HRESULT + GetHttpModule( + CHttpModule ** ppModule, + IModuleAllocator * pAllocator + ) override; + + VOID + Terminate() noexcept override; + + private: + std::shared_ptr m_pApplicationManager; + HTTP_MODULE_ID m_moduleId; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/resource.h b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/resource.h new file mode 100644 index 0000000000..289df99bc3 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/resource.h @@ -0,0 +1,19 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by HtmlResponses.rc +// +#define IN_PROCESS_SHIM_STATIC_HTML 101 +#define OUT_OF_PROCESS_SHIM_STATIC_HTML 102 +#define UNKNOWN_HOSTING_STATIC_HTML 103 + + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 104 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/stdafx.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/stdafx.cpp new file mode 100644 index 0000000000..12fcb1d436 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/stdafx.cpp @@ -0,0 +1,4 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +// Do not remove this file. It is used for precompiled header generation diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/stdafx.h b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/stdafx.h new file mode 100644 index 0000000000..29c485bc9c --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/AspNetCore/stdafx.h @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once +#pragma warning( disable : 4091) + +// +// System related headers +// +#define WIN32_LEAN_AND_MEAN + +#define NTDDI_VERSION NTDDI_WIN7 +#define WINVER _WIN32_WINNT_WIN7 +#define _WIN32_WINNT 0x0601 + +#include +#include +#include +#include +#include "stringu.h" +#include "stringa.h" + +#pragma warning( error : 4091) diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/BaseOutputManager.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/BaseOutputManager.h new file mode 100644 index 0000000000..32706b0192 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/BaseOutputManager.h @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once +#include "IOutputManager.h" +#include "StdWrapper.h" +#include "EventLog.h" +#include "exceptions.h" +#include "StringHelpers.h" +#include "debugutil.h" + +class BaseOutputManager : + public IOutputManager +{ +public: + BaseOutputManager() : BaseOutputManager(/* fEnableNativeLogging */ true) {} + BaseOutputManager(bool enableNativeLogging) : + m_disposed(false), + stdoutWrapper(nullptr), + stderrWrapper(nullptr), + m_enableNativeRedirection(enableNativeLogging) + { + InitializeSRWLock(&m_srwLock); + } + ~BaseOutputManager() {} + + void + TryStartRedirection() + { + const auto startLambda = [&]() { this->Start(); }; + TryOperation(startLambda, L"Could not start stdout redirection in %s. Exception message: %s."); + } + + void + TryStopRedirection() + { + const auto stopLambda = [&]() { this->Stop(); }; + TryOperation(stopLambda, L"Could not stop stdout redirection in %s. Exception message: %s."); + } + +protected: + std::wstring m_stdOutContent; + bool m_disposed; + bool m_enableNativeRedirection; + SRWLOCK m_srwLock{}; + std::unique_ptr stdoutWrapper; + std::unique_ptr stderrWrapper; + + template + void + TryOperation(Functor func, + std::wstring exceptionMessage) + { + try + { + func(); + } + catch (std::runtime_error& exception) + { + EventLog::Warn(ASPNETCORE_EVENT_GENERAL_WARNING, exceptionMessage.c_str(), GetModuleName().c_str(), to_wide_string(exception.what(), GetConsoleOutputCP()).c_str()); + } + catch (...) + { + OBSERVE_CAUGHT_EXCEPTION(); + } + } +}; + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/CommonLib.vcxproj b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/CommonLib.vcxproj new file mode 100644 index 0000000000..2386dfc6ee --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/CommonLib.vcxproj @@ -0,0 +1,293 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {55494E58-E061-4C4C-A0A8-837008E72F85} + Win32Proj + NewCommon + 10.0.15063.0 + + + + StaticLibrary + true + v141 + Unicode + + + StaticLibrary + false + v141 + true + Unicode + + + StaticLibrary + true + v141 + Unicode + + + StaticLibrary + false + v141 + false + Unicode + + + + + + + + + + + + + + + + + + + + + true + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + true + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + false + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + false + C:\AspNetCoreModule\src\IISLib;$(IncludePath) + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + + Use + Level4 + true + Disabled + false + WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + $(IntDir)$(TargetName).pch + stdafx.h + true + MultiThreadedDebug + false + ProgramDatabase + ..\iislib; + true + stdcpp17 + stdafx.h + + + Windows + true + + + + + Use + Level4 + true + Disabled + false + _DEBUG;_LIB;%(PreprocessorDefinitions) + $(IntDir)$(TargetName).pch + stdafx.h + true + ProgramDatabase + false + MultiThreadedDebug + false + ..\iislib; + true + stdcpp17 + stdafx.h + + + Windows + true + + + + + Use + Level4 + true + MaxSpeed + true + true + false + WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + $(IntDir)$(TargetName).pch + stdafx.h + true + MultiThreaded + false + ..\iislib; + true + stdcpp17 + stdafx.h + + + Windows + true + /NODEFAULTLIB:libucrt.lib /DEFAULTLIB:ucrt.lib %(AdditionalOptions) + true + true + + + + + Use + Level4 + true + MaxSpeed + true + true + false + NDEBUG;_LIB;%(PreprocessorDefinitions) + $(IntDir)$(TargetName).pch + stdafx.h + true + ..\iislib; + true + + + MultiThreaded + false + stdcpp17 + stdafx.h + + + Windows + true + /NODEFAULTLIB:libucrt.lib /DEFAULTLIB:ucrt.lib %(AdditionalOptions) + true + true + + + ..\iislib + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create + Create + Create + Create + + + + + + + + {4787a64f-9a3e-4867-a55a-70cb4b2b2ffe} + + + + + Document + mc %(FullPath) + Compiling Event Messages ... + %(Filename).rc;%(Filename).h;MSG0409.bin + mc %(FullPath) + Compiling Event Messages ... + %(Filename).rc;%(Filename).h;MSG0409.bin + mc %(FullPath) + Compiling Event Messages ... + %(Filename).rc;%(Filename).h;MSG0409.bin + mc %(FullPath) + Compiling Event Messages ... + %(Filename).rc;%(Filename).h;MSG0409.bin + + + + + + \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/ConfigurationLoadException.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/ConfigurationLoadException.h new file mode 100644 index 0000000000..0aade37061 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/ConfigurationLoadException.h @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include + +class ConfigurationLoadException: public std::runtime_error +{ + public: + ConfigurationLoadException(std::wstring msg) + : runtime_error("Configuration load exception has occured"), message(std::move(msg)) + { + } + + std::wstring get_message() const { return message; } + + private: + std::wstring message; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/ConfigurationSection.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/ConfigurationSection.cpp new file mode 100644 index 0000000000..11e790cff7 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/ConfigurationSection.cpp @@ -0,0 +1,67 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "ConfigurationSection.h" + +#include "StringHelpers.h" +#include "ConfigurationLoadException.h" + +std::wstring ConfigurationSection::GetRequiredString(const std::wstring& name) const +{ + auto result = GetString(name); + if (!result.has_value() || result.value().empty()) + { + ThrowRequiredException(name); + } + return result.value(); +} + +bool ConfigurationSection::GetRequiredBool(const std::wstring& name) const +{ + auto result = GetBool(name); + if (!result.has_value()) + { + ThrowRequiredException(name); + } + return result.value(); +} + +DWORD ConfigurationSection::GetRequiredLong(const std::wstring& name) const +{ + auto result = GetLong(name); + if (!result.has_value()) + { + ThrowRequiredException(name); + } + return result.value(); +} + +DWORD ConfigurationSection::GetRequiredTimespan(const std::wstring& name) const +{ + auto result = GetTimespan(name); + if (!result.has_value()) + { + ThrowRequiredException(name); + } + return result.value(); +} + +void ConfigurationSection::ThrowRequiredException(const std::wstring& name) +{ + throw ConfigurationLoadException(format(L"Attribute '%s' is required.", name.c_str())); +} + +std::optional find_element(const std::vector>& pairs, const std::wstring& name) +{ + const auto iter = std::find_if( + pairs.begin(), + pairs.end(), + [&](const std::pair& pair) { return equals_ignore_case(pair.first, name); }); + + if (iter == pairs.end()) + { + return std::nullopt; + } + + return std::make_optional(iter->second); +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/ConfigurationSection.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/ConfigurationSection.h new file mode 100644 index 0000000000..f998478db2 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/ConfigurationSection.h @@ -0,0 +1,50 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include +#include +#include + +#include "NonCopyable.h" + +#define CS_ASPNETCORE_COLLECTION_ITEM_NAME L"name" +#define CS_ASPNETCORE_COLLECTION_ITEM_VALUE L"value" +#define CS_ASPNETCORE_ENVIRONMENT_VARIABLES L"environmentVariables" +#define CS_ASPNETCORE_STDOUT_LOG_FILE L"stdoutLogFile" +#define CS_ASPNETCORE_STDOUT_LOG_ENABLED L"stdoutLogEnabled" +#define CS_ASPNETCORE_PROCESS_EXE_PATH L"processPath" +#define CS_ASPNETCORE_PROCESS_ARGUMENTS L"arguments" +#define CS_ASPNETCORE_PROCESS_ARGUMENTS_DEFAULT L"" +#define CS_ASPNETCORE_PROCESS_STARTUP_TIME_LIMIT L"startupTimeLimit" +#define CS_ASPNETCORE_PROCESS_SHUTDOWN_TIME_LIMIT L"shutdownTimeLimit" +#define CS_ASPNETCORE_HOSTING_MODEL_OUTOFPROCESS L"outofprocess" +#define CS_ASPNETCORE_HOSTING_MODEL_INPROCESS L"inprocess" +#define CS_ASPNETCORE_HOSTING_MODEL L"hostingModel" +#define CS_ASPNETCORE_HANDLER_SETTINGS L"handlerSettings" +#define CS_ASPNETCORE_DISABLE_START_UP_ERROR_PAGE L"disableStartUpErrorPage" +#define CS_ENABLED L"enabled" + +class ConfigurationSection: NonCopyable +{ +public: + ConfigurationSection() = default; + virtual ~ConfigurationSection() = default; + virtual std::optional GetString(const std::wstring& name) const = 0; + virtual std::optional GetBool(const std::wstring& name) const = 0; + virtual std::optional GetLong(const std::wstring& name) const = 0; + virtual std::optional GetTimespan(const std::wstring& name) const = 0; + + std::wstring GetRequiredString(const std::wstring& name) const; + bool GetRequiredBool(const std::wstring& name) const; + DWORD GetRequiredLong(const std::wstring& name) const; + DWORD GetRequiredTimespan(const std::wstring& name) const; + + virtual std::vector> GetKeyValuePairs(const std::wstring& name) const = 0; + +protected: + static void ThrowRequiredException(const std::wstring& name); +}; + +std::optional find_element(const std::vector>& pairs, const std::wstring& name); diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/ConfigurationSource.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/ConfigurationSource.cpp new file mode 100644 index 0000000000..558d054659 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/ConfigurationSource.cpp @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "ConfigurationSource.h" + +#include "StringHelpers.h" +#include "ConfigurationLoadException.h" + +std::shared_ptr ConfigurationSource::GetRequiredSection(const std::wstring& name) const +{ + auto section = GetSection(name); + if (!section) + { + throw ConfigurationLoadException(format(L"Unable to get required configuration section '%s'. Possible reason is web.config authoring error.", name.c_str())); + } + return section; +} + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/ConfigurationSource.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/ConfigurationSource.h new file mode 100644 index 0000000000..53ab5a5218 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/ConfigurationSource.h @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include +#include +#include +#include "NonCopyable.h" +#include "ConfigurationSection.h" + +#define CS_ASPNETCORE_SECTION L"system.webServer/aspNetCore" +#define CS_WINDOWS_AUTHENTICATION_SECTION L"system.webServer/security/authentication/windowsAuthentication" +#define CS_BASIC_AUTHENTICATION_SECTION L"system.webServer/security/authentication/basicAuthentication" +#define CS_ANONYMOUS_AUTHENTICATION_SECTION L"system.webServer/security/authentication/anonymousAuthentication" + +class ConfigurationSource: NonCopyable +{ +public: + ConfigurationSource() = default; + virtual ~ConfigurationSource() = default; + virtual std::shared_ptr GetSection(const std::wstring& name) const = 0; + std::shared_ptr GetRequiredSection(const std::wstring& name) const; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/Environment.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/Environment.cpp new file mode 100644 index 0000000000..5e71216ce5 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/Environment.cpp @@ -0,0 +1,62 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "Environment.h" + +#include + +std::wstring +Environment::ExpandEnvironmentVariables(const std::wstring & str) +{ + DWORD requestedSize = ExpandEnvironmentStringsW(str.c_str(), nullptr, 0); + if (requestedSize == 0) + { + throw std::system_error(GetLastError(), std::system_category(), "ExpandEnvironmentVariables"); + } + + std::wstring expandedStr; + do + { + expandedStr.resize(requestedSize); + requestedSize = ExpandEnvironmentStringsW(str.c_str(), expandedStr.data(), requestedSize); + if (requestedSize == 0) + { + throw std::system_error(GetLastError(), std::system_category(), "ExpandEnvironmentVariables"); + } + } while (expandedStr.size() != requestedSize); + + // trim null character as ExpandEnvironmentStringsW returns size including null character + expandedStr.resize(requestedSize - 1); + + return expandedStr; +} + +std::optional +Environment::GetEnvironmentVariableValue(const std::wstring & str) +{ + DWORD requestedSize = GetEnvironmentVariableW(str.c_str(), nullptr, 0); + if (requestedSize == 0) + { + if (GetLastError() == ERROR_ENVVAR_NOT_FOUND) + { + return std::nullopt; + } + + throw std::system_error(GetLastError(), std::system_category(), "GetEnvironmentVariableW"); + } + + std::wstring expandedStr; + do + { + expandedStr.resize(requestedSize); + requestedSize = GetEnvironmentVariableW(str.c_str(), expandedStr.data(), requestedSize); + if (requestedSize == 0) + { + throw std::system_error(GetLastError(), std::system_category(), "ExpandEnvironmentStringsW"); + } + } while (expandedStr.size() != requestedSize + 1); + + expandedStr.resize(requestedSize); + + return expandedStr; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/Environment.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/Environment.h new file mode 100644 index 0000000000..16022e18cf --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/Environment.h @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include +#include + +class Environment +{ +public: + Environment() = delete; + ~Environment() = delete; + + static + std::wstring ExpandEnvironmentVariables(const std::wstring & str); + static + std::optional GetEnvironmentVariableValue(const std::wstring & str); +}; + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/EventLog.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/EventLog.cpp new file mode 100644 index 0000000000..e5ee8c1708 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/EventLog.cpp @@ -0,0 +1,78 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include +#include "EventLog.h" +#include "debugutil.h" +#include "exceptions.h" + +extern HANDLE g_hEventLog; + +bool +EventLog::LogEventNoTrace( + _In_ WORD dwEventInfoType, + _In_ DWORD dwEventId, + _In_ LPCWSTR pstrMsg +) +{ + if (g_hEventLog == nullptr) + { + return true; + } + // Static locals to avoid getting the process ID and string multiple times. + // Effectively have the same semantics as global variables, except initialized + // on first occurence. + static const auto processIdString = GetProcessIdString(); + static const auto versionInfoString = GetVersionInfoString(); + + std::array eventLogDataStrings + { + pstrMsg, + processIdString.c_str(), + versionInfoString.c_str() + }; + + return ReportEventW(g_hEventLog, + dwEventInfoType, + 0, // wCategory + dwEventId, + NULL, // lpUserSid + static_cast(eventLogDataStrings.size()), // wNumStrings + 0, // dwDataSize, + eventLogDataStrings.data(), + NULL // lpRawData + ); +} + +VOID +EventLog::LogEvent( + _In_ WORD dwEventInfoType, + _In_ DWORD dwEventId, + _In_ LPCWSTR pstrMsg +) +{ + LOG_LAST_ERROR_IF(!LogEventNoTrace(dwEventInfoType, dwEventId, pstrMsg)); + + DebugPrintfW(dwEventInfoType == EVENTLOG_ERROR_TYPE ? ASPNETCORE_DEBUG_FLAG_ERROR : ASPNETCORE_DEBUG_FLAG_INFO, L"Event Log: '%ls' \r\nEnd Event Log Message.", pstrMsg); +} + +VOID +EventLog::LogEventF( + _In_ WORD dwEventInfoType, + _In_ DWORD dwEventId, + _In_ LPCWSTR pstrMsg, + _In_ va_list argsList +) +{ + STACK_STRU ( strEventMsg, 256 ); + + if (SUCCEEDED(strEventMsg.SafeVsnwprintf( + pstrMsg, + argsList))) + { + EventLog::LogEvent( + dwEventInfoType, + dwEventId, + strEventMsg.QueryStr()); + } +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/EventLog.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/EventLog.h new file mode 100644 index 0000000000..4bb72b623a --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/EventLog.h @@ -0,0 +1,86 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "resources.h" + +#define _va_start(ap, x) \ + __pragma(warning(push)) \ + __pragma(warning(disable:26481 26492)) /*Don't use pointer arithmetic. Don't use const_cast to cast away const.*/ \ + va_start(ap, x) \ + __pragma(warning(pop)) + +#define _va_end(args) \ + __pragma(warning(push)) \ + __pragma(warning(disable:26477)) /*Use 'nullptr' rather than 0 or NULL*/ \ + va_end(args) \ + __pragma(warning(pop)) + +class EventLog +{ +public: + static + VOID + Error( + _In_ DWORD dwEventId, + _In_ PCWSTR pstrMsg, + ...) + { + va_list args; + _va_start(args, pstrMsg); + LogEventF(EVENTLOG_ERROR_TYPE, dwEventId, pstrMsg, args); + _va_end(args); + } + + static + VOID + Info( + _In_ DWORD dwEventId, + _In_ PCWSTR pstrMsg, + ...) + { + va_list args; + _va_start(args, pstrMsg); + LogEventF(EVENTLOG_INFORMATION_TYPE, dwEventId, pstrMsg, args); + _va_end(args); + } + + static + VOID + Warn( + _In_ DWORD dwEventId, + _In_ PCWSTR pstrMsg, + ...) + { + va_list args; + _va_start(args, pstrMsg); + LogEventF(EVENTLOG_WARNING_TYPE, dwEventId, pstrMsg, args); + _va_end(args); + } + + static + bool + LogEventNoTrace( + _In_ WORD dwEventInfoType, + _In_ DWORD dwEventId, + _In_ LPCWSTR pstrMsg); + +private: + static + VOID + LogEvent( + _In_ WORD dwEventInfoType, + _In_ DWORD dwEventId, + _In_ LPCWSTR pstrMsg + ); + + static + VOID + LogEventF( + _In_ WORD dwEventInfoType, + _In_ DWORD dwEventId, + __in PCWSTR pstrMsg, + va_list argsList + ); +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/EventTracing.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/EventTracing.h new file mode 100644 index 0000000000..3734068aae --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/EventTracing.h @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include + +#pragma warning( push ) +#pragma warning ( disable : ALL_CODE_ANALYSIS_WARNINGS ) + +#include "aspnetcore_event.h" + +#pragma warning( pop ) + +template< class EVENT, typename ...Params > +void RaiseEvent(IHttpTraceContext * pTraceContext,Params&&... params) +{ + if (pTraceContext != nullptr && EVENT::IsEnabled(pTraceContext)) + { + EVENT::RaiseEvent(pTraceContext, std::forward(params)...); + } +} + +template< class EVENT, typename ...Params > +void RaiseEvent(IHttpContext * pHttpContext,Params&&... params) +{ + ::RaiseEvent(pHttpContext->GetTraceContext(), std::forward(params)...); +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/FileOutputManager.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/FileOutputManager.cpp new file mode 100644 index 0000000000..1b3772bb4f --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/FileOutputManager.cpp @@ -0,0 +1,178 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "stdafx.h" +#include "FileOutputManager.h" +#include "sttimer.h" +#include "exceptions.h" +#include "debugutil.h" +#include "SRWExclusiveLock.h" +#include "file_utility.h" +#include "StdWrapper.h" +#include "StringHelpers.h" + +FileOutputManager::FileOutputManager(std::wstring pwzStdOutLogFileName, std::wstring pwzApplicationPath) : + FileOutputManager(pwzStdOutLogFileName, pwzApplicationPath, /* fEnableNativeLogging */ true) { } + +FileOutputManager::FileOutputManager(std::wstring pwzStdOutLogFileName, std::wstring pwzApplicationPath, bool fEnableNativeLogging) : + BaseOutputManager(fEnableNativeLogging), + m_hLogFileHandle(INVALID_HANDLE_VALUE), + m_applicationPath(pwzApplicationPath), + m_stdOutLogFileName(pwzStdOutLogFileName) +{ + InitializeSRWLock(&m_srwLock); +} + +FileOutputManager::~FileOutputManager() +{ + FileOutputManager::Stop(); +} + +// Start redirecting stdout and stderr into the file handle. +// Uses sttimer to continuously flush output into the file. +void +FileOutputManager::Start() +{ + SYSTEMTIME systemTime; + SECURITY_ATTRIBUTES saAttr = { 0 }; + FILETIME processCreationTime; + FILETIME dummyFileTime; + + // To make Console.* functions work, allocate a console + // in the current process. + if (!AllocConsole()) + { + THROW_LAST_ERROR_IF(GetLastError() != ERROR_ACCESS_DENIED); + } + + // Concatenate the log file name and application path + auto logPath = m_applicationPath / m_stdOutLogFileName; + create_directories(logPath.parent_path()); + + THROW_LAST_ERROR_IF(!GetProcessTimes( + GetCurrentProcess(), + &processCreationTime, + &dummyFileTime, + &dummyFileTime, + &dummyFileTime)); + + THROW_LAST_ERROR_IF(!FileTimeToSystemTime(&processCreationTime, &systemTime)); + + m_logFilePath = format(L"%s_%d%02d%02d%02d%02d%02d_%d.log", + logPath.c_str(), + systemTime.wYear, + systemTime.wMonth, + systemTime.wDay, + systemTime.wHour, + systemTime.wMinute, + systemTime.wSecond, + GetCurrentProcessId()); + + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = TRUE; + saAttr.lpSecurityDescriptor = NULL; + + // Create the file with both READ and WRITE. + m_hLogFileHandle = CreateFileW(m_logFilePath.c_str(), + FILE_READ_DATA | FILE_WRITE_DATA, + FILE_SHARE_READ, + &saAttr, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + nullptr); + + THROW_LAST_ERROR_IF(m_hLogFileHandle == INVALID_HANDLE_VALUE); + + stdoutWrapper = std::make_unique(stdout, STD_OUTPUT_HANDLE, m_hLogFileHandle, m_enableNativeRedirection); + stderrWrapper = std::make_unique(stderr, STD_ERROR_HANDLE, m_hLogFileHandle, m_enableNativeRedirection); + + stdoutWrapper->StartRedirection(); + stderrWrapper->StartRedirection(); +} + +void +FileOutputManager::Stop() +{ + CHAR pzFileContents[MAX_FILE_READ_SIZE] = { 0 }; + DWORD dwNumBytesRead; + LARGE_INTEGER li = { 0 }; + DWORD dwFilePointer = 0; + HANDLE handle = NULL; + WIN32_FIND_DATA fileData; + + if (m_disposed) + { + return; + } + + SRWExclusiveLock lock(m_srwLock); + + if (m_disposed) + { + return; + } + + m_disposed = true; + + if (m_hLogFileHandle == INVALID_HANDLE_VALUE) + { + THROW_HR(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)); + } + + FlushFileBuffers(m_hLogFileHandle); + + if (stdoutWrapper != nullptr) + { + THROW_IF_FAILED(stdoutWrapper->StopRedirection()); + } + + if (stderrWrapper != nullptr) + { + THROW_IF_FAILED(stderrWrapper->StopRedirection()); + } + + // delete empty log file + handle = FindFirstFile(m_logFilePath.c_str(), &fileData); + if (handle != INVALID_HANDLE_VALUE && + handle != NULL && + fileData.nFileSizeHigh == 0 && + fileData.nFileSizeLow == 0) // skip check of nFileSizeHigh + { + FindClose(handle); + LOG_LAST_ERROR_IF(!DeleteFile(m_logFilePath.c_str())); + return; + } + + // Read the first 30Kb from the file and store it in a buffer. + // By doing this, we can close the handle to the file and be done with it. + THROW_LAST_ERROR_IF(!GetFileSizeEx(m_hLogFileHandle, &li)); + + if (li.HighPart > 0) + { + THROW_HR(HRESULT_FROM_WIN32(ERROR_FILE_INVALID)); + } + + dwFilePointer = SetFilePointer(m_hLogFileHandle, 0, NULL, FILE_BEGIN); + + THROW_LAST_ERROR_IF(dwFilePointer == INVALID_SET_FILE_POINTER); + + THROW_LAST_ERROR_IF(!ReadFile(m_hLogFileHandle, pzFileContents, MAX_FILE_READ_SIZE, &dwNumBytesRead, NULL)); + + m_stdOutContent = to_wide_string(std::string(pzFileContents, dwNumBytesRead), GetConsoleOutputCP()); + + auto content = GetStdOutContent(); + if (!content.empty()) + { + // printf will fail in in full IIS + if (wprintf(content.c_str()) != -1) + { + // Need to flush contents for the new stdout and stderr + _flushall(); + } + } +} + +std::wstring FileOutputManager::GetStdOutContent() +{ + return m_stdOutContent; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/FileOutputManager.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/FileOutputManager.h new file mode 100644 index 0000000000..435e07d4ff --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/FileOutputManager.h @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "sttimer.h" +#include "HandleWrapper.h" +#include "StdWrapper.h" +#include "stringa.h" +#include "stringu.h" +#include "BaseOutputManager.h" + +class FileOutputManager : public BaseOutputManager +{ + #define MAX_FILE_READ_SIZE 30000 +public: + FileOutputManager(std::wstring pwzApplicationPath, std::wstring pwzStdOutLogFileName); + FileOutputManager(std::wstring pwzApplicationPath, std::wstring pwzStdOutLogFileName, bool fEnableNativeLogging); + ~FileOutputManager(); + + virtual std::wstring GetStdOutContent() override; + void Start() override; + void Stop() override; + +private: + HandleWrapper m_hLogFileHandle; + std::wstring m_stdOutLogFileName; + std::filesystem::path m_applicationPath; + std::filesystem::path m_logFilePath; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/GlobalVersionUtility.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/GlobalVersionUtility.cpp new file mode 100644 index 0000000000..15ce48b8c5 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/GlobalVersionUtility.cpp @@ -0,0 +1,133 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#include +#include + +#include "GlobalVersionUtility.h" + +namespace fs = std::filesystem; + +// throws runtime error if no request handler versions are installed. +// Throw invalid_argument if any argument is null +std::wstring +GlobalVersionUtility::GetGlobalRequestHandlerPath(PCWSTR pwzAspNetCoreFolderPath, PCWSTR pwzHandlerVersion, PCWSTR pwzHandlerName) +{ + if (pwzAspNetCoreFolderPath == NULL) + { + throw new std::invalid_argument("pwzAspNetCoreFolderPath is NULL"); + } + + if (pwzHandlerVersion == NULL) + { + throw new std::invalid_argument("pwzHandlerVersion is NULL"); + } + + if (pwzHandlerName == NULL) + { + throw new std::invalid_argument("pwzHandlerName is NULL"); + } + + std::wstring folderVersion(pwzHandlerVersion); + fs::path aspNetCoreFolderPath(pwzAspNetCoreFolderPath); + + if (folderVersion.empty()) + { + folderVersion = FindHighestGlobalVersion(pwzAspNetCoreFolderPath); + } + + aspNetCoreFolderPath = aspNetCoreFolderPath + .append(folderVersion) + .append(pwzHandlerName); + return aspNetCoreFolderPath; +} + +// Throw filesystem_error if directory_iterator can't iterate over the directory +// Throw invalid_argument if any argument is null +std::vector +GlobalVersionUtility::GetRequestHandlerVersions(PCWSTR pwzAspNetCoreFolderPath) +{ + if (pwzAspNetCoreFolderPath == NULL) + { + throw new std::invalid_argument("pwzAspNetCoreFolderPath is NULL"); + } + + std::vector versionsInDirectory; + for (auto& p : fs::directory_iterator(pwzAspNetCoreFolderPath)) + { + if (!fs::is_directory(p)) + { + continue; + } + + fx_ver_t requested_ver(-1, -1, -1); + if (fx_ver_t::parse(p.path().filename(), &requested_ver, false)) + { + versionsInDirectory.push_back(requested_ver); + } + } + return versionsInDirectory; +} + +// throws runtime error if no request handler versions are installed. +// Throw invalid_argument if any argument is null +std::wstring +GlobalVersionUtility::FindHighestGlobalVersion(PCWSTR pwzAspNetCoreFolderPath) +{ + if (pwzAspNetCoreFolderPath == NULL) + { + throw std::invalid_argument("pwzAspNetCoreFolderPath is NULL"); + } + + std::vector versionsInDirectory = GetRequestHandlerVersions(pwzAspNetCoreFolderPath); + if (versionsInDirectory.empty()) + { + throw std::runtime_error("Cannot find request handler next to aspnetcorev2.dll. Verify a version of the request handler is installed in a version folder."); + } + std::sort(versionsInDirectory.begin(), versionsInDirectory.end()); + + return versionsInDirectory.back().as_str(); +} + +// Throws std::out_of_range if there is an index out of range +// Throw invalid_argument if any argument is null +std::wstring +GlobalVersionUtility::RemoveFileNameFromFolderPath(std::wstring fileName) +{ + fs::path path(fileName); + return path.parent_path(); +} + +std::wstring +GlobalVersionUtility::GetModuleName(HMODULE hModuleName) +{ + DWORD dwSize = MAX_PATH; + BOOL fDone = FALSE; + + // Instead of creating a temporary buffer, use the std::wstring directly as the receive buffer. + std::wstring retVal; + retVal.resize(dwSize); + + while (!fDone) + { + DWORD dwReturnedSize = GetModuleFileNameW(hModuleName, &retVal[0], dwSize); + if (dwReturnedSize == 0) + { + throw new std::runtime_error("GetModuleFileNameW returned 0."); + } + else if ((dwReturnedSize == dwSize) && (GetLastError() == ERROR_INSUFFICIENT_BUFFER)) + { + dwSize *= 2; + retVal.resize(dwSize); // smaller buffer. increase the buffer and retry + } + else + { + // GetModuleFilename will not account for the null terminator + // std::wstring auto appends one, so we don't need to subtract 1 when resizing. + retVal.resize(dwReturnedSize); + fDone = TRUE; + } + } + + return retVal; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/GlobalVersionUtility.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/GlobalVersionUtility.h new file mode 100644 index 0000000000..7cd8420b5b --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/GlobalVersionUtility.h @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#pragma once + +#include "fx_ver.h" + +class GlobalVersionUtility +{ +public: + + static + std::wstring + GetGlobalRequestHandlerPath(PCWSTR pwzAspNetCoreFolderPath, PCWSTR pwzHandlerVersion, PCWSTR pwzHandlerName); + + static + std::wstring + FindHighestGlobalVersion(PCWSTR pwzAspNetCoreFolderPath); + + static + std::wstring + RemoveFileNameFromFolderPath(std::wstring fileName); + + static + std::vector + GetRequestHandlerVersions(PCWSTR pwzAspNetCoreFolderPath); + + static + std::wstring + GetModuleName(HMODULE hModuleName); +}; + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/HandleWrapper.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/HandleWrapper.cpp new file mode 100644 index 0000000000..4960b06f4e --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/HandleWrapper.cpp @@ -0,0 +1,8 @@ +// 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. + +#include "HandleWrapper.h" + +// Workaround for VC++ bug https://developercommunity.visualstudio.com/content/problem/33928/constexpr-failing-on-nullptr-v141-compiler-regress.html +const HANDLE InvalidHandleTraits::DefaultHandle = INVALID_HANDLE_VALUE; +const HANDLE FindFileHandleTraits::DefaultHandle = INVALID_HANDLE_VALUE; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/HandleWrapper.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/HandleWrapper.h new file mode 100644 index 0000000000..56da4ae035 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/HandleWrapper.h @@ -0,0 +1,77 @@ +// 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. + +#pragma once + +#include +#include "ntassert.h" + +struct InvalidHandleTraits +{ + using HandleType = HANDLE; + static const HANDLE DefaultHandle; + static void Close(HANDLE handle) noexcept { CloseHandle(handle); } +}; + +struct NullHandleTraits +{ + using HandleType = HANDLE; + static constexpr HANDLE DefaultHandle = nullptr; + static void Close(HANDLE handle) noexcept { CloseHandle(handle); } +}; + +struct FindFileHandleTraits +{ + using HandleType = HANDLE; + static const HANDLE DefaultHandle; + static void Close(HANDLE handle) noexcept { FindClose(handle); } +}; + +struct ModuleHandleTraits +{ + using HandleType = HMODULE; + static constexpr HMODULE DefaultHandle = nullptr; + static void Close(HMODULE handle) noexcept { FreeModule(handle); } +}; + +// Code analysis doesn't like nullptr usages via traits +#pragma warning( push ) +#pragma warning ( disable : 26477 ) // disable Use 'nullptr' rather than 0 or NULL (es.47). + +template +class HandleWrapper +{ +public: + using HandleType = typename traits::HandleType; + + HandleWrapper(HandleType handle = traits::DefaultHandle) noexcept : m_handle(handle) { } + ~HandleWrapper() + { + if (m_handle != traits::DefaultHandle) + { + traits::Close(m_handle); + } + } + + operator HandleType() noexcept { return m_handle; } + HandleWrapper& operator =(HandleType value) noexcept + { + DBG_ASSERT(m_handle == traits::DefaultHandle); + m_handle = value; + return *this; + } + + HandleType* operator&() noexcept { return &m_handle; } + + HandleType release() noexcept + { + auto value = m_handle; + m_handle = traits::DefaultHandle; + return value; + } + +private: + HandleType m_handle; +}; + +#pragma warning( pop ) diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/IOutputManager.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/IOutputManager.h new file mode 100644 index 0000000000..4f19ad983d --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/IOutputManager.h @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "stdafx.h" +#include "stringa.h" + +class IOutputManager +{ +public: + virtual + void + Start() = 0; + + virtual + ~IOutputManager() = default; + + virtual + std::wstring + GetStdOutContent() = 0; + + virtual + void + Stop() = 0; +}; + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/InvalidOperationException.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/InvalidOperationException.h new file mode 100644 index 0000000000..3963091911 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/InvalidOperationException.h @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once +#include + +class InvalidOperationException: public std::runtime_error +{ + public: + InvalidOperationException(std::wstring msg) + : runtime_error("InvalidOperationException"), message(std::move(msg)) + { + } + + std::wstring as_wstring() const + { + return message; + } + + private: + std::wstring message; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/LoggingHelpers.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/LoggingHelpers.cpp new file mode 100644 index 0000000000..0a33c4c145 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/LoggingHelpers.cpp @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "stdafx.h" +#include "LoggingHelpers.h" +#include "FileOutputManager.h" +#include "PipeOutputManager.h" +#include "NullOutputManager.h" +#include "debugutil.h" +#include +#include +#include "ntassert.h" +#include "exceptions.h" +#include "EventLog.h" +#include "BaseOutputManager.h" + +HRESULT +LoggingHelpers::CreateLoggingProvider( + bool fIsLoggingEnabled, + bool fEnableNativeLogging, + PCWSTR pwzStdOutFileName, + PCWSTR pwzApplicationPath, + std::unique_ptr& outputManager +) +{ + HRESULT hr = S_OK; + + DBG_ASSERT(outputManager != NULL); + + try + { + // Check if there is an existing active console window before redirecting + // Window == IISExpress with active console window, don't redirect to a pipe + // if true. + CONSOLE_SCREEN_BUFFER_INFO dummy; + + if (fIsLoggingEnabled) + { + auto manager = std::make_unique(pwzStdOutFileName, pwzApplicationPath, fEnableNativeLogging); + outputManager = std::move(manager); + } + else if (!GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &dummy)) + { + outputManager = std::make_unique(fEnableNativeLogging); + } + else + { + outputManager = std::make_unique(); + } + } + catch (std::bad_alloc&) + { + hr = E_OUTOFMEMORY; + } + + return hr; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/LoggingHelpers.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/LoggingHelpers.h new file mode 100644 index 0000000000..eff25b47d1 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/LoggingHelpers.h @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "BaseOutputManager.h" + +class LoggingHelpers +{ +public: + + static + HRESULT + CreateLoggingProvider( + bool fLoggingEnabled, + bool fEnableNativeLogging, + PCWSTR pwzStdOutFileName, + PCWSTR pwzApplicationPath, + std::unique_ptr& outputManager + ); +}; + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/ModuleHelpers.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/ModuleHelpers.h new file mode 100644 index 0000000000..cab86d76ea --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/ModuleHelpers.h @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once +#include "HandleWrapper.h" +#include "exceptions.h" + +extern HMODULE g_hModule; + +class ModuleHelpers +{ +public: + static + void IncrementCurrentModuleRefCount(HandleWrapper &handle) + { + WCHAR path[MAX_PATH]; + +#pragma warning( push ) +#pragma warning ( disable : 26485 ) // Calling WinAPI causes expected array to pointer decay + + THROW_LAST_ERROR_IF(!GetModuleFileName(g_hModule, path, MAX_PATH)); + THROW_LAST_ERROR_IF(!GetModuleHandleEx(0, path, &handle)); + +#pragma warning( pop ) + } + + template + static + Func GetKnownProcAddress(HMODULE hModule, LPCSTR lpProcName) { + +#pragma warning( push ) +#pragma warning ( disable : 26490 ) // Disable Don't use reinterpret_cast + auto proc = reinterpret_cast(GetProcAddress(hModule, lpProcName)); +#pragma warning( pop ) + + THROW_LAST_ERROR_IF (!proc); + return proc; + } +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/NonCopyable.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/NonCopyable.h new file mode 100644 index 0000000000..11f73903fa --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/NonCopyable.h @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +class NonCopyable { +public: + NonCopyable() = default; + NonCopyable(const NonCopyable&) = delete; + NonCopyable& operator=(const NonCopyable&) = delete; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/NullOutputManager.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/NullOutputManager.h new file mode 100644 index 0000000000..d9c63b682d --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/NullOutputManager.h @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "stdafx.h" + +class NullOutputManager : public BaseOutputManager +{ +public: + + NullOutputManager() = default; + + ~NullOutputManager() = default; + + void Start() + { + } + + void Stop() + { + } + + std::wstring GetStdOutContent() + { + return L""; + } +}; + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/PipeOutputManager.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/PipeOutputManager.cpp new file mode 100644 index 0000000000..8a9fe0612e --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/PipeOutputManager.cpp @@ -0,0 +1,228 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "PipeOutputManager.h" + +#include "stdafx.h" +#include "Exceptions.h" +#include "SRWExclusiveLock.h" +#include "StdWrapper.h" +#include "ntassert.h" +#include "StringHelpers.h" + +#define LOG_IF_DUPFAIL(err) do { if (err == -1) { LOG_IF_FAILED(HRESULT_FROM_WIN32(_doserrno)); } } while (0, 0); +#define LOG_IF_ERRNO(err) do { if (err != 0) { LOG_IF_FAILED(HRESULT_FROM_WIN32(_doserrno)); } } while (0, 0); + +PipeOutputManager::PipeOutputManager() + : PipeOutputManager( /* fEnableNativeLogging */ true) +{ +} + +PipeOutputManager::PipeOutputManager(bool fEnableNativeLogging) : + BaseOutputManager(fEnableNativeLogging), + m_hErrReadPipe(INVALID_HANDLE_VALUE), + m_hErrWritePipe(INVALID_HANDLE_VALUE), + m_hErrThread(nullptr), + m_numBytesReadTotal(0) +{ +} + +PipeOutputManager::~PipeOutputManager() +{ + PipeOutputManager::Stop(); +} + +// Start redirecting stdout and stderr into a pipe +// Continuously read the pipe on a background thread +// until Stop is called. +void PipeOutputManager::Start() +{ + SECURITY_ATTRIBUTES saAttr = { 0 }; + HANDLE hStdErrReadPipe; + HANDLE hStdErrWritePipe; + + // To make Console.* functions work, allocate a console + // in the current process. + if (!AllocConsole()) + { + // ERROR_ACCESS_DENIED means there is a console already present. + if (GetLastError() != ERROR_ACCESS_DENIED) + { + THROW_LAST_ERROR(); + } + } + + THROW_LAST_ERROR_IF(!CreatePipe(&hStdErrReadPipe, &hStdErrWritePipe, &saAttr, 0 /*nSize*/)); + + m_hErrReadPipe = hStdErrReadPipe; + m_hErrWritePipe = hStdErrWritePipe; + + stdoutWrapper = std::make_unique(stdout, STD_OUTPUT_HANDLE, hStdErrWritePipe, m_enableNativeRedirection); + stderrWrapper = std::make_unique(stderr, STD_ERROR_HANDLE, hStdErrWritePipe, m_enableNativeRedirection); + + LOG_IF_FAILED(stdoutWrapper->StartRedirection()); + LOG_IF_FAILED(stderrWrapper->StartRedirection()); + + // Read the stderr handle on a separate thread until we get 30Kb. + m_hErrThread = CreateThread( + nullptr, // default security attributes + 0, // default stack size + reinterpret_cast(ReadStdErrHandle), + this, // thread function arguments + 0, // default creation flags + nullptr); // receive thread identifier + + THROW_LAST_ERROR_IF_NULL(m_hErrThread); +} + +// Stop redirecting stdout and stderr into a pipe +// This closes the background thread reading from the pipe +// and prints any output that was captured in the pipe. +// If more than 30Kb was written to the pipe, that output will +// be thrown away. +void PipeOutputManager::Stop() +{ + DWORD dwThreadStatus = 0; + + if (m_disposed) + { + return; + } + + SRWExclusiveLock lock(m_srwLock); + + if (m_disposed) + { + return; + } + + m_disposed = true; + + // Both pipe wrappers duplicate the pipe writer handle + // meaning we are fine to close the handle too. + if (m_hErrWritePipe != INVALID_HANDLE_VALUE) + { + // Flush the pipe writer before closing to capture all output + THROW_LAST_ERROR_IF(!FlushFileBuffers(m_hErrWritePipe)); + CloseHandle(m_hErrWritePipe); + m_hErrWritePipe = INVALID_HANDLE_VALUE; + } + + // Tell each pipe wrapper to stop redirecting output and restore the original values + if (stdoutWrapper != nullptr) + { + LOG_IF_FAILED(stdoutWrapper->StopRedirection()); + } + + if (stderrWrapper != nullptr) + { + LOG_IF_FAILED(stderrWrapper->StopRedirection()); + } + + // Forces ReadFile to cancel, causing the read loop to complete. + // Don't check return value as IO may or may not be completed already. + if (m_hErrThread != nullptr) + { + CancelSynchronousIo(m_hErrThread); + } + + // GetExitCodeThread returns 0 on failure; thread status code is invalid. + if (m_hErrThread != nullptr && + !LOG_LAST_ERROR_IF(GetExitCodeThread(m_hErrThread, &dwThreadStatus) == 0) && + dwThreadStatus == STILL_ACTIVE) + { + // Wait for graceful shutdown, i.e., the exit of the background thread or timeout + if (WaitForSingleObject(m_hErrThread, PIPE_OUTPUT_THREAD_TIMEOUT) != WAIT_OBJECT_0) + { + // If the thread is still running, we need kill it first before exit to avoid AV + if (!LOG_LAST_ERROR_IF(GetExitCodeThread(m_hErrThread, &dwThreadStatus) == 0) && + dwThreadStatus == STILL_ACTIVE) + { + LOG_WARN(L"Thread reading stdout/err hit timeout, forcibly closing thread."); + TerminateThread(m_hErrThread, STATUS_CONTROL_C_EXIT); + } + } + } + + if (m_hErrThread != nullptr) + { + CloseHandle(m_hErrThread); + m_hErrThread = nullptr; + } + + if (m_hErrReadPipe != INVALID_HANDLE_VALUE) + { + CloseHandle(m_hErrReadPipe); + m_hErrReadPipe = INVALID_HANDLE_VALUE; + } + + // If we captured any output, relog it to the original stdout + // Useful for the IIS Express scenario as it is running with stdout and stderr + m_stdOutContent = to_wide_string(std::string(m_pipeContents, m_numBytesReadTotal), GetConsoleOutputCP()); + + if (!m_stdOutContent.empty()) + { + // printf will fail in in full IIS + if (wprintf(m_stdOutContent.c_str()) != -1) + { + // Need to flush contents for the new stdout and stderr + _flushall(); + } + } +} + +std::wstring PipeOutputManager::GetStdOutContent() +{ + return m_stdOutContent; +} + +void +PipeOutputManager::ReadStdErrHandle( + LPVOID pContext +) +{ + auto pLoggingProvider = static_cast(pContext); + DBG_ASSERT(pLoggingProvider != NULL); + pLoggingProvider->ReadStdErrHandleInternal(); +} + +void +PipeOutputManager::ReadStdErrHandleInternal() +{ + // If ReadFile ever returns false, exit the thread + DWORD dwNumBytesRead = 0; + while (true) + { + // Fill a maximum of MAX_PIPE_READ_SIZE into a buffer. + if (ReadFile(m_hErrReadPipe, + &m_pipeContents[m_numBytesReadTotal], + MAX_PIPE_READ_SIZE - m_numBytesReadTotal, + &dwNumBytesRead, + nullptr)) + { + m_numBytesReadTotal += dwNumBytesRead; + if (m_numBytesReadTotal >= MAX_PIPE_READ_SIZE) + { + break; + } + } + else + { + return; + } + } + + // Using std::string as a wrapper around new char[] so we don't need to call delete + // Also don't allocate on stack as stack size is 128KB by default. + std::string tempBuffer; + tempBuffer.resize(MAX_PIPE_READ_SIZE); + + // After reading the maximum amount of data, keep reading in a loop until Stop is called on the output manager. + while (true) + { + if (!ReadFile(m_hErrReadPipe, tempBuffer.data(), MAX_PIPE_READ_SIZE, &dwNumBytesRead, nullptr)) + { + return; + } + } +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/PipeOutputManager.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/PipeOutputManager.h new file mode 100644 index 0000000000..3a1a74cd43 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/PipeOutputManager.h @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "StdWrapper.h" +#include "stringu.h" +#include "BaseOutputManager.h" + +class PipeOutputManager : public BaseOutputManager +{ + // Timeout to be used if a thread never exits + #define PIPE_OUTPUT_THREAD_TIMEOUT 2000 + + // Max event log message is ~32KB, limit pipe size just below that. + #define MAX_PIPE_READ_SIZE 30000 +public: + PipeOutputManager(); + PipeOutputManager(bool fEnableNativeLogging); + ~PipeOutputManager(); + + void Start() override; + void Stop() override; + std::wstring GetStdOutContent() override; + + // Thread functions + void ReadStdErrHandleInternal(); + + static void ReadStdErrHandle(LPVOID pContext); + +private: + + HANDLE m_hErrReadPipe; + HANDLE m_hErrWritePipe; + HANDLE m_hErrThread; + CHAR m_pipeContents[MAX_PIPE_READ_SIZE] = { 0 }; + DWORD m_numBytesReadTotal; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/ResultException.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/ResultException.h new file mode 100644 index 0000000000..e6cb8b4b81 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/ResultException.h @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +class ResultException: public std::runtime_error +{ +public: + ResultException(HRESULT hr, LOCATION_ARGUMENTS_ONLY) : + runtime_error(format("HRESULT 0x%x returned at " LOCATION_FORMAT, hr, LOCATION_CALL_ONLY)), + m_hr(hr) + { + } + + HRESULT GetResult() const noexcept { return m_hr; } + +private: + HRESULT m_hr; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/SRWExclusiveLock.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/SRWExclusiveLock.cpp new file mode 100644 index 0000000000..896189c8a8 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/SRWExclusiveLock.cpp @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "SRWExclusiveLock.h" + +SRWExclusiveLock::SRWExclusiveLock(const SRWLOCK& lock) noexcept + : m_lock(lock) +{ + AcquireSRWLockExclusive(const_cast(&m_lock)); +} + +SRWExclusiveLock::~SRWExclusiveLock() +{ + ReleaseSRWLockExclusive(const_cast(&m_lock)); +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/SRWExclusiveLock.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/SRWExclusiveLock.h new file mode 100644 index 0000000000..6c17e13111 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/SRWExclusiveLock.h @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include + +class SRWExclusiveLock +{ +public: + SRWExclusiveLock(const SRWLOCK& lock) noexcept; + ~SRWExclusiveLock(); +private: + const SRWLOCK& m_lock; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/SRWSharedLock.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/SRWSharedLock.cpp new file mode 100644 index 0000000000..51f88ccbe0 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/SRWSharedLock.cpp @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "SRWSharedLock.h" + +SRWSharedLock::SRWSharedLock(const SRWLOCK& lock) + : m_lock(lock) +{ + AcquireSRWLockShared(const_cast(&m_lock)); +} + +SRWSharedLock::~SRWSharedLock() +{ + ReleaseSRWLockShared(const_cast(&m_lock)); +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/SRWSharedLock.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/SRWSharedLock.h new file mode 100644 index 0000000000..7f1573e27e --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/SRWSharedLock.h @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include + +class SRWSharedLock +{ +public: + SRWSharedLock(const SRWLOCK& lock); + ~SRWSharedLock(); +private: + const SRWLOCK& m_lock; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/ServerErrorHandler.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/ServerErrorHandler.h new file mode 100644 index 0000000000..eb493ce722 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/ServerErrorHandler.h @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once +#include "requesthandler.h" +#include "file_utility.h" +#include "Environment.h" + +class ServerErrorHandler : public REQUEST_HANDLER +{ +public: + + ServerErrorHandler(IHttpContext &pContext, USHORT statusCode, USHORT subStatusCode, std::string statusText, HRESULT hr, HINSTANCE moduleInstance, bool disableStartupPage, int page) noexcept + : REQUEST_HANDLER(pContext), + m_pContext(pContext), + m_HR(hr), + m_disableStartupPage(disableStartupPage), + m_page(page), + m_moduleInstance(moduleInstance), + m_statusCode(statusCode), + m_subStatusCode(subStatusCode), + m_statusText(std::move(statusText)) + { + } + + REQUEST_NOTIFICATION_STATUS ExecuteRequestHandler() override + { + static std::string s_html500Page = GetHtml(m_moduleInstance, m_page); + + WriteStaticResponse(m_pContext, s_html500Page, m_HR, m_disableStartupPage); + + return RQ_NOTIFICATION_FINISH_REQUEST; + } + +private: + void WriteStaticResponse(IHttpContext& pContext, std::string &page, HRESULT hr, bool disableStartupErrorPage) const + { + if (disableStartupErrorPage) + { + pContext.GetResponse()->SetStatus(m_statusCode, m_statusText.c_str(), m_subStatusCode, E_FAIL); + return; + } + + HTTP_DATA_CHUNK dataChunk = {}; + IHttpResponse* pResponse = pContext.GetResponse(); + pResponse->SetStatus(m_statusCode, m_statusText.c_str(), m_subStatusCode, hr, nullptr, true); + pResponse->SetHeader("Content-Type", + "text/html", + (USHORT)strlen("text/html"), + FALSE + ); + dataChunk.DataChunkType = HttpDataChunkFromMemory; + + dataChunk.FromMemory.pBuffer = page.data(); + dataChunk.FromMemory.BufferLength = static_cast(page.size()); + pResponse->WriteEntityChunkByReference(&dataChunk); + } + + static + std::string + GetHtml(HMODULE module, int page) + { + try + { + HRSRC rc = nullptr; + HGLOBAL rcData = nullptr; + std::string data; + const char* pTempData = nullptr; + + THROW_LAST_ERROR_IF_NULL(rc = FindResource(module, MAKEINTRESOURCE(page), RT_HTML)); + THROW_LAST_ERROR_IF_NULL(rcData = LoadResource(module, rc)); + auto const size = SizeofResource(module, rc); + THROW_LAST_ERROR_IF(size == 0); + THROW_LAST_ERROR_IF_NULL(pTempData = static_cast(LockResource(rcData))); + data = std::string(pTempData, size); + + auto additionalErrorLink = Environment::GetEnvironmentVariableValue(L"ANCM_ADDITIONAL_ERROR_PAGE_LINK"); + std::string additionalHtml; + + if (additionalErrorLink.has_value()) + { + additionalHtml = format(" %S and ", additionalErrorLink->c_str(), additionalErrorLink->c_str()); + } + + return format(data, additionalHtml.c_str()); + } + catch (...) + { + OBSERVE_CAUGHT_EXCEPTION(); + return ""; + } + } + + IHttpContext &m_pContext; + HRESULT m_HR; + bool m_disableStartupPage; + int m_page; + HINSTANCE m_moduleInstance; + USHORT m_statusCode; + USHORT m_subStatusCode; + std::string m_statusText; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/StdWrapper.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/StdWrapper.cpp new file mode 100644 index 0000000000..809864f06a --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/StdWrapper.cpp @@ -0,0 +1,148 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "stdafx.h" +#include "StdWrapper.h" +#include "exceptions.h" +#include "LoggingHelpers.h" +#include +#include + +StdWrapper::StdWrapper(FILE* stdStream, DWORD stdHandleNumber, HANDLE handleToRedirectTo, BOOL fEnableNativeRedirection) + : m_previousFileDescriptor(0), + m_stdStream(stdStream), + m_stdHandleNumber(stdHandleNumber), + m_enableNativeRedirection(fEnableNativeRedirection), + m_handleToRedirectTo(handleToRedirectTo), + m_redirectedFile(nullptr) +{ +} + +StdWrapper::~StdWrapper() = default; + +// Redirects stdout/stderr to the provided handle. +// Example: +// If the handleToRedirecTo = 0x24 +// Before: +// _fileno(stdout) = 1 +// GetStdHandle(STD_OUTPUT_HANDLE) = 0x20 +// After: +// _fileno(stdout) = 3 +// GetStdHandle(STD_OUTPUT_HANDLE) = 0x28 <- Duplicated from 0x24 +HRESULT +StdWrapper::StartRedirection() +{ + HANDLE stdHandle; + + // In IIS, stdout and stderr are set to null as w3wp is created with DETACHED_PROCESS, + // so fileno(m_stdStream) returns -2. + // Open a null file such that undoing the redirection succeeds and _dup2 works. + // m_previousFileDescriptor will be used for restoring stdout/stderr + if (_fileno(m_stdStream) == -2) + { + freopen_s((FILE**)&m_stdStream, "nul", "w", m_stdStream); + m_previousFileDescriptor = _dup(_fileno(m_stdStream)); + } + else + { + m_previousFileDescriptor = _dup(_fileno(m_stdStream)); + } + + if (!m_enableNativeRedirection) + { + RETURN_LAST_ERROR_IF(!SetStdHandle(m_stdHandleNumber, m_handleToRedirectTo)); + + return S_OK; + } + // After setting the std handle, we need to set stdout/stderr to the current + // output/error handle. + + // Duplicate the handle before opening the handle and associating a file pointer. + // If we don't by calling close on the file, the same handle value will be closed + // multiple times. + // Note, by calling duplicate handle, the new handle returned will have a different value + // than the original, but point to the same underlying file object. + RETURN_LAST_ERROR_IF(!DuplicateHandle( + /* hSourceProcessHandle*/ GetCurrentProcess(), + /* hSourceHandle */ m_handleToRedirectTo, + /* hTargetProcessHandle */ GetCurrentProcess(), + /* lpTargetHandle */&stdHandle, + /* dwDesiredAccess */ 0, // dwDesired is ignored if DUPLICATE_SAME_ACCESS is specified + /* bInheritHandle */ TRUE, + /* dwOptions */ DUPLICATE_SAME_ACCESS)); + + RETURN_LAST_ERROR_IF(!SetStdHandle(m_stdHandleNumber, stdHandle)); + + // _open_osfhandle will associate a filedescriptor with the handle. + const auto fileDescriptor = _open_osfhandle(reinterpret_cast(stdHandle), _O_WTEXT | _O_TEXT); + + if (fileDescriptor == -1) + { + RETURN_HR(HRESULT_FROM_WIN32(ERROR_FILE_INVALID)); + } + + m_redirectedFile = _fdopen(fileDescriptor, "w"); + + if (m_redirectedFile == nullptr) + { + RETURN_HR(HRESULT_FROM_WIN32(ERROR_FILE_INVALID)); + } + + // Set stdout/stderr to the newly created file. + const auto dup2Result = _dup2(_fileno(m_redirectedFile), _fileno(m_stdStream)); + + if (dup2Result != 0) + { + RETURN_HR(HRESULT_FROM_WIN32(ERROR_FILE_INVALID)); + } + + // Removes buffering from the output + if (setvbuf(m_stdStream, nullptr, _IONBF, 0) != 0) + { + RETURN_HR(HRESULT_FROM_WIN32(ERROR_FILE_INVALID)); + } + + return S_OK; +} + +// Redirects stdout/stderr back to the original stdout/stderr. +// Note, this will not restore the original handle values returned by GetStdHandle, +// rather a duplicated number. This is because the original handle value is invalid +// due to dup2 closing the file originally in stdout/stderr +HRESULT +StdWrapper::StopRedirection() const +{ + // After setting the std handle, we need to set stdout/stderr to the current + // output/error handle. + FILE * file = _fdopen(m_previousFileDescriptor, "w"); + if (file == nullptr) + { + RETURN_HR(HRESULT_FROM_WIN32(ERROR_FILE_INVALID)); + } + + RETURN_LAST_ERROR_IF(!SetStdHandle(m_stdHandleNumber, reinterpret_cast(_get_osfhandle(m_previousFileDescriptor)))); + + if (!m_enableNativeRedirection) + { + return S_OK; + } + + // Set stdout/stderr to the newly created file output. + const auto dup2Result = _dup2(_fileno(file), _fileno(m_stdStream)); + if (dup2Result != 0) + { + RETURN_HR(HRESULT_FROM_WIN32(ERROR_FILE_INVALID)); + } + + if (setvbuf(m_stdStream, nullptr, _IONBF, 0) != 0) + { + RETURN_HR(HRESULT_FROM_WIN32(ERROR_FILE_INVALID)); + } + + if (fclose(m_redirectedFile) != 0) + { + RETURN_HR(HRESULT_FROM_WIN32(ERROR_FILE_INVALID)); + } + + return S_OK; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/StdWrapper.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/StdWrapper.h new file mode 100644 index 0000000000..17373f093e --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/StdWrapper.h @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include + +// Wraps stdout/stderr stream, modifying them to redirect to the given handle +class StdWrapper +{ +public: + StdWrapper(FILE* stdStream, DWORD stdHandleNumber, HANDLE handleToRedirectTo, BOOL fEnableNativeRedirection); + ~StdWrapper(); + HRESULT StartRedirection(); + HRESULT StopRedirection() const; + +private: + int m_previousFileDescriptor; + FILE* m_stdStream; + DWORD m_stdHandleNumber; + BOOL m_enableNativeRedirection; + HANDLE m_handleToRedirectTo; + FILE* m_redirectedFile; +}; + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/StringHelpers.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/StringHelpers.cpp new file mode 100644 index 0000000000..736597b9a5 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/StringHelpers.cpp @@ -0,0 +1,43 @@ +// 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. + +#include "StringHelpers.h" +#include "exceptions.h" + +bool ends_with(const std::wstring &source, const std::wstring &suffix, bool ignoreCase) +{ + if (source.length() < suffix.length()) + { + return false; + } + + const auto offset = source.length() - suffix.length(); + return CSTR_EQUAL == CompareStringOrdinal(source.c_str() + offset, static_cast(suffix.length()), suffix.c_str(), static_cast(suffix.length()), ignoreCase); +} + +bool equals_ignore_case(const std::wstring& s1, const std::wstring& s2) +{ + return CSTR_EQUAL == CompareStringOrdinal(s1.c_str(), static_cast(s1.length()), s2.c_str(), static_cast(s2.length()), true); +} + +std::wstring to_wide_string(const std::string &source, const unsigned int codePage) +{ + // MultiByteToWideChar returns 0 on failure, which is also the same return value + // for empty strings. Preemptive return. + if (source.length() == 0) + { + return L""; + } + + std::wstring destination; + + int nChars = MultiByteToWideChar(codePage, 0, source.data(), static_cast(source.length()), NULL, 0); + THROW_LAST_ERROR_IF(nChars == 0); + + destination.resize(nChars); + + nChars = MultiByteToWideChar(codePage, 0, source.data(), static_cast(source.length()), destination.data(), nChars); + THROW_LAST_ERROR_IF(nChars == 0); + + return destination; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/StringHelpers.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/StringHelpers.h new file mode 100644 index 0000000000..3adc8863a4 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/StringHelpers.h @@ -0,0 +1,50 @@ +// 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. + +#pragma once + +#include + +[[nodiscard]] +bool ends_with(const std::wstring &source, const std::wstring &suffix, bool ignoreCase = false); + +[[nodiscard]] +bool equals_ignore_case(const std::wstring& s1, const std::wstring& s2); + +[[nodiscard]] +std::wstring to_wide_string(const std::string &source, const unsigned int codePage); + +template +[[nodiscard]] +std::wstring format(const std::wstring& format, Args ... args) +{ + std::wstring result; + if (!format.empty()) + { + const size_t size = swprintf(nullptr, 0, format.c_str(), args ...); // Extra char for '\0' + result.resize(size); + if (swprintf(result.data(), result.size() + 1, format.c_str(), args ... ) == -1) + { + throw std::system_error(std::error_code(errno, std::system_category())); + } + } + return result; +} + +template +[[nodiscard]] +std::string format(const std::string& format, Args ... args) +{ + std::string result; + if (!format.empty()) + { + const size_t size = snprintf(nullptr, 0, format.c_str(), args ...); // Extra char for '\0' + result.resize(size); + if (snprintf(result.data(), result.size() + 1, format.c_str(), args ... ) == -1) + { + throw std::system_error(std::error_code(errno, std::system_category())); + } + } + return result; +} + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/WebConfigConfigurationSection.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/WebConfigConfigurationSection.cpp new file mode 100644 index 0000000000..7003442f01 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/WebConfigConfigurationSection.cpp @@ -0,0 +1,92 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "WebConfigConfigurationSection.h" + +#include "exceptions.h" +#include "ahutil.h" + +std::optional WebConfigConfigurationSection::GetString(const std::wstring& name) const +{ + CComBSTR result; + if (FAILED_LOG(GetElementStringProperty(m_element, name.c_str(), &result.m_str))) + { + return std::nullopt; + } + + return std::make_optional(std::wstring(result)); +} + +std::optional WebConfigConfigurationSection::GetBool(const std::wstring& name) const +{ + bool result; + if (FAILED_LOG(GetElementBoolProperty(m_element, name.c_str(), &result))) + { + return std::nullopt; + } + + return std::make_optional(result); +} + +std::optional WebConfigConfigurationSection::GetLong(const std::wstring& name) const +{ + DWORD result; + if (FAILED_LOG(GetElementDWORDProperty(m_element, name.c_str(), &result))) + { + return std::nullopt; + } + + return std::make_optional(result); +} + +std::optional WebConfigConfigurationSection::GetTimespan(const std::wstring& name) const +{ + ULONGLONG result; + if (FAILED_LOG(GetElementRawTimeSpanProperty(m_element, name.c_str(), &result))) + { + return std::nullopt; + } + + return std::make_optional(static_cast(result / 10000ull)); +} + +std::vector> WebConfigConfigurationSection::GetKeyValuePairs(const std::wstring& name) const +{ + std::vector> pairs; + HRESULT findElementResult; + CComPtr element = nullptr; + CComPtr elementCollection = nullptr; + CComPtr collectionEntry = nullptr; + ENUM_INDEX index{}; + + if (FAILED_LOG(GetElementChildByName(m_element, name.c_str(), &element))) + { + return pairs; + } + + THROW_IF_FAILED(element->get_Collection(&elementCollection)); + THROW_IF_FAILED(findElementResult = FindFirstElement(elementCollection, &index, &collectionEntry)); + + while (findElementResult != S_FALSE) + { + CComBSTR strHandlerName; + if (LOG_IF_FAILED(GetElementStringProperty(collectionEntry, CS_ASPNETCORE_COLLECTION_ITEM_NAME, &strHandlerName.m_str))) + { + ThrowRequiredException(CS_ASPNETCORE_COLLECTION_ITEM_NAME); + } + + CComBSTR strHandlerValue; + if (LOG_IF_FAILED(GetElementStringProperty(collectionEntry, CS_ASPNETCORE_COLLECTION_ITEM_VALUE, &strHandlerValue.m_str))) + { + ThrowRequiredException(CS_ASPNETCORE_COLLECTION_ITEM_VALUE); + } + + pairs.emplace_back(strHandlerName, strHandlerValue); + + collectionEntry.Release(); + + THROW_IF_FAILED(findElementResult = FindNextElement(elementCollection, &index, &collectionEntry)); + } + + return pairs; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/WebConfigConfigurationSection.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/WebConfigConfigurationSection.h new file mode 100644 index 0000000000..b206c02049 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/WebConfigConfigurationSection.h @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include +#include +#include "ConfigurationSection.h" + +class WebConfigConfigurationSection: public ConfigurationSection +{ +public: + WebConfigConfigurationSection(IAppHostElement* pElement) + : m_element(pElement) + { + } + + std::optional GetString(const std::wstring& name) const override; + std::optional GetBool(const std::wstring& name) const override; + std::optional GetLong(const std::wstring& name) const override; + std::optional GetTimespan(const std::wstring& name) const override; + std::vector> GetKeyValuePairs(const std::wstring& name) const override; + +private: + CComPtr m_element; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/WebConfigConfigurationSource.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/WebConfigConfigurationSource.cpp new file mode 100644 index 0000000000..c433913ec7 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/WebConfigConfigurationSource.cpp @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "WebConfigConfigurationSource.h" + +#include "exceptions.h" +#include "WebConfigConfigurationSection.h" + +std::shared_ptr WebConfigConfigurationSource::GetSection(const std::wstring& name) const +{ + const CComBSTR bstrAspNetCoreSection = name.c_str(); + const CComBSTR applicationConfigPath = m_application.GetAppConfigPath(); + + IAppHostElement* sectionElement; + if (LOG_IF_FAILED(m_manager->GetAdminSection(bstrAspNetCoreSection, applicationConfigPath, §ionElement))) + { + return nullptr; + } + return std::make_unique(sectionElement); +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/WebConfigConfigurationSource.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/WebConfigConfigurationSource.h new file mode 100644 index 0000000000..d0c866096b --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/WebConfigConfigurationSource.h @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once +#include +#include "ConfigurationSection.h" +#include "ConfigurationSource.h" + +class WebConfigConfigurationSource: public ConfigurationSource +{ +public: + WebConfigConfigurationSource(IAppHostAdminManager *pAdminManager, const IHttpApplication &pHttpApplication) noexcept + : m_manager(pAdminManager), + m_application(pHttpApplication) + { + } + + std::shared_ptr GetSection(const std::wstring& name) const override; + +private: + CComPtr m_manager; + const IHttpApplication &m_application; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/application.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/application.h new file mode 100644 index 0000000000..add050b208 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/application.h @@ -0,0 +1,148 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include +#include "iapplication.h" +#include "ntassert.h" +#include "SRWExclusiveLock.h" +#include "exceptions.h" + +class APPLICATION : public IAPPLICATION +{ +public: + // Non-copyable + APPLICATION(const APPLICATION&) = delete; + const APPLICATION& operator=(const APPLICATION&) = delete; + + HRESULT + TryCreateHandler( + _In_ IHttpContext *pHttpContext, + _Outptr_result_maybenull_ IREQUEST_HANDLER **pRequestHandler) override + { + TraceContextScope traceScope(pHttpContext->GetTraceContext()); + *pRequestHandler = nullptr; + + if (m_fStopCalled) + { + return S_FALSE; + } + + return CreateHandler(pHttpContext, pRequestHandler); + } + + virtual + HRESULT + CreateHandler( + _In_ IHttpContext *pHttpContext, + _Outptr_opt_ IREQUEST_HANDLER **pRequestHandler) = 0; + + APPLICATION(const IHttpApplication& pHttpApplication) + : m_fStopCalled(false), + m_cRefs(1), + m_applicationPhysicalPath(pHttpApplication.GetApplicationPhysicalPath()), + m_applicationConfigPath(pHttpApplication.GetAppConfigPath()), + m_applicationId(pHttpApplication.GetApplicationId()) + { + InitializeSRWLock(&m_stateLock); + m_applicationVirtualPath = ToVirtualPath(m_applicationConfigPath); + } + + + VOID + Stop(bool fServerInitiated) override + { + SRWExclusiveLock stopLock(m_stateLock); + + if (m_fStopCalled) + { + return; + } + + m_fStopCalled = true; + + StopInternal(fServerInitiated); + } + + virtual + VOID + StopInternal(bool fServerInitiated) + { + UNREFERENCED_PARAMETER(fServerInitiated); + } + + VOID + ReferenceApplication() noexcept override + { + DBG_ASSERT(m_cRefs > 0); + + InterlockedIncrement(&m_cRefs); + } + + VOID + DereferenceApplication() noexcept override + { + DBG_ASSERT(m_cRefs > 0); + + if (InterlockedDecrement(&m_cRefs) == 0) + { + delete this; + } + } + + const std::wstring& + QueryApplicationId() const noexcept + { + return m_applicationId; + } + + const std::wstring& + QueryApplicationPhysicalPath() const noexcept + { + return m_applicationPhysicalPath; + } + + const std::wstring& + QueryApplicationVirtualPath() const noexcept + { + return m_applicationVirtualPath; + } + + const std::wstring& + QueryConfigPath() const noexcept + { + return m_applicationConfigPath; + } + +protected: + SRWLOCK m_stateLock {}; + bool m_fStopCalled; + +private: + mutable LONG m_cRefs; + + std::wstring m_applicationPhysicalPath; + std::wstring m_applicationVirtualPath; + std::wstring m_applicationConfigPath; + std::wstring m_applicationId; + + static std::wstring ToVirtualPath(const std::wstring& configurationPath) + { + auto segments = 0; + auto position = configurationPath.find('/'); + // Skip first 4 segments of config path + while (segments != 3 && position != std::wstring::npos) + { + segments++; + position = configurationPath.find('/', position + 1); + } + + if (position != std::wstring::npos) + { + return configurationPath.substr(position); + } + + return L"/"; + } +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/aspnetcore_event.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/aspnetcore_event.h new file mode 100644 index 0000000000..d3ed02223a --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/aspnetcore_event.h @@ -0,0 +1,1077 @@ + +#ifndef __ASPNETCOREEVENT_H__ +#define __ASPNETCOREEVENT_H__ +/*++ + + Copyright (c) 2005 Microsoft Corporation + + Module Name: + + aspnetcore_event.h + + Abstract: + + Header file has been generated from mof file containing + IIS trace event descriptions + +--*/ + + + +// +// Start of the new provider class WWWServerTraceProvider, +// GUID: {3a2a4e84-4c21-4981-ae10-3fda0d9b0f83} +// Description: IIS: WWW Server +// + +class WWWServerTraceProvider +{ +public: + static + LPCGUID + GetProviderGuid( VOID ) + // return GUID for the current event class + { + static const GUID ProviderGuid = + {0x3a2a4e84,0x4c21,0x4981,{0xae,0x10,0x3f,0xda,0x0d,0x9b,0x0f,0x83}}; + return &ProviderGuid; + }; + enum enumAreaFlags + { + // AspNetCore module events + ANCM = 0x10000 + }; + static + LPCWSTR + TranslateEnumAreaFlagsToString( enum enumAreaFlags EnumValue) + { + switch( (DWORD) EnumValue ) + { + case 0x10000: return L"ANCM"; + } + return NULL; + }; + + static + BOOL + CheckTracingEnabled( + IHttpTraceContext * pHttpTraceContext, + enumAreaFlags AreaFlags, + DWORD dwVerbosity ) + { + HRESULT hr; + HTTP_TRACE_CONFIGURATION TraceConfig; + TraceConfig.pProviderGuid = GetProviderGuid(); + hr = pHttpTraceContext->GetTraceConfiguration( &TraceConfig ); + if ( FAILED( hr ) || !TraceConfig.fProviderEnabled ) + { + return FALSE; + } + if ( TraceConfig.dwVerbosity >= dwVerbosity && + ( TraceConfig.dwAreas == (DWORD) AreaFlags || + ( TraceConfig.dwAreas & (DWORD)AreaFlags ) == (DWORD)AreaFlags ) ) + { + return TRUE; + } + return FALSE; + }; +}; + +// +// Start of the new event class ANCMEvents, +// GUID: {82ADEAD7-12B2-4781-BDCA-5A4B6C757191} +// Description: ANCM runtime events +// + +class ANCMEvents +{ +public: + static + LPCGUID + GetAreaGuid( VOID ) + // return GUID for the current event class + { + static const GUID AreaGuid = + {0x82adead7,0x12b2,0x4781,{0xbd,0xca,0x5a,0x4b,0x6c,0x75,0x71,0x91}}; + return &AreaGuid; + }; + + // + // Event: mof class name ANCMAppStart, + // Description: Start application success + // EventTypeName: ANCM_START_APPLICATION_SUCCESS + // EventType: 1 + // EventLevel: 4 + // + + class ANCM_START_APPLICATION_SUCCESS + { + public: + static + HRESULT + RaiseEvent( + IHttpTraceContext * pHttpTraceContext, + LPCGUID pContextId, + LPCWSTR pAppDescription + ) + // + // Raise ANCM_START_APPLICATION_SUCCESS Event + // + { + HTTP_TRACE_EVENT Event; + Event.pProviderGuid = WWWServerTraceProvider::GetProviderGuid(); + Event.dwArea = WWWServerTraceProvider::ANCM; + Event.pAreaGuid = ANCMEvents::GetAreaGuid(); + Event.dwEvent = 1; + Event.pszEventName = L"ANCM_START_APPLICATION_SUCCESS"; + Event.dwEventVersion = 1; + Event.dwVerbosity = 4; + Event.cEventItems = 2; + Event.pActivityGuid = NULL; + Event.pRelatedActivityGuid = NULL; + Event.dwTimeStamp = 0; + Event.dwFlags = HTTP_TRACE_EVENT_FLAG_STATIC_DESCRIPTIVE_FIELDS; + + // pActivityGuid, pRelatedActivityGuid, Timestamp to be filled in by IIS + + HTTP_TRACE_EVENT_ITEM Items[ 2 ]; + Items[ 0 ].pszName = L"ContextId"; + Items[ 0 ].dwDataType = HTTP_TRACE_TYPE_LPCGUID; // mof type (object) + Items[ 0 ].pbData = (PBYTE) pContextId; + Items[ 0 ].cbData = 16; + Items[ 0 ].pszDataDescription = NULL; + Items[ 1 ].pszName = L"AppDescription"; + Items[ 1 ].dwDataType = HTTP_TRACE_TYPE_LPCWSTR; // mof type (string) + Items[ 1 ].pbData = (PBYTE) pAppDescription; + Items[ 1 ].cbData = + ( Items[ 1 ].pbData == NULL )? 0 : ( sizeof(WCHAR) * (1 + (DWORD) wcslen( (PWSTR) Items[ 1 ].pbData ) ) ); + Items[ 1 ].pszDataDescription = NULL; + Event.pEventItems = Items; + pHttpTraceContext->RaiseTraceEvent( &Event ); + return S_OK; + }; + + static + BOOL + IsEnabled( + IHttpTraceContext * pHttpTraceContext ) + // Check if tracing for this event is enabled + { + return WWWServerTraceProvider::CheckTracingEnabled( + pHttpTraceContext, + WWWServerTraceProvider::ANCM, + 4 ); //Verbosity + }; + }; + // + // Event: mof class name ANCMAppStartFail, + // Description: Start application failed + // EventTypeName: ANCM_START_APPLICATION_FAIL + // EventType: 2 + // EventLevel: 2 + // + + class ANCM_START_APPLICATION_FAIL + { + public: + static + HRESULT + RaiseEvent( + IHttpTraceContext * pHttpTraceContext, + LPCGUID pContextId, + LPCWSTR pFailureDescription + ) + // + // Raise ANCM_START_APPLICATION_FAIL Event + // + { + HTTP_TRACE_EVENT Event; + Event.pProviderGuid = WWWServerTraceProvider::GetProviderGuid(); + Event.dwArea = WWWServerTraceProvider::ANCM; + Event.pAreaGuid = ANCMEvents::GetAreaGuid(); + Event.dwEvent = 2; + Event.pszEventName = L"ANCM_START_APPLICATION_FAIL"; + Event.dwEventVersion = 1; + Event.dwVerbosity = 2; + Event.cEventItems = 2; + Event.pActivityGuid = NULL; + Event.pRelatedActivityGuid = NULL; + Event.dwTimeStamp = 0; + Event.dwFlags = HTTP_TRACE_EVENT_FLAG_STATIC_DESCRIPTIVE_FIELDS; + + // pActivityGuid, pRelatedActivityGuid, Timestamp to be filled in by IIS + + HTTP_TRACE_EVENT_ITEM Items[ 2 ]; + Items[ 0 ].pszName = L"ContextId"; + Items[ 0 ].dwDataType = HTTP_TRACE_TYPE_LPCGUID; // mof type (object) + Items[ 0 ].pbData = (PBYTE) pContextId; + Items[ 0 ].cbData = 16; + Items[ 0 ].pszDataDescription = NULL; + Items[ 1 ].pszName = L"FailureDescription"; + Items[ 1 ].dwDataType = HTTP_TRACE_TYPE_LPCWSTR; // mof type (string) + Items[ 1 ].pbData = (PBYTE) pFailureDescription; + Items[ 1 ].cbData = + ( Items[ 1 ].pbData == NULL )? 0 : ( sizeof(WCHAR) * (1 + (DWORD) wcslen( (PWSTR) Items[ 1 ].pbData ) ) ); + Items[ 1 ].pszDataDescription = NULL; + Event.pEventItems = Items; + pHttpTraceContext->RaiseTraceEvent( &Event ); + return S_OK; + }; + + static + BOOL + IsEnabled( + IHttpTraceContext * pHttpTraceContext ) + // Check if tracing for this event is enabled + { + return WWWServerTraceProvider::CheckTracingEnabled( + pHttpTraceContext, + WWWServerTraceProvider::ANCM, + 2 ); //Verbosity + }; + }; + // + // Event: mof class name ANCMForwardStart, + // Description: Start forwarding request + // EventTypeName: ANCM_REQUEST_FORWARD_START + // EventType: 3 + // EventLevel: 4 + // + + class ANCM_REQUEST_FORWARD_START + { + public: + static + HRESULT + RaiseEvent( + IHttpTraceContext * pHttpTraceContext, + LPCGUID pContextId + ) + // + // Raise ANCM_REQUEST_FORWARD_START Event + // + { + HTTP_TRACE_EVENT Event; + Event.pProviderGuid = WWWServerTraceProvider::GetProviderGuid(); + Event.dwArea = WWWServerTraceProvider::ANCM; + Event.pAreaGuid = ANCMEvents::GetAreaGuid(); + Event.dwEvent = 3; + Event.pszEventName = L"ANCM_REQUEST_FORWARD_START"; + Event.dwEventVersion = 1; + Event.dwVerbosity = 4; + Event.cEventItems = 1; + Event.pActivityGuid = NULL; + Event.pRelatedActivityGuid = NULL; + Event.dwTimeStamp = 0; + Event.dwFlags = HTTP_TRACE_EVENT_FLAG_STATIC_DESCRIPTIVE_FIELDS; + + // pActivityGuid, pRelatedActivityGuid, Timestamp to be filled in by IIS + + HTTP_TRACE_EVENT_ITEM Items[ 1 ]; + Items[ 0 ].pszName = L"ContextId"; + Items[ 0 ].dwDataType = HTTP_TRACE_TYPE_LPCGUID; // mof type (object) + Items[ 0 ].pbData = (PBYTE) pContextId; + Items[ 0 ].cbData = 16; + Items[ 0 ].pszDataDescription = NULL; + Event.pEventItems = Items; + pHttpTraceContext->RaiseTraceEvent( &Event ); + return S_OK; + }; + + static + BOOL + IsEnabled( + IHttpTraceContext * pHttpTraceContext ) + // Check if tracing for this event is enabled + { + return WWWServerTraceProvider::CheckTracingEnabled( + pHttpTraceContext, + WWWServerTraceProvider::ANCM, + 4 ); //Verbosity + }; + }; + // + // Event: mof class name ANCMForwardEnd, + // Description: Finish forwarding request + // EventTypeName: ANCM_REQUEST_FORWARD_END + // EventType: 4 + // EventLevel: 4 + // + + class ANCM_REQUEST_FORWARD_END + { + public: + static + HRESULT + RaiseEvent( + IHttpTraceContext * pHttpTraceContext, + LPCGUID pContextId + ) + // + // Raise ANCM_REQUEST_FORWARD_END Event + // + { + HTTP_TRACE_EVENT Event; + Event.pProviderGuid = WWWServerTraceProvider::GetProviderGuid(); + Event.dwArea = WWWServerTraceProvider::ANCM; + Event.pAreaGuid = ANCMEvents::GetAreaGuid(); + Event.dwEvent = 4; + Event.pszEventName = L"ANCM_REQUEST_FORWARD_END"; + Event.dwEventVersion = 1; + Event.dwVerbosity = 4; + Event.cEventItems = 1; + Event.pActivityGuid = NULL; + Event.pRelatedActivityGuid = NULL; + Event.dwTimeStamp = 0; + Event.dwFlags = HTTP_TRACE_EVENT_FLAG_STATIC_DESCRIPTIVE_FIELDS; + + // pActivityGuid, pRelatedActivityGuid, Timestamp to be filled in by IIS + + HTTP_TRACE_EVENT_ITEM Items[ 1 ]; + Items[ 0 ].pszName = L"ContextId"; + Items[ 0 ].dwDataType = HTTP_TRACE_TYPE_LPCGUID; // mof type (object) + Items[ 0 ].pbData = (PBYTE) pContextId; + Items[ 0 ].cbData = 16; + Items[ 0 ].pszDataDescription = NULL; + Event.pEventItems = Items; + pHttpTraceContext->RaiseTraceEvent( &Event ); + return S_OK; + }; + + static + BOOL + IsEnabled( + IHttpTraceContext * pHttpTraceContext ) + // Check if tracing for this event is enabled + { + return WWWServerTraceProvider::CheckTracingEnabled( + pHttpTraceContext, + WWWServerTraceProvider::ANCM, + 4 ); //Verbosity + }; + }; + // + // Event: mof class name ANCMForwardFail, + // Description: Forwarding request failure + // EventTypeName: ANCM_REQUEST_FORWARD_FAIL + // EventType: 5 + // EventLevel: 2 + // + + class ANCM_REQUEST_FORWARD_FAIL + { + public: + static + HRESULT + RaiseEvent( + IHttpTraceContext * pHttpTraceContext, + LPCGUID pContextId, + ULONG ErrorCode + ) + // + // Raise ANCM_REQUEST_FORWARD_FAIL Event + // + { + HTTP_TRACE_EVENT Event; + Event.pProviderGuid = WWWServerTraceProvider::GetProviderGuid(); + Event.dwArea = WWWServerTraceProvider::ANCM; + Event.pAreaGuid = ANCMEvents::GetAreaGuid(); + Event.dwEvent = 5; + Event.pszEventName = L"ANCM_REQUEST_FORWARD_FAIL"; + Event.dwEventVersion = 1; + Event.dwVerbosity = 2; + Event.cEventItems = 2; + Event.pActivityGuid = NULL; + Event.pRelatedActivityGuid = NULL; + Event.dwTimeStamp = 0; + Event.dwFlags = HTTP_TRACE_EVENT_FLAG_STATIC_DESCRIPTIVE_FIELDS; + + // pActivityGuid, pRelatedActivityGuid, Timestamp to be filled in by IIS + + HTTP_TRACE_EVENT_ITEM Items[ 2 ]; + Items[ 0 ].pszName = L"ContextId"; + Items[ 0 ].dwDataType = HTTP_TRACE_TYPE_LPCGUID; // mof type (object) + Items[ 0 ].pbData = (PBYTE) pContextId; + Items[ 0 ].cbData = 16; + Items[ 0 ].pszDataDescription = NULL; + Items[ 1 ].pszName = L"ErrorCode"; + Items[ 1 ].dwDataType = HTTP_TRACE_TYPE_ULONG; // mof type (uint32) + Items[ 1 ].pbData = (PBYTE) &ErrorCode; + Items[ 1 ].cbData = 4; + Items[ 1 ].pszDataDescription = NULL; + Event.pEventItems = Items; + pHttpTraceContext->RaiseTraceEvent( &Event ); + return S_OK; + }; + + static + BOOL + IsEnabled( + IHttpTraceContext * pHttpTraceContext ) + // Check if tracing for this event is enabled + { + return WWWServerTraceProvider::CheckTracingEnabled( + pHttpTraceContext, + WWWServerTraceProvider::ANCM, + 2 ); //Verbosity + }; + }; + // + // Event: mof class name ANCMWinHttpCallBack, + // Description: Receiving callback from WinHttp + // EventTypeName: ANCM_WINHTTP_CALLBACK + // EventType: 6 + // EventLevel: 4 + // + + class ANCM_WINHTTP_CALLBACK + { + public: + static + HRESULT + RaiseEvent( + IHttpTraceContext * pHttpTraceContext, + LPCGUID pContextId, + ULONG InternetStatus + ) + // + // Raise ANCM_WINHTTP_CALLBACK Event + // + { + HTTP_TRACE_EVENT Event; + Event.pProviderGuid = WWWServerTraceProvider::GetProviderGuid(); + Event.dwArea = WWWServerTraceProvider::ANCM; + Event.pAreaGuid = ANCMEvents::GetAreaGuid(); + Event.dwEvent = 6; + Event.pszEventName = L"ANCM_WINHTTP_CALLBACK"; + Event.dwEventVersion = 1; + Event.dwVerbosity = 4; + Event.cEventItems = 2; + Event.pActivityGuid = NULL; + Event.pRelatedActivityGuid = NULL; + Event.dwTimeStamp = 0; + Event.dwFlags = HTTP_TRACE_EVENT_FLAG_STATIC_DESCRIPTIVE_FIELDS; + + // pActivityGuid, pRelatedActivityGuid, Timestamp to be filled in by IIS + + HTTP_TRACE_EVENT_ITEM Items[ 2 ]; + Items[ 0 ].pszName = L"ContextId"; + Items[ 0 ].dwDataType = HTTP_TRACE_TYPE_LPCGUID; // mof type (object) + Items[ 0 ].pbData = (PBYTE) pContextId; + Items[ 0 ].cbData = 16; + Items[ 0 ].pszDataDescription = NULL; + Items[ 1 ].pszName = L"InternetStatus"; + Items[ 1 ].dwDataType = HTTP_TRACE_TYPE_ULONG; // mof type (uint32) + Items[ 1 ].pbData = (PBYTE) &InternetStatus; + Items[ 1 ].cbData = 4; + Items[ 1 ].pszDataDescription = NULL; + Event.pEventItems = Items; + pHttpTraceContext->RaiseTraceEvent( &Event ); + return S_OK; + }; + + static + BOOL + IsEnabled( + IHttpTraceContext * pHttpTraceContext ) + // Check if tracing for this event is enabled + { + return WWWServerTraceProvider::CheckTracingEnabled( + pHttpTraceContext, + WWWServerTraceProvider::ANCM, + 4 ); //Verbosity + }; + }; + // + // Event: mof class name ANCMExecuteStart, + // Description: Starting inprocess execute request + // EventTypeName: ANCM_INPROC_EXECUTE_REQUEST_START + // EventType: 7 + // EventLevel: 4 + // + + class ANCM_INPROC_EXECUTE_REQUEST_START + { + public: + static + HRESULT + RaiseEvent( + IHttpTraceContext * pHttpTraceContext, + LPCGUID pContextId + ) + // + // Raise ANCM_INPROC_EXECUTE_REQUEST_START Event + // + { + HTTP_TRACE_EVENT Event; + Event.pProviderGuid = WWWServerTraceProvider::GetProviderGuid(); + Event.dwArea = WWWServerTraceProvider::ANCM; + Event.pAreaGuid = ANCMEvents::GetAreaGuid(); + Event.dwEvent = 7; + Event.pszEventName = L"ANCM_INPROC_EXECUTE_REQUEST_START"; + Event.dwEventVersion = 1; + Event.dwVerbosity = 4; + Event.cEventItems = 1; + Event.pActivityGuid = NULL; + Event.pRelatedActivityGuid = NULL; + Event.dwTimeStamp = 0; + Event.dwFlags = HTTP_TRACE_EVENT_FLAG_STATIC_DESCRIPTIVE_FIELDS; + + // pActivityGuid, pRelatedActivityGuid, Timestamp to be filled in by IIS + + HTTP_TRACE_EVENT_ITEM Items[ 1 ]; + Items[ 0 ].pszName = L"ContextId"; + Items[ 0 ].dwDataType = HTTP_TRACE_TYPE_LPCGUID; // mof type (object) + Items[ 0 ].pbData = (PBYTE) pContextId; + Items[ 0 ].cbData = 16; + Items[ 0 ].pszDataDescription = NULL; + Event.pEventItems = Items; + pHttpTraceContext->RaiseTraceEvent( &Event ); + return S_OK; + }; + + static + BOOL + IsEnabled( + IHttpTraceContext * pHttpTraceContext ) + // Check if tracing for this event is enabled + { + return WWWServerTraceProvider::CheckTracingEnabled( + pHttpTraceContext, + WWWServerTraceProvider::ANCM, + 4 ); //Verbosity + }; + }; + // + // Event: mof class name ANCMExecuteEnd, + // Description: Ending inprocess execute request + // EventTypeName: ANCM_INPROC_EXECUTE_REQUEST_COMPLETION + // EventType: 8 + // EventLevel: 5 + // + + class ANCM_INPROC_EXECUTE_REQUEST_COMPLETION + { + public: + static + HRESULT + RaiseEvent( + IHttpTraceContext * pHttpTraceContext, + LPCGUID pContextId, + ULONG requestStatus + ) + // + // Raise ANCM_INPROC_EXECUTE_REQUEST_COMPLETION Event + // + { + HTTP_TRACE_EVENT Event; + Event.pProviderGuid = WWWServerTraceProvider::GetProviderGuid(); + Event.dwArea = WWWServerTraceProvider::ANCM; + Event.pAreaGuid = ANCMEvents::GetAreaGuid(); + Event.dwEvent = 8; + Event.pszEventName = L"ANCM_INPROC_EXECUTE_REQUEST_COMPLETION"; + Event.dwEventVersion = 1; + Event.dwVerbosity = 5; + Event.cEventItems = 2; + Event.pActivityGuid = NULL; + Event.pRelatedActivityGuid = NULL; + Event.dwTimeStamp = 0; + Event.dwFlags = HTTP_TRACE_EVENT_FLAG_STATIC_DESCRIPTIVE_FIELDS; + + // pActivityGuid, pRelatedActivityGuid, Timestamp to be filled in by IIS + + HTTP_TRACE_EVENT_ITEM Items[ 2 ]; + Items[ 0 ].pszName = L"ContextId"; + Items[ 0 ].dwDataType = HTTP_TRACE_TYPE_LPCGUID; // mof type (object) + Items[ 0 ].pbData = (PBYTE) pContextId; + Items[ 0 ].cbData = 16; + Items[ 0 ].pszDataDescription = NULL; + Items[ 1 ].pszName = L"requestStatus"; + Items[ 1 ].dwDataType = HTTP_TRACE_TYPE_ULONG; // mof type (uint32) + Items[ 1 ].pbData = (PBYTE) &requestStatus; + Items[ 1 ].cbData = 4; + Items[ 1 ].pszDataDescription = NULL; + Event.pEventItems = Items; + pHttpTraceContext->RaiseTraceEvent( &Event ); + return S_OK; + }; + + static + BOOL + IsEnabled( + IHttpTraceContext * pHttpTraceContext ) + // Check if tracing for this event is enabled + { + return WWWServerTraceProvider::CheckTracingEnabled( + pHttpTraceContext, + WWWServerTraceProvider::ANCM, + 5 ); //Verbosity + }; + }; + // + // Event: mof class name ANCMAsyncStart, + // Description: Starting inprocess async completion + // EventTypeName: ANCM_INPROC_ASYNC_COMPLETION_START + // EventType: 9 + // EventLevel: 5 + // + + class ANCM_INPROC_ASYNC_COMPLETION_START + { + public: + static + HRESULT + RaiseEvent( + IHttpTraceContext * pHttpTraceContext, + LPCGUID pContextId + ) + // + // Raise ANCM_INPROC_ASYNC_COMPLETION_START Event + // + { + HTTP_TRACE_EVENT Event; + Event.pProviderGuid = WWWServerTraceProvider::GetProviderGuid(); + Event.dwArea = WWWServerTraceProvider::ANCM; + Event.pAreaGuid = ANCMEvents::GetAreaGuid(); + Event.dwEvent = 9; + Event.pszEventName = L"ANCM_INPROC_ASYNC_COMPLETION_START"; + Event.dwEventVersion = 1; + Event.dwVerbosity = 5; + Event.cEventItems = 1; + Event.pActivityGuid = NULL; + Event.pRelatedActivityGuid = NULL; + Event.dwTimeStamp = 0; + Event.dwFlags = HTTP_TRACE_EVENT_FLAG_STATIC_DESCRIPTIVE_FIELDS; + + // pActivityGuid, pRelatedActivityGuid, Timestamp to be filled in by IIS + + HTTP_TRACE_EVENT_ITEM Items[ 1 ]; + Items[ 0 ].pszName = L"ContextId"; + Items[ 0 ].dwDataType = HTTP_TRACE_TYPE_LPCGUID; // mof type (object) + Items[ 0 ].pbData = (PBYTE) pContextId; + Items[ 0 ].cbData = 16; + Items[ 0 ].pszDataDescription = NULL; + Event.pEventItems = Items; + pHttpTraceContext->RaiseTraceEvent( &Event ); + return S_OK; + }; + + static + BOOL + IsEnabled( + IHttpTraceContext * pHttpTraceContext ) + // Check if tracing for this event is enabled + { + return WWWServerTraceProvider::CheckTracingEnabled( + pHttpTraceContext, + WWWServerTraceProvider::ANCM, + 5 ); //Verbosity + }; + }; + // + // Event: mof class name ANCMAsyncEnd, + // Description: Ending inprocess async completion + // EventTypeName: ANCM_INPROC_ASYNC_COMPLETION_COMPLETION + // EventType: 10 + // EventLevel: 5 + // + + class ANCM_INPROC_ASYNC_COMPLETION_COMPLETION + { + public: + static + HRESULT + RaiseEvent( + IHttpTraceContext * pHttpTraceContext, + LPCGUID pContextId, + ULONG requestStatus + ) + // + // Raise ANCM_INPROC_ASYNC_COMPLETION_COMPLETION Event + // + { + HTTP_TRACE_EVENT Event; + Event.pProviderGuid = WWWServerTraceProvider::GetProviderGuid(); + Event.dwArea = WWWServerTraceProvider::ANCM; + Event.pAreaGuid = ANCMEvents::GetAreaGuid(); + Event.dwEvent = 10; + Event.pszEventName = L"ANCM_INPROC_ASYNC_COMPLETION_COMPLETION"; + Event.dwEventVersion = 1; + Event.dwVerbosity = 5; + Event.cEventItems = 2; + Event.pActivityGuid = NULL; + Event.pRelatedActivityGuid = NULL; + Event.dwTimeStamp = 0; + Event.dwFlags = HTTP_TRACE_EVENT_FLAG_STATIC_DESCRIPTIVE_FIELDS; + + // pActivityGuid, pRelatedActivityGuid, Timestamp to be filled in by IIS + + HTTP_TRACE_EVENT_ITEM Items[ 2 ]; + Items[ 0 ].pszName = L"ContextId"; + Items[ 0 ].dwDataType = HTTP_TRACE_TYPE_LPCGUID; // mof type (object) + Items[ 0 ].pbData = (PBYTE) pContextId; + Items[ 0 ].cbData = 16; + Items[ 0 ].pszDataDescription = NULL; + Items[ 1 ].pszName = L"requestStatus"; + Items[ 1 ].dwDataType = HTTP_TRACE_TYPE_ULONG; // mof type (uint32) + Items[ 1 ].pbData = (PBYTE) &requestStatus; + Items[ 1 ].cbData = 4; + Items[ 1 ].pszDataDescription = NULL; + Event.pEventItems = Items; + pHttpTraceContext->RaiseTraceEvent( &Event ); + return S_OK; + }; + + static + BOOL + IsEnabled( + IHttpTraceContext * pHttpTraceContext ) + // Check if tracing for this event is enabled + { + return WWWServerTraceProvider::CheckTracingEnabled( + pHttpTraceContext, + WWWServerTraceProvider::ANCM, + 5 ); //Verbosity + }; + }; + // + // Event: mof class name ANCMRequestShutdown, + // Description: Inprocess app shutdown + // EventTypeName: ANCM_INPROC_REQUEST_SHUTDOWN + // EventType: 11 + // EventLevel: 4 + // + + class ANCM_INPROC_REQUEST_SHUTDOWN + { + public: + static + HRESULT + RaiseEvent( + IHttpTraceContext * pHttpTraceContext, + LPCGUID pContextId + ) + // + // Raise ANCM_INPROC_REQUEST_SHUTDOWN Event + // + { + HTTP_TRACE_EVENT Event; + Event.pProviderGuid = WWWServerTraceProvider::GetProviderGuid(); + Event.dwArea = WWWServerTraceProvider::ANCM; + Event.pAreaGuid = ANCMEvents::GetAreaGuid(); + Event.dwEvent = 11; + Event.pszEventName = L"ANCM_INPROC_REQUEST_SHUTDOWN"; + Event.dwEventVersion = 1; + Event.dwVerbosity = 4; + Event.cEventItems = 1; + Event.pActivityGuid = NULL; + Event.pRelatedActivityGuid = NULL; + Event.dwTimeStamp = 0; + Event.dwFlags = HTTP_TRACE_EVENT_FLAG_STATIC_DESCRIPTIVE_FIELDS; + + // pActivityGuid, pRelatedActivityGuid, Timestamp to be filled in by IIS + + HTTP_TRACE_EVENT_ITEM Items[ 1 ]; + Items[ 0 ].pszName = L"ContextId"; + Items[ 0 ].dwDataType = HTTP_TRACE_TYPE_LPCGUID; // mof type (object) + Items[ 0 ].pbData = (PBYTE) pContextId; + Items[ 0 ].cbData = 16; + Items[ 0 ].pszDataDescription = NULL; + Event.pEventItems = Items; + pHttpTraceContext->RaiseTraceEvent( &Event ); + return S_OK; + }; + + static + BOOL + IsEnabled( + IHttpTraceContext * pHttpTraceContext ) + // Check if tracing for this event is enabled + { + return WWWServerTraceProvider::CheckTracingEnabled( + pHttpTraceContext, + WWWServerTraceProvider::ANCM, + 4 ); //Verbosity + }; + }; + // + // Event: mof class name ANCMRequestDisconnect, + // Description: Inprocess request disconnect + // EventTypeName: ANCM_INPROC_REQUEST_DISCONNECT + // EventType: 12 + // EventLevel: 4 + // + + class ANCM_INPROC_REQUEST_DISCONNECT + { + public: + static + HRESULT + RaiseEvent( + IHttpTraceContext * pHttpTraceContext, + LPCGUID pContextId + ) + // + // Raise ANCM_INPROC_REQUEST_DISCONNECT Event + // + { + HTTP_TRACE_EVENT Event; + Event.pProviderGuid = WWWServerTraceProvider::GetProviderGuid(); + Event.dwArea = WWWServerTraceProvider::ANCM; + Event.pAreaGuid = ANCMEvents::GetAreaGuid(); + Event.dwEvent = 12; + Event.pszEventName = L"ANCM_INPROC_REQUEST_DISCONNECT"; + Event.dwEventVersion = 1; + Event.dwVerbosity = 4; + Event.cEventItems = 1; + Event.pActivityGuid = NULL; + Event.pRelatedActivityGuid = NULL; + Event.dwTimeStamp = 0; + Event.dwFlags = HTTP_TRACE_EVENT_FLAG_STATIC_DESCRIPTIVE_FIELDS; + + // pActivityGuid, pRelatedActivityGuid, Timestamp to be filled in by IIS + + HTTP_TRACE_EVENT_ITEM Items[ 1 ]; + Items[ 0 ].pszName = L"ContextId"; + Items[ 0 ].dwDataType = HTTP_TRACE_TYPE_LPCGUID; // mof type (object) + Items[ 0 ].pbData = (PBYTE) pContextId; + Items[ 0 ].cbData = 16; + Items[ 0 ].pszDataDescription = NULL; + Event.pEventItems = Items; + pHttpTraceContext->RaiseTraceEvent( &Event ); + return S_OK; + }; + + static + BOOL + IsEnabled( + IHttpTraceContext * pHttpTraceContext ) + // Check if tracing for this event is enabled + { + return WWWServerTraceProvider::CheckTracingEnabled( + pHttpTraceContext, + WWWServerTraceProvider::ANCM, + 4 ); //Verbosity + }; + }; + // + // Event: mof class name ANCMManagedRequestCompletion, + // Description: Indicate managed request complete + // EventTypeName: ANCM_INPROC_MANAGED_REQUEST_COMPLETION + // EventType: 13 + // EventLevel: 4 + // + + class ANCM_INPROC_MANAGED_REQUEST_COMPLETION + { + public: + static + HRESULT + RaiseEvent( + IHttpTraceContext * pHttpTraceContext, + LPCGUID pContextId + ) + // + // Raise ANCM_INPROC_MANAGED_REQUEST_COMPLETION Event + // + { + HTTP_TRACE_EVENT Event; + Event.pProviderGuid = WWWServerTraceProvider::GetProviderGuid(); + Event.dwArea = WWWServerTraceProvider::ANCM; + Event.pAreaGuid = ANCMEvents::GetAreaGuid(); + Event.dwEvent = 13; + Event.pszEventName = L"ANCM_INPROC_MANAGED_REQUEST_COMPLETION"; + Event.dwEventVersion = 1; + Event.dwVerbosity = 4; + Event.cEventItems = 1; + Event.pActivityGuid = NULL; + Event.pRelatedActivityGuid = NULL; + Event.dwTimeStamp = 0; + Event.dwFlags = HTTP_TRACE_EVENT_FLAG_STATIC_DESCRIPTIVE_FIELDS; + + // pActivityGuid, pRelatedActivityGuid, Timestamp to be filled in by IIS + + HTTP_TRACE_EVENT_ITEM Items[ 1 ]; + Items[ 0 ].pszName = L"ContextId"; + Items[ 0 ].dwDataType = HTTP_TRACE_TYPE_LPCGUID; // mof type (object) + Items[ 0 ].pbData = (PBYTE) pContextId; + Items[ 0 ].cbData = 16; + Items[ 0 ].pszDataDescription = NULL; + Event.pEventItems = Items; + pHttpTraceContext->RaiseTraceEvent( &Event ); + return S_OK; + }; + + static + BOOL + IsEnabled( + IHttpTraceContext * pHttpTraceContext ) + // Check if tracing for this event is enabled + { + return WWWServerTraceProvider::CheckTracingEnabled( + pHttpTraceContext, + WWWServerTraceProvider::ANCM, + 4 ); //Verbosity + }; + }; + // + // Event: mof class name ANCMHRESULTFailed, + // Description: Failed HRESULT + // EventTypeName: ANCM_HRESULT_FAILED + // EventType: 14 + // EventLevel: 3 + // + + class ANCM_HRESULT_FAILED + { + public: + static + HRESULT + RaiseEvent( + IHttpTraceContext * pHttpTraceContext, + LPCGUID pContextId, + LPCSTR pFile, + ULONG Line, + ULONG HResult + ) + // + // Raise ANCM_HRESULT_FAILED Event + // + { + HTTP_TRACE_EVENT Event; + Event.pProviderGuid = WWWServerTraceProvider::GetProviderGuid(); + Event.dwArea = WWWServerTraceProvider::ANCM; + Event.pAreaGuid = ANCMEvents::GetAreaGuid(); + Event.dwEvent = 14; + Event.pszEventName = L"ANCM_HRESULT_FAILED"; + Event.dwEventVersion = 1; + Event.dwVerbosity = 3; + Event.cEventItems = 4; + Event.pActivityGuid = NULL; + Event.pRelatedActivityGuid = NULL; + Event.dwTimeStamp = 0; + Event.dwFlags = HTTP_TRACE_EVENT_FLAG_STATIC_DESCRIPTIVE_FIELDS; + + // pActivityGuid, pRelatedActivityGuid, Timestamp to be filled in by IIS + + HTTP_TRACE_EVENT_ITEM Items[ 4 ]; + Items[ 0 ].pszName = L"ContextId"; + Items[ 0 ].dwDataType = HTTP_TRACE_TYPE_LPCGUID; // mof type (object) + Items[ 0 ].pbData = (PBYTE) pContextId; + Items[ 0 ].cbData = 16; + Items[ 0 ].pszDataDescription = NULL; + Items[ 1 ].pszName = L"File"; + Items[ 1 ].dwDataType = HTTP_TRACE_TYPE_LPCSTR; // mof type (string) + Items[ 1 ].pbData = (PBYTE) pFile; + Items[ 1 ].cbData = + ( Items[ 1 ].pbData == NULL )? 0 : (1 + (DWORD) strlen( (PSTR) Items[ 1 ].pbData ) ); + Items[ 1 ].pszDataDescription = NULL; + Items[ 2 ].pszName = L"Line"; + Items[ 2 ].dwDataType = HTTP_TRACE_TYPE_ULONG; // mof type (uint32) + Items[ 2 ].pbData = (PBYTE) &Line; + Items[ 2 ].cbData = 4; + Items[ 2 ].pszDataDescription = NULL; + Items[ 3 ].pszName = L"HResult"; + Items[ 3 ].dwDataType = HTTP_TRACE_TYPE_ULONG; // mof type (uint32) + Items[ 3 ].pbData = (PBYTE) &HResult; + Items[ 3 ].cbData = 4; + Items[ 3 ].pszDataDescription = NULL; + Event.pEventItems = Items; + pHttpTraceContext->RaiseTraceEvent( &Event ); + return S_OK; + }; + + static + BOOL + IsEnabled( + IHttpTraceContext * pHttpTraceContext ) + // Check if tracing for this event is enabled + { + return WWWServerTraceProvider::CheckTracingEnabled( + pHttpTraceContext, + WWWServerTraceProvider::ANCM, + 3 ); //Verbosity + }; + }; + // + // Event: mof class name ANCMExceptionCaughFailed, + // Description: Caught exception + // EventTypeName: ANCM_EXCEPTION_CAUGHT + // EventType: 15 + // EventLevel: 3 + // + + class ANCM_EXCEPTION_CAUGHT + { + public: + static + HRESULT + RaiseEvent( + IHttpTraceContext * pHttpTraceContext, + LPCGUID pContextId, + LPCSTR pFile, + ULONG Line, + LPCSTR pDescription + ) + // + // Raise ANCM_EXCEPTION_CAUGHT Event + // + { + HTTP_TRACE_EVENT Event; + Event.pProviderGuid = WWWServerTraceProvider::GetProviderGuid(); + Event.dwArea = WWWServerTraceProvider::ANCM; + Event.pAreaGuid = ANCMEvents::GetAreaGuid(); + Event.dwEvent = 15; + Event.pszEventName = L"ANCM_EXCEPTION_CAUGHT"; + Event.dwEventVersion = 1; + Event.dwVerbosity = 3; + Event.cEventItems = 4; + Event.pActivityGuid = NULL; + Event.pRelatedActivityGuid = NULL; + Event.dwTimeStamp = 0; + Event.dwFlags = HTTP_TRACE_EVENT_FLAG_STATIC_DESCRIPTIVE_FIELDS; + + // pActivityGuid, pRelatedActivityGuid, Timestamp to be filled in by IIS + + HTTP_TRACE_EVENT_ITEM Items[ 4 ]; + Items[ 0 ].pszName = L"ContextId"; + Items[ 0 ].dwDataType = HTTP_TRACE_TYPE_LPCGUID; // mof type (object) + Items[ 0 ].pbData = (PBYTE) pContextId; + Items[ 0 ].cbData = 16; + Items[ 0 ].pszDataDescription = NULL; + Items[ 1 ].pszName = L"File"; + Items[ 1 ].dwDataType = HTTP_TRACE_TYPE_LPCSTR; // mof type (string) + Items[ 1 ].pbData = (PBYTE) pFile; + Items[ 1 ].cbData = + ( Items[ 1 ].pbData == NULL )? 0 : (1 + (DWORD) strlen( (PSTR) Items[ 1 ].pbData ) ); + Items[ 1 ].pszDataDescription = NULL; + Items[ 2 ].pszName = L"Line"; + Items[ 2 ].dwDataType = HTTP_TRACE_TYPE_ULONG; // mof type (uint32) + Items[ 2 ].pbData = (PBYTE) &Line; + Items[ 2 ].cbData = 4; + Items[ 2 ].pszDataDescription = NULL; + Items[ 3 ].pszName = L"Description"; + Items[ 3 ].dwDataType = HTTP_TRACE_TYPE_LPCSTR; // mof type (string) + Items[ 3 ].pbData = (PBYTE) pDescription; + Items[ 3 ].cbData = + ( Items[ 3 ].pbData == NULL )? 0 : (1 + (DWORD) strlen( (PSTR) Items[ 3 ].pbData ) ); + Items[ 3 ].pszDataDescription = NULL; + Event.pEventItems = Items; + pHttpTraceContext->RaiseTraceEvent( &Event ); + return S_OK; + }; + + static + BOOL + IsEnabled( + IHttpTraceContext * pHttpTraceContext ) + // Check if tracing for this event is enabled + { + return WWWServerTraceProvider::CheckTracingEnabled( + pHttpTraceContext, + WWWServerTraceProvider::ANCM, + 3 ); //Verbosity + }; + }; +}; +#endif \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/aspnetcore_msg.mc b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/aspnetcore_msg.mc new file mode 100644 index 0000000000..547192d0df --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/aspnetcore_msg.mc @@ -0,0 +1,213 @@ +;/*++ +; +; Copyright (c) .NET Foundation. All rights reserved. +; Licensed under the MIT License. See License.txt in the project root for license information. +; +;Module Name: +; +; aspnetcore_msg.mc +; +;Abstract: +; +; Asp.Net Core Module localizable messages. +; +;--*/ +; +; +;#ifndef _ASPNETCORE_MSG_H_ +;#define _ASPNETCORE_MSG_H_ +; + +SeverityNames=(Success=0x0 + Informational=0x1 + Warning=0x2 + Error=0x3 + ) + +MessageIdTypedef=DWORD + +Messageid=1000 +SymbolicName=ASPNETCORE_EVENT_PROCESS_START_ERROR +Language=English +%1 +. + +Messageid=1001 +SymbolicName=ASPNETCORE_EVENT_PROCESS_START_SUCCESS +Language=English +%1 +. + +Messageid=1003 +SymbolicName=ASPNETCORE_EVENT_RAPID_FAIL_COUNT_EXCEEDED +Language=English +%1 +. + +Messageid=1004 +SymbolicName=ASPNETCORE_EVENT_CONFIG_ERROR +Language=English +%1 +. + +Messageid=1005 +SymbolicName=ASPNETCORE_EVENT_GRACEFUL_SHUTDOWN_FAILURE +Language=English +%1 +. + +Messageid=1006 +SymbolicName=ASPNETCORE_EVENT_SENT_SHUTDOWN_HTTP_REQUEST +Language=English +%1 +. + +Messageid=1007 +SymbolicName=ASPNETCORE_EVENT_LOAD_CLR_FALIURE +Language=English +%1 +. + +Messageid=1008 +SymbolicName=ASPNETCORE_EVENT_DUPLICATED_INPROCESS_APP +Language=English +%1 +. + +Messageid=1009 +SymbolicName=ASPNETCORE_EVENT_MIXED_HOSTING_MODEL_ERROR +Language=English +%1 +. + +Messageid=1010 +SymbolicName=ASPNETCORE_EVENT_ADD_APPLICATION_ERROR +Language=English +%1 +. + +Messageid=1011 +SymbolicName=ASPNETCORE_EVENT_INPROCESS_THREAD_EXIT +Language=English +%1 +. + +Messageid=1012 +SymbolicName=ASPNETCORE_EVENT_RECYCLE_APPOFFLINE +Language=English +%1 +. + +Messageid=1013 +SymbolicName=ASPNETCORE_EVENT_MODULE_DISABLED +Language=English +%1 +. + +Messageid=1018 +SymbolicName=ASPNETCORE_EVENT_INPROCESS_THREAD_EXCEPTION +Language=English +%1 +. + +Messageid=1020 +SymbolicName=ASPNETCORE_EVENT_PROCESS_START_FAILURE +Language=English +%1 +. + +Messageid=1021 +SymbolicName=ASPNETCORE_EVENT_RECYCLE_CONFIGURATION +Language=English +%1 +. + +Messageid=1022 +SymbolicName=ASPNETCORE_EVENT_RECYCLE_APP_FAILURE +Language=English +%1 +. + +Messageid=1024 +SymbolicName=ASPNETCORE_EVENT_MONITOR_APPOFFLINE_ERROR +Language=English +%1 +. + +Messageid=1025 +SymbolicName=ASPNETCORE_EVENT_GENERAL_INFO +Language=English +%1 +. + +Messageid=1026 +SymbolicName=ASPNETCORE_EVENT_GENERAL_WARNING +Language=English +%1 +. + +Messageid=1027 +SymbolicName=ASPNETCORE_EVENT_GENERAL_ERROR +Language=English +%1 +. + +Messageid=1028 +SymbolicName=ASPNETCORE_EVENT_INPROCESS_RH_MISSING +Language=English +%1 +. + +Messageid=1029 +SymbolicName=ASPNETCORE_EVENT_OUT_OF_PROCESS_RH_MISSING +Language=English +%1 +. + +Messageid=1030 +SymbolicName=ASPNETCORE_EVENT_PROCESS_SHUTDOWN +Language=English +%1 +. + +Messageid=1031 +SymbolicName=ASPNETCORE_EVENT_INPROCESS_START_ERROR +Language=English +%1 +. + +Messageid=1032 +SymbolicName=ASPNETCORE_EVENT_INPROCESS_START_SUCCESS +Language=English +%1 +. + + +Messageid=1033 +SymbolicName=ASPNETCORE_EVENT_APP_SHUTDOWN_SUCCESSFUL +Language=English +%1 +. + +Messageid=1034 +SymbolicName=ASPNETCORE_CONFIGURATION_LOAD_ERROR +Language=English +%1 +. + +Messageid=1035 +SymbolicName=ASPNETCORE_EVENT_INPROCESS_THREAD_EXIT_STDOUT +Language=English +%1 +. + +Messageid=1036 +SymbolicName=ASPNETCORE_EVENT_DEBUG_LOG +Language=English +%1 +. + + +; +;#endif // _ASPNETCORE_MODULE_MSG_H_ +; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/config_utility.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/config_utility.h new file mode 100644 index 0000000000..ae4aeb0466 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/config_utility.h @@ -0,0 +1,87 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#pragma once + +#include +#include "ahutil.h" +#include "stringu.h" +#include "exceptions.h" +#include "atlbase.h" + +class ConfigUtility +{ + #define CS_ASPNETCORE_HANDLER_SETTINGS L"handlerSettings" + #define CS_ASPNETCORE_HANDLER_VERSION L"handlerVersion" + #define CS_ASPNETCORE_DEBUG_FILE L"debugFile" + #define CS_ASPNETCORE_DEBUG_LEVEL L"debugLevel" + #define CS_ASPNETCORE_HANDLER_SETTINGS_NAME L"name" + #define CS_ASPNETCORE_HANDLER_SETTINGS_VALUE L"value" + +public: + static + HRESULT + FindHandlerVersion(IAppHostElement* pElement, STRU& strHandlerVersionValue) + { + return FindKeyValuePair(pElement, CS_ASPNETCORE_HANDLER_VERSION, strHandlerVersionValue); + } + + static + HRESULT + FindDebugFile(IAppHostElement* pElement, STRU& strDebugFile) + { + return FindKeyValuePair(pElement, CS_ASPNETCORE_DEBUG_FILE, strDebugFile); + } + + static + HRESULT + FindDebugLevel(IAppHostElement* pElement, STRU& strDebugFile) + { + return FindKeyValuePair(pElement, CS_ASPNETCORE_DEBUG_LEVEL, strDebugFile); + } + +private: + static + HRESULT + FindKeyValuePair(IAppHostElement* pElement, PCWSTR key, STRU& strHandlerVersionValue) + { + HRESULT hr; + CComPtr pHandlerSettings = nullptr; + CComPtr pHandlerSettingsCollection = nullptr; + CComPtr pHandlerVar = nullptr; + ENUM_INDEX index{}; + STRU strHandlerName; + STRU strHandlerValue; + + // backwards complatibility with systems not having schema for handlerSettings + if (FAILED_LOG(GetElementChildByName(pElement, CS_ASPNETCORE_HANDLER_SETTINGS, &pHandlerSettings))) + { + return S_OK; + } + + RETURN_IF_FAILED(pHandlerSettings->get_Collection(&pHandlerSettingsCollection)); + + RETURN_IF_FAILED(hr = FindFirstElement(pHandlerSettingsCollection, &index, &pHandlerVar)); + + while (hr != S_FALSE) + { + RETURN_IF_FAILED(GetElementStringProperty(pHandlerVar, CS_ASPNETCORE_HANDLER_SETTINGS_NAME, &strHandlerName)); + RETURN_IF_FAILED(GetElementStringProperty(pHandlerVar, CS_ASPNETCORE_HANDLER_SETTINGS_VALUE, &strHandlerValue)); + + if (strHandlerName.Equals(key, TRUE)) + { + RETURN_IF_FAILED(strHandlerVersionValue.Copy(strHandlerValue)); + break; + } + + strHandlerName.Reset(); + strHandlerValue.Reset(); + pHandlerVar.Release(); + + RETURN_IF_FAILED(hr = FindNextElement(pHandlerSettingsCollection, &index, &pHandlerVar)); + } + + return S_OK; + } +}; + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/debugutil.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/debugutil.cpp new file mode 100644 index 0000000000..3492be7354 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/debugutil.cpp @@ -0,0 +1,437 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "debugutil.h" + +#include +#include +#include "dbgutil.h" +#include "stringu.h" +#include "stringa.h" +#include "Environment.h" +#include "SRWExclusiveLock.h" +#include "exceptions.h" +#include "atlbase.h" +#include "config_utility.h" +#include "StringHelpers.h" +#include "aspnetcore_msg.h" +#include "EventLog.h" + +inline HANDLE g_logFile = INVALID_HANDLE_VALUE; +inline HMODULE g_hModule; +inline SRWLOCK g_logFileLock; +inline HANDLE g_stdOutHandle = INVALID_HANDLE_VALUE; + +HRESULT +PrintDebugHeader() +{ + // Major, minor are stored in dwFileVersionMS field and patch, build in dwFileVersionLS field as pair of 32 bit numbers + LOG_INFOF(L"Initializing logs for '%ls'. %ls. %ls.", + GetModuleName().c_str(), + GetProcessIdString().c_str(), + GetVersionInfoString().c_str() + ); + + return S_OK; +} + +std::wstring +GetProcessIdString() +{ + return format(L"Process Id: %u.", GetCurrentProcessId()); +} + +std::wstring +GetVersionInfoString() +{ + auto func = [](std::wstring& res) + { + DWORD verHandle = 0; + UINT size = 0; + LPVOID lpBuffer = NULL; + + auto path = GetModuleName(); + + DWORD verSize = GetFileVersionInfoSize(path.c_str(), &verHandle); + RETURN_LAST_ERROR_IF(verSize == 0); + + // Allocate memory to hold data structure returned by GetFileVersionInfo + std::vector verData(verSize); + + RETURN_LAST_ERROR_IF(!GetFileVersionInfo(path.c_str(), verHandle, verSize, verData.data())); + RETURN_LAST_ERROR_IF(!VerQueryValue(verData.data(), L"\\", &lpBuffer, &size)); + + auto verInfo = reinterpret_cast(lpBuffer); + if (verInfo->dwSignature != VS_FFI_SIGNATURE) + { + RETURN_IF_FAILED(E_FAIL); + } + + LPVOID pvProductName = NULL; + unsigned int iProductNameLen = 0; + RETURN_LAST_ERROR_IF(!VerQueryValue(verData.data(), _T("\\StringFileInfo\\040904b0\\FileDescription"), &pvProductName, &iProductNameLen)); + + res = format(L"File Version: %d.%d.%d.%d. Description: %s", + (verInfo->dwFileVersionMS >> 16) & 0xffff, + (verInfo->dwFileVersionMS >> 0) & 0xffff, + (verInfo->dwFileVersionLS >> 16) & 0xffff, + (verInfo->dwFileVersionLS >> 0) & 0xffff, + pvProductName); + return S_OK; + }; + + std::wstring versionInfoString; + + return func(versionInfoString) == S_OK ? versionInfoString : L""; +} + +std::wstring +GetModuleName() +{ + WCHAR path[MAX_PATH]; + LOG_LAST_ERROR_IF(!GetModuleFileName(g_hModule, path, sizeof(path))); + return path; +} + +void SetDebugFlags(const std::wstring &debugValue) +{ + try + { + std::wstringstream stringStream(debugValue); + std::wstring flag; + + while (std::getline(stringStream, flag, L',')) + { + if (_wcsnicmp(flag.c_str(), L"error", wcslen(L"error")) == 0) DEBUG_FLAGS_VAR |= DEBUG_FLAGS_ERROR; + if (_wcsnicmp(flag.c_str(), L"warning", wcslen(L"warning")) == 0) DEBUG_FLAGS_VAR |= DEBUG_FLAGS_WARN; + if (_wcsnicmp(flag.c_str(), L"info", wcslen(L"info")) == 0) DEBUG_FLAGS_VAR |= DEBUG_FLAGS_INFO; + if (_wcsnicmp(flag.c_str(), L"trace", wcslen(L"trace")) == 0) DEBUG_FLAGS_VAR |= DEBUG_FLAGS_TRACE; + if (_wcsnicmp(flag.c_str(), L"console", wcslen(L"console")) == 0) DEBUG_FLAGS_VAR |= ASPNETCORE_DEBUG_FLAG_CONSOLE; + if (_wcsnicmp(flag.c_str(), L"file", wcslen(L"file")) == 0) DEBUG_FLAGS_VAR |= ASPNETCORE_DEBUG_FLAG_FILE; + if (_wcsnicmp(flag.c_str(), L"eventlog", wcslen(L"eventlog")) == 0) DEBUG_FLAGS_VAR |= ASPNETCORE_DEBUG_FLAG_EVENTLOG; + } + + // If file or console is enabled but level is not set, enable levels up to info + if (DEBUG_FLAGS_VAR != 0 && (DEBUG_FLAGS_VAR & DEBUG_FLAGS_ANY) == 0) + { + DEBUG_FLAGS_VAR |= DEBUG_FLAGS_INFO; + } + } + catch (...) + { + // ignore + } +} + +bool CreateDebugLogFile(const std::wstring &debugOutputFile) +{ + try + { + if (!debugOutputFile.empty()) + { + if (g_logFile != INVALID_HANDLE_VALUE) + { + LOG_INFOF(L"Switching debug log files to '%ls'", debugOutputFile.c_str()); + } + + SRWExclusiveLock lock(g_logFileLock); + if (g_logFile != INVALID_HANDLE_VALUE) + { + CloseHandle(g_logFile); + g_logFile = INVALID_HANDLE_VALUE; + } + g_logFile = CreateFileW(debugOutputFile.c_str(), + (GENERIC_READ | GENERIC_WRITE), + (FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE), + nullptr, + OPEN_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + nullptr + ); + return true; + } + } + catch (...) + { + // ignore + } + + return false; +} + +VOID +DebugInitialize(HMODULE hModule) +{ + g_hModule = hModule; + DuplicateHandle( + /* hSourceProcessHandle*/ GetCurrentProcess(), + /* hSourceHandle */ GetStdHandle(STD_OUTPUT_HANDLE), + /* hTargetProcessHandle */ GetCurrentProcess(), + /* lpTargetHandle */&g_stdOutHandle, + /* dwDesiredAccess */ 0, // dwDesired is ignored if DUPLICATE_SAME_ACCESS is specified + /* bInheritHandle */ TRUE, + /* dwOptions */ DUPLICATE_SAME_ACCESS); + + HKEY hKey; + InitializeSRWLock(&g_logFileLock); + + if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, + L"SOFTWARE\\Microsoft\\IIS Extensions\\IIS AspNetCore Module V2\\Parameters", + 0, + KEY_READ, + &hKey) == NO_ERROR) + { + DWORD dwType; + DWORD dwData; + DWORD cbData; + + cbData = sizeof(dwData); + if ((RegQueryValueEx(hKey, + L"DebugFlags", + NULL, + &dwType, + (LPBYTE)&dwData, + &cbData) == NO_ERROR) && + (dwType == REG_DWORD)) + { + DEBUG_FLAGS_VAR = dwData; + } + + RegCloseKey(hKey); + } + + try + { + SetDebugFlags(Environment::GetEnvironmentVariableValue(L"ASPNETCORE_MODULE_DEBUG").value_or(L"")); + } + catch (...) + { + // ignore + } + + try + { + const auto debugOutputFile = Environment::GetEnvironmentVariableValue(L"ASPNETCORE_MODULE_DEBUG_FILE"); + + CreateDebugLogFile(debugOutputFile.value_or(L"")); + } + catch (...) + { + // ignore + } + + if (IsDebuggerPresent()) + { + DEBUG_FLAGS_VAR |= DEBUG_FLAGS_INFO; + } + + PrintDebugHeader(); +} + +HRESULT +DebugInitializeFromConfig(IHttpServer& pHttpServer, IHttpApplication& pHttpApplication) +{ + auto oldFlags = DEBUG_FLAGS_VAR; + + CComPtr pAspNetCoreElement; + + const CComBSTR bstrAspNetCoreSection = L"system.webServer/aspNetCore"; + CComBSTR bstrConfigPath = pHttpApplication.GetAppConfigPath(); + + RETURN_IF_FAILED(pHttpServer.GetAdminManager()->GetAdminSection(bstrAspNetCoreSection, + bstrConfigPath, + &pAspNetCoreElement)); + + STRU debugFile; + RETURN_IF_FAILED(ConfigUtility::FindDebugFile(pAspNetCoreElement, debugFile)); + + STRU debugValue; + RETURN_IF_FAILED(ConfigUtility::FindDebugLevel(pAspNetCoreElement, debugValue)); + + SetDebugFlags(debugValue.QueryStr()); + + if (debugFile.QueryCCH() == 0 && IsEnabled(ASPNETCORE_DEBUG_FLAG_FILE)) + { + debugFile.Append(L".\\aspnetcore-debug.log"); + } + + std::filesystem::path filePath = std::filesystem::path(debugFile.QueryStr()); + if (!filePath.empty() && filePath.is_relative()) + { + filePath = std::filesystem::path(pHttpApplication.GetApplicationPhysicalPath()) / filePath; + } + + const auto reopenedFile = CreateDebugLogFile(filePath); + + // Print header if flags changed + if (oldFlags != DEBUG_FLAGS_VAR || reopenedFile) + { + PrintDebugHeader(); + } + + return S_OK; +} + +VOID +DebugStop() +{ + if (g_logFile != INVALID_HANDLE_VALUE) + { + CloseHandle(g_logFile); + } + + if (g_stdOutHandle != INVALID_HANDLE_VALUE) + { + CloseHandle(g_stdOutHandle); + } +} + +BOOL +IsEnabled( + DWORD dwFlag + ) +{ + return ( dwFlag & DEBUG_FLAGS_VAR ); +} + +void WriteFileEncoded(UINT codePage, HANDLE hFile, const LPCWSTR szString) +{ + DWORD nBytesWritten = 0; + auto const encodedByteCount = WideCharToMultiByte(codePage, 0, szString, -1, nullptr, 0, nullptr, nullptr); + auto encodedBytes = std::shared_ptr(new CHAR[encodedByteCount]); + WideCharToMultiByte(codePage, 0, szString, -1, encodedBytes.get(), encodedByteCount, nullptr, nullptr); + + WriteFile(hFile, encodedBytes.get(), encodedByteCount - 1, &nBytesWritten, nullptr); +} + +VOID +DebugPrintW( + DWORD dwFlag, + const LPCWSTR szString + ) +{ + STACK_STRU (strOutput, 256); + HRESULT hr = S_OK; + + if ( IsEnabled( dwFlag ) ) + { + hr = strOutput.SafeSnwprintf( + L"[%S] %s\r\n", + DEBUG_LABEL_VAR, szString ); + + if (FAILED (hr)) + { + return; + } + + OutputDebugString( strOutput.QueryStr() ); + + if (IsEnabled(ASPNETCORE_DEBUG_FLAG_CONSOLE) || g_logFile != INVALID_HANDLE_VALUE) + { + if (IsEnabled(ASPNETCORE_DEBUG_FLAG_CONSOLE)) + { + WriteFileEncoded(GetConsoleOutputCP(), g_stdOutHandle, strOutput.QueryStr()); + } + + if (g_logFile != INVALID_HANDLE_VALUE) + { + SRWExclusiveLock lock(g_logFileLock); + + SetFilePointer(g_logFile, 0, nullptr, FILE_END); + WriteFileEncoded(CP_UTF8, g_logFile, strOutput.QueryStr()); + FlushFileBuffers(g_logFile); + } + } + + if (IsEnabled(ASPNETCORE_DEBUG_FLAG_EVENTLOG)) + { + WORD eventType; + switch (dwFlag) + { + case ASPNETCORE_DEBUG_FLAG_ERROR: + eventType = EVENTLOG_ERROR_TYPE; + break; + case ASPNETCORE_DEBUG_FLAG_WARNING: + eventType = EVENTLOG_WARNING_TYPE; + break; + default: + eventType = EVENTLOG_INFORMATION_TYPE; + break; + } + EventLog::LogEventNoTrace(eventType, ASPNETCORE_EVENT_DEBUG_LOG, strOutput.QueryStr()); + } + } +} + +VOID +DebugPrintfW( + DWORD dwFlag, + const LPCWSTR szFormat, + ... + ) +{ + STACK_STRU (strCooked,256); + + va_list args; + HRESULT hr = S_OK; + + if ( IsEnabled( dwFlag ) ) + { + va_start( args, szFormat ); + + hr = strCooked.SafeVsnwprintf(szFormat, args ); + + va_end( args ); + + if (FAILED (hr)) + { + return; + } + + DebugPrintW( dwFlag, strCooked.QueryStr() ); + } +} + +VOID +DebugPrint( + DWORD dwFlag, + const LPCSTR szString + ) +{ + STACK_STRU (strOutput, 256); + + if ( IsEnabled( dwFlag ) ) + { + strOutput.CopyA(szString); + DebugPrintW(dwFlag, strOutput.QueryStr()); + } +} + +VOID +DebugPrintf( + DWORD dwFlag, + const LPCSTR szFormat, + ... + ) +{ + STACK_STRA (strCooked,256); + + va_list args; + HRESULT hr = S_OK; + + if ( IsEnabled( dwFlag ) ) + { + va_start( args, szFormat ); + + hr = strCooked.SafeVsnprintf(szFormat, args ); + + va_end( args ); + + if (FAILED (hr)) + { + return; + } + + DebugPrint( dwFlag, strCooked.QueryStr() ); + } +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/debugutil.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/debugutil.h new file mode 100644 index 0000000000..24446dbd80 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/debugutil.h @@ -0,0 +1,79 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "stdafx.h" +#include "stringu.h" +#include +#include "dbgutil.h" + +#define ASPNETCORE_DEBUG_FLAG_TRACE DEBUG_FLAG_TRACE +#define ASPNETCORE_DEBUG_FLAG_INFO DEBUG_FLAG_INFO +#define ASPNETCORE_DEBUG_FLAG_WARNING DEBUG_FLAG_WARN +#define ASPNETCORE_DEBUG_FLAG_ERROR DEBUG_FLAG_ERROR +#define ASPNETCORE_DEBUG_FLAG_CONSOLE 0x00000010 +#define ASPNETCORE_DEBUG_FLAG_FILE 0x00000020 +#define ASPNETCORE_DEBUG_FLAG_EVENTLOG 0x00000040 + +#define LOG_TRACE(...) DebugPrintW(ASPNETCORE_DEBUG_FLAG_TRACE, __VA_ARGS__) +#define LOG_TRACEF(...) DebugPrintfW(ASPNETCORE_DEBUG_FLAG_TRACE, __VA_ARGS__) + +#define LOG_INFO(...) DebugPrintW(ASPNETCORE_DEBUG_FLAG_INFO, __VA_ARGS__) +#define LOG_INFOF(...) DebugPrintfW(ASPNETCORE_DEBUG_FLAG_INFO, __VA_ARGS__) + +#define LOG_WARN(...) DebugPrintW(ASPNETCORE_DEBUG_FLAG_WARNING, __VA_ARGS__) +#define LOG_WARNF(...) DebugPrintfW(ASPNETCORE_DEBUG_FLAG_WARNING, __VA_ARGS__) + +#define LOG_ERROR(...) DebugPrintW(ASPNETCORE_DEBUG_FLAG_ERROR, __VA_ARGS__) +#define LOG_ERRORF(...) DebugPrintfW(ASPNETCORE_DEBUG_FLAG_ERROR, __VA_ARGS__) + +VOID +DebugInitialize(HMODULE hModule); + +HRESULT +DebugInitializeFromConfig(IHttpServer& pHttpServer, IHttpApplication& pHttpApplication); + +VOID +DebugStop(); + +BOOL +IsEnabled( + DWORD dwFlag + ); + +VOID +DebugPrintW( + DWORD dwFlag, + LPCWSTR szString + ); + +VOID +DebugPrintfW( + DWORD dwFlag, + LPCWSTR szFormat, + ... + ); + + +VOID +DebugPrint( + DWORD dwFlag, + LPCSTR szString + ); + +VOID +DebugPrintf( + DWORD dwFlag, + LPCSTR szFormat, + ... + ); + +std::wstring +GetProcessIdString(); + +std::wstring +GetVersionInfoString(); + +std::wstring +GetModuleName(); diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/exceptions.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/exceptions.h new file mode 100644 index 0000000000..18115c0e48 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/exceptions.h @@ -0,0 +1,216 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include +#include + +#include "debugutil.h" +#include "StringHelpers.h" +#include "InvalidOperationException.h" +#include "ntassert.h" +#include "NonCopyable.h" +#include "EventTracing.h" + +#define LOCATION_INFO_ENABLED TRUE + +#if LOCATION_INFO_ENABLED +#define LOCATION_FORMAT "%s:%d " +#define LOCATION_ARGUMENTS_ONLY _In_opt_ PCSTR fileName, unsigned int lineNumber +#define LOCATION_ARGUMENTS LOCATION_ARGUMENTS_ONLY, +#define LOCATION_CALL_ONLY fileName, lineNumber +#define LOCATION_CALL LOCATION_CALL_ONLY, +#define LOCATION_INFO __FILE__, __LINE__ +#else +#define LOCATION_FORMAT +#define LOCATION_ARGUMENTS_ONLY +#define LOCATION_ARGUMENTS +#define LOCATION_CALL_ONLY +#define LOCATION_CALL +#define LOCATION_INFO +#endif + +#define OBSERVE_CAUGHT_EXCEPTION() CaughtExceptionHResult(LOCATION_INFO); +#define RETURN_CAUGHT_EXCEPTION() return CaughtExceptionHResult(LOCATION_INFO); + +#define _CHECK_FAILED(expr) __pragma(warning(push)) \ + __pragma(warning(disable:4127)) /*disable condition is const warning*/ \ + FAILED(expr) \ + __pragma(warning(pop)) + +#define _HR_RET(hr) __pragma(warning(push)) \ + __pragma(warning(disable:26498)) /*disable constexpr warning */ \ + const HRESULT __hrRet = hr; \ + __pragma(warning(pop)) + +#define RETURN_HR(hr) do { _HR_RET(hr); if (_CHECK_FAILED(__hrRet)) { LogHResultFailed(LOCATION_INFO, __hrRet); } return __hrRet; } while (0, 0) +#define RETURN_LAST_ERROR() do { return LogLastError(LOCATION_INFO); } while (0, 0) +#define RETURN_IF_FAILED(hr) do { _HR_RET(hr); if (FAILED(__hrRet)) { LogHResultFailed(LOCATION_INFO, __hrRet); return __hrRet; }} while (0, 0) +#define RETURN_LAST_ERROR_IF(condition) do { if (condition) { return LogLastError(LOCATION_INFO); }} while (0, 0) +#define RETURN_LAST_ERROR_IF_NULL(ptr) do { if ((ptr) == nullptr) { return LogLastError(LOCATION_INFO); }} while (0, 0) + +#define _GOTO_FINISHED() __pragma(warning(push)) \ + __pragma(warning(disable:26438)) /*disable avoid goto warning*/ \ + goto Finished \ + __pragma(warning(pop)) + + +#define FINISHED(hrr) do { _HR_RET(hrr); if (_CHECK_FAILED(__hrRet)) { LogHResultFailed(LOCATION_INFO, __hrRet); } hr = __hrRet; _GOTO_FINISHED(); } while (0, 0) +#define FINISHED_IF_FAILED(hrr) do { _HR_RET(hrr); if (FAILED(__hrRet)) { LogHResultFailed(LOCATION_INFO, __hrRet); hr = __hrRet; _GOTO_FINISHED(); }} while (0, 0) +#define FINISHED_IF_NULL_ALLOC(ptr) do { if ((ptr) == nullptr) { hr = LogHResultFailed(LOCATION_INFO, E_OUTOFMEMORY); _GOTO_FINISHED(); }} while (0, 0) +#define FINISHED_LAST_ERROR_IF(condition) do { if (condition) { hr = LogLastError(LOCATION_INFO); _GOTO_FINISHED(); }} while (0, 0) +#define FINISHED_LAST_ERROR_IF_NULL(ptr) do { if ((ptr) == nullptr) { hr = LogLastError(LOCATION_INFO); _GOTO_FINISHED(); }} while (0, 0) + +#define THROW_HR(hr) do { _HR_RET(hr); ThrowResultException(LOCATION_INFO, LogHResultFailed(LOCATION_INFO, __hrRet)); } while (0, 0) +#define THROW_LAST_ERROR() do { ThrowResultException(LOCATION_INFO, LogLastError(LOCATION_INFO)); } while (0, 0) +#define THROW_IF_FAILED(hr) do { _HR_RET(hr); if (FAILED(__hrRet)) { ThrowResultException(LOCATION_INFO, __hrRet); }} while (0, 0) +#define THROW_LAST_ERROR_IF(condition) do { if (condition) { ThrowResultException(LOCATION_INFO, LogLastError(LOCATION_INFO)); }} while (0, 0) +#define THROW_LAST_ERROR_IF_NULL(ptr) do { if ((ptr) == nullptr) { ThrowResultException(LOCATION_INFO, LogLastError(LOCATION_INFO)); }} while (0, 0) + +#define THROW_IF_NULL_ALLOC(ptr) Throw_IfNullAlloc(ptr) + +#define CATCH_RETURN() catch (...) { RETURN_CAUGHT_EXCEPTION(); } +#define LOG_IF_FAILED(hr) LogHResultFailed(LOCATION_INFO, hr) +#define LOG_LAST_ERROR() LogLastErrorIf(LOCATION_INFO, true) +#define LOG_LAST_ERROR_IF(condition) LogLastErrorIf(LOCATION_INFO, condition) +#define SUCCEEDED_LOG(hr) SUCCEEDED(LOG_IF_FAILED(hr)) +#define FAILED_LOG(hr) FAILED(LOG_IF_FAILED(hr)) + +inline thread_local IHttpTraceContext* g_traceContext; + + __declspec(noinline) inline VOID TraceHRESULT(LOCATION_ARGUMENTS HRESULT hr) +{ + ::RaiseEvent(g_traceContext, nullptr, fileName, lineNumber, hr); +} + + __declspec(noinline) inline VOID TraceException(LOCATION_ARGUMENTS const std::exception& exception) +{ + ::RaiseEvent(g_traceContext, nullptr, fileName, lineNumber, exception.what()); +} + +class ResultException: public std::runtime_error +{ +public: + ResultException(HRESULT hr, LOCATION_ARGUMENTS_ONLY) : + runtime_error(format("HRESULT 0x%x returned at " LOCATION_FORMAT, hr, LOCATION_CALL_ONLY)), + m_hr(hr) + { + } + + HRESULT GetResult() const noexcept { return m_hr; } + +private: + +#pragma warning( push ) +#pragma warning ( disable : 26495 ) // bug in CA: m_hr is reported as uninitialized + const HRESULT m_hr = S_OK; +}; +#pragma warning( pop ) + + __declspec(noinline) inline VOID ReportUntypedException(LOCATION_ARGUMENTS_ONLY) +{ + DebugPrintf(ASPNETCORE_DEBUG_FLAG_ERROR, LOCATION_FORMAT "Unhandled non-standard exception", LOCATION_CALL_ONLY); +} + + __declspec(noinline) inline HRESULT LogLastError(LOCATION_ARGUMENTS_ONLY) +{ + const auto lastError = GetLastError(); + const auto hr = HRESULT_FROM_WIN32(lastError); + + TraceHRESULT(LOCATION_CALL hr); + DebugPrintf(ASPNETCORE_DEBUG_FLAG_ERROR, LOCATION_FORMAT "Operation failed with LastError: %d HR: 0x%x", LOCATION_CALL lastError, hr); + + return hr; +} + + __declspec(noinline) inline bool LogLastErrorIf(LOCATION_ARGUMENTS_ONLY, bool condition) +{ + if (condition) + { + LogLastError(LOCATION_CALL_ONLY); + } + + return condition; +} + + __declspec(noinline) inline VOID ReportException(LOCATION_ARGUMENTS const std::exception& exception) +{ + TraceException(LOCATION_CALL exception); + DebugPrintf(ASPNETCORE_DEBUG_FLAG_ERROR, "Exception '%s' caught at " LOCATION_FORMAT, exception.what(), LOCATION_CALL_ONLY); +} + + __declspec(noinline) inline HRESULT LogHResultFailed(LOCATION_ARGUMENTS HRESULT hr) +{ + if (FAILED(hr)) + { + TraceHRESULT(LOCATION_CALL hr); + DebugPrintf(ASPNETCORE_DEBUG_FLAG_ERROR, "Failed HRESULT returned: 0x%x at " LOCATION_FORMAT, hr, LOCATION_CALL_ONLY); + } + return hr; +} + +__declspec(noinline) inline HRESULT CaughtExceptionHResult(LOCATION_ARGUMENTS_ONLY) +{ + try + { + throw; + } + catch (const std::bad_alloc&) + { + return E_OUTOFMEMORY; + } + catch (const ResultException& exception) + { + ReportException(LOCATION_CALL exception); + return exception.GetResult(); + } + catch (const std::exception& exception) + { + ReportException(LOCATION_CALL exception); + return HRESULT_FROM_WIN32(ERROR_UNHANDLED_EXCEPTION); + } + catch (...) + { + ReportUntypedException(LOCATION_CALL_ONLY); + return HRESULT_FROM_WIN32(ERROR_UNHANDLED_EXCEPTION); + } +} + +[[noreturn]] + __declspec(noinline) inline void ThrowResultException(LOCATION_ARGUMENTS HRESULT hr) +{ + DebugPrintf(ASPNETCORE_DEBUG_FLAG_ERROR, "Throwing ResultException for HRESULT 0x%x at " LOCATION_FORMAT, hr, LOCATION_CALL_ONLY); + throw ResultException(hr, LOCATION_CALL_ONLY); +} + +template auto Throw_IfNullAlloc(PointerT pointer) +{ + if (pointer == nullptr) + { + throw std::bad_alloc(); + } + return pointer; +} +__declspec(noinline) inline std::wstring GetUnexpectedExceptionMessage(const std::runtime_error& ex) +{ + return format(L"Unexpected exception: %S", ex.what()); +} + +class TraceContextScope: NonCopyable +{ +public: + TraceContextScope(IHttpTraceContext* pTraceContext) noexcept + { + m_pPreviousTraceContext = g_traceContext; + g_traceContext = pTraceContext; + } + + ~TraceContextScope() + { + g_traceContext = m_pPreviousTraceContext; + } + private: + IHttpTraceContext* m_pPreviousTraceContext; +}; + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/file_utility.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/file_utility.cpp new file mode 100644 index 0000000000..2831eac64d --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/file_utility.cpp @@ -0,0 +1,169 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "file_utility.h" + +#include +#include "debugutil.h" +#include "exceptions.h" + +HRESULT +FILE_UTILITY::IsPathUnc( + __in LPCWSTR pszPath, + __out BOOL * pfIsUnc +) +{ + HRESULT hr = S_OK; + STRU strTempPath; + + if ( pszPath == NULL || pfIsUnc == NULL ) + { + hr = E_INVALIDARG; + goto Finished; + } + + hr = MakePathCanonicalizationProof( (LPWSTR) pszPath, &strTempPath ); + if ( FAILED(hr) ) + { + goto Finished; + } + + // + // MakePathCanonicalizationProof will map \\?\UNC, \\.\UNC and \\ to \\?\UNC + // + (*pfIsUnc) = ( _wcsnicmp( strTempPath.QueryStr(), L"\\\\?\\UNC\\", 8 /* sizeof \\?\UNC\ */) == 0 ); + +Finished: + + return hr; +} + +HRESULT +FILE_UTILITY::ConvertPathToFullPath( + _In_ LPCWSTR pszPath, + _In_ LPCWSTR pszRootPath, + _Out_ STRU* pStruFullPath +) +{ + HRESULT hr = S_OK; + STRU strFileFullPath; + LPWSTR pszFullPath = NULL; + + // if relative path, prefix with root path and then convert to absolute path. + if ( PathIsRelative(pszPath) ) + { + hr = strFileFullPath.Copy(pszRootPath); + if(FAILED(hr)) + { + goto Finished; + } + + if(!strFileFullPath.EndsWith(L"\\")) + { + hr = strFileFullPath.Append(L"\\"); + if(FAILED(hr)) + { + goto Finished; + } + } + } + + hr = strFileFullPath.Append( pszPath ); + if (FAILED(hr)) + { + goto Finished; + } + + pszFullPath = new WCHAR[ strFileFullPath.QueryCCH() + 1]; + + if(_wfullpath( pszFullPath, + strFileFullPath.QueryStr(), + strFileFullPath.QueryCCH() + 1 ) == NULL ) + { + hr = HRESULT_FROM_WIN32( ERROR_INVALID_PARAMETER ); + goto Finished; + } + + // convert to canonical path + hr = MakePathCanonicalizationProof( pszFullPath, pStruFullPath ); + if (FAILED(hr)) + { + goto Finished; + } + +Finished: + + if ( pszFullPath != NULL ) + { + delete[] pszFullPath; + pszFullPath = NULL; + } + + return hr; +} + +HRESULT +FILE_UTILITY::EnsureDirectoryPathExist( + _In_ LPCWSTR pszPath +) +{ + HRESULT hr = S_OK; + STRU struPath; + DWORD dwPosition = 0; + BOOL fDone = FALSE; + BOOL fUnc = FALSE; + + struPath.Copy(pszPath); + hr = IsPathUnc(pszPath, &fUnc); + if (FAILED(hr)) + { + goto Finished; + } + if (fUnc) + { + // "\\?\UNC\" + dwPosition = 8; + } + else if (struPath.IndexOf(L'?', 0) != -1) + { + // sceanrio "\\?\" + dwPosition = 4; + } + while (!fDone) + { + dwPosition = struPath.IndexOf(L'\\', dwPosition + 1); + if (dwPosition == -1) + { + // not found '/' + fDone = TRUE; + goto Finished; + } + else if (dwPosition ==0) + { + hr = ERROR_INTERNAL_ERROR; + goto Finished; + } + else if (struPath.QueryStr()[dwPosition-1] == L':') + { + // skip volume case + continue; + } + else + { + struPath.QueryStr()[dwPosition] = L'\0'; + } + + if (!CreateDirectory(struPath.QueryStr(), NULL) && + ERROR_ALREADY_EXISTS != GetLastError()) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + fDone = TRUE; + goto Finished; + } + struPath.QueryStr()[dwPosition] = L'\\'; + } + +Finished: + return hr; +} + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/file_utility.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/file_utility.h new file mode 100644 index 0000000000..686e810cbe --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/file_utility.h @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "stdafx.h" + +#include +#include "stringu.h" + +class FILE_UTILITY +{ +public: + + static + HRESULT + ConvertPathToFullPath( + _In_ LPCWSTR pszPath, + _In_ LPCWSTR pszRootPath, + _Out_ STRU* pStrFullPath + ); + + static + HRESULT + EnsureDirectoryPathExist( + _In_ LPCWSTR pszPath + ); + + static + std::string + GetHtml(HMODULE module, int page); + +private: + static + HRESULT + IsPathUnc( + __in LPCWSTR pszPath, + __out BOOL * pfIsUnc + ); + +}; + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/fx_ver.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/fx_ver.cpp new file mode 100644 index 0000000000..63183adf01 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/fx_ver.cpp @@ -0,0 +1,196 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#include "fx_ver.h" + +#include +#include +#include + +fx_ver_t::fx_ver_t(int major, int minor, int patch, const std::wstring& pre, const std::wstring& build) + : m_major(major) + , m_minor(minor) + , m_patch(patch) + , m_pre(pre) + , m_build(build) +{ +} + +fx_ver_t::fx_ver_t(int major, int minor, int patch, const std::wstring& pre) + : fx_ver_t(major, minor, patch, pre, TEXT("")) +{ +} + +fx_ver_t::fx_ver_t(int major, int minor, int patch) + : fx_ver_t(major, minor, patch, TEXT(""), TEXT("")) +{ +} + +bool fx_ver_t::operator ==(const fx_ver_t& b) const +{ + return compare(*this, b) == 0; +} + +bool fx_ver_t::operator !=(const fx_ver_t& b) const +{ + return !operator ==(b); +} + +bool fx_ver_t::operator <(const fx_ver_t& b) const +{ + return compare(*this, b) < 0; +} + +bool fx_ver_t::operator >(const fx_ver_t& b) const +{ + return compare(*this, b) > 0; +} + +bool fx_ver_t::operator <=(const fx_ver_t& b) const +{ + return compare(*this, b) <= 0; +} + +bool fx_ver_t::operator >=(const fx_ver_t& b) const +{ + return compare(*this, b) >= 0; +} + +std::wstring fx_ver_t::as_str() const +{ + std::wstringstream stream; + stream << m_major << TEXT(".") << m_minor << TEXT(".") << m_patch; + if (!m_pre.empty()) + { + stream << m_pre; + } + if (!m_build.empty()) + { + stream << TEXT("+") << m_build; + } + return stream.str(); +} + +/* static */ +int fx_ver_t::compare(const fx_ver_t&a, const fx_ver_t& b) +{ + // compare(u.v.w-p+b, x.y.z-q+c) + if (a.m_major != b.m_major) + { + return (a.m_major > b.m_major) ? 1 : -1; + } + + if (a.m_minor != b.m_minor) + { + return (a.m_minor > b.m_minor) ? 1 : -1; + } + + if (a.m_patch != b.m_patch) + { + return (a.m_patch > b.m_patch) ? 1 : -1; + } + + if (a.m_pre.empty() != b.m_pre.empty()) + { + // Either a is empty or b is empty + return a.m_pre.empty() ? 1 : -1; + } + + // Either both are empty or both are non-empty (may be equal) + int pre_cmp = a.m_pre.compare(b.m_pre); + if (pre_cmp != 0) + { + return pre_cmp; + } + + return a.m_build.compare(b.m_build); +} + +bool try_stou(const std::wstring& str, unsigned* num) +{ + if (str.empty()) + { + return false; + } + if (str.find_first_not_of(TEXT("0123456789")) != std::wstring::npos) + { + return false; + } + *num = (unsigned)std::stoul(str); + return true; +} + +bool parse_internal(const std::wstring& ver, fx_ver_t* fx_ver, bool parse_only_production) +{ + size_t maj_start = 0; + size_t maj_sep = ver.find(TEXT('.')); + if (maj_sep == std::wstring::npos) + { + return false; + } + unsigned major = 0; + if (!try_stou(ver.substr(maj_start, maj_sep), &major)) + { + return false; + } + + size_t min_start = maj_sep + 1; + size_t min_sep = ver.find(TEXT('.'), min_start); + if (min_sep == std::wstring::npos) + { + return false; + } + + unsigned minor = 0; + if (!try_stou(ver.substr(min_start, min_sep - min_start), &minor)) + { + return false; + } + + unsigned patch = 0; + size_t pat_start = min_sep + 1; + size_t pat_sep = ver.find_first_not_of(TEXT("0123456789"), pat_start); + if (pat_sep == std::wstring::npos) + { + if (!try_stou(ver.substr(pat_start), &patch)) + { + return false; + } + + *fx_ver = fx_ver_t(major, minor, patch); + return true; + } + + if (parse_only_production) + { + // This is a prerelease or has build suffix. + return false; + } + + if (!try_stou(ver.substr(pat_start, pat_sep - pat_start), &patch)) + { + return false; + } + + size_t pre_start = pat_sep; + size_t pre_sep = ver.find(TEXT('+'), pre_start); + if (pre_sep == std::wstring::npos) + { + *fx_ver = fx_ver_t(major, minor, patch, ver.substr(pre_start)); + return true; + } + else + { + size_t build_start = pre_sep + 1; + *fx_ver = fx_ver_t(major, minor, patch, ver.substr(pre_start, pre_sep - pre_start), ver.substr(build_start)); + return true; + } +} + +/* static */ +bool fx_ver_t::parse(const std::wstring& ver, fx_ver_t* fx_ver, bool parse_only_production) +{ + bool valid = parse_internal(ver, fx_ver, parse_only_production); + assert(!valid || fx_ver->as_str() == ver); + return valid; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/fx_ver.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/fx_ver.h new file mode 100644 index 0000000000..723a0da360 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/fx_ver.h @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#pragma once + +#include + +// Note: This is not SemVer (esp., in comparing pre-release part, fx_ver_t does not +// compare multiple dot separated identifiers individually.) ex: 1.0.0-beta.2 vs. 1.0.0-beta.11 +struct fx_ver_t +{ + fx_ver_t(int major, int minor, int patch); + fx_ver_t(int major, int minor, int patch, const std::wstring& pre); + fx_ver_t(int major, int minor, int patch, const std::wstring& pre, const std::wstring& build); + + int get_major() const noexcept { return m_major; } + int get_minor() const noexcept { return m_minor; } + int get_patch() const noexcept { return m_patch; } + + void set_major(int m) noexcept { m_major = m; } + void set_minor(int m) noexcept { m_minor = m; } + void set_patch(int p) noexcept { m_patch = p; } + + bool is_prerelease() const noexcept { return !m_pre.empty(); } + + std::wstring as_str() const; + + bool operator ==(const fx_ver_t& b) const; + bool operator !=(const fx_ver_t& b) const; + bool operator <(const fx_ver_t& b) const; + bool operator >(const fx_ver_t& b) const; + bool operator <=(const fx_ver_t& b) const; + bool operator >=(const fx_ver_t& b) const; + + static bool parse(const std::wstring& ver, fx_ver_t* fx_ver, bool parse_only_production = false); + +private: + int m_major; + int m_minor; + int m_patch; + std::wstring m_pre; + std::wstring m_build; + + static int compare(const fx_ver_t&a, const fx_ver_t& b); +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/hostfxr_utility.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/hostfxr_utility.cpp new file mode 100644 index 0000000000..bc27526a41 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/hostfxr_utility.cpp @@ -0,0 +1,519 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "hostfxr_utility.h" + +#include +#include "fx_ver.h" +#include "debugutil.h" +#include "exceptions.h" +#include "HandleWrapper.h" +#include "Environment.h" +#include "StringHelpers.h" + +namespace fs = std::filesystem; + +void +HOSTFXR_UTILITY::GetHostFxrParameters( + const fs::path &processPath, + const fs::path &applicationPhysicalPath, + const std::wstring &applicationArguments, + fs::path &hostFxrDllPath, + fs::path &dotnetExePath, + std::vector &arguments +) +{ + LOG_INFOF(L"Resolving hostfxr parameters for application: '%ls' arguments: '%ls' path: '%ls'", + processPath.c_str(), + applicationArguments.c_str(), + applicationPhysicalPath.c_str()); + arguments = std::vector(); + + fs::path expandedProcessPath = Environment::ExpandEnvironmentVariables(processPath); + const auto expandedApplicationArguments = Environment::ExpandEnvironmentVariables(applicationArguments); + + LOG_INFOF(L"Known dotnet.exe location: '%ls'", dotnetExePath.c_str()); + + if (!expandedProcessPath.has_extension()) + { + // The only executable extension inprocess supports + expandedProcessPath.replace_extension(".exe"); + } + else if (!ends_with(expandedProcessPath, L".exe", true)) + { + throw InvalidOperationException(format(L"Process path '%s' doesn't have '.exe' extension.", expandedProcessPath.c_str())); + } + + // Check if the absolute path is to dotnet or not. + if (IsDotnetExecutable(expandedProcessPath)) + { + LOG_INFOF(L"Process path '%ls' is dotnet, treating application as portable", expandedProcessPath.c_str()); + + if (applicationArguments.empty()) + { + throw InvalidOperationException(L"Application arguments are empty."); + } + + if (dotnetExePath.empty()) + { + dotnetExePath = GetAbsolutePathToDotnet(applicationPhysicalPath, expandedProcessPath); + } + + hostFxrDllPath = GetAbsolutePathToHostFxr(dotnetExePath); + + arguments.push_back(dotnetExePath); + AppendArguments( + expandedApplicationArguments, + applicationPhysicalPath, + arguments, + true); + } + else + { + LOG_INFOF(L"Process path '%ls' is not dotnet, treating application as standalone or portable with bootstrapper", expandedProcessPath.c_str()); + + auto executablePath = expandedProcessPath; + + if (executablePath.is_relative()) + { + executablePath = applicationPhysicalPath / expandedProcessPath; + } + + // + // The processPath is a path to the application executable + // like: C:\test\MyApp.Exe or MyApp.Exe + // Check if the file exists, and if it does, get the parameters for a standalone application + // + if (is_regular_file(executablePath)) + { + auto applicationDllPath = executablePath; + applicationDllPath.replace_extension(".dll"); + + LOG_INFOF(L"Checking application.dll at '%ls'", applicationDllPath.c_str()); + if (!is_regular_file(applicationDllPath)) + { + throw InvalidOperationException(format(L"Application .dll was not found at %s", applicationDllPath.c_str())); + } + + hostFxrDllPath = executablePath.parent_path() / "hostfxr.dll"; + LOG_INFOF(L"Checking hostfxr.dll at '%ls'", hostFxrDllPath.c_str()); + if (is_regular_file(hostFxrDllPath)) + { + LOG_INFOF(L"hostfxr.dll found app local at '%ls', treating application as standalone", hostFxrDllPath.c_str()); + // For standalone apps we need .exe to be argv[0], dll would be discovered next to it + arguments.push_back(executablePath); + } + else + { + LOG_INFOF(L"hostfxr.dll found app local at '%ls', treating application as portable with launcher", hostFxrDllPath.c_str()); + + // passing "dotnet" here because we don't know where dotnet.exe should come from + // so trying all fallbacks is appropriate + if (dotnetExePath.empty()) + { + dotnetExePath = GetAbsolutePathToDotnet(applicationPhysicalPath, L"dotnet"); + } + hostFxrDllPath = GetAbsolutePathToHostFxr(dotnetExePath); + + // For portable with launcher apps we need dotnet.exe to be argv[0] and .dll be argv[1] + arguments.push_back(dotnetExePath); + arguments.push_back(applicationDllPath); + } + + AppendArguments( + expandedApplicationArguments, + applicationPhysicalPath, + arguments); + } + else + { + // + // If the processPath file does not exist and it doesn't include dotnet.exe or dotnet + // then it is an invalid argument. + // + throw InvalidOperationException(format(L"Executable was not found at '%s'", executablePath.c_str())); + } + } +} + +BOOL +HOSTFXR_UTILITY::IsDotnetExecutable(const std::filesystem::path & dotnetPath) +{ + return ends_with(dotnetPath, L"dotnet.exe", true); +} + +void +HOSTFXR_UTILITY::AppendArguments( + const std::wstring &applicationArguments, + const fs::path &applicationPhysicalPath, + std::vector &arguments, + bool expandDllPaths +) +{ + if (applicationArguments.empty()) + { + return; + } + + // don't throw while trying to expand arguments + std::error_code ec; + + // Try to treat entire arguments section as a single path + if (expandDllPaths) + { + fs::path argumentAsPath = applicationArguments; + if (is_regular_file(argumentAsPath, ec)) + { + LOG_INFOF(L"Treating '%ls' as a single path argument", applicationArguments.c_str()); + arguments.push_back(argumentAsPath); + return; + } + + if (argumentAsPath.is_relative()) + { + argumentAsPath = applicationPhysicalPath / argumentAsPath; + if (is_regular_file(argumentAsPath, ec)) + { + LOG_INFOF(L"Converted argument '%ls' to '%ls'", applicationArguments.c_str(), argumentAsPath.c_str()); + arguments.push_back(argumentAsPath); + return; + } + } + } + + int argc = 0; + auto pwzArgs = std::unique_ptr(CommandLineToArgvW(applicationArguments.c_str(), &argc)); + if (!pwzArgs) + { + throw InvalidOperationException(format(L"Unable parse command line arguments '%s'", applicationArguments.c_str())); + } + + for (int intArgsProcessed = 0; intArgsProcessed < argc; intArgsProcessed++) + { + std::wstring argument = pwzArgs[intArgsProcessed]; + + // Try expanding arguments ending in .dll to a full paths + if (expandDllPaths && ends_with(argument, L".dll", true)) + { + fs::path argumentAsPath = argument; + if (argumentAsPath.is_relative()) + { + argumentAsPath = applicationPhysicalPath / argumentAsPath; + if (is_regular_file(argumentAsPath, ec)) + { + LOG_INFOF(L"Converted argument '%ls' to '%ls'", argument.c_str(), argumentAsPath.c_str()); + argument = argumentAsPath; + } + } + } + + arguments.push_back(argument); + } +} + +// The processPath ends with dotnet.exe or dotnet +// like: C:\Program Files\dotnet\dotnet.exe, C:\Program Files\dotnet\dotnet, dotnet.exe, or dotnet. +// Get the absolute path to dotnet. If the path is already an absolute path, it will return that path +fs::path +HOSTFXR_UTILITY::GetAbsolutePathToDotnet( + const fs::path & applicationPath, + const fs::path & requestedPath +) +{ + LOG_INFOF(L"Resolving absolute path to dotnet.exe from '%ls'", requestedPath.c_str()); + + auto processPath = requestedPath; + if (processPath.is_relative()) + { + processPath = applicationPath / processPath; + } + + // + // If we are given an absolute path to dotnet.exe, we are done + // + if (is_regular_file(processPath)) + { + LOG_INFOF(L"Found dotnet.exe at '%ls'", processPath.c_str()); + + return processPath; + } + + // At this point, we are calling where.exe to find dotnet. + // If we encounter any failures, try getting dotnet.exe from the + // backup location. + // Only do it if no path is specified + if (requestedPath.has_parent_path()) + { + LOG_INFOF(L"Absolute path to dotnet.exe was not found at '%ls'", requestedPath.c_str()); + + throw InvalidOperationException(format(L"Could not find dotnet.exe at '%s'", processPath.c_str())); + } + + const auto dotnetViaWhere = InvokeWhereToFindDotnet(); + if (dotnetViaWhere.has_value()) + { + LOG_INFOF(L"Found dotnet.exe via where.exe invocation at '%ls'", dotnetViaWhere.value().c_str()); + + return dotnetViaWhere.value(); + } + + const auto programFilesLocation = GetAbsolutePathToDotnetFromProgramFiles(); + if (programFilesLocation.has_value()) + { + LOG_INFOF(L"Found dotnet.exe in Program Files at '%ls'", programFilesLocation.value().c_str()); + + return programFilesLocation.value(); + } + + LOG_INFOF(L"dotnet.exe not found"); + throw InvalidOperationException(format( + L"Could not find dotnet.exe at '%s' or using the system PATH environment variable." + " Check that a valid path to dotnet is on the PATH and the bitness of dotnet matches the bitness of the IIS worker process.", + processPath.c_str())); +} + +fs::path +HOSTFXR_UTILITY::GetAbsolutePathToHostFxr( + const fs::path & dotnetPath +) +{ + std::vector versionFolders; + const auto hostFxrBase = dotnetPath.parent_path() / "host" / "fxr"; + + LOG_INFOF(L"Resolving absolute path to hostfxr.dll from '%ls'", dotnetPath.c_str()); + + if (!is_directory(hostFxrBase)) + { + throw InvalidOperationException(format(L"Unable to find hostfxr directory at %s", hostFxrBase.c_str())); + } + + FindDotNetFolders(hostFxrBase, versionFolders); + + if (versionFolders.empty()) + { + throw InvalidOperationException(format(L"Hostfxr directory '%s' doesn't contain any version subdirectories", hostFxrBase.c_str())); + } + + const auto highestVersion = FindHighestDotNetVersion(versionFolders); + const auto hostFxrPath = hostFxrBase / highestVersion / "hostfxr.dll"; + + if (!is_regular_file(hostFxrPath)) + { + throw InvalidOperationException(format(L"hostfxr.dll not found at '%s'", hostFxrPath.c_str())); + } + + LOG_INFOF(L"hostfxr.dll located at '%ls'", hostFxrPath.c_str()); + return hostFxrPath; +} + +// +// Tries to call where.exe to find the location of dotnet.exe. +// Will check that the bitness of dotnet matches the current +// worker process bitness. +// Returns true if a valid dotnet was found, else false.R +// +std::optional +HOSTFXR_UTILITY::InvokeWhereToFindDotnet() +{ + HRESULT hr = S_OK; + // Arguments to call where.exe + STARTUPINFOW startupInfo = { 0 }; + PROCESS_INFORMATION processInformation = { 0 }; + SECURITY_ATTRIBUTES securityAttributes; + + CHAR pzFileContents[READ_BUFFER_SIZE]; + HandleWrapper hStdOutReadPipe; + HandleWrapper hStdOutWritePipe; + HandleWrapper hProcess; + HandleWrapper hThread; + CComBSTR pwzDotnetName = NULL; + DWORD dwFilePointer; + BOOL fIsWow64Process; + BOOL fIsCurrentProcess64Bit; + DWORD dwExitCode; + STRU struDotnetSubstring; + STRU struDotnetLocationsString; + DWORD dwNumBytesRead; + DWORD dwBinaryType; + INT index = 0; + INT prevIndex = 0; + std::optional result; + + // Set the security attributes for the read/write pipe + securityAttributes.nLength = sizeof(securityAttributes); + securityAttributes.lpSecurityDescriptor = NULL; + securityAttributes.bInheritHandle = TRUE; + + LOG_INFO(L"Invoking where.exe to find dotnet.exe"); + + // Create a read/write pipe that will be used for reading the result of where.exe + FINISHED_LAST_ERROR_IF(!CreatePipe(&hStdOutReadPipe, &hStdOutWritePipe, &securityAttributes, 0)); + FINISHED_LAST_ERROR_IF(!SetHandleInformation(hStdOutReadPipe, HANDLE_FLAG_INHERIT, 0)); + + // Set the stdout and err pipe to the write pipes. + startupInfo.cb = sizeof(startupInfo); + startupInfo.dwFlags |= STARTF_USESTDHANDLES; + startupInfo.hStdOutput = hStdOutWritePipe; + startupInfo.hStdError = hStdOutWritePipe; + + // CreateProcess requires a mutable string to be passed to commandline + // See https://blogs.msdn.microsoft.com/oldnewthing/20090601-00/?p=18083/ + pwzDotnetName = L"\"where.exe\" dotnet.exe"; + + // Create a process to invoke where.exe + FINISHED_LAST_ERROR_IF(!CreateProcessW(NULL, + pwzDotnetName, + NULL, + NULL, + TRUE, + CREATE_NO_WINDOW, + NULL, + NULL, + &startupInfo, + &processInformation + )); + + // Store handles into wrapper so they get closed automatically + hProcess = processInformation.hProcess; + hThread = processInformation.hThread; + + // Wait for where.exe to return + WaitForSingleObject(processInformation.hProcess, INFINITE); + + // + // where.exe will return 0 on success, 1 if the file is not found + // and 2 if there was an error. Check if the exit code is 1 and set + // a new hr result saying it couldn't find dotnet.exe + // + FINISHED_LAST_ERROR_IF (!GetExitCodeProcess(processInformation.hProcess, &dwExitCode)); + + // + // In this block, if anything fails, we will goto our fallback of + // looking in C:/Program Files/ + // + if (dwExitCode != 0) + { + FINISHED_IF_FAILED(E_FAIL); + } + + // Where succeeded. + // Reset file pointer to the beginning of the file. + dwFilePointer = SetFilePointer(hStdOutReadPipe, 0, NULL, FILE_BEGIN); + if (dwFilePointer == INVALID_SET_FILE_POINTER) + { + FINISHED_IF_FAILED(E_FAIL); + } + + // + // As the call to where.exe succeeded (dotnet.exe was found), ReadFile should not hang. + // TODO consider putting ReadFile in a separate thread with a timeout to guarantee it doesn't block. + // + FINISHED_LAST_ERROR_IF (!ReadFile(hStdOutReadPipe, pzFileContents, READ_BUFFER_SIZE, &dwNumBytesRead, NULL)); + + if (dwNumBytesRead >= READ_BUFFER_SIZE) + { + // This shouldn't ever be this large. We could continue to call ReadFile in a loop, + // however if someone had this many dotnet.exes on their machine. + FINISHED_IF_FAILED(E_FAIL); + } + + FINISHED_IF_FAILED(struDotnetLocationsString.CopyA(pzFileContents, dwNumBytesRead)); + + LOG_INFOF(L"where.exe invocation returned: '%ls'", struDotnetLocationsString.QueryStr()); + + // Check the bitness of the currently running process + // matches the dotnet.exe found. + FINISHED_LAST_ERROR_IF (!IsWow64Process(GetCurrentProcess(), &fIsWow64Process)); + + if (fIsWow64Process) + { + // 32 bit mode + fIsCurrentProcess64Bit = FALSE; + } + else + { + // Check the SystemInfo to see if we are currently 32 or 64 bit. + SYSTEM_INFO systemInfo; + GetNativeSystemInfo(&systemInfo); + fIsCurrentProcess64Bit = systemInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64; + } + + LOG_INFOF(L"Current process bitness type detected as isX64=%d", fIsCurrentProcess64Bit); + + while (TRUE) + { + index = struDotnetLocationsString.IndexOf(L"\r\n", prevIndex); + if (index == -1) + { + break; + } + + FINISHED_IF_FAILED(struDotnetSubstring.Copy(&struDotnetLocationsString.QueryStr()[prevIndex], index - prevIndex)); + // \r\n is two wchars, so add 2 here. + prevIndex = index + 2; + + LOG_INFOF(L"Processing entry '%ls'", struDotnetSubstring.QueryStr()); + + if (LOG_LAST_ERROR_IF(!GetBinaryTypeW(struDotnetSubstring.QueryStr(), &dwBinaryType))) + { + continue; + } + + LOG_INFOF(L"Binary type %d", dwBinaryType); + + if (fIsCurrentProcess64Bit == (dwBinaryType == SCS_64BIT_BINARY)) + { + // The bitness of dotnet matched with the current worker process bitness. + return std::make_optional(struDotnetSubstring.QueryStr()); + } + } + + Finished: + return result; +} + +std::optional +HOSTFXR_UTILITY::GetAbsolutePathToDotnetFromProgramFiles() +{ + const auto programFilesDotnet = fs::path(Environment::ExpandEnvironmentVariables(L"%ProgramFiles%")) / "dotnet" / "dotnet.exe"; + return is_regular_file(programFilesDotnet) ? std::make_optional(programFilesDotnet) : std::nullopt; +} + +std::wstring +HOSTFXR_UTILITY::FindHighestDotNetVersion( + _In_ std::vector & vFolders +) +{ + fx_ver_t max_ver(-1, -1, -1); + for (const auto& dir : vFolders) + { + fx_ver_t fx_ver(-1, -1, -1); + if (fx_ver_t::parse(dir, &fx_ver, false)) + { + max_ver = max(max_ver, fx_ver); + } + } + + return max_ver.as_str(); +} + +VOID +HOSTFXR_UTILITY::FindDotNetFolders( + const std::filesystem::path &path, + _Out_ std::vector &pvFolders +) +{ + WIN32_FIND_DATAW data = {}; + const auto searchPattern = std::wstring(path) + L"\\*"; + HandleWrapper handle = FindFirstFileExW(searchPattern.c_str(), FindExInfoStandard, &data, FindExSearchNameMatch, nullptr, 0); + if (handle == INVALID_HANDLE_VALUE) + { + LOG_LAST_ERROR(); + return; + } + + do + { + pvFolders.emplace_back(data.cFileName); + } while (FindNextFileW(handle, &data)); +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/hostfxr_utility.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/hostfxr_utility.h new file mode 100644 index 0000000000..c015fadc88 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/hostfxr_utility.h @@ -0,0 +1,91 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include +#include +#include +#include +#include + +typedef INT(*hostfxr_get_native_search_directories_fn) (CONST INT argc, CONST PCWSTR* argv, PWSTR buffer, DWORD buffer_size, DWORD* required_buffer_size); +typedef INT(*hostfxr_main_fn) (CONST DWORD argc, CONST PCWSTR argv[]); + +#define READ_BUFFER_SIZE 4096 + +class HOSTFXR_UTILITY +{ +public: + + static + void + GetHostFxrParameters( + const std::filesystem::path &processPath, + const std::filesystem::path &applicationPhysicalPath, + const std::wstring &applicationArguments, + std::filesystem::path &hostFxrDllPath, + std::filesystem::path &dotnetExePath, + std::vector &arguments + ); + + static + void + AppendArguments( + const std::wstring &arugments, + const std::filesystem::path &applicationPhysicalPath, + std::vector &arguments, + bool expandDllPaths = false + ); + + static + std::optional + GetAbsolutePathToDotnetFromProgramFiles(); +private: + + static + BOOL + IsDotnetExecutable( + const std::filesystem::path & dotnetPath + ); + + static + VOID + FindDotNetFolders( + const std::filesystem::path& path, + std::vector & pvFolders + ); + + static + std::wstring + FindHighestDotNetVersion( + std::vector & vFolders + ); + + static + std::filesystem::path + GetAbsolutePathToHostFxr( + const std::filesystem::path & dotnetPath + ); + + + static + std::optional + InvokeWhereToFindDotnet(); + + static + std::filesystem::path + GetAbsolutePathToDotnet( + const std::filesystem::path & applicationPath, + const std::filesystem::path & requestedPath + ); + + struct LocalFreeDeleter + { + void operator ()(_In_ LPWSTR* ptr) const + { + LocalFree(ptr); + } + }; +}; + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/hostfxroptions.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/hostfxroptions.cpp new file mode 100644 index 0000000000..060224c60d --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/hostfxroptions.cpp @@ -0,0 +1,67 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "hostfxroptions.h" + +#include "hostfxr_utility.h" +#include "debugutil.h" +#include "exceptions.h" +#include "EventLog.h" + +HRESULT HOSTFXR_OPTIONS::Create( + _In_ const std::wstring& pcwzDotnetExePath, + _In_ const std::wstring& pcwzProcessPath, + _In_ const std::wstring& pcwzApplicationPhysicalPath, + _In_ const std::wstring& pcwzArguments, + _Out_ std::unique_ptr& ppWrapper) +{ + std::filesystem::path knownDotnetLocation; + + if (!pcwzDotnetExePath.empty()) + { + knownDotnetLocation = pcwzDotnetExePath; + } + + try + { + std::filesystem::path hostFxrDllPath; + std::vector arguments; + HOSTFXR_UTILITY::GetHostFxrParameters( + pcwzProcessPath, + pcwzApplicationPhysicalPath, + pcwzArguments, + hostFxrDllPath, + knownDotnetLocation, + arguments); + + LOG_INFOF(L"Parsed hostfxr options: dotnet location: '%ls' hostfxr path: '%ls' arguments:", knownDotnetLocation.c_str(), hostFxrDllPath.c_str()); + for (size_t i = 0; i < arguments.size(); i++) + { + LOG_INFOF(L"Argument[%d] = '%ls'", i, arguments[i].c_str()); + } + ppWrapper = std::make_unique(knownDotnetLocation, hostFxrDllPath, arguments); + } + catch (InvalidOperationException &ex) + { + EventLog::Error( + ASPNETCORE_EVENT_INPROCESS_START_ERROR, + ASPNETCORE_EVENT_INPROCESS_START_ERROR_MSG, + pcwzApplicationPhysicalPath.c_str(), + ex.as_wstring().c_str()); + + RETURN_CAUGHT_EXCEPTION(); + } + catch (std::runtime_error &ex) + { + EventLog::Error( + ASPNETCORE_EVENT_INPROCESS_START_ERROR, + ASPNETCORE_EVENT_INPROCESS_START_ERROR_MSG, + pcwzApplicationPhysicalPath.c_str(), + GetUnexpectedExceptionMessage(ex).c_str()); + + RETURN_CAUGHT_EXCEPTION(); + } + CATCH_RETURN(); + + return S_OK; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/hostfxroptions.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/hostfxroptions.h new file mode 100644 index 0000000000..243cc53ae7 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/hostfxroptions.h @@ -0,0 +1,61 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include +#include +#include +#include +#include +#include + +class HOSTFXR_OPTIONS +{ +public: + HOSTFXR_OPTIONS( + std::filesystem::path dotnetExeLocation, + std::filesystem::path hostFxrLocation, + std::vector arguments + ) noexcept + : m_dotnetExeLocation(std::move(dotnetExeLocation)), + m_hostFxrLocation(std::move(hostFxrLocation)), + m_arguments(std::move(arguments)) + {} + + void + GetArguments(DWORD &hostfxrArgc, std::unique_ptr &hostfxrArgv) const + { + hostfxrArgc = static_cast(m_arguments.size()); + hostfxrArgv = std::make_unique(hostfxrArgc); + for (DWORD i = 0; i < hostfxrArgc; ++i) + { + hostfxrArgv[i] = m_arguments[i].c_str(); + } + } + + const std::filesystem::path& + GetHostFxrLocation() const noexcept + { + return m_hostFxrLocation; + } + + const std::filesystem::path& + GetDotnetExeLocation() const noexcept + { + return m_dotnetExeLocation; + } + + static + HRESULT Create( + _In_ const std::wstring& pcwzExeLocation, + _In_ const std::wstring& pcwzProcessPath, + _In_ const std::wstring& pcwzApplicationPhysicalPath, + _In_ const std::wstring& pcwzArguments, + _Out_ std::unique_ptr& ppWrapper); + +private: + const std::filesystem::path m_dotnetExeLocation; + const std::filesystem::path m_hostFxrLocation; + const std::vector m_arguments; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/iapplication.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/iapplication.h new file mode 100644 index 0000000000..52a1a7e2a9 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/iapplication.h @@ -0,0 +1,75 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include +#include "irequesthandler.h" + +struct APPLICATION_PARAMETER +{ + LPCSTR pzName; + void *pValue; +}; + +class IAPPLICATION +{ +public: + virtual + VOID + Stop(bool fServerInitiated) = 0; + + virtual + ~IAPPLICATION() = 0 { }; + + virtual + VOID + ReferenceApplication() = 0; + + virtual + VOID + DereferenceApplication() = 0; + + virtual + HRESULT + TryCreateHandler( + _In_ IHttpContext *pHttpContext, + _Outptr_ IREQUEST_HANDLER **pRequestHandler) = 0; +}; + +struct IAPPLICATION_DELETER +{ + void operator ()(IAPPLICATION* application) const + { + application->DereferenceApplication(); + } +}; + +template< class APPLICATION > +std::unique_ptr ReferenceApplication(APPLICATION* application) +{ + application->ReferenceApplication(); + return std::unique_ptr(application); +}; + +template< class APPLICATION, typename ...Params > +std::unique_ptr make_application(Params&&... params) +{ +#pragma warning( push ) +#pragma warning ( disable : 26409 ) // Disable "Avoid using new", using custom deleter here + return std::unique_ptr(new APPLICATION(std::forward(params)...)); +#pragma warning( pop ) +} + +template< class TYPE > +TYPE FindParameter(LPCSTR sRequiredParameter, APPLICATION_PARAMETER *pParameters, DWORD nParameters) +{ + for (DWORD i = 0; i < nParameters; i++) + { + if (_stricmp(pParameters[i].pzName, sRequiredParameter) == 0) + { + return reinterpret_cast(pParameters[i].pValue); + } + } + return nullptr; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/irequesthandler.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/irequesthandler.h new file mode 100644 index 0000000000..c89411f8a0 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/irequesthandler.h @@ -0,0 +1,62 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include +#include + +// +// Pure abstract class +// +class IREQUEST_HANDLER +{ +public: + + virtual + REQUEST_NOTIFICATION_STATUS + OnExecuteRequestHandler() = 0; + + virtual + REQUEST_NOTIFICATION_STATUS + OnAsyncCompletion( + DWORD cbCompletion, + HRESULT hrCompletionStatus + ) = 0; + + virtual + VOID + NotifyDisconnect() noexcept(false) = 0; + + virtual + ~IREQUEST_HANDLER( + VOID + ) = 0 { } + + virtual + VOID + ReferenceRequestHandler( + VOID + ) = 0; + + virtual + VOID + DereferenceRequestHandler( + VOID + ) = 0; +}; + + +struct IREQUEST_HANDLER_DELETER +{ + void operator ()(IREQUEST_HANDLER* application) const + { + application->DereferenceRequestHandler(); + } +}; + +inline std::unique_ptr ReferenceRequestHandler(IREQUEST_HANDLER* handler) +{ + handler->ReferenceRequestHandler(); + return std::unique_ptr(handler); +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/requesthandler.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/requesthandler.h new file mode 100644 index 0000000000..8bac2885b0 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/requesthandler.h @@ -0,0 +1,79 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "irequesthandler.h" +#include "ntassert.h" +#include "exceptions.h" + +// +// Pure abstract class +// +class REQUEST_HANDLER: public virtual IREQUEST_HANDLER +{ +public: + REQUEST_HANDLER(IHttpContext& pHttpContext) noexcept : m_pHttpContext(pHttpContext) + { + } + + virtual + REQUEST_NOTIFICATION_STATUS + ExecuteRequestHandler() = 0; + + VOID + ReferenceRequestHandler() noexcept override + { + InterlockedIncrement(&m_cRefs); + } + + VOID + DereferenceRequestHandler() noexcept override + { + DBG_ASSERT(m_cRefs != 0); + + if (InterlockedDecrement(&m_cRefs) == 0) + { + delete this; + } + } + + + REQUEST_NOTIFICATION_STATUS + OnExecuteRequestHandler() final + { + TraceContextScope traceScope(m_pHttpContext.GetTraceContext()); + return ExecuteRequestHandler(); + } + + REQUEST_NOTIFICATION_STATUS + OnAsyncCompletion( + DWORD cbCompletion, + HRESULT hrCompletionStatus + ) final + { + TraceContextScope traceScope(m_pHttpContext.GetTraceContext()); + return AsyncCompletion(cbCompletion, hrCompletionStatus); + }; + + virtual + REQUEST_NOTIFICATION_STATUS AsyncCompletion(DWORD cbCompletion, HRESULT hrCompletionStatus) + { + UNREFERENCED_PARAMETER(cbCompletion); + UNREFERENCED_PARAMETER(hrCompletionStatus); + // We shouldn't get here in default implementation + DBG_ASSERT(FALSE); + return RQ_NOTIFICATION_FINISH_REQUEST; + } + + #pragma warning( push ) + #pragma warning ( disable : 26440 ) // Disable "Can be marked with noexcept" + VOID NotifyDisconnect() override + #pragma warning( pop ) + { + } + +private: + IHttpContext& m_pHttpContext; + mutable LONG m_cRefs = 1; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/resources.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/resources.h new file mode 100644 index 0000000000..fdee8b9578 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/resources.h @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "aspnetcore_msg.h" + +#define IDS_INVALID_PROPERTY 1000 +#define IDS_SERVER_ERROR 1001 + +#define ASPNETCORE_EVENT_PROVIDER L"IIS AspNetCore Module V2" +#define ASPNETCORE_IISEXPRESS_EVENT_PROVIDER L"IIS Express AspNetCore Module V2" + +#define ASPNETCORE_EVENT_PROCESS_START_SUCCESS_MSG L"Application '%s' started process '%d' successfully and process '%d' is listening on port '%d'." +#define ASPNETCORE_EVENT_RAPID_FAIL_COUNT_EXCEEDED_MSG L"Maximum rapid fail count per minute of '%d' exceeded." +#define ASPNETCORE_EVENT_PROCESS_START_ERROR_MSG L"Application '%s' with physical root '%s' failed to start process with commandline '%s' at stage '%s', ErrorCode = '0x%x', assigned port %d, retryCounter '%d'." +#define ASPNETCORE_EVENT_PROCESS_START_FAILURE_MSG L"Application '%s' with physical root '%s' failed to start process with commandline '%s' with multiple retries. The last try of listening port is '%d'. See previous warnings for details." +#define ASPNETCORE_EVENT_PROCESS_START_STATUS_ERROR_MSG L"Application '%s' with physical root '%s' failed to start process with commandline '%s' , ErrorCode = '0x%x', processId '%d', processStatus '%d'." +#define ASPNETCORE_EVENT_PROCESS_START_PORTSETUP_ERROR_MSG L"Application '%s' with physical root '%s' failed to choose listen port '%d' given port range '%d - %d', ErrorCode = '0x%x'. If environment variable 'ASPNETCORE_PORT' was set, try removing it such that a random port is selected instead." +#define ASPNETCORE_EVENT_PROCESS_START_WRONGPORT_ERROR_MSG L"Application '%s' with physical root '%s' created process with commandline '%s' but failed to listen on the given port '%d'" +#define ASPNETCORE_EVENT_PROCESS_START_NOTREADY_ERROR_MSG L"Application '%s' with physical root '%s' created process with commandline '%s' but either crashed or did not respond or did not listen on the given port '%d', ErrorCode = '0x%x'" +#define ASPNETCORE_EVENT_PROCESS_SHUTDOWN_MSG L"Application '%s' with physical root '%s' shut down process with Id '%d' listening on port '%d'" +#define ASPNETCORE_EVENT_INVALID_STDOUT_LOG_FILE_MSG L"Warning: Could not create stdoutLogFile %s, ErrorCode = '0x%x'." +#define ASPNETCORE_EVENT_GRACEFUL_SHUTDOWN_FAILURE_MSG L"Failed to gracefully shutdown process '%d'." +#define ASPNETCORE_EVENT_SENT_SHUTDOWN_HTTP_REQUEST_MSG L"Sent shutdown HTTP message to process '%d' and received http status '%d'." +#define ASPNETCORE_EVENT_APP_SHUTDOWN_FAILURE_MSG L"Failed to gracefully shutdown application '%s'." +#define ASPNETCORE_EVENT_LOAD_CLR_FALIURE_MSG L"Application '%s' with physical root '%s' failed to load clr and managed application. %s" +#define ASPNETCORE_EVENT_APP_SHUTDOWN_SUCCESSFUL_MSG L"Application '%s' has shutdown." +#define ASPNETCORE_EVENT_DUPLICATED_INPROCESS_APP_MSG L"Only one inprocess application is allowed per IIS application pool. Please assign the application '%s' to a different IIS application pool." +#define ASPNETCORE_EVENT_MIXED_HOSTING_MODEL_ERROR_MSG L"Mixed hosting model is not supported. Application '%s' configured with different hostingModel value '%d' other than the one of running application(s)." +#define ASPNETCORE_CONFIGURATION_LOAD_ERROR_MSG L"Could not load configuration. Exception message: %s" +#define ASPNETCORE_EVENT_ADD_APPLICATION_ERROR_MSG L"Failed to start application '%s', ErrorCode '0x%x'." +#define ASPNETCORE_EVENT_INPROCESS_THREAD_EXIT_STDOUT_MSG L"Application '%s' with physical root '%s' hit unexpected managed background thread exit, exit code = '%d'. Last 4KB characters of captured stdout and stderr logs:\r\n%s" +#define ASPNETCORE_EVENT_INPROCESS_THREAD_EXIT_MSG L"Application '%s' with physical root '%s' hit unexpected managed background thread exit, exit code = '%d'. Please check the stderr logs for more information." +#define ASPNETCORE_EVENT_APP_IN_SHUTDOWN_MSG L"Application shutting down." +#define ASPNETCORE_EVENT_RECYCLE_APPOFFLINE_MSG L"Application '%s' was recycled after detecting the app_offline file." +#define ASPNETCORE_EVENT_MONITOR_APPOFFLINE_ERROR_MSG L"Monitoring app_offline.htm failed for application '%s', ErrorCode '0x%x'. " +#define ASPNETCORE_EVENT_RECYCLE_CONFIGURATION_MSG L"Application '%s' was recycled due to configuration change" +#define ASPNETCORE_EVENT_RECYCLE_FAILURE_CONFIGURATION_MSG L"Failed to recycle application due to a configuration change at '%s'. Recycling worker process." +#define ASPNETCORE_EVENT_MODULE_DISABLED_MSG L"AspNetCore Module is disabled" +#define ASPNETCORE_EVENT_HOSTFXR_DLL_INVALID_VERSION_MSG L"Hostfxr version used does not support 'hostfxr_get_native_search_directories', update the version of hostfxr to a higher version. Path to hostfxr: '%s'." +#define ASPNETCORE_EVENT_HOSTFXR_FAILURE_MSG L"Invoking hostfxr to find the inprocess request handler failed without finding any native dependencies. This most likely means the app is misconfigured, please check the versions of Microsoft.NetCore.App and Microsoft.AspNetCore.App that are targeted by the application and are installed on the machine." +#define ASPNETCORE_EVENT_INPROCESS_THREAD_EXCEPTION_MSG L"Application '%s' with physical root '%s' hit unexpected managed exception, exception code = '0x%x'. Please check the stderr logs for more information." +#define ASPNETCORE_EVENT_INPROCESS_THREAD_EXCEPTION_STDOUT_MSG L"Application '%s' with physical root '%s' hit unexpected managed exception, exception code = '0x%x'. Last 4KB characters of captured stdout and stderr logs:\r\n%s" +#define ASPNETCORE_EVENT_INPROCESS_RH_ERROR_MSG L"Could not find inprocess request handler. Captured output from invoking hostfxr: %s" +#define ASPNETCORE_EVENT_INPROCESS_RH_REFERENCE_MSG L"Could not find the assembly '%s' referenced for the in-process application. Please confirm the Microsoft.AspNetCore.Server.IIS package is referenced in your application." +#define ASPNETCORE_EVENT_OUT_OF_PROCESS_RH_MISSING_MSG L"Could not find the assembly '%s' for out-of-process application. Please confirm the assembly is installed correctly for IIS or IISExpress." +#define ASPNETCORE_EVENT_INPROCESS_START_SUCCESS_MSG L"Application '%s' started the coreclr in-process successfully." +#define ASPNETCORE_EVENT_INPROCESS_START_ERROR_MSG L"Application '%s' wasn't able to start. %s" diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/stdafx.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/stdafx.cpp new file mode 100644 index 0000000000..12fcb1d436 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/stdafx.cpp @@ -0,0 +1,4 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +// Do not remove this file. It is used for precompiled header generation diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/stdafx.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/stdafx.h new file mode 100644 index 0000000000..b17ae43485 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/stdafx.h @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "targetver.h" + +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers + +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/sttimer.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/sttimer.h new file mode 100644 index 0000000000..26d79d0737 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/sttimer.h @@ -0,0 +1,281 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + + +#ifndef _STTIMER_H +#define _STTIMER_H +#include "stringu.h" + +class STTIMER +{ +public: + + STTIMER() + : _pTimer( NULL ) + { + fInCanel = FALSE; + } + + virtual + ~STTIMER() + { + if ( _pTimer ) + { + CancelTimer(); + CloseThreadpoolTimer( _pTimer ); + _pTimer = NULL; + } + } + + HRESULT + InitializeTimer( + PTP_TIMER_CALLBACK pfnCallback, + VOID * pContext, + DWORD dwInitialWait = 0, + DWORD dwPeriod = 0 + ) + { + _pTimer = CreateThreadpoolTimer( pfnCallback, + pContext, + NULL ); + + if ( !_pTimer ) + { + return HRESULT_FROM_WIN32( GetLastError() ); + } + + if ( dwInitialWait ) + { + SetTimer( dwInitialWait, + dwPeriod ); + } + + return S_OK; + } + + VOID + SetTimer( + DWORD dwInitialWait, + DWORD dwPeriod = 0 + ) + { + FILETIME ftInitialWait; + + if ( dwInitialWait == 0 && dwPeriod == 0 ) + { + // + // Special case. We are preventing new callbacks + // from being queued. Any existing callbacks in the + // queue will still run. + // + // This effectively disables the timer. It can be + // re-enabled by setting non-zero initial wait or + // period values. + // + if (_pTimer != NULL) + { + SetThreadpoolTimer(_pTimer, NULL, 0, 0); + } + + return; + } + + InitializeRelativeFileTime( &ftInitialWait, dwInitialWait ); + + SetThreadpoolTimer( _pTimer, + &ftInitialWait, + dwPeriod, + 0 ); + } + + VOID + CancelTimer() + { + // + // Disable the timer + // + if (fInCanel) + return; + + fInCanel = TRUE; + SetTimer( 0 ); + + // + // Wait until any callbacks queued prior to disabling + // have completed. + // + if (_pTimer != NULL) + { + WaitForThreadpoolTimerCallbacks(_pTimer, TRUE); + } + + fInCanel = FALSE; + } + + static + VOID + CALLBACK + TimerCallback( + _In_ PTP_CALLBACK_INSTANCE Instance, + _In_ PVOID Context, + _In_ PTP_TIMER Timer + ) + { + Instance; + Timer; + STRU* pstruLogFilePath = (STRU*)Context; + HANDLE hStdoutHandle = NULL; + SECURITY_ATTRIBUTES saAttr = { 0 }; + HRESULT hr = S_OK; + + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = TRUE; + saAttr.lpSecurityDescriptor = NULL; + + hStdoutHandle = CreateFileW(pstruLogFilePath->QueryStr(), + FILE_READ_DATA, + FILE_SHARE_WRITE, + &saAttr, + OPEN_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + NULL); + if (hStdoutHandle == INVALID_HANDLE_VALUE) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + } + + CloseHandle(hStdoutHandle); + } + +private: + + VOID + InitializeRelativeFileTime( + FILETIME * pft, + DWORD dwMilliseconds + ) + { + LARGE_INTEGER li; + + // + // The pftDueTime parameter expects the time to be + // expressed as the number of 100 nanosecond intervals + // times -1. + // + // To convert from milliseconds, we'll multiply by + // -10000 + // + + li.QuadPart = (LONGLONG)dwMilliseconds * -10000; + + pft->dwHighDateTime = li.HighPart; + pft->dwLowDateTime = li.LowPart; + }; + + TP_TIMER * _pTimer; + BOOL fInCanel; +}; + +class STELAPSED +{ +public: + + STELAPSED() + : _dwInitTime( 0 ), + _dwInitTickCount( 0 ), + _dwPerfCountsPerMillisecond( 0 ), + _fUsingHighResolution( FALSE ) + { + LARGE_INTEGER li; + BOOL fResult; + + _dwInitTickCount = GetTickCount64(); + + fResult = QueryPerformanceFrequency( &li ); + + if ( !fResult ) + { + goto Finished; + } + + _dwPerfCountsPerMillisecond = li.QuadPart / 1000; + + fResult = QueryPerformanceCounter( &li ); + + if ( !fResult ) + { + goto Finished; + } + + _dwInitTime = li.QuadPart / _dwPerfCountsPerMillisecond; + + _fUsingHighResolution = TRUE; + +Finished: + + return; + } + + virtual + ~STELAPSED() + { + } + + LONGLONG + QueryElapsedTime() + { + LARGE_INTEGER li; + + if ( _fUsingHighResolution && QueryPerformanceCounter( &li ) ) + { + DWORD64 dwCurrentTime = li.QuadPart / _dwPerfCountsPerMillisecond; + + if ( dwCurrentTime < _dwInitTime ) + { + // + // It's theoretically possible that QueryPerformanceCounter + // may return slightly different values on different CPUs. + // In this case, we don't want to return an unexpected value + // so we'll return zero. This is acceptable because + // presumably such a case would only happen for a very short + // time window. + // + // It would be possible to prevent this by ensuring processor + // affinity for all calls to QueryPerformanceCounter, but that + // would be undesirable in the general case because it could + // introduce unnecessary context switches and potentially a + // CPU bottleneck. + // + // Note that this issue also applies to callers doing rapid + // calls to this function. If a caller wants to mitigate + // that, they could enforce the affinitization, or they + // could implement a similar sanity check when comparing + // returned values from this function. + // + + return 0; + } + + return dwCurrentTime - _dwInitTime; + } + + return GetTickCount64() - _dwInitTickCount; + } + + BOOL + QueryUsingHighResolution() + { + return _fUsingHighResolution; + } + +private: + + DWORD64 _dwInitTime; + DWORD64 _dwInitTickCount; + DWORD64 _dwPerfCountsPerMillisecond; + BOOL _fUsingHighResolution; +}; + +#endif // _STTIMER_H diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/targetver.h b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/targetver.h new file mode 100644 index 0000000000..5b1f29cad0 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/CommonLib/targetver.h @@ -0,0 +1,8 @@ +#pragma once + +// Including SDKDDKVer.h defines the highest available Windows platform. + +// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and +// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. + +#include \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/DefaultRules.ruleset b/src/IISIntegration/src/AspNetCoreModuleV2/DefaultRules.ruleset new file mode 100644 index 0000000000..1c2bbdaeaf --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/DefaultRules.ruleset @@ -0,0 +1,432 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/IISLib.vcxproj b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/IISLib.vcxproj new file mode 100644 index 0000000000..15d4ef1317 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/IISLib.vcxproj @@ -0,0 +1,206 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {09D9D1D6-2951-4E14-BC35-76A23CF9391A} + Win32Proj + IISLib + IISLib + 10.0.15063.0 + + + + StaticLibrary + true + v141 + Unicode + + + StaticLibrary + true + v141 + Unicode + + + StaticLibrary + false + v141 + true + Unicode + + + StaticLibrary + false + v141 + true + Unicode + + + + + + + + + + + + + + + + + + + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + + + + Level3 + Disabled + WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + true + ProgramDatabase + MultiThreadedDebug + false + true + true + + + Windows + true + + + + + + + Level3 + Disabled + WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + true + ProgramDatabase + MultiThreadedDebug + false + true + true + + + Windows + true + + + + + Level3 + + + MaxSpeed + true + true + WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + true + MultiThreaded + false + true + true + + + Windows + true + true + /NODEFAULTLIB:libucrt.lib /DEFAULTLIB:ucrt.lib %(AdditionalOptions) + true + + + + + Level3 + + + MaxSpeed + true + true + WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + true + MultiThreaded + false + true + true + + + Windows + true + true + /NODEFAULTLIB:libucrt.lib /DEFAULTLIB:ucrt.lib %(AdditionalOptions) + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/acache.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/acache.cpp new file mode 100644 index 0000000000..5f2125134e --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/acache.cpp @@ -0,0 +1,448 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "precomp.h" + +#pragma warning( push ) +#pragma warning ( disable : ALL_CODE_ANALYSIS_WARNINGS ) + +LONG ALLOC_CACHE_HANDLER::sm_nFillPattern = 0xACA50000; +HANDLE ALLOC_CACHE_HANDLER::sm_hHeap; + +// +// This class is used to implement the free list. We cast the free'd +// memory block to a FREE_LIST_HEADER*. The signature is used to guard against +// double deletion. We also fill memory with a pattern. +// +class FREE_LIST_HEADER +{ +public: + SLIST_ENTRY ListEntry; + DWORD dwSignature; + + enum + { + FREE_SIGNATURE = (('A') | ('C' << 8) | ('a' << 16) | (('$' << 24) | 0x80)), + }; +}; + +ALLOC_CACHE_HANDLER::ALLOC_CACHE_HANDLER( + VOID +) : m_nThreshold(0), + m_cbSize(0), + m_pFreeLists(NULL), + m_nTotal(0) +{ +} + +ALLOC_CACHE_HANDLER::~ALLOC_CACHE_HANDLER( + VOID +) +{ + if (m_pFreeLists != NULL) + { + CleanupLookaside(); + m_pFreeLists->Dispose(); + m_pFreeLists = NULL; + } +} + +HRESULT +ALLOC_CACHE_HANDLER::Initialize( + DWORD cbSize, + LONG nThreshold +) +{ + HRESULT hr = S_OK; + + m_nThreshold = nThreshold; + if ( m_nThreshold > 0xffff) + { + // + // This will be compared against QueryDepthSList return value (USHORT). + // + m_nThreshold = 0xffff; + } + + if ( IsPageheapEnabled() ) + { + // + // Disable acache. + // + m_nThreshold = 0; + } + + // + // Make sure the block is big enough to hold a FREE_LIST_HEADER. + // + m_cbSize = cbSize; + m_cbSize = max(m_cbSize, sizeof(FREE_LIST_HEADER)); + + // + // Round up the block size to a multiple of the size of a LONG (for + // the fill pattern in Free()). + // + m_cbSize = (m_cbSize + sizeof(LONG) - 1) & ~(sizeof(LONG) - 1); + +#if defined(_MSC_VER) && _MSC_VER >= 1600 // VC10 + auto Init = [] (SLIST_HEADER* pHead) + { + InitializeSListHead(pHead); + }; +#else + class Functor + { + public: + void operator()(SLIST_HEADER* pHead) + { + InitializeSListHead(pHead); + } + } Init; +#endif + + hr = PER_CPU::Create(Init, + &m_pFreeLists ); + if (FAILED(hr)) + { + goto Finished; + } + + m_nFillPattern = InterlockedIncrement(&sm_nFillPattern); + +Finished: + + return hr; +} + +// static +HRESULT +ALLOC_CACHE_HANDLER::StaticInitialize( + VOID +) +{ + // + // Since the memory allocated is fixed size, + // a heap is not really needed, allocations can be done + // using VirtualAllocEx[Numa]. For now use Windows Heap. + // + // Be aware that creating one private heap consumes more + // virtual address space for the worker process. + // + sm_hHeap = GetProcessHeap(); + return S_OK; +} + + +// static +VOID +ALLOC_CACHE_HANDLER::StaticTerminate( + VOID +) +{ + sm_hHeap = NULL; +} + +VOID +ALLOC_CACHE_HANDLER::CleanupLookaside( + VOID +) +/*++ + Description: + This function cleans up the lookaside list by removing storage space. + + Arguments: + None. + + Returns: + None +--*/ +{ + // + // Free up all the entries in the list. + // Don't use InterlockedFlushSList, in order to work + // memory must be 16 bytes aligned and currently it is 64. + // + +#if defined(_MSC_VER) && _MSC_VER >= 1600 // VC10 + auto Predicate = [=] (SLIST_HEADER * pListHeader) + { + PSLIST_ENTRY pl; + LONG NodesToDelete = QueryDepthSList( pListHeader ); + + pl = InterlockedPopEntrySList( pListHeader ); + while ( pl != NULL && --NodesToDelete >= 0 ) + { + InterlockedDecrement( &m_nTotal); + + ::HeapFree( sm_hHeap, 0, pl ); + + pl = InterlockedPopEntrySList(pListHeader); + } + }; +#else + class Functor + { + public: + explicit Functor(ALLOC_CACHE_HANDLER * pThis) : _pThis(pThis) + { + } + void operator()(SLIST_HEADER * pListHeader) + { + PSLIST_ENTRY pl; + LONG NodesToDelete = QueryDepthSList( pListHeader ); + + pl = InterlockedPopEntrySList( pListHeader ); + while ( pl != NULL && --NodesToDelete >= 0 ) + { + InterlockedDecrement( &_pThis->m_nTotal); + + ::HeapFree( sm_hHeap, 0, pl ); + + pl = InterlockedPopEntrySList(pListHeader); + } + } + private: + ALLOC_CACHE_HANDLER * _pThis; + } Predicate(this); +#endif + + m_pFreeLists ->ForEach(Predicate); +} + +LPVOID +ALLOC_CACHE_HANDLER::Alloc( + VOID +) +{ + LPVOID pMemory = NULL; + + if ( m_nThreshold > 0 ) + { + SLIST_HEADER * pListHeader = m_pFreeLists ->GetLocal(); + pMemory = (LPVOID) InterlockedPopEntrySList(pListHeader); // get the real object + + if (pMemory != NULL) + { + FREE_LIST_HEADER* pfl = (FREE_LIST_HEADER*) pMemory; + // + // If the signature is wrong then somebody's been scribbling + // on memory that they've freed. + // + DBG_ASSERT(pfl->dwSignature == FREE_LIST_HEADER::FREE_SIGNATURE); + } + } + + if ( pMemory == NULL ) + { + // + // No free entry. Need to alloc a new object. + // + pMemory = (LPVOID) ::HeapAlloc( sm_hHeap, + 0, + m_cbSize ); + + if ( pMemory != NULL ) + { + // + // Update counters. + // + m_nTotal++; + } + } + + if ( pMemory == NULL ) + { + SetLastError( ERROR_NOT_ENOUGH_MEMORY ); + } + else + { + FREE_LIST_HEADER* pfl = (FREE_LIST_HEADER*) pMemory; + pfl->dwSignature = 0; // clear; just in case caller never overwrites + } + + return pMemory; +} + +VOID +ALLOC_CACHE_HANDLER::Free( + __in LPVOID pMemory +) +{ + // + // Assume that this is allocated using the Alloc() function. + // + DBG_ASSERT(NULL != pMemory); + + // + // Use a signature to check against double deletions. + // + FREE_LIST_HEADER* pfl = (FREE_LIST_HEADER*) pMemory; + DBG_ASSERT(pfl->dwSignature != FREE_LIST_HEADER::FREE_SIGNATURE); + + // + // Start filling the space beyond the portion overlaid by the initial + // FREE_LIST_HEADER. Fill at most 6 DWORDS. + // + LONG* pl = (LONG*) (pfl+1); + + for (LONG cb = (LONG)min(6 * sizeof(LONG),m_cbSize) - sizeof(FREE_LIST_HEADER); + cb > 0; + cb -= sizeof(LONG)) + { + *pl++ = m_nFillPattern; + } + + // + // Now, set the signature. + // + pfl->dwSignature = FREE_LIST_HEADER::FREE_SIGNATURE; + + // + // Store the items in the alloc cache. + // + SLIST_HEADER * pListHeader = m_pFreeLists ->GetLocal(); + + if ( QueryDepthSList(pListHeader) >= m_nThreshold ) + { + // + // Threshold for free entries is exceeded. Free the object to + // process pool. + // + ::HeapFree( sm_hHeap, 0, pMemory ); + } + else + { + // + // Store the given pointer in the single linear list + // + InterlockedPushEntrySList(pListHeader, &pfl->ListEntry); + } +} + +DWORD +ALLOC_CACHE_HANDLER::QueryDepthForAllSLists( + VOID +) +/*++ + +Description: + + Aggregates the total count of elements in all lists. + +Arguments: + + None. + +Return Value: + + Total count (snapshot). + +--*/ +{ + DWORD Count = 0; + + if (m_pFreeLists != NULL) + { +#if defined(_MSC_VER) && _MSC_VER >= 1600 // VC10 + auto Predicate = [&Count] (SLIST_HEADER * pListHeader) + { + Count += QueryDepthSList(pListHeader); + }; +#else + class Functor + { + public: + explicit Functor(DWORD& Count) : _Count(Count) + { + } + void operator()(SLIST_HEADER * pListHeader) + { + _Count += QueryDepthSList(pListHeader); + } + private: + DWORD& _Count; + } Predicate(Count); +#endif + // + // [&Count] means that the method can modify local variable Count. + // + m_pFreeLists ->ForEach(Predicate); + } + + return Count; +} + +// static +BOOL +ALLOC_CACHE_HANDLER::IsPageheapEnabled( + VOID +) +{ + BOOL fRet = FALSE; + BOOL fLockedHeap = FALSE; + HMODULE hModule = NULL; + HANDLE hHeap = NULL; + PROCESS_HEAP_ENTRY heapEntry = {0}; + + // + // If verifier.dll is loaded - we are running under app verifier == pageheap is enabled + // + hModule = GetModuleHandle( L"verifier.dll" ); + if ( hModule != NULL ) + { + hModule = NULL; + fRet = TRUE; + goto Finished; + } + + // + // Create a heap for calling heapwalk + // otherwise HeapWalk turns off lookasides for a useful heap + // + hHeap = ::HeapCreate( 0, 0, 0 ); + if ( hHeap == NULL ) + { + fRet = FALSE; + goto Finished; + } + + fRet = ::HeapLock( hHeap ); + if ( !fRet ) + { + goto Finished; + } + fLockedHeap = TRUE; + + // + // If HeapWalk is unsupported -> then running page heap + // + fRet = ::HeapWalk( hHeap, &heapEntry ); + if ( !fRet ) + { + if ( GetLastError() == ERROR_INVALID_FUNCTION ) + { + fRet = TRUE; + goto Finished; + } + } + + fRet = FALSE; + +Finished: + + if ( fLockedHeap ) + { + fLockedHeap = FALSE; + DBG_REQUIRE( ::HeapUnlock( hHeap ) ); + } + + if ( hHeap ) + { + DBG_REQUIRE( ::HeapDestroy( hHeap ) ); + hHeap = NULL; + } + + return fRet; +} + +#pragma warning( pop ) diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/acache.h b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/acache.h new file mode 100644 index 0000000000..048df2b507 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/acache.h @@ -0,0 +1,115 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "percpu.h" + +class ALLOC_CACHE_HANDLER +{ +public: + + ALLOC_CACHE_HANDLER( + VOID + ); + + ~ALLOC_CACHE_HANDLER( + VOID + ); + + HRESULT + Initialize( + DWORD cbSize, + LONG nThreshold + ); + + LPVOID + Alloc( + VOID + ); + + VOID + Free( + __in LPVOID pMemory + ); + + +private: + + VOID + CleanupLookaside( + VOID + ); + + DWORD + QueryDepthForAllSLists( + VOID + ); + + LONG m_nThreshold; + DWORD m_cbSize; + + PER_CPU * m_pFreeLists; + + // + // Total heap allocations done over the lifetime. + // Note that this is not interlocked, it is just a hint for debugging. + // + volatile LONG m_nTotal; + + LONG m_nFillPattern; + +public: + + static + HRESULT + StaticInitialize( + VOID + ); + + static + VOID + StaticTerminate( + VOID + ); + + static + BOOL + IsPageheapEnabled(); + +private: + + static LONG sm_nFillPattern; + static HANDLE sm_hHeap; +}; + + +// You can use ALLOC_CACHE_HANDLER as a per-class allocator +// in your C++ classes. Add the following to your class definition: +// +// protected: +// static ALLOC_CACHE_HANDLER* sm_palloc; +// public: +// static void* operator new(size_t s) +// { +// IRTLASSERT(s == sizeof(C)); +// IRTLASSERT(sm_palloc != NULL); +// return sm_palloc->Alloc(); +// } +// static void operator delete(void* pv) +// { +// IRTLASSERT(pv != NULL); +// if (sm_palloc != NULL) +// sm_palloc->Free(pv); +// } +// +// Obviously, you must initialize sm_palloc before you can allocate +// any objects of this class. +// +// Note that if you derive a class from this base class, the derived class +// must also provide its own operator new and operator delete. If not, the +// base class's allocator will be called, but the size of the derived +// object will almost certainly be larger than that of the base object. +// Furthermore, the allocator will not be used for arrays of objects +// (override operator new[] and operator delete[]), but this is a +// harder problem since the allocator works with one fixed size. diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/ahutil.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/ahutil.cpp new file mode 100644 index 0000000000..dae2027f33 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/ahutil.cpp @@ -0,0 +1,1671 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "precomp.h" + +HRESULT +SetElementProperty( + IN IAppHostElement * pElement, + IN CONST WCHAR * szPropName, + IN CONST VARIANT * varPropValue + ) +{ + HRESULT hr = NOERROR; + + CComPtr pPropElement; + + BSTR bstrPropName = SysAllocString( szPropName ); + + if( !bstrPropName ) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR( hr ); + goto exit; + } + + hr = pElement->GetPropertyByName( bstrPropName, + &pPropElement ); + if( FAILED(hr) ) + { + DBGERROR_HR( hr ); + goto exit; + } + + hr = pPropElement->put_Value( *varPropValue ); + if( FAILED(hr) ) + { + DBGERROR_HR( hr ); + goto exit; + } + +exit: + + if( bstrPropName ) + { + SysFreeString( bstrPropName ); + bstrPropName = NULL; + } + + return hr; +} + +HRESULT +SetElementStringProperty( + IN IAppHostElement * pElement, + IN CONST WCHAR * szPropName, + IN CONST WCHAR * szPropValue + ) +{ + HRESULT hr; + VARIANT varPropValue; + VariantInit(&varPropValue); + + hr = VariantAssign(&varPropValue, szPropValue); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + hr = SetElementProperty(pElement, szPropName, &varPropValue); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + +exit: + + VariantClear(&varPropValue); + return hr; +} + +HRESULT +GetElementStringProperty( + IN IAppHostElement * pElement, + IN CONST WCHAR * szPropName, + OUT BSTR * pbstrPropValue + ) +{ + HRESULT hr = S_OK; + BSTR bstrPropName = SysAllocString( szPropName ); + IAppHostProperty* pProperty = NULL; + + *pbstrPropValue = NULL; + + if (!bstrPropName) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR( hr ); + goto exit; + } + + hr = pElement->GetPropertyByName( bstrPropName, &pProperty ); + if (FAILED(hr)) + { + DBGERROR_HR( hr ); + goto exit; + } + + hr = pProperty->get_StringValue( pbstrPropValue ); + if (FAILED(hr)) + { + DBGERROR_HR( hr ); + goto exit; + } + +exit: + + if (pProperty) + { + pProperty->Release(); + } + + if (bstrPropName) + { + SysFreeString( bstrPropName ); + } + + return hr; +} + + +HRESULT +GetElementStringProperty( + IN IAppHostElement * pElement, + IN CONST WCHAR * szPropName, + OUT STRU * pstrPropValue + ) +{ + HRESULT hr = S_OK; + BSTR bstrPropName = SysAllocString( szPropName ); + IAppHostProperty* pProperty = NULL; + BSTR bstrPropValue = NULL; + + if (!bstrPropName) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR( hr ); + goto exit; + } + + hr = pElement->GetPropertyByName( bstrPropName, &pProperty ); + if (FAILED(hr)) + { + DBGERROR_HR( hr ); + goto exit; + } + + hr = pProperty->get_StringValue( &bstrPropValue ); + if (FAILED(hr)) + { + DBGERROR_HR( hr ); + goto exit; + } + + hr = pstrPropValue->Copy(bstrPropValue); + if (FAILED(hr)) + { + DBGERROR_HR( hr ); + goto exit; + } + +exit: + + if (pProperty) + { + pProperty->Release(); + } + + if (bstrPropValue) + { + SysFreeString( bstrPropValue ); + } + + if (bstrPropName) + { + SysFreeString( bstrPropName ); + } + + return hr; +} + +HRESULT +GetElementChildByName( + IN IAppHostElement * pElement, + IN LPCWSTR pszElementName, + OUT IAppHostElement ** ppChildElement +) +{ + BSTR bstrElementName = SysAllocString(pszElementName); + if (bstrElementName == NULL) + { + return E_OUTOFMEMORY; + } + HRESULT hr = pElement->GetElementByName(bstrElementName, + ppChildElement); + SysFreeString(bstrElementName); + return hr; +} + +HRESULT +GetElementBoolProperty( + IN IAppHostElement * pElement, + IN LPCWSTR pszPropertyName, + OUT bool * pBool +) +{ + BOOL fValue; + HRESULT hr = GetElementBoolProperty(pElement, + pszPropertyName, + &fValue); + if (SUCCEEDED(hr)) + { + *pBool = !!fValue; + } + return hr; +} + +HRESULT +GetElementBoolProperty( + IN IAppHostElement * pElement, + IN LPCWSTR pszPropertyName, + OUT BOOL * pBool +) +{ + HRESULT hr = S_OK; + BSTR bstrPropertyName = NULL; + IAppHostProperty * pProperty = NULL; + VARIANT varValue; + + VariantInit( &varValue ); + + bstrPropertyName = SysAllocString( pszPropertyName ); + if ( bstrPropertyName == NULL ) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR(hr); + goto exit; + } + + // Now ask for the property and if it succeeds it is returned directly back. + hr = pElement->GetPropertyByName( bstrPropertyName, &pProperty ); + if ( FAILED ( hr ) ) + { + goto exit; + } + + // Now let's get the property and then extract it from the Variant. + hr = pProperty->get_Value( &varValue ); + if ( FAILED ( hr ) ) + { + goto exit; + } + + hr = VariantChangeType( &varValue, &varValue, 0, VT_BOOL ); + if ( FAILED ( hr ) ) + { + goto exit; + } + + // extract the value + *pBool = ( V_BOOL( &varValue ) == VARIANT_TRUE ); + +exit: + + VariantClear( &varValue ); + + if ( bstrPropertyName != NULL ) + { + SysFreeString( bstrPropertyName ); + bstrPropertyName = NULL; + } + + if ( pProperty != NULL ) + { + pProperty->Release(); + pProperty = NULL; + } + + return hr; + +} + +HRESULT +GetElementDWORDProperty( + IN IAppHostElement * pSitesCollectionEntry, + IN LPCWSTR pwszName, + OUT DWORD * pdwValue +) +{ + HRESULT hr = S_OK; + IAppHostProperty * pProperty = NULL; + BSTR bstrName = NULL; + VARIANT varValue; + + VariantInit( &varValue ); + + bstrName = SysAllocString( pwszName ); + if ( bstrName == NULL ) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR(hr); + goto error; + } + + hr = pSitesCollectionEntry->GetPropertyByName( bstrName, + &pProperty ); + if ( FAILED ( hr ) ) + { + goto error; + } + + hr = pProperty->get_Value( &varValue ); + if ( FAILED ( hr ) ) + { + goto error; + } + + hr = VariantChangeType( &varValue, &varValue, 0, VT_UI4 ); + if ( FAILED ( hr ) ) + { + goto error; + } + + // extract the value + *pdwValue = varValue.ulVal; + +error: + + VariantClear( &varValue ); + + if ( pProperty != NULL ) + { + pProperty->Release(); + pProperty = NULL; + } + + if ( bstrName != NULL ) + { + SysFreeString( bstrName ); + bstrName = NULL; + } + + return hr; +} + +HRESULT +GetElementLONGLONGProperty( + IN IAppHostElement * pSitesCollectionEntry, + IN LPCWSTR pwszName, + OUT LONGLONG * pllValue +) +{ + HRESULT hr = S_OK; + IAppHostProperty * pProperty = NULL; + BSTR bstrName = NULL; + VARIANT varValue; + + VariantInit( &varValue ); + + bstrName = SysAllocString( pwszName ); + if ( bstrName == NULL ) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR(hr); + goto error; + } + + hr = pSitesCollectionEntry->GetPropertyByName( bstrName, + &pProperty ); + if ( FAILED ( hr ) ) + { + goto error; + } + + hr = pProperty->get_Value( &varValue ); + if ( FAILED ( hr ) ) + { + goto error; + } + + hr = VariantChangeType( &varValue, &varValue, 0, VT_I8 ); + if ( FAILED ( hr ) ) + { + goto error; + } + + // extract the value + *pllValue = varValue.ulVal; + +error: + + VariantClear( &varValue ); + + if ( pProperty != NULL ) + { + pProperty->Release(); + pProperty = NULL; + } + + if ( bstrName != NULL ) + { + SysFreeString( bstrName ); + bstrName = NULL; + } + + return hr; +} + +HRESULT +GetElementRawTimeSpanProperty( + IN IAppHostElement * pElement, + IN LPCWSTR pszPropertyName, + OUT ULONGLONG * pulonglong +) +{ + HRESULT hr = S_OK; + BSTR bstrPropertyName = NULL; + IAppHostProperty * pProperty = NULL; + VARIANT varValue; + + VariantInit( &varValue ); + + bstrPropertyName = SysAllocString( pszPropertyName ); + if ( bstrPropertyName == NULL ) + { + hr = HRESULT_FROM_WIN32( ERROR_NOT_ENOUGH_MEMORY ); + goto Finished; + } + + // Now ask for the property and if it succeeds it is returned directly back + hr = pElement->GetPropertyByName( bstrPropertyName, &pProperty ); + if ( FAILED ( hr ) ) + { + goto Finished; + } + + // Now let's get the property and then extract it from the Variant. + hr = pProperty->get_Value( &varValue ); + if ( FAILED ( hr ) ) + { + goto Finished; + } + + hr = VariantChangeType( &varValue, &varValue, 0, VT_UI8 ); + if ( FAILED ( hr ) ) + { + goto Finished; + } + + // extract the value + *pulonglong = varValue.ullVal; + + +Finished: + + VariantClear( &varValue ); + + if ( bstrPropertyName != NULL ) + { + SysFreeString( bstrPropertyName ); + bstrPropertyName = NULL; + } + + if ( pProperty != NULL ) + { + pProperty->Release(); + pProperty = NULL; + } + + return hr; + +} // end of Config_GetRawTimeSpanProperty + +HRESULT +DeleteElementFromCollection( + IAppHostElementCollection *pCollection, + CONST WCHAR * szKeyName, + CONST WCHAR * szKeyValue, + ULONG BehaviorFlags, + BOOL * pfDeleted + ) +{ + HRESULT hr = NOERROR; + ULONG index; + + VARIANT varIndex; + VariantInit( &varIndex ); + + *pfDeleted = FALSE; + + hr = FindElementInCollection( + pCollection, + szKeyName, + szKeyValue, + BehaviorFlags, + &index + ); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + if (hr == S_FALSE) + { + // + // Not found. + // + + goto exit; + } + + varIndex.vt = VT_UI4; + varIndex.ulVal = index; + + hr = pCollection->DeleteElement( varIndex ); + + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto exit; + } + + *pfDeleted = TRUE; + +exit: + + return hr; +} + +HRESULT +DeleteAllElementsFromCollection( + IAppHostElementCollection *pCollection, + CONST WCHAR * szKeyName, + CONST WCHAR * szKeyValue, + ULONG BehaviorFlags, + UINT * pNumDeleted + ) +{ + HRESULT hr = S_OK; + UINT numDeleted = 0; + BOOL fDeleted = TRUE; + + while (fDeleted) + { + hr = DeleteElementFromCollection( + pCollection, + szKeyName, + szKeyValue, + BehaviorFlags, + &fDeleted + ); + + if (hr == S_FALSE) + { + hr = S_OK; + break; + } + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + break; + } + + if (fDeleted) + { + numDeleted++; + } + } + + *pNumDeleted = numDeleted; + return hr; +} + +BOOL +FindCompareCaseSensitive( + CONST WCHAR * szLookupValue, + CONST WCHAR * szKeyValue + ) +{ + return !wcscmp(szLookupValue, szKeyValue); +} + +BOOL +FindCompareCaseInsensitive( + CONST WCHAR * szLookupValue, + CONST WCHAR * szKeyValue + ) +{ + return !_wcsicmp(szLookupValue, szKeyValue); +} + +typedef +BOOL +(*PFN_FIND_COMPARE_PROC)( + CONST WCHAR *szLookupValue, + CONST WCHAR *szKeyValue + ); + +HRESULT +FindElementInCollection( + IAppHostElementCollection *pCollection, + CONST WCHAR * szKeyName, + CONST WCHAR * szKeyValue, + ULONG BehaviorFlags, + OUT ULONG * pIndex + ) +{ + HRESULT hr = NOERROR; + + CComPtr pElement; + CComPtr pKeyProperty; + + VARIANT varIndex; + VariantInit( &varIndex ); + + VARIANT varKeyValue; + VariantInit( &varKeyValue ); + + DWORD count; + DWORD i; + + BSTR bstrKeyName = NULL; + PFN_FIND_COMPARE_PROC compareProc; + + compareProc = (BehaviorFlags & FIND_ELEMENT_CASE_INSENSITIVE) + ? &FindCompareCaseInsensitive + : &FindCompareCaseSensitive; + + bstrKeyName = SysAllocString( szKeyName ); + if( !bstrKeyName ) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR(hr); + goto exit; + } + + hr = pCollection->get_Count( &count ); + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto exit; + } + + for( i = 0; i < count; i++ ) + { + varIndex.vt = VT_UI4; + varIndex.ulVal = i; + + hr = pCollection->get_Item( varIndex, + &pElement ); + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto tryNext; + } + + hr = pElement->GetPropertyByName( bstrKeyName, + &pKeyProperty ); + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto tryNext; + } + + hr = pKeyProperty->get_Value( &varKeyValue ); + + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto tryNext; + } + + if ((compareProc)(szKeyValue, varKeyValue.bstrVal)) + { + *pIndex = i; + break; + } + +tryNext: + + pElement.Release(); + pKeyProperty.Release(); + + VariantClear( &varKeyValue ); + } + + if (i >= count) + { + hr = S_FALSE; + } + +exit: + + SysFreeString( bstrKeyName ); + VariantClear( &varKeyValue ); + + return hr; +} + +HRESULT +VariantAssign( + IN OUT VARIANT * pv, + IN CONST WCHAR * sz + ) +{ + if( !pv || !sz ) + { + return E_INVALIDARG; + } + + HRESULT hr = NOERROR; + + BSTR bstr = SysAllocString( sz ); + if( !bstr ) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR( hr ); + goto exit; + } + + hr = VariantClear( pv ); + if( FAILED(hr) ) + { + DBGERROR_HR( hr ); + goto exit; + } + + pv->vt = VT_BSTR; + pv->bstrVal = bstr; + bstr = NULL; + +exit: + + SysFreeString( bstr ); + + return hr; +} + +HRESULT +GetLocationFromFile( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + IN CONST WCHAR * szLocationPath, + OUT IAppHostConfigLocation ** ppLocation, + OUT BOOL * pFound + ) +{ + HRESULT hr = NOERROR; + + CComPtr pLocationCollection; + CComPtr pLocation; + + BSTR bstrLocationPath = NULL; + + *ppLocation = NULL; + *pFound = FALSE; + + hr = GetLocationCollection( pAdminMgr, + szConfigPath, + &pLocationCollection ); + + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto exit; + } + + DWORD count; + DWORD i; + VARIANT varIndex; + VariantInit( &varIndex ); + + hr = pLocationCollection->get_Count( &count ); + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto exit; + } + + for( i = 0; i < count; i++ ) + { + varIndex.vt = VT_UI4; + varIndex.ulVal = i; + + hr = pLocationCollection->get_Item( varIndex, + &pLocation ); + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto exit; + } + + hr = pLocation->get_Path( &bstrLocationPath ); + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto exit; + } + + if( 0 == wcscmp ( szLocationPath, bstrLocationPath ) ) + { + *pFound = TRUE; + *ppLocation = pLocation.Detach(); + break; + } + + + pLocation.Release(); + + SysFreeString( bstrLocationPath ); + bstrLocationPath = NULL; + } + +exit: + + SysFreeString( bstrLocationPath ); + + return hr; +} + +HRESULT +GetSectionFromLocation( + IN IAppHostConfigLocation * pLocation, + IN CONST WCHAR * szSectionName, + OUT IAppHostElement ** ppSectionElement, + OUT BOOL * pFound + ) +{ + HRESULT hr = NOERROR; + + CComPtr pSectionElement; + + DWORD count; + DWORD i; + + VARIANT varIndex; + VariantInit( &varIndex ); + + BSTR bstrSectionName = NULL; + + *pFound = FALSE; + *ppSectionElement = NULL; + + hr = pLocation->get_Count( &count ); + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto exit; + } + + for( i = 0; i < count; i++ ) + { + varIndex.vt = VT_UI4; + varIndex.ulVal = i; + + + hr = pLocation->get_Item( varIndex, + &pSectionElement ); + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto exit; + } + + hr = pSectionElement->get_Name( &bstrSectionName ); + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto exit; + } + + if( 0 == wcscmp ( szSectionName, bstrSectionName ) ) + { + *pFound = TRUE; + *ppSectionElement = pSectionElement.Detach(); + break; + } + + pSectionElement.Release(); + + SysFreeString( bstrSectionName ); + bstrSectionName = NULL; + } + +exit: + + SysFreeString( bstrSectionName ); + + return hr; +} + + +HRESULT +GetAdminElement( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + IN CONST WCHAR * szElementName, + OUT IAppHostElement ** pElement +) +{ + HRESULT hr = S_OK; + BSTR bstrConfigPath = NULL; + BSTR bstrElementName = NULL; + + bstrConfigPath = SysAllocString(szConfigPath); + bstrElementName = SysAllocString(szElementName); + + if (bstrConfigPath == NULL || bstrElementName == NULL) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR(hr); + goto exit; + } + + hr = pAdminMgr->GetAdminSection( bstrElementName, + bstrConfigPath, + pElement ); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + +exit: + + if ( bstrElementName != NULL ) + { + SysFreeString(bstrElementName); + bstrElementName = NULL; + } + if ( bstrConfigPath != NULL ) + { + SysFreeString(bstrConfigPath); + bstrConfigPath = NULL; + } + + return hr; +} + + +HRESULT +ClearAdminElement( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + IN CONST WCHAR * szElementName + ) +{ + HRESULT hr; + CComPtr pElement; + + hr = GetAdminElement( + pAdminMgr, + szConfigPath, + szElementName, + &pElement + ); + + if (FAILED(hr)) + { + if (hr == HRESULT_FROM_WIN32(ERROR_NOT_FOUND)) + { + hr = S_OK; + } + else + { + DBGERROR_HR(hr); + } + + goto exit; + } + + hr = pElement->Clear(); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + +exit: + + return hr; +} + + +HRESULT +ClearElementFromAllSites( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + IN CONST WCHAR * szElementName + ) +{ + HRESULT hr; + CComPtr pSitesCollection; + CComPtr pSiteElement; + CComPtr pChildCollection; + ENUM_INDEX index; + BOOL found; + + // + // Enumerate the sites, remove the specified elements. + // + + hr = GetSitesCollection( + pAdminMgr, + szConfigPath, + &pSitesCollection + ); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + for (hr = FindFirstElement(pSitesCollection, &index, &pSiteElement) ; + SUCCEEDED(hr) ; + hr = FindNextElement(pSitesCollection, &index, &pSiteElement)) + { + if (hr == S_FALSE) + { + hr = S_OK; + break; + } + + hr = pSiteElement->get_ChildElements(&pChildCollection); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + if (pChildCollection) + { + hr = ClearChildElementsByName( + pChildCollection, + szElementName, + &found + ); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + } + + pSiteElement.Release(); + } + +exit: + + return hr; + +} + + +HRESULT +ClearElementFromAllLocations( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + IN CONST WCHAR * szElementName + ) +{ + HRESULT hr; + CComPtr pLocationCollection; + CComPtr pLocation; + CComPtr pChildCollection; + ENUM_INDEX index; + + // + // Enum the tags, remove the specified elements. + // + + hr = GetLocationCollection( + pAdminMgr, + szConfigPath, + &pLocationCollection + ); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + for (hr = FindFirstLocation(pLocationCollection, &index, &pLocation) ; + SUCCEEDED(hr) ; + hr = FindNextLocation(pLocationCollection, &index, &pLocation)) + { + if (hr == S_FALSE) + { + hr = S_OK; + break; + } + + hr = ClearLocationElements(pLocation, szElementName); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + pLocation.Release(); + } + +exit: + + return hr; + +} + +HRESULT +ClearLocationElements( + IN IAppHostConfigLocation * pLocation, + IN CONST WCHAR * szElementName + ) +{ + HRESULT hr; + CComPtr pElement; + ENUM_INDEX index; + BOOL matched; + + for (hr = FindFirstLocationElement(pLocation, &index, &pElement) ; + SUCCEEDED(hr) ; + hr = FindNextLocationElement(pLocation, &index, &pElement)) + { + if (hr == S_FALSE) + { + hr = S_OK; + break; + } + + hr = CompareElementName(pElement, szElementName, &matched); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + if (matched) + { + pElement->Clear(); + } + + pElement.Release(); + } + +exit: + + return hr; +} + +HRESULT +CompareElementName( + IN IAppHostElement * pElement, + IN CONST WCHAR * szNameToMatch, + OUT BOOL * pMatched + ) +{ + HRESULT hr; + BSTR bstrElementName = NULL; + + *pMatched = FALSE; // until proven otherwise + + hr = pElement->get_Name(&bstrElementName); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + if( 0 == wcscmp ( szNameToMatch, bstrElementName ) ) + { + *pMatched = TRUE; + } + +exit: + + SysFreeString(bstrElementName); + return hr; +} + + +HRESULT +ClearChildElementsByName( + IN IAppHostChildElementCollection * pCollection, + IN CONST WCHAR * szElementName, + OUT BOOL * pFound + ) +{ + HRESULT hr; + CComPtr pElement; + ENUM_INDEX index; + BOOL matched; + + *pFound = FALSE; + + for (hr = FindFirstChildElement(pCollection, &index, &pElement) ; + SUCCEEDED(hr) ; + hr = FindNextChildElement(pCollection, &index, &pElement)) + { + if (hr == S_FALSE) + { + hr = S_OK; + break; + } + + hr = CompareElementName(pElement, szElementName, &matched); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + if (matched) + { + hr = pElement->Clear(); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + *pFound = TRUE; + } + + pElement.Release(); + } + +exit: + + return hr; +} + + +HRESULT +GetSitesCollection( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + OUT IAppHostElementCollection ** pSitesCollection + ) +{ + HRESULT hr; + CComPtr pSitesElement; + BSTR bstrConfigPath; + BSTR bstrSitesSectionName; + + bstrConfigPath = SysAllocString(szConfigPath); + bstrSitesSectionName = SysAllocString(L"system.applicationHost/sites"); + *pSitesCollection = NULL; + + if (bstrConfigPath == NULL || bstrSitesSectionName == NULL) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR(hr); + goto exit; + } + + // + // Chase down the sites collection. + // + + hr = pAdminMgr->GetAdminSection( bstrSitesSectionName, + bstrConfigPath, + &pSitesElement ); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + hr = pSitesElement->get_Collection(pSitesCollection); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + +exit: + + SysFreeString(bstrSitesSectionName); + SysFreeString(bstrConfigPath); + return hr; +} + + +HRESULT +GetLocationCollection( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + OUT IAppHostConfigLocationCollection ** pLocationCollection + ) +{ + HRESULT hr; + BSTR bstrConfigPath; + CComPtr pConfigMgr; + CComPtr pConfigFile; + + bstrConfigPath = SysAllocString(szConfigPath); + *pLocationCollection = NULL; + + if (bstrConfigPath == NULL) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR(hr); + goto exit; + } + + hr = pAdminMgr->get_ConfigManager(&pConfigMgr); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + hr = pConfigMgr->GetConfigFile(bstrConfigPath, &pConfigFile); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + + hr = pConfigFile->get_Locations(pLocationCollection); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + goto exit; + } + +exit: + + SysFreeString(bstrConfigPath); + return hr; +} + + +HRESULT +FindFirstElement( + IN IAppHostElementCollection * pCollection, + OUT ENUM_INDEX * pIndex, + OUT IAppHostElement ** pElement + ) +{ + HRESULT hr; + + hr = pCollection->get_Count(&pIndex->Count); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + return hr; + } + + VariantInit(&pIndex->Index); + pIndex->Index.vt = VT_UI4; + pIndex->Index.ulVal = 0; + + return FindNextElement(pCollection, pIndex, pElement); +} + +HRESULT +FindNextElement( + IN IAppHostElementCollection * pCollection, + IN OUT ENUM_INDEX * pIndex, + OUT IAppHostElement ** pElement + ) +{ + HRESULT hr; + + *pElement = NULL; + + if (pIndex->Index.ulVal >= pIndex->Count) + { + return S_FALSE; + } + + hr = pCollection->get_Item(pIndex->Index, pElement); + + if (SUCCEEDED(hr)) + { + pIndex->Index.ulVal++; + } + + return hr; +} + +HRESULT +FindFirstChildElement( + IN IAppHostChildElementCollection * pCollection, + OUT ENUM_INDEX * pIndex, + OUT IAppHostElement ** pElement + ) +{ + HRESULT hr; + + hr = pCollection->get_Count(&pIndex->Count); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + return hr; + } + + VariantInit(&pIndex->Index); + pIndex->Index.vt = VT_UI4; + pIndex->Index.ulVal = 0; + + return FindNextChildElement(pCollection, pIndex, pElement); +} + +HRESULT +FindNextChildElement( + IN IAppHostChildElementCollection * pCollection, + IN OUT ENUM_INDEX * pIndex, + OUT IAppHostElement ** pElement + ) +{ + HRESULT hr; + + *pElement = NULL; + + if (pIndex->Index.ulVal >= pIndex->Count) + { + return S_FALSE; + } + + hr = pCollection->get_Item(pIndex->Index, pElement); + + if (SUCCEEDED(hr)) + { + pIndex->Index.ulVal++; + } + + return hr; +} + +HRESULT +FindFirstLocation( + IN IAppHostConfigLocationCollection * pCollection, + OUT ENUM_INDEX * pIndex, + OUT IAppHostConfigLocation ** pLocation + ) +{ + HRESULT hr; + + hr = pCollection->get_Count(&pIndex->Count); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + return hr; + } + + VariantInit(&pIndex->Index); + pIndex->Index.vt = VT_UI4; + pIndex->Index.ulVal = 0; + + return FindNextLocation(pCollection, pIndex, pLocation); +} + +HRESULT +FindNextLocation( + IN IAppHostConfigLocationCollection * pCollection, + IN OUT ENUM_INDEX * pIndex, + OUT IAppHostConfigLocation ** pLocation + ) +{ + HRESULT hr; + + *pLocation = NULL; + + if (pIndex->Index.ulVal >= pIndex->Count) + { + return S_FALSE; + } + + hr = pCollection->get_Item(pIndex->Index, pLocation); + + if (SUCCEEDED(hr)) + { + pIndex->Index.ulVal++; + } + + return hr; +} + +HRESULT +FindFirstLocationElement( + IN IAppHostConfigLocation * pLocation, + OUT ENUM_INDEX * pIndex, + OUT IAppHostElement ** pElement + ) +{ + HRESULT hr; + + hr = pLocation->get_Count(&pIndex->Count); + + if (FAILED(hr)) + { + DBGERROR_HR(hr); + return hr; + } + + VariantInit(&pIndex->Index); + pIndex->Index.vt = VT_UI4; + pIndex->Index.ulVal = 0; + + return FindNextLocationElement(pLocation, pIndex, pElement); +} + +HRESULT +FindNextLocationElement( + IN IAppHostConfigLocation * pLocation, + IN OUT ENUM_INDEX * pIndex, + OUT IAppHostElement ** pElement + ) +{ + HRESULT hr; + + *pElement = NULL; + + if (pIndex->Index.ulVal >= pIndex->Count) + { + return S_FALSE; + } + + hr = pLocation->get_Item(pIndex->Index, pElement); + + if (SUCCEEDED(hr)) + { + pIndex->Index.ulVal++; + } + + return hr; +} + +HRESULT +GetSharedConfigEnabled( + BOOL * pfIsSharedConfig +) +/*++ + +Routine Description: + Search the configuration for the shared configuration property. + +Arguments: + + pfIsSharedConfig - true if shared configuration is enabled + +Return Value: + HRESULT + +--*/ +{ + HRESULT hr = S_OK; + IAppHostAdminManager *pAdminManager = NULL; + + BSTR bstrSectionName = NULL; + BSTR bstrConfigPath = NULL; + + IAppHostElement * pConfigRedirSection = NULL; + + + bstrSectionName = SysAllocString( L"configurationRedirection" ); + + if ( bstrSectionName == NULL ) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR(hr); + goto exit; + } + + bstrConfigPath = SysAllocString( L"MACHINE/REDIRECTION" ); + if ( bstrConfigPath == NULL ) + { + hr = E_OUTOFMEMORY; + DBGERROR_HR(hr); + goto exit; + } + + hr = CoCreateInstance( CLSID_AppHostAdminManager, + NULL, + CLSCTX_INPROC_SERVER, + IID_IAppHostAdminManager, + (VOID **)&pAdminManager ); + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto exit; + } + + hr = pAdminManager->GetAdminSection( bstrSectionName, + bstrConfigPath, + &pConfigRedirSection ); + if( FAILED(hr) ) + { + DBGERROR_HR(hr); + goto exit; + } + + hr = GetElementBoolProperty( pConfigRedirSection, + L"enabled", + pfIsSharedConfig ); + + if ( FAILED( hr ) ) + { + DBGERROR_HR(hr); + goto exit; + } + + pConfigRedirSection->Release(); + pConfigRedirSection = NULL; + + +exit: + + // + // dump config exception to setup log file (if available) + // + + if ( pConfigRedirSection != NULL ) + { + pConfigRedirSection->Release(); + } + + if ( pAdminManager != NULL ) + { + pAdminManager->Release(); + } + + if ( bstrConfigPath != NULL ) + { + SysFreeString( bstrConfigPath ); + } + + if ( bstrSectionName != NULL ) + { + SysFreeString( bstrSectionName ); + } + + return hr; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/ahutil.h b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/ahutil.h new file mode 100644 index 0000000000..d17dc7be30 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/ahutil.h @@ -0,0 +1,259 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once +#include "stringu.h" +#include + +HRESULT +SetElementProperty( + IN IAppHostElement * pElement, + IN CONST WCHAR * szPropName, + IN CONST VARIANT * varPropValue + ); + +HRESULT +SetElementStringProperty( + IN IAppHostElement * pElement, + IN CONST WCHAR * szPropName, + IN CONST WCHAR * szPropValue + ); + +HRESULT +GetElementStringProperty( + IN IAppHostElement * pElement, + IN CONST WCHAR * szPropName, + OUT BSTR * pbstrPropValue + ); + +HRESULT +GetElementStringProperty( + IN IAppHostElement * pElement, + IN CONST WCHAR * szPropName, + OUT STRU * pstrPropValue + ); + +HRESULT +GetElementBoolProperty( + IN IAppHostElement * pElement, + IN LPCWSTR pszPropertyName, + OUT BOOL * pBool + ); + +HRESULT +GetElementBoolProperty( + IN IAppHostElement * pElement, + IN LPCWSTR pszPropertyName, + OUT bool * pBool + ); + +HRESULT +GetElementChildByName( + IN IAppHostElement * pElement, + IN LPCWSTR pszElementName, + OUT IAppHostElement ** ppChildElement + ); + +HRESULT +GetElementDWORDProperty( + IN IAppHostElement * pElement, + IN LPCWSTR pszPropertyName, + OUT DWORD * pdwValue + ); + +HRESULT +GetElementLONGLONGProperty( + IN IAppHostElement * pElement, + IN LPCWSTR pszPropertyName, + OUT LONGLONG * pllValue +); + + +HRESULT +GetElementRawTimeSpanProperty( + IN IAppHostElement * pElement, + IN LPCWSTR pszPropertyName, + OUT ULONGLONG * pulonglong + ); + +#define FIND_ELEMENT_CASE_SENSITIVE 0x00000000 +#define FIND_ELEMENT_CASE_INSENSITIVE 0x00000001 + +HRESULT +DeleteElementFromCollection( + IAppHostElementCollection *pCollection, + CONST WCHAR * szKeyName, + CONST WCHAR * szKeyValue, + ULONG BehaviorFlags, + BOOL * pfDeleted + ); + +HRESULT +DeleteAllElementsFromCollection( + IAppHostElementCollection *pCollection, + CONST WCHAR * szKeyName, + CONST WCHAR * szKeyValue, + ULONG BehaviorFlags, + UINT * pNumDeleted + ); + +HRESULT +FindElementInCollection( + IAppHostElementCollection *pCollection, + CONST WCHAR * szKeyName, + CONST WCHAR * szKeyValue, + ULONG BehaviorFlags, + OUT ULONG * pIndex + ); + +HRESULT +VariantAssign( + IN OUT VARIANT * pv, + IN CONST WCHAR * sz + ); + +HRESULT +GetLocationFromFile( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + IN CONST WCHAR * szLocationPath, + OUT IAppHostConfigLocation ** ppLocation, + OUT BOOL * pFound + ); + +HRESULT +GetSectionFromLocation( + IN IAppHostConfigLocation * pLocation, + IN CONST WCHAR * szSectionName, + OUT IAppHostElement ** ppSectionElement, + OUT BOOL * pFound + ); + +HRESULT +GetAdminElement( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + IN CONST WCHAR * szElementName, + OUT IAppHostElement ** pElement + ); + +HRESULT +ClearAdminElement( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + IN CONST WCHAR * szElementName + ); + +HRESULT +ClearElementFromAllSites( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + IN CONST WCHAR * szElementName + ); + +HRESULT +ClearElementFromAllLocations( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + IN CONST WCHAR * szElementName + ); + +HRESULT +ClearLocationElements( + IN IAppHostConfigLocation * pLocation, + IN CONST WCHAR * szElementName + ); + +HRESULT +CompareElementName( + IN IAppHostElement * pElement, + IN CONST WCHAR * szNameToMatch, + OUT BOOL * pMatched + ); + +HRESULT +ClearChildElementsByName( + IN IAppHostChildElementCollection * pCollection, + IN CONST WCHAR * szElementName, + OUT BOOL * pFound + ); + +HRESULT +GetSitesCollection( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + OUT IAppHostElementCollection ** pSitesCollection + ); + +HRESULT +GetLocationCollection( + IN IAppHostAdminManager * pAdminMgr, + IN CONST WCHAR * szConfigPath, + OUT IAppHostConfigLocationCollection ** pLocationCollection + ); + +struct ENUM_INDEX +{ + VARIANT Index; + ULONG Count; +}; + +HRESULT +FindFirstElement( + IN IAppHostElementCollection * pCollection, + OUT ENUM_INDEX * pIndex, + OUT IAppHostElement ** pElement + ); + +HRESULT +FindNextElement( + IN IAppHostElementCollection * pCollection, + IN OUT ENUM_INDEX * pIndex, + OUT IAppHostElement ** pElement + ); + +HRESULT +FindFirstChildElement( + IN IAppHostChildElementCollection * pCollection, + OUT ENUM_INDEX * pIndex, + OUT IAppHostElement ** pElement + ); + +HRESULT +FindNextChildElement( + IN IAppHostChildElementCollection * pCollection, + IN OUT ENUM_INDEX * pIndex, + OUT IAppHostElement ** pElement + ); + +HRESULT +FindFirstLocation( + IN IAppHostConfigLocationCollection * pCollection, + OUT ENUM_INDEX * pIndex, + OUT IAppHostConfigLocation ** pLocation + ); + +HRESULT +FindNextLocation( + IN IAppHostConfigLocationCollection * pCollection, + IN OUT ENUM_INDEX * pIndex, + OUT IAppHostConfigLocation ** pLocation + ); + +HRESULT +FindFirstLocationElement( + IN IAppHostConfigLocation * pLocation, + OUT ENUM_INDEX * pIndex, + OUT IAppHostElement ** pElement + ); + +HRESULT +FindNextLocationElement( + IN IAppHostConfigLocation * pLocation, + IN OUT ENUM_INDEX * pIndex, + OUT IAppHostElement ** pElement + ); + +HRESULT GetSharedConfigEnabled( + BOOL * pfIsSharedConfig +); diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/base64.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/base64.cpp new file mode 100644 index 0000000000..b8b6a0bf74 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/base64.cpp @@ -0,0 +1,482 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "precomp.h" + +DWORD +Base64Encode( + __in_bcount(cbDecodedBufferSize) VOID * pDecodedBuffer, + IN DWORD cbDecodedBufferSize, + __out_ecount_opt(cchEncodedStringSize) PWSTR pszEncodedString, + IN DWORD cchEncodedStringSize, + __out_opt DWORD * pcchEncoded + ) +/*++ + +Routine Description: + + Decode a base64-encoded string. + +Arguments: + + pDecodedBuffer (IN) - buffer to encode. + cbDecodedBufferSize (IN) - size of buffer to encode. + cchEncodedStringSize (IN) - size of the buffer for the encoded string. + pszEncodedString (OUT) = the encoded string. + pcchEncoded (OUT) - size in characters of the encoded string. + +Return Values: + + 0 - success. + E_OUTOFMEMORY + +--*/ +{ + static WCHAR rgchEncodeTable[64] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' + }; + + DWORD ib; + DWORD ich; + DWORD cchEncoded; + BYTE b0, b1, b2; + BYTE * pbDecodedBuffer = (BYTE *) pDecodedBuffer; + + // Calculate encoded string size. + cchEncoded = 1 + (cbDecodedBufferSize + 2) / 3 * 4; + + if (NULL != pcchEncoded) { + *pcchEncoded = cchEncoded; + } + + if (cchEncodedStringSize == 0 && pszEncodedString == NULL) { + return ERROR_SUCCESS; + } + + if (cchEncodedStringSize < cchEncoded) { + // Given buffer is too small to hold encoded string. + return ERROR_INSUFFICIENT_BUFFER; + } + + // Encode data byte triplets into four-byte clusters. + ib = ich = 0; + while (ib < cbDecodedBufferSize) { + b0 = pbDecodedBuffer[ib++]; + b1 = (ib < cbDecodedBufferSize) ? pbDecodedBuffer[ib++] : 0; + b2 = (ib < cbDecodedBufferSize) ? pbDecodedBuffer[ib++] : 0; + + // + // The checks below for buffer overflow seems redundant to me. + // But it's the only way I can find to keep OACR quiet so it + // will have to do. + // + + pszEncodedString[ich++] = rgchEncodeTable[b0 >> 2]; + if ( ich >= cchEncodedStringSize ) + { + DBG_ASSERT( FALSE ); + return ERROR_BUFFER_OVERFLOW; + } + + pszEncodedString[ich++] = rgchEncodeTable[((b0 << 4) & 0x30) | ((b1 >> 4) & 0x0f)]; + if ( ich >= cchEncodedStringSize ) + { + DBG_ASSERT( FALSE ); + return ERROR_BUFFER_OVERFLOW; + } + + pszEncodedString[ich++] = rgchEncodeTable[((b1 << 2) & 0x3c) | ((b2 >> 6) & 0x03)]; + if ( ich >= cchEncodedStringSize ) + { + DBG_ASSERT( FALSE ); + return ERROR_BUFFER_OVERFLOW; + } + + pszEncodedString[ich++] = rgchEncodeTable[b2 & 0x3f]; + if ( ich >= cchEncodedStringSize ) + { + DBG_ASSERT( FALSE ); + return ERROR_BUFFER_OVERFLOW; + } + } + + // Pad the last cluster as necessary to indicate the number of data bytes + // it represents. + switch (cbDecodedBufferSize % 3) { + case 0: + break; + case 1: + pszEncodedString[ich - 2] = '='; + __fallthrough; + case 2: + pszEncodedString[ich - 1] = '='; + break; + } + + // Null-terminate the encoded string. + pszEncodedString[ich++] = '\0'; + + DBG_ASSERT(ich == cchEncoded); + + return ERROR_SUCCESS; +} + + +DWORD +Base64Decode( + __in PCWSTR pszEncodedString, + __out_opt VOID * pDecodeBuffer, + __in DWORD cbDecodeBufferSize, + __out_opt DWORD * pcbDecoded + ) +/*++ + +Routine Description: + + Decode a base64-encoded string. + +Arguments: + + pszEncodedString (IN) - base64-encoded string to decode. + cbDecodeBufferSize (IN) - size in bytes of the decode buffer. + pbDecodeBuffer (OUT) - holds the decoded data. + pcbDecoded (OUT) - number of data bytes in the decoded data (if success or + STATUS_BUFFER_TOO_SMALL). + +Return Values: + + 0 - success. + E_OUTOFMEMORY + E_INVALIDARG + +--*/ +{ +#define NA (255) +#define DECODE(x) (((ULONG)(x) < sizeof(rgbDecodeTable)) ? rgbDecodeTable[x] : NA) + + static BYTE rgbDecodeTable[128] = { + NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, // 0-15 + NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, // 16-31 + NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 62, NA, NA, NA, 63, // 32-47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, NA, NA, NA, 0, NA, NA, // 48-63 + NA, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 64-79 + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, NA, NA, NA, NA, NA, // 80-95 + NA, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 96-111 + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, NA, NA, NA, NA, NA, // 112-127 + }; + + DWORD cbDecoded; + DWORD cchEncodedSize; + DWORD ich; + DWORD ib; + BYTE b0, b1, b2, b3; + BYTE * pbDecodeBuffer = (BYTE *) pDecodeBuffer; + + cchEncodedSize = (DWORD)wcslen(pszEncodedString); + if (NULL != pcbDecoded) { + *pcbDecoded = 0; + } + + if ((0 == cchEncodedSize) || (0 != (cchEncodedSize % 4))) { + // Input string is not sized correctly to be base64. + return ERROR_INVALID_PARAMETER; + } + + // Calculate decoded buffer size. + cbDecoded = (cchEncodedSize + 3) / 4 * 3; + if (pszEncodedString[cchEncodedSize-1] == '=') { + if (pszEncodedString[cchEncodedSize-2] == '=') { + // Only one data byte is encoded in the last cluster. + cbDecoded -= 2; + } + else { + // Only two data bytes are encoded in the last cluster. + cbDecoded -= 1; + } + } + + if (NULL != pcbDecoded) { + *pcbDecoded = cbDecoded; + } + + if (cbDecodeBufferSize == 0 && pDecodeBuffer == NULL) { + return ERROR_SUCCESS; + } + + if (cbDecoded > cbDecodeBufferSize) { + // Supplied buffer is too small. + return ERROR_INSUFFICIENT_BUFFER; + } + + // Decode each four-byte cluster into the corresponding three data bytes. + ich = ib = 0; + while (ich < cchEncodedSize) { + b0 = DECODE(pszEncodedString[ich]); ich++; + b1 = DECODE(pszEncodedString[ich]); ich++; + b2 = DECODE(pszEncodedString[ich]); ich++; + b3 = DECODE(pszEncodedString[ich]); ich++; + + if ((NA == b0) || (NA == b1) || (NA == b2) || (NA == b3)) { + // Contents of input string are not base64. + return ERROR_INVALID_PARAMETER; + } + + pbDecodeBuffer[ib++] = (b0 << 2) | (b1 >> 4); + + if (ib < cbDecoded) { + pbDecodeBuffer[ib++] = (b1 << 4) | (b2 >> 2); + + if (ib < cbDecoded) { + pbDecodeBuffer[ib++] = (b2 << 6) | b3; + } + } + } + + DBG_ASSERT(ib == cbDecoded); + + return ERROR_SUCCESS; +} + + +DWORD +Base64Encode( + __in_bcount(cbDecodedBufferSize) VOID * pDecodedBuffer, + IN DWORD cbDecodedBufferSize, + __out_ecount_opt(cchEncodedStringSize) PSTR pszEncodedString, + IN DWORD cchEncodedStringSize, + __out_opt DWORD * pcchEncoded + ) +/*++ + +Routine Description: + + Decode a base64-encoded string. + +Arguments: + + pDecodedBuffer (IN) - buffer to encode. + cbDecodedBufferSize (IN) - size of buffer to encode. + cchEncodedStringSize (IN) - size of the buffer for the encoded string. + pszEncodedString (OUT) = the encoded string. + pcchEncoded (OUT) - size in characters of the encoded string. + +Return Values: + + 0 - success. + E_OUTOFMEMORY + +--*/ +{ + static CHAR rgchEncodeTable[64] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' + }; + + DWORD ib; + DWORD ich; + DWORD cchEncoded; + BYTE b0, b1, b2; + BYTE * pbDecodedBuffer = (BYTE *) pDecodedBuffer; + + // Calculate encoded string size. + cchEncoded = 1 + (cbDecodedBufferSize + 2) / 3 * 4; + + if (NULL != pcchEncoded) { + *pcchEncoded = cchEncoded; + } + + if (cchEncodedStringSize == 0 && pszEncodedString == NULL) { + return ERROR_SUCCESS; + } + + if (cchEncodedStringSize < cchEncoded) { + // Given buffer is too small to hold encoded string. + return ERROR_INSUFFICIENT_BUFFER; + } + + // Encode data byte triplets into four-byte clusters. + ib = ich = 0; + while (ib < cbDecodedBufferSize) { + b0 = pbDecodedBuffer[ib++]; + b1 = (ib < cbDecodedBufferSize) ? pbDecodedBuffer[ib++] : 0; + b2 = (ib < cbDecodedBufferSize) ? pbDecodedBuffer[ib++] : 0; + + // + // The checks below for buffer overflow seems redundant to me. + // But it's the only way I can find to keep OACR quiet so it + // will have to do. + // + + pszEncodedString[ich++] = rgchEncodeTable[b0 >> 2]; + if ( ich >= cchEncodedStringSize ) + { + DBG_ASSERT( FALSE ); + return ERROR_BUFFER_OVERFLOW; + } + + pszEncodedString[ich++] = rgchEncodeTable[((b0 << 4) & 0x30) | ((b1 >> 4) & 0x0f)]; + if ( ich >= cchEncodedStringSize ) + { + DBG_ASSERT( FALSE ); + return ERROR_BUFFER_OVERFLOW; + } + + pszEncodedString[ich++] = rgchEncodeTable[((b1 << 2) & 0x3c) | ((b2 >> 6) & 0x03)]; + if ( ich >= cchEncodedStringSize ) + { + DBG_ASSERT( FALSE ); + return ERROR_BUFFER_OVERFLOW; + } + + pszEncodedString[ich++] = rgchEncodeTable[b2 & 0x3f]; + if ( ich >= cchEncodedStringSize ) + { + DBG_ASSERT( FALSE ); + return ERROR_BUFFER_OVERFLOW; + } + } + + // Pad the last cluster as necessary to indicate the number of data bytes + // it represents. + switch (cbDecodedBufferSize % 3) { + case 0: + break; + case 1: + pszEncodedString[ich - 2] = '='; + __fallthrough; + case 2: + pszEncodedString[ich - 1] = '='; + break; + } + + // Null-terminate the encoded string. + pszEncodedString[ich++] = '\0'; + + DBG_ASSERT(ich == cchEncoded); + + return ERROR_SUCCESS; +} + + +DWORD +Base64Decode( + __in PCSTR pszEncodedString, + __out_opt VOID * pDecodeBuffer, + __in DWORD cbDecodeBufferSize, + __out_opt DWORD * pcbDecoded + ) +/*++ + +Routine Description: + + Decode a base64-encoded string. + +Arguments: + + pszEncodedString (IN) - base64-encoded string to decode. + cbDecodeBufferSize (IN) - size in bytes of the decode buffer. + pbDecodeBuffer (OUT) - holds the decoded data. + pcbDecoded (OUT) - number of data bytes in the decoded data (if success or + STATUS_BUFFER_TOO_SMALL). + +Return Values: + + 0 - success. + E_OUTOFMEMORY + E_INVALIDARG + +--*/ +{ +#define NA (255) +#define DECODE(x) (((ULONG)(x) < sizeof(rgbDecodeTable)) ? rgbDecodeTable[x] : NA) + + static BYTE rgbDecodeTable[128] = { + NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, // 0-15 + NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, // 16-31 + NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 62, NA, NA, NA, 63, // 32-47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, NA, NA, NA, 0, NA, NA, // 48-63 + NA, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 64-79 + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, NA, NA, NA, NA, NA, // 80-95 + NA, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 96-111 + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, NA, NA, NA, NA, NA, // 112-127 + }; + + DWORD cbDecoded; + DWORD cchEncodedSize; + DWORD ich; + DWORD ib; + BYTE b0, b1, b2, b3; + BYTE * pbDecodeBuffer = (BYTE *) pDecodeBuffer; + + cchEncodedSize = (DWORD)strlen(pszEncodedString); + if (NULL != pcbDecoded) { + *pcbDecoded = 0; + } + + if ((0 == cchEncodedSize) || (0 != (cchEncodedSize % 4))) { + // Input string is not sized correctly to be base64. + return ERROR_INVALID_PARAMETER; + } + + // Calculate decoded buffer size. + cbDecoded = (cchEncodedSize + 3) / 4 * 3; + if (pszEncodedString[cchEncodedSize-1] == '=') { + if (pszEncodedString[cchEncodedSize-2] == '=') { + // Only one data byte is encoded in the last cluster. + cbDecoded -= 2; + } + else { + // Only two data bytes are encoded in the last cluster. + cbDecoded -= 1; + } + } + + if (NULL != pcbDecoded) { + *pcbDecoded = cbDecoded; + } + + if (cbDecodeBufferSize == 0 && pDecodeBuffer == NULL) { + return ERROR_SUCCESS; + } + + if (cbDecoded > cbDecodeBufferSize) { + // Supplied buffer is too small. + return ERROR_INSUFFICIENT_BUFFER; + } + + // Decode each four-byte cluster into the corresponding three data bytes. + ich = ib = 0; + while (ich < cchEncodedSize) { + b0 = DECODE(pszEncodedString[ich]); ich++; + b1 = DECODE(pszEncodedString[ich]); ich++; + b2 = DECODE(pszEncodedString[ich]); ich++; + b3 = DECODE(pszEncodedString[ich]); ich++; + + if ((NA == b0) || (NA == b1) || (NA == b2) || (NA == b3)) { + // Contents of input string are not base64. + return ERROR_INVALID_PARAMETER; + } + + pbDecodeBuffer[ib++] = (b0 << 2) | (b1 >> 4); + + if (ib < cbDecoded) { + pbDecodeBuffer[ib++] = (b1 << 4) | (b2 >> 2); + + if (ib < cbDecoded) { + pbDecodeBuffer[ib++] = (b2 << 6) | b3; + } + } + } + + DBG_ASSERT(ib == cbDecoded); + + return ERROR_SUCCESS; +} + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/base64.h b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/base64.h new file mode 100644 index 0000000000..469b074d73 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/base64.h @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#ifndef _BASE64_H_ +#define _BASE64_H_ + +DWORD +Base64Encode( + __in_bcount( cbDecodedBufferSize ) VOID * pDecodedBuffer, + IN DWORD cbDecodedBufferSize, + __out_ecount_opt( cchEncodedStringSize ) PWSTR pszEncodedString, + IN DWORD cchEncodedStringSize, + __out_opt DWORD * pcchEncoded + ); + +DWORD +Base64Decode( + __in PCWSTR pszEncodedString, + __out_opt VOID * pDecodeBuffer, + __in DWORD cbDecodeBufferSize, + __out_opt DWORD * pcbDecoded + ); + +DWORD +Base64Encode( + __in_bcount( cbDecodedBufferSize ) VOID * pDecodedBuffer, + IN DWORD cbDecodedBufferSize, + __out_ecount_opt( cchEncodedStringSize ) PSTR pszEncodedString, + IN DWORD cchEncodedStringSize, + __out_opt DWORD * pcchEncoded + ); + +DWORD +Base64Decode( + __in PCSTR pszEncodedString, + __out_opt VOID * pDecodeBuffer, + __in DWORD cbDecodeBufferSize, + __out_opt DWORD * pcbDecoded + ); + +#endif // _BASE64_HXX_ + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/buffer.h b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/buffer.h new file mode 100644 index 0000000000..385b73d717 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/buffer.h @@ -0,0 +1,276 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include +#include + +#pragma warning( push ) +#pragma warning ( disable : ALL_CODE_ANALYSIS_WARNINGS ) + +// +// BUFFER_T class shouldn't be used directly. Use BUFFER specialization class instead. +// The only BUFFER_T partners are STRU and STRA classes. +// BUFFER_T cannot hold other but primitive types since it doesn't call +// constructor and destructor. +// +// Note: Size is in bytes. +// +template +class BUFFER_T +{ +public: + + BUFFER_T() + : m_cbBuffer( sizeof(m_rgBuffer) ), + m_fHeapAllocated( false ), + m_pBuffer(m_rgBuffer) + /*++ + Description: + + Default constructor where the inline buffer is used. + + Arguments: + + None. + + Returns: + + None. + + --*/ + { + } + + BUFFER_T( + __inout_bcount(cbInit) T* pbInit, + __in DWORD cbInit + ) : m_pBuffer( pbInit ), + m_cbBuffer( cbInit ), + m_fHeapAllocated( false ) + /*++ + Description: + + Instantiate BUFFER, initially using pbInit as buffer + This is useful for stack-buffers and inline-buffer class members + (see STACK_BUFFER and INLINE_BUFFER_INIT below) + + BUFFER does not free pbInit. + + Arguments: + + pbInit - Initial buffer to use. + cbInit - Size of pbInit in bytes (not in elements). + + Returns: + + None. + + --*/ + { + _ASSERTE( NULL != pbInit ); + _ASSERTE( cbInit > 0 ); + } + + ~BUFFER_T() + { + if( IsHeapAllocated() ) + { + _ASSERTE( NULL != m_pBuffer ); + HeapFree( GetProcessHeap(), 0, m_pBuffer ); + m_pBuffer = NULL; + m_cbBuffer = 0; + m_fHeapAllocated = false; + } + } + + T* + QueryPtr( + VOID + ) const + { + // + // Return pointer to data buffer. + // + return m_pBuffer; + } + + DWORD + QuerySize( + VOID + ) const + { + // + // Return number of bytes. + // + return m_cbBuffer; + } + + __success(return == true) + bool + Resize( + const SIZE_T cbNewSize, + const bool fZeroMemoryBeyondOldSize = false + ) + /*++ + Description: + + Resizes the buffer. + + Arguments: + + cbNewSize - Size in bytes to grow to. + fZeroMemoryBeyondOldSize + - Whether to zero the region of memory of the + new buffer beyond the original size. + + Returns: + + TRUE on success, FALSE on failure. + + --*/ + { + PVOID pNewMem; + + if ( cbNewSize <= m_cbBuffer ) + { + return true; + } + + if ( cbNewSize > MAXDWORD ) + { + SetLastError( ERROR_INVALID_PARAMETER ); + return false; + } + + DWORD dwHeapAllocFlags = fZeroMemoryBeyondOldSize ? HEAP_ZERO_MEMORY : 0; + + if( IsHeapAllocated() ) + { + pNewMem = HeapReAlloc( GetProcessHeap(), dwHeapAllocFlags, m_pBuffer, cbNewSize ); + } + else + { + pNewMem = HeapAlloc( GetProcessHeap(), dwHeapAllocFlags, cbNewSize ); + } + + if( pNewMem == NULL ) + { + SetLastError( ERROR_NOT_ENOUGH_MEMORY ); + return false; + } + + if( !IsHeapAllocated() ) + { + // + // First time this block is allocated. Copy over old contents. + // + memcpy_s( pNewMem, static_cast(cbNewSize), m_pBuffer, m_cbBuffer ); + m_fHeapAllocated = true; + } + + m_pBuffer = reinterpret_cast(pNewMem); + m_cbBuffer = static_cast(cbNewSize); + + _ASSERTE( m_pBuffer != NULL ); + + return true; + } + +private: + + bool + IsHeapAllocated( + VOID + ) const + { + return m_fHeapAllocated; + } + + // + // The default inline buffer. + // This member should be at the beginning for alignment purposes. + // + T m_rgBuffer[LENGTH]; + + // + // Is m_pBuffer dynamically allocated? + // + bool m_fHeapAllocated; + + // + // Size of the buffer as requested by client in bytes. + // + DWORD m_cbBuffer; + + // + // Pointer to buffer. + // + __field_bcount_full(m_cbBuffer) + T* m_pBuffer; +}; + +// +// Resizes the buffer by 2 if the ideal size is bigger +// than the buffer length. That give us lg(n) allocations. +// +// Use template inferring like: +// +// BUFFER buff; +// hr = ResizeBufferByTwo(buff, 100); +// +template +HRESULT +ResizeBufferByTwo( + BUFFER_T& Buffer, + SIZE_T cbIdealSize, + bool fZeroMemoryBeyondOldSize = false +) +{ + if (cbIdealSize > Buffer.QuerySize()) + { + if (!Buffer.Resize(max(cbIdealSize, static_cast(Buffer.QuerySize() * 2)), + fZeroMemoryBeyondOldSize)) + { + return E_OUTOFMEMORY; + } + } + return S_OK; +} + + +// +// +// Lots of code uses BUFFER class to store a bunch of different +// structures, so m_rgBuffer needs to be 8 byte aligned when it is used +// as an opaque buffer. +// +#define INLINED_BUFFER_LEN 32 +typedef BUFFER_T BUFFER; + +// +// Assumption of macros below for pointer alignment purposes +// +C_ASSERT( sizeof(VOID*) <= sizeof(ULONGLONG) ); + +// +// Declare a BUFFER that will use stack memory of +// bytes. If the buffer overflows then a heap buffer will be allocated. +// +#define STACK_BUFFER( _name, _size ) \ + ULONGLONG __aqw##_name[ ( ( (_size) + sizeof(ULONGLONG) - 1 ) / sizeof(ULONGLONG) ) ]; \ + BUFFER _name( (BYTE*)__aqw##_name, sizeof(__aqw##_name) ) + +// +// Macros for declaring and initializing a BUFFER that will use inline memory +// of bytes as a member of an object. +// +#define INLINE_BUFFER( _name, _size ) \ + ULONGLONG __aqw##_name[ ( ( (_size) + sizeof(ULONGLONG) - 1 ) / sizeof(ULONGLONG) ) ]; \ + BUFFER _name; + +#define INLINE_BUFFER_INIT( _name ) \ + _name( (BYTE*)__aqw##_name, sizeof( __aqw##_name ) ) + +#pragma warning( pop ) diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/datetime.h b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/datetime.h new file mode 100644 index 0000000000..fd09b7a6a0 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/datetime.h @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#ifndef _DATETIME_H_ +#define _DATETIME_H_ + +BOOL +StringTimeToFileTime( + PCSTR pszTime, + ULONGLONG * pulTime +); + +#endif + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/dbgutil.h b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/dbgutil.h new file mode 100644 index 0000000000..9b714e9bbf --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/dbgutil.h @@ -0,0 +1,104 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#ifndef _DBGUTIL_H_ +#define _DBGUTIL_H_ + +#include + +// +// TODO +// Using _CrtDbg implementation. If hooking is desired +// wrappers should be provided here so that we can reimplement +// if neecessary. +// +// IF_DEBUG/DEBUG FLAGS +// +// registry configuration +// + +// +// Debug error levels for DEBUG_FLAGS_VAR. +// + +#define DEBUG_FLAG_TRACE 0x00000001 +#define DEBUG_FLAG_INFO 0x00000002 +#define DEBUG_FLAG_WARN 0x00000004 +#define DEBUG_FLAG_ERROR 0x00000008 + +// +// Predefined error level values. These are backwards from the +// windows definitions. +// + +#define DEBUG_FLAGS_TRACE (DEBUG_FLAG_ERROR | DEBUG_FLAG_WARN | DEBUG_FLAG_INFO | DEBUG_FLAG_TRACE) +#define DEBUG_FLAGS_INFO (DEBUG_FLAG_ERROR | DEBUG_FLAG_WARN | DEBUG_FLAG_INFO) +#define DEBUG_FLAGS_WARN (DEBUG_FLAG_ERROR | DEBUG_FLAG_WARN) +#define DEBUG_FLAGS_ERROR (DEBUG_FLAG_ERROR) +#define DEBUG_FLAGS_ANY (DEBUG_FLAG_INFO | DEBUG_FLAG_WARN | DEBUG_FLAG_ERROR | DEBUG_FLAG_TRACE) + +// +// Global variables to control tracing. Generally per module +// + +#ifndef DEBUG_FLAGS_VAR +#define DEBUG_FLAGS_VAR g_dwDebugFlags +#endif + +#ifndef DEBUG_LABEL_VAR +#define DEBUG_LABEL_VAR g_szDebugLabel +#endif + +extern PCSTR DEBUG_LABEL_VAR; +extern DWORD DEBUG_FLAGS_VAR; + +// +// Module should make this declaration globally. +// + +#define DECLARE_DEBUG_PRINT_OBJECT( _pszLabel_ ) \ + PCSTR DEBUG_LABEL_VAR = _pszLabel_; \ + DWORD DEBUG_FLAGS_VAR = 0; \ + +#define DECLARE_DEBUG_PRINT_OBJECT2( _pszLabel_, _dwLevel_ ) \ + PCSTR DEBUG_LABEL_VAR = _pszLabel_; \ + DWORD DEBUG_FLAGS_VAR = _dwLevel_; \ + +// +// This doesn't do anything now. Should be safe to call in dll main. +// + +#define CREATE_DEBUG_PRINT_OBJECT + +// +// Trace macros +// + +#define DBG_CONTEXT _CRT_WARN, __FILE__, __LINE__, DEBUG_LABEL_VAR + +#ifdef DEBUG +#define DBGINFO(args) \ +{if (DEBUG_FLAGS_VAR & DEBUG_FLAG_INFO) { _CrtDbgReport args; }} +#define DBGWARN(args) \ +{if (DEBUG_FLAGS_VAR & DEBUG_FLAG_WARN) { _CrtDbgReport args; }} +#define DBGERROR(args) \ +{if (DEBUG_FLAGS_VAR & DEBUG_FLAG_ERROR) { _CrtDbgReport args; }} +#else +#define DBGINFO +#define DBGWARN +#define DBGERROR +#endif + +#define DBGPRINTF DBGINFO + +// +// Simple error traces +// + +#define DBGERROR_HR( _hr_ ) \ + DBGERROR((DBG_CONTEXT, "hr=0x%x\n", _hr_)) + +#define DBGERROR_STATUS( _status_ ) \ + DBGERROR((DBG_CONTEXT, "status=%d\n", _status_)) + +#endif diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/hashfn.h b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/hashfn.h new file mode 100644 index 0000000000..a7bfeda2cf --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/hashfn.h @@ -0,0 +1,325 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#ifndef __HASHFN_H__ +#define __HASHFN_H__ + + +// Produce a scrambled, randomish number in the range 0 to RANDOM_PRIME-1. +// Applying this to the results of the other hash functions is likely to +// produce a much better distribution, especially for the identity hash +// functions such as Hash(char c), where records will tend to cluster at +// the low end of the hashtable otherwise. LKRhash applies this internally +// to all hash signatures for exactly this reason. + +inline DWORD +HashScramble(DWORD dwHash) +{ + // Here are 10 primes slightly greater than 10^9 + // 1000000007, 1000000009, 1000000021, 1000000033, 1000000087, + // 1000000093, 1000000097, 1000000103, 1000000123, 1000000181. + + // default value for "scrambling constant" + const DWORD RANDOM_CONSTANT = 314159269UL; + // large prime number, also used for scrambling + const DWORD RANDOM_PRIME = 1000000007UL; + + return (RANDOM_CONSTANT * dwHash) % RANDOM_PRIME ; +} + + +// Faster scrambling function suggested by Eric Jacobsen + +inline DWORD +HashRandomizeBits(DWORD dw) +{ + return (((dw * 1103515245 + 12345) >> 16) + | ((dw * 69069 + 1) & 0xffff0000)); +} + + +// Small prime number used as a multiplier in the supplied hash functions +const DWORD HASH_MULTIPLIER = 101; + +#undef HASH_SHIFT_MULTIPLY + +#ifdef HASH_SHIFT_MULTIPLY +# define HASH_MULTIPLY(dw) (((dw) << 7) - (dw)) +#else +# define HASH_MULTIPLY(dw) ((dw) * HASH_MULTIPLIER) +#endif + +// Fast, simple hash function that tends to give a good distribution. +// Apply HashScramble to the result if you're using this for something +// other than LKRhash. + +inline DWORD +HashString( + const char* psz, + DWORD dwHash = 0) +{ + // force compiler to use unsigned arithmetic + const unsigned char* upsz = (const unsigned char*) psz; + + for ( ; *upsz; ++upsz) + dwHash = HASH_MULTIPLY(dwHash) + *upsz; + + return dwHash; +} + +inline DWORD +HashString( + __in_ecount(cch) const char* psz, + __in DWORD cch, + __in DWORD dwHash +) +{ + // force compiler to use unsigned arithmetic + const unsigned char* upsz = (const unsigned char*) psz; + + for (DWORD Index = 0; + Index < cch; + ++Index, ++upsz) + { + dwHash = HASH_MULTIPLY(dwHash) + *upsz; + } + + return dwHash; +} + + +// Unicode version of above + +inline DWORD +HashString( + const wchar_t* pwsz, + DWORD dwHash = 0) +{ + for ( ; *pwsz; ++pwsz) + dwHash = HASH_MULTIPLY(dwHash) + *pwsz; + + return dwHash; +} + +// Based on length of the string instead of null-terminating character + +inline DWORD +HashString( + __in_ecount(cch) const wchar_t* pwsz, + __in DWORD cch, + __in DWORD dwHash +) +{ + for (DWORD Index = 0; + Index < cch; + ++Index, ++pwsz) + { + dwHash = HASH_MULTIPLY(dwHash) + *pwsz; + } + + return dwHash; +} + + +// Quick-'n'-dirty case-insensitive string hash function. +// Make sure that you follow up with _stricmp or _mbsicmp. You should +// also cache the length of strings and check those first. Caching +// an uppercase version of a string can help too. +// Again, apply HashScramble to the result if using with something other +// than LKRhash. +// Note: this is not really adequate for MBCS strings. + +inline DWORD +HashStringNoCase( + const char* psz, + DWORD dwHash = 0) +{ + const unsigned char* upsz = (const unsigned char*) psz; + + for ( ; *upsz; ++upsz) + dwHash = HASH_MULTIPLY(dwHash) + + (*upsz & 0xDF); // strip off lowercase bit + + return dwHash; +} + +inline DWORD +HashStringNoCase( + __in_ecount(cch) + const char* psz, + SIZE_T cch, + DWORD dwHash) +{ + const unsigned char* upsz = (const unsigned char*) psz; + + for (SIZE_T Index = 0; + Index < cch; + ++Index, ++upsz) + { + dwHash = HASH_MULTIPLY(dwHash) + + (*upsz & 0xDF); // strip off lowercase bit + } + return dwHash; +} + + +// Unicode version of above + +inline DWORD +HashStringNoCase( + const wchar_t* pwsz, + DWORD dwHash = 0) +{ + for ( ; *pwsz; ++pwsz) + dwHash = HASH_MULTIPLY(dwHash) + (*pwsz & 0xFFDF); + + return dwHash; +} + +// Unicode version of above with length + +inline DWORD +HashStringNoCase( + __in_ecount(cch) + const wchar_t* pwsz, + SIZE_T cch, + DWORD dwHash) +{ + for (SIZE_T Index = 0; + Index < cch; + ++Index, ++pwsz) + { + dwHash = HASH_MULTIPLY(dwHash) + (*pwsz & 0xFFDF); + } + return dwHash; +} + + +// HashBlob returns the hash of a blob of arbitrary binary data. +// +// Warning: HashBlob is generally not the right way to hash a class object. +// Consider: +// class CFoo { +// public: +// char m_ch; +// double m_d; +// char* m_psz; +// }; +// +// inline DWORD Hash(const CFoo& rFoo) +// { return HashBlob(&rFoo, sizeof(CFoo)); } +// +// This is the wrong way to hash a CFoo for two reasons: (a) there will be +// a 7-byte gap between m_ch and m_d imposed by the alignment restrictions +// of doubles, which will be filled with random data (usually non-zero for +// stack variables), and (b) it hashes the address (rather than the +// contents) of the string m_psz. Similarly, +// +// bool operator==(const CFoo& rFoo1, const CFoo& rFoo2) +// { return memcmp(&rFoo1, &rFoo2, sizeof(CFoo)) == 0; } +// +// does the wrong thing. Much better to do this: +// +// DWORD Hash(const CFoo& rFoo) +// { +// return HashString(rFoo.m_psz, +// HASH_MULTIPLIER * Hash(rFoo.m_ch) +// + Hash(rFoo.m_d)); +// } +// +// Again, apply HashScramble if using with something other than LKRhash. + +inline DWORD +HashBlob( + const void* pv, + size_t cb, + DWORD dwHash = 0) +{ + const BYTE * pb = static_cast(pv); + + while (cb-- > 0) + dwHash = HASH_MULTIPLY(dwHash) + *pb++; + + return dwHash; +} + + + +// +// Overloaded hash functions for all the major builtin types. +// Again, apply HashScramble to result if using with something other than +// LKRhash. +// + +inline DWORD Hash(const char* psz) +{ return HashString(psz); } + +inline DWORD Hash(const unsigned char* pusz) +{ return HashString(reinterpret_cast(pusz)); } + +inline DWORD Hash(const signed char* pssz) +{ return HashString(reinterpret_cast(pssz)); } + +inline DWORD Hash(const wchar_t* pwsz) +{ return HashString(pwsz); } + +inline DWORD +Hash( + const GUID* pguid, + DWORD dwHash = 0) +{ + + return * reinterpret_cast(const_cast(pguid)) + dwHash; +} + +// Identity hash functions: scalar values map to themselves +inline DWORD Hash(char c) +{ return c; } + +inline DWORD Hash(unsigned char uc) +{ return uc; } + +inline DWORD Hash(signed char sc) +{ return sc; } + +inline DWORD Hash(short sh) +{ return sh; } + +inline DWORD Hash(unsigned short ush) +{ return ush; } + +inline DWORD Hash(int i) +{ return i; } + +inline DWORD Hash(unsigned int u) +{ return u; } + +inline DWORD Hash(long l) +{ return l; } + +inline DWORD Hash(unsigned long ul) +{ return ul; } + +inline DWORD Hash(float f) +{ + // be careful of rounding errors when computing keys + union { + float f; + DWORD dw; + } u; + u.f = f; + return u.dw; +} + +inline DWORD Hash(double dbl) +{ + // be careful of rounding errors when computing keys + union { + double dbl; + DWORD dw[2]; + } u; + u.dbl = dbl; + return u.dw[0] * HASH_MULTIPLIER + u.dw[1]; +} + +#endif // __HASHFN_H__ diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/hashtable.h b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/hashtable.h new file mode 100644 index 0000000000..9319e5643d --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/hashtable.h @@ -0,0 +1,666 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include +#include "rwlock.h" +#include "prime.h" + +template +class HASH_NODE +{ + template + friend class HASH_TABLE; + + HASH_NODE( + _Record * pRecord, + DWORD dwHash + ) : _pNext (NULL), + _pRecord (pRecord), + _dwHash (dwHash) + {} + + ~HASH_NODE() + { + _ASSERTE(_pRecord == NULL); + } + + private: + // Next node in the hash table look-aside + HASH_NODE<_Record> *_pNext; + + // actual record + _Record * _pRecord; + + // hash value + DWORD _dwHash; +}; + +template +class HASH_TABLE +{ +protected: + typedef BOOL + (PFN_DELETE_IF)( + _Record * pRecord, + PVOID pvContext + ); + + typedef VOID + (PFN_APPLY)( + _Record * pRecord, + PVOID pvContext + ); + +public: + HASH_TABLE( + VOID + ) + : _ppBuckets( NULL ), + _nBuckets( 0 ), + _nItems( 0 ) + { + } + + virtual + ~HASH_TABLE(); + + virtual + VOID + ReferenceRecord( + _Record * pRecord + ) = 0; + + virtual + VOID + DereferenceRecord( + _Record * pRecord + ) = 0; + + virtual + _Key + ExtractKey( + _Record * pRecord + ) = 0; + + virtual + DWORD + CalcKeyHash( + _Key key + ) = 0; + + virtual + BOOL + EqualKeys( + _Key key1, + _Key key2 + ) = 0; + + DWORD + Count( + VOID + ) const; + + bool + IsInitialized( + VOID + ) const; + + virtual + VOID + Clear(); + + HRESULT + Initialize( + DWORD nBucketSize + ); + + virtual + VOID + FindKey( + _Key key, + _Record ** ppRecord + ); + + virtual + HRESULT + InsertRecord( + _Record * pRecord + ); + + virtual + VOID + DeleteKey( + _Key key + ); + + virtual + VOID + DeleteIf( + PFN_DELETE_IF pfnDeleteIf, + PVOID pvContext + ); + + VOID + Apply( + PFN_APPLY pfnApply, + PVOID pvContext + ); + +private: + + __success(*ppNode != NULL && return != FALSE) + BOOL + FindNodeInternal( + _Key key, + DWORD dwHash, + __deref_out + HASH_NODE<_Record> ** ppNode, + __deref_opt_out + HASH_NODE<_Record> *** pppPreviousNodeNextPointer = NULL + ); + + VOID + DeleteNode( + HASH_NODE<_Record> * pNode + ) + { + if (pNode->_pRecord != NULL) + { + DereferenceRecord(pNode->_pRecord); + pNode->_pRecord = NULL; + } + + delete pNode; + } + + VOID + RehashTableIfNeeded( + VOID + ); + + HASH_NODE<_Record> ** _ppBuckets; + DWORD _nBuckets; + DWORD _nItems; + // + // Allow to use lock object in const methods. + // + mutable + CWSDRWLock _tableLock; +}; + +template +HRESULT +HASH_TABLE<_Record,_Key>::Initialize( + DWORD nBuckets +) +{ + HRESULT hr = S_OK; + + if ( nBuckets == 0 ) + { + hr = E_INVALIDARG; + goto Failed; + } + + if (nBuckets >= MAXDWORD/sizeof(HASH_NODE<_Record> *)) + { + hr = E_INVALIDARG; + goto Failed; + } + + _ASSERTE(_ppBuckets == NULL ); + if ( _ppBuckets != NULL ) + { + hr = E_INVALIDARG; + goto Failed; + } + + hr = _tableLock.Init(); + if ( FAILED( hr ) ) + { + goto Failed; + } + + _ppBuckets = (HASH_NODE<_Record> **)HeapAlloc( + GetProcessHeap(), + HEAP_ZERO_MEMORY, + nBuckets*sizeof(HASH_NODE<_Record> *)); + if (_ppBuckets == NULL) + { + hr = HRESULT_FROM_WIN32(ERROR_NOT_ENOUGH_MEMORY); + goto Failed; + } + _nBuckets = nBuckets; + + return S_OK; + +Failed: + + if (_ppBuckets) + { + HeapFree(GetProcessHeap(), + 0, + _ppBuckets); + _ppBuckets = NULL; + } + + return hr; +} + + +template +HASH_TABLE<_Record,_Key>::~HASH_TABLE() +{ + if (_ppBuckets == NULL) + { + return; + } + + _ASSERTE(_nItems == 0); + + HeapFree(GetProcessHeap(), + 0, + _ppBuckets); + _ppBuckets = NULL; + _nBuckets = 0; +} + +template< class _Record, class _Key> +DWORD +HASH_TABLE<_Record,_Key>::Count() const +{ + return _nItems; +} + +template< class _Record, class _Key> +bool +HASH_TABLE<_Record,_Key>::IsInitialized( + VOID +) const +{ + return _ppBuckets != NULL; +} + + +template +VOID +HASH_TABLE<_Record,_Key>::Clear() +{ + HASH_NODE<_Record> *pCurrent; + HASH_NODE<_Record> *pNext; + + // This is here in the off cases where someone instantiates a hashtable + // and then does an automatic "clear" before its destruction WITHOUT + // ever initializing it. + if ( ! _tableLock.QueryInited() ) + { + return; + } + + _tableLock.ExclusiveAcquire(); + + for (DWORD i=0; i<_nBuckets; i++) + { + pCurrent = _ppBuckets[i]; + _ppBuckets[i] = NULL; + while (pCurrent != NULL) + { + pNext = pCurrent->_pNext; + DeleteNode(pCurrent); + pCurrent = pNext; + } + } + + _nItems = 0; + _tableLock.ExclusiveRelease(); +} + +template +__success(*ppNode != NULL && return != FALSE) +BOOL +HASH_TABLE<_Record,_Key>::FindNodeInternal( + _Key key, + DWORD dwHash, + __deref_out + HASH_NODE<_Record> ** ppNode, + __deref_opt_out + HASH_NODE<_Record> *** pppPreviousNodeNextPointer +) +/*++ + Return value indicates whether the item is found + key, dwHash - key and hash for the node to find + ppNode - on successful return, the node found, on failed return, the first + node with hash value greater than the node to be found + pppPreviousNodeNextPointer - the pointer to previous node's _pNext + + This routine may be called under either read or write lock +--*/ +{ + HASH_NODE<_Record> **ppPreviousNodeNextPointer; + HASH_NODE<_Record> *pNode; + BOOL fFound = FALSE; + + ppPreviousNodeNextPointer = _ppBuckets + (dwHash % _nBuckets); + pNode = *ppPreviousNodeNextPointer; + while (pNode != NULL) + { + if (pNode->_dwHash == dwHash) + { + if (EqualKeys(key, + ExtractKey(pNode->_pRecord))) + { + fFound = TRUE; + break; + } + } + else if (pNode->_dwHash > dwHash) + { + break; + } + + ppPreviousNodeNextPointer = &(pNode->_pNext); + pNode = *ppPreviousNodeNextPointer; + } + + __analysis_assume( (pNode == NULL && fFound == FALSE) || + (pNode != NULL && fFound == TRUE ) ); + *ppNode = pNode; + if (pppPreviousNodeNextPointer != NULL) + { + *pppPreviousNodeNextPointer = ppPreviousNodeNextPointer; + } + return fFound; +} + +template +VOID +HASH_TABLE<_Record,_Key>::FindKey( + _Key key, + _Record ** ppRecord +) +{ + HASH_NODE<_Record> *pNode; + + *ppRecord = NULL; + + DWORD dwHash = CalcKeyHash(key); + + _tableLock.SharedAcquire(); + + if (FindNodeInternal(key, dwHash, &pNode) && + pNode->_pRecord != NULL) + { + ReferenceRecord(pNode->_pRecord); + *ppRecord = pNode->_pRecord; + } + + _tableLock.SharedRelease(); +} + +template +HRESULT +HASH_TABLE<_Record,_Key>::InsertRecord( + _Record * pRecord +) +/*++ + This method inserts a node for this record and also empty nodes for paths + in the heirarchy leading upto this path + + The insert is done under only a read-lock - this is possible by keeping + the hashes in a bucket in increasing order and using interlocked operations + to actually insert the item in the hash-bucket lookaside list and the parent + children list + + Returns HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS) if the record already exists. + Never leak this error to the end user because "*file* already exists" may be confusing. +--*/ +{ + BOOL fLocked = FALSE; + _Key key = ExtractKey(pRecord); + DWORD dwHash = CalcKeyHash(key); + HRESULT hr = S_OK; + HASH_NODE<_Record> * pNewNode; + HASH_NODE<_Record> * pNextNode; + HASH_NODE<_Record> ** ppPreviousNodeNextPointer; + + // + // Ownership of pRecord is not transferred to pNewNode yet, so remember + // to either set it to null before deleting pNewNode or add an extra + // reference later - this is to make sure we do not do an extra ref/deref + // which users may view as getting flushed out of the hash-table + // + pNewNode = new HASH_NODE<_Record>(pRecord, dwHash); + if (pNewNode == NULL) + { + hr = HRESULT_FROM_WIN32(ERROR_NOT_ENOUGH_MEMORY); + goto Finished; + } + + _tableLock.SharedAcquire(); + fLocked = TRUE; + + do + { + // + // Find the right place to add this node + // + if (FindNodeInternal(key, dwHash, &pNextNode, &ppPreviousNodeNextPointer)) + { + // + // If node already there, return error + // + pNewNode->_pRecord = NULL; + DeleteNode(pNewNode); + + // + // We should never leak this error to the end user + // because "file already exists" may be confusing. + // + hr = HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS); + goto Finished; + } + + // + // If another node got inserted in between, we will have to retry + // + pNewNode->_pNext = pNextNode; + } while (InterlockedCompareExchangePointer((PVOID *)ppPreviousNodeNextPointer, + pNewNode, + pNextNode) != pNextNode); + // pass ownership of pRecord now + if (pRecord != NULL) + { + ReferenceRecord(pRecord); + pRecord = NULL; + } + InterlockedIncrement((LONG *)&_nItems); + +Finished: + + if (fLocked) + { + _tableLock.SharedRelease(); + } + + if (SUCCEEDED(hr)) + { + RehashTableIfNeeded(); + } + + return hr; +} + +template +VOID +HASH_TABLE<_Record,_Key>::DeleteKey( + _Key key +) +{ + HASH_NODE<_Record> *pNode; + HASH_NODE<_Record> **ppPreviousNodeNextPointer; + + DWORD dwHash = CalcKeyHash(key); + + _tableLock.ExclusiveAcquire(); + + if (FindNodeInternal(key, dwHash, &pNode, &ppPreviousNodeNextPointer)) + { + *ppPreviousNodeNextPointer = pNode->_pNext; + DeleteNode(pNode); + _nItems--; + } + + _tableLock.ExclusiveRelease(); +} + +template +VOID +HASH_TABLE<_Record,_Key>::DeleteIf( + PFN_DELETE_IF pfnDeleteIf, + PVOID pvContext +) +{ + HASH_NODE<_Record> *pNode; + HASH_NODE<_Record> **ppPreviousNodeNextPointer; + + _tableLock.ExclusiveAcquire(); + + for (DWORD i=0; i<_nBuckets; i++) + { + ppPreviousNodeNextPointer = _ppBuckets + i; + pNode = *ppPreviousNodeNextPointer; + while (pNode != NULL) + { + // + // Non empty nodes deleted based on DeleteIf, empty nodes deleted + // if they have no children + // + if (pfnDeleteIf(pNode->_pRecord, pvContext)) + { + *ppPreviousNodeNextPointer = pNode->_pNext; + DeleteNode(pNode); + _nItems--; + } + else + { + ppPreviousNodeNextPointer = &pNode->_pNext; + } + + pNode = *ppPreviousNodeNextPointer; + } + } + + _tableLock.ExclusiveRelease(); +} + +template +VOID +HASH_TABLE<_Record,_Key>::Apply( + PFN_APPLY pfnApply, + PVOID pvContext +) +{ + HASH_NODE<_Record> *pNode; + + _tableLock.SharedAcquire(); + + for (DWORD i=0; i<_nBuckets; i++) + { + pNode = _ppBuckets[i]; + while (pNode != NULL) + { + if (pNode->_pRecord != NULL) + { + pfnApply(pNode->_pRecord, pvContext); + } + + pNode = pNode->_pNext; + } + } + + _tableLock.SharedRelease(); +} + +template +VOID +HASH_TABLE<_Record,_Key>::RehashTableIfNeeded( + VOID +) +{ + HASH_NODE<_Record> **ppBuckets; + DWORD nBuckets; + HASH_NODE<_Record> *pNode; + HASH_NODE<_Record> *pNextNode; + HASH_NODE<_Record> **ppNextPointer; + HASH_NODE<_Record> *pNewNextNode; + DWORD nNewBuckets; + + // + // If number of items has become too many, we will double the hash table + // size (we never reduce it however) + // + if (_nItems <= PRIME::GetPrime(2*_nBuckets)) + { + return; + } + + _tableLock.ExclusiveAcquire(); + + nNewBuckets = PRIME::GetPrime(2*_nBuckets); + + if (_nItems <= nNewBuckets) + { + goto Finished; + } + + nBuckets = nNewBuckets; + if (nBuckets >= 0xffffffff/sizeof(HASH_NODE<_Record> *)) + { + goto Finished; + } + ppBuckets = (HASH_NODE<_Record> **)HeapAlloc( + GetProcessHeap(), + HEAP_ZERO_MEMORY, + nBuckets*sizeof(HASH_NODE<_Record> *)); + if (ppBuckets == NULL) + { + goto Finished; + } + + // + // Take out nodes from the old hash table and insert in the new one, make + // sure to keep the hashes in increasing order + // + for (DWORD i=0; i<_nBuckets; i++) + { + pNode = _ppBuckets[i]; + while (pNode != NULL) + { + pNextNode = pNode->_pNext; + + ppNextPointer = ppBuckets + (pNode->_dwHash % nBuckets); + pNewNextNode = *ppNextPointer; + while (pNewNextNode != NULL && + pNewNextNode->_dwHash <= pNode->_dwHash) + { + ppNextPointer = &pNewNextNode->_pNext; + pNewNextNode = pNewNextNode->_pNext; + } + pNode->_pNext = pNewNextNode; + *ppNextPointer = pNode; + + pNode = pNextNode; + } + } + + HeapFree(GetProcessHeap(), 0, _ppBuckets); + _ppBuckets = ppBuckets; + _nBuckets = nBuckets; + ppBuckets = NULL; + +Finished: + + _tableLock.ExclusiveRelease(); +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/listentry.h b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/listentry.h new file mode 100644 index 0000000000..80b70e97a9 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/listentry.h @@ -0,0 +1,163 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#ifndef _LIST_ENTRY_H +#define _LIST_ENTRY_H + +// +// Doubly-linked list manipulation routines. +// + + +#define InitializeListHead32(ListHead) (\ + (ListHead)->Flink = (ListHead)->Blink = PtrToUlong((ListHead))) + + +FORCEINLINE +VOID +InitializeListHead( + IN PLIST_ENTRY ListHead + ) +{ + ListHead->Flink = ListHead->Blink = ListHead; +} + +FORCEINLINE +BOOLEAN +IsListEmpty( + IN const LIST_ENTRY * ListHead + ) +{ + return (BOOLEAN)(ListHead->Flink == ListHead); +} + +FORCEINLINE +BOOLEAN +RemoveEntryList( + IN PLIST_ENTRY Entry + ) +{ + PLIST_ENTRY Blink; + PLIST_ENTRY Flink; + + Flink = Entry->Flink; + Blink = Entry->Blink; + Blink->Flink = Flink; + Flink->Blink = Blink; + return (BOOLEAN)(Flink == Blink); +} + +FORCEINLINE +PLIST_ENTRY +RemoveHeadList( + IN PLIST_ENTRY ListHead + ) +{ + PLIST_ENTRY Flink; + PLIST_ENTRY Entry; + + Entry = ListHead->Flink; + Flink = Entry->Flink; + ListHead->Flink = Flink; + Flink->Blink = ListHead; + return Entry; +} + + + +FORCEINLINE +PLIST_ENTRY +RemoveTailList( + IN PLIST_ENTRY ListHead + ) +{ + PLIST_ENTRY Blink; + PLIST_ENTRY Entry; + + Entry = ListHead->Blink; + Blink = Entry->Blink; + ListHead->Blink = Blink; + Blink->Flink = ListHead; + return Entry; +} + + +FORCEINLINE +VOID +InsertTailList( + IN PLIST_ENTRY ListHead, + IN PLIST_ENTRY Entry + ) +{ + PLIST_ENTRY Blink; + + Blink = ListHead->Blink; + Entry->Flink = ListHead; + Entry->Blink = Blink; + Blink->Flink = Entry; + ListHead->Blink = Entry; +} + + +FORCEINLINE +VOID +InsertHeadList( + IN PLIST_ENTRY ListHead, + IN PLIST_ENTRY Entry + ) +{ + PLIST_ENTRY Flink; + + Flink = ListHead->Flink; + Entry->Flink = Flink; + Entry->Blink = ListHead; + Flink->Blink = Entry; + ListHead->Flink = Entry; +} + +FORCEINLINE +VOID +AppendTailList( + IN PLIST_ENTRY ListHead, + IN PLIST_ENTRY ListToAppend + ) +{ + PLIST_ENTRY ListEnd = ListHead->Blink; + + ListHead->Blink->Flink = ListToAppend; + ListHead->Blink = ListToAppend->Blink; + ListToAppend->Blink->Flink = ListHead; + ListToAppend->Blink = ListEnd; +} + +FORCEINLINE +PSINGLE_LIST_ENTRY +PopEntryList( + PSINGLE_LIST_ENTRY ListHead + ) +{ + PSINGLE_LIST_ENTRY FirstEntry; + FirstEntry = ListHead->Next; + if (FirstEntry != NULL) { + ListHead->Next = FirstEntry->Next; + } + + return FirstEntry; +} + + +FORCEINLINE +VOID +PushEntryList( + PSINGLE_LIST_ENTRY ListHead, + PSINGLE_LIST_ENTRY Entry + ) +{ + Entry->Next = ListHead->Next; + ListHead->Next = Entry; +} + + +#endif diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/macros.h b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/macros.h new file mode 100644 index 0000000000..960f663a98 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/macros.h @@ -0,0 +1,63 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#ifndef _MACROS_H +#define _MACROS_H + +// +// The DIFF macro should be used around an expression involving pointer +// subtraction. The expression passed to DIFF is cast to a size_t type, +// allowing the result to be easily assigned to any 32-bit variable or +// passed to a function expecting a 32-bit argument. +// + +#define DIFF(x) ((size_t)(x)) + +// Change a hexadecimal digit to its numerical equivalent +#define TOHEX( ch ) \ + ((ch) > L'9' ? \ + (ch) >= L'a' ? \ + (ch) - L'a' + 10 : \ + (ch) - L'A' + 10 \ + : (ch) - L'0') + + +// Change a number to its Hexadecimal equivalent + +#define TODIGIT( nDigit ) \ + (CHAR)((nDigit) > 9 ? \ + (nDigit) - 10 + 'A' \ + : (nDigit) + '0') + + +inline int +SAFEIsSpace(UCHAR c) +{ + return isspace( c ); +} + +inline int +SAFEIsAlNum(UCHAR c) +{ + return isalnum( c ); +} + +inline int +SAFEIsAlpha(UCHAR c) +{ + return isalpha( c ); +} + +inline int +SAFEIsXDigit(UCHAR c) +{ + return isxdigit( c ); +} + +inline int +SAFEIsDigit(UCHAR c) +{ + return isdigit( c ); +} + +#endif // _MACROS_H diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/multisz.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/multisz.cpp new file mode 100644 index 0000000000..775ec4cd0c --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/multisz.cpp @@ -0,0 +1,474 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + + +#pragma warning (disable : 4267) + +#include "precomp.h" +#include "multisz.h" +#include + +// +// Private Definitions +// + +#define MAXULONG 4294967295 +#define ISWHITE( ch ) ((ch) == L' ' || (ch) == L'\t' || (ch) == L'\r') + +// +// When appending data, this is the extra amount we request to avoid +// reallocations +// +#define STR_SLOP 128 + + +DWORD +MULTISZ::CalcLength( const WCHAR * str, + LPDWORD pcStrings ) +{ + DWORD count = 0; + DWORD total = 1; + DWORD len; + + while( *str ) { + len = ::wcslen( str ) + 1; + total += len; + str += len; + count++; + } + + if( pcStrings != NULL ) { + *pcStrings = count; + } + + return total; + +} // MULTISZ::CalcLength + + +BOOL +MULTISZ::FindString( const WCHAR * str ) +{ + + WCHAR * multisz; + + // + // Sanity check. + // + + DBG_ASSERT( QueryStr() != NULL ); + DBG_ASSERT( str != NULL ); + DBG_ASSERT( *str != '\0' ); + + // + // Scan it. + // + + multisz = QueryStr(); + + while( *multisz != '\0' ) { + + if( !::wcscmp( multisz, str ) ) { + + return TRUE; + + } + + multisz += ::wcslen( multisz ) + 1; + + } + + return FALSE; + +} // MULTISZ::FindString + + +BOOL +MULTISZ::FindStringNoCase( const WCHAR * str ) +{ + + WCHAR * multisz; + + // + // Sanity check. + // + + DBG_ASSERT( QueryStr() != NULL ); + DBG_ASSERT( str != NULL ); + DBG_ASSERT( *str != '\0' ); + + // + // Scan it. + // + + multisz = QueryStr(); + + while( *multisz != '\0' ) { + + if( !_wcsicmp( multisz, str ) ) { + + return TRUE; + + } + + multisz += wcslen( multisz ) + 1; + + } + + return FALSE; + +} // MULTISZ::FindStringNoCase + + +VOID +MULTISZ::AuxInit( const WCHAR * pInit ) +{ + BOOL fRet; + + if ( pInit ) + { + DWORD cStrings; + int cbCopy = CalcLength( pInit, &cStrings ) * sizeof(WCHAR); + fRet = Resize( cbCopy ); + + if ( fRet ) { + CopyMemory( QueryPtr(), pInit, cbCopy ); + m_cchLen = (cbCopy)/sizeof(WCHAR); + m_cStrings = cStrings; + } else { +// BUFFER::SetValid( FALSE); + } + + } else { + + Reset(); + + } + +} // MULTISZ::AuxInit() + + +/******************************************************************* + + NAME: MULTISZ::AuxAppend + + SYNOPSIS: Appends the string onto the multisz. + + ENTRY: Object to append +********************************************************************/ + +BOOL MULTISZ::AuxAppend( const WCHAR * pStr, UINT cbStr, BOOL fAddSlop ) +{ + DBG_ASSERT( pStr != NULL ); + + UINT cbThis = QueryCB(); + + DBG_ASSERT( cbThis >= 2 ); + + if( cbThis == 4 ) { + + // + // It's empty, so start at the beginning. + // + + cbThis = 0; + + } else { + + // + // It's not empty, so back up over the final terminating NULL. + // + + cbThis -= sizeof(WCHAR); + + } + + // + // Only resize when we have to. When we do resize, we tack on + // some extra space to avoid extra reallocations. + // + // Note: QuerySize returns the requested size of the string buffer, + // *not* the strlen of the buffer + // + + //AcIncrement( CacMultiszAppend); + + // + // Check for the arithmetic overflow + // + // ( 2 * sizeof( WCHAR ) ) is for the double terminator + // + ULONGLONG cb64Required = (ULONGLONG)cbThis + cbStr + 2 * sizeof(WCHAR); + if ( cb64Required > MAXULONG ) + { + SetLastError( ERROR_ARITHMETIC_OVERFLOW ); + return FALSE; + } + if ( QuerySize() < (DWORD) cb64Required ) + { + ULONGLONG cb64AllocSize = cb64Required + (fAddSlop ? STR_SLOP : 0 ); + // + // Check for the arithmetic overflow + // + if ( cb64AllocSize > MAXULONG ) + { + SetLastError( ERROR_ARITHMETIC_OVERFLOW ); + return FALSE; + } + if ( !Resize( (DWORD) cb64AllocSize ) ) + return FALSE; + } + + // copy the exact string and tack on the double terminator + memcpy( (BYTE *) QueryPtr() + cbThis, + pStr, + cbStr); + *(WCHAR *)((BYTE *)QueryPtr() + cbThis + cbStr) = L'\0'; + *(WCHAR *)((BYTE *)QueryPtr() + cbThis + cbStr + sizeof(WCHAR) ) = L'\0'; + + m_cchLen = CalcLength( (const WCHAR *)QueryPtr(), &m_cStrings ); + return TRUE; + +} // MULTISZ::AuxAppend() + + +#if 0 + +BOOL +MULTISZ::CopyToBuffer( WCHAR * lpszBuffer, LPDWORD lpcch) const +/*++ + Description: + Copies the string into the WCHAR buffer passed in if the buffer + is sufficient to hold the translated string. + If the buffer is small, the function returns small and sets *lpcch + to contain the required number of characters. + + Arguments: + lpszBuffer pointer to WCHAR buffer which on return contains + the UNICODE version of string on success. + lpcch pointer to DWORD containing the length of the buffer. + If *lpcch == 0 then the function returns TRUE with + the count of characters required stored in *lpcch. + Also in this case lpszBuffer is not affected. + Returns: + TRUE on success. + FALSE on failure. Use GetLastError() for further details. +--*/ +{ + BOOL fReturn = TRUE; + + if ( lpcch == NULL) { + SetLastError( ERROR_INVALID_PARAMETER); + return ( FALSE); + } + + if ( *lpcch == 0) { + + // + // Inquiring the size of buffer alone + // + *lpcch = QueryCCH() + 1; // add one character for terminating null + } else { + + // + // Copy after conversion from ANSI to Unicode + // + int iRet; + iRet = MultiByteToWideChar( CP_ACP, + MB_PRECOMPOSED | MB_ERR_INVALID_CHARS, + QueryStrA(), QueryCCH() + 1, + lpszBuffer, (int )*lpcch); + + if ( iRet == 0 || iRet != (int ) *lpcch) { + + // + // Error in conversion. + // + fReturn = FALSE; + } + } + + return ( fReturn); +} // MULTISZ::CopyToBuffer() +#endif + +BOOL +MULTISZ::CopyToBuffer( __out_ecount_opt(*lpcch) WCHAR * lpszBuffer, LPDWORD lpcch) const +/*++ + Description: + Copies the string into the WCHAR buffer passed in if the buffer + is sufficient to hold the translated string. + If the buffer is small, the function returns small and sets *lpcch + to contain the required number of characters. + + Arguments: + lpszBuffer pointer to WCHAR buffer which on return contains + the string on success. + lpcch pointer to DWORD containing the length of the buffer. + If *lpcch == 0 then the function returns TRUE with + the count of characters required stored in lpcch. + Also in this case lpszBuffer is not affected. + Returns: + TRUE on success. + FALSE on failure. Use GetLastError() for further details. +--*/ +{ + BOOL fReturn = TRUE; + + if ( lpcch == NULL) { + SetLastError( ERROR_INVALID_PARAMETER); + return ( FALSE); + } + + register DWORD cch = QueryCCH(); + + if ( *lpcch >= cch) { + + DBG_ASSERT( lpszBuffer); + memcpy( lpszBuffer, QueryStr(), cch * sizeof(WCHAR)); + } else { + DBG_ASSERT( *lpcch < cch); + SetLastError( ERROR_INSUFFICIENT_BUFFER); + fReturn = FALSE; + } + + *lpcch = cch; + + return ( fReturn); +} // MULTISZ::CopyToBuffer() + +BOOL +MULTISZ::Equals( + MULTISZ* pmszRhs +) +// +// Compares this to pmszRhs, returns TRUE if equal +// +{ + DBG_ASSERT( NULL != pmszRhs ); + + PCWSTR pszLhs = First( ); + PCWSTR pszRhs = pmszRhs->First( ); + + if( m_cStrings != pmszRhs->m_cStrings ) + { + return FALSE; + } + + while( NULL != pszLhs ) + { + DBG_ASSERT( NULL != pszRhs ); + + if( 0 != wcscmp( pszLhs, pszRhs ) ) + { + return FALSE; + } + + pszLhs = Next( pszLhs ); + pszRhs = pmszRhs->Next( pszRhs ); + } + + return TRUE; +} + +HRESULT +SplitCommaDelimitedString( + PCWSTR pszList, + BOOL fTrimEntries, + BOOL fRemoveEmptyEntries, + MULTISZ * pmszList +) +/*++ + +Routine Description: + + Split comma delimited string into a multisz. Additional leading empty + entries after the first are discarded. + +Arguments: + + pszList - List to split up + fTrimEntries - Whether each entry should be trimmed before added to multisz + fRemoveEmptyEntries - Whether empty entires should be discarded + pmszList - Filled with MULTISZ list + +Return Value: + + HRESULT + +--*/ +{ + HRESULT hr = S_OK; + + if ( pszList == NULL || + pmszList == NULL ) + { + DBG_ASSERT( FALSE ); + hr = HRESULT_FROM_WIN32( ERROR_INVALID_PARAMETER ); + goto Finished; + } + + pmszList->Reset(); + + /* + pszCurrent: start of the current entry which may be the comma that + precedes the next entry if the entry is empty + + pszNext: the comma that precedes the next entry. If + pszCurrent == pszNext, then the entry is empty + + pszEnd: just past the end of the current entry + */ + + for ( PCWSTR pszCurrent = pszList, + pszNext = wcschr( pszCurrent, L',' ) + ; + ; + pszCurrent = pszNext + 1, + pszNext = wcschr( pszCurrent, L',' ) ) + { + PCWSTR pszEnd = NULL; + + if ( pszNext != NULL ) + { + pszEnd = pszNext; + } + else + { + pszEnd = pszCurrent + wcslen( pszCurrent ); + } + + if ( fTrimEntries ) + { + while ( pszCurrent < pszEnd && ISWHITE( pszCurrent[ 0 ] ) ) + { + pszCurrent++; + } + + while ( pszEnd > pszCurrent && ISWHITE( pszEnd[ -1 ] ) ) + { + pszEnd--; + } + } + + if ( pszCurrent != pszEnd || !fRemoveEmptyEntries ) + { + if ( !pmszList->Append( pszCurrent, (DWORD) ( pszEnd - pszCurrent ) ) ) + { + hr = HRESULT_FROM_WIN32( GetLastError() ); + goto Finished; + } + } + + if ( pszNext == NULL ) + { + break; + } + } + +Finished: + + return hr; +} + +#pragma warning(default:4267) \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/multisz.h b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/multisz.h new file mode 100644 index 0000000000..9473f52033 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/multisz.h @@ -0,0 +1,230 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#ifndef _MULTISZ_H_ +#define _MULTISZ_H_ + +#include "stringu.h" +#include "ntassert.h" +#include + +#pragma warning( push ) +#pragma warning ( disable : ALL_CODE_ANALYSIS_WARNINGS ) + +/*++ + class MULTISZ: + + Intention: + A light-weight multi-string class supporting encapsulated string class. + + This object is derived from BUFFER class. + It maintains following state: + + m_fValid - whether this object is valid - + used only by MULTISZ() init functions + * NYI: I need to kill this someday * + m_cchLen - string length cached when we update the string. + m_cStrings - number of strings. + + Member Functions: + There are two categories of functions: + 1) Safe Functions - which do integrity checking of state + 2) UnSafe Functions - which do not do integrity checking, but + enable writing to the data stream freely. + (someday this will be enabled as Safe versions without + problem for users) + +--*/ +class MULTISZ : public BUFFER +{ +public: + + MULTISZ() + : BUFFER (), + m_cchLen ( 0), + m_cStrings(0) + { Reset(); } + + // creates a stack version of the MULTISZ object - uses passed in stack buffer + // MULTISZ does not free this pbInit on its own. + MULTISZ( __in_bcount(cbInit) WCHAR * pbInit, DWORD cbInit) + : BUFFER( (BYTE *) pbInit, cbInit), + m_cchLen (0), + m_cStrings(0) + {} + + MULTISZ( const WCHAR * pchInit ) + : BUFFER (), + m_cchLen ( 0), + m_cStrings(0) + { AuxInit(pchInit); } + + MULTISZ( const MULTISZ & str ) + : BUFFER (), + m_cchLen ( 0), + m_cStrings(0) + { AuxInit( str.QueryStr()); } + +// BOOL IsValid(VOID) const { return ( BUFFER::IsValid()) ; } + // + // Checks and returns TRUE if this string has no valid data else FALSE + // + BOOL IsEmpty( VOID) const { return ( *QueryStr() == L'\0'); } + + BOOL Append( const WCHAR * pchInit ) { + return ((pchInit != NULL) ? (AuxAppend( pchInit, + (DWORD) (::wcslen(pchInit)) * sizeof(WCHAR) + )) : + TRUE); + } + + + BOOL Append( const WCHAR * pchInit, DWORD cchLen ) { + return ((pchInit != NULL) ? (AuxAppend( pchInit, + cchLen * sizeof(WCHAR))) : + TRUE); + } + + BOOL Append( STRU & str ) + { return AuxAppend( str.QueryStr(), + (str.QueryCCH()) * sizeof(WCHAR)); } + + // Resets the internal string to be NULL string. Buffer remains cached. + VOID Reset( VOID) + { DBG_ASSERT( QueryPtr() != NULL); + QueryStr()[0] = L'\0'; + QueryStr()[1] = L'\0'; + m_cchLen = 2; + m_cStrings = 0; + } + + BOOL Copy( const WCHAR * pchInit, IN DWORD cbLen ) { + if ( QueryPtr() ) { Reset(); } + return ( (pchInit != NULL) ? + AuxAppend( pchInit, cbLen, FALSE ): + TRUE); + } + + BOOL Copy( const MULTISZ & str ) + { return ( Copy(str.QueryStr(), str.QueryCB())); } + + // + // Returns the number of bytes in the string including the terminating + // NULLs + // + UINT QueryCB( VOID ) const + { return ( m_cchLen * sizeof(WCHAR)); } + + // + // Returns # of characters in the string including the terminating NULLs + // + UINT QueryCCH( VOID ) const { return (m_cchLen); } + + // + // Returns # of strings in the multisz. + // + + DWORD QueryStringCount( VOID ) const { return m_cStrings; } + + // + // Makes a copy of the stored string in given buffer + // + BOOL CopyToBuffer( __out_ecount_opt(*lpcch) WCHAR * lpszBuffer, LPDWORD lpcch) const; + + // + // Return the string buffer + // + WCHAR * QueryStrA( VOID ) const { return ( QueryStr()); } + WCHAR * QueryStr( VOID ) const { return ((WCHAR *) QueryPtr()); } + + // + // Makes a clone of the current string in the string pointer passed in. + // + BOOL + Clone( OUT MULTISZ * pstrClone) const + { + return ((pstrClone == NULL) ? + (SetLastError(ERROR_INVALID_PARAMETER), FALSE) : + (pstrClone->Copy( *this)) + ); + } // MULTISZ::Clone() + + // + // Recalculates the length of *this because we've modified the buffers + // directly + // + + VOID RecalcLen( VOID ) + { m_cchLen = MULTISZ::CalcLength( QueryStr(), &m_cStrings ); } + + // + // Calculate total character length of a MULTI_SZ, including the + // terminating NULLs. + // + + static DWORD CalcLength( const WCHAR * str, + LPDWORD pcStrings = NULL ); + + // + // Determine if the MULTISZ contains a specific string. + // + + BOOL FindString( const WCHAR * str ); + + BOOL FindString( STRU & str ) + { return FindString( str.QueryStr() ); } + + // + // Determine if the MULTISZ contains a specific string - case-insensitive + // + + BOOL FindStringNoCase( const WCHAR * str ); + + BOOL FindStringNoCase( STRU & str ) + { return FindStringNoCase( str.QueryStr() ); } + + // + // Used for scanning a multisz. + // + + const WCHAR * First( VOID ) const + { return *QueryStr() == L'\0' ? NULL : QueryStr(); } + + const WCHAR * Next( const WCHAR * Current ) const + { Current += ::wcslen( Current ) + 1; + return *Current == L'\0' ? NULL : Current; } + + BOOL + Equals( + MULTISZ* pmszRhs + ); + +private: + + DWORD m_cchLen; + DWORD m_cStrings; + VOID AuxInit( const WCHAR * pInit ); + BOOL AuxAppend( const WCHAR * pInit, + UINT cbStr, BOOL fAddSlop = TRUE ); + +}; + +// +// Quick macro for declaring a MULTISZ that will use stack memory of +// bytes. If the buffer overflows then a heap buffer will be allocated +// + +#define STACK_MULTISZ( name, size ) WCHAR __ach##name[size]; \ + MULTISZ name( __ach##name, sizeof( __ach##name )) + +HRESULT +SplitCommaDelimitedString( + PCWSTR pszList, + BOOL fTrimEntries, + BOOL fRemoveEmptyEntries, + MULTISZ * pmszList +); + +#pragma warning( pop ) + +#endif // !_MULTISZ_HXX_ diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/multisza.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/multisza.cpp new file mode 100644 index 0000000000..54717edf05 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/multisza.cpp @@ -0,0 +1,408 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma warning (disable : 4267) +#include "precomp.h" +#include "multisza.h" +#include + +// +// Private Definitions +// + +#define MAXULONG 4294967295 +#define ISWHITE( ch ) ((ch) == L' ' || (ch) == L'\t' || (ch) == L'\r') + +// +// When appending data, this is the extra amount we request to avoid +// reallocations +// +#define STR_SLOP 128 + + +DWORD +MULTISZA::CalcLength( const CHAR * str, + LPDWORD pcStrings ) +{ + DWORD count = 0; + DWORD total = 1; + DWORD len; + + while( *str ) { + len = ::strlen( str ) + 1; + total += len; + str += len; + count++; + } + + if( pcStrings != NULL ) { + *pcStrings = count; + } + + return total; + +} // MULTISZA::CalcLength + + +BOOL +MULTISZA::FindString( const CHAR * str ) +{ + + CHAR * multisz; + + // + // Sanity check. + // + + DBG_ASSERT( QueryStr() != NULL ); + DBG_ASSERT( str != NULL ); + DBG_ASSERT( *str != '\0' ); + + // + // Scan it. + // + + multisz = QueryStr(); + + while( *multisz != '\0' ) { + + if( !::strcmp( multisz, str ) ) { + + return TRUE; + + } + + multisz += ::strlen( multisz ) + 1; + + } + + return FALSE; + +} // MULTISZA::FindString + + +BOOL +MULTISZA::FindStringNoCase( const CHAR * str ) +{ + + CHAR * multisz; + + // + // Sanity check. + // + + DBG_ASSERT( QueryStr() != NULL ); + DBG_ASSERT( str != NULL ); + DBG_ASSERT( *str != '\0' ); + + // + // Scan it. + // + + multisz = QueryStr(); + + while( *multisz != '\0' ) { + + if( !_stricmp( multisz, str ) ) { + + return TRUE; + + } + + multisz += strlen( multisz ) + 1; + + } + + return FALSE; + +} // MULTISZA::FindStringNoCase + + +VOID +MULTISZA::AuxInit( const CHAR * pInit ) +{ + BOOL fRet; + + if ( pInit ) + { + DWORD cStrings; + int cbCopy = CalcLength( pInit, &cStrings ) * sizeof(CHAR); + fRet = Resize( cbCopy ); + + if ( fRet ) { + CopyMemory( QueryPtr(), pInit, cbCopy ); + m_cchLen = (cbCopy)/sizeof(CHAR); + m_cStrings = cStrings; + } else { +// BUFFER::SetValid( FALSE); + } + + } else { + + Reset(); + + } + +} // MULTISZA::AuxInit() + + +/******************************************************************* + + NAME: MULTISZA::AuxAppend + + SYNOPSIS: Appends the string onto the MULTISZA. + + ENTRY: Object to append +********************************************************************/ + +BOOL MULTISZA::AuxAppend( const CHAR * pStr, UINT cbStr, BOOL fAddSlop ) +{ + DBG_ASSERT( pStr != NULL ); + + UINT cbThis = QueryCB(); + + if( cbThis == 2 ) { + + // + // It's empty, so start at the beginning. + // + + cbThis = 0; + + } else { + + // + // It's not empty, so back up over the final terminating NULL. + // + + cbThis -= sizeof(CHAR); + + } + + // + // Only resize when we have to. When we do resize, we tack on + // some extra space to avoid extra reallocations. + // + // Note: QuerySize returns the requested size of the string buffer, + // *not* the strlen of the buffer + // + + //AcIncrement( CacMultiszAppend); + + // + // Check for the arithmetic overflow + // + // ( 2 * sizeof( CHAR ) ) is for the double terminator + // + ULONGLONG cb64Required = (ULONGLONG)cbThis + cbStr + 2 * sizeof(CHAR); + if ( cb64Required > MAXULONG ) + { + SetLastError( ERROR_ARITHMETIC_OVERFLOW ); + return FALSE; + } + if ( QuerySize() < (DWORD) cb64Required ) + { + ULONGLONG cb64AllocSize = cb64Required + (fAddSlop ? STR_SLOP : 0 ); + // + // Check for the arithmetic overflow + // + if ( cb64AllocSize > MAXULONG ) + { + SetLastError( ERROR_ARITHMETIC_OVERFLOW ); + return FALSE; + } + if ( !Resize( (DWORD) cb64AllocSize ) ) + return FALSE; + } + + // copy the exact string and tack on the double terminator + memcpy( (BYTE *) QueryPtr() + cbThis, + pStr, + cbStr); + *(CHAR *)((BYTE *)QueryPtr() + cbThis + cbStr) = L'\0'; + *(CHAR *)((BYTE *)QueryPtr() + cbThis + cbStr + sizeof(CHAR) ) = L'\0'; + + m_cchLen = CalcLength( (const CHAR *)QueryPtr(), &m_cStrings ); + return TRUE; + +} // MULTISZA::AuxAppend() + +BOOL +MULTISZA::CopyToBuffer( __out_ecount_opt(*lpcch) CHAR * lpszBuffer, LPDWORD lpcch) const +/*++ + Description: + Copies the string into the CHAR buffer passed in if the buffer + is sufficient to hold the translated string. + If the buffer is small, the function returns small and sets *lpcch + to contain the required number of characters. + + Arguments: + lpszBuffer pointer to CHAR buffer which on return contains + the string on success. + lpcch pointer to DWORD containing the length of the buffer. + If *lpcch == 0 then the function returns TRUE with + the count of characters required stored in lpcch. + Also in this case lpszBuffer is not affected. + Returns: + TRUE on success. + FALSE on failure. Use GetLastError() for further details. +--*/ +{ + BOOL fReturn = TRUE; + + if ( lpcch == NULL) { + SetLastError( ERROR_INVALID_PARAMETER); + return ( FALSE); + } + + register DWORD cch = QueryCCH(); + + if ( *lpcch >= cch) { + + DBG_ASSERT( lpszBuffer); + memcpy( lpszBuffer, QueryStr(), cch * sizeof(CHAR)); + } else { + DBG_ASSERT( *lpcch < cch); + SetLastError( ERROR_INSUFFICIENT_BUFFER); + fReturn = FALSE; + } + + *lpcch = cch; + + return ( fReturn); +} // MULTISZA::CopyToBuffer() + +BOOL +MULTISZA::Equals( + MULTISZA* pmszRhs +) +// +// Compares this to pmszRhs, returns TRUE if equal +// +{ + DBG_ASSERT( NULL != pmszRhs ); + + PCSTR pszLhs = First( ); + PCSTR pszRhs = pmszRhs->First( ); + + if( m_cStrings != pmszRhs->m_cStrings ) + { + return FALSE; + } + + while( NULL != pszLhs ) + { + DBG_ASSERT( NULL != pszRhs ); + + if( 0 != strcmp( pszLhs, pszRhs ) ) + { + return FALSE; + } + + pszLhs = Next( pszLhs ); + pszRhs = pmszRhs->Next( pszRhs ); + } + + return TRUE; +} + +HRESULT +SplitCommaDelimitedString( + PCSTR pszList, + BOOL fTrimEntries, + BOOL fRemoveEmptyEntries, + MULTISZA * pmszList +) +/*++ + +Routine Description: + + Split comma delimited string into a MULTISZA. Additional leading empty + entries after the first are discarded. + +Arguments: + + pszList - List to split up + fTrimEntries - Whether each entry should be trimmed before added to MULTISZA + fRemoveEmptyEntries - Whether empty entires should be discarded + pmszList - Filled with MULTISZA list + +Return Value: + + HRESULT + +--*/ +{ + HRESULT hr = S_OK; + + if ( pszList == NULL || + pmszList == NULL ) + { + DBG_ASSERT( FALSE ); + hr = HRESULT_FROM_WIN32( ERROR_INVALID_PARAMETER ); + goto Finished; + } + + pmszList->Reset(); + + /* + pszCurrent: start of the current entry which may be the comma that + precedes the next entry if the entry is empty + + pszNext: the comma that precedes the next entry. If + pszCurrent == pszNext, then the entry is empty + + pszEnd: just past the end of the current entry + */ + + for ( PCSTR pszCurrent = pszList, + pszNext = strchr( pszCurrent, L',' ) + ; + ; + pszCurrent = pszNext + 1, + pszNext = strchr( pszCurrent, L',' ) ) + { + PCSTR pszEnd = NULL; + + if ( pszNext != NULL ) + { + pszEnd = pszNext; + } + else + { + pszEnd = pszCurrent + strlen( pszCurrent ); + } + + if ( fTrimEntries ) + { + while ( pszCurrent < pszEnd && ISWHITE( pszCurrent[ 0 ] ) ) + { + pszCurrent++; + } + + while ( pszEnd > pszCurrent && ISWHITE( pszEnd[ -1 ] ) ) + { + pszEnd--; + } + } + + if ( pszCurrent != pszEnd || !fRemoveEmptyEntries ) + { + if ( !pmszList->Append( pszCurrent, (DWORD) ( pszEnd - pszCurrent ) ) ) + { + hr = HRESULT_FROM_WIN32( GetLastError() ); + goto Finished; + } + } + + if ( pszNext == NULL ) + { + break; + } + } + +Finished: + + return hr; +} +#pragma warning(default:4267) \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/multisza.h b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/multisza.h new file mode 100644 index 0000000000..d575ec9423 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/multisza.h @@ -0,0 +1,226 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#ifndef _MULTISZA_H_ +#define _MULTISZA_H_ + +#include +#include "stringa.h" + + +/*++ + class MULTISZ: + + Intention: + A light-weight multi-string class supporting encapsulated string class. + + This object is derived from BUFFER class. + It maintains following state: + + m_fValid - whether this object is valid - + used only by MULTISZ() init functions + * NYI: I need to kill this someday * + m_cchLen - string length cached when we update the string. + m_cStrings - number of strings. + + Member Functions: + There are two categories of functions: + 1) Safe Functions - which do integrity checking of state + 2) UnSafe Functions - which do not do integrity checking, but + enable writing to the data stream freely. + (someday this will be enabled as Safe versions without + problem for users) + +--*/ +class MULTISZA : public BUFFER +{ +public: + + MULTISZA() + : BUFFER (), + m_cchLen ( 0), + m_cStrings(0) + { Reset(); } + + // creates a stack version of the MULTISZA object - uses passed in stack buffer + // MULTISZA does not free this pbInit on its own. + MULTISZA( __in_bcount(cbInit) CHAR * pbInit, DWORD cbInit) + : BUFFER( (BYTE *) pbInit, cbInit), + m_cchLen (0), + m_cStrings(0) + {} + + MULTISZA( const CHAR * pchInit ) + : BUFFER (), + m_cchLen ( 0), + m_cStrings(0) + { AuxInit(pchInit); } + + MULTISZA( const MULTISZA & str ) + : BUFFER (), + m_cchLen ( 0), + m_cStrings(0) + { AuxInit( str.QueryStr()); } + +// BOOL IsValid(VOID) const { return ( BUFFER::IsValid()) ; } + // + // Checks and returns TRUE if this string has no valid data else FALSE + // + BOOL IsEmpty( VOID) const { return ( *QueryStr() == L'\0'); } + + BOOL Append( const CHAR * pchInit ) { + return ((pchInit != NULL) ? (AuxAppend( pchInit, + (DWORD) (::strlen(pchInit)) * sizeof(CHAR) + )) : + TRUE); + } + + + BOOL Append( const CHAR * pchInit, DWORD cchLen ) { + return ((pchInit != NULL) ? (AuxAppend( pchInit, + cchLen * sizeof(CHAR))) : + TRUE); + } + + BOOL Append( STRA & str ) + { return AuxAppend( str.QueryStr(), + (str.QueryCCH()) * sizeof(CHAR)); } + + // Resets the internal string to be NULL string. Buffer remains cached. + VOID Reset( VOID) + { DBG_ASSERT( QueryPtr() != NULL); + QueryStr()[0] = L'\0'; + QueryStr()[1] = L'\0'; + m_cchLen = 2; + m_cStrings = 0; + } + + BOOL Copy( const CHAR * pchInit, IN DWORD cbLen ) { + if ( QueryPtr() ) { Reset(); } + return ( (pchInit != NULL) ? + AuxAppend( pchInit, cbLen, FALSE ): + TRUE); + } + + BOOL Copy( const MULTISZA & str ) + { return ( Copy(str.QueryStr(), str.QueryCB())); } + + // + // Returns the number of bytes in the string including the terminating + // NULLs + // + UINT QueryCB( VOID ) const + { return ( m_cchLen * sizeof(CHAR)); } + + // + // Returns # of characters in the string including the terminating NULLs + // + UINT QueryCCH( VOID ) const { return (m_cchLen); } + + // + // Returns # of strings in the MULTISZA. + // + + DWORD QueryStringCount( VOID ) const { return m_cStrings; } + + // + // Makes a copy of the stored string in given buffer + // + BOOL CopyToBuffer( __out_ecount_opt(*lpcch) CHAR * lpszBuffer, LPDWORD lpcch) const; + + // + // Return the string buffer + // + CHAR * QueryStrA( VOID ) const { return ( QueryStr()); } + CHAR * QueryStr( VOID ) const { return ((CHAR *) QueryPtr()); } + + // + // Makes a clone of the current string in the string pointer passed in. + // + BOOL + Clone( OUT MULTISZA * pstrClone) const + { + return ((pstrClone == NULL) ? + (SetLastError(ERROR_INVALID_PARAMETER), FALSE) : + (pstrClone->Copy( *this)) + ); + } // MULTISZA::Clone() + + // + // Recalculates the length of *this because we've modified the buffers + // directly + // + + VOID RecalcLen( VOID ) + { m_cchLen = MULTISZA::CalcLength( QueryStr(), &m_cStrings ); } + + // + // Calculate total character length of a MULTI_SZ, including the + // terminating NULLs. + // + + static DWORD CalcLength( const CHAR * str, + LPDWORD pcStrings = NULL ); + + // + // Determine if the MULTISZA contains a specific string. + // + + BOOL FindString( const CHAR * str ); + + BOOL FindString( STRA & str ) + { return FindString( str.QueryStr() ); } + + // + // Determine if the MULTISZA contains a specific string - case-insensitive + // + + BOOL FindStringNoCase( const CHAR * str ); + + BOOL FindStringNoCase( STRA & str ) + { return FindStringNoCase( str.QueryStr() ); } + + // + // Used for scanning a MULTISZA. + // + + const CHAR * First( VOID ) const + { return *QueryStr() == L'\0' ? NULL : QueryStr(); } + + const CHAR * Next( const CHAR * Current ) const + { Current += ::strlen( Current ) + 1; + return *Current == L'\0' ? NULL : Current; } + + BOOL + Equals( + MULTISZA* pmszRhs + ); + +private: + + DWORD m_cchLen; + DWORD m_cStrings; + VOID AuxInit( const CHAR * pInit ); + BOOL AuxAppend( const CHAR * pInit, + UINT cbStr, BOOL fAddSlop = TRUE ); + +}; + +// +// Quick macro for declaring a MULTISZA that will use stack memory of +// bytes. If the buffer overflows then a heap buffer will be allocated +// + +#define STACK_MULTISZA( name, size ) CHAR __ach##name[size]; \ + MULTISZA name( __ach##name, sizeof( __ach##name )) + +HRESULT +SplitCommaDelimitedString( + PCSTR pszList, + BOOL fTrimEntries, + BOOL fRemoveEmptyEntries, + MULTISZA * pmszList +); + +#endif // !_MULTISZA_HXX_ + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/ntassert.h b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/ntassert.h new file mode 100644 index 0000000000..8e19e03239 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/ntassert.h @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#ifdef _ASSERTE + #undef _ASSERTE +#endif + +#ifdef ASSERT + #undef ASSERT +#endif + +#ifdef DEBUG + #define DBG 1 +#endif + +#if defined( DBG ) && DBG + #define SX_ASSERT( _x ) ( (VOID)( ( ( _x ) ) ? TRUE : ( __annotation( L"Debug", L"AssertFail", L#_x ), DbgRaiseAssertionFailure(), FALSE ) ) ) + #define SX_ASSERTMSG( _m, _x ) ( (VOID)( ( ( _x ) ) ? TRUE : ( __annotation( L"Debug", L"AssertFail", L##_m ), DbgRaiseAssertionFailure(), FALSE ) ) ) + #define SX_VERIFY( _x ) SX_ASSERT( _x ) + #define _ASSERTE( _x ) SX_ASSERT( _x ) + #define ASSERT( _x ) SX_ASSERT( _x ) + #define assert( _x ) SX_ASSERT( _x ) + #define DBG_ASSERT( _x ) SX_ASSERT( _x ) + #define DBG_REQUIRE( _x ) SX_ASSERT( _x ) +#else + #define SX_ASSERT( _x ) ( (VOID)0 ) + #define SX_ASSERTMSG( _m, _x ) ( (VOID)0 ) + #define SX_VERIFY( _x ) ( (VOID)( ( _x ) ? TRUE : FALSE ) ) + #define _ASSERTE( _x ) ( (VOID)0 ) + #define assert( _x ) ( (VOID)0 ) + #define DBG_ASSERT( _x ) ( (VOID)0 ) + #define DBG_REQUIRE( _x ) ((VOID)(_x)) +#endif + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/percpu.h b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/percpu.h new file mode 100644 index 0000000000..07828830d7 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/percpu.h @@ -0,0 +1,310 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#pragma warning( push ) +#pragma warning ( disable : 26451 ) + +template +class PER_CPU +{ +public: + + template + inline + static + HRESULT + Create( + FunctionInitializer Initializer, + __deref_out PER_CPU ** ppInstance + ); + + inline + T * + GetLocal( + VOID + ); + + template + inline + VOID + ForEach( + FunctionForEach Function + ); + + inline + VOID + Dispose( + VOID + ); + +private: + + PER_CPU( + VOID + ) + { + // + // Don't perform any operation during constructor. + // Constructor will never be called. + // + } + + ~PER_CPU( + VOID + ) + { + // + // Don't perform any operation during destructor. + // Constructor will never be called. + // + } + + template + HRESULT + Initialize( + FunctionInitializer Initializer, + DWORD NumberOfVariables, + DWORD Alignment + ); + + T * + GetObject( + DWORD Index + ); + + static + HRESULT + GetProcessorInformation( + __out DWORD * pCacheLineSize, + __out DWORD * pNumberOfProcessors + ); + + // + // Pointer to the begining of the inlined array. + // + PVOID m_pVariables; + SIZE_T m_Alignment; + SIZE_T m_VariablesCount; +}; + +template +template +inline +// static +HRESULT +PER_CPU::Create( + FunctionInitializer Initializer, + __deref_out PER_CPU ** ppInstance +) +{ + HRESULT hr = S_OK; + DWORD CacheLineSize = 0; + DWORD ObjectCacheLineSize = 0; + DWORD NumberOfProcessors = 0; + PER_CPU * pInstance = NULL; + + hr = GetProcessorInformation(&CacheLineSize, + &NumberOfProcessors); + if (FAILED(hr)) + { + goto Finished; + } + + if (sizeof(T) > CacheLineSize) + { + // + // Round to the next multiple of the cache line size. + // + ObjectCacheLineSize = (sizeof(T) + CacheLineSize-1) & (CacheLineSize-1); + } + else + { + ObjectCacheLineSize = CacheLineSize; + } + + // + // Calculate the size of the PER_CPU object, including the array. + // The first cache line is for the member variables and the array + // starts in the next cache line. + // + SIZE_T Size = CacheLineSize + NumberOfProcessors * ObjectCacheLineSize; + + pInstance = (PER_CPU*) _aligned_malloc(Size, CacheLineSize); + if (pInstance == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + ZeroMemory(pInstance, Size); + + // + // The array start in the 2nd cache line. + // + pInstance->m_pVariables = reinterpret_cast(pInstance) + CacheLineSize; + + // + // Pass a disposer for disposing initialized items in case of failure. + // + hr = pInstance->Initialize(Initializer, + NumberOfProcessors, + ObjectCacheLineSize); + if (FAILED(hr)) + { + goto Finished; + } + + *ppInstance = pInstance; + pInstance = NULL; + +Finished: + + if (pInstance != NULL) + { + // + // Free the instance without disposing it. + // + pInstance->Dispose(); + pInstance = NULL; + } + + return hr; +} + +template +inline +T * +PER_CPU::GetLocal( + VOID +) +{ + // Use GetCurrentProcessorNumber (up to 64 logical processors) instead of + // GetCurrentProcessorNumberEx (more than 64 logical processors) because + // the number of processors are not densely packed per group. + // The idea of distributing variables per CPU is to have + // a scalability multiplier (could be NUMA node instead). + // + // Make sure the index don't go beyond the array size, if that happens, + // there won't be even distribution, but still better + // than one single variable. + // + return GetObject(GetCurrentProcessorNumber()); +} + +template +inline +T * +PER_CPU::GetObject( + DWORD Index +) +{ + return reinterpret_cast(static_cast(m_pVariables) + Index * m_Alignment); +} + +template +template +inline +VOID +PER_CPU::ForEach( + FunctionForEach Function +) +{ + for(DWORD Index = 0; Index < m_VariablesCount; ++Index) + { + T * pObject = GetObject(Index); + Function(pObject); + } +} + +template +VOID +PER_CPU::Dispose( + VOID +) +{ + _aligned_free(this); +} + +template +template +inline +HRESULT +PER_CPU::Initialize( + FunctionInitializer Initializer, + DWORD NumberOfVariables, + DWORD Alignment +) +/*++ + +Routine Description: + + Initialize each object using the initializer function. + If initialization for any object fails, it dispose the + objects that were successfully initialized. + +Arguments: + + Initializer - Function for initialize one object. + Signature: HRESULT Func(T*) + Dispose - Function for disposing initialized objects in case of failure. + Signature: void Func(T*) + NumberOfVariables - The length of the array of variables. + Alignment - Alignment to use for avoiding false sharing. + +Return: + + HRESULT - E_OUTOFMEMORY + +--*/ +{ + HRESULT hr = S_OK; + DWORD Index = 0; + + m_VariablesCount = NumberOfVariables; + m_Alignment = Alignment; + + for (; Index < m_VariablesCount; ++Index) + { + T * pObject = GetObject(Index); + Initializer(pObject); + } + + return hr; +} + +template +// static +HRESULT +PER_CPU::GetProcessorInformation( + __out DWORD * pCacheLineSize, + __out DWORD * pNumberOfProcessors +) +/*++ + +Routine Description: + + Gets the CPU cache-line size for the current system. + This information is used for avoiding CPU false sharing. + +Arguments: + + pCacheLineSize - The processor cache-line size. + pNumberOfProcessors - Maximum number of processors per group. + +Return: + + HRESULT - E_OUTOFMEMORY + +--*/ +{ + SYSTEM_INFO SystemInfo = { }; + + GetSystemInfo(&SystemInfo); + *pNumberOfProcessors = SystemInfo.dwNumberOfProcessors; + *pCacheLineSize = SYSTEM_CACHE_ALIGNMENT_SIZE; + + return S_OK; +} + +#pragma warning( pop ) diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/precomp.h b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/precomp.h new file mode 100644 index 0000000000..9cccea4045 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/precomp.h @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include +#include +#pragma warning( disable:4127 ) +#include +#include +#include +#include +#include +#include + +#include "macros.h" +#include "stringu.h" +#include "stringa.h" +#include "dbgutil.h" +#include "ntassert.h" +#include "ahutil.h" +#include "acache.h" +//#include "base64.hxx" + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/prime.h b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/prime.h new file mode 100644 index 0000000000..6a6a88ed78 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/prime.h @@ -0,0 +1,85 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include +#include + +// +// Pre-calculated prime numbers (up to 10,049,369). +// +extern __declspec(selectany) const DWORD g_Primes [] = { + 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, + 761, 919, 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, + 12143, 14591, 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, + 130363, 156437, 187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, + 968897, 1162687, 1395263, 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, + 5999471, 7199369, 7849369, 8649369, 9249369, 10049369 +}; + +class PRIME +{ +public: + + static + DWORD + GetPrime( + DWORD dwMinimum + ) + { + // + // Try to use the precalculated numbers. + // + for ( DWORD Index = 0; Index < _countof( g_Primes ); Index++ ) + { + DWORD dwCandidate = g_Primes[Index]; + if ( dwCandidate >= dwMinimum ) + { + return dwCandidate; + } + } + + // + // Do calculation. + // + for ( DWORD dwCandidate = dwMinimum | 1; + dwCandidate < MAXDWORD; + dwCandidate += 2 ) + { + if ( IsPrime( dwCandidate ) ) + { + return dwCandidate; + } + } + return dwMinimum; + } + +private: + + static + BOOL + IsPrime( + DWORD dwCandidate + ) + { + if ((dwCandidate & 1) == 0) + { + return ( dwCandidate == 2 ); + } + + DWORD dwMax = static_cast(sqrt(static_cast(dwCandidate))); + + for ( DWORD Index = 3; Index <= dwMax; Index += 2 ) + { + if ( (dwCandidate % Index) == 0 ) + { + return FALSE; + } + } + return TRUE; + } + + PRIME() {} + ~PRIME() {} +}; \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/reftrace.c b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/reftrace.c new file mode 100644 index 0000000000..877d358c76 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/reftrace.c @@ -0,0 +1,228 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include +#include "dbgutil.h" +#include "reftrace.h" + + +PTRACE_LOG +CreateRefTraceLog( + IN LONG LogSize, + IN LONG ExtraBytesInHeader + ) +/*++ + +Routine Description: + + Creates a new (empty) ref count trace log buffer. + +Arguments: + + LogSize - The number of entries in the log. + + ExtraBytesInHeader - The number of extra bytes to include in the + log header. This is useful for adding application-specific + data to the log. + +Return Value: + + PTRACE_LOG - Pointer to the newly created log if successful, + NULL otherwise. + +--*/ +{ + + return CreateTraceLog( + LogSize, + ExtraBytesInHeader, + sizeof(REF_TRACE_LOG_ENTRY) + ); + +} // CreateRefTraceLog + + +VOID +DestroyRefTraceLog( + IN PTRACE_LOG Log + ) +/*++ + +Routine Description: + + Destroys a ref count trace log buffer created with CreateRefTraceLog(). + +Arguments: + + Log - The ref count trace log buffer to destroy. + +Return Value: + + None. + +--*/ +{ + + DestroyTraceLog( Log ); + +} // DestroyRefTraceLog + + +// +// N.B. For RtlCaptureBacktrace() to work properly, the calling function +// *must* be __cdecl, and must have a "normal" stack frame. So, we decorate +// WriteRefTraceLog[Ex]() with the __cdecl modifier and disable the frame +// pointer omission (FPO) optimization. +// + +//#pragma optimize( "y", off ) // disable frame pointer omission (FPO) +#pragma optimize( "", off ) // disable frame pointer omission (FPO) + +LONG +__cdecl +WriteRefTraceLog( + IN PTRACE_LOG Log, + IN LONG NewRefCount, + IN CONST VOID * Context + ) +/*++ + +Routine Description: + + Writes a new entry to the specified ref count trace log. The entry + written contains the updated reference count and a stack backtrace + leading up to the current caller. + +Arguments: + + Log - The log to write to. + + NewRefCount - The updated reference count. + + Context - An uninterpreted context to associate with the log entry. + +Return Value: + + Index of entry in log. + +--*/ +{ + + return WriteRefTraceLogEx( + Log, + NewRefCount, + Context, + REF_TRACE_EMPTY_CONTEXT, // suppress use of optional extra contexts + REF_TRACE_EMPTY_CONTEXT, + REF_TRACE_EMPTY_CONTEXT + ); + +} // WriteRefTraceLog + + + + +LONG +__cdecl +WriteRefTraceLogEx( + IN PTRACE_LOG Log, + IN LONG NewRefCount, + IN CONST VOID * Context, + IN CONST VOID * Context1, // optional extra context + IN CONST VOID * Context2, // optional extra context + IN CONST VOID * Context3 // optional extra context + ) +/*++ + +Routine Description: + + Writes a new "extended" entry to the specified ref count trace log. + The entry written contains the updated reference count, stack backtrace + leading up to the current caller and extra context information. + +Arguments: + + Log - The log to write to. + + NewRefCount - The updated reference count. + + Context - An uninterpreted context to associate with the log entry. + Context1 - An uninterpreted context to associate with the log entry. + Context2 - An uninterpreted context to associate with the log entry. + Context3 - An uninterpreted context to associate with the log entry. + + NOTE Context1/2/3 are "optional" in that the caller may suppress + debug display of these values by passing REF_TRACE_EMPTY_CONTEXT + for each of them. + +Return Value: + + Index of entry in log. + +--*/ +{ + + REF_TRACE_LOG_ENTRY entry; + ULONG hash; + DWORD cStackFramesSkipped; + + // + // Initialize the entry. + // + + RtlZeroMemory( + &entry, + sizeof(entry) + ); + + // + // Set log entry members. + // + + entry.NewRefCount = NewRefCount; + entry.Context = Context; + entry.Thread = GetCurrentThreadId(); + entry.Context1 = Context1; + entry.Context2 = Context2; + entry.Context3 = Context3; + + // + // Capture the stack backtrace. Normally, we skip two stack frames: + // one for this routine, and one for RtlCaptureBacktrace() itself. + // For non-Ex callers who come in via WriteRefTraceLog, + // we skip three stack frames. + // + + if ( entry.Context1 == REF_TRACE_EMPTY_CONTEXT + && entry.Context2 == REF_TRACE_EMPTY_CONTEXT + && entry.Context3 == REF_TRACE_EMPTY_CONTEXT + ) { + + cStackFramesSkipped = 2; + + } else { + + cStackFramesSkipped = 1; + + } + + RtlCaptureStackBackTrace( + cStackFramesSkipped, + REF_TRACE_LOG_STACK_DEPTH, + entry.Stack, + &hash + ); + + // + // Write it to the log. + // + + return WriteTraceLog( + Log, + &entry + ); + +} // WriteRefTraceLogEx + +#pragma optimize( "", on ) // restore frame pointer omission (FPO) + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/reftrace.h b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/reftrace.h new file mode 100644 index 0000000000..e90ca0444a --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/reftrace.h @@ -0,0 +1,87 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#ifndef _REFTRACE_H_ +#define _REFTRACE_H_ + + +#if defined(__cplusplus) +extern "C" { +#endif // __cplusplus + +#include +#include "tracelog.h" + +// +// This is the number of stack backtrace values captured in each +// trace log entry. This value is chosen to make the log entry +// exactly twelve dwords long, making it a bit easier to interpret +// from within the debugger without the debugger extension. +// + +#define REF_TRACE_LOG_STACK_DEPTH 9 + +// No-op value for the Context1,2,3 parameters of WriteRefTraceLogEx +//#define REF_TRACE_EMPTY_CONTEXT ((PVOID) -1) +#define REF_TRACE_EMPTY_CONTEXT NULL + + +// +// This defines the entry written to the trace log. +// + +typedef struct _REF_TRACE_LOG_ENTRY { + + LONG NewRefCount; + CONST VOID * Context; + CONST VOID * Context1; + CONST VOID * Context2; + CONST VOID * Context3; + DWORD Thread; + PVOID Stack[REF_TRACE_LOG_STACK_DEPTH]; + +} REF_TRACE_LOG_ENTRY, *PREF_TRACE_LOG_ENTRY; + + +// +// Manipulators. +// + +PTRACE_LOG +CreateRefTraceLog( + IN LONG LogSize, + IN LONG ExtraBytesInHeader + ); + +VOID +DestroyRefTraceLog( + IN PTRACE_LOG Log + ); + +LONG +__cdecl +WriteRefTraceLog( + IN PTRACE_LOG Log, + IN LONG NewRefCount, + IN CONST VOID * Context + ); + +LONG +__cdecl +WriteRefTraceLogEx( + IN PTRACE_LOG Log, + IN LONG NewRefCount, + IN CONST VOID * Context, + IN CONST VOID * Context1, + IN CONST VOID * Context2, + IN CONST VOID * Context3 + ); + + +#if defined(__cplusplus) +} // extern "C" +#endif // __cplusplus + + +#endif // _REFTRACE_H_ + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/rwlock.h b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/rwlock.h new file mode 100644 index 0000000000..dc7ccf834b --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/rwlock.h @@ -0,0 +1,193 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#if (_WIN32_WINNT < 0x600) + +// +// XP implementation. +// +class CWSDRWLock +{ +public: + + CWSDRWLock() + : m_bInited(FALSE) + { + } + + ~CWSDRWLock() + { + if (m_bInited) + { + DeleteCriticalSection(&m_rwLock.critsec); + CloseHandle(m_rwLock.ReadersDoneEvent); + } + } + + BOOL QueryInited() const + { + return m_bInited; + } + + HRESULT Init() + { + HRESULT hr = S_OK; + + if (FALSE == m_bInited) + { + m_rwLock.fWriterWaiting = FALSE; + m_rwLock.LockCount = 0; + if ( !InitializeCriticalSectionAndSpinCount( &m_rwLock.critsec, 0 )) + { + DWORD dwError = GetLastError(); + hr = HRESULT_FROM_WIN32(dwError); + return hr; + } + + m_rwLock.ReadersDoneEvent = CreateEvent(NULL, FALSE, FALSE, NULL); + if( NULL == m_rwLock.ReadersDoneEvent ) + { + DWORD dwError = GetLastError(); + hr = HRESULT_FROM_WIN32(dwError); + DeleteCriticalSection(&m_rwLock.critsec); + return hr; + } + m_bInited = TRUE; + } + + return hr; + } + + void SharedAcquire() + { + EnterCriticalSection(&m_rwLock.critsec); + InterlockedIncrement(&m_rwLock.LockCount); + LeaveCriticalSection(&m_rwLock.critsec); + } + + void SharedRelease() + { + ReleaseRWLock(); + } + + void ExclusiveAcquire() + { + EnterCriticalSection( &m_rwLock.critsec ); + + m_rwLock.fWriterWaiting = TRUE; + + // check if there are any readers active + if ( InterlockedExchangeAdd( &m_rwLock.LockCount, 0 ) > 0 ) + { + // + // Wait for all the readers to get done.. + // + WaitForSingleObject( m_rwLock.ReadersDoneEvent, INFINITE ); + } + m_rwLock.LockCount = -1; + } + + void ExclusiveRelease() + { + ReleaseRWLock(); + } + +private: + + BOOL m_bInited; + + typedef struct _RW_LOCK + { + BOOL fWriterWaiting; // Is a writer waiting on the lock? + LONG LockCount; + CRITICAL_SECTION critsec; + HANDLE ReadersDoneEvent; + } RW_LOCK, *PRW_LOCK; + + RW_LOCK m_rwLock; + +private: + + void ReleaseRWLock() + { + LONG Count = InterlockedDecrement( &m_rwLock.LockCount ); + + if ( 0 <= Count ) + { + // releasing a read lock + if (( m_rwLock.fWriterWaiting ) && ( 0 == Count )) + { + SetEvent( m_rwLock.ReadersDoneEvent ); + } + } + else + { + // Releasing a write lock + m_rwLock.LockCount = 0; + m_rwLock.fWriterWaiting = FALSE; + LeaveCriticalSection(&m_rwLock.critsec); + } + } +}; + +#else + +// +// Implementation for Windows Vista or greater. +// +class CWSDRWLock +{ +public: + + CWSDRWLock() + { + InitializeSRWLock(&m_rwLock); + } + + BOOL QueryInited() + { + return TRUE; + } + + + HRESULT Init() + { + // + // Method defined to keep compatibility with CWSDRWLock class for XP. + // + return S_OK; + } + + void SharedAcquire() + { + AcquireSRWLockShared(&m_rwLock); + } + + void SharedRelease() + { + ReleaseSRWLockShared(&m_rwLock); + } + + void ExclusiveAcquire() + { + AcquireSRWLockExclusive(&m_rwLock); + } + + void ExclusiveRelease() + { + ReleaseSRWLockExclusive(&m_rwLock); + } + +private: + + SRWLOCK m_rwLock; +}; + +#endif + +// +// Rename the lock class to a more clear name. +// +typedef CWSDRWLock READ_WRITE_LOCK; \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/stringa.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/stringa.cpp new file mode 100644 index 0000000000..29da773bca --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/stringa.cpp @@ -0,0 +1,1767 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "precomp.h" + +STRA::STRA( + VOID +) : m_cchLen( 0 ) +{ + *( QueryStr() ) = '\0'; +} + +STRA::STRA( + __inout_ecount(cchInit) CHAR* pbInit, + __in DWORD cchInit +) : m_Buff( pbInit, cchInit * sizeof( CHAR ) ), + m_cchLen(0) +/*++ + Description: + + Used by STACK_STRA. Initially populates underlying buffer with pbInit. + + pbInit is not freed. + + Arguments: + + pbInit - initial memory to use + cchInit - count, in characters, of pbInit + + Returns: + + None. + +--*/ +{ + _ASSERTE( NULL != pbInit ); + _ASSERTE( cchInit > 0 ); + _ASSERTE( pbInit[0] == '\0' ); +} + +BOOL +STRA::IsEmpty( + VOID +) const +{ + return ( m_cchLen == 0 ); +} + +BOOL +STRA::Equals( + __in PCSTR pszRhs, + __in BOOL fIgnoreCase /*= FALSE*/ +) const +{ + _ASSERTE( NULL != pszRhs ); + + if( fIgnoreCase ) + { + return ( 0 == _stricmp( QueryStr(), pszRhs ) ); + } + + return ( 0 == strcmp( QueryStr(), pszRhs ) ); +} + +BOOL +STRA::Equals( + __in const STRA * pstrRhs, + __in BOOL fIgnoreCase /*= FALSE*/ +) const +{ + _ASSERTE( NULL != pstrRhs ); + return Equals( pstrRhs->QueryStr(), fIgnoreCase ); +} + +BOOL +STRA::Equals( + __in const STRA & strRhs, + __in BOOL fIgnoreCase /*= FALSE*/ +) const +{ + return Equals( strRhs.QueryStr(), fIgnoreCase ); +} + +DWORD +STRA::QueryCB( + VOID +) const +// +// Returns the number of bytes in the string excluding the terminating NULL +// +{ + return m_cchLen * sizeof( CHAR ); +} + +DWORD +STRA::QueryCCH( + VOID +) const +// +// Returns the number of characters in the string excluding the terminating NULL +// +{ + return m_cchLen; +} + +DWORD +STRA::QuerySizeCCH( + VOID +) const +// +// Returns size of the underlying storage buffer, in characters +// +{ + return m_Buff.QuerySize() / sizeof( CHAR ); +} + +DWORD +STRA::QuerySize( + VOID +) const +// +// Returns the size of the storage buffer in bytes +// +{ + return m_Buff.QuerySize(); +} + +__nullterminated +__bcount(this->m_cchLen) +CHAR * +STRA::QueryStr( + VOID +) const +// +// Return the string buffer +// +{ + return m_Buff.QueryPtr(); +} + +VOID +STRA::Reset( + VOID +) +// +// Resets the internal string to be NULL string. Buffer remains cached. +// +{ + _ASSERTE( QueryStr() != NULL ); + *(QueryStr()) = '\0'; + m_cchLen = 0; +} + +HRESULT +STRA::Resize( + __in DWORD cchSize +) +{ + if( !m_Buff.Resize( cchSize * sizeof( CHAR ) ) ) + { + return E_OUTOFMEMORY; + } + + return S_OK; +} + +HRESULT +STRA::SyncWithBuffer( + VOID +) +// +// Recalculate the length of the string, etc. because we've modified +// the buffer directly. +// +{ + HRESULT hr; + size_t size; + hr = StringCchLengthA( QueryStr(), + QuerySizeCCH(), + &size ); + if ( SUCCEEDED( hr ) ) + { + m_cchLen = static_cast(size); + } + return hr; +} + +HRESULT +STRA::Copy( + __in PCSTR pszCopy +) +{ + HRESULT hr; + size_t cbLen; + hr = StringCbLengthA( pszCopy, + STRSAFE_MAX_CCH, + &cbLen ); + if ( FAILED( hr ) ) + { + return hr; + } + return Copy( pszCopy, cbLen ); +} + + +HRESULT +STRA::Copy( + __in_ecount(cchLen) + PCSTR pszCopy, + __in SIZE_T cbLen +) +// +// Copy the contents of another string to this one +// +{ + _ASSERTE( cbLen <= MAXDWORD ); + + return AuxAppend( + pszCopy, + static_cast(cbLen), + 0 + ); +} + +HRESULT +STRA::Copy( + __in const STRA * pstrRhs +) +{ + _ASSERTE( pstrRhs != NULL ); + return Copy( pstrRhs->QueryStr(), pstrRhs->QueryCCH() ); +} + +HRESULT +STRA::Copy( + __in const STRA & strRhs +) +{ + return Copy( strRhs.QueryStr(), strRhs.QueryCCH() ); +} + +HRESULT +STRA::CopyW( + __in PCWSTR pszCopyW +) +{ + HRESULT hr; + size_t cchLen; + hr = StringCchLengthW( pszCopyW, + STRSAFE_MAX_CCH, + &cchLen ); + if ( FAILED( hr ) ) + { + return hr; + } + return CopyW( pszCopyW, cchLen ); +} + +HRESULT +STRA::CopyWTruncate( + __in PCWSTR pszCopyWTruncate +) +{ + HRESULT hr; + size_t cchLen; + hr = StringCchLengthW( pszCopyWTruncate, + STRSAFE_MAX_CCH, + &cchLen ); + if ( FAILED( hr ) ) + { + return hr; + } + return CopyWTruncate( pszCopyWTruncate, cchLen ); +} + +HRESULT +STRA::CopyWTruncate( + __in_ecount(cchLen) + PCWSTR pszCopyWTruncate, + __in SIZE_T cchLen +) +// +// The "Truncate" methods do not do proper conversion. They do a (CHAR) caste +// +{ + _ASSERTE( cchLen <= MAXDWORD ); + + return AuxAppendWTruncate( + pszCopyWTruncate, + static_cast(cchLen), + 0 + ); +} + +HRESULT +STRA::Append( + __in PCSTR pszAppend +) +{ + HRESULT hr; + size_t cbLen; + hr = StringCbLengthA( pszAppend, + STRSAFE_MAX_CCH, + &cbLen ); + if ( FAILED( hr ) ) + { + return hr; + } + return Append( pszAppend, cbLen ); +} + +HRESULT +STRA::Append( + __in_ecount(cchLen) + PCSTR pszAppend, + __in SIZE_T cbLen +) +{ + _ASSERTE( cbLen <= MAXDWORD ); + if ( cbLen == 0 ) + { + return S_OK; + } + return AuxAppend( + pszAppend, + static_cast(cbLen), + QueryCB() + ); +} + +HRESULT +STRA::Append( + __in const STRA * pstrRhs +) +{ + _ASSERTE( pstrRhs != NULL ); + return Append( pstrRhs->QueryStr(), pstrRhs->QueryCCH() ); +} + +HRESULT +STRA::Append( + __in const STRA & strRhs +) +{ + return Append( strRhs.QueryStr(), strRhs.QueryCCH() ); +} + +HRESULT +STRA::AppendWTruncate( + __in PCWSTR pszAppendWTruncate +) +{ + HRESULT hr; + size_t cchLen; + hr = StringCchLengthW( pszAppendWTruncate, + STRSAFE_MAX_CCH, + &cchLen ); + if ( FAILED( hr ) ) + { + return hr; + } + return AppendWTruncate( pszAppendWTruncate, cchLen ); +} + +HRESULT +STRA::AppendWTruncate( + __in_ecount(cchLen) + PCWSTR pszAppendWTruncate, + __in SIZE_T cchLen +) +// +// The "Truncate" methods do not do proper conversion. They do a (CHAR) caste +// +{ + _ASSERTE( cchLen <= MAXDWORD ); + if ( cchLen == 0 ) + { + return S_OK; + } + return AuxAppendWTruncate( + pszAppendWTruncate, + static_cast(cchLen), + QueryCB() + ); +} + +HRESULT +STRA::CopyToBuffer( + __out_bcount(*pcb) CHAR* pszBuffer, + __inout DWORD * pcb +) const +// +// Makes a copy of the stored string into the given buffer +// +{ + _ASSERTE( NULL != pszBuffer ); + _ASSERTE( NULL != pcb ); + + HRESULT hr = S_OK; + DWORD cbNeeded = QueryCB() + sizeof( CHAR ); + + if( *pcb < cbNeeded ) + { + hr = HRESULT_FROM_WIN32( ERROR_INSUFFICIENT_BUFFER ); + goto Finished; + } + + memcpy( pszBuffer, QueryStr(), cbNeeded ); + +Finished: + + *pcb = cbNeeded; + + return hr; +} + +HRESULT +STRA::SetLen( + __in DWORD cchLen +) +/*++ + * +Routine Description: + + Set the length of the string and null terminate, if there + is sufficient buffer already allocated. Will not reallocate. + +Arguments: + + cchLen - The number of characters in the new string. + +Return Value: + + HRESULT + +--*/ +{ + if( cchLen >= QuerySizeCCH() ) + { + return HRESULT_FROM_WIN32( ERROR_INVALID_PARAMETER ); + } + + *( QueryStr() + cchLen ) = '\0'; + m_cchLen = cchLen; + + return S_OK; +} + + +HRESULT +STRA::SafeSnprintf( + __in __format_string + PCSTR pszFormatString, + ... +) +/*++ + +Routine Description: + + Writes to a STRA, growing it as needed. It arbitrarily caps growth at 64k chars. + +Arguments: + + pszFormatString - printf format + ... - printf args + +Return Value: + + HRESULT + +--*/ +{ + HRESULT hr = S_OK; + va_list argsList; + va_start( argsList, pszFormatString ); + + hr = SafeVsnprintf(pszFormatString, argsList); + + va_end( argsList ); + return hr; +} + +HRESULT +STRA::SafeVsnprintf( + __in __format_string + PCSTR pszFormatString, + va_list argsList +) +/*++ + +Routine Description: + + Writes to a STRA, growing it as needed. It arbitrarily caps growth at 64k chars. + +Arguments: + + pszFormatString - printf format + argsList - printf va_list + +Return Value: + + HRESULT + +--*/ +{ + HRESULT hr = S_OK; + int cchOutput; + int cchNeeded; + + // + // Format the incoming message using vsnprintf() + // so that the overflows are captured + // + cchOutput = _vsnprintf_s( + QueryStr(), + QuerySizeCCH(), + QuerySizeCCH() - 1, + pszFormatString, + argsList + ); + + if( cchOutput == -1 ) + { + // + // Couldn't fit this in the original STRU size. + // + cchNeeded = _vscprintf( pszFormatString, argsList ); + if( cchNeeded > 64 * 1024 ) + { + // + // If we're trying to produce a string > 64k chars, then + // there is probably a problem + // + hr = HRESULT_FROM_WIN32( ERROR_INVALID_DATA ); + goto Finished; + } + + // + // _vscprintf doesn't include terminating null character + // + cchNeeded++; + + hr = Resize( cchNeeded ); + if( FAILED( hr ) ) + { + goto Finished; + } + + cchOutput = _vsnprintf_s( + QueryStr(), + QuerySizeCCH(), + QuerySizeCCH() - 1, + pszFormatString, + argsList + ); + if( -1 == cchOutput ) + { + // + // This should never happen, cause we should already have correctly sized memory + // + _ASSERTE( FALSE ); + + hr = HRESULT_FROM_WIN32( ERROR_INVALID_DATA ); + goto Finished; + } + } + + // + // always null terminate at the last WCHAR + // + QueryStr()[ QuerySizeCCH() - 1 ] = L'\0'; + + // + // we directly touched the buffer - therefore: + // + hr = SyncWithBuffer(); + if( FAILED( hr ) ) + { + goto Finished; + } + +Finished: + + if( FAILED( hr ) ) + { + Reset(); + } + + return hr; +} + +bool +FShouldEscapeUtf8( + BYTE ch + ) +{ + if ( ( ch >= 128 ) ) + { + return true; + } + + return false; +} + +bool +FShouldEscapeUrl( + BYTE ch + ) +{ + if ( ( ch >= 128 || + ch <= 32 || + ch == '<' || + ch == '>' || + ch == '%' || + ch == '?' || + ch == '#' ) && + !( ch == '\n' || ch == '\r' ) ) + { + return true; + } + + return false; +} + +HRESULT +STRA::Escape( + VOID +) +/*++ + +Routine Description: + + Escapes a STRA + +Arguments: + + None + +Return Value: + + None + +--*/ +{ + return EscapeInternal( FShouldEscapeUrl ); +} + +HRESULT +STRA::EscapeUtf8( + VOID +) +/*++ + +Routine Description: + + Escapes the high-bit chars in a STRA. LWS, CR, LF & controls are untouched. + +Arguments: + + None + +Return Value: + + None + +--*/ +{ + return EscapeInternal( FShouldEscapeUtf8 ); +} + + +HRESULT +STRA::EscapeInternal( + PFN_F_SHOULD_ESCAPE pfnFShouldEscape +) +/*++ + +Routine Description: + + Escapes a STRA according to the predicate function passed in + +Arguments: + + None + +Return Value: + + None + +--*/ +{ + LPCSTR pch = QueryStr(); + __analysis_assume( pch != NULL ); + int i = 0; + BYTE ch; + HRESULT hr = S_OK; + BOOL fRet = FALSE; + SIZE_T NewSize = 0; + + // Set to true if any % escaping occurs + BOOL fEscapingDone = FALSE; + + // + // If there are any characters that need to be escaped we copy the entire string + // character by character into straTemp, escaping as we go, then at the end + // copy all of straTemp over. Don't modify InlineBuffer directly. + // + CHAR InlineBuffer[512]; + InlineBuffer[0] = '\0'; + STRA straTemp(InlineBuffer, sizeof(InlineBuffer)/sizeof(*InlineBuffer)); + + _ASSERTE( pch ); + + while (ch = pch[i]) + { + // + // Escape characters that are in the non-printable range + // but ignore CR and LF + // + + if ( pfnFShouldEscape( ch ) ) + { + if (FALSE == fEscapingDone) + { + // first character in the string that needed escaping + fEscapingDone = TRUE; + + // guess that the size needs to be larger than + // what we used to have times two + NewSize = QueryCCH() * 2; + if ( NewSize > MAXDWORD ) + { + hr = HRESULT_FROM_WIN32( ERROR_ARITHMETIC_OVERFLOW ); + return hr; + } + + hr = straTemp.Resize( static_cast(NewSize) ); + + if (FAILED(hr)) + { + return hr; + } + + // Copy all of the previous buffer into buffTemp, only if it is not the first character: + + if ( i > 0) + { + hr = straTemp.Copy(QueryStr(), + i * sizeof(CHAR)); + if (FAILED(hr)) + { + return hr; + } + } + } + + // resize the temporary (if needed) with the slop of the entire buffer length + // this fixes constant reallocation if the entire string needs to be escaped + NewSize = QueryCCH() + 2 * sizeof(CHAR) + 1 * sizeof(CHAR); + if ( NewSize > MAXDWORD ) + { + hr = HRESULT_FROM_WIN32( ERROR_ARITHMETIC_OVERFLOW ); + return hr; + } + + fRet = straTemp.m_Buff.Resize( NewSize ); + if ( !fRet ) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + return hr; + } + + // + // Create the string to append for the current character + // + + CHAR chHex[3]; + chHex[0] = '%'; + + // + // Convert the low then the high character to hex + // + + UINT nLowDigit = (UINT)(ch % 16); + chHex[2] = TODIGIT( nLowDigit ); + + ch /= 16; + + UINT nHighDigit = (UINT)(ch % 16); + + chHex[1] = TODIGIT( nHighDigit ); + + // + // Actually append the converted character to the end of the temporary + // + hr = straTemp.Append(chHex, 3); + if (FAILED(hr)) + { + return hr; + } + } + else + { + // if no escaping done, no need to copy + if (fEscapingDone) + { + // if ANY escaping done, copy current character into new buffer + straTemp.Append(&pch[i], 1); + } + } + + // inspect the next character in the string + i++; + } + + if (fEscapingDone) + { + // the escaped string is now in straTemp + hr = Copy(straTemp); + } + + return hr; + +} // EscapeInternal() + +VOID +STRA::Unescape( + VOID +) +/*++ + +Routine Description: + + Unescapes a STRA + + Supported escape sequences are: + %uxxxx unescapes Unicode character xxxx into system codepage + %xx unescapes character xx + % without following hex digits is ignored + +Arguments: + + None + +Return Value: + + None + +--*/ +{ + CHAR *pScan; + CHAR *pDest; + CHAR *pNextScan; + WCHAR wch; + DWORD dwLen; + BOOL fChanged = FALSE; + + // + // Now take care of any escape characters + // + pDest = pScan = strchr(QueryStr(), '%'); + + while (pScan) + { + if ((pScan[1] == 'u' || pScan[1] == 'U') && + SAFEIsXDigit(pScan[2]) && + SAFEIsXDigit(pScan[3]) && + SAFEIsXDigit(pScan[4]) && + SAFEIsXDigit(pScan[5])) + { + wch = TOHEX(pScan[2]) * 4096 + TOHEX(pScan[3]) * 256 + + TOHEX(pScan[4]) * 16 + TOHEX(pScan[5]); + + dwLen = WideCharToMultiByte(CP_ACP, + WC_NO_BEST_FIT_CHARS, + &wch, + 1, + (LPSTR) pDest, + 6, + NULL, + NULL); + + pDest += dwLen; + pScan += 6; + fChanged = TRUE; + } + else if (SAFEIsXDigit(pScan[1]) && SAFEIsXDigit(pScan[2])) + { + *pDest = TOHEX(pScan[1]) * 16 + TOHEX(pScan[2]); + + pDest ++; + pScan += 3; + fChanged = TRUE; + } + else // Not an escaped char, just a '%' + { + if (fChanged) + { + *pDest = *pScan; + } + + pDest++; + pScan++; + } + + // + // Copy all the information between this and the next escaped char + // + pNextScan = strchr(pScan, '%'); + + if (fChanged) // pScan!=pDest, so we have to copy the char's + { + if (!pNextScan) // That was the last '%' in the string + { + memmove(pDest, + pScan, + QueryCCH() - DIFF(pScan - QueryStr()) + 1); + } + else + { + // There is another '%', move intermediate chars + if ((dwLen = (DWORD)DIFF(pNextScan - pScan)) != 0) + { + memmove(pDest, + pScan, + dwLen); + pDest += dwLen; + } + } + } + + pScan = pNextScan; + } + + if (fChanged) + { + m_cchLen = (DWORD)strlen(QueryStr()); // for safety recalc the length + } + + return; +} + +HRESULT +STRA::CopyWToUTF8Unescaped( + __in LPCWSTR cpchStr +) +{ + return STRA::CopyWToUTF8Unescaped(cpchStr, (DWORD) wcslen(cpchStr)); +} + +HRESULT +STRA::CopyWToUTF8Unescaped( + __in_ecount(cch) + LPCWSTR cpchStr, + __in DWORD cch +) +{ + HRESULT hr = S_OK; + int iRet; + + if (cch == 0) + { + Reset(); + return S_OK; + } + + iRet = ConvertUnicodeToUTF8(cpchStr, + &m_Buff, + cch); + if (-1 == iRet) + { + // could not convert + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + m_cchLen = iRet; + + _ASSERTE(strlen(m_Buff.QueryPtr()) == m_cchLen); +Finished: + return hr; +} + +HRESULT +STRA::CopyWToUTF8Escaped( + __in LPCWSTR cpchStr +) +{ + return STRA::CopyWToUTF8Escaped(cpchStr, (DWORD) wcslen(cpchStr)); +} + +HRESULT +STRA::CopyWToUTF8Escaped( + __in_ecount(cch) + LPCWSTR cpchStr, + __in DWORD cch +) +{ + HRESULT hr = S_OK; + + hr = CopyWToUTF8Unescaped(cpchStr, cch); + if (FAILED(hr)) + { + goto Finished; + } + + hr = Escape(); + if (FAILED(hr)) + { + goto Finished; + } + + hr = S_OK; +Finished: + return hr; +} + +HRESULT +STRA::AuxAppend( + __in_ecount(cbLen) + LPCSTR pStr, + __in DWORD cbLen, + __in DWORD cbOffset +) +{ + _ASSERTE( NULL != pStr ); + _ASSERTE( cbOffset <= QueryCB() ); + + ULONGLONG cb64NewSize = (ULONGLONG)cbOffset + cbLen + sizeof( CHAR ); + if( cb64NewSize > MAXDWORD ) + { + return HRESULT_FROM_WIN32( ERROR_ARITHMETIC_OVERFLOW ); + } + + if( m_Buff.QuerySize() < cb64NewSize ) + { + if( !m_Buff.Resize( static_cast(cb64NewSize) ) ) + { + return E_OUTOFMEMORY; + } + } + + memcpy( reinterpret_cast(m_Buff.QueryPtr()) + cbOffset, pStr, cbLen ); + + m_cchLen = cbLen + cbOffset; + + *( QueryStr() + m_cchLen ) = '\0'; + + return S_OK; +} + +HRESULT +STRA::AuxAppendW( + __in_ecount(cchAppendW) + PCWSTR pszAppendW, + __in DWORD cchAppendW, + __in DWORD cbOffset, + __in UINT CodePage, + __in BOOL fFailIfNoTranslation, + __in DWORD dwFlags +) +{ + HRESULT hr = S_OK; + DWORD cbAvailable = 0; + DWORD cbRet = 0; + + // + // There are only two expect places to append + // + _ASSERTE( 0 == cbOffset || QueryCB() == cbOffset ); + + if ( cchAppendW == 0 ) + { + goto Finished; + } + + // + // start by assuming 1 char to 1 char will be enough space + // + if( !m_Buff.Resize( cbOffset + cchAppendW + sizeof( CHAR ) ) ) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + cbAvailable = m_Buff.QuerySize() - cbOffset; + + cbRet = WideCharToMultiByte( + CodePage, + dwFlags, + pszAppendW, + cchAppendW, + QueryStr() + cbOffset, + cbAvailable, + NULL, + NULL + ); + if( 0 != cbRet ) + { + if(!m_Buff.Resize(cbOffset + cbRet + 1)) + { + hr = E_OUTOFMEMORY; + } + + // + // not zero --> success, so we're done + // + goto Finished; + } + + // + // We only know how to handle ERROR_INSUFFICIENT_BUFFER + // + hr = HRESULT_FROM_WIN32( GetLastError() ); + if( hr != HRESULT_FROM_WIN32( ERROR_INSUFFICIENT_BUFFER ) ) + { + goto Finished; + } + + // + // Reset HResult because we need to get the number of bytes needed + // + hr = S_OK; + cbRet = WideCharToMultiByte( + CodePage, + dwFlags, + pszAppendW, + cchAppendW, + NULL, + 0, + NULL, + NULL + ); + if( 0 == cbRet ) + { + // + // no idea how we could ever reach here + // + hr = HRESULT_FROM_WIN32( GetLastError() ); + goto Finished; + } + + if( !m_Buff.Resize( cbOffset + cbRet + 1) ) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + cbAvailable = m_Buff.QuerySize() - cbOffset; + + cbRet = WideCharToMultiByte( + CodePage, + dwFlags, + pszAppendW, + cchAppendW, + QueryStr() + cbOffset, + cbAvailable, + NULL, + NULL + ); + if( 0 == cbRet ) + { + hr = HRESULT_FROM_WIN32( GetLastError() ); + goto Finished; + } + +Finished: + + if( SUCCEEDED( hr ) && 0 != cbRet ) + { + m_cchLen = cbRet + cbOffset; + } + + // + // ensure we're still NULL terminated in the right spot + // (regardless of success or failure) + // + QueryStr()[m_cchLen] = '\0'; + + return hr; +} + +HRESULT +STRA::AuxAppendWTruncate( + __in_ecount(cchAppendW) + __in PCWSTR pszAppendW, + __in DWORD cchAppendW, + __in DWORD cbOffset +) +// +// Cheesey WCHAR --> CHAR conversion +// +{ + HRESULT hr = S_OK; + CHAR* pszBuffer; + + _ASSERTE( NULL != pszAppendW ); + _ASSERTE( 0 == cbOffset || cbOffset == QueryCB() ); + + if( !pszAppendW ) + { + hr = HRESULT_FROM_WIN32( ERROR_INVALID_PARAMETER ); + goto Finished; + } + + ULONGLONG cbNeeded = (ULONGLONG)cbOffset + cchAppendW + sizeof( CHAR ); + if( cbNeeded > MAXDWORD ) + { + hr = HRESULT_FROM_WIN32( ERROR_ARITHMETIC_OVERFLOW ); + goto Finished; + } + + if( !m_Buff.Resize( static_cast(cbNeeded) ) ) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + // + // Copy/convert the UNICODE string over (by making two bytes into one) + // + pszBuffer = QueryStr() + cbOffset; + for( DWORD i = 0; i < cchAppendW; i++ ) + { + pszBuffer[i] = static_cast(pszAppendW[i]); + } + + m_cchLen = cchAppendW + cbOffset; + *( QueryStr() + m_cchLen ) = '\0'; + +Finished: + + return hr; +} + +// static +int +STRA::ConvertUnicodeToCodePage( + __in_ecount(dwStringLen) + LPCWSTR pszSrcUnicodeString, + __inout BUFFER_T * pbufDstAnsiString, + __in DWORD dwStringLen, + __in UINT uCodePage +) +{ + _ASSERTE(NULL != pszSrcUnicodeString); + _ASSERTE(NULL != pbufDstAnsiString); + + BOOL bTemp; + int iStrLen = 0; + DWORD dwFlags; + + if (uCodePage == CP_ACP) + { + dwFlags = WC_NO_BEST_FIT_CHARS; + } + else + { + dwFlags = 0; + } + + iStrLen = WideCharToMultiByte(uCodePage, + dwFlags, + pszSrcUnicodeString, + dwStringLen, + (LPSTR)pbufDstAnsiString->QueryPtr(), + (int)pbufDstAnsiString->QuerySize(), + NULL, + NULL); + if ((iStrLen == 0) && (GetLastError() == ERROR_INSUFFICIENT_BUFFER)) { + iStrLen = WideCharToMultiByte(uCodePage, + dwFlags, + pszSrcUnicodeString, + dwStringLen, + NULL, + 0, + NULL, + NULL); + if (iStrLen != 0) { + // add one just for the extra NULL + bTemp = pbufDstAnsiString->Resize(iStrLen + 1); + if (!bTemp) + { + iStrLen = 0; + } + else + { + iStrLen = WideCharToMultiByte(uCodePage, + dwFlags, + pszSrcUnicodeString, + dwStringLen, + (LPSTR)pbufDstAnsiString->QueryPtr(), + (int)pbufDstAnsiString->QuerySize(), + NULL, + NULL); + } + + } + } + + if (0 != iStrLen && + pbufDstAnsiString->Resize(iStrLen + 1)) + { + // insert a terminating NULL into buffer for the dwStringLen+1 in the case that the dwStringLen+1 was not a NULL. + ((CHAR*)pbufDstAnsiString->QueryPtr())[iStrLen] = '\0'; + } + else + { + iStrLen = -1; + } + + return iStrLen; +} + +// static +HRESULT +STRA::ConvertUnicodeToMultiByte( + __in_ecount(dwStringLen) + LPCWSTR pszSrcUnicodeString, + __in BUFFER_T * pbufDstAnsiString, + __in DWORD dwStringLen +) +{ + return ConvertUnicodeToCodePage( pszSrcUnicodeString, + pbufDstAnsiString, + dwStringLen, + CP_ACP ); +} + +// static +HRESULT +STRA::ConvertUnicodeToUTF8( + __in_ecount(dwStringLen) + LPCWSTR pszSrcUnicodeString, + __in BUFFER_T * pbufDstAnsiString, + __in DWORD dwStringLen +) +{ + return ConvertUnicodeToCodePage( pszSrcUnicodeString, + pbufDstAnsiString, + dwStringLen, + CP_UTF8 ); +} + +/*++ + +Routine Description: + + Removes leading and trailing whitespace + +--*/ + +VOID +STRA::Trim() +{ + PSTR pszString = QueryStr(); + DWORD cchNewLength = m_cchLen; + DWORD cchLeadingWhitespace = 0; + DWORD cchTempLength = 0; + + for (LONG ixString = m_cchLen - 1; ixString >= 0; ixString--) + { + if (isspace((unsigned char) pszString[ixString]) != 0) + { + pszString[ixString] = '\0'; + cchNewLength--; + } + else + { + break; + } + } + + cchTempLength = cchNewLength; + for (DWORD ixString = 0; ixString < cchTempLength; ixString++) + { + if (isspace((unsigned char) pszString[ixString]) != 0) + { + cchLeadingWhitespace++; + cchNewLength--; + } + else + { + break; + } + } + + if (cchNewLength == 0) + { + + Reset(); + } + else if (cchLeadingWhitespace > 0) + { + memmove(pszString, pszString + cchLeadingWhitespace, cchNewLength * sizeof(CHAR)); + pszString[cchNewLength] = '\0'; + } + + SyncWithBuffer(); +} + +/*++ + +Routine Description: + + Compares the string to the provided prefix to check for equality + +Arguments: + + pStraPrefix - string to compare with + fIgnoreCase - indicates whether the string comparison should be case-sensitive + +Return Value: + + TRUE if prefix string matches with internal string, FALSE otherwise + +--*/ +BOOL +STRA::StartsWith( + __in const STRA * pStraPrefix, + __in bool fIgnoreCase) const +{ + _ASSERTE( pStraPrefix != NULL ); + return StartsWith(pStraPrefix->QueryStr(), fIgnoreCase); +} + +/*++ + +Routine Description: + + Compares the string to the provided prefix to check for equality + +Arguments: + + straPrefix - string to compare with + fIgnoreCase - indicates whether the string comparison should be case-sensitive + +Return Value: + + TRUE if prefix string matches with internal string, FALSE otherwise + +--*/ +BOOL +STRA::StartsWith( + __in const STRA & straPrefix, + __in bool fIgnoreCase) const +{ + return StartsWith(straPrefix.QueryStr(), fIgnoreCase); +} + +/*++ + +Routine Description: + + Compares the string to the provided prefix to check for equality + +Arguments: + + pszPrefix - string to compare with + fIgnoreCase - indicates whether the string comparison should be case-sensitive + +Return Value: + + TRUE if prefix string matches with internal string, FALSE otherwise + +--*/ +BOOL +STRA::StartsWith( + __in PCSTR pszPrefix, + __in bool fIgnoreCase) const +{ + HRESULT hr = S_OK; + BOOL fMatch = FALSE; + size_t cchPrefix = 0; + + if (pszPrefix == NULL) + { + goto Finished; + } + + hr = StringCchLengthA( pszPrefix, + STRSAFE_MAX_CCH, + &cchPrefix ); + if (FAILED(hr)) + { + goto Finished; + } + + _ASSERTE( cchPrefix <= MAXDWORD ); + + if (cchPrefix > m_cchLen) + { + goto Finished; + } + + if( fIgnoreCase ) + { + fMatch = ( 0 == _strnicmp( QueryStr(), pszPrefix, cchPrefix ) ); + } + else + { + fMatch = ( 0 == strncmp( QueryStr(), pszPrefix, cchPrefix ) ); + } + + +Finished: + + return fMatch; +} + +/*++ + +Routine Description: + + Compares the string to the provided suffix to check for equality + +Arguments: + + pStraSuffix - string to compare with + fIgnoreCase - indicates whether the string comparison should be case-sensitive + +Return Value: + + TRUE if suffix string matches with internal string, FALSE otherwise + +--*/ +BOOL +STRA::EndsWith( + __in const STRA * pStraSuffix, + __in bool fIgnoreCase) const +{ + _ASSERTE( pStraSuffix != NULL ); + return EndsWith(pStraSuffix->QueryStr(), fIgnoreCase); +} + + +/*++ + +Routine Description: + + Compares the string to the provided suffix to check for equality + +Arguments: + + straSuffix - string to compare with + fIgnoreCase - indicates whether the string comparison should be case-sensitive + +Return Value: + + TRUE if suffix string matches with internal string, FALSE otherwise + +--*/ +BOOL +STRA::EndsWith( + __in const STRA & straSuffix, + __in bool fIgnoreCase) const +{ + return EndsWith(straSuffix.QueryStr(), fIgnoreCase); +} + + +/*++ + +Routine Description: + + Compares the string to the provided suffix to check for equality + +Arguments: + + pszSuffix - string to compare with + fIgnoreCase - indicates whether the string comparison should be case-sensitive + +Return Value: + + TRUE if suffix string matches with internal string, FALSE otherwise + +--*/ +BOOL +STRA::EndsWith( + __in PCSTR pszSuffix, + __in bool fIgnoreCase) const +{ + HRESULT hr = S_OK; + PSTR pszString = QueryStr(); + BOOL fMatch = FALSE; + size_t cchSuffix = 0; + ptrdiff_t ixOffset = 0; + + if (pszSuffix == NULL) + { + goto Finished; + } + + hr = StringCchLengthA( pszSuffix, + STRSAFE_MAX_CCH, + &cchSuffix ); + if (FAILED(hr)) + { + goto Finished; + } + + _ASSERTE( cchSuffix <= MAXDWORD ); + + if (cchSuffix > m_cchLen) + { + goto Finished; + } + + ixOffset = m_cchLen - cchSuffix; + _ASSERTE(ixOffset >= 0 && ixOffset <= MAXDWORD); + + if( fIgnoreCase ) + { + fMatch = ( 0 == _strnicmp( pszString + ixOffset, pszSuffix, cchSuffix ) ); + } + else + { + fMatch = ( 0 == strncmp( pszString + ixOffset, pszSuffix, cchSuffix ) ); + } + +Finished: + + return fMatch; +} + + +/*++ + +Routine Description: + + Searches the string for the first occurrence of the specified character. + +Arguments: + + charValue - character to find + dwStartIndex - the initial index. + +Return Value: + + The index for the first character occurence in the string. + + -1 if not found. + +--*/ +INT +STRA::IndexOf( + __in CHAR charValue, + __in DWORD dwStartIndex + ) const +{ + INT nIndex = -1; + + // Make sure that there are no buffer overruns. + if( dwStartIndex >= QueryCCH() ) + { + goto Finished; + } + + const CHAR* pChar = strchr( QueryStr() + dwStartIndex, charValue ); + + // Determine the index if found + if( pChar ) + { + // nIndex will be set to -1 on failure. + (VOID)SizeTToInt( pChar - QueryStr(), &nIndex ); + } + +Finished: + + return nIndex; +} + + +/*++ + +Routine Description: + + Searches the string for the first occurrence of the specified substring. + +Arguments: + + pszValue - substring to find + dwStartIndex - initial index. + +Return Value: + + The index for the first character occurence in the string. + + -1 if not found. + +--*/ +INT +STRA::IndexOf( + __in PCSTR pszValue, + __in DWORD dwStartIndex + ) const +{ + HRESULT hr = S_OK; + INT nIndex = -1; + SIZE_T cchValue = 0; + + // Validate input parameters + if( dwStartIndex >= QueryCCH() || !pszValue ) + { + goto Finished; + } + + const CHAR* pChar = strstr( QueryStr() + dwStartIndex, pszValue ); + + // Determine the index if found + if( pChar ) + { + // nIndex will be set to -1 on failure. + (VOID)SizeTToInt( pChar - QueryStr(), &nIndex ); + } + +Finished: + + return nIndex; +} + + +/*++ + +Routine Description: + + Searches the string for the last occurrence of the specified character. + +Arguments: + + charValue - character to find + dwStartIndex - initial index. + +Return Value: + + The index for the last character occurence in the string. + + -1 if not found. + +--*/ +INT +STRA::LastIndexOf( + __in CHAR charValue, + __in DWORD dwStartIndex + ) const +{ + INT nIndex = -1; + + // Make sure that there are no buffer overruns. + if( dwStartIndex >= QueryCCH() ) + { + goto Finished; + } + + const CHAR* pChar = strrchr( QueryStr() + dwStartIndex, charValue ); + + // Determine the index if found + if( pChar ) + { + // nIndex will be set to -1 on failure. + (VOID)SizeTToInt( pChar - QueryStr(), &nIndex ); + } + +Finished: + + return nIndex; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/stringa.h b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/stringa.h new file mode 100644 index 0000000000..94ace540f6 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/stringa.h @@ -0,0 +1,521 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "buffer.h" +#include "macros.h" +#include + + +#pragma warning( push ) +#pragma warning ( disable : ALL_CODE_ANALYSIS_WARNINGS ) + +class STRA +{ + +public: + + STRA( + VOID + ); + + STRA( + __inout_ecount(cchInit) CHAR* pbInit, + __in DWORD cchInit + ); + + BOOL + IsEmpty( + VOID + ) const; + + BOOL + Equals( + __in PCSTR pszRhs, + __in BOOL fIgnoreCase = FALSE + ) const; + + BOOL + Equals( + __in const STRA * pstrRhs, + __in BOOL fIgnoreCase = FALSE + ) const; + + BOOL + Equals( + __in const STRA & strRhs, + __in BOOL fIgnoreCase = FALSE + ) const; + + static + BOOL + Equals( + __in PCSTR pszLhs, + __in PCSTR pszRhs, + __in bool fIgnoreCase = false + ) + { + // Return FALSE if either or both strings are NULL. + if (!pszLhs || !pszRhs) return FALSE; + + if( fIgnoreCase ) + { + return ( 0 == _stricmp( pszLhs, pszRhs ) ); + } + + return ( 0 == strcmp( pszLhs, pszRhs ) ); + } + + VOID + Trim(); + + BOOL + StartsWith( + __in const STRA * pStraPrefix, + __in bool fIgnoreCase = FALSE + ) const; + + BOOL + StartsWith( + __in const STRA & straPrefix, + __in bool fIgnoreCase = FALSE + ) const; + + BOOL + StartsWith( + __in PCSTR pszPrefix, + __in bool fIgnoreCase = FALSE + ) const; + + BOOL + EndsWith( + __in const STRA * pStraSuffix, + __in bool fIgnoreCase = FALSE + ) const; + + BOOL + EndsWith( + __in const STRA & straSuffix, + __in bool fIgnoreCase = FALSE + ) const; + + BOOL + EndsWith( + __in PCSTR pszSuffix, + __in bool fIgnoreCase = FALSE + ) const; + + INT + IndexOf( + __in CHAR charValue, + __in DWORD dwStartIndex = 0 + ) const; + + INT + IndexOf( + __in PCSTR pszValue, + __in DWORD dwStartIndex = 0 + ) const; + + INT + LastIndexOf( + __in CHAR charValue, + __in DWORD dwStartIndex = 0 + ) const; + + DWORD + QueryCB( + VOID + ) const; + + DWORD + QueryCCH( + VOID + ) const; + + DWORD + QuerySizeCCH( + VOID + ) const; + + DWORD + QuerySize( + VOID + ) const; + + __nullterminated + __bcount(this->m_cchLen) + CHAR * + QueryStr( + VOID + ) const; + + VOID + Reset( + VOID + ); + + HRESULT + Resize( + __in DWORD cchSize + ); + + HRESULT + SyncWithBuffer( + VOID + ); + + HRESULT + Copy( + __in PCSTR pszCopy + ); + + HRESULT + Copy( + __in_ecount(cbLen) + PCSTR pszCopy, + __in SIZE_T cbLen + ); + + HRESULT + Copy( + __in const STRA * pstrRhs + ); + + HRESULT + Copy( + __in const STRA & strRhs + ); + + HRESULT + CopyW( + __in PCWSTR pszCopyW + ); + + HRESULT + CopyW( + __in_ecount(cchLen) + PCWSTR pszCopyW, + __in SIZE_T cchLen, + __in UINT CodePage = CP_UTF8, + __in BOOL fFailIfNoTranslation = FALSE + ) + { + _ASSERTE( cchLen <= MAXDWORD ); + + return AuxAppendW( + pszCopyW, + static_cast(cchLen), + 0, + CodePage, + fFailIfNoTranslation + ); + } + + HRESULT + CopyWTruncate( + __in PCWSTR pszCopyWTruncate + ); + + HRESULT + CopyWTruncate( + __in_ecount(cchLen) + PCWSTR pszCopyWTruncate, + __in SIZE_T cchLen + ); + + HRESULT + Append( + __in PCSTR pszAppend + ); + + HRESULT + Append( + __in_ecount(cbLen) + PCSTR pszAppend, + __in SIZE_T cbLen + ); + + HRESULT + Append( + __in const STRA * pstrRhs + ); + + HRESULT + Append( + __in const STRA & strRhs + ); + + HRESULT + AppendW( + __in PCWSTR pszAppendW + ) + { + HRESULT hr; + size_t cchLen; + hr = StringCchLengthW( pszAppendW, + STRSAFE_MAX_CCH, + &cchLen ); + if ( FAILED( hr ) ) + { + return hr; + } + return AppendW( pszAppendW, cchLen ); + } + + HRESULT + AppendW( + __in_ecount(cchLen) + PCWSTR pszAppendW, + __in SIZE_T cchLen, + __in UINT CodePage = CP_UTF8, + __in BOOL fFailIfNoTranslation = FALSE + ) + { + _ASSERTE( cchLen <= MAXDWORD ); + if ( cchLen == 0 ) + { + return S_OK; + } + return AuxAppendW( + pszAppendW, + static_cast(cchLen), + QueryCB(), + CodePage, + fFailIfNoTranslation + ); + } + + HRESULT + AppendWTruncate( + __in PCWSTR pszAppendWTruncate + ); + + HRESULT + AppendWTruncate( + __in_ecount(cchLen) + PCWSTR pszAppendWTruncate, + __in SIZE_T cchLen + ); + + HRESULT + CopyToBuffer( + __out_bcount(*pcb) CHAR* pszBuffer, + __inout DWORD * pcb + ) const; + + HRESULT + SetLen( + __in DWORD cchLen + ); + + HRESULT + SafeSnprintf( + __in __format_string + PCSTR pszFormatString, + ... + ); + + HRESULT + SafeVsnprintf( + __in __format_string + PCSTR pszFormatString, + va_list argsList + ); + + HRESULT + Escape( + VOID + ); + + HRESULT + EscapeUtf8( + VOID + ); + + VOID + Unescape( + VOID + ); + + HRESULT + CopyWToUTF8Unescaped( + __in LPCWSTR cpchStr + ); + + HRESULT + CopyWToUTF8Unescaped( + __in_ecount(cch) + LPCWSTR cpchStr, + __in DWORD cch + ); + + HRESULT + CopyWToUTF8Escaped( + __in LPCWSTR cpchStr + ); + + HRESULT + CopyWToUTF8Escaped( + __in_ecount(cch) + LPCWSTR cpchStr, + __in DWORD cch + ); + +private: + + // + // Avoid C++ errors. This object should never go through a copy + // constructor, unintended cast or assignment. + // + STRA( const STRA &); + STRA & operator = (const STRA &); + + HRESULT + AuxAppend( + __in_ecount(cbLen) + LPCSTR pStr, + __in DWORD cbLen, + __in DWORD cbOffset + ); + + HRESULT + AuxAppendW( + __in_ecount(cchAppendW) + PCWSTR pszAppendW, + __in DWORD cchAppendW, + __in DWORD cbOffset, + __in UINT CodePage, + __in BOOL fFailIfNoTranslation + ) + { + DWORD dwFlags = 0; + + if( CP_ACP == CodePage ) + { + dwFlags = WC_NO_BEST_FIT_CHARS; + } + else if( fFailIfNoTranslation && CodePage == CP_UTF8 ) + { + // + // WC_ERR_INVALID_CHARS is only supported in Longhorn or greater. + // +#if defined( NTDDI_VERSION ) && NTDDI_VERSION >= NTDDI_LONGHORN + dwFlags |= WC_ERR_INVALID_CHARS; +#else + UNREFERENCED_PARAMETER(fFailIfNoTranslation); +#endif + } + + return AuxAppendW( pszAppendW, + cchAppendW, + cbOffset, + CodePage, + fFailIfNoTranslation, + dwFlags ); + } + + HRESULT + AuxAppendW( + __in_ecount(cchAppendW) + PCWSTR pszAppendW, + __in DWORD cchAppendW, + __in DWORD cbOffset, + __in UINT CodePage, + __in BOOL fFailIfNoTranslation, + __in DWORD dwFlags + ); + + HRESULT + AuxAppendWTruncate( + __in_ecount(cchAppendW) + __in PCWSTR pszAppendW, + __in DWORD cchAppendW, + __in DWORD cbOffset + ); + + static + int + ConvertUnicodeToCodePage( + __in_ecount(dwStringLen) + LPCWSTR pszSrcUnicodeString, + __inout BUFFER_T * pbufDstAnsiString, + __in DWORD dwStringLen, + __in UINT uCodePage + ); + + static + HRESULT + ConvertUnicodeToMultiByte( + __in_ecount(dwStringLen) + LPCWSTR pszSrcUnicodeString, + __in BUFFER_T * pbufDstAnsiString, + __in DWORD dwStringLen + ); + + static + HRESULT + ConvertUnicodeToUTF8( + __in_ecount(dwStringLen) + LPCWSTR pszSrcUnicodeString, + __in BUFFER_T * pbufDstAnsiString, + __in DWORD dwStringLen + ); + + typedef bool (* PFN_F_SHOULD_ESCAPE)(BYTE ch); + + HRESULT + EscapeInternal( + PFN_F_SHOULD_ESCAPE pfnFShouldEscape + ); + + // + // Buffer with an inline buffer of 1, + // enough to hold null-terminating character. + // + BUFFER_T m_Buff; + DWORD m_cchLen; +}; + +inline +HRESULT +AppendToString( + ULONGLONG Number, + STRA & String +) +{ + // prefast complains Append requires input + // to be null terminated, so zero initialize + // and pass the size of the buffer minus one + // to _ui64toa_s + CHAR chNumber[32] = {0}; + if (_ui64toa_s(Number, + chNumber, + sizeof(chNumber) - sizeof(CHAR), + 10) != 0) + { + return E_INVALIDARG; + } + return String.Append(chNumber); +} + +template +CHAR* InitHelper(__out CHAR (&psz)[size]) +{ + psz[0] = '\0'; + return psz; +} + +// +// Heap operation reduction macros +// +#define STACK_STRA(name, size) CHAR __ach##name[size];\ + STRA name(InitHelper(__ach##name), sizeof(__ach##name)) + +#define INLINE_STRA(name, size) CHAR __ach##name[size];\ + STRA name; + +#define INLINE_STRA_INIT(name) name(InitHelper(__ach##name), sizeof(__ach##name)) + +#pragma warning( pop ) diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/stringu.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/stringu.cpp new file mode 100644 index 0000000000..74f8595482 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/stringu.cpp @@ -0,0 +1,1257 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + + +#include "precomp.h" + +#pragma warning( push ) +#pragma warning ( disable : 4267 ALL_CODE_ANALYSIS_WARNINGS ) + +STRU::STRU( + VOID +) : m_cchLen( 0 ) +{ + *(QueryStr()) = L'\0'; +} + +STRU::STRU( + __inout_ecount(cchInit) WCHAR* pbInit, + __in DWORD cchInit +) : m_Buff( pbInit, cchInit * sizeof( WCHAR ) ), + m_cchLen( 0 ) +/*++ + Description: + + Used by STACK_STRU. Initially populates underlying buffer with pbInit. + + pbInit is not freed. + + Arguments: + + pbInit - initial memory to use + cchInit - count, in characters, of pbInit + + Returns: + + None. + +--*/ +{ + _ASSERTE( cchInit <= (MAXDWORD / sizeof( WCHAR )) ); + _ASSERTE( NULL != pbInit ); + _ASSERTE(cchInit > 0 ); + _ASSERTE(pbInit[0] == L'\0'); +} + +BOOL +STRU::IsEmpty( + VOID +) const +{ + return ( m_cchLen == 0 ); +} + +DWORD +STRU::QueryCB( + VOID +) const +// +// Returns the number of bytes in the string excluding the terminating NULL +// +{ + return m_cchLen * sizeof( WCHAR ); +} + +DWORD +STRU::QueryCCH( + VOID +) const +// +// Returns the number of characters in the string excluding the terminating NULL +// +{ + return m_cchLen; +} + +DWORD +STRU::QuerySizeCCH( + VOID +) const +// +// Returns size of the underlying storage buffer, in characters +// +{ + return m_Buff.QuerySize() / sizeof( WCHAR ); +} + +__nullterminated +__ecount(this->m_cchLen) +WCHAR* +STRU::QueryStr( + VOID +) const +// +// Return the string buffer +// +{ + return m_Buff.QueryPtr(); +} + +VOID +STRU::Reset( + VOID +) +// +// Resets the internal string to be NULL string. Buffer remains cached. +// +{ + _ASSERTE( QueryStr() != NULL ); + *(QueryStr()) = L'\0'; + m_cchLen = 0; +} + +HRESULT +STRU::Resize( + DWORD cchSize +) +{ + SIZE_T cbSize = cchSize * sizeof( WCHAR ); + if ( cbSize > MAXDWORD ) + { + return HRESULT_FROM_WIN32( ERROR_ARITHMETIC_OVERFLOW ); + } + if( !m_Buff.Resize( cbSize ) ) + { + return E_OUTOFMEMORY; + } + + return S_OK; +} + +HRESULT +STRU::SyncWithBuffer( + VOID +) +// +// Recalculate the length of the string, etc. because we've modified +// the buffer directly. +// +{ + HRESULT hr; + size_t size; + hr = StringCchLengthW( QueryStr(), + QuerySizeCCH(), + &size ); + if ( SUCCEEDED( hr ) ) + { + m_cchLen = static_cast(size); + } + return hr; +} + +HRESULT +STRU::Copy( + __in PCWSTR pszCopy +) +{ + HRESULT hr; + size_t cbStr; + + hr = StringCchLengthW( pszCopy, + STRSAFE_MAX_CCH, + &cbStr ); + if ( FAILED( hr ) ) + { + return hr; + } + + _ASSERTE( cbStr <= MAXDWORD ); + return Copy( pszCopy, + cbStr ); +} + +HRESULT +STRU::Copy( + __in_ecount(cchLen) + PCWSTR pszCopy, + SIZE_T cchLen +) +// +// Copy the contents of another string to this one +// +{ + return AuxAppend( pszCopy, + cchLen * sizeof(WCHAR), + 0); +} + +HRESULT +STRU::Copy( + __in const STRU * pstrRhs +) +{ + _ASSERTE( NULL != pstrRhs ); + return Copy( pstrRhs->QueryStr(), pstrRhs->QueryCCH() ); +} + +HRESULT +STRU::Copy( + __in const STRU & str +) +{ + return Copy( str.QueryStr(), str.QueryCCH() ); +} + +HRESULT +STRU::CopyAndExpandEnvironmentStrings( + __in PCWSTR pszSource +) +{ + HRESULT hr = S_OK; + DWORD cchDestReqBuff = 0; + + Reset(); + + cchDestReqBuff = ExpandEnvironmentStringsW( pszSource, + QueryStr(), + QuerySizeCCH() ); + if ( cchDestReqBuff == 0 ) + { + hr = HRESULT_FROM_WIN32( GetLastError() ); + goto Finished; + } + else if ( cchDestReqBuff > QuerySizeCCH() ) + { + hr = Resize( cchDestReqBuff ); + if ( FAILED( hr ) ) + { + goto Finished; + } + + cchDestReqBuff = ExpandEnvironmentStringsW( pszSource, + QueryStr(), + QuerySizeCCH() ); + + if ( cchDestReqBuff == 0 || cchDestReqBuff > QuerySizeCCH() ) + { + _ASSERTE( FALSE ); + hr = HRESULT_FROM_WIN32( GetLastError() ); + goto Finished; + } + } + + hr = SyncWithBuffer(); + if ( FAILED( hr ) ) + { + goto Finished; + } + +Finished: + + return hr; + +} + +HRESULT +STRU::CopyA( + __in PCSTR pszCopyA +) +{ + HRESULT hr; + size_t cbStr; + + hr = StringCbLengthA( pszCopyA, + STRSAFE_MAX_CCH, + &cbStr ); + if ( FAILED( hr ) ) + { + return hr; + } + + _ASSERTE( cbStr <= MAXDWORD ); + return CopyA( pszCopyA, + cbStr ); +} + +HRESULT +STRU::CopyA( + __in_bcount(cchLen) + PCSTR pszCopyA, + SIZE_T cchLen, + UINT CodePage /*= CP_UTF8*/ +) +{ + return AuxAppendA( + pszCopyA, + cchLen, + 0, + CodePage + ); +} + +HRESULT +STRU::Append( + __in PCWSTR pszAppend +) +{ + HRESULT hr; + size_t cbStr; + + hr = StringCchLengthW( pszAppend, + STRSAFE_MAX_CCH, + &cbStr ); + if ( FAILED( hr ) ) + { + return hr; + } + + _ASSERTE( cbStr <= MAXDWORD ); + return Append( pszAppend, + cbStr ); +} + +HRESULT +STRU::Append( + __in_ecount(cchLen) + PCWSTR pszAppend, + SIZE_T cchLen +) +// +// Append something to the end of the string +// +{ + if ( cchLen == 0 ) + { + return S_OK; + } + return AuxAppend( pszAppend, + cchLen * sizeof(WCHAR), + QueryCB() ); +} + +HRESULT +STRU::Append( + __in const STRU * pstrRhs +) +{ + _ASSERTE( NULL != pstrRhs ); + return Append( pstrRhs->QueryStr(), pstrRhs->QueryCCH() ); +} + +HRESULT +STRU::Append( + __in const STRU & strRhs +) +{ + return Append( strRhs.QueryStr(), strRhs.QueryCCH() ); +} + +HRESULT +STRU::AppendA( + __in PCSTR pszAppendA +) +{ + HRESULT hr; + size_t cbStr; + + hr = StringCbLengthA( pszAppendA, + STRSAFE_MAX_CCH, + &cbStr ); + if ( FAILED( hr ) ) + { + return hr; + } + + _ASSERTE( cbStr <= MAXDWORD ); + return AppendA( pszAppendA, + cbStr ); +} + +HRESULT +STRU::AppendA( + __in_bcount(cchLen) + PCSTR pszAppendA, + SIZE_T cchLen, + UINT CodePage /*= CP_UTF8*/ +) +{ + if ( cchLen == 0 ) + { + return S_OK; + } + return AuxAppendA( + pszAppendA, + cchLen, + QueryCB(), + CodePage + ); +} + +HRESULT +STRU::CopyToBuffer( + __out_bcount(*pcb) WCHAR* pszBuffer, + PDWORD pcb +) const +// +// Makes a copy of the stored string into the given buffer +// +{ + _ASSERTE( NULL != pszBuffer ); + _ASSERTE( NULL != pcb ); + + HRESULT hr = S_OK; + DWORD cbNeeded = QueryCB() + sizeof( WCHAR ); + + if( *pcb < cbNeeded ) + { + hr = HRESULT_FROM_WIN32( ERROR_INSUFFICIENT_BUFFER ); + goto Finished; + } + + // + // BUGBUG: StringCchCopy? + // + memcpy( pszBuffer, QueryStr(), cbNeeded ); + +Finished: + + *pcb = cbNeeded; + + return hr; +} + +HRESULT +STRU::SetLen( + __in DWORD cchLen +) +/*++ + * +Routine Description: + + Set the length of the string and null terminate, if there + is sufficient buffer already allocated. Will not reallocate. + +Arguments: + + cchLen - The number of characters in the new string. + +Return Value: + + HRESULT + +--*/ +{ + if( cchLen >= QuerySizeCCH() ) + { + return HRESULT_FROM_WIN32( ERROR_INVALID_PARAMETER ); + } + + *( QueryStr() + cchLen ) = L'\0'; + m_cchLen = cchLen; + + return S_OK; +} + +HRESULT +STRU::SafeSnwprintf( + __in PCWSTR pwszFormatString, + ... +) +/*++ + +Routine Description: + + Writes to a STRU, growing it as needed. It arbitrarily caps growth at 64k chars. + +Arguments: + + pwszFormatString - printf format + ... - printf args + +Return Value: + + HRESULT + +--*/ +{ + HRESULT hr = S_OK; + va_list argsList; + va_start( argsList, pwszFormatString ); + + hr = SafeVsnwprintf(pwszFormatString, argsList); + + va_end( argsList ); + return hr; +} + +HRESULT +STRU::SafeVsnwprintf( + __in PCWSTR pwszFormatString, + va_list argsList +) +/*++ + +Routine Description: + + Writes to a STRU, growing it as needed. It arbitrarily caps growth at 64k chars. + +Arguments: + + pwszFormatString - printf format + argsList - printf va_list + +Return Value: + + HRESULT + +--*/ +{ + HRESULT hr = S_OK; + int cchOutput; + int cchNeeded; + + // + // Format the incoming message using vsnprintf() + // so that the overflows are captured + // + cchOutput = _vsnwprintf_s( + QueryStr(), + QuerySizeCCH(), + QuerySizeCCH() - 1, + pwszFormatString, + argsList + ); + + if( cchOutput == -1 ) + { + // + // Couldn't fit this in the original STRU size. + // + cchNeeded = _vscwprintf( pwszFormatString, argsList ); + if( cchNeeded > 64 * 1024 ) + { + // + // If we're trying to produce a string > 64k chars, then + // there is probably a problem + // + hr = HRESULT_FROM_WIN32( ERROR_INVALID_DATA ); + goto Finished; + } + + // + // _vscprintf doesn't include terminating null character + // + cchNeeded++; + + hr = Resize( cchNeeded ); + if( FAILED( hr ) ) + { + goto Finished; + } + + cchOutput = _vsnwprintf_s( + QueryStr(), + QuerySizeCCH(), + QuerySizeCCH() - 1, + pwszFormatString, + argsList + ); + if( -1 == cchOutput ) + { + // + // This should never happen, cause we should already have correctly sized memory + // + _ASSERTE( FALSE ); + + hr = HRESULT_FROM_WIN32( ERROR_INVALID_DATA ); + goto Finished; + } + } + + // + // always null terminate at the last WCHAR + // + QueryStr()[ QuerySizeCCH() - 1 ] = L'\0'; + + // + // we directly touched the buffer - therefore: + // + hr = SyncWithBuffer(); + if ( FAILED( hr ) ) + { + goto Finished; + } + +Finished: + + if( FAILED( hr ) ) + { + Reset(); + } + + return hr; +} + +HRESULT +STRU::AuxAppend( + __in_ecount(cNumStrings) + PCWSTR const rgpszStrings[], + SIZE_T cNumStrings +) +/*++ + +Routine Description: + + Appends an array of strings of length cNumStrings + +Arguments: + + rgStrings - The array of strings to be appened + cNumStrings - The count of String + +Return Value: + + HRESULT + +--*/ +{ + HRESULT hr = S_OK; + size_t cbStringsTotal = sizeof( WCHAR ); // Account for null-terminator + + // + // Compute total size of the string. + // Resize internal buffer + // Copy each array element one by one to backing buffer + // Update backing buffer string length + // + for ( SIZE_T i = 0; i < cNumStrings; i++ ) + { + _ASSERTE( rgpszStrings[ i ] != NULL ); + if ( NULL == rgpszStrings[ i ] ) + { + return E_INVALIDARG; + } + + size_t cbString = 0; + + hr = StringCbLengthW( rgpszStrings[ i ], + STRSAFE_MAX_CCH * sizeof( WCHAR ), + &cbString ); + if ( FAILED( hr ) ) + { + return hr; + } + + cbStringsTotal += cbString; + + if ( cbStringsTotal > MAXDWORD ) + { + return HRESULT_FROM_WIN32( ERROR_ARITHMETIC_OVERFLOW ); + } + } + + size_t cbBufSizeRequired = QueryCB() + cbStringsTotal; + if ( cbBufSizeRequired > MAXDWORD ) + { + return HRESULT_FROM_WIN32( ERROR_ARITHMETIC_OVERFLOW ); + } + + if( m_Buff.QuerySize() < cbBufSizeRequired ) + { + if( !m_Buff.Resize( cbBufSizeRequired ) ) + { + return E_OUTOFMEMORY; + } + } + + STRSAFE_LPWSTR pszStringEnd = QueryStr() + QueryCCH(); + size_t cchRemaining = QuerySizeCCH() - QueryCCH(); + for ( SIZE_T i = 0; i < cNumStrings; i++ ) + { + hr = StringCchCopyExW( pszStringEnd, // pszDest + cchRemaining, // cchDest + rgpszStrings[ i ], // pszSrc + &pszStringEnd, // ppszDestEnd + &cchRemaining, // pcchRemaining + 0 ); // dwFlags + if ( FAILED( hr ) ) + { + _ASSERTE( FALSE ); + HRESULT hr2 = SyncWithBuffer(); + if ( FAILED( hr2 ) ) + { + return hr2; + } + return hr; + } + } + + m_cchLen = static_cast< DWORD >( cbBufSizeRequired ) / sizeof( WCHAR ) - 1; + + return S_OK; +} + +HRESULT +STRU::AuxAppend( + __in_bcount(cbStr) + const WCHAR* pStr, + SIZE_T cbStr, + DWORD cbOffset +) +/*++ + +Routine Description: + + Appends to the string starting at the (byte) offset cbOffset. + +Arguments: + + pStr - A unicode string to be appended + cbStr - Length, in bytes, of pStr + cbOffset - Offset, in bytes, at which to begin the append + +Return Value: + + HRESULT + +--*/ +{ + _ASSERTE( NULL != pStr ); + _ASSERTE( 0 == cbStr % sizeof( WCHAR ) ); + _ASSERTE( cbOffset <= QueryCB() ); + _ASSERTE( 0 == cbOffset % sizeof( WCHAR ) ); + + ULONGLONG cb64NewSize = (ULONGLONG)cbOffset + cbStr + sizeof( WCHAR ); + if( cb64NewSize > MAXDWORD ) + { + return HRESULT_FROM_WIN32( ERROR_ARITHMETIC_OVERFLOW ); + } + + if( m_Buff.QuerySize() < cb64NewSize ) + { + if( !m_Buff.Resize( static_cast(cb64NewSize) ) ) + { + return E_OUTOFMEMORY; + } + } + + memcpy( reinterpret_cast(m_Buff.QueryPtr()) + cbOffset, pStr, cbStr ); + + m_cchLen = (static_cast(cbStr) + cbOffset) / sizeof(WCHAR); + + *( QueryStr() + m_cchLen ) = L'\0'; + + return S_OK; +} + +HRESULT +STRU::AuxAppendA( + __in_bcount(cbStr) + const CHAR* pStr, + SIZE_T cbStr, + DWORD cbOffset, + UINT CodePage +) +/*++ + +Routine Description: + + Convert and append an ANSI string to the string starting at + the (byte) offset cbOffset + +Arguments: + + pStr - An ANSI string to be appended + cbStr - Length, in bytes, of pStr + cbOffset - Offset, in bytes, at which to begin the append + CodePage - code page to use for conversion + +Return Value: + + HRESULT + +--*/ +{ + WCHAR* pszBuffer; + DWORD cchBuffer; + DWORD cchCharsCopied = 0; + + _ASSERTE( NULL != pStr ); + _ASSERTE( cbOffset <= QueryCB() ); + _ASSERTE( 0 == cbOffset % sizeof( WCHAR ) ); + + if ( NULL == pStr ) + { + return E_INVALIDARG; + } + + if( 0 == cbStr ) + { + return S_OK; + } + + // + // Only resize when we have to. When we do resize, we tack on + // some extra space to avoid extra reallocations. + // + if( m_Buff.QuerySize() < (ULONGLONG)cbOffset + (cbStr * sizeof( WCHAR )) + sizeof(WCHAR) ) + { + ULONGLONG cb64NewSize = (ULONGLONG)( cbOffset + cbStr * sizeof(WCHAR) + sizeof( WCHAR ) ); + + // + // Check for the arithmetic overflow + // + if( cb64NewSize > MAXDWORD ) + { + return HRESULT_FROM_WIN32( ERROR_ARITHMETIC_OVERFLOW ); + } + + if( !m_Buff.Resize( static_cast(cb64NewSize) ) ) + { + return E_OUTOFMEMORY; + } + } + + pszBuffer = reinterpret_cast(reinterpret_cast(m_Buff.QueryPtr()) + cbOffset); + cchBuffer = ( m_Buff.QuerySize() - cbOffset - sizeof( WCHAR ) ) / sizeof( WCHAR ); + + cchCharsCopied = MultiByteToWideChar( + CodePage, + MB_ERR_INVALID_CHARS, + pStr, + static_cast(cbStr), + pszBuffer, + cchBuffer + ); + if( 0 == cchCharsCopied ) + { + return HRESULT_FROM_WIN32( GetLastError() ); + } + + // + // set the new length + // + m_cchLen = cchCharsCopied + cbOffset/sizeof(WCHAR); + + // + // Must be less than, cause still need to add NULL + // + _ASSERTE( m_cchLen < QuerySizeCCH() ); + + // + // append NULL character + // + *(QueryStr() + m_cchLen) = L'\0'; + + return S_OK; +} + + +/*++ + +Routine Description: + + Removes leading and trailing whitespace + +--*/ + +VOID +STRU::Trim() +{ + PWSTR pwszString = QueryStr(); + DWORD cchNewLength = m_cchLen; + DWORD cchLeadingWhitespace = 0; + DWORD cchTempLength = 0; + + for (LONG ixString = m_cchLen - 1; ixString >= 0; ixString--) + { + if (iswspace(pwszString[ixString]) != 0) + { + pwszString[ixString] = L'\0'; + cchNewLength--; + } + else + { + break; + } + } + + cchTempLength = cchNewLength; + for (DWORD ixString = 0; ixString < cchTempLength; ixString++) + { + if (iswspace(pwszString[ixString]) != 0) + { + cchLeadingWhitespace++; + cchNewLength--; + } + else + { + break; + } + } + + if (cchNewLength == 0) + { + + Reset(); + } + else if (cchLeadingWhitespace > 0) + { + memmove(pwszString, pwszString + cchLeadingWhitespace, cchNewLength * sizeof(WCHAR)); + pwszString[cchNewLength] = L'\0'; + } + + SyncWithBuffer(); +} + +/*++ + +Routine Description: + + Compares the string to the provided prefix to check for equality + +Arguments: + + pwszPrefix - wide char string to compare with + fIgnoreCase - indicates whether the string comparison should be case-sensitive + +Return Value: + + TRUE if prefix string matches with internal string, FALSE otherwise + +--*/ + +BOOL +STRU::StartsWith( + __in PCWSTR pwszPrefix, + __in bool fIgnoreCase) const +{ + HRESULT hr = S_OK; + BOOL fMatch = FALSE; + size_t cchPrefix = 0; + + if (pwszPrefix == NULL) + { + goto Finished; + } + + hr = StringCchLengthW( pwszPrefix, + STRSAFE_MAX_CCH, + &cchPrefix ); + if (FAILED(hr)) + { + goto Finished; + } + + _ASSERTE( cchPrefix <= MAXDWORD ); + + if (cchPrefix > m_cchLen) + { + goto Finished; + } + + #if defined( NTDDI_VERSION ) && NTDDI_VERSION >= NTDDI_LONGHORN + + fMatch = ( CSTR_EQUAL == CompareStringOrdinal( QueryStr(), + cchPrefix, + pwszPrefix, + cchPrefix, + fIgnoreCase ) ); + #else + + if( fIgnoreCase ) + { + fMatch = ( 0 == _wcsnicmp( QueryStr(), pwszPrefix, cchPrefix ) ); + } + else + { + fMatch = ( 0 == wcsncmp( QueryStr(), pwszPrefix, cchPrefix ) ); + } + + #endif + +Finished: + + return fMatch; +} + +/*++ + +Routine Description: + + Compares the string to the provided suffix to check for equality + +Arguments: + + pwszSuffix - wide char string to compare with + fIgnoreCase - indicates whether the string comparison should be case-sensitive + +Return Value: + + TRUE if suffix string matches with internal string, FALSE otherwise + +--*/ + + +BOOL +STRU::EndsWith( + __in PCWSTR pwszSuffix, + __in bool fIgnoreCase) const +{ + HRESULT hr = S_OK; + PWSTR pwszString = QueryStr(); + BOOL fMatch = FALSE; + size_t cchSuffix = 0; + ptrdiff_t ixOffset = 0; + + if (pwszSuffix == NULL) + { + goto Finished; + } + + hr = StringCchLengthW( pwszSuffix, + STRSAFE_MAX_CCH, + &cchSuffix ); + if (FAILED(hr)) + { + goto Finished; + } + + _ASSERTE( cchSuffix <= MAXDWORD ); + + if (cchSuffix > m_cchLen) + { + goto Finished; + } + + ixOffset = m_cchLen - cchSuffix; + _ASSERTE(ixOffset >= 0 && ixOffset <= MAXDWORD); + + #if defined( NTDDI_VERSION ) && NTDDI_VERSION >= NTDDI_LONGHORN + + fMatch = ( CSTR_EQUAL == CompareStringOrdinal( pwszString + ixOffset, + cchSuffix, + pwszSuffix, + cchSuffix, + fIgnoreCase ) ); + #else + + if( fIgnoreCase ) + { + fMatch = ( 0 == _wcsnicmp( pwszString + ixOffset, pwszSuffix, cchSuffix ) ); + } + else + { + fMatch = ( 0 == wcsncmp( pwszString + ixOffset, pwszSuffix, cchSuffix ) ); + } + + #endif + +Finished: + + return fMatch; +} + +/*++ + +Routine Description: + + Searches the string for the first occurrence of the specified character. + +Arguments: + + charValue - character to find + dwStartIndex - the initial index. + +Return Value: + + The index for the first character occurence in the string. + + -1 if not found. + +--*/ +INT +STRU::IndexOf( + __in WCHAR charValue, + __in DWORD dwStartIndex + ) const +{ + INT nIndex = -1; + + // Make sure that there are no buffer overruns. + if( dwStartIndex >= QueryCCH() ) + { + goto Finished; + } + + const WCHAR* pwChar = wcschr( QueryStr() + dwStartIndex, charValue ); + + // Determine the index if found + if( pwChar ) + { + // nIndex will be set to -1 on failure. + (VOID)SizeTToInt( pwChar - QueryStr(), &nIndex ); + } + +Finished: + + return nIndex; +} + + +/*++ + +Routine Description: + + Searches the string for the first occurrence of the specified substring. + +Arguments: + + pwszValue - substring to find + dwStartIndex - initial index. + +Return Value: + + The index for the first character occurence in the string. + + -1 if not found. + +--*/ +INT +STRU::IndexOf( + __in PCWSTR pwszValue, + __in DWORD dwStartIndex + ) const +{ + HRESULT hr = S_OK; + INT nIndex = -1; + SIZE_T cchValue = 0; + + // Validate input parameters + if( dwStartIndex >= QueryCCH() || !pwszValue ) + { + goto Finished; + } + + const WCHAR* pwChar = wcsstr( QueryStr() + dwStartIndex, pwszValue ); + + // Determine the index if found + if( pwChar ) + { + // nIndex will be set to -1 on failure. + (VOID)SizeTToInt( pwChar - QueryStr(), &nIndex ); + } + +Finished: + + return nIndex; +} + + +/*++ + +Routine Description: + + Searches the string for the last occurrence of the specified character. + +Arguments: + + charValue - character to find + dwStartIndex - initial index. + +Return Value: + + The index for the last character occurence in the string. + + -1 if not found. + +--*/ +INT +STRU::LastIndexOf( + __in WCHAR charValue, + __in DWORD dwStartIndex + ) const +{ + INT nIndex = -1; + + // Make sure that there are no buffer overruns. + if( dwStartIndex >= QueryCCH() ) + { + goto Finished; + } + + const WCHAR* pwChar = wcsrchr( QueryStr() + dwStartIndex, charValue ); + + // Determine the index if found + if( pwChar ) + { + // nIndex will be set to -1 on failure. + (VOID)SizeTToInt( pwChar - QueryStr(), &nIndex ); + } + +Finished: + + return nIndex; +} + +//static +HRESULT +STRU::ExpandEnvironmentVariables( + __in PCWSTR pszString, + __out STRU * pstrExpandedString + ) +/*++ +Routine Description: + Expand the environment variables in a string +Arguments: + pszString - String with environment variables to expand + pstrExpandedString - Receives expanded string on success +Return Value: + HRESULT +--*/ +{ + HRESULT hr = S_OK; + DWORD cchNewSize = 0; + if ( pszString == NULL || + pstrExpandedString == NULL ) + { + DBG_ASSERT( FALSE ); + hr = HRESULT_FROM_WIN32( ERROR_INVALID_PARAMETER ); + goto Exit; + } + cchNewSize = ExpandEnvironmentStrings( pszString, + pstrExpandedString->QueryStr(), + pstrExpandedString->QuerySizeCCH() ); + if ( cchNewSize == 0 ) + { + hr = HRESULT_FROM_WIN32( GetLastError() ); + goto Exit; + } + if ( cchNewSize > pstrExpandedString->QuerySizeCCH() ) + { + hr = pstrExpandedString->Resize( + ( cchNewSize + 1 ) * sizeof( WCHAR ) + ); + if ( FAILED( hr ) ) + { + goto Exit; + } + cchNewSize = ExpandEnvironmentStrings( + pszString, + pstrExpandedString->QueryStr(), + pstrExpandedString->QuerySizeCCH() + ); + if ( cchNewSize == 0 || + cchNewSize > pstrExpandedString->QuerySizeCCH() ) + { + hr = HRESULT_FROM_WIN32( GetLastError() ); + goto Exit; + } + } + pstrExpandedString->SyncWithBuffer(); + hr = S_OK; +Exit: + return hr; +} + +#pragma warning( pop ) diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/stringu.h b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/stringu.h new file mode 100644 index 0000000000..f60f04cfbb --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/stringu.h @@ -0,0 +1,431 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "buffer.h" +#include +#include + +#pragma warning( push ) +#pragma warning ( disable : ALL_CODE_ANALYSIS_WARNINGS ) + +class STRU +{ + +public: + + STRU( + VOID + ); + + STRU( + __inout_ecount(cchInit) WCHAR* pbInit, + __in DWORD cchInit + ); + + BOOL + IsEmpty( + VOID + ) const; + + BOOL + Equals( + __in const STRU * pstrRhs, + __in BOOL fIgnoreCase = FALSE + ) const + { + _ASSERTE( pstrRhs != NULL ); + return Equals( pstrRhs->QueryStr(), fIgnoreCase ); + } + + BOOL + Equals( + __in const STRU & strRhs, + __in BOOL fIgnoreCase = FALSE + ) const + { + return Equals( strRhs.QueryStr(), fIgnoreCase ); + } + + BOOL + Equals( + __in PCWSTR pszRhs, + __in BOOL fIgnoreCase = FALSE + ) const + { + _ASSERTE( NULL != pszRhs ); + if ( NULL == pszRhs ) + { + return FALSE; + } + + #if defined( NTDDI_VERSION ) && NTDDI_VERSION >= NTDDI_LONGHORN + + return ( CSTR_EQUAL == CompareStringOrdinal( QueryStr(), + QueryCCH(), + pszRhs, + -1, + fIgnoreCase ) ); + #else + + if( fIgnoreCase ) + { + return ( 0 == _wcsicmp( QueryStr(), pszRhs ) ); + } + return ( 0 == wcscmp( QueryStr(), pszRhs ) ); + + #endif + } + + + static + BOOL + Equals( + __in PCWSTR pwszLhs, + __in PCWSTR pwszRhs, + __in bool fIgnoreCase = false + ) + { + // Return FALSE if either or both strings are NULL. + if (!pwszLhs || !pwszRhs) return FALSE; + + // + // This method performs a ordinal string comparison when OS is Vista or + // greater and a culture sensitive comparison if not (XP). This is + // consistent with the existing Equals implementation (see above). + // +#if defined( NTDDI_VERSION ) && NTDDI_VERSION >= NTDDI_LONGHORN + + return ( CSTR_EQUAL == CompareStringOrdinal( pwszLhs, + -1, + pwszRhs, + -1, + fIgnoreCase ) ); +#else + + if( fIgnoreCase ) + { + return ( 0 == _wcsicmp( pwszLhs, pwszRhs ) ); + } + else + { + return ( 0 == wcscmp( pwszLhs, pwszRhs ) ); + } + +#endif + } + + VOID + Trim(); + + BOOL + StartsWith( + __in const STRU * pStruPrefix, + __in bool fIgnoreCase = FALSE + ) const + { + _ASSERTE( pStruPrefix != NULL ); + return StartsWith( pStruPrefix->QueryStr(), fIgnoreCase ); + } + + BOOL + StartsWith( + __in const STRU & struPrefix, + __in bool fIgnoreCase = FALSE + ) const + { + return StartsWith( struPrefix.QueryStr(), fIgnoreCase ); + } + + BOOL + StartsWith( + __in PCWSTR pwszPrefix, + __in bool fIgnoreCase = FALSE + ) const; + + BOOL + EndsWith( + __in const STRU * pStruSuffix, + __in bool fIgnoreCase = FALSE + ) const + { + _ASSERTE( pStruSuffix != NULL ); + return EndsWith( pStruSuffix->QueryStr(), fIgnoreCase ); + } + + BOOL + EndsWith( + __in const STRU & struSuffix, + __in bool fIgnoreCase = FALSE + ) const + { + return EndsWith( struSuffix.QueryStr(), fIgnoreCase ); + } + + BOOL + EndsWith( + __in PCWSTR pwszSuffix, + __in bool fIgnoreCase = FALSE + ) const; + + INT + IndexOf( + __in WCHAR charValue, + __in DWORD dwStartIndex = 0 + ) const; + + INT + IndexOf( + __in PCWSTR pwszValue, + __in DWORD dwStartIndex = 0 + ) const; + + INT + LastIndexOf( + __in WCHAR charValue, + __in DWORD dwStartIndex = 0 + ) const; + + DWORD + QueryCB( + VOID + ) const; + + DWORD + QueryCCH( + VOID + ) const; + + DWORD + QuerySizeCCH( + VOID + ) const; + + __nullterminated + __ecount(this->m_cchLen) + WCHAR* + QueryStr( + VOID + ) const; + + VOID + Reset( + VOID + ); + + HRESULT + Resize( + DWORD cchSize + ); + + HRESULT + SyncWithBuffer( + VOID + ); + + template + HRESULT + Copy( + __in PCWSTR const (&rgpszStrings)[size] + ) + // + // Copies an array of strings declared as stack array. For example: + // + // LPCWSTR rgExample[] { L"one", L"two" }; + // hr = str.Copy( rgExample ); + // + { + Reset(); + + return AuxAppend( rgpszStrings, _countof( rgpszStrings ) ); + } + + HRESULT + Copy( + __in PCWSTR pszCopy + ); + + HRESULT + Copy( + __in_ecount(cchLen) + PCWSTR pszCopy, + SIZE_T cchLen + ); + + HRESULT + Copy( + __in const STRU * pstrRhs + ); + + HRESULT + Copy( + __in const STRU & str + ); + + HRESULT + CopyAndExpandEnvironmentStrings( + __in PCWSTR pszSource + ); + + HRESULT + CopyA( + __in PCSTR pszCopyA + ); + + HRESULT + CopyA( + __in_bcount(cchLen) + PCSTR pszCopyA, + SIZE_T cchLen, + UINT CodePage = CP_UTF8 + ); + + template + HRESULT + Append( + __in PCWSTR const (&rgpszStrings)[size] + ) + // + // Appends an array of strings declared as stack array. For example: + // + // LPCWSTR rgExample[] { L"one", L"two" }; + // hr = str.Append( rgExample ); + // + { + return AuxAppend( rgpszStrings, _countof( rgpszStrings ) ); + } + + HRESULT + Append( + __in PCWSTR pszAppend + ); + + HRESULT + Append( + __in_ecount(cchLen) + PCWSTR pszAppend, + SIZE_T cchLen + ); + + HRESULT + Append( + __in const STRU * pstrRhs + ); + + HRESULT + Append( + __in const STRU & strRhs + ); + + HRESULT + AppendA( + __in PCSTR pszAppendA + ); + + HRESULT + AppendA( + __in_bcount(cchLen) + PCSTR pszAppendA, + SIZE_T cchLen, + UINT CodePage = CP_UTF8 + ); + + HRESULT + CopyToBuffer( + __out_bcount(*pcb) WCHAR* pszBuffer, + PDWORD pcb + ) const; + + HRESULT + SetLen( + __in DWORD cchLen + ); + + HRESULT + SafeSnwprintf( + __in PCWSTR pwszFormatString, + ... + ); + + HRESULT + SafeVsnwprintf( + __in PCWSTR pwszFormatString, + va_list argsList + ); + + static + HRESULT ExpandEnvironmentVariables( + __in PCWSTR pszString, + __out STRU * pstrExpandedString + ); +private: + + // + // Avoid C++ errors. This object should never go through a copy + // constructor, unintended cast or assignment. + // + STRU( const STRU & ); + STRU & operator = ( const STRU & ); + + HRESULT + AuxAppend( + __in_ecount(cNumStrings) + PCWSTR const rgpszStrings[], + SIZE_T cNumStrings + ); + + HRESULT + AuxAppend( + __in_bcount(cbStr) + const WCHAR* pStr, + SIZE_T cbStr, + DWORD cbOffset + ); + + HRESULT + AuxAppendA( + __in_bcount(cbStr) + const CHAR* pStr, + SIZE_T cbStr, + DWORD cbOffset, + UINT CodePage + ); + + // + // Buffer with an inline buffer of 1, + // enough to hold null-terminating character. + // + BUFFER_T m_Buff; + DWORD m_cchLen; +}; + +// +// Helps to initialize an external buffer before +// constructing the STRU object. +// +template +WCHAR* InitHelper(__out WCHAR (&psz)[size]) +{ + psz[0] = L'\0'; + return psz; +} + +// +// Heap operation reduction macros +// +#define STACK_STRU(name, size) WCHAR __ach##name[size];\ + STRU name(InitHelper(__ach##name), sizeof(__ach##name)/sizeof(*__ach##name)) + +#define INLINE_STRU(name, size) WCHAR __ach##name[size];\ + STRU name; + +#define INLINE_STRU_INIT(name) name(InitHelper(__ach##name), sizeof(__ach##name)/sizeof(*__ach##name)) + + +HRESULT +MakePathCanonicalizationProof( + IN PCWSTR pszName, + OUT STRU * pstrPath +); +#pragma warning( pop ) diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/tracelog.c b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/tracelog.c new file mode 100644 index 0000000000..6c0d080299 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/tracelog.c @@ -0,0 +1,234 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include +#include "tracelog.h" +#include + + +#define ALLOC_MEM(cb) (PVOID)LocalAlloc( LPTR, (cb) ) +#define FREE_MEM(ptr) (VOID)LocalFree( (HLOCAL)(ptr) ) + + + +PTRACE_LOG +CreateTraceLog( + IN LONG LogSize, + IN LONG ExtraBytesInHeader, + IN LONG EntrySize + ) +/*++ + +Routine Description: + + Creates a new (empty) trace log buffer. + +Arguments: + + LogSize - The number of entries in the log. + + ExtraBytesInHeader - The number of extra bytes to include in the + log header. This is useful for adding application-specific + data to the log. + + EntrySize - The size (in bytes) of each entry. + +Return Value: + + PTRACE_LOG - Pointer to the newly created log if successful, + NULL otherwise. + +--*/ +{ + + ULONG ulTotalSize = 0; + ULONG ulLogSize = 0; + ULONG ulEntrySize = 0; + ULONG ulTmpResult = 0; + ULONG ulExtraBytesInHeader = 0; + PTRACE_LOG log = NULL; + HRESULT hr = S_OK; + + // + // Sanity check the parameters. + // + + //DBG_ASSERT( LogSize > 0 ); + //DBG_ASSERT( EntrySize > 0 ); + //DBG_ASSERT( ExtraBytesInHeader >= 0 ); + //DBG_ASSERT( ( EntrySize & 3 ) == 0 ); + + // + // converting to unsigned long. Since all these values are positive + // so its safe to cast them to their unsigned equivalent directly. + // + ulLogSize = (ULONG) LogSize; + ulEntrySize = (ULONG) EntrySize; + ulExtraBytesInHeader = (ULONG) ExtraBytesInHeader; + + // + // Check if the multiplication operation will overflow a LONG + // ulTotalSize = LogSize * EntrySize; + // + hr = ULongMult( ulLogSize, ulEntrySize, &ulTotalSize ); + if ( FAILED(hr) ) + { + SetLastError( ERROR_ARITHMETIC_OVERFLOW ); + return NULL; + } + + // + // check for overflow in addition operation. + // ulTmpResult = sizeof(TRACE_LOG) + ulExtraBytesInHeader + // + hr = ULongAdd( (ULONG) sizeof(TRACE_LOG), ulExtraBytesInHeader, &ulTmpResult ); + if ( FAILED(hr) ) + { + SetLastError( ERROR_ARITHMETIC_OVERFLOW ); + return NULL; + } + + // + // check for overflow in addition operation. + // ulTotalSize = ulTotalSize + ulTmpResult; + // + hr = ULongAdd( ulTmpResult, ulTotalSize, &ulTotalSize ); + if ( FAILED(hr) ) + { + SetLastError( ERROR_ARITHMETIC_OVERFLOW ); + return NULL; + } + + if ( ulTotalSize > (ULONG) 0x7FFFFFFF ) + { + SetLastError( ERROR_ARITHMETIC_OVERFLOW ); + return NULL; + } + + // + // Allocate & initialize the log structure. + // + + log = (PTRACE_LOG)ALLOC_MEM( ulTotalSize ); + + // + // Initialize it. + // + + if( log != NULL ) { + + RtlZeroMemory( log, ulTotalSize ); + + log->Signature = TRACE_LOG_SIGNATURE; + log->LogSize = LogSize; + log->NextEntry = -1; + log->EntrySize = EntrySize; + log->LogBuffer = (PUCHAR)( log + 1 ) + ExtraBytesInHeader; + } + + return log; + +} // CreateTraceLog + + +VOID +DestroyTraceLog( + IN PTRACE_LOG Log + ) +/*++ + +Routine Description: + + Destroys a trace log buffer created with CreateTraceLog(). + +Arguments: + + Log - The trace log buffer to destroy. + +Return Value: + + None. + +--*/ +{ + if ( Log != NULL ) { + //DBG_ASSERT( Log->Signature == TRACE_LOG_SIGNATURE ); + + Log->Signature = TRACE_LOG_SIGNATURE_X; + FREE_MEM( Log ); + } + +} // DestroyTraceLog + + +LONG +WriteTraceLog( + IN PTRACE_LOG Log, + IN PVOID Entry + ) +/*++ + +Routine Description: + + Writes a new entry to the specified trace log. + +Arguments: + + Log - The log to write to. + + Entry - Pointer to the data to write. This buffer is assumed to be + Log->EntrySize bytes long. + +Return Value: + + Index of entry in log. This is useful for correlating the output + of !inetdbg.ref to a particular point in the output debug stream + +--*/ +{ + + PUCHAR target; + ULONG index; + + //DBG_ASSERT( Log != NULL ); + //DBG_ASSERT( Log->Signature == TRACE_LOG_SIGNATURE ); + //DBG_ASSERT( Entry != NULL ); + + // + // Find the next slot, copy the entry to the slot. + // + + index = ( (ULONG) InterlockedIncrement( &Log->NextEntry ) ) % (ULONG) Log->LogSize; + + //DBG_ASSERT( index < (ULONG) Log->LogSize ); + + target = Log->LogBuffer + ( index * Log->EntrySize ); + + RtlCopyMemory( + target, + Entry, + Log->EntrySize + ); + + return index; +} // WriteTraceLog + + +VOID +ResetTraceLog( + IN PTRACE_LOG Log + ) +{ + + //DBG_ASSERT( Log != NULL ); + //DBG_ASSERT( Log->Signature == TRACE_LOG_SIGNATURE ); + + RtlZeroMemory( + ( Log + 1 ), + Log->LogSize * Log->EntrySize + ); + + Log->NextEntry = -1; + +} // ResetTraceLog + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/tracelog.h b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/tracelog.h new file mode 100644 index 0000000000..ed34bcffc9 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/tracelog.h @@ -0,0 +1,105 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#ifndef _TRACELOG_H_ +#define _TRACELOG_H_ + + +#if defined(__cplusplus) +extern "C" { +#endif // __cplusplus + + +typedef struct _TRACE_LOG { + + // + // Signature. + // + + LONG Signature; + + // + // The total number of entries available in the log. + // + + LONG LogSize; + + // + // The index of the next entry to use. + // + + LONG NextEntry; + + // + // The byte size of each entry. + // + + LONG EntrySize; + + // + // Pointer to the start of the circular buffer. + // + + PUCHAR LogBuffer; + + // + // The extra header bytes and actual log entries go here. + // + // BYTE ExtraHeaderBytes[ExtraBytesInHeader]; + // BYTE Entries[LogSize][EntrySize]; + // + +} TRACE_LOG, *PTRACE_LOG; + + +// +// Log header signature. +// + +#define TRACE_LOG_SIGNATURE ((DWORD)'gOlT') +#define TRACE_LOG_SIGNATURE_X ((DWORD)'golX') + + +// +// This macro maps a TRACE_LOG pointer to a pointer to the 'extra' +// data associated with the log. +// + +#define TRACE_LOG_TO_EXTRA_DATA(log) (PVOID)( (log) + 1 ) + + +// +// Manipulators. +// + +PTRACE_LOG +CreateTraceLog( + IN LONG LogSize, + IN LONG ExtraBytesInHeader, + IN LONG EntrySize + ); + +VOID +DestroyTraceLog( + IN PTRACE_LOG Log + ); + +LONG +WriteTraceLog( + IN PTRACE_LOG Log, + IN PVOID Entry + ); + +VOID +ResetTraceLog( + IN PTRACE_LOG Log + ); + + +#if defined(__cplusplus) +} // extern "C" +#endif // __cplusplus + + +#endif // _TRACELOG_H_ + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/treehash.h b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/treehash.h new file mode 100644 index 0000000000..baa50726ce --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/treehash.h @@ -0,0 +1,850 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include +#include "rwlock.h" +#include "prime.h" + +template +class TREE_HASH_NODE +{ + template + friend class TREE_HASH_TABLE; + + private: + // Next node in the hash table look-aside + TREE_HASH_NODE<_Record> *_pNext; + + // links in the tree structure + TREE_HASH_NODE * _pParentNode; + TREE_HASH_NODE * _pFirstChild; + TREE_HASH_NODE * _pNextSibling; + + // actual record + _Record * _pRecord; + + // hash value + PCWSTR _pszPath; + DWORD _dwHash; +}; + +template +class TREE_HASH_TABLE +{ +protected: + typedef BOOL + (PFN_DELETE_IF)( + _Record * pRecord, + PVOID pvContext + ); + + typedef VOID + (PFN_APPLY)( + _Record * pRecord, + PVOID pvContext + ); + +public: + TREE_HASH_TABLE( + BOOL fCaseSensitive + ) : _ppBuckets( NULL ), + _nBuckets( 0 ), + _nItems( 0 ), + _fCaseSensitive( fCaseSensitive ) + { + } + + virtual + ~TREE_HASH_TABLE(); + + virtual + VOID + ReferenceRecord( + _Record * pRecord + ) = 0; + + virtual + VOID + DereferenceRecord( + _Record * pRecord + ) = 0; + + virtual + PCWSTR + GetKey( + _Record * pRecord + ) = 0; + + DWORD + Count() + { + return _nItems; + } + + virtual + VOID + Clear(); + + HRESULT + Initialize( + DWORD nBucketSize + ); + + DWORD + CalcHash( + PCWSTR pszKey + ) + { + return _fCaseSensitive ? HashString(pszKey) : HashStringNoCase(pszKey); + } + + virtual + VOID + FindKey( + PCWSTR pszKey, + _Record ** ppRecord + ); + + virtual + HRESULT + InsertRecord( + _Record * pRecord + ); + + virtual + VOID + DeleteKey( + PCWSTR pszKey + ); + + virtual + VOID + DeleteIf( + PFN_DELETE_IF pfnDeleteIf, + PVOID pvContext + ); + + VOID + Apply( + PFN_APPLY pfnApply, + PVOID pvContext + ); + +private: + + BOOL + FindNodeInternal( + PCWSTR pszKey, + DWORD dwHash, + TREE_HASH_NODE<_Record> ** ppNode, + TREE_HASH_NODE<_Record> *** pppPreviousNodeNextPointer = NULL + ); + + HRESULT + AddNodeInternal( + PCWSTR pszPath, + DWORD dwHash, + _Record * pRecord, + TREE_HASH_NODE<_Record> * pParentNode, + TREE_HASH_NODE<_Record> ** ppNewNode + ); + + HRESULT + AllocateNode( + PCWSTR pszPath, + DWORD dwHash, + _Record * pRecord, + TREE_HASH_NODE<_Record> * pParentNode, + TREE_HASH_NODE<_Record> ** ppNewNode + ); + + VOID + DeleteNode( + TREE_HASH_NODE<_Record> * pNode + ) + { + if (pNode->_pRecord != NULL) + { + DereferenceRecord(pNode->_pRecord); + pNode->_pRecord = NULL; + } + + HeapFree(GetProcessHeap(), + 0, + pNode); + } + + VOID + DeleteNodeInternal( + TREE_HASH_NODE<_Record> ** ppPreviousNodeNextPointer, + TREE_HASH_NODE<_Record> * pNode + ); + + VOID + RehashTableIfNeeded( + VOID + ); + + TREE_HASH_NODE<_Record> ** _ppBuckets; + DWORD _nBuckets; + DWORD _nItems; + BOOL _fCaseSensitive; + CWSDRWLock _tableLock; +}; + +template +HRESULT +TREE_HASH_TABLE<_Record>::AllocateNode( + PCWSTR pszPath, + DWORD dwHash, + _Record * pRecord, + TREE_HASH_NODE<_Record> * pParentNode, + TREE_HASH_NODE<_Record> ** ppNewNode +) +{ + // + // Allocate enough extra space for pszPath + // + DWORD cchPath = (DWORD) wcslen(pszPath); + if (cchPath >= ((0xffffffff - sizeof(TREE_HASH_NODE<_Record>))/sizeof(WCHAR) - 1)) + { + return HRESULT_FROM_WIN32(ERROR_NOT_ENOUGH_MEMORY); + } + TREE_HASH_NODE<_Record> *pNode = (TREE_HASH_NODE<_Record> *)HeapAlloc( + GetProcessHeap(), + HEAP_ZERO_MEMORY, + sizeof(TREE_HASH_NODE<_Record>) + (cchPath+1)*sizeof(WCHAR)); + if (pNode == NULL) + { + return HRESULT_FROM_WIN32(ERROR_NOT_ENOUGH_MEMORY); + } + + memcpy(pNode+1, pszPath, (cchPath+1)*sizeof(WCHAR)); + pNode->_pszPath = (PCWSTR)(pNode+1); + pNode->_dwHash = dwHash; + pNode->_pNext = pNode->_pNextSibling = pNode->_pFirstChild = NULL; + pNode->_pParentNode = pParentNode; + pNode->_pRecord = pRecord; + + *ppNewNode = pNode; + return S_OK; +} + +template +HRESULT +TREE_HASH_TABLE<_Record>::Initialize( + DWORD nBuckets +) +{ + HRESULT hr = S_OK; + + if ( nBuckets == 0 ) + { + hr = E_INVALIDARG; + goto Failed; + } + + hr = _tableLock.Init(); + if ( FAILED( hr ) ) + { + goto Failed; + } + + if (nBuckets >= 0xffffffff/sizeof(TREE_HASH_NODE<_Record> *)) + { + hr = E_INVALIDARG; + goto Failed; + } + + _ppBuckets = (TREE_HASH_NODE<_Record> **)HeapAlloc( + GetProcessHeap(), + HEAP_ZERO_MEMORY, + nBuckets*sizeof(TREE_HASH_NODE<_Record> *)); + if (_ppBuckets == NULL) + { + hr = HRESULT_FROM_WIN32(ERROR_NOT_ENOUGH_MEMORY); + goto Failed; + } + _nBuckets = nBuckets; + + return S_OK; + +Failed: + + if (_ppBuckets) + { + HeapFree(GetProcessHeap(), + 0, + _ppBuckets); + _ppBuckets = NULL; + } + + return hr; +} + + +template +TREE_HASH_TABLE<_Record>::~TREE_HASH_TABLE() +{ + if (_ppBuckets == NULL) + { + return; + } + + _ASSERTE(_nItems == 0); + + HeapFree(GetProcessHeap(), + 0, + _ppBuckets); + _ppBuckets = NULL; + _nBuckets = 0; +} + +template +VOID +TREE_HASH_TABLE<_Record>::Clear() +{ + TREE_HASH_NODE<_Record> *pCurrent; + TREE_HASH_NODE<_Record> *pNext; + + _tableLock.ExclusiveAcquire(); + + for (DWORD i=0; i<_nBuckets; i++) + { + pCurrent = _ppBuckets[i]; + _ppBuckets[i] = NULL; + while (pCurrent != NULL) + { + pNext = pCurrent->_pNext; + DeleteNode(pCurrent); + pCurrent = pNext; + } + } + + _nItems = 0; + _tableLock.ExclusiveRelease(); +} + +template +BOOL +TREE_HASH_TABLE<_Record>::FindNodeInternal( + PCWSTR pszKey, + DWORD dwHash, + TREE_HASH_NODE<_Record> ** ppNode, + TREE_HASH_NODE<_Record> *** pppPreviousNodeNextPointer +) +/*++ + Return value indicates whether the item is found + key, dwHash - key and hash for the node to find + ppNode - on successful return, the node found, on failed return, the first + node with hash value greater than the node to be found + pppPreviousNodeNextPointer - the pointer to previous node's _pNext + + This routine may be called under either read or write lock +--*/ +{ + TREE_HASH_NODE<_Record> **ppPreviousNodeNextPointer; + TREE_HASH_NODE<_Record> *pNode; + BOOL fFound = FALSE; + + ppPreviousNodeNextPointer = _ppBuckets + (dwHash % _nBuckets); + pNode = *ppPreviousNodeNextPointer; + while (pNode != NULL) + { + if (pNode->_dwHash == dwHash) + { + if (CompareStringOrdinal(pszKey, + -1, + pNode->_pszPath, + -1, + !_fCaseSensitive) == CSTR_EQUAL) + { + fFound = TRUE; + break; + } + } + else if (pNode->_dwHash > dwHash) + { + break; + } + + ppPreviousNodeNextPointer = &(pNode->_pNext); + pNode = *ppPreviousNodeNextPointer; + } + + *ppNode = pNode; + if (pppPreviousNodeNextPointer != NULL) + { + *pppPreviousNodeNextPointer = ppPreviousNodeNextPointer; + } + return fFound; +} + +template +VOID +TREE_HASH_TABLE<_Record>::FindKey( + PCWSTR pszKey, + _Record ** ppRecord +) +{ + TREE_HASH_NODE<_Record> *pNode; + + *ppRecord = NULL; + + DWORD dwHash = CalcHash(pszKey); + + _tableLock.SharedAcquire(); + + if (FindNodeInternal(pszKey, dwHash, &pNode) && + pNode->_pRecord != NULL) + { + ReferenceRecord(pNode->_pRecord); + *ppRecord = pNode->_pRecord; + } + + _tableLock.SharedRelease(); +} + +template +HRESULT +TREE_HASH_TABLE<_Record>::AddNodeInternal( + PCWSTR pszPath, + DWORD dwHash, + _Record * pRecord, + TREE_HASH_NODE<_Record> * pParentNode, + TREE_HASH_NODE<_Record> ** ppNewNode +) +/*++ + Return value is HRESULT indicating sucess or failure + pszPath, dwHash, pRecord - path, hash value and record to be inserted + pParentNode - this will be the parent of the node being inserted + ppNewNode - on successful return, the new node created and inserted + + This function may be called under a read or write lock +--*/ +{ + TREE_HASH_NODE<_Record> *pNewNode; + TREE_HASH_NODE<_Record> *pNextNode; + TREE_HASH_NODE<_Record> **ppNextPointer; + HRESULT hr; + + // + // Ownership of pRecord is not transferred to pNewNode yet, so remember + // to either set it to null before deleting pNewNode or add an extra + // reference later - this is to make sure we do not do an extra ref/deref + // which users may view as getting flushed out of the hash-table + // + hr = AllocateNode(pszPath, + dwHash, + pRecord, + pParentNode, + &pNewNode); + if (FAILED(hr)) + { + return hr; + } + + do + { + // + // Find the right place to add this node + // + + if (FindNodeInternal(pszPath, dwHash, &pNextNode, &ppNextPointer)) + { + // + // If node already there, record may still need updating + // + if (pRecord != NULL && + InterlockedCompareExchangePointer((PVOID *)&pNextNode->_pRecord, + pRecord, + NULL) == NULL) + { + ReferenceRecord(pRecord); + hr = S_OK; + } + else + { + hr = HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS); + } + + // ownership of pRecord has either passed to existing record or + // not to anyone at all + pNewNode->_pRecord = NULL; + DeleteNode(pNewNode); + *ppNewNode = pNextNode; + return hr; + } + + // + // If another node got inserted in betwen, we will have to retry + // + pNewNode->_pNext = pNextNode; + } while (InterlockedCompareExchangePointer((PVOID *)ppNextPointer, + pNewNode, + pNextNode) != pNextNode); + // pass ownership of pRecord now + if (pRecord != NULL) + { + ReferenceRecord(pRecord); + pRecord = NULL; + } + InterlockedIncrement((LONG *)&_nItems); + + // + // update the parent + // + if (pParentNode != NULL) + { + ppNextPointer = &pParentNode->_pFirstChild; + do + { + pNextNode = *ppNextPointer; + pNewNode->_pNextSibling = pNextNode; + } while (InterlockedCompareExchangePointer((PVOID *)ppNextPointer, + pNewNode, + pNextNode) != pNextNode); + } + + *ppNewNode = pNewNode; + return S_OK; +} + +template +HRESULT +TREE_HASH_TABLE<_Record>::InsertRecord( + _Record * pRecord +) +/*++ + This method inserts a node for this record and also empty nodes for paths + in the heirarchy leading upto this path + + The insert is done under only a read-lock - this is possible by keeping + the hashes in a bucket in increasing order and using interlocked operations + to actually insert the item in the hash-bucket lookaside list and the parent + children list + + Returns HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS) if the record already exists. + Never leak this error to the end user because "*file* already exists" may be confusing. +--*/ +{ + PCWSTR pszKey = GetKey(pRecord); + STACK_STRU( strPartialPath, 256); + PWSTR pszPartialPath; + DWORD dwHash; + DWORD cchEnd; + HRESULT hr; + TREE_HASH_NODE<_Record> *pParentNode = NULL; + + hr = strPartialPath.Copy(pszKey); + if (FAILED(hr)) + { + goto Finished; + } + pszPartialPath = strPartialPath.QueryStr(); + + _tableLock.SharedAcquire(); + + // + // First find the lowest parent node present + // + for (cchEnd = strPartialPath.QueryCCH() - 1; cchEnd > 0; cchEnd--) + { + if (pszPartialPath[cchEnd] == L'/' || pszPartialPath[cchEnd] == L'\\') + { + pszPartialPath[cchEnd] = L'\0'; + + dwHash = CalcHash(pszPartialPath); + if (FindNodeInternal(pszPartialPath, dwHash, &pParentNode)) + { + pszPartialPath[cchEnd] = pszKey[cchEnd]; + break; + } + pParentNode = NULL; + } + } + + // + // Now go ahead and add the rest of the tree (including our record) + // + for (; cchEnd <= strPartialPath.QueryCCH(); cchEnd++) + { + if (pszPartialPath[cchEnd] == L'\0') + { + dwHash = CalcHash(pszPartialPath); + hr = AddNodeInternal( + pszPartialPath, + dwHash, + (cchEnd == strPartialPath.QueryCCH()) ? pRecord : NULL, + pParentNode, + &pParentNode); + if (FAILED(hr) && + hr != HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS)) + { + goto Finished; + } + + pszPartialPath[cchEnd] = pszKey[cchEnd]; + } + } + +Finished: + _tableLock.SharedRelease(); + + if (SUCCEEDED(hr)) + { + RehashTableIfNeeded(); + } + + return hr; +} + +template +VOID +TREE_HASH_TABLE<_Record>::DeleteNodeInternal( + TREE_HASH_NODE<_Record> ** ppNextPointer, + TREE_HASH_NODE<_Record> * pNode +) +/*++ + pNode is the node to be deleted + ppNextPointer is the pointer to the previous node's next pointer pointing + to this node + + This function should be called under write-lock +--*/ +{ + // + // First remove this node from hash table + // + *ppNextPointer = pNode->_pNext; + + // + // Now fixup parent + // + if (pNode->_pParentNode != NULL) + { + ppNextPointer = &pNode->_pParentNode->_pFirstChild; + while (*ppNextPointer != pNode) + { + ppNextPointer = &(*ppNextPointer)->_pNextSibling; + } + *ppNextPointer = pNode->_pNextSibling; + } + + // + // Now remove all children recursively + // + TREE_HASH_NODE<_Record> *pChild = pNode->_pFirstChild; + TREE_HASH_NODE<_Record> *pNextChild; + while (pChild != NULL) + { + pNextChild = pChild->_pNextSibling; + + ppNextPointer = _ppBuckets + (pChild->_dwHash % _nBuckets); + while (*ppNextPointer != pChild) + { + ppNextPointer = &(*ppNextPointer)->_pNext; + } + pChild->_pParentNode = NULL; + DeleteNodeInternal(ppNextPointer, pChild); + + pChild = pNextChild; + } + + DeleteNode(pNode); + _nItems--; +} + +template +VOID +TREE_HASH_TABLE<_Record>::DeleteKey( + PCWSTR pszKey +) +{ + TREE_HASH_NODE<_Record> *pNode; + TREE_HASH_NODE<_Record> **ppPreviousNodeNextPointer; + + DWORD dwHash = CalcHash(pszKey); + + _tableLock.ExclusiveAcquire(); + + if (FindNodeInternal(pszKey, dwHash, &pNode, &ppPreviousNodeNextPointer)) + { + DeleteNodeInternal(ppPreviousNodeNextPointer, pNode); + } + + _tableLock.ExclusiveRelease(); +} + +template +VOID +TREE_HASH_TABLE<_Record>::DeleteIf( + PFN_DELETE_IF pfnDeleteIf, + PVOID pvContext +) +{ + TREE_HASH_NODE<_Record> *pNode; + TREE_HASH_NODE<_Record> **ppPreviousNodeNextPointer; + BOOL fDelete; + + _tableLock.ExclusiveAcquire(); + + for (DWORD i=0; i<_nBuckets; i++) + { + ppPreviousNodeNextPointer = _ppBuckets + i; + pNode = *ppPreviousNodeNextPointer; + while (pNode != NULL) + { + // + // Non empty nodes deleted based on DeleteIf, empty nodes deleted + // if they have no children + // + fDelete = FALSE; + if (pNode->_pRecord != NULL) + { + if (pfnDeleteIf(pNode->_pRecord, pvContext)) + { + fDelete = TRUE; + } + } + else if (pNode->_pFirstChild == NULL) + { + fDelete = TRUE; + } + + if (fDelete) + { + if (pNode->_pFirstChild == NULL) + { + DeleteNodeInternal(ppPreviousNodeNextPointer, pNode); + } + else + { + DereferenceRecord(pNode->_pRecord); + pNode->_pRecord = NULL; + } + } + else + { + ppPreviousNodeNextPointer = &pNode->_pNext; + } + + pNode = *ppPreviousNodeNextPointer; + } + } + + _tableLock.ExclusiveRelease(); +} + +template +VOID +TREE_HASH_TABLE<_Record>::Apply( + PFN_APPLY pfnApply, + PVOID pvContext +) +{ + TREE_HASH_NODE<_Record> *pNode; + + _tableLock.SharedAcquire(); + + for (DWORD i=0; i<_nBuckets; i++) + { + pNode = _ppBuckets[i]; + while (pNode != NULL) + { + if (pNode->_pRecord != NULL) + { + pfnApply(pNode->_pRecord, pvContext); + } + + pNode = pNode->_pNext; + } + } + + _tableLock.SharedRelease(); +} + +template +VOID +TREE_HASH_TABLE<_Record>::RehashTableIfNeeded( + VOID +) +{ + TREE_HASH_NODE<_Record> **ppBuckets; + DWORD nBuckets; + TREE_HASH_NODE<_Record> *pNode; + TREE_HASH_NODE<_Record> *pNextNode; + TREE_HASH_NODE<_Record> **ppNextPointer; + TREE_HASH_NODE<_Record> *pNewNextNode; + DWORD nNewBuckets; + + // + // If number of items has become too many, we will double the hash table + // size (we never reduce it however) + // + if (_nItems <= PRIME::GetPrime(2*_nBuckets)) + { + return; + } + + _tableLock.ExclusiveAcquire(); + + nNewBuckets = PRIME::GetPrime(2*_nBuckets); + + if (_nItems <= nNewBuckets) + { + goto Finished; + } + + nBuckets = nNewBuckets; + if (nBuckets >= 0xffffffff/sizeof(TREE_HASH_NODE<_Record> *)) + { + goto Finished; + } + ppBuckets = (TREE_HASH_NODE<_Record> **)HeapAlloc( + GetProcessHeap(), + HEAP_ZERO_MEMORY, + nBuckets*sizeof(TREE_HASH_NODE<_Record> *)); + if (ppBuckets == NULL) + { + goto Finished; + } + + // + // Take out nodes from the old hash table and insert in the new one, make + // sure to keep the hashes in increasing order + // + for (DWORD i=0; i<_nBuckets; i++) + { + pNode = _ppBuckets[i]; + while (pNode != NULL) + { + pNextNode = pNode->_pNext; + + ppNextPointer = ppBuckets + (pNode->_dwHash % nBuckets); + pNewNextNode = *ppNextPointer; + while (pNewNextNode != NULL && + pNewNextNode->_dwHash <= pNode->_dwHash) + { + ppNextPointer = &pNewNextNode->_pNext; + pNewNextNode = pNewNextNode->_pNext; + } + pNode->_pNext = pNewNextNode; + *ppNextPointer = pNode; + + pNode = pNextNode; + } + } + + HeapFree(GetProcessHeap(), 0, _ppBuckets); + _ppBuckets = ppBuckets; + _nBuckets = nBuckets; + ppBuckets = NULL; + +Finished: + + _tableLock.ExclusiveRelease(); +} + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/util.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/util.cpp new file mode 100644 index 0000000000..214ee65abf --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/IISLib/util.cpp @@ -0,0 +1,78 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "precomp.h" + +HRESULT +MakePathCanonicalizationProof( + IN PCWSTR pszName, + OUT STRU * pstrPath +) +/*++ + +Routine Description: + + This functions adds a prefix + to the string, which is "\\?\UNC\" for a UNC path, and "\\?\" for + other paths. This prefix tells Windows not to parse the path. + +Arguments: + + IN pszName - The path to be converted + OUT pstrPath - Output path created + +Return Values: + + HRESULT + +--*/ +{ + HRESULT hr; + + if (pszName[0] == L'\\' && pszName[1] == L'\\') + { + // + // If the path is already canonicalized, just return + // + + if ((pszName[2] == '?' || pszName[2] == '.') && + pszName[3] == '\\') + { + hr = pstrPath->Copy(pszName); + + if (SUCCEEDED(hr)) + { + // + // If the path was in DOS form ("\\.\"), + // we need to change it to Win32 from ("\\?\") + // + + pstrPath->QueryStr()[2] = L'?'; + } + + return hr; + } + + pszName += 2; + + + if (FAILED(hr = pstrPath->Copy(L"\\\\?\\UNC\\"))) + { + return hr; + } + } + else if (wcslen(pszName) > MAX_PATH) + { + if (FAILED(hr = pstrPath->Copy(L"\\\\?\\"))) + { + return hr; + } + } + else + { + pstrPath->Reset(); + } + + return pstrPath->Append(pszName); +} + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/HtmlResponses.rc b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/HtmlResponses.rc new file mode 100644 index 0000000000..25a99683c5 Binary files /dev/null and b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/HtmlResponses.rc differ diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.cpp new file mode 100644 index 0000000000..fb9fe8a015 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.cpp @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "InProcessApplicationBase.h" + +InProcessApplicationBase::InProcessApplicationBase( + IHttpServer& pHttpServer, + IHttpApplication& pHttpApplication) + : AppOfflineTrackingApplication(pHttpApplication), + m_fRecycleCalled(FALSE), + m_pHttpServer(pHttpServer) +{ +} + +VOID +InProcessApplicationBase::StopInternal(bool fServerInitiated) +{ + AppOfflineTrackingApplication::StopInternal(fServerInitiated); + + // Stop was initiated by server no need to do anything, server would stop on it's own + if (fServerInitiated) + { + return; + } + + if (!m_pHttpServer.IsCommandLineLaunch()) + { + // IIS scenario. + // We don't actually handle any shutdown logic here. + // Instead, we notify IIS that the process needs to be recycled, which will call + // ApplicationManager->Shutdown(). This will call shutdown on the application. + m_pHttpServer.RecycleProcess(L"AspNetCore InProcess Recycle Process on Demand"); + } + else + { + // Send WM_QUIT to the main window to initiate graceful shutdown + EnumWindows([](HWND hwnd, LPARAM) -> BOOL + { + DWORD processId; + + if (GetWindowThreadProcessId(hwnd, &processId) && + processId == GetCurrentProcessId() && + GetConsoleWindow() != hwnd) + { + PostMessage(hwnd, WM_QUIT, 0, 0); + return false; + } + + return true; + }, 0); + } +} + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.h b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.h new file mode 100644 index 0000000000..5bc2dd41e1 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.h @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "AppOfflineTrackingApplication.h" + +typedef INT(*hostfxr_main_fn) (CONST DWORD argc, CONST PCWSTR argv[]); // TODO these may need to be BSTRs + +class InProcessApplicationBase : public AppOfflineTrackingApplication +{ +public: + + InProcessApplicationBase( + IHttpServer& pHttpServer, + IHttpApplication& pHttpApplication); + + ~InProcessApplicationBase() = default; + + VOID StopInternal(bool fServerInitiated) override; + +protected: + BOOL m_fRecycleCalled; + IHttpServer& m_pHttpServer; +}; + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.cpp new file mode 100644 index 0000000000..1b3e22e1dc --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.cpp @@ -0,0 +1,67 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "InProcessOptions.h" +#include "InvalidOperationException.h" +#include "EventLog.h" + +HRESULT InProcessOptions::Create( + IHttpServer& pServer, + IHttpApplication& pHttpApplication, + std::unique_ptr& options) +{ + try + { + const WebConfigConfigurationSource configurationSource(pServer.GetAdminManager(), pHttpApplication); + options = std::make_unique(configurationSource); + } + catch (InvalidOperationException& ex) + { + EventLog::Error( + ASPNETCORE_CONFIGURATION_LOAD_ERROR, + ASPNETCORE_CONFIGURATION_LOAD_ERROR_MSG, + ex.as_wstring().c_str()); + + RETURN_CAUGHT_EXCEPTION(); + } + catch (std::runtime_error& ex) + { + EventLog::Error( + ASPNETCORE_CONFIGURATION_LOAD_ERROR, + ASPNETCORE_CONFIGURATION_LOAD_ERROR_MSG, + GetUnexpectedExceptionMessage(ex).c_str()); + + RETURN_CAUGHT_EXCEPTION(); + } + CATCH_RETURN(); + + return S_OK; +} + +InProcessOptions::InProcessOptions(const ConfigurationSource &configurationSource) : + m_fStdoutLogEnabled(false), + m_fWindowsAuthEnabled(false), + m_fBasicAuthEnabled(false), + m_fAnonymousAuthEnabled(false), + m_dwStartupTimeLimitInMS(INFINITE), + m_dwShutdownTimeLimitInMS(INFINITE) +{ + auto const aspNetCoreSection = configurationSource.GetRequiredSection(CS_ASPNETCORE_SECTION); + m_strArguments = aspNetCoreSection->GetString(CS_ASPNETCORE_PROCESS_ARGUMENTS).value_or(CS_ASPNETCORE_PROCESS_ARGUMENTS_DEFAULT); + m_strProcessPath = aspNetCoreSection->GetRequiredString(CS_ASPNETCORE_PROCESS_EXE_PATH); + m_fStdoutLogEnabled = aspNetCoreSection->GetRequiredBool(CS_ASPNETCORE_STDOUT_LOG_ENABLED); + m_struStdoutLogFile = aspNetCoreSection->GetRequiredString(CS_ASPNETCORE_STDOUT_LOG_FILE); + m_fDisableStartUpErrorPage = aspNetCoreSection->GetRequiredBool(CS_ASPNETCORE_DISABLE_START_UP_ERROR_PAGE); + m_environmentVariables = aspNetCoreSection->GetKeyValuePairs(CS_ASPNETCORE_ENVIRONMENT_VARIABLES); + m_dwStartupTimeLimitInMS = aspNetCoreSection->GetRequiredLong(CS_ASPNETCORE_PROCESS_STARTUP_TIME_LIMIT) * 1000; + m_dwShutdownTimeLimitInMS = aspNetCoreSection->GetRequiredLong(CS_ASPNETCORE_PROCESS_SHUTDOWN_TIME_LIMIT) * 1000; + + const auto basicAuthSection = configurationSource.GetSection(CS_BASIC_AUTHENTICATION_SECTION); + m_fBasicAuthEnabled = basicAuthSection && basicAuthSection->GetBool(CS_ENABLED).value_or(false); + + const auto windowsAuthSection = configurationSource.GetSection(CS_WINDOWS_AUTHENTICATION_SECTION); + m_fWindowsAuthEnabled = windowsAuthSection && windowsAuthSection->GetBool(CS_ENABLED).value_or(false); + + const auto anonAuthSection = configurationSource.GetSection(CS_ANONYMOUS_AUTHENTICATION_SECTION); + m_fAnonymousAuthEnabled = anonAuthSection && anonAuthSection->GetBool(CS_ENABLED).value_or(false); +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.h b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.h new file mode 100644 index 0000000000..e299c2033e --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.h @@ -0,0 +1,112 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include +#include "ConfigurationSource.h" +#include "WebConfigConfigurationSource.h" + +class InProcessOptions: NonCopyable +{ +public: + const std::wstring& + QueryProcessPath() const + { + return m_strProcessPath; + } + + const std::wstring& + QueryArguments() const + { + return m_strArguments; + } + + bool + QueryStdoutLogEnabled() const + { + return m_fStdoutLogEnabled; + } + + const std::wstring& + QueryStdoutLogFile() const + { + return m_struStdoutLogFile; + } + + bool + QueryDisableStartUpErrorPage() const + { + return m_fDisableStartUpErrorPage; + } + + bool + QueryWindowsAuthEnabled() const + { + return m_fWindowsAuthEnabled; + } + + bool + QueryBasicAuthEnabled() const + { + return m_fBasicAuthEnabled; + } + + bool + QueryAnonymousAuthEnabled() const + { + return m_fAnonymousAuthEnabled; + } + + DWORD + QueryStartupTimeLimitInMS() const + { + if (IsDebuggerPresent()) + { + return INFINITE; + } + + return m_dwStartupTimeLimitInMS; + } + + DWORD + QueryShutdownTimeLimitInMS() const + { + if (IsDebuggerPresent()) + { + return INFINITE; + } + + return m_dwShutdownTimeLimitInMS; + } + + const std::vector>& + QueryEnvironmentVariables() const + { + return m_environmentVariables; + } + + InProcessOptions(const ConfigurationSource &configurationSource); + + static + HRESULT InProcessOptions::Create( + IHttpServer& pServer, + IHttpApplication& pHttpApplication, + std::unique_ptr& options); + +private: + std::wstring m_strArguments; + std::wstring m_strProcessPath; + std::wstring m_struStdoutLogFile; + bool m_fStdoutLogEnabled; + bool m_fDisableStartUpErrorPage; + bool m_fWindowsAuthEnabled; + bool m_fBasicAuthEnabled; + bool m_fAnonymousAuthEnabled; + DWORD m_dwStartupTimeLimitInMS; + DWORD m_dwShutdownTimeLimitInMS; + std::vector> m_environmentVariables; + +protected: + InProcessOptions() = default; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessRequestHandler.vcxproj b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessRequestHandler.vcxproj new file mode 100644 index 0000000000..69435f32d6 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessRequestHandler.vcxproj @@ -0,0 +1,276 @@ + + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {D57EA297-6DC2-4BC0-8C91-334863327863} + Win32Proj + InProcessRequestHandler + 10.0.15063.0 + InProcessRequestHandler + + + + DynamicLibrary + true + v141 + Unicode + + + DynamicLibrary + false + v141 + true + Unicode + + + DynamicLibrary + true + v141 + Unicode + + + DynamicLibrary + false + v141 + true + Unicode + + + + + + + + + + + + + + + + + + + + + aspnetcorev2_inprocess + + + false + + + false + + + + Use + Level4 + Disabled + WIN32;_DEBUG;REQUESTHANDLER_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + stdafx.h + $(IntDir)$(TargetName).pch + ProgramDatabase + MultiThreadedDebug + ..\IISLib;..\CommonLib;.\Inc;..\RequestHandlerLib + true + true + true + false + SyncCThrow + 8Bytes + true + false + true + CompileAsCpp + true + stdcpp17 + stdafx.h + true + + + Windows + true + kernel32.lib;user32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;ahadmin.lib;ws2_32.lib;iphlpapi.lib;version.lib + Source.def + UseLinkTimeCodeGeneration + + + + + Use + Level4 + Disabled + WIN32;_DEBUG;REQUESTHANDLER_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + stdafx.h + $(IntDir)$(TargetName).pch + ProgramDatabase + MultiThreadedDebug + ..\IISLib;..\CommonLib;.\Inc;..\RequestHandlerLib + true + true + true + false + SyncCThrow + 8Bytes + true + false + true + CompileAsCpp + true + stdcpp17 + stdafx.h + true + + + Windows + true + kernel32.lib;user32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;ahadmin.lib;ws2_32.lib;iphlpapi.lib;version.lib + Source.def + UseLinkTimeCodeGeneration + + + + + Level4 + Use + MaxSpeed + true + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;REQUESTHANDLER_EXPORTS;%(PreprocessorDefinitions) + stdafx.h + MultiThreaded + ..\IISLib;..\CommonLib;.\Inc;..\RequestHandlerLib + true + true + true + false + SyncCThrow + 8Bytes + true + false + true + CompileAsCpp + true + stdcpp17 + stdafx.h + true + + + Windows + false + true + /NODEFAULTLIB:libucrt.lib /DEFAULTLIB:ucrt.lib %(AdditionalOptions) + true + Source.def + kernel32.lib;user32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;ahadmin.lib;ws2_32.lib;iphlpapi.lib;version.lib + UseLinkTimeCodeGeneration + + + + + Level4 + Use + MaxSpeed + true + true + NDEBUG;REQUESTHANDLER_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + stdafx.h + MultiThreaded + ..\IISLib;..\CommonLib;.\Inc;..\RequestHandlerLib + true + true + true + false + SyncCThrow + 8Bytes + true + false + true + CompileAsCpp + true + stdcpp17 + stdafx.h + true + + + Windows + false + true + /NODEFAULTLIB:libucrt.lib /DEFAULTLIB:ucrt.lib %(AdditionalOptions) + true + Source.def + kernel32.lib;user32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;ahadmin.lib;ws2_32.lib;iphlpapi.lib;version.lib + UseLinkTimeCodeGeneration + + + + + + + + + + + + + + + + + + + + + + Create + Create + Create + Create + + + + + {55494e58-e061-4c4c-a0a8-837008e72f85} + + + {09d9d1d6-2951-4e14-bc35-76a23cf9391a} + + + {1533e271-f61b-441b-8b74-59fb61df0552} + + + + + + + + + true + + + + + + + + \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessRhStaticHtml.htm b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessRhStaticHtml.htm new file mode 100644 index 0000000000..59d6b0dacb --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessRhStaticHtml.htm @@ -0,0 +1,83 @@ + + + + + HTTP Error 500.30 - ANCM In-Process Start Failure + + + +

HTTP Error 500.30 - ANCM In-Process Start Failure

+ +

Common causes of this issue:

+
    +
  • The application failed to start
  • +
  • The application started but then stopped
  • +
  • The application started but threw an exception during startup
  • +
+ +

Troubleshooting steps:

+
    +
  • Check the system event log for error messages
  • +
  • Enable logging the application process' stdout messages
  • +
  • Attach a debugger to the application process and inspect
  • +
+ +

+ For more information visit: + %s https://go.microsoft.com/fwlink/?LinkID=2028265 +

+ + + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/ShuttingDownApplication.h b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/ShuttingDownApplication.h new file mode 100644 index 0000000000..171ad4a2ef --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/ShuttingDownApplication.h @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "InProcessApplicationBase.h" + +class ShuttingDownHandler : public REQUEST_HANDLER +{ +public: + ShuttingDownHandler(IHttpContext* pContext) + : REQUEST_HANDLER(*pContext), + m_pContext(pContext) + { + } + + REQUEST_NOTIFICATION_STATUS ExecuteRequestHandler() override + { + return ServerShutdownMessage(m_pContext); + } + + static REQUEST_NOTIFICATION_STATUS ServerShutdownMessage(IHttpContext * pContext) + { + pContext->GetResponse()->SetStatus(503, "Server has been shutdown", 0, HRESULT_FROM_WIN32(ERROR_SHUTDOWN_IN_PROGRESS)); + return RQ_NOTIFICATION_FINISH_REQUEST; + } +private: + IHttpContext * m_pContext; +}; + +class ShuttingDownApplication : public InProcessApplicationBase +{ +public: + ShuttingDownApplication(IHttpServer& pHttpServer, IHttpApplication& pHttpApplication) + : InProcessApplicationBase(pHttpServer, pHttpApplication) + { + } + + ~ShuttingDownApplication() = default; + + HRESULT CreateHandler(IHttpContext * pHttpContext, IREQUEST_HANDLER ** pRequestHandler) override + { + *pRequestHandler = new ShuttingDownHandler(pHttpContext); + return S_OK; + } +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/Source.def b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/Source.def new file mode 100644 index 0000000000..c540402c00 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/Source.def @@ -0,0 +1,5 @@ +LIBRARY aspnetcorev2_inprocess + +EXPORTS + CreateApplication + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionApplication.h b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionApplication.h new file mode 100644 index 0000000000..72b26d857c --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionApplication.h @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "InProcessApplicationBase.h" +#include "ServerErrorHandler.h" +#include "resource.h" + +class StartupExceptionApplication : public InProcessApplicationBase +{ +public: + StartupExceptionApplication( + IHttpServer& pServer, + IHttpApplication& pApplication, + HINSTANCE moduleInstance, + BOOL disableLogs, + HRESULT hr) + : m_disableLogs(disableLogs), + m_HR(hr), + m_moduleInstance(moduleInstance), + InProcessApplicationBase(pServer, pApplication) + { + } + + ~StartupExceptionApplication() = default; + + HRESULT CreateHandler(IHttpContext *pHttpContext, IREQUEST_HANDLER ** pRequestHandler) + { + *pRequestHandler = new ServerErrorHandler(*pHttpContext, 500, 30, "Internal Server Error", m_HR, m_moduleInstance, m_disableLogs, IN_PROCESS_RH_STATIC_HTML); + return S_OK; + } + + std::string& + GetStaticHtml500Content() + { + return html500Page; + } + +private: + std::string html500Page; + BOOL m_disableLogs; + HRESULT m_HR; + HINSTANCE m_moduleInstance; +}; + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionHandler.h b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionHandler.h new file mode 100644 index 0000000000..1d0f3b7cc9 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionHandler.h @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "requesthandler.h" +#include "resource.h" +#include "file_utility.h" + + +class StartupExceptionHandler : public REQUEST_HANDLER +{ +public: + + StartupExceptionHandler(IHttpContext& pContext, BOOL disableLogs, HRESULT hr) + : m_pContext(pContext), + m_disableLogs(disableLogs), + m_HR(hr) + { + } + + ~StartupExceptionHandler() + { + } + + REQUEST_NOTIFICATION_STATUS OnExecuteRequestHandler() + { + static std::string s_html500Page = FILE_UTILITY::GetHtml(g_hModule, IN_PROCESS_SHIM_STATIC_HTML); + + WriteStaticResponse(m_pContext, s_html500Page, m_HR, m_disableLogs); + + return REQUEST_NOTIFICATION_STATUS::RQ_NOTIFICATION_FINISH_REQUEST; + } + +private: + IHttpContext& m_pContext; + BOOL m_disableLogs; + HRESULT m_HR; +}; + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/dllmain.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/dllmain.cpp new file mode 100644 index 0000000000..eb84da2869 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/dllmain.cpp @@ -0,0 +1,137 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +// dllmain.cpp : Defines the entry point for the DLL application. + +#include + +#include "inprocessapplication.h" +#include "inprocesshandler.h" +#include "requesthandler_config.h" +#include "debugutil.h" +#include "resources.h" +#include "exceptions.h" +#include "ShuttingDownApplication.h" +#include "InProcessOptions.h" +#include "EventLog.h" +#include "WebConfigConfigurationSource.h" +#include "ConfigurationLoadException.h" +#include "StartupExceptionApplication.h" + +DECLARE_DEBUG_PRINT_OBJECT("aspnetcorev2_inprocess.dll"); + +BOOL g_fGlobalInitialize = FALSE; +BOOL g_fProcessDetach = FALSE; +SRWLOCK g_srwLockRH; +IHttpServer * g_pHttpServer = NULL; +HINSTANCE g_hWinHttpModule; +HINSTANCE g_hAspNetCoreModule; +HANDLE g_hEventLog = NULL; +bool g_fInProcessApplicationCreated = false; +HINSTANCE g_hServerModule; + +HRESULT +InitializeGlobalConfiguration( + IHttpServer * pServer, + IHttpApplication* pHttpApplication +) +{ + if (!g_fGlobalInitialize) + { + SRWExclusiveLock lock(g_srwLockRH); + + if (!g_fGlobalInitialize) + { + g_pHttpServer = pServer; + RETURN_IF_FAILED(ALLOC_CACHE_HANDLER::StaticInitialize()); + RETURN_IF_FAILED(IN_PROCESS_HANDLER::StaticInitialize()); + + if (pServer->IsCommandLineLaunch()) + { + g_hEventLog = RegisterEventSource(NULL, ASPNETCORE_IISEXPRESS_EVENT_PROVIDER); + } + else + { + g_hEventLog = RegisterEventSource(NULL, ASPNETCORE_EVENT_PROVIDER); + } + + DebugInitializeFromConfig(*pServer, *pHttpApplication); + + g_fGlobalInitialize = TRUE; + } + } + + return S_OK; +} + +BOOL APIENTRY DllMain(HMODULE hModule, + DWORD ul_reason_for_call, + LPVOID lpReserved +) +{ + UNREFERENCED_PARAMETER(lpReserved); + g_hServerModule = hModule; + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + DisableThreadLibraryCalls(hModule); + InitializeSRWLock(&g_srwLockRH); + DebugInitialize(hModule); + break; + case DLL_PROCESS_DETACH: + g_fProcessDetach = TRUE; + IN_PROCESS_HANDLER::StaticTerminate(); + ALLOC_CACHE_HANDLER::StaticTerminate(); + DebugStop(); + default: + break; + } + return TRUE; +} + +HRESULT +__stdcall +CreateApplication( + _In_ IHttpServer *pServer, + _In_ IHttpApplication *pHttpApplication, + _In_ APPLICATION_PARAMETER *pParameters, + _In_ DWORD nParameters, + _Out_ IAPPLICATION **ppApplication +) +{ + TraceContextScope traceScope(FindParameter("TraceContext", pParameters, nParameters)); + try + { + HRESULT hr = S_OK; + RETURN_IF_FAILED(InitializeGlobalConfiguration(pServer, pHttpApplication)); + + // In process application was already created so another call to CreateApplication + // means that server is shutting does and request arrived in the meantime + if (g_fInProcessApplicationCreated) + { + *ppApplication = new ShuttingDownApplication(*pServer, *pHttpApplication); + return S_OK; + } + + // never create two inprocess applications in one process + g_fInProcessApplicationCreated = true; + + std::unique_ptr inProcessApplication; + if (!FAILED_LOG(hr = IN_PROCESS_APPLICATION::Start(*pServer, *pHttpApplication, pParameters, nParameters, inProcessApplication))) + { + *ppApplication = inProcessApplication.release(); + } + else + { + std::unique_ptr options; + THROW_IF_FAILED(InProcessOptions::Create(*pServer, *pHttpApplication, options)); + // Set the currently running application to a fake application that returns startup exceptions. + auto pErrorApplication = std::make_unique(*pServer, *pHttpApplication, g_hServerModule, options->QueryDisableStartUpErrorPage(), hr); + + RETURN_IF_FAILED(pErrorApplication->StartMonitoringAppOffline()); + *ppApplication = pErrorApplication.release(); + } + return S_OK; + } + CATCH_RETURN(); +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp new file mode 100644 index 0000000000..c351dbc4c1 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp @@ -0,0 +1,508 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "inprocessapplication.h" +#include "inprocesshandler.h" +#include "hostfxroptions.h" +#include "requesthandler_config.h" +#include "environmentvariablehelpers.h" +#include "exceptions.h" +#include "LoggingHelpers.h" +#include "resources.h" +#include "EventLog.h" +#include "ModuleHelpers.h" + +IN_PROCESS_APPLICATION* IN_PROCESS_APPLICATION::s_Application = NULL; + +IN_PROCESS_APPLICATION::IN_PROCESS_APPLICATION( + IHttpServer& pHttpServer, + IHttpApplication& pApplication, + std::unique_ptr pConfig, + APPLICATION_PARAMETER *pParameters, + DWORD nParameters) : + InProcessApplicationBase(pHttpServer, pApplication), + m_Initialized(false), + m_blockManagedCallbacks(true), + m_waitForShutdown(true), + m_pConfig(std::move(pConfig)) +{ + DBG_ASSERT(m_pConfig); + + const auto knownLocation = FindParameter(s_exeLocationParameterName, pParameters, nParameters); + if (knownLocation != nullptr) + { + m_dotnetExeKnownLocation = knownLocation; + } +} + +IN_PROCESS_APPLICATION::~IN_PROCESS_APPLICATION() +{ + s_Application = nullptr; +} + +VOID +IN_PROCESS_APPLICATION::StopInternal(bool fServerInitiated) +{ + StopClr(); + InProcessApplicationBase::StopInternal(fServerInitiated); +} + +VOID +IN_PROCESS_APPLICATION::StopClr() +{ + LOG_INFO(L"Stopping CLR"); + + if (!m_blockManagedCallbacks) + { + // We cannot call into managed if the dll is detaching from the process. + // Calling into managed code when the dll is detaching is strictly a bad idea, + // and usually results in an AV saying "The string binding is invalid" + const auto shutdownHandler = m_ShutdownHandler; + + if (!g_fProcessDetach && shutdownHandler != nullptr) + { + shutdownHandler(m_ShutdownHandlerContext); + } + } + + // Signal shutdown + if (m_pShutdownEvent != nullptr) + { + LOG_IF_FAILED(SetEvent(m_pShutdownEvent)); + } + + if (m_workerThread.joinable()) + { + // Worker thread would wait for clr to finish and log error if required + m_workerThread.join(); + } + + s_Application = nullptr; +} + +VOID +IN_PROCESS_APPLICATION::SetCallbackHandles( + _In_ PFN_REQUEST_HANDLER request_handler, + _In_ PFN_SHUTDOWN_HANDLER shutdown_handler, + _In_ PFN_DISCONNECT_HANDLER disconnect_callback, + _In_ PFN_ASYNC_COMPLETION_HANDLER async_completion_handler, + _In_ VOID* pvRequstHandlerContext, + _In_ VOID* pvShutdownHandlerContext +) +{ + LOG_INFO(L"In-process callbacks set"); + + m_RequestHandler = request_handler; + m_RequestHandlerContext = pvRequstHandlerContext; + m_DisconnectHandler = disconnect_callback; + m_ShutdownHandler = shutdown_handler; + m_ShutdownHandlerContext = pvShutdownHandlerContext; + m_AsyncCompletionHandler = async_completion_handler; + + m_blockManagedCallbacks = false; + m_Initialized = true; + + // Can't check the std err handle as it isn't a critical error + // Initialization complete + EventLog::Info( + ASPNETCORE_EVENT_INPROCESS_START_SUCCESS, + ASPNETCORE_EVENT_INPROCESS_START_SUCCESS_MSG, + QueryApplicationPhysicalPath().c_str()); + + SetEvent(m_pInitializeEvent); +} + +HRESULT +IN_PROCESS_APPLICATION::LoadManagedApplication() +{ + THROW_LAST_ERROR_IF_NULL(m_pInitializeEvent = CreateEvent( + nullptr, // default security attributes + TRUE, // manual reset event + FALSE, // not set + nullptr)); // name + + THROW_LAST_ERROR_IF_NULL(m_pShutdownEvent = CreateEvent( + nullptr, // default security attributes + TRUE, // manual reset event + FALSE, // not set + nullptr)); // name + + LOG_INFO(L"Waiting for initialization"); + + m_workerThread = std::thread([](std::unique_ptr application) + { + LOG_INFO(L"Starting in-process worker thread"); + application->ExecuteApplication(); + LOG_INFO(L"Stopping in-process worker thread"); + }, ::ReferenceApplication(this)); + + const HANDLE waitHandles[2] = { m_pInitializeEvent, m_workerThread.native_handle() }; + + // Wait for shutdown request + const auto waitResult = WaitForMultipleObjects(2, waitHandles, FALSE, m_pConfig->QueryStartupTimeLimitInMS()); + THROW_LAST_ERROR_IF(waitResult == WAIT_FAILED); + + if (waitResult == WAIT_TIMEOUT) + { + // If server wasn't initialized in time shut application down without waiting for CLR thread to exit + m_waitForShutdown = false; + StopClr(); + throw InvalidOperationException(format(L"Managed server didn't initialize after %u ms.", m_pConfig->QueryStartupTimeLimitInMS())); + } + + // WAIT_OBJECT_0 + 1 is the worker thead handle + if (waitResult == WAIT_OBJECT_0 + 1) + { + // Worker thread exited stop + StopClr(); + throw InvalidOperationException(format(L"CLR worker thread exited prematurely")); + } + + THROW_IF_FAILED(StartMonitoringAppOffline()); + + return S_OK; +} + + +void +IN_PROCESS_APPLICATION::ExecuteApplication() +{ + try + { + std::unique_ptr hostFxrOptions; + + auto context = std::make_shared(); + + auto pProc = s_fMainCallback; + if (pProc == nullptr) + { + HMODULE hModule; + // hostfxr should already be loaded by the shim. If not, then we will need + // to load it ourselves by finding hostfxr again. + THROW_LAST_ERROR_IF_NULL(hModule = GetModuleHandle(L"hostfxr.dll")); + + // Get the entry point for main + pProc = reinterpret_cast(GetProcAddress(hModule, "hostfxr_main")); + THROW_LAST_ERROR_IF_NULL(pProc); + + THROW_IF_FAILED(HOSTFXR_OPTIONS::Create( + m_dotnetExeKnownLocation, + m_pConfig->QueryProcessPath(), + QueryApplicationPhysicalPath(), + m_pConfig->QueryArguments(), + hostFxrOptions + )); + + hostFxrOptions->GetArguments(context->m_argc, context->m_argv); + THROW_IF_FAILED(SetEnvironmentVariablesOnWorkerProcess()); + } + context->m_pProc = pProc; + + if (m_pLoggerProvider == nullptr) + { + THROW_IF_FAILED(LoggingHelpers::CreateLoggingProvider( + m_pConfig->QueryStdoutLogEnabled(), + !m_pHttpServer.IsCommandLineLaunch(), + m_pConfig->QueryStdoutLogFile().c_str(), + QueryApplicationPhysicalPath().c_str(), + m_pLoggerProvider)); + + m_pLoggerProvider->TryStartRedirection(); + } + + // There can only ever be a single instance of .NET Core + // loaded in the process but we need to get config information to boot it up in the + // first place. This is happening in an execute request handler and everyone waits + // until this initialization is done. + // We set a static so that managed code can call back into this instance and + // set the callbacks + s_Application = this; + + //Start CLR thread + m_clrThread = std::thread(ClrThreadEntryPoint, context); + + // Wait for thread exit or shutdown event + const HANDLE waitHandles[2] = { m_pShutdownEvent, m_clrThread.native_handle() }; + + // Wait for shutdown request + const auto waitResult = WaitForMultipleObjects(2, waitHandles, FALSE, INFINITE); + THROW_LAST_ERROR_IF(waitResult == WAIT_FAILED); + + LOG_INFOF(L"Starting shutdown sequence %d", waitResult); + + bool clrThreadExited = waitResult == (WAIT_OBJECT_0 + 1); + // shutdown was signaled + // only wait for shutdown in case of successful startup + if (m_waitForShutdown) + { + const auto clrWaitResult = WaitForSingleObject(m_clrThread.native_handle(), m_pConfig->QueryShutdownTimeLimitInMS()); + THROW_LAST_ERROR_IF(waitResult == WAIT_FAILED); + + clrThreadExited = clrWaitResult != WAIT_TIMEOUT; + } + + LOG_INFOF(L"Clr thread wait ended: clrThreadExited: %d", clrThreadExited); + + // At this point CLR thread either finished or timed out, abandon it. + m_clrThread.detach(); + + m_pLoggerProvider->TryStopRedirection(); + + if (m_fStopCalled) + { + if (clrThreadExited) + { + EventLog::Info( + ASPNETCORE_EVENT_APP_SHUTDOWN_SUCCESSFUL, + ASPNETCORE_EVENT_APP_SHUTDOWN_SUCCESSFUL_MSG, + QueryConfigPath().c_str()); + } + else + { + EventLog::Warn( + ASPNETCORE_EVENT_GRACEFUL_SHUTDOWN_FAILURE, + ASPNETCORE_EVENT_APP_SHUTDOWN_FAILURE_MSG, + QueryConfigPath().c_str()); + } + } + else + { + if (clrThreadExited) + { + UnexpectedThreadExit(*context); + // If the inprocess server was initialized, we need to cause recycle to be called on the worker process. + // in case when it was not initialized we need to keep server running to serve 502 page + if (m_Initialized) + { + QueueStop(); + } + } + } + } + catch (InvalidOperationException& ex) + { + EventLog::Error( + ASPNETCORE_EVENT_LOAD_CLR_FALIURE, + ASPNETCORE_EVENT_LOAD_CLR_FALIURE_MSG, + QueryApplicationId().c_str(), + QueryApplicationPhysicalPath().c_str(), + ex.as_wstring().c_str()); + + OBSERVE_CAUGHT_EXCEPTION(); + } + catch (std::runtime_error& ex) + { + EventLog::Error( + ASPNETCORE_EVENT_LOAD_CLR_FALIURE, + ASPNETCORE_EVENT_LOAD_CLR_FALIURE_MSG, + QueryApplicationId().c_str(), + QueryApplicationPhysicalPath().c_str(), + GetUnexpectedExceptionMessage(ex).c_str()); + + OBSERVE_CAUGHT_EXCEPTION(); + } +} + +void IN_PROCESS_APPLICATION::QueueStop() +{ + if (m_fStopCalled) + { + return; + } + + LOG_INFO(L"Queueing in-process stop thread"); + + std::thread stoppingThread([](std::unique_ptr application) + { + LOG_INFO(L"Starting in-process stop thread"); + application->Stop(false); + LOG_INFO(L"Stopping in-process stop thread"); + }, ::ReferenceApplication(this)); + + stoppingThread.detach(); +} + +HRESULT IN_PROCESS_APPLICATION::Start( + IHttpServer& pServer, + IHttpApplication& pHttpApplication, + APPLICATION_PARAMETER* pParameters, + DWORD nParameters, + std::unique_ptr& application) +{ + try + { + std::unique_ptr options; + THROW_IF_FAILED(InProcessOptions::Create(pServer, pHttpApplication, options)); + application = std::unique_ptr( + new IN_PROCESS_APPLICATION(pServer, pHttpApplication, std::move(options), pParameters, nParameters)); + THROW_IF_FAILED(application->LoadManagedApplication()); + return S_OK; + } + catch (InvalidOperationException& ex) + { + EventLog::Error( + ASPNETCORE_EVENT_LOAD_CLR_FALIURE, + ASPNETCORE_EVENT_LOAD_CLR_FALIURE_MSG, + pHttpApplication.GetApplicationId(), + pHttpApplication.GetApplicationPhysicalPath(), + ex.as_wstring().c_str()); + + RETURN_CAUGHT_EXCEPTION(); + } + catch (std::runtime_error& ex) + { + EventLog::Error( + ASPNETCORE_EVENT_LOAD_CLR_FALIURE, + ASPNETCORE_EVENT_LOAD_CLR_FALIURE_MSG, + pHttpApplication.GetApplicationId(), + pHttpApplication.GetApplicationPhysicalPath(), + GetUnexpectedExceptionMessage(ex).c_str()); + + RETURN_CAUGHT_EXCEPTION(); + } + CATCH_RETURN(); +} + +// Required because __try and objects with destructors can not be mixed +void +IN_PROCESS_APPLICATION::ExecuteClr(const std::shared_ptr& context) +{ + __try + { + auto const exitCode = context->m_pProc(context->m_argc, context->m_argv.get()); + + LOG_INFOF(L"Managed application exited with code %d", exitCode); + + context->m_exitCode = exitCode; + } + __except(GetExceptionCode() != 0) + { + LOG_INFOF(L"Managed threw an exception %d", GetExceptionCode()); + + context->m_exceptionCode = GetExceptionCode(); + } +} + +// +// Calls hostfxr_main with the hostfxr and application as arguments. +// This method should not access IN_PROCESS_APPLICATION instance as it may be already freed +// in case of startup timeout +// +VOID +IN_PROCESS_APPLICATION::ClrThreadEntryPoint(const std::shared_ptr &context) +{ + // Keep aspnetcorev2_inprocess.dll loaded while this thread is running + // this is required because thread might be abandoned + HandleWrapper moduleHandle; + ModuleHelpers::IncrementCurrentModuleRefCount(moduleHandle); + + ExecuteClr(context); + + FreeLibraryAndExitThread(moduleHandle.release(), 0); +} + +HRESULT +IN_PROCESS_APPLICATION::SetEnvironmentVariablesOnWorkerProcess() +{ + auto variables = m_pConfig->QueryEnvironmentVariables(); + auto inputTable = std::unique_ptr(new ENVIRONMENT_VAR_HASH()); + RETURN_IF_FAILED(inputTable->Initialize(37 /*prime*/)); + // Copy environment variables to old style hash table + for (auto & variable : variables) + { + auto pNewEntry = std::unique_ptr(new ENVIRONMENT_VAR_ENTRY()); + RETURN_IF_FAILED(pNewEntry->Initialize((variable.first + L"=").c_str(), variable.second.c_str())); + RETURN_IF_FAILED(inputTable->InsertRecord(pNewEntry.get())); + } + + ENVIRONMENT_VAR_HASH* pHashTable = NULL; + std::unique_ptr table; + RETURN_IF_FAILED(ENVIRONMENT_VAR_HELPERS::InitEnvironmentVariablesTable( + inputTable.get(), + m_pConfig->QueryWindowsAuthEnabled(), + m_pConfig->QueryBasicAuthEnabled(), + m_pConfig->QueryAnonymousAuthEnabled(), + &pHashTable)); + + table.reset(pHashTable); + + HRESULT hr = S_OK; + table->Apply(ENVIRONMENT_VAR_HELPERS::AppendEnvironmentVariables, &hr); + RETURN_IF_FAILED(hr); + + table->Apply(ENVIRONMENT_VAR_HELPERS::SetEnvironmentVariables, &hr); + RETURN_IF_FAILED(hr); + + return S_OK; +} + +VOID +IN_PROCESS_APPLICATION::UnexpectedThreadExit(const ExecuteClrContext& context) const +{ + auto content = m_pLoggerProvider->GetStdOutContent(); + + if (context.m_exceptionCode != 0) + { + if (!content.empty()) + { + EventLog::Error( + ASPNETCORE_EVENT_INPROCESS_THREAD_EXCEPTION, + ASPNETCORE_EVENT_INPROCESS_THREAD_EXCEPTION_STDOUT_MSG, + QueryApplicationId().c_str(), + QueryApplicationPhysicalPath().c_str(), + context.m_exceptionCode, + content.c_str()); + } + else + { + EventLog::Error( + ASPNETCORE_EVENT_INPROCESS_THREAD_EXCEPTION, + ASPNETCORE_EVENT_INPROCESS_THREAD_EXCEPTION_MSG, + QueryApplicationId().c_str(), + QueryApplicationPhysicalPath().c_str(), + context.m_exceptionCode + ); + } + return; + } + + // + // Ungraceful shutdown, try to log an error message. + // This will be a common place for errors as it means the hostfxr_main returned + // or there was an exception. + // + + if (!content.empty()) + { + EventLog::Error( + ASPNETCORE_EVENT_INPROCESS_THREAD_EXIT_STDOUT, + ASPNETCORE_EVENT_INPROCESS_THREAD_EXIT_STDOUT_MSG, + QueryApplicationId().c_str(), + QueryApplicationPhysicalPath().c_str(), + context.m_exitCode, + content.c_str()); + } + else + { + EventLog::Error( + ASPNETCORE_EVENT_INPROCESS_THREAD_EXIT, + ASPNETCORE_EVENT_INPROCESS_THREAD_EXIT_MSG, + QueryApplicationId().c_str(), + QueryApplicationPhysicalPath().c_str(), + context.m_exitCode); + } +} + +HRESULT +IN_PROCESS_APPLICATION::CreateHandler( + _In_ IHttpContext *pHttpContext, + _Out_ IREQUEST_HANDLER **pRequestHandler) +{ + try + { + *pRequestHandler = new IN_PROCESS_HANDLER(::ReferenceApplication(this), pHttpContext, m_RequestHandler, m_RequestHandlerContext, m_DisconnectHandler, m_AsyncCompletionHandler); + } + CATCH_RETURN(); + + return S_OK; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h new file mode 100644 index 0000000000..caf16193d6 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h @@ -0,0 +1,185 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include +#include "InProcessApplicationBase.h" +#include "InProcessOptions.h" +#include "BaseOutputManager.h" + +class IN_PROCESS_HANDLER; +typedef REQUEST_NOTIFICATION_STATUS(WINAPI * PFN_REQUEST_HANDLER) (IN_PROCESS_HANDLER* pInProcessHandler, void* pvRequestHandlerContext); +typedef VOID(WINAPI * PFN_DISCONNECT_HANDLER) (void *pvManagedHttpContext); +typedef BOOL(WINAPI * PFN_SHUTDOWN_HANDLER) (void* pvShutdownHandlerContext); +typedef REQUEST_NOTIFICATION_STATUS(WINAPI * PFN_ASYNC_COMPLETION_HANDLER)(void *pvManagedHttpContext, HRESULT hrCompletionStatus, DWORD cbCompletion); + +class IN_PROCESS_APPLICATION : public InProcessApplicationBase +{ +public: + IN_PROCESS_APPLICATION( + IHttpServer& pHttpServer, + IHttpApplication& pApplication, + std::unique_ptr pConfig, + APPLICATION_PARAMETER *pParameters, + DWORD nParameters); + + ~IN_PROCESS_APPLICATION(); + + __override + VOID + StopInternal(bool fServerInitiated) override; + + VOID + SetCallbackHandles( + _In_ PFN_REQUEST_HANDLER request_callback, + _In_ PFN_SHUTDOWN_HANDLER shutdown_callback, + _In_ PFN_DISCONNECT_HANDLER disconnect_callback, + _In_ PFN_ASYNC_COMPLETION_HANDLER managed_context_callback, + _In_ VOID* pvRequstHandlerContext, + _In_ VOID* pvShutdownHandlerContext + ); + + __override + HRESULT + CreateHandler( + _In_ IHttpContext *pHttpContext, + _Out_ IREQUEST_HANDLER **pRequestHandler) + override; + + // Executes the .NET Core process + void + ExecuteApplication(); + + HRESULT + LoadManagedApplication(); + + + void + QueueStop(); + + void + StopIncomingRequests() + { + QueueStop(); + } + + void + StopCallsIntoManaged() + { + m_blockManagedCallbacks = true; + } + + static + VOID SetMainCallback(hostfxr_main_fn mainCallback) + { + s_fMainCallback = mainCallback; + } + + static + IN_PROCESS_APPLICATION* + GetInstance() + { + return s_Application; + } + + const std::wstring& + QueryExeLocation() const + { + return m_dotnetExeKnownLocation; + } + + const InProcessOptions& + QueryConfig() const + { + return *m_pConfig; + } + + bool + QueryBlockCallbacksIntoManaged() const + { + return m_blockManagedCallbacks; + } + + static + HRESULT Start( + IHttpServer& pServer, + IHttpApplication& pHttpApplication, + APPLICATION_PARAMETER* pParameters, + DWORD nParameters, + std::unique_ptr& application); + +private: + struct ExecuteClrContext: std::enable_shared_from_this + { + ExecuteClrContext(): + m_argc(0), + m_pProc(nullptr), + m_exitCode(0), + m_exceptionCode(0) + { + } + + DWORD m_argc; + std::unique_ptr m_argv; + hostfxr_main_fn m_pProc; + + int m_exitCode; + int m_exceptionCode; + }; + + // Thread executing the .NET Core process this might be abandoned in timeout cases + std::thread m_clrThread; + // Thread tracking the CLR thread, this one is always joined on shutdown + std::thread m_workerThread; + // The event that gets triggered when managed initialization is complete + HandleWrapper m_pInitializeEvent; + // The event that gets triggered when worker thread should exit + HandleWrapper m_pShutdownEvent; + + // The request handler callback from managed code + PFN_REQUEST_HANDLER m_RequestHandler; + VOID* m_RequestHandlerContext; + + // The shutdown handler callback from managed code + PFN_SHUTDOWN_HANDLER m_ShutdownHandler; + VOID* m_ShutdownHandlerContext; + + PFN_ASYNC_COMPLETION_HANDLER m_AsyncCompletionHandler; + PFN_DISCONNECT_HANDLER m_DisconnectHandler; + + std::wstring m_dotnetExeKnownLocation; + + std::atomic_bool m_blockManagedCallbacks; + bool m_Initialized; + bool m_waitForShutdown; + + std::unique_ptr m_pConfig; + + static IN_PROCESS_APPLICATION* s_Application; + + std::unique_ptr m_pLoggerProvider; + + inline static const LPCSTR s_exeLocationParameterName = "InProcessExeLocation"; + + VOID + UnexpectedThreadExit(const ExecuteClrContext& context) const; + + HRESULT + SetEnvironmentVariablesOnWorkerProcess(); + + void + StopClr(); + + static + void + ClrThreadEntryPoint(const std::shared_ptr &context); + + static + void + ExecuteClr(const std::shared_ptr &context); + + // Allows to override call to hostfxr_main with custom callback + // used in testing + inline static hostfxr_main_fn s_fMainCallback = nullptr; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.cpp new file mode 100644 index 0000000000..8fee377a30 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.cpp @@ -0,0 +1,220 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "inprocesshandler.h" +#include "inprocessapplication.h" +#include "IOutputManager.h" +#include "ShuttingDownApplication.h" +#include "ntassert.h" + +ALLOC_CACHE_HANDLER * IN_PROCESS_HANDLER::sm_pAlloc = NULL; + +IN_PROCESS_HANDLER::IN_PROCESS_HANDLER( + _In_ std::unique_ptr pApplication, + _In_ IHttpContext *pW3Context, + _In_ PFN_REQUEST_HANDLER pRequestHandler, + _In_ void * pRequestHandlerContext, + _In_ PFN_DISCONNECT_HANDLER pDisconnectHandler, + _In_ PFN_ASYNC_COMPLETION_HANDLER pAsyncCompletion +): REQUEST_HANDLER(*pW3Context), + m_pManagedHttpContext(nullptr), + m_requestNotificationStatus(RQ_NOTIFICATION_PENDING), + m_fManagedRequestComplete(FALSE), + m_pW3Context(pW3Context), + m_pApplication(std::move(pApplication)), + m_pRequestHandler(pRequestHandler), + m_pRequestHandlerContext(pRequestHandlerContext), + m_pAsyncCompletionHandler(pAsyncCompletion), + m_pDisconnectHandler(pDisconnectHandler), + m_disconnectFired(false) +{ + InitializeSRWLock(&m_srwDisconnectLock); +} + +__override +REQUEST_NOTIFICATION_STATUS +IN_PROCESS_HANDLER::ExecuteRequestHandler() +{ + ::RaiseEvent(m_pW3Context, nullptr); + + if (m_pRequestHandler == NULL) + { + ::RaiseEvent(m_pW3Context, nullptr, RQ_NOTIFICATION_FINISH_REQUEST); + return RQ_NOTIFICATION_FINISH_REQUEST; + } + else if (m_pApplication->QueryBlockCallbacksIntoManaged()) + { + return ServerShutdownMessage(); + } + + auto status = m_pRequestHandler(this, m_pRequestHandlerContext); + ::RaiseEvent(m_pW3Context, nullptr, status); + return status; +} + +__override +REQUEST_NOTIFICATION_STATUS +IN_PROCESS_HANDLER::AsyncCompletion( + DWORD cbCompletion, + HRESULT hrCompletionStatus +) +{ + + ::RaiseEvent(m_pW3Context, nullptr); + + if (m_fManagedRequestComplete) + { + // means PostCompletion has been called and this is the associated callback. + ::RaiseEvent(m_pW3Context, nullptr, m_requestNotificationStatus); + return m_requestNotificationStatus; + } + if (m_pApplication->QueryBlockCallbacksIntoManaged()) + { + // this can potentially happen in ungraceful shutdown. + // Or something really wrong happening with async completions + // At this point, managed is in a shutting down state and we cannot send a request to it. + return ServerShutdownMessage(); + } + + assert(m_pManagedHttpContext != nullptr); + // Call the managed handler for async completion. + + auto status = m_pAsyncCompletionHandler(m_pManagedHttpContext, hrCompletionStatus, cbCompletion); + ::RaiseEvent(m_pW3Context, nullptr, status); + return status; +} + +REQUEST_NOTIFICATION_STATUS IN_PROCESS_HANDLER::ServerShutdownMessage() const +{ + ::RaiseEvent(m_pW3Context, nullptr); + return ShuttingDownHandler::ServerShutdownMessage(m_pW3Context); +} + +VOID +IN_PROCESS_HANDLER::NotifyDisconnect() +{ + ::RaiseEvent(m_pW3Context, nullptr); + + if (m_pApplication->QueryBlockCallbacksIntoManaged() || + m_fManagedRequestComplete) + { + return; + } + + // NotifyDisconnect can be called before the m_pManagedHttpContext is set, + // so save that in a bool. + // Don't lock when calling m_pDisconnect to avoid the potential deadlock between this + // and SetManagedHttpContext + void* pManagedHttpContext = nullptr; + { + SRWExclusiveLock lock(m_srwDisconnectLock); + pManagedHttpContext = m_pManagedHttpContext; + m_disconnectFired = true; + } + + if (pManagedHttpContext != nullptr) + { + m_pDisconnectHandler(m_pManagedHttpContext); + } +} + +VOID +IN_PROCESS_HANDLER::IndicateManagedRequestComplete( + VOID +) +{ + m_fManagedRequestComplete = TRUE; + m_pManagedHttpContext = nullptr; + ::RaiseEvent(m_pW3Context, nullptr); +} + +VOID +IN_PROCESS_HANDLER::SetAsyncCompletionStatus( + REQUEST_NOTIFICATION_STATUS requestNotificationStatus +) +{ + m_requestNotificationStatus = requestNotificationStatus; +} + +VOID +IN_PROCESS_HANDLER::SetManagedHttpContext( + PVOID pManagedHttpContext +) +{ + { + SRWExclusiveLock lock(m_srwDisconnectLock); + m_pManagedHttpContext = pManagedHttpContext; + } + + if (m_disconnectFired && m_pManagedHttpContext != nullptr) + { + m_pDisconnectHandler(m_pManagedHttpContext); + } +} + +// static +void * IN_PROCESS_HANDLER::operator new(size_t) +{ + DBG_ASSERT(sm_pAlloc != NULL); + if (sm_pAlloc == NULL) + { + return NULL; + } + return sm_pAlloc->Alloc(); +} + +// static +void IN_PROCESS_HANDLER::operator delete(void * pMemory) +{ + DBG_ASSERT(sm_pAlloc != NULL); + if (sm_pAlloc != NULL) + { + sm_pAlloc->Free(pMemory); + } +} + +// static +HRESULT +IN_PROCESS_HANDLER::StaticInitialize(VOID) +/*++ + +Routine Description: + +Global initialization routine for IN_PROCESS_HANDLER + +Return Value: + +HRESULT + +--*/ +{ + HRESULT hr = S_OK; + + sm_pAlloc = new ALLOC_CACHE_HANDLER; + if (sm_pAlloc == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + hr = sm_pAlloc->Initialize(sizeof(IN_PROCESS_HANDLER), + 64); // nThreshold + +Finished: + if (FAILED(hr)) + { + StaticTerminate(); + } + return hr; +} + +// static +void +IN_PROCESS_HANDLER::StaticTerminate(VOID) +{ + if (sm_pAlloc != NULL) + { + delete sm_pAlloc; + sm_pAlloc = NULL; + } +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.h b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.h new file mode 100644 index 0000000000..20a5192c18 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.h @@ -0,0 +1,88 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "requesthandler.h" +#include +#include "iapplication.h" +#include "inprocessapplication.h" + +class IN_PROCESS_APPLICATION; + +class IN_PROCESS_HANDLER : public REQUEST_HANDLER +{ +public: + IN_PROCESS_HANDLER( + _In_ std::unique_ptr pApplication, + _In_ IHttpContext *pW3Context, + _In_ PFN_REQUEST_HANDLER pRequestHandler, + _In_ void * pRequestHandlerContext, + _In_ PFN_DISCONNECT_HANDLER m_DisconnectHandler, + _In_ PFN_ASYNC_COMPLETION_HANDLER pAsyncCompletion); + + ~IN_PROCESS_HANDLER() override = default; + + __override + REQUEST_NOTIFICATION_STATUS + ExecuteRequestHandler() override; + + __override + REQUEST_NOTIFICATION_STATUS + AsyncCompletion( + DWORD cbCompletion, + HRESULT hrCompletionStatus + ) override; + + __override + VOID + NotifyDisconnect() override; + + IHttpContext* + QueryHttpContext() const + { + return m_pW3Context; + } + + VOID + SetManagedHttpContext( + PVOID pManagedHttpContext + ); + + VOID + IndicateManagedRequestComplete(); + + VOID + SetAsyncCompletionStatus( + REQUEST_NOTIFICATION_STATUS requestNotificationStatus + ); + + static void * operator new(size_t size); + + static void operator delete(void * pMemory); + + static + HRESULT + StaticInitialize(); + + static + void + StaticTerminate(); + +private: + REQUEST_NOTIFICATION_STATUS + ServerShutdownMessage() const; + + PVOID m_pManagedHttpContext; + BOOL m_fManagedRequestComplete; + REQUEST_NOTIFICATION_STATUS m_requestNotificationStatus; + IHttpContext* m_pW3Context; + std::unique_ptr m_pApplication; + PFN_REQUEST_HANDLER m_pRequestHandler; + void* m_pRequestHandlerContext; + PFN_ASYNC_COMPLETION_HANDLER m_pAsyncCompletionHandler; + PFN_DISCONNECT_HANDLER m_pDisconnectHandler; + static ALLOC_CACHE_HANDLER * sm_pAlloc; + bool m_disconnectFired; + SRWLOCK m_srwDisconnectLock; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessrequesthandler.rc b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessrequesthandler.rc new file mode 100644 index 0000000000..2cddc56709 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessrequesthandler.rc @@ -0,0 +1,119 @@ +// Microsoft Visual C++ generated resource script. +// +#include +#include "version.h" +#include "..\CommonLib\resources.h" +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#define FileDescription "IIS ASP.NET Core Module V2 Request Handler. Commit: " CommitHash + +///////////////////////////////////////////////////////////////////////////// +// +// 11 +// + +//1 11 +//BEGIN +// 0x0001, 0x0000, 0x03e8, 0x0000, 0x03ed, 0x0000, 0x0010, 0x0000, 0x0010, +// 0x0001, 0x0025, 0x0031, 0x000d, 0x000a, 0x0000, 0x0000, 0x0010, 0x0001, +// 0x0025, 0x0031, 0x000d, 0x000a, 0x0000, 0x0000, 0x0010, 0x0001, 0x0025, +// 0x0031, 0x000d, 0x000a, 0x0000, 0x0000, 0x0010, 0x0001, 0x0025, 0x0031, +// 0x000d, 0x000a, 0x0000, 0x0000, 0x0010, 0x0001, 0x0025, 0x0031, 0x000d, +// 0x000a, 0x0000, 0x0000, 0x0010, 0x0001, 0x0025, 0x0031, 0x000d, 0x000a, +// 0x0000, 0x0000 +//END + + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "..\CommonLib\resources.h\0" +END + +2 TEXTINCLUDE +BEGIN + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION FileVersion + PRODUCTVERSION ProductVersion + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x40004L + FILETYPE 0x2L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", "Microsoft" + VALUE "FileDescription", FileDescription + VALUE "FileVersion", FileVersionStr + VALUE "InternalName", "aspnetcorev2_inprocess.dll" + VALUE "LegalCopyright", "Copyright (C) Microsoft Corporation" + VALUE "OriginalFilename", "aspnetcorev2_inprocess.dll" + VALUE "ProductName", "ASP.NET Core Module Request Handler" + VALUE "ProductVersion", ProductVersionStr + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END + + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + +STRINGTABLE +BEGIN + IDS_INVALID_PROPERTY "Property name '%s' in system.webServer/aspNetCore section has invalid value '%s' which does not conform to the prescribed format" + IDS_SERVER_ERROR "There was a connection error while trying to route the request." +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp new file mode 100644 index 0000000000..2185f975a4 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp @@ -0,0 +1,514 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#include "inprocessapplication.h" +#include "inprocesshandler.h" +#include "requesthandler_config.h" + +extern bool g_fInProcessApplicationCreated; + +// +// Initialization export +// +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +HRESULT +register_callbacks( + _In_ IN_PROCESS_APPLICATION* pInProcessApplication, + _In_ PFN_REQUEST_HANDLER request_handler, + _In_ PFN_SHUTDOWN_HANDLER shutdown_handler, + _In_ PFN_DISCONNECT_HANDLER disconnect_handler, + _In_ PFN_ASYNC_COMPLETION_HANDLER async_completion_handler, + _In_ VOID* pvRequstHandlerContext, + _In_ VOID* pvShutdownHandlerContext +) +{ + if (pInProcessApplication == NULL) + { + return E_INVALIDARG; + } + + pInProcessApplication->SetCallbackHandles( + request_handler, + shutdown_handler, + disconnect_handler, + async_completion_handler, + pvRequstHandlerContext, + pvShutdownHandlerContext + ); + + return S_OK; +} + +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +HTTP_REQUEST* +http_get_raw_request( + _In_ IN_PROCESS_HANDLER* pInProcessHandler +) +{ + return pInProcessHandler->QueryHttpContext()->GetRequest()->GetRawHttpRequest(); +} + +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +HTTP_RESPONSE* +http_get_raw_response( + _In_ IN_PROCESS_HANDLER* pInProcessHandler +) +{ + return pInProcessHandler->QueryHttpContext()->GetResponse()->GetRawHttpResponse(); +} + +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +HRESULT +http_get_server_variable( + _In_ IN_PROCESS_HANDLER* pInProcessHandler, + _In_ PCSTR pszVariableName, + _Out_ BSTR* pwszReturn +) +{ + PCWSTR pszVariableValue; + DWORD cbLength; + + HRESULT hr = pInProcessHandler + ->QueryHttpContext() + ->GetServerVariable(pszVariableName, &pszVariableValue, &cbLength); + + if (FAILED(hr) || cbLength == 0) + { + goto Finished; + } + + *pwszReturn = SysAllocString(pszVariableValue); + + if (*pwszReturn == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } +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( + _In_ IN_PROCESS_HANDLER* pInProcessHandler, + _In_ USHORT statusCode, + _In_ PCSTR pszReason +) +{ + return pInProcessHandler->QueryHttpContext()->GetResponse()->SetStatus(statusCode, pszReason, 0, 0, nullptr, + true); // fTrySkipCustomErrors +} + +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +HRESULT +http_post_completion( + _In_ IN_PROCESS_HANDLER* pInProcessHandler, + DWORD cbBytes +) +{ + return pInProcessHandler->QueryHttpContext()->PostCompletion(cbBytes); +} + +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +HRESULT +http_set_completion_status( + _In_ IN_PROCESS_HANDLER* pInProcessHandler, + _In_ REQUEST_NOTIFICATION_STATUS requestNotificationStatus +) +{ + HRESULT hr = S_OK; + + pInProcessHandler->IndicateManagedRequestComplete(); + pInProcessHandler->SetAsyncCompletionStatus(requestNotificationStatus); + return hr; +} + +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +HRESULT +http_set_managed_context( + _In_ IN_PROCESS_HANDLER* pInProcessHandler, + _In_ PVOID pvManagedContext +) +{ + // todo: should we consider changing the signature + HRESULT hr = S_OK; + pInProcessHandler->SetManagedHttpContext(pvManagedContext); + + return hr; +} + +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +VOID +http_indicate_completion( + _In_ IN_PROCESS_HANDLER* pInProcessHandler, + _In_ REQUEST_NOTIFICATION_STATUS notificationStatus +) +{ + pInProcessHandler->QueryHttpContext()->IndicateCompletion(notificationStatus); +} + +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +VOID +http_get_completion_info( + _In_ IHttpCompletionInfo2* info, + _Out_ DWORD* cbBytes, + _Out_ HRESULT* hr +) +{ + *cbBytes = info->GetCompletionBytes(); + *hr = info->GetCompletionStatus(); +} + +// +// the signature should be changed. application's based address should be passed in +// + +struct IISConfigurationData +{ + IN_PROCESS_APPLICATION* pInProcessApplication; + BSTR pwzFullApplicationPath; + BSTR pwzVirtualApplicationPath; + BOOL fWindowsAuthEnabled; + BOOL fBasicAuthEnabled; + BOOL fAnonymousAuthEnable; +}; + +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +HRESULT +http_get_application_properties( + _In_ IISConfigurationData* pIISCofigurationData +) +{ + auto pInProcessApplication = IN_PROCESS_APPLICATION::GetInstance(); + if (pInProcessApplication == NULL) + { + return E_FAIL; + } + + const auto& pConfiguration = pInProcessApplication->QueryConfig(); + + pIISCofigurationData->pInProcessApplication = pInProcessApplication; + pIISCofigurationData->pwzFullApplicationPath = SysAllocString(pInProcessApplication->QueryApplicationPhysicalPath().c_str()); + pIISCofigurationData->pwzVirtualApplicationPath = SysAllocString(pInProcessApplication->QueryApplicationVirtualPath().c_str()); + pIISCofigurationData->fWindowsAuthEnabled = pConfiguration.QueryWindowsAuthEnabled(); + pIISCofigurationData->fBasicAuthEnabled = pConfiguration.QueryBasicAuthEnabled(); + pIISCofigurationData->fAnonymousAuthEnable = pConfiguration.QueryAnonymousAuthEnabled(); + + return S_OK; +} + +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +HRESULT +http_read_request_bytes( + _In_ IN_PROCESS_HANDLER* pInProcessHandler, + _Out_ CHAR* pvBuffer, + _In_ DWORD dwCbBuffer, + _Out_ DWORD* pdwBytesReceived, + _Out_ BOOL* pfCompletionPending +) +{ + HRESULT hr = S_OK; + + if (pInProcessHandler == NULL) + { + return E_FAIL; + } + if (dwCbBuffer == 0) + { + return E_FAIL; + } + IHttpRequest *pHttpRequest = (IHttpRequest*)pInProcessHandler->QueryHttpContext()->GetRequest(); + + // Check if there is anything to read + if (pHttpRequest->GetRemainingEntityBytes() > 0) + { + BOOL fAsync = TRUE; + hr = pHttpRequest->ReadEntityBody( + pvBuffer, + dwCbBuffer, + fAsync, + pdwBytesReceived, + pfCompletionPending); + + if (hr == HRESULT_FROM_WIN32(ERROR_HANDLE_EOF)) + { + // We reached the end of the data + hr = S_OK; + } + } + else + { + *pdwBytesReceived = 0; + *pfCompletionPending = FALSE; + } + + return hr; +} + +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +HRESULT +http_write_response_bytes( + _In_ IN_PROCESS_HANDLER* pInProcessHandler, + _In_ HTTP_DATA_CHUNK* pDataChunks, + _In_ DWORD dwChunks, + _In_ BOOL* pfCompletionExpected +) +{ + IHttpResponse *pHttpResponse = (IHttpResponse*)pInProcessHandler->QueryHttpContext()->GetResponse(); + BOOL fAsync = TRUE; + BOOL fMoreData = TRUE; + DWORD dwBytesSent = 0; + + HRESULT hr = pHttpResponse->WriteEntityChunks( + pDataChunks, + dwChunks, + fAsync, + fMoreData, + &dwBytesSent, + pfCompletionExpected); + + return hr; +} + +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +HRESULT +http_flush_response_bytes( + _In_ IN_PROCESS_HANDLER* pInProcessHandler, + _Out_ BOOL* pfCompletionExpected +) +{ + IHttpResponse *pHttpResponse = (IHttpResponse*)pInProcessHandler->QueryHttpContext()->GetResponse(); + + BOOL fAsync = TRUE; + BOOL fMoreData = TRUE; + DWORD dwBytesSent = 0; + + HRESULT hr = pHttpResponse->Flush( + fAsync, + fMoreData, + &dwBytesSent, + pfCompletionExpected); + return hr; +} + +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +HRESULT +http_websockets_read_bytes( + _In_ IN_PROCESS_HANDLER* pInProcessHandler, + _In_ CHAR* pvBuffer, + _In_ DWORD cbBuffer, + _In_ PFN_ASYNC_COMPLETION pfnCompletionCallback, + _In_ VOID* pvCompletionContext, + _In_ DWORD* pDwBytesReceived, + _In_ BOOL* pfCompletionPending +) +{ + IHttpRequest3 *pHttpRequest = (IHttpRequest3*)pInProcessHandler->QueryHttpContext()->GetRequest(); + + BOOL fAsync = TRUE; + + HRESULT hr = pHttpRequest->ReadEntityBody( + pvBuffer, + cbBuffer, + fAsync, + pfnCompletionCallback, + pvCompletionContext, + pDwBytesReceived, + pfCompletionPending); + + if (hr == HRESULT_FROM_WIN32(ERROR_HANDLE_EOF)) + { + // We reached the end of the data + hr = S_OK; + } + + return hr; +} + +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +HRESULT +http_websockets_write_bytes( + _In_ IN_PROCESS_HANDLER* pInProcessHandler, + _In_ HTTP_DATA_CHUNK* pDataChunks, + _In_ DWORD dwChunks, + _In_ PFN_ASYNC_COMPLETION pfnCompletionCallback, + _In_ VOID* pvCompletionContext, + _In_ BOOL* pfCompletionExpected +) +{ + IHttpResponse2 *pHttpResponse = (IHttpResponse2*)pInProcessHandler->QueryHttpContext()->GetResponse(); + + BOOL fAsync = TRUE; + BOOL fMoreData = TRUE; + DWORD dwBytesSent; + + HRESULT hr = pHttpResponse->WriteEntityChunks( + pDataChunks, + dwChunks, + fAsync, + fMoreData, + pfnCompletionCallback, + pvCompletionContext, + &dwBytesSent, + pfCompletionExpected); + + return hr; +} + +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +HRESULT +http_websockets_flush_bytes( + _In_ IN_PROCESS_HANDLER* pInProcessHandler, + _In_ PFN_ASYNC_COMPLETION pfnCompletionCallback, + _In_ VOID* pvCompletionContext, + _In_ BOOL* pfCompletionExpected +) +{ + IHttpResponse2 *pHttpResponse = (IHttpResponse2*)pInProcessHandler->QueryHttpContext()->GetResponse(); + + BOOL fAsync = TRUE; + BOOL fMoreData = TRUE; + DWORD dwBytesSent; + + HRESULT hr = pHttpResponse->Flush( + fAsync, + fMoreData, + pfnCompletionCallback, + pvCompletionContext, + &dwBytesSent, + pfCompletionExpected); + return hr; +} + +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +HRESULT +http_enable_websockets( + _In_ IN_PROCESS_HANDLER* pInProcessHandler +) +{ + ((IHttpContext3*)pInProcessHandler->QueryHttpContext())->EnableFullDuplex(); + ((IHttpResponse2*)pInProcessHandler->QueryHttpContext()->GetResponse())->DisableBuffering(); + + return S_OK; +} + +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +HRESULT +http_cancel_io( + _In_ IN_PROCESS_HANDLER* pInProcessHandler +) +{ + 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_close_connection( + _In_ IN_PROCESS_HANDLER* pInProcessHandler +) +{ + pInProcessHandler->QueryHttpContext()->GetResponse()->ResetConnection(); + return S_OK; +} + +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +HRESULT +http_response_set_unknown_header( + _In_ IN_PROCESS_HANDLER* pInProcessHandler, + _In_ PCSTR pszHeaderName, + _In_ PCSTR pszHeaderValue, + _In_ USHORT usHeaderValueLength, + _In_ BOOL fReplace +) +{ + return pInProcessHandler->QueryHttpContext()->GetResponse()->SetHeader(pszHeaderName, pszHeaderValue, usHeaderValueLength, fReplace); +} + +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +HRESULT +http_response_set_known_header( + _In_ IN_PROCESS_HANDLER* pInProcessHandler, + _In_ HTTP_HEADER_ID dwHeaderId, + _In_ PCSTR pszHeaderValue, + _In_ USHORT usHeaderValueLength, + _In_ BOOL fReplace +) +{ + return pInProcessHandler->QueryHttpContext()->GetResponse()->SetHeader(dwHeaderId, pszHeaderValue, usHeaderValueLength, fReplace); +} + +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +HRESULT +http_get_authentication_information( + _In_ IN_PROCESS_HANDLER* pInProcessHandler, + _Out_ BSTR* pstrAuthType, + _Out_ VOID** pvToken +) +{ + *pstrAuthType = SysAllocString(pInProcessHandler->QueryHttpContext()->GetUser()->GetAuthenticationType()); + *pvToken = pInProcessHandler->QueryHttpContext()->GetUser()->GetPrimaryToken(); + + return S_OK; +} + +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +HRESULT +http_stop_calls_into_managed(_In_ IN_PROCESS_APPLICATION* pInProcessApplication) +{ + if (pInProcessApplication == NULL) + { + return E_INVALIDARG; + } + + pInProcessApplication->StopCallsIntoManaged(); + return S_OK; +} + +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +HRESULT +http_stop_incoming_requests(_In_ IN_PROCESS_APPLICATION* pInProcessApplication) +{ + if (pInProcessApplication == NULL) + { + return E_INVALIDARG; + } + + pInProcessApplication->StopIncomingRequests(); + return S_OK; +} + +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +VOID +set_main_handler(_In_ hostfxr_main_fn main) +{ + // Allow inprocess application to be recreated as we reuse the same CLR + g_fInProcessApplicationCreated = false; + IN_PROCESS_APPLICATION::SetMainCallback(main); +} + +// End of export diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/resource.h b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/resource.h new file mode 100644 index 0000000000..61f67c2833 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by HtmlResponses.rc +// +#define IN_PROCESS_RH_STATIC_HTML 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/stdafx.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/stdafx.cpp new file mode 100644 index 0000000000..12fcb1d436 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/stdafx.cpp @@ -0,0 +1,4 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +// Do not remove this file. It is used for precompiled header generation diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/stdafx.h b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/stdafx.h new file mode 100644 index 0000000000..77492b07d5 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/InProcessRequestHandler/stdafx.h @@ -0,0 +1,88 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once +#pragma warning( disable : 4091) + +// +// System related headers +// +#define _WINSOCKAPI_ + +#define NTDDI_VERSION 0x06010000 +#define WINVER 0x0601 +#define _WIN32_WINNT 0x0601 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +// This should remove our issue of compiling for win7 without header files. +// We force the Windows 8 version check logic in iiswebsocket.h to succeed even though we're compiling for Windows 7. +// Then, we set the version defines back to Windows 7 to for the remainder of the compilation. +#undef NTDDI_VERSION +#undef WINVER +#undef _WIN32_WINNT +#define NTDDI_VERSION 0x06020000 +#define WINVER 0x0602 +#define _WIN32_WINNT 0x0602 +#include +#undef NTDDI_VERSION +#undef WINVER +#undef _WIN32_WINNT + +#include +#include +#include + +#define NTDDI_VERSION 0x06010000 +#define WINVER 0x0601 +#define _WIN32_WINNT 0x0601 + +#ifdef max +#undef max +template inline T max(T a, T b) +{ + return a > b ? a : b; +} +#endif + +#ifdef min +#undef min +template inline T min(T a, T b) +{ + return a < b ? a : b; +} +#endif + + +inline bool IsSpace(char ch) +{ + switch (ch) + { + case 32: // ' ' + case 9: // '\t' + case 10: // '\n' + case 13: // '\r' + case 11: // '\v' + case 12: // '\f' + return true; + default: + return false; + } +} + +extern BOOL g_fAsyncDisconnectAvailable; +extern BOOL g_fEnableReferenceCountTracing; +extern BOOL g_fProcessDetach; +extern SRWLOCK g_srwLockRH; +extern HANDLE g_hEventLog; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/HtmlResponses.rc b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/HtmlResponses.rc new file mode 100644 index 0000000000..231f122fef --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/HtmlResponses.rc @@ -0,0 +1,68 @@ +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// HTML +// + +OUT_OF_PROCESS_RH_STATIC_HTML HTML "OutOfProcessRhStaticHtml.htm" + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/OutOfProcessRequestHandler.vcxproj b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/OutOfProcessRequestHandler.vcxproj new file mode 100644 index 0000000000..ca2bc0f4aa --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/OutOfProcessRequestHandler.vcxproj @@ -0,0 +1,285 @@ + + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {7F87406C-A3C8-4139-A68D-E4C344294A67} + Win32Proj + OutOfProcessRequestHandler + 10.0.15063.0 + OutOfProcessRequestHandler + + + + DynamicLibrary + true + v141 + Unicode + + + DynamicLibrary + false + v141 + true + Unicode + + + DynamicLibrary + true + v141 + Unicode + + + DynamicLibrary + false + v141 + true + Unicode + + + + + + + + + + + + + + + + + + + + + aspnetcorev2_outofprocess + + + false + + + false + + + + Use + Level4 + Disabled + WIN32;_DEBUG;REQUESTHANDLER_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + stdafx.h + $(IntDir)$(TargetName).pch + ProgramDatabase + MultiThreadedDebug + ..\IISLib;..\CommonLib;.\Inc;..\RequestHandlerLib + true + true + true + false + SyncCThrow + 8Bytes + true + false + true + CompileAsCpp + true + true + stdcpp17 + stdafx.h + + + Windows + true + kernel32.lib;user32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;ahadmin.lib;ws2_32.lib;iphlpapi.lib;version.lib;Rpcrt4.lib;winhttp.lib + Source.def + UseLinkTimeCodeGeneration + + + + + Use + Level4 + Disabled + WIN32;_DEBUG;REQUESTHANDLER_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + stdafx.h + $(IntDir)$(TargetName).pch + ProgramDatabase + MultiThreadedDebug + ..\IISLib;..\CommonLib;.\Inc;..\RequestHandlerLib + true + true + true + false + SyncCThrow + 8Bytes + true + false + true + CompileAsCpp + true + true + stdcpp17 + stdafx.h + + + Windows + true + kernel32.lib;user32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;ahadmin.lib;ws2_32.lib;iphlpapi.lib;version.lib;Rpcrt4.lib;winhttp.lib + Source.def + UseLinkTimeCodeGeneration + + + + + Level4 + Use + MaxSpeed + true + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;REQUESTHANDLER_EXPORTS;%(PreprocessorDefinitions) + stdafx.h + MultiThreaded + ..\IISLib;..\CommonLib;.\Inc;..\RequestHandlerLib + true + true + true + false + SyncCThrow + 8Bytes + true + false + true + CompileAsCpp + true + true + stdcpp17 + stdafx.h + + + Windows + false + true + /NODEFAULTLIB:libucrt.lib /DEFAULTLIB:ucrt.lib %(AdditionalOptions) + true + Source.def + kernel32.lib;user32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;ahadmin.lib;ws2_32.lib;iphlpapi.lib;version.lib;Rpcrt4.lib;winhttp.lib + UseLinkTimeCodeGeneration + + + + + Level4 + Use + MaxSpeed + true + true + NDEBUG;REQUESTHANDLER_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + stdafx.h + MultiThreaded + ..\IISLib;..\CommonLib;.\Inc;..\RequestHandlerLib + true + true + true + false + SyncCThrow + 8Bytes + true + false + true + CompileAsCpp + true + true + stdcpp17 + stdafx.h + + + Windows + false + true + /NODEFAULTLIB:libucrt.lib /DEFAULTLIB:ucrt.lib %(AdditionalOptions) + true + Source.def + kernel32.lib;user32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;ahadmin.lib;ws2_32.lib;iphlpapi.lib;version.lib;Rpcrt4.lib;winhttp.lib + UseLinkTimeCodeGeneration + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create + Create + Create + Create + + + + + + + + {55494e58-e061-4c4c-a0a8-837008e72f85} + + + {4787a64f-9a3e-4867-a55a-70cb4b2b2ffe} + + + {1533e271-f61b-441b-8b74-59fb61df0552} + + + + + + + + + true + + + + + + + + \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/OutOfProcessRhStaticHtml.htm b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/OutOfProcessRhStaticHtml.htm new file mode 100644 index 0000000000..e7d2f04e7c --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/OutOfProcessRhStaticHtml.htm @@ -0,0 +1,85 @@ + + + + + HTTP Error 502.5 - ANCM Out-Of-Process Startup Failure + + + +

HTTP Error 502.5 - ANCM Out-Of-Process Startup Failure

+ +

Common causes of this issue:

+ +
    +
  • The application process failed to start
  • +
  • The application process started but then stopped
  • +
  • The application process started but failed to listen on the configured port
  • +
+ +

Troubleshooting steps:

+ +
    +
  • Check the system event log for error messages
  • +
  • Enable logging the application process' stdout messages
  • +
  • Attach a debugger to the application process and inspect
  • +
+ +

+ For more information visit: + %s https://go.microsoft.com/fwlink/?LinkID=808681 +

+ + + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/Source.def b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/Source.def new file mode 100644 index 0000000000..37d763da5a --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/Source.def @@ -0,0 +1,5 @@ +LIBRARY aspnetcorev2_outofprocess + +EXPORTS + CreateApplication + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/dllmain.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/dllmain.cpp new file mode 100644 index 0000000000..981e3acd36 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/dllmain.cpp @@ -0,0 +1,270 @@ +// dllmain.cpp : Defines the entry point for the DLL application. + +#include +#include +#include "exceptions.h" + +DECLARE_DEBUG_PRINT_OBJECT("aspnetcorev2_outofprocess.dll"); + +BOOL g_fNsiApiNotSupported = FALSE; +BOOL g_fWebSocketStaticInitialize = FALSE; +BOOL g_fEnableReferenceCountTracing = FALSE; +BOOL g_fGlobalInitialize = FALSE; +BOOL g_fOutOfProcessInitialize = FALSE; +BOOL g_fOutOfProcessInitializeError = FALSE; +BOOL g_fWinHttpNonBlockingCallbackAvailable = FALSE; +BOOL g_fProcessDetach = FALSE; +DWORD g_OptionalWinHttpFlags = 0; +DWORD g_dwTlsIndex = TLS_OUT_OF_INDEXES; +SRWLOCK g_srwLockRH; +HINTERNET g_hWinhttpSession = NULL; +IHttpServer * g_pHttpServer = NULL; +HINSTANCE g_hWinHttpModule; +HINSTANCE g_hOutOfProcessRHModule; +HINSTANCE g_hAspNetCoreModule; +HANDLE g_hEventLog = NULL; + +VOID +InitializeGlobalConfiguration( + IHttpServer * pServer +) +{ + HKEY hKey; + BOOL fLocked = FALSE; + DWORD dwSize = 0; + DWORD dwResult = 0; + + if (!g_fGlobalInitialize) + { + AcquireSRWLockExclusive(&g_srwLockRH); + fLocked = TRUE; + + if (g_fGlobalInitialize) + { + // Done by another thread + goto Finished; + } + + g_pHttpServer = pServer; + if (pServer->IsCommandLineLaunch()) + { + g_hEventLog = RegisterEventSource(NULL, ASPNETCORE_IISEXPRESS_EVENT_PROVIDER); + } + else + { + g_hEventLog = RegisterEventSource(NULL, ASPNETCORE_EVENT_PROVIDER); + } + + if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, + L"SOFTWARE\\Microsoft\\IIS Extensions\\IIS AspNetCore Module V2\\Parameters", + 0, + KEY_READ, + &hKey) == NO_ERROR) + { + DWORD dwType; + DWORD dwData; + DWORD cbData; + + cbData = sizeof(dwData); + if ((RegQueryValueEx(hKey, + L"OptionalWinHttpFlags", + NULL, + &dwType, + (LPBYTE)&dwData, + &cbData) == NO_ERROR) && + (dwType == REG_DWORD)) + { + g_OptionalWinHttpFlags = dwData; + } + + cbData = sizeof(dwData); + if ((RegQueryValueEx(hKey, + L"EnableReferenceCountTracing", + NULL, + &dwType, + (LPBYTE)&dwData, + &cbData) == NO_ERROR) && + (dwType == REG_DWORD) && (dwData == 1 || dwData == 0)) + { + g_fEnableReferenceCountTracing = !!dwData; + } + } + + dwResult = GetExtendedTcpTable(NULL, + &dwSize, + FALSE, + AF_INET, + TCP_TABLE_OWNER_PID_LISTENER, + 0); + if (dwResult != NO_ERROR && dwResult != ERROR_INSUFFICIENT_BUFFER) + { + g_fNsiApiNotSupported = TRUE; + } + + g_fWebSocketStaticInitialize = IsWindows8OrGreater(); + + g_fGlobalInitialize = TRUE; + } +Finished: + if (fLocked) + { + ReleaseSRWLockExclusive(&g_srwLockRH); + } +} + +// +// Global initialization routine for OutOfProcess +// +HRESULT +EnsureOutOfProcessInitializtion(IHttpApplication *pHttpApplication) +{ + + DBG_ASSERT(g_pHttpServer); + + HRESULT hr = S_OK; + + if (g_fOutOfProcessInitializeError) + { + FINISHED(E_NOT_VALID_STATE); + } + + if (g_fOutOfProcessInitialize) + { + FINISHED(S_OK); + } + + { + auto lock = SRWExclusiveLock(g_srwLockRH); + + if (g_fOutOfProcessInitializeError) + { + FINISHED(E_NOT_VALID_STATE); + } + + if (g_fOutOfProcessInitialize) + { + // Done by another thread + FINISHED(S_OK); + } + + g_fOutOfProcessInitialize = TRUE; + + g_hWinHttpModule = GetModuleHandle(TEXT("winhttp.dll")); + + g_hAspNetCoreModule = GetModuleHandle(TEXT("aspnetcorev2.dll")); + + hr = WINHTTP_HELPER::StaticInitialize(); + if (FAILED_LOG(hr)) + { + if (hr == HRESULT_FROM_WIN32(ERROR_PROC_NOT_FOUND)) + { + g_fWebSocketStaticInitialize = FALSE; + } + else + { + FINISHED(hr); + } + } + + g_hWinhttpSession = WinHttpOpen(L"", + WINHTTP_ACCESS_TYPE_NO_PROXY, + WINHTTP_NO_PROXY_NAME, + WINHTTP_NO_PROXY_BYPASS, + WINHTTP_FLAG_ASYNC); + FINISHED_LAST_ERROR_IF(g_hWinhttpSession == NULL); + + // + // Don't set non-blocking callbacks WINHTTP_OPTION_ASSURED_NON_BLOCKING_CALLBACKS, + // as we will call WinHttpQueryDataAvailable to get response on the same thread + // that we received callback from Winhttp on completing sending/forwarding the request + // + + // + // Setup the callback function + // + FINISHED_LAST_ERROR_IF(WinHttpSetStatusCallback(g_hWinhttpSession, + FORWARDING_HANDLER::OnWinHttpCompletion, + (WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS | + WINHTTP_CALLBACK_STATUS_SENDING_REQUEST), + NULL) == WINHTTP_INVALID_STATUS_CALLBACK); + + // + // Make sure we see the redirects (rather than winhttp doing it + // automatically) + // + DWORD dwRedirectOption = WINHTTP_OPTION_REDIRECT_POLICY_NEVER; + FINISHED_LAST_ERROR_IF(!WinHttpSetOption(g_hWinhttpSession, + WINHTTP_OPTION_REDIRECT_POLICY, + &dwRedirectOption, + sizeof(dwRedirectOption))); + + g_dwTlsIndex = TlsAlloc(); + FINISHED_LAST_ERROR_IF(g_dwTlsIndex == TLS_OUT_OF_INDEXES); + FINISHED_IF_FAILED(ALLOC_CACHE_HANDLER::StaticInitialize()); + FINISHED_IF_FAILED(FORWARDING_HANDLER::StaticInitialize(g_fEnableReferenceCountTracing)); + FINISHED_IF_FAILED(WEBSOCKET_HANDLER::StaticInitialize(g_fEnableReferenceCountTracing)); + + DebugInitializeFromConfig(*g_pHttpServer, *pHttpApplication); + } +Finished: + if (FAILED(hr)) + { + g_fOutOfProcessInitializeError = TRUE; + } + return hr; +} + +BOOL APIENTRY DllMain(HMODULE hModule, + DWORD ul_reason_for_call, + LPVOID lpReserved +) +{ + UNREFERENCED_PARAMETER(lpReserved); + + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + g_hOutOfProcessRHModule = hModule; + DisableThreadLibraryCalls(hModule); + InitializeSRWLock(&g_srwLockRH); + DebugInitialize(hModule); + break; + case DLL_PROCESS_DETACH: + g_fProcessDetach = TRUE; + FORWARDING_HANDLER::StaticTerminate(); + ALLOC_CACHE_HANDLER::StaticTerminate(); + DebugStop(); + default: + break; + } + return TRUE; +} + +HRESULT +__stdcall +CreateApplication( + _In_ IHttpServer *pServer, + _In_ IHttpApplication *pHttpApplication, + _In_ APPLICATION_PARAMETER *pParameters, + _In_ DWORD nParameters, + _Out_ IAPPLICATION **ppApplication +) +{ + TraceContextScope traceScope(FindParameter("TraceContext", pParameters, nParameters)); + + InitializeGlobalConfiguration(pServer); + + REQUESTHANDLER_CONFIG *pConfig = nullptr; + RETURN_IF_FAILED(REQUESTHANDLER_CONFIG::CreateRequestHandlerConfig(pServer, pHttpApplication, &pConfig)); + std::unique_ptr pRequestHandlerConfig(pConfig); + + RETURN_IF_FAILED(EnsureOutOfProcessInitializtion(pHttpApplication)); + + std::unique_ptr pApplication = std::make_unique(*pHttpApplication, std::move(pRequestHandlerConfig)); + + RETURN_IF_FAILED(pApplication->Initialize()); + RETURN_IF_FAILED(pApplication->StartMonitoringAppOffline()); + + *ppApplication = pApplication.release(); + return S_OK; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/forwarderconnection.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/forwarderconnection.cpp new file mode 100644 index 0000000000..ee49e99242 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/forwarderconnection.cpp @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "forwarderconnection.h" +#include "exceptions.h" + +FORWARDER_CONNECTION::FORWARDER_CONNECTION( + VOID +) : m_cRefs (1), + m_hConnection (NULL) +{ +} + +HRESULT +FORWARDER_CONNECTION::Initialize( + DWORD dwPort +) +{ + RETURN_IF_FAILED(m_ConnectionKey.Initialize( dwPort )); + m_hConnection = WinHttpConnect(g_hWinhttpSession, + L"127.0.0.1", + (USHORT) dwPort, + 0); + RETURN_LAST_ERROR_IF_NULL(m_hConnection); + // + // Since WinHttp will not emit WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING + // when closing WebSocket handle on Win8. Register callback at Connect level as a workaround + // + RETURN_LAST_ERROR_IF (WinHttpSetStatusCallback(m_hConnection, + FORWARDING_HANDLER::OnWinHttpCompletion, + WINHTTP_CALLBACK_FLAG_HANDLES, + NULL) == WINHTTP_INVALID_STATUS_CALLBACK); + return S_OK; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/forwarderconnection.h b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/forwarderconnection.h new file mode 100644 index 0000000000..232e239888 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/forwarderconnection.h @@ -0,0 +1,157 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +// +// The key used for hash-table lookups, consists of the port on which the http process is created. +// +class FORWARDER_CONNECTION_KEY +{ +public: + + FORWARDER_CONNECTION_KEY( + VOID + ) + { + } + + HRESULT + Initialize( + _In_ DWORD dwPort + ) + { + m_dwPort = dwPort; + return S_OK; + } + + BOOL + GetIsEqual( + const FORWARDER_CONNECTION_KEY * key2 + ) const + { + return m_dwPort == key2->m_dwPort; + } + + DWORD CalcKeyHash() const + { + // TODO: Review hash distribution. + return Hash(m_dwPort); + } + +private: + + DWORD m_dwPort; +}; + +class FORWARDER_CONNECTION +{ +public: + + FORWARDER_CONNECTION( + VOID + ); + + HRESULT + Initialize( + DWORD dwPort + ); + + HINTERNET + QueryHandle() const + { + return m_hConnection; + } + + VOID + ReferenceForwarderConnection() const + { + InterlockedIncrement(&m_cRefs); + } + + VOID + DereferenceForwarderConnection() const + { + if (InterlockedDecrement(&m_cRefs) == 0) + { + delete this; + } + } + + FORWARDER_CONNECTION_KEY * + QueryConnectionKey() + { + return &m_ConnectionKey; + } + +private: + + ~FORWARDER_CONNECTION() + { + if (m_hConnection != NULL) + { + WinHttpCloseHandle(m_hConnection); + m_hConnection = NULL; + } + } + + mutable LONG m_cRefs; + FORWARDER_CONNECTION_KEY m_ConnectionKey; + HINTERNET m_hConnection; +}; + +class FORWARDER_CONNECTION_HASH : + public HASH_TABLE +{ + +public: + + FORWARDER_CONNECTION_HASH() + {} + + FORWARDER_CONNECTION_KEY * + ExtractKey( + FORWARDER_CONNECTION *pConnection + ) + { + return pConnection->QueryConnectionKey(); + } + + DWORD + CalcKeyHash( + FORWARDER_CONNECTION_KEY *key + ) + { + return key->CalcKeyHash(); + } + + BOOL + EqualKeys( + FORWARDER_CONNECTION_KEY *key1, + FORWARDER_CONNECTION_KEY *key2 + ) + { + return key1->GetIsEqual(key2); + } + + VOID + ReferenceRecord( + FORWARDER_CONNECTION *pConnection + ) + { + pConnection->ReferenceForwarderConnection(); + } + + VOID + DereferenceRecord( + FORWARDER_CONNECTION *pConnection + ) + { + pConnection->DereferenceForwarderConnection(); + } + +private: + + FORWARDER_CONNECTION_HASH(const FORWARDER_CONNECTION_HASH &); + void operator=(const FORWARDER_CONNECTION_HASH &); +}; \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/forwardinghandler.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/forwardinghandler.cpp new file mode 100644 index 0000000000..c8c0640803 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/forwardinghandler.cpp @@ -0,0 +1,2703 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "forwardinghandler.h" +#include "url_utility.h" +#include "exceptions.h" +#include "ServerErrorHandler.h" +#include "resource.h" + +// Just to be aware of the FORWARDING_HANDLER object size. +C_ASSERT(sizeof(FORWARDING_HANDLER) <= 632); + +#define DEF_MAX_FORWARDS 32 +#define HEX_TO_ASCII(c) ((CHAR)(((c) < 10) ? ((c) + '0') : ((c) + 'a' - 10))) +#define BUFFER_SIZE (8192UL) +#define ENTITY_BUFFER_SIZE (6 + BUFFER_SIZE + 2) + +#define FORWARDING_HANDLER_SIGNATURE ((DWORD)'FHLR') +#define FORWARDING_HANDLER_SIGNATURE_FREE ((DWORD)'fhlr') + +ALLOC_CACHE_HANDLER * FORWARDING_HANDLER::sm_pAlloc = NULL; +TRACE_LOG * FORWARDING_HANDLER::sm_pTraceLog = NULL; +PROTOCOL_CONFIG FORWARDING_HANDLER::sm_ProtocolConfig; +RESPONSE_HEADER_HASH * FORWARDING_HANDLER::sm_pResponseHeaderHash = NULL; + +FORWARDING_HANDLER::FORWARDING_HANDLER( + _In_ IHttpContext *pW3Context, + _In_ std::unique_ptr pApplication +) : REQUEST_HANDLER(*pW3Context), + m_Signature(FORWARDING_HANDLER_SIGNATURE), + m_RequestStatus(FORWARDER_START), + m_fClientDisconnected(FALSE), + m_fResponseHeadersReceivedAndSet(FALSE), + m_fDoReverseRewriteHeaders(FALSE), + m_fFinishRequest(FALSE), + m_fHasError(FALSE), + m_pszHeaders(NULL), + m_cchHeaders(0), + m_BytesToReceive(0), + m_BytesToSend(0), + m_fWebSocketEnabled(FALSE), + m_pWebSocket(NULL), + m_dwHandlers (1), // default http handler + m_fDoneAsyncCompletion(FALSE), + m_fHttpHandleInClose(FALSE), + m_fWebSocketHandleInClose(FALSE), + m_fServerResetConn(FALSE), + m_cRefs(1), + m_pW3Context(pW3Context), + m_pApplication(std::move(pApplication)), + m_fReactToDisconnect(FALSE) +{ + LOG_TRACE(L"FORWARDING_HANDLER::FORWARDING_HANDLER"); + + m_fWebSocketSupported = m_pApplication->QueryWebsocketStatus(); + InitializeSRWLock(&m_RequestLock); +} + +FORWARDING_HANDLER::~FORWARDING_HANDLER( +) +{ + // + // Destructor has started. + // + m_Signature = FORWARDING_HANDLER_SIGNATURE_FREE; + + LOG_TRACE(L"FORWARDING_HANDLER::~FORWARDING_HANDLER"); + + // + // RemoveRequest() should already have been called and m_pDisconnect + // has been freed or m_pDisconnect was never initialized. + // + // Disconnect notification cleanup would happen first, before + // the FORWARDING_HANDLER instance got removed from m_pSharedhandler list. + // The m_pServer cleanup would happen afterwards, since there may be a + // call pending from SHARED_HANDLER to FORWARDING_HANDLER::SetStatusAndHeaders() + // + DBG_ASSERT(!m_fReactToDisconnect); + + RemoveRequest(); + + FreeResponseBuffers(); + + if (m_pWebSocket) + { + m_pWebSocket->Terminate(); + m_pWebSocket = NULL; + } +} + +__override +REQUEST_NOTIFICATION_STATUS +FORWARDING_HANDLER::ExecuteRequestHandler() +{ + REQUEST_NOTIFICATION_STATUS retVal = RQ_NOTIFICATION_CONTINUE; + HRESULT hr = S_OK; + BOOL fRequestLocked = FALSE; + BOOL fFailedToStartKestrel = FALSE; + BOOL fSecure = FALSE; + HINTERNET hConnect = NULL; + IHttpRequest *pRequest = m_pW3Context->GetRequest(); + IHttpResponse *pResponse = m_pW3Context->GetResponse(); + IHttpConnection *pClientConnection = NULL; + PROTOCOL_CONFIG *pProtocol = &sm_ProtocolConfig; + SERVER_PROCESS *pServerProcess = NULL; + + USHORT cchHostName = 0; + + STACK_STRU(strDestination, 32); + STACK_STRU(strUrl, 2048); + STACK_STRU(struEscapedUrl, 2048); + + // + // Take a reference so that object does not go away as a result of + // async completion. + // + ReferenceRequestHandler(); + + // override Protocol related config from aspNetCore config + pProtocol->OverrideConfig(m_pApplication->QueryConfig()); + + // check connection + pClientConnection = m_pW3Context->GetConnection(); + if (pClientConnection == NULL || + !pClientConnection->IsConnected()) + { + hr = HRESULT_FROM_WIN32(WSAECONNRESET); + goto Failure; + } + + if (m_pApplication == NULL) + { + hr = E_INVALIDARG; + goto Failure; + } + + hr = m_pApplication->GetProcess(&pServerProcess); + if (FAILED_LOG(hr)) + { + fFailedToStartKestrel = TRUE; + goto Failure; + } + + if (pServerProcess == NULL) + { + fFailedToStartKestrel = TRUE; + hr = HRESULT_FROM_WIN32(ERROR_CREATE_FAILED); + goto Failure; + } + + if (pServerProcess->QueryWinHttpConnection() == NULL) + { + hr = HRESULT_FROM_WIN32(ERROR_INVALID_HANDLE); + goto Failure; + } + + hConnect = pServerProcess->QueryWinHttpConnection()->QueryHandle(); + + m_pszOriginalHostHeader = pRequest->GetHeader(HttpHeaderHost, &cchHostName); + // + // parse original url + // + if (FAILED_LOG(hr = URL_UTILITY::SplitUrl(pRequest->GetRawHttpRequest()->CookedUrl.pFullUrl, + &fSecure, + &strDestination, + &strUrl))) + { + goto Failure; + } + + if (FAILED_LOG(hr = URL_UTILITY::EscapeAbsPath(pRequest, &struEscapedUrl))) + { + goto Failure; + } + + m_fDoReverseRewriteHeaders = pProtocol->QueryReverseRewriteHeaders(); + + m_cMinBufferLimit = pProtocol->QueryMinResponseBuffer(); + + // + // Mark request as websocket if upgrade header is present. + // + if (m_fWebSocketSupported) + { + USHORT cchHeader = 0; + PCSTR pszWebSocketHeader = pRequest->GetHeader("Upgrade", &cchHeader); + if (cchHeader == 9 && _stricmp(pszWebSocketHeader, "websocket") == 0) + { + m_fWebSocketEnabled = TRUE; + } + } + + hr = CreateWinHttpRequest(pRequest, + pProtocol, + hConnect, + &struEscapedUrl, + pServerProcess); + if (FAILED_LOG(hr)) + { + goto Failure; + } + + m_fReactToDisconnect = TRUE; + + // require lock as client disconnect callback may happen + AcquireSRWLockShared(&m_RequestLock); + fRequestLocked = TRUE; + + // + // Remember the handler being processed in the current thread + // before staring a WinHTTP operation. + // + DBG_ASSERT(fRequestLocked); + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == NULL); + TlsSetValue(g_dwTlsIndex, this); + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == this); + + if (m_hRequest == NULL) + { + hr = HRESULT_FROM_WIN32(WSAECONNRESET); + goto Failure; + } + + // + // Begins normal request handling. Send request to server. + // + m_RequestStatus = FORWARDER_SENDING_REQUEST; + + // + // Calculate the bytes to receive from the content length. + // + DWORD cbContentLength = 0; + PCSTR pszContentLength = pRequest->GetHeader(HttpHeaderContentLength); + if (pszContentLength != NULL) + { + cbContentLength = m_BytesToReceive = atol(pszContentLength); + if (m_BytesToReceive == INFINITE) + { + hr = HRESULT_FROM_WIN32(WSAECONNRESET); + goto Failure; + } + } + else if (pRequest->GetHeader(HttpHeaderTransferEncoding) != NULL) + { + m_BytesToReceive = INFINITE; + } + + if (m_fWebSocketEnabled) + { + // + // Set the upgrade flag for a websocket request. + // + if (!WinHttpSetOption(m_hRequest, + WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET, + NULL, + 0)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + } + + m_cchLastSend = m_cchHeaders; + + //FREB log + if (ANCMEvents::ANCM_REQUEST_FORWARD_START::IsEnabled(m_pW3Context->GetTraceContext())) + { + ANCMEvents::ANCM_REQUEST_FORWARD_START::RaiseEvent( + m_pW3Context->GetTraceContext(), + NULL); + } + + if (!WinHttpSendRequest(m_hRequest, + m_pszHeaders, + m_cchHeaders, + NULL, + 0, + cbContentLength, + reinterpret_cast(static_cast(this)))) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + + LOG_TRACE(L"FORWARDING_HANDLER::OnExecuteRequestHandler, Send request failed"); + + // FREB log + if (ANCMEvents::ANCM_REQUEST_FORWARD_FAIL::IsEnabled(m_pW3Context->GetTraceContext())) + { + ANCMEvents::ANCM_REQUEST_FORWARD_FAIL::RaiseEvent( + m_pW3Context->GetTraceContext(), + NULL, + hr); + } + + goto Failure; + } + + // + // Async WinHTTP operation is in progress. Release this thread meanwhile, + // OnWinHttpCompletion method should resume the work by posting an IIS completion. + // + retVal = RQ_NOTIFICATION_PENDING; + goto Finished; + +Failure: + m_RequestStatus = FORWARDER_DONE; + + //disable client disconnect callback + RemoveRequest(); + + pResponse->DisableKernelCache(); + pResponse->GetRawHttpResponse()->EntityChunkCount = 0; + if (hr == HRESULT_FROM_WIN32(WSAECONNRESET)) + { + pResponse->SetStatus(400, "Bad Request", 0, hr); + } + else if (hr == E_APPLICATION_EXITING) + { + pResponse->SetStatus(503, "Service Unavailable", 0, S_OK, nullptr, TRUE); + } + else if (fFailedToStartKestrel && !m_pApplication->QueryConfig()->QueryDisableStartUpErrorPage()) + { + ServerErrorHandler handler(*m_pW3Context, 502, 5, "Bad Gateway", hr, g_hOutOfProcessRHModule, m_pApplication->QueryConfig()->QueryDisableStartUpErrorPage(), OUT_OF_PROCESS_RH_STATIC_HTML); + handler.ExecuteRequestHandler(); + } + else + { + // + // default error behavior + // + pResponse->SetStatus(502, "Bad Gateway", 3, hr); + } + // + // Finish the request on failure. + // + retVal = RQ_NOTIFICATION_FINISH_REQUEST; + +Finished: + if (fRequestLocked) + { + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == this); + TlsSetValue(g_dwTlsIndex, NULL); + ReleaseSRWLockShared(&m_RequestLock); + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == NULL); + } + + DereferenceRequestHandler(); + // + // Do not use this object after dereferencing it, it may be gone. + // + + return retVal; +} + +__override +REQUEST_NOTIFICATION_STATUS +FORWARDING_HANDLER::AsyncCompletion( + DWORD cbCompletion, + HRESULT hrCompletionStatus +) +/*++ + +Routine Description: + +Handle the completion from IIS and continue the execution +of this request based on the current state. + +Arguments: + +cbCompletion - Number of bytes associated with this completion +dwCompletionStatus - the win32 status associated with this completion + +Return Value: + +REQUEST_NOTIFICATION_STATUS + +--*/ +{ + HRESULT hr = S_OK; + REQUEST_NOTIFICATION_STATUS retVal = RQ_NOTIFICATION_PENDING; + BOOL fLocked = FALSE; + BOOL fClientError = FALSE; + BOOL fClosed = FALSE; + BOOL fWebSocketUpgraded = FALSE; + + DBG_ASSERT(m_pW3Context != NULL); + __analysis_assume(m_pW3Context != NULL); + + // + // Take a reference so that object does not go away as a result of + // async completion. + // + ReferenceRequestHandler(); + + if (sm_pTraceLog != NULL) + { + WriteRefTraceLogEx(sm_pTraceLog, + m_cRefs, + this, + "FORWARDING_HANDLER::OnAsyncCompletion Enter", + reinterpret_cast(static_cast(cbCompletion)), + reinterpret_cast(static_cast(hrCompletionStatus))); + } + + if (TlsGetValue(g_dwTlsIndex) != this) + { + // + // Acquire exclusive lock as WinHTTP callback may happen on different thread + // We don't want two threads signal IIS pipeline simultaneously + // + AcquireLockExclusive(); + fLocked = TRUE; + } + + if (m_fClientDisconnected && (m_RequestStatus != FORWARDER_DONE)) + { + hr = ERROR_CONNECTION_ABORTED; + goto Failure; + } + + if (m_RequestStatus == FORWARDER_RECEIVED_WEBSOCKET_RESPONSE) + { + LOG_TRACE(L"FORWARDING_HANDLER::OnAsyncCompletion, Send completed for 101 response"); + + // + // This should be the write completion of the 101 response. + // + m_pWebSocket = new WEBSOCKET_HANDLER(); + if (m_pWebSocket == NULL) + { + hr = E_OUTOFMEMORY; + goto Failure; + } + + hr = m_pWebSocket->ProcessRequest(this, m_pW3Context, m_hRequest, &fWebSocketUpgraded); + if (fWebSocketUpgraded) + { + // WinHttp WebSocket handle has been created, bump the counter so that remember to close it + // and prevent from premature postcomplation and unexpected callback from winhttp + InterlockedIncrement(&m_dwHandlers); + } + + if (FAILED_LOG(hr)) + { + // This failure could happen when client disconnect happens or backend server fails + // after websocket upgrade + goto Failure; + } + + // + // WebSocket upgrade is successful. Close the WinHttpRequest Handle + // + m_fHttpHandleInClose = TRUE; + fClosed = WinHttpCloseHandle(m_hRequest); + DBG_ASSERT(fClosed); + m_hRequest = NULL; + + if (!fClosed) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Failure; + } + retVal = RQ_NOTIFICATION_PENDING; + goto Finished; + } + + // + // Begins normal completion handling. There is already an exclusive acquired lock + // for protecting the WinHTTP request handle from being closed. + // + switch (m_RequestStatus) + { + case FORWARDER_RECEIVING_RESPONSE: + + // + // This is a completion of a write (send) to http.sys, abort in case of + // failure, if there is more data available from WinHTTP, read it + // or else ask if there is more. + // + if (FAILED_LOG(hrCompletionStatus)) + { + hr = hrCompletionStatus; + fClientError = TRUE; + goto Failure; + } + + hr = OnReceivingResponse(); + if (FAILED_LOG(hr)) + { + goto Failure; + } + break; + + case FORWARDER_SENDING_REQUEST: + + hr = OnSendingRequest(cbCompletion, + hrCompletionStatus, + &fClientError); + if (FAILED_LOG(hr)) + { + goto Failure; + } + break; + + default: + DBG_ASSERT(m_RequestStatus == FORWARDER_DONE); + if (m_hRequest == NULL && m_pWebSocket == NULL) + { + // Request must have been done + if (!m_fFinishRequest) + { + goto Failure; + } + + if (m_fHasError) + { + retVal = RQ_NOTIFICATION_FINISH_REQUEST; + } + else + { + retVal = RQ_NOTIFICATION_CONTINUE; + } + } + goto Finished; + } + + // + // Either OnReceivingResponse or OnSendingRequest initiated an + // async WinHTTP operation, release this thread meanwhile, + // OnWinHttpCompletion method should resume the work by posting an IIS completion. + // + retVal = RQ_NOTIFICATION_PENDING; + goto Finished; + +Failure: + + // + // Reset status for consistency. + // + m_RequestStatus = FORWARDER_DONE; + if (!m_fHasError) + { + m_fHasError = TRUE; + + // + // Do the right thing based on where the error originated from. + // + IHttpResponse *pResponse = m_pW3Context->GetResponse(); + pResponse->DisableKernelCache(); + pResponse->GetRawHttpResponse()->EntityChunkCount = 0; + + if (fClientError || m_fClientDisconnected) + { + if (!m_fResponseHeadersReceivedAndSet) + { + pResponse->SetStatus(400, "Bad Request", 0, HRESULT_FROM_WIN32(WSAECONNRESET)); + } + else + { + // + // Response headers from origin server were + // already received and set for the current response. + // Honor the response status. + // + } + } + else + { + STACK_STRU(strDescription, 128); + + pResponse->SetStatus(502, "Bad Gateway", 3, hr); + + if (hr > HRESULT_FROM_WIN32(WINHTTP_ERROR_BASE) && + hr <= HRESULT_FROM_WIN32(WINHTTP_ERROR_LAST)) + { +#pragma prefast (suppress : __WARNING_FUNCTION_NEEDS_REVIEW, "Function and parameters reviewed.") + FormatMessage( + FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_FROM_HMODULE, + g_hWinHttpModule, + HRESULT_CODE(hr), + 0, + strDescription.QueryStr(), + strDescription.QuerySizeCCH(), + NULL); + } + else + { + LoadString(g_hAspNetCoreModule, + IDS_SERVER_ERROR, + strDescription.QueryStr(), + strDescription.QuerySizeCCH()); + } + (VOID)strDescription.SyncWithBuffer(); + + if (strDescription.QueryCCH() != 0) + { + pResponse->SetErrorDescription( + strDescription.QueryStr(), + strDescription.QueryCCH(), + FALSE); + } + + if (hr == HRESULT_FROM_WIN32(ERROR_WINHTTP_INVALID_SERVER_RESPONSE)) + { + if (!m_fServerResetConn) + { + RemoveRequest(); + pResponse->ResetConnection(); + m_fServerResetConn = TRUE; + } + } + } + } + + if (m_pWebSocket != NULL && !m_fWebSocketHandleInClose) + { + m_fWebSocketHandleInClose = TRUE; + m_pWebSocket->TerminateRequest(); + } + + if (m_hRequest != NULL && !m_fHttpHandleInClose) + { + m_fHttpHandleInClose = TRUE; + WinHttpCloseHandle(m_hRequest); + m_hRequest = NULL; + } + +Finished: + + if (retVal != RQ_NOTIFICATION_PENDING) + { + + DBG_ASSERT(m_dwHandlers == 0); + RemoveRequest(); + + // This is just a safety guard to prevent from returning non pending status no more once + // which should never happen + if (!m_fDoneAsyncCompletion) + { + m_fDoneAsyncCompletion = TRUE; + } + else + { + retVal = RQ_NOTIFICATION_PENDING; + } + } + + if (fLocked) + { + ReleaseLockExclusive(); + } + + DereferenceRequestHandler(); + // + // Do not use this object after dereferencing it, it may be gone. + // + LOG_TRACEF(L"FORWARDING_HANDLER::OnAsyncCompletion Done %d", retVal); + return retVal; +} + +// static +HRESULT +FORWARDING_HANDLER::StaticInitialize( + BOOL fEnableReferenceCountTracing +) +/*++ + +Routine Description: + +Global initialization routine for FORWARDING_HANDLERs + +Arguments: + +fEnableReferenceCountTracing - True if ref count tracing should be use. + +Return Value: + +HRESULT + +--*/ +{ + HRESULT hr = S_OK; + + sm_pAlloc = new ALLOC_CACHE_HANDLER; + if (sm_pAlloc == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + hr = sm_pAlloc->Initialize(sizeof(FORWARDING_HANDLER), + 64); // nThreshold + if (FAILED_LOG(hr)) + { + goto Finished; + } + + sm_pResponseHeaderHash = new RESPONSE_HEADER_HASH; + if (sm_pResponseHeaderHash == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + hr = sm_pResponseHeaderHash->Initialize(); + if (FAILED_LOG(hr)) + { + goto Finished; + } + + // Initialize PROTOCOL_CONFIG + hr = sm_ProtocolConfig.Initialize(); + if (FAILED_LOG(hr)) + { + goto Finished; + } + + if (fEnableReferenceCountTracing) + { + sm_pTraceLog = CreateRefTraceLog(10000, 0); + } + +Finished: + if (FAILED_LOG(hr)) + { + StaticTerminate(); + } + return hr; +} + +//static +VOID +FORWARDING_HANDLER::StaticTerminate() +{ + if (sm_pResponseHeaderHash != NULL) + { + sm_pResponseHeaderHash->Clear(); + delete sm_pResponseHeaderHash; + sm_pResponseHeaderHash = NULL; + } + + if (sm_pTraceLog != NULL) + { + DestroyRefTraceLog(sm_pTraceLog); + sm_pTraceLog = NULL; + } + + if (sm_pAlloc != NULL) + { + delete sm_pAlloc; + sm_pAlloc = NULL; + } +} + +// static +void * FORWARDING_HANDLER::operator new(size_t) +{ + DBG_ASSERT(sm_pAlloc != NULL); + if (sm_pAlloc == NULL) + { + return NULL; + } + return sm_pAlloc->Alloc(); +} + +// static +void FORWARDING_HANDLER::operator delete(void * pMemory) +{ + DBG_ASSERT(sm_pAlloc != NULL); + if (sm_pAlloc != NULL) + { + sm_pAlloc->Free(pMemory); + } +} + +HRESULT +FORWARDING_HANDLER::GetHeaders( + _In_ const PROTOCOL_CONFIG * pProtocol, + _In_ BOOL fForwardWindowsAuthToken, + _In_ SERVER_PROCESS* pServerProcess, + _Out_ PCWSTR * ppszHeaders, + _Inout_ DWORD * pcchHeaders +) +{ + HRESULT hr = S_OK; + PCSTR pszCurrentHeader; + PCSTR ppHeadersToBeRemoved; + PCSTR pszFinalHeader; + USHORT cchCurrentHeader; + DWORD cchFinalHeader; + BOOL fSecure = FALSE; // dummy. Used in SplitUrl. Value will not be used + // as ANCM always use http protocol to communicate with backend + STRU struDestination; + STRU struUrl; + STACK_STRA(strTemp, 64); + HTTP_REQUEST_HEADERS *pHeaders; + IHttpRequest *pRequest = m_pW3Context->GetRequest(); + MULTISZA mszMsAspNetCoreHeaders; + + // + // We historically set the host section in request url to the new host header + // this is wrong but Kestrel has dependency on it. + // should change it in the future + // + if (!pProtocol->QueryPreserveHostHeader()) + { + if (FAILED_LOG(hr = URL_UTILITY::SplitUrl(pRequest->GetRawHttpRequest()->CookedUrl.pFullUrl, + &fSecure, + &struDestination, + &struUrl)) || + FAILED_LOG(hr = strTemp.CopyW(struDestination.QueryStr())) || + FAILED_LOG(hr = pRequest->SetHeader(HttpHeaderHost, + strTemp.QueryStr(), + static_cast(strTemp.QueryCCH()), + TRUE))) // fReplace + { + return hr; + } + } + // + // Strip all headers starting with MS-ASPNETCORE. + // These headers are generated by the asp.net core module and + // passed to the process it creates. + // + + pHeaders = &m_pW3Context->GetRequest()->GetRawHttpRequest()->Headers; + for (DWORD i = 0; iUnknownHeaderCount; i++) + { + if (_strnicmp(pHeaders->pUnknownHeaders[i].pName, "MS-ASPNETCORE", 13) == 0) + { + mszMsAspNetCoreHeaders.Append(pHeaders->pUnknownHeaders[i].pName, (DWORD)pHeaders->pUnknownHeaders[i].NameLength); + } + } + + ppHeadersToBeRemoved = mszMsAspNetCoreHeaders.First(); + + // + // iterate the list of headers to be removed and delete them from the request. + // + + while (ppHeadersToBeRemoved != NULL) + { + m_pW3Context->GetRequest()->DeleteHeader(ppHeadersToBeRemoved); + ppHeadersToBeRemoved = mszMsAspNetCoreHeaders.Next(ppHeadersToBeRemoved); + } + + if (pServerProcess->QueryGuid() != NULL) + { + hr = m_pW3Context->GetRequest()->SetHeader("MS-ASPNETCORE-TOKEN", + pServerProcess->QueryGuid(), + (USHORT)strlen(pServerProcess->QueryGuid()), + TRUE); + if (FAILED_LOG(hr)) + { + return hr; + } + } + + if (fForwardWindowsAuthToken && + (_wcsicmp(m_pW3Context->GetUser()->GetAuthenticationType(), L"negotiate") == 0 || + _wcsicmp(m_pW3Context->GetUser()->GetAuthenticationType(), L"ntlm") == 0)) + { + if (m_pW3Context->GetUser()->GetPrimaryToken() != NULL && + m_pW3Context->GetUser()->GetPrimaryToken() != INVALID_HANDLE_VALUE) + { + HANDLE hTargetTokenHandle = NULL; + hr = pServerProcess->SetWindowsAuthToken(m_pW3Context->GetUser()->GetPrimaryToken(), + &hTargetTokenHandle); + if (FAILED_LOG(hr)) + { + return hr; + } + + // + // set request header with target token value + // + CHAR pszHandleStr[16] = { 0 }; + if (_ui64toa_s((UINT64)hTargetTokenHandle, pszHandleStr, 16, 16) != 0) + { + hr = HRESULT_FROM_WIN32(ERROR_INVALID_DATA); + return hr; + } + + hr = m_pW3Context->GetRequest()->SetHeader("MS-ASPNETCORE-WINAUTHTOKEN", + pszHandleStr, + (USHORT)strlen(pszHandleStr), + TRUE); + if (FAILED_LOG(hr)) + { + return hr; + } + } + } + + if (!pProtocol->QueryXForwardedForName()->IsEmpty()) + { + strTemp.Reset(); + + pszCurrentHeader = pRequest->GetHeader(pProtocol->QueryXForwardedForName()->QueryStr(), &cchCurrentHeader); + if (pszCurrentHeader != NULL) + { + if (FAILED_LOG(hr = strTemp.Copy(pszCurrentHeader, cchCurrentHeader)) || + FAILED_LOG(hr = strTemp.Append(", ", 2))) + { + return hr; + } + } + + if (FAILED_LOG(hr = m_pW3Context->GetServerVariable("REMOTE_ADDR", + &pszFinalHeader, + &cchFinalHeader))) + { + return hr; + } + + if (pRequest->GetRawHttpRequest()->Address.pRemoteAddress->sa_family == AF_INET6) + { + if (FAILED_LOG(hr = strTemp.Append("[", 1)) || + FAILED_LOG(hr = strTemp.Append(pszFinalHeader, cchFinalHeader)) || + FAILED_LOG(hr = strTemp.Append("]", 1))) + { + return hr; + } + } + else + { + if (FAILED_LOG(hr = strTemp.Append(pszFinalHeader, cchFinalHeader))) + { + return hr; + } + } + + if (pProtocol->QueryIncludePortInXForwardedFor()) + { + if (FAILED_LOG(hr = m_pW3Context->GetServerVariable("REMOTE_PORT", + &pszFinalHeader, + &cchFinalHeader))) + { + return hr; + } + + if (FAILED_LOG(hr = strTemp.Append(":", 1)) || + FAILED_LOG(hr = strTemp.Append(pszFinalHeader, cchFinalHeader))) + { + return hr; + } + } + + if (FAILED_LOG(hr = pRequest->SetHeader(pProtocol->QueryXForwardedForName()->QueryStr(), + strTemp.QueryStr(), + static_cast(strTemp.QueryCCH()), + TRUE))) // fReplace + { + return hr; + } + } + + if (!pProtocol->QuerySslHeaderName()->IsEmpty()) + { + const HTTP_SSL_INFO *pSslInfo = pRequest->GetRawHttpRequest()->pSslInfo; + LPSTR pszScheme = "http"; + if (pSslInfo != NULL) + { + pszScheme = "https"; + } + + strTemp.Reset(); + + pszCurrentHeader = pRequest->GetHeader(pProtocol->QuerySslHeaderName()->QueryStr(), &cchCurrentHeader); + if (pszCurrentHeader != NULL) + { + if (FAILED_LOG(hr = strTemp.Copy(pszCurrentHeader, cchCurrentHeader)) || + FAILED_LOG(hr = strTemp.Append(", ", 2))) + { + return hr; + } + } + + if (FAILED_LOG(hr = strTemp.Append(pszScheme))) + { + return hr; + } + + if (FAILED_LOG(pRequest->SetHeader(pProtocol->QuerySslHeaderName()->QueryStr(), + strTemp.QueryStr(), + (USHORT)strTemp.QueryCCH(), + TRUE))) + { + return hr; + } + } + + if (!pProtocol->QueryClientCertName()->IsEmpty()) + { + if (pRequest->GetRawHttpRequest()->pSslInfo == NULL || + pRequest->GetRawHttpRequest()->pSslInfo->pClientCertInfo == NULL) + { + pRequest->DeleteHeader(pProtocol->QueryClientCertName()->QueryStr()); + } + else + { + // Resize the buffer large enough to hold the encoded certificate info + if (FAILED_LOG(hr = strTemp.Resize( + 1 + (pRequest->GetRawHttpRequest()->pSslInfo->pClientCertInfo->CertEncodedSize + 2) / 3 * 4))) + { + return hr; + } + + Base64Encode( + pRequest->GetRawHttpRequest()->pSslInfo->pClientCertInfo->pCertEncoded, + pRequest->GetRawHttpRequest()->pSslInfo->pClientCertInfo->CertEncodedSize, + strTemp.QueryStr(), + strTemp.QuerySize(), + NULL); + strTemp.SyncWithBuffer(); + + if (FAILED_LOG(hr = pRequest->SetHeader( + pProtocol->QueryClientCertName()->QueryStr(), + strTemp.QueryStr(), + static_cast(strTemp.QueryCCH()), + TRUE))) // fReplace + { + return hr; + } + } + } + + // + // Remove the connection header + // + if (!m_fWebSocketEnabled) + { + pRequest->DeleteHeader(HttpHeaderConnection); + } + + // + // Get all the headers to send to the client + // + hr = m_pW3Context->GetServerVariable("ALL_RAW", + ppszHeaders, + pcchHeaders); + if (FAILED_LOG(hr)) + { + return hr; + } + + return S_OK; +} + +HRESULT +FORWARDING_HANDLER::CreateWinHttpRequest( + _In_ const IHttpRequest * pRequest, + _In_ const PROTOCOL_CONFIG * pProtocol, + _In_ HINTERNET hConnect, + _Inout_ STRU * pstrUrl, + _In_ SERVER_PROCESS* pServerProcess +) +{ + HRESULT hr = S_OK; + PCWSTR pszVersion = NULL; + PCSTR pszVerb; + DWORD dwTimeout = INFINITE; + STACK_STRU(strVerb, 32); + + // + // Create the request handle for this request (leave some fields blank, + // we will fill them when sending the request) + // + pszVerb = pRequest->GetHttpMethod(); + if (FAILED_LOG(hr = strVerb.CopyA(pszVerb))) + { + goto Finished; + } + + //pszVersion = pProtocol->QueryVersion(); + if (pszVersion == NULL) + { + DWORD cchUnused; + hr = m_pW3Context->GetServerVariable( + "HTTP_VERSION", + &pszVersion, + &cchUnused); + if (FAILED_LOG(hr)) + { + goto Finished; + } + } + + m_hRequest = WinHttpOpenRequest(hConnect, + strVerb.QueryStr(), + pstrUrl->QueryStr(), + pszVersion, + WINHTTP_NO_REFERER, + WINHTTP_DEFAULT_ACCEPT_TYPES, + WINHTTP_FLAG_ESCAPE_DISABLE_QUERY + | g_OptionalWinHttpFlags); + if (m_hRequest == NULL) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + if (!pServerProcess->IsDebuggerAttached()) + { + dwTimeout = pProtocol->QueryTimeout(); + } + + if (!WinHttpSetTimeouts(m_hRequest, + dwTimeout, //resolve timeout + dwTimeout, // connect timeout + dwTimeout, // send timeout + dwTimeout)) // receive timeout + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + DWORD dwResponseBufferLimit = pProtocol->QueryResponseBufferLimit(); + if (!WinHttpSetOption(m_hRequest, + WINHTTP_OPTION_MAX_RESPONSE_DRAIN_SIZE, + &dwResponseBufferLimit, + sizeof(dwResponseBufferLimit))) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + DWORD dwMaxHeaderSize = pProtocol->QueryMaxResponseHeaderSize(); + if (!WinHttpSetOption(m_hRequest, + WINHTTP_OPTION_MAX_RESPONSE_HEADER_SIZE, + &dwMaxHeaderSize, + sizeof(dwMaxHeaderSize))) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + DWORD dwOption = WINHTTP_DISABLE_COOKIES; + + dwOption |= WINHTTP_DISABLE_AUTHENTICATION; + + if (!pProtocol->QueryDoKeepAlive()) + { + dwOption |= WINHTTP_DISABLE_KEEP_ALIVE; + } + if (!WinHttpSetOption(m_hRequest, + WINHTTP_OPTION_DISABLE_FEATURE, + &dwOption, + sizeof(dwOption))) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + if (WinHttpSetStatusCallback(m_hRequest, + FORWARDING_HANDLER::OnWinHttpCompletion, + (WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS | + WINHTTP_CALLBACK_FLAG_HANDLES | + WINHTTP_CALLBACK_STATUS_SENDING_REQUEST), + NULL) == WINHTTP_INVALID_STATUS_CALLBACK) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + hr = GetHeaders(pProtocol, + m_pApplication->QueryConfig()->QueryForwardWindowsAuthToken(), + pServerProcess, + &m_pszHeaders, + &m_cchHeaders); + if (FAILED_LOG(hr)) + { + goto Finished; + } + +Finished: + + return hr; +} + +VOID +FORWARDING_HANDLER::OnWinHttpCompletion( + HINTERNET hRequest, + DWORD_PTR dwContext, + DWORD dwInternetStatus, + LPVOID lpvStatusInformation, + DWORD dwStatusInformationLength +) +{ + FORWARDING_HANDLER * pThis = static_cast(reinterpret_cast(dwContext)); + if (pThis == NULL) + { + //error happened, nothing can be done here + return; + } + DBG_ASSERT(pThis->m_Signature == FORWARDING_HANDLER_SIGNATURE); + pThis->OnWinHttpCompletionInternal(hRequest, + dwInternetStatus, + lpvStatusInformation, + dwStatusInformationLength); +} + +VOID +FORWARDING_HANDLER::OnWinHttpCompletionInternal( + _In_ HINTERNET hRequest, + _In_ DWORD dwInternetStatus, + _In_ LPVOID lpvStatusInformation, + _In_ DWORD dwStatusInformationLength +) +/*++ + +Routine Description: + +Completion call associated with a WinHTTP operation + +Arguments: + +hRequest - The winhttp request handle associated with this completion +dwInternetStatus - enum specifying what the completion is for +lpvStatusInformation - completion specific information +dwStatusInformationLength - length of the above information + +Return Value: + +None + +--*/ +{ + HRESULT hr = S_OK; + BOOL fExclusiveLocked = FALSE; + BOOL fSharedLocked = FALSE; + BOOL fClientError = FALSE; + BOOL fAnotherCompletionExpected = FALSE; + BOOL fDoPostCompletion = FALSE; + BOOL fHandleClosing = (dwInternetStatus == WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING); + DWORD dwHandlers = 1; // defaullt for http handler + + + DBG_ASSERT(m_pW3Context != NULL); + __analysis_assume(m_pW3Context != NULL); + IHttpResponse * pResponse = m_pW3Context->GetResponse(); + + // Reference the request handler to prevent it from being released prematurely + ReferenceRequestHandler(); + + UNREFERENCED_PARAMETER(dwStatusInformationLength); + + if (sm_pTraceLog != NULL) + { + WriteRefTraceLogEx(sm_pTraceLog, + m_cRefs, + this, + "FORWARDING_HANDLER::OnWinHttpCompletionInternal Enter", + reinterpret_cast(static_cast(dwInternetStatus)), + NULL); + } + + //FREB log + if (ANCMEvents::ANCM_WINHTTP_CALLBACK::IsEnabled(m_pW3Context->GetTraceContext())) + { + ANCMEvents::ANCM_WINHTTP_CALLBACK::RaiseEvent( + m_pW3Context->GetTraceContext(), + NULL, + dwInternetStatus); + } + + LOG_TRACEF(L"FORWARDING_HANDLER::OnWinHttpCompletionInternal %x -- %d --%p\n", dwInternetStatus, GetCurrentThreadId(), m_pW3Context); + + // + // Exclusive lock on the winhttp handle to protect from a client disconnect/ + // server stop closing the handle while we are using it. + // + // WinHttp can call async completion on the same thread/stack, so + // we have to account for that and not try to take the lock again, + // otherwise, we could end up in a deadlock. + // + + if (TlsGetValue(g_dwTlsIndex) != this) + { + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == NULL); + if (m_RequestStatus != FORWARDER_RECEIVED_WEBSOCKET_RESPONSE) + { + // Webscoket has already been guarded by critical section + // Only require exclisive lock for non-websocket scenario which has duplex channel + // Otherwise, there will be a deadlock + AcquireLockExclusive(); + fExclusiveLocked = TRUE; + } + else + { + AcquireSRWLockShared(&m_RequestLock); + TlsSetValue(g_dwTlsIndex, this); + fSharedLocked = TRUE; + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == this); + } + } + + if (fHandleClosing) + { + dwHandlers = InterlockedDecrement(&m_dwHandlers); + } + + if (m_fFinishRequest) + { + // Request was done by another thread, skip + goto Finished; + } + + + if (m_fClientDisconnected && (m_RequestStatus != FORWARDER_DONE)) + { + hr = ERROR_CONNECTION_ABORTED; + goto Failure; + } + + // + // In case of websocket, http request handle (m_hRequest) will be closed immediately after upgrading success + // This close will trigger a callback with WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING + // As m_RequestStatus is FORWARDER_RECEIVED_WEBSOCKET_RESPONSE, this callback will be skipped. + // When WebSocket handle (m_pWebsocket) gets closed, another winhttp handle close callback will be triggered + // This callback will be captured and then notify IIS pipeline to continue + // This ensures no request leaks + // + if (m_RequestStatus == FORWARDER_RECEIVED_WEBSOCKET_RESPONSE) + { + fAnotherCompletionExpected = TRUE; + if (m_pWebSocket == NULL) + { + goto Finished; + } + + switch (dwInternetStatus) + { + case WINHTTP_CALLBACK_STATUS_SHUTDOWN_COMPLETE: + m_pWebSocket->OnWinHttpShutdownComplete(); + break; + + case WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE: + m_pWebSocket->OnWinHttpSendComplete( + (WINHTTP_WEB_SOCKET_STATUS*)lpvStatusInformation + ); + break; + + case WINHTTP_CALLBACK_STATUS_READ_COMPLETE: + m_pWebSocket->OnWinHttpReceiveComplete( + (WINHTTP_WEB_SOCKET_STATUS*)lpvStatusInformation + ); + break; + + case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR: + m_pWebSocket->OnWinHttpIoError( + (WINHTTP_WEB_SOCKET_ASYNC_RESULT*)lpvStatusInformation + ); + break; + } + goto Finished; + } + + switch (dwInternetStatus) + { + case WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE: + case WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE: + hr = OnWinHttpCompletionSendRequestOrWriteComplete(hRequest, + dwInternetStatus, + &fClientError, + &fAnotherCompletionExpected); + break; + + case WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE: + hr = OnWinHttpCompletionStatusHeadersAvailable(hRequest, + &fAnotherCompletionExpected); + break; + + case WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE: + hr = OnWinHttpCompletionStatusDataAvailable(hRequest, + *reinterpret_cast(lpvStatusInformation), // dwBytes + &fAnotherCompletionExpected); + break; + + case WINHTTP_CALLBACK_STATUS_READ_COMPLETE: + hr = OnWinHttpCompletionStatusReadComplete(pResponse, + dwStatusInformationLength, + &fAnotherCompletionExpected); + break; + + case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR: + hr = HRESULT_FROM_WIN32(static_cast(lpvStatusInformation)->dwError); + break; + + case WINHTTP_CALLBACK_STATUS_SENDING_REQUEST: + // + // This is a notification, not a completion. This notifiation happens + // during the Send Request operation. + // + fAnotherCompletionExpected = TRUE; + break; + + case WINHTTP_CALLBACK_STATUS_REQUEST_SENT: + // + // Need to ignore this event. We get it as a side-effect of registering + // for WINHTTP_CALLBACK_STATUS_SENDING_REQUEST (which we actually need). + // + hr = S_OK; + fAnotherCompletionExpected = TRUE; + break; + + case WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING: + if (ANCMEvents::ANCM_REQUEST_FORWARD_END::IsEnabled(m_pW3Context->GetTraceContext())) + { + ANCMEvents::ANCM_REQUEST_FORWARD_END::RaiseEvent( + m_pW3Context->GetTraceContext(), + NULL); + } + if (m_RequestStatus != FORWARDER_DONE) + { + hr = ERROR_CONNECTION_ABORTED; + fClientError = m_fClientDisconnected; + } + m_hRequest = NULL; + fAnotherCompletionExpected = FALSE; + break; + + case WINHTTP_CALLBACK_STATUS_CONNECTION_CLOSED: + hr = ERROR_CONNECTION_ABORTED; + break; + + default: + // + // E_UNEXPECTED is rarely used, if seen means that this condition may been occurred. + // + DBG_ASSERT(FALSE); + hr = E_UNEXPECTED; + if (sm_pTraceLog != NULL) + { + WriteRefTraceLogEx(sm_pTraceLog, + m_cRefs, + this, + "FORWARDING_HANDLER::OnWinHttpCompletionInternal Unexpected WinHTTP Status", + reinterpret_cast(static_cast(dwInternetStatus)), + NULL); + } + break; + } + + // + // Handle failure code for switch statement above. + // + if (FAILED_LOG(hr)) + { + goto Failure; + } + + // + // WinHTTP completion handled successfully. + // + goto Finished; + +Failure: + + if (!m_fHasError) + { + m_RequestStatus = FORWARDER_DONE; + m_fHasError = TRUE; + + pResponse->DisableKernelCache(); + pResponse->GetRawHttpResponse()->EntityChunkCount = 0; + + if (hr == HRESULT_FROM_WIN32(ERROR_WINHTTP_INVALID_SERVER_RESPONSE)) + { + m_fResetConnection = TRUE; + } + + if (fClientError || m_fClientDisconnected) + { + if (!m_fResponseHeadersReceivedAndSet) + { + pResponse->SetStatus(400, "Bad Request", 0, HRESULT_FROM_WIN32(WSAECONNRESET)); + } + else + { + // + // Response headers from origin server were + // already received and set for the current response. + // Honor the response status. + // + } + } + else + { + STACK_STRU(strDescription, 128); + + pResponse->SetStatus(502, "Bad Gateway", 3, hr); + + if (!(hr > HRESULT_FROM_WIN32(WINHTTP_ERROR_BASE) && + hr <= HRESULT_FROM_WIN32(WINHTTP_ERROR_LAST)) || +#pragma prefast (suppress : __WARNING_FUNCTION_NEEDS_REVIEW, "Function and parameters reviewed.") + FormatMessage( + FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_FROM_HMODULE, + g_hWinHttpModule, + HRESULT_CODE(hr), + 0, + strDescription.QueryStr(), + strDescription.QuerySizeCCH(), + NULL) == 0) + { + LoadString(g_hAspNetCoreModule, + IDS_SERVER_ERROR, + strDescription.QueryStr(), + strDescription.QuerySizeCCH()); + } + + strDescription.SyncWithBuffer(); + if (strDescription.QueryCCH() != 0) + { + pResponse->SetErrorDescription( + strDescription.QueryStr(), + strDescription.QueryCCH(), + FALSE); + } + } + } + + // FREB log + if (ANCMEvents::ANCM_REQUEST_FORWARD_FAIL::IsEnabled(m_pW3Context->GetTraceContext())) + { + ANCMEvents::ANCM_REQUEST_FORWARD_FAIL::RaiseEvent( + m_pW3Context->GetTraceContext(), + NULL, + hr); + } + +Finished: + // + // Since we use TLS to guard WinHttp operation, call PostCompletion instead of + // IndicateCompletion to allow cleaning up the TLS before thread reuse. + // Never post after the request has been finished for whatever reason + // + // Only postCompletion after all WinHttp handles (http and websocket) got closed, + // i.e., received WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING callback for both handles + // So that no further WinHttp callback will be called + // Never post completion again after that + // Otherwise, there will be a AV as the request already passed IIS pipeline + // + if (fHandleClosing && dwHandlers == 0) + { + // + // Happy path + // + // Marked the request is finished, no more PostCompletion is allowed + RemoveRequest(); + m_fFinishRequest = TRUE; + fDoPostCompletion = TRUE; + if (m_pWebSocket != NULL) + { + m_pWebSocket->Terminate(); + m_pWebSocket = NULL; + } + } + else if (m_RequestStatus == FORWARDER_DONE) + { + // + // Error path + // + RemoveRequest(); + if (m_hRequest != NULL && !m_fHttpHandleInClose) + { + m_fHttpHandleInClose = TRUE; + WinHttpCloseHandle(m_hRequest); + m_hRequest = NULL; + } + + if (m_pWebSocket != NULL && !m_fWebSocketHandleInClose) + { + m_fWebSocketHandleInClose = TRUE; + m_pWebSocket->TerminateRequest(); + } + + if (fHandleClosing) + { + fDoPostCompletion = dwHandlers == 0; + m_fFinishRequest = fDoPostCompletion; + } + } + else if (!fAnotherCompletionExpected) + { + // + // Regular async IO operation + // + fDoPostCompletion = !m_fFinishRequest; + } + + // + // No code should access IIS m_pW3Context after posting the completion. + // + if (fDoPostCompletion) + { + m_pW3Context->PostCompletion(0); + } + + if (fExclusiveLocked) + { + ReleaseLockExclusive(); + } + else if (fSharedLocked) + { + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == this); + TlsSetValue(g_dwTlsIndex, NULL); + ReleaseSRWLockShared(&m_RequestLock); + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == NULL); + } + + DereferenceRequestHandler(); + +} + +HRESULT +FORWARDING_HANDLER::OnWinHttpCompletionSendRequestOrWriteComplete( + HINTERNET hRequest, + DWORD, + __out BOOL * pfClientError, + __out BOOL * pfAnotherCompletionExpected +) +{ + HRESULT hr = S_OK; + IHttpRequest * pRequest = m_pW3Context->GetRequest(); + + // + // completion for sending the initial request or request entity to + // winhttp, get more request entity if available, else start receiving + // the response + // + if (m_BytesToReceive > 0) + { + if (m_pEntityBuffer == NULL) + { + m_pEntityBuffer = GetNewResponseBuffer( + ENTITY_BUFFER_SIZE); + if (m_pEntityBuffer == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + } + + if (sm_pTraceLog != NULL) + { + WriteRefTraceLogEx(sm_pTraceLog, + m_cRefs, + this, + "Calling ReadEntityBody", + NULL, + NULL); + } + hr = pRequest->ReadEntityBody( + m_pEntityBuffer + 6, + min(m_BytesToReceive, BUFFER_SIZE), + TRUE, // fAsync + NULL, // pcbBytesReceived + NULL); // pfCompletionPending + if (hr == HRESULT_FROM_WIN32(ERROR_HANDLE_EOF)) + { + DBG_ASSERT(m_BytesToReceive == 0 || + m_BytesToReceive == INFINITE); + + // + // ERROR_HANDLE_EOF is not an error. + // + hr = S_OK; + + if (m_BytesToReceive == INFINITE) + { + m_BytesToReceive = 0; + m_cchLastSend = 5; + + // + // WinHttpWriteData can operate asynchronously. + // + // Take reference so that object does not go away as a result of + // async completion. + // + //ReferenceForwardingHandler(); + if (!WinHttpWriteData(m_hRequest, + "0\r\n\r\n", + 5, + NULL)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + //DereferenceForwardingHandler(); + goto Finished; + } + *pfAnotherCompletionExpected = TRUE; + + goto Finished; + } + } + else if (FAILED_LOG(hr)) + { + *pfClientError = TRUE; + goto Finished; + } + else + { + // + // ReadEntityBody will post a completion to IIS. + // + *pfAnotherCompletionExpected = TRUE; + + goto Finished; + } + } + + m_RequestStatus = FORWARDER_RECEIVING_RESPONSE; + + if (!WinHttpReceiveResponse(hRequest, NULL)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + *pfAnotherCompletionExpected = TRUE; + +Finished: + + return hr; +} + +HRESULT +FORWARDING_HANDLER::OnWinHttpCompletionStatusHeadersAvailable( + HINTERNET hRequest, + __out BOOL * pfAnotherCompletionExpected +) +{ + HRESULT hr = S_OK; + STACK_BUFFER(bufHeaderBuffer, 2048); + STACK_STRA(strHeaders, 2048); + DWORD dwHeaderSize = bufHeaderBuffer.QuerySize(); + + UNREFERENCED_PARAMETER(pfAnotherCompletionExpected); + + // + // Headers are available, read the status line and headers and pass + // them on to the client + // + // WinHttpQueryHeaders operates synchronously, + // no need for taking reference. + // + dwHeaderSize = bufHeaderBuffer.QuerySize(); + if (!WinHttpQueryHeaders(hRequest, + WINHTTP_QUERY_RAW_HEADERS_CRLF, + WINHTTP_HEADER_NAME_BY_INDEX, + bufHeaderBuffer.QueryPtr(), + &dwHeaderSize, + WINHTTP_NO_HEADER_INDEX)) + { + if (!bufHeaderBuffer.Resize(dwHeaderSize)) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + // + // WinHttpQueryHeaders operates synchronously, + // no need for taking reference. + // + if (!WinHttpQueryHeaders(hRequest, + WINHTTP_QUERY_RAW_HEADERS_CRLF, + WINHTTP_HEADER_NAME_BY_INDEX, + bufHeaderBuffer.QueryPtr(), + &dwHeaderSize, + WINHTTP_NO_HEADER_INDEX)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + } + + if (FAILED_LOG(hr = strHeaders.CopyW( + reinterpret_cast(bufHeaderBuffer.QueryPtr())))) + { + goto Finished; + } + + // Issue: The reason we add trailing \r\n is to eliminate issues that have been observed + // in some configurations where status and headers would not have final \r\n nor \r\n\r\n + // (last header was null terminated).That caused crash within header parsing code that expected valid + // format. Parsing code was fized to return ERROR_INVALID_PARAMETER, but we still should make + // Example of a status+header string that was causing problems (note the missing \r\n at the end) + // HTTP/1.1 302 Moved Permanently\r\n....\r\nLocation:http://site\0 + // + + if (!strHeaders.IsEmpty() && strHeaders.QueryStr()[strHeaders.QueryCCH() - 1] != '\n') + { + hr = strHeaders.Append("\r\n"); + if (FAILED_LOG(hr)) + { + goto Finished; + } + } + + if (FAILED_LOG(hr = SetStatusAndHeaders( + strHeaders.QueryStr(), + strHeaders.QueryCCH()))) + { + goto Finished; + } + + FreeResponseBuffers(); + + // + // If the request was websocket, and response was 101, + // trigger a flush, so that IIS's websocket module + // can get a chance to initialize and complete the handshake. + // + + if (m_fWebSocketEnabled) + { + m_RequestStatus = FORWARDER_RECEIVED_WEBSOCKET_RESPONSE; + + hr = m_pW3Context->GetResponse()->Flush( + TRUE, + TRUE, + NULL, + NULL); + + if (FAILED_LOG(hr)) + { + *pfAnotherCompletionExpected = FALSE; + } + else + { + *pfAnotherCompletionExpected = TRUE; + } + } + +Finished: + + return hr; +} + +HRESULT +FORWARDING_HANDLER::OnWinHttpCompletionStatusDataAvailable( + HINTERNET hRequest, + DWORD dwBytes, + _Out_ BOOL * pfAnotherCompletionExpected +) +{ + HRESULT hr = S_OK; + + // + // Response data is available from winhttp, read it + // + if (dwBytes == 0) + { + if (m_cContentLength != 0) + { + hr = HRESULT_FROM_WIN32(ERROR_WINHTTP_INVALID_SERVER_RESPONSE); + goto Finished; + } + + m_RequestStatus = FORWARDER_DONE; + + goto Finished; + } + + m_BytesToSend = dwBytes; + if (m_cContentLength != 0) + { + m_cContentLength -= dwBytes; + } + + m_pEntityBuffer = GetNewResponseBuffer( + min(m_BytesToSend, BUFFER_SIZE)); + if (m_pEntityBuffer == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + // + // WinHttpReadData can operate asynchronously. + // + // Take reference so that object does not go away as a result of + // async completion. + // + //ReferenceForwardingHandler(); + if (!WinHttpReadData(hRequest, + m_pEntityBuffer, + min(m_BytesToSend, BUFFER_SIZE), + NULL)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + //DereferenceForwardingHandler(); + goto Finished; + } + *pfAnotherCompletionExpected = TRUE; + +Finished: + + return hr; +} + +HRESULT +FORWARDING_HANDLER::OnWinHttpCompletionStatusReadComplete( + __in IHttpResponse * pResponse, + DWORD dwStatusInformationLength, + __out BOOL * pfAnotherCompletionExpected +) +{ + HRESULT hr = S_OK; + + // + // Response data has been read from winhttp, send it to the client + // + m_BytesToSend -= dwStatusInformationLength; + + if (m_cMinBufferLimit >= BUFFER_SIZE / 2) + { + if (m_cContentLength != 0) + { + m_cContentLength -= dwStatusInformationLength; + } + + // + // If we were not using WinHttpQueryDataAvailable and winhttp + // did not fill our buffer, we must have reached the end of the + // response + // + if (dwStatusInformationLength == 0 || + m_BytesToSend != 0) + { + if (m_cContentLength != 0) + { + hr = HRESULT_FROM_WIN32(ERROR_WINHTTP_INVALID_SERVER_RESPONSE); + goto Finished; + } + + m_RequestStatus = FORWARDER_DONE; + } + } + else + { + DBG_ASSERT(dwStatusInformationLength != 0); + } + + if (dwStatusInformationLength == 0) + { + goto Finished; + } + else + { + m_cBytesBuffered += dwStatusInformationLength; + + HTTP_DATA_CHUNK Chunk; + Chunk.DataChunkType = HttpDataChunkFromMemory; + Chunk.FromMemory.pBuffer = m_pEntityBuffer; + Chunk.FromMemory.BufferLength = dwStatusInformationLength; + if (FAILED_LOG(hr = pResponse->WriteEntityChunkByReference(&Chunk))) + { + goto Finished; + } + } + + if (m_cBytesBuffered >= m_cMinBufferLimit) + { + // + // Always post a completion to resume the WinHTTP data pump. + // + hr = pResponse->Flush(TRUE, // fAsync + TRUE, // fMoreData + NULL); // pcbSent + if (FAILED_LOG(hr)) + { + goto Finished; + } + *pfAnotherCompletionExpected = TRUE; + } + else + { + *pfAnotherCompletionExpected = FALSE; + } + +Finished: + + return hr; +} + +HRESULT +FORWARDING_HANDLER::OnSendingRequest( + DWORD cbCompletion, + HRESULT hrCompletionStatus, + __out BOOL * pfClientError +) +{ + HRESULT hr = S_OK; + // + // This is a completion for a read from http.sys, abort in case + // of failure, if we read anything write it out over WinHTTP, + // but we have already reached EOF, now read the response + // + if (hrCompletionStatus == HRESULT_FROM_WIN32(ERROR_HANDLE_EOF)) + { + DBG_ASSERT(m_BytesToReceive == 0 || m_BytesToReceive == INFINITE); + if (m_BytesToReceive == INFINITE) + { + m_BytesToReceive = 0; + m_cchLastSend = 5; // "0\r\n\r\n" + + if (!WinHttpWriteData(m_hRequest, + "0\r\n\r\n", + 5, + NULL)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Failure; + } + } + else + { + m_RequestStatus = FORWARDER_RECEIVING_RESPONSE; + + if (!WinHttpReceiveResponse(m_hRequest, NULL)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Failure; + } + } + } + else if (SUCCEEDED(hrCompletionStatus)) + { + DWORD cbOffset; + + if (m_BytesToReceive != INFINITE) + { + m_BytesToReceive -= cbCompletion; + cbOffset = 6; + } + else + { + // + // For chunk-encoded requests, need to re-chunk the entity body + // Add the CRLF just before and after the chunk data + // + m_pEntityBuffer[4] = '\r'; + m_pEntityBuffer[5] = '\n'; + + m_pEntityBuffer[cbCompletion + 6] = '\r'; + m_pEntityBuffer[cbCompletion + 7] = '\n'; + + if (cbCompletion < 0x10) + { + cbOffset = 3; + m_pEntityBuffer[3] = HEX_TO_ASCII(cbCompletion); + cbCompletion += 5; + } + else if (cbCompletion < 0x100) + { + cbOffset = 2; + m_pEntityBuffer[2] = HEX_TO_ASCII(cbCompletion >> 4); + m_pEntityBuffer[3] = HEX_TO_ASCII(cbCompletion & 0xf); + cbCompletion += 6; + } + else if (cbCompletion < 0x1000) + { + cbOffset = 1; + m_pEntityBuffer[1] = HEX_TO_ASCII(cbCompletion >> 8); + m_pEntityBuffer[2] = HEX_TO_ASCII((cbCompletion >> 4) & 0xf); + m_pEntityBuffer[3] = HEX_TO_ASCII(cbCompletion & 0xf); + cbCompletion += 7; + } + else + { + DBG_ASSERT(cbCompletion < 0x10000); + + cbOffset = 0; + m_pEntityBuffer[0] = HEX_TO_ASCII(cbCompletion >> 12); + m_pEntityBuffer[1] = HEX_TO_ASCII((cbCompletion >> 8) & 0xf); + m_pEntityBuffer[2] = HEX_TO_ASCII((cbCompletion >> 4) & 0xf); + m_pEntityBuffer[3] = HEX_TO_ASCII(cbCompletion & 0xf); + cbCompletion += 8; + } + } + m_cchLastSend = cbCompletion; + + if (!WinHttpWriteData(m_hRequest, + m_pEntityBuffer + cbOffset, + cbCompletion, + NULL)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Failure; + } + } + else + { + hr = hrCompletionStatus; + *pfClientError = TRUE; + goto Failure; + } + +Failure: + + return hr; +} + +HRESULT +FORWARDING_HANDLER::OnReceivingResponse( +) +{ + HRESULT hr = S_OK; + + if (m_cBytesBuffered >= m_cMinBufferLimit) + { + FreeResponseBuffers(); + } + + if (m_BytesToSend == 0) + { + // + // If response buffering is enabled, try to read large chunks + // at a time - also treat very small buffering limit as no + // buffering + // + m_BytesToSend = min(m_cMinBufferLimit, BUFFER_SIZE); + if (m_BytesToSend < BUFFER_SIZE / 2) + { + // + // Disable buffering. + // + m_BytesToSend = 0; + } + } + + if (m_BytesToSend == 0) + { + // + // No buffering enabled. + // + if (!WinHttpQueryDataAvailable(m_hRequest, NULL)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Failure; + } + } + else + { + // + // Buffering enabled. + // + if (m_pEntityBuffer == NULL) + { + m_pEntityBuffer = GetNewResponseBuffer(min(m_BytesToSend, BUFFER_SIZE)); + if (m_pEntityBuffer == NULL) + { + hr = E_OUTOFMEMORY; + goto Failure; + } + } + + if (!WinHttpReadData(m_hRequest, + m_pEntityBuffer, + min(m_BytesToSend, BUFFER_SIZE), + NULL)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Failure; + } + } + +Failure: + return hr; +} + +BYTE * +FORWARDING_HANDLER::GetNewResponseBuffer( + DWORD dwBufferSize +) +{ + DWORD dwNeededSize = (m_cEntityBuffers + 1) * sizeof(BYTE *); + if (dwNeededSize > m_buffEntityBuffers.QuerySize() && + !m_buffEntityBuffers.Resize( + max(dwNeededSize, m_buffEntityBuffers.QuerySize() * 2))) + { + return NULL; + } + + BYTE *pBuffer = (BYTE *)HeapAlloc(GetProcessHeap(), + 0, // dwFlags + dwBufferSize); + if (pBuffer == NULL) + { + return NULL; + } + + m_buffEntityBuffers.QueryPtr()[m_cEntityBuffers] = pBuffer; + m_cEntityBuffers++; + + return pBuffer; +} + +VOID +FORWARDING_HANDLER::FreeResponseBuffers() +{ + BYTE **pBuffers = m_buffEntityBuffers.QueryPtr(); + for (DWORD i = 0; iGetResponse(); + IHttpRequest * pRequest = m_pW3Context->GetRequest(); + STACK_STRA(strHeaderName, 128); + STACK_STRA(strHeaderValue, 2048); + DWORD index = 0; + PSTR pchNewline; + PCSTR pchEndofHeaderValue; + BOOL fServerHeaderPresent = FALSE; + + _ASSERT(pszHeaders != NULL); + + // + // The first line is the status line + // + PSTR pchStatus = const_cast(strchr(pszHeaders, ' ')); + if (pchStatus == NULL) + { + return HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER); + } + while (*pchStatus == ' ') + { + pchStatus++; + } + USHORT uStatus = static_cast(atoi(pchStatus)); + + if (m_fWebSocketEnabled && uStatus != 101) + { + // + // Expected 101 response. + // + + m_fWebSocketEnabled = FALSE; + } + + pchStatus = strchr(pchStatus, ' '); + if (pchStatus == NULL) + { + return HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER); + } + while (*pchStatus == ' ') + { + pchStatus++; + } + if (*pchStatus == '\r' || *pchStatus == '\n') + { + pchStatus--; + } + + pchNewline = strchr(pchStatus, '\n'); + if (pchNewline == NULL) + { + return HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER); + } + + if (uStatus != 200) + { + // + // Skip over any spaces before the '\n' + // + for (pchEndofHeaderValue = pchNewline - 1; + (pchEndofHeaderValue > pchStatus) && + ((*pchEndofHeaderValue == ' ') || + (*pchEndofHeaderValue == '\r')); + pchEndofHeaderValue--) + { + } + + // + // Copy the status description + // + if (FAILED_LOG(hr = strHeaderValue.Copy( + pchStatus, + (DWORD)(pchEndofHeaderValue - pchStatus) + 1)) || + FAILED_LOG(hr = pResponse->SetStatus(uStatus, + strHeaderValue.QueryStr(), + 0, + S_OK, + NULL, + TRUE))) + { + return hr; + } + } + + for (index = static_cast(pchNewline - pszHeaders) + 1; + pszHeaders[index] != '\r' && pszHeaders[index] != '\n' && pszHeaders[index] != '\0'; + index = static_cast(pchNewline - pszHeaders) + 1) + { + // + // Find the ':' in Header : Value\r\n + // + PCSTR pchColon = strchr(pszHeaders + index, ':'); + + // + // Find the '\n' in Header : Value\r\n + // + pchNewline = const_cast(strchr(pszHeaders + index, '\n')); + + if (pchNewline == NULL) + { + return HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER); + } + + // + // Take care of header continuation + // + while (pchNewline[1] == ' ' || + pchNewline[1] == '\t') + { + pchNewline = strchr(pchNewline + 1, '\n'); + } + + DBG_ASSERT( + (pchColon != NULL) && (pchColon < pchNewline)); + if ((pchColon == NULL) || (pchColon >= pchNewline)) + { + return HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER); + } + + // + // Skip over any spaces before the ':' + // + PCSTR pchEndofHeaderName; + for (pchEndofHeaderName = pchColon - 1; + (pchEndofHeaderName >= pszHeaders + index) && + (*pchEndofHeaderName == ' '); + pchEndofHeaderName--) + { + } + + pchEndofHeaderName++; + + // + // Copy the header name + // + if (FAILED_LOG(hr = strHeaderName.Copy( + pszHeaders + index, + (DWORD)(pchEndofHeaderName - pszHeaders) - index))) + { + return hr; + } + + // + // Skip over the ':' and any trailing spaces + // + for (index = static_cast(pchColon - pszHeaders) + 1; + pszHeaders[index] == ' '; + index++) + { + } + + // + // Skip over any spaces before the '\n' + // + for (pchEndofHeaderValue = pchNewline - 1; + (pchEndofHeaderValue >= pszHeaders + index) && + ((*pchEndofHeaderValue == ' ') || + (*pchEndofHeaderValue == '\r')); + pchEndofHeaderValue--) + { + } + + pchEndofHeaderValue++; + + // + // Copy the header value + // + if (pchEndofHeaderValue == pszHeaders + index) + { + strHeaderValue.Reset(); + } + else if (FAILED_LOG(hr = strHeaderValue.Copy( + pszHeaders + index, + (DWORD)(pchEndofHeaderValue - pszHeaders) - index))) + { + return hr; + } + + // + // Do not pass the transfer-encoding:chunked, Connection, Date or + // Server headers along + // + DWORD headerIndex = sm_pResponseHeaderHash->GetIndex(strHeaderName.QueryStr()); + if (headerIndex == UNKNOWN_INDEX) + { + hr = pResponse->SetHeader(strHeaderName.QueryStr(), + strHeaderValue.QueryStr(), + static_cast(strHeaderValue.QueryCCH()), + FALSE); // fReplace + } + else + { + switch (headerIndex) + { + case HttpHeaderTransferEncoding: + if (!strHeaderValue.Equals("chunked", TRUE)) + { + break; + } + __fallthrough; + case HttpHeaderConnection: + case HttpHeaderDate: + continue; + + case HttpHeaderServer: + fServerHeaderPresent = TRUE; + break; + + case HttpHeaderContentLength: + if (pRequest->GetRawHttpRequest()->Verb != HttpVerbHEAD) + { + m_cContentLength = _atoi64(strHeaderValue.QueryStr()); + } + break; + } + + hr = pResponse->SetHeader(static_cast(headerIndex), + strHeaderValue.QueryStr(), + static_cast(strHeaderValue.QueryCCH()), + TRUE); // fReplace + } + if (FAILED_LOG(hr)) + { + return hr; + } + } + + // + // Explicitly remove the Server header if the back-end didn't set one. + // + + if (!fServerHeaderPresent) + { + pResponse->DeleteHeader("Server"); + } + + if (m_fDoReverseRewriteHeaders) + { + hr = DoReverseRewrite(pResponse); + if (FAILED_LOG(hr)) + { + return hr; + } + } + + m_fResponseHeadersReceivedAndSet = TRUE; + + return S_OK; +} + +HRESULT +FORWARDING_HANDLER::DoReverseRewrite( + _In_ IHttpResponse *pResponse +) +{ + DBG_ASSERT(pResponse == m_pW3Context->GetResponse()); + BOOL fSecure = (m_pW3Context->GetRequest()->GetRawHttpRequest()->pSslInfo != NULL); + STRA strTemp; + PCSTR pszHeader; + PCSTR pszStartHost; + PCSTR pszEndHost; + HTTP_RESPONSE_HEADERS *pHeaders; + HRESULT hr; + + // + // Content-Location and Location are easy, one known header in + // http[s]://host/url format + // + pszHeader = pResponse->GetHeader(HttpHeaderContentLocation); + if (pszHeader != NULL) + { + if (_strnicmp(pszHeader, "http://", 7) == 0) + { + pszStartHost = pszHeader + 7; + } + else if (_strnicmp(pszHeader, "https://", 8) == 0) + { + pszStartHost = pszHeader + 8; + } + else + { + goto Location; + } + + pszEndHost = strchr(pszStartHost, '/'); + + if (FAILED_LOG(hr = strTemp.Copy(fSecure ? "https://" : "http://")) || + FAILED_LOG(hr = strTemp.Append(m_pszOriginalHostHeader))) + { + return hr; + } + if (pszEndHost != NULL && + FAILED_LOG(hr = strTemp.Append(pszEndHost))) + { + return hr; + } + if (FAILED_LOG(hr = pResponse->SetHeader(HttpHeaderContentLocation, + strTemp.QueryStr(), + static_cast(strTemp.QueryCCH()), + TRUE))) + { + return hr; + } + } + +Location: + + pszHeader = pResponse->GetHeader(HttpHeaderLocation); + if (pszHeader != NULL) + { + if (_strnicmp(pszHeader, "http://", 7) == 0) + { + pszStartHost = pszHeader + 7; + } + else if (_strnicmp(pszHeader, "https://", 8) == 0) + { + pszStartHost = pszHeader + 8; + } + else + { + goto SetCookie; + } + + pszEndHost = strchr(pszStartHost, '/'); + + if (FAILED_LOG(hr = strTemp.Copy(fSecure ? "https://" : "http://")) || + FAILED_LOG(hr = strTemp.Append(m_pszOriginalHostHeader))) + { + return hr; + } + if (pszEndHost != NULL && + FAILED_LOG(hr = strTemp.Append(pszEndHost))) + { + return hr; + } + if (FAILED_LOG(hr = pResponse->SetHeader(HttpHeaderLocation, + strTemp.QueryStr(), + static_cast(strTemp.QueryCCH()), + TRUE))) + { + return hr; + } + } + +SetCookie: + + // + // Set-Cookie is different - possibly multiple unknown headers with + // syntax name=value ; ... ; Domain=.host ; ... + // + pHeaders = &pResponse->GetRawHttpResponse()->Headers; + for (DWORD i = 0; iUnknownHeaderCount; i++) + { + if (_stricmp(pHeaders->pUnknownHeaders[i].pName, "Set-Cookie") != 0) + { + continue; + } + + pszHeader = pHeaders->pUnknownHeaders[i].pRawValue; + pszStartHost = strchr(pszHeader, ';'); + while (pszStartHost != NULL) + { + pszStartHost++; + while (IsSpace(*pszStartHost)) + { + pszStartHost++; + } + + if (_strnicmp(pszStartHost, "Domain", 6) != 0) + { + pszStartHost = strchr(pszStartHost, ';'); + continue; + } + pszStartHost += 6; + + while (IsSpace(*pszStartHost)) + { + pszStartHost++; + } + if (*pszStartHost != '=') + { + break; + } + pszStartHost++; + while (IsSpace(*pszStartHost)) + { + pszStartHost++; + } + if (*pszStartHost == '.') + { + pszStartHost++; + } + pszEndHost = pszStartHost; + while (!IsSpace(*pszEndHost) && + *pszEndHost != ';' && + *pszEndHost != '\0') + { + pszEndHost++; + } + + if (FAILED_LOG(hr = strTemp.Copy(pszHeader, static_cast(pszStartHost - pszHeader))) || + FAILED_LOG(hr = strTemp.Append(m_pszOriginalHostHeader)) || + FAILED_LOG(hr = strTemp.Append(pszEndHost))) + { + return hr; + } + + pszHeader = (PCSTR)m_pW3Context->AllocateRequestMemory(strTemp.QueryCCH() + 1); + if (pszHeader == NULL) + { + return E_OUTOFMEMORY; + } + StringCchCopyA(const_cast(pszHeader), strTemp.QueryCCH() + 1, strTemp.QueryStr()); + pHeaders->pUnknownHeaders[i].pRawValue = pszHeader; + pHeaders->pUnknownHeaders[i].RawValueLength = static_cast(strTemp.QueryCCH()); + + break; + } + } + + return S_OK; +} + +VOID +FORWARDING_HANDLER::RemoveRequest( + VOID +) +{ + m_fReactToDisconnect = FALSE; +} + +VOID +FORWARDING_HANDLER::NotifyDisconnect() +{ + if (!m_fReactToDisconnect) + { + return; + } + + BOOL fLocked = FALSE; + if (TlsGetValue(g_dwTlsIndex) != this) + { + // + // Acquire exclusive lock as WinHTTP callback may happen on different thread + // We don't want two threads signal IIS pipeline simultaneously + // + AcquireLockExclusive(); + fLocked = TRUE; + } + + // Set tls as close winhttp handle will immediately trigger + // a winhttp callback on the same thread and we donot want to + // acquire the lock again + + LOG_TRACEF(L"FORWARDING_HANDLER::TerminateRequest %d --%p\n", GetCurrentThreadId(), m_pW3Context); + + if (!m_fHttpHandleInClose) + { + m_fClientDisconnected = true; + } + + if (fLocked) + { + ReleaseLockExclusive(); + } +} + +VOID +FORWARDING_HANDLER::AcquireLockExclusive() +{ + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == NULL); + AcquireSRWLockExclusive(&m_RequestLock); + TlsSetValue(g_dwTlsIndex, this); + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == this); +} + +VOID +FORWARDING_HANDLER::ReleaseLockExclusive() +{ + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == this); + TlsSetValue(g_dwTlsIndex, NULL); + ReleaseSRWLockExclusive(&m_RequestLock); + DBG_ASSERT(TlsGetValue(g_dwTlsIndex) == NULL); +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/forwardinghandler.h b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/forwardinghandler.h new file mode 100644 index 0000000000..cc855dfcf2 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/forwardinghandler.h @@ -0,0 +1,241 @@ +#pragma once + +extern DWORD g_OptionalWinHttpFlags; +extern HINSTANCE g_hOutOfProcessRHModule; +extern HINSTANCE g_hWinHttpModule; +extern HINSTANCE g_hAspNetCoreModule; + +class OUT_OF_PROCESS_APPLICATION; + +enum FORWARDING_REQUEST_STATUS +{ + FORWARDER_START, + FORWARDER_SENDING_REQUEST, + FORWARDER_RECEIVING_RESPONSE, + FORWARDER_RECEIVED_WEBSOCKET_RESPONSE, + FORWARDER_DONE, + FORWARDER_FINISH_REQUEST +}; + + +class FORWARDING_HANDLER : public REQUEST_HANDLER +{ +public: + FORWARDING_HANDLER( + _In_ IHttpContext *pW3Context, + _In_ std::unique_ptr pApplication + ); + + ~FORWARDING_HANDLER(); + + __override + REQUEST_NOTIFICATION_STATUS + ExecuteRequestHandler(); + + __override + REQUEST_NOTIFICATION_STATUS + AsyncCompletion( + DWORD cbCompletion, + HRESULT hrCompletionStatus + ); + + VOID + SetStatus( + FORWARDING_REQUEST_STATUS status + ) + { + m_RequestStatus = status; + } + + static + VOID + CALLBACK + FORWARDING_HANDLER::OnWinHttpCompletion( + HINTERNET hRequest, + DWORD_PTR dwContext, + DWORD dwInternetStatus, + LPVOID lpvStatusInformation, + DWORD dwStatusInformationLength + ); + + static + HRESULT + StaticInitialize( + BOOL fEnableReferenceCountTracing + ); + + static + VOID + StaticTerminate(); + + VOID + NotifyDisconnect() override; + + static void * operator new(size_t size); + + static void operator delete(void * pMemory); + +private: + + VOID + AcquireLockExclusive(); + + VOID + ReleaseLockExclusive(); + + HRESULT + CreateWinHttpRequest( + _In_ const IHttpRequest * pRequest, + _In_ const PROTOCOL_CONFIG * pProtocol, + _In_ HINTERNET hConnect, + _Inout_ STRU * pstrUrl, + _In_ SERVER_PROCESS* pServerProcess + ); + + VOID + FORWARDING_HANDLER::OnWinHttpCompletionInternal( + _In_ HINTERNET hRequest, + _In_ DWORD dwInternetStatus, + _In_ LPVOID lpvStatusInformation, + _In_ DWORD dwStatusInformationLength + ); + + HRESULT + OnWinHttpCompletionSendRequestOrWriteComplete( + HINTERNET hRequest, + DWORD dwInternetStatus, + _Out_ BOOL * pfClientError, + _Out_ BOOL * pfAnotherCompletionExpected + ); + + HRESULT + OnWinHttpCompletionStatusHeadersAvailable( + HINTERNET hRequest, + _Out_ BOOL * pfAnotherCompletionExpected + ); + + HRESULT + OnWinHttpCompletionStatusDataAvailable( + HINTERNET hRequest, + DWORD dwBytes, + _Out_ BOOL * pfAnotherCompletionExpected + ); + + HRESULT + OnWinHttpCompletionStatusReadComplete( + _In_ IHttpResponse * pResponse, + DWORD dwStatusInformationLength, + _Out_ BOOL * pfAnotherCompletionExpected + ); + + HRESULT + OnSendingRequest( + DWORD cbCompletion, + HRESULT hrCompletionStatus, + _Out_ BOOL * pfClientError + ); + + HRESULT + OnReceivingResponse(); + + BYTE * + GetNewResponseBuffer( + DWORD dwBufferSize + ); + + VOID + FreeResponseBuffers(); + + HRESULT + SetStatusAndHeaders( + PCSTR pszHeaders, + DWORD cchHeaders + ); + + HRESULT + DoReverseRewrite( + _In_ IHttpResponse *pResponse + ); + + HRESULT + GetHeaders( + _In_ const PROTOCOL_CONFIG * pProtocol, + _In_ BOOL fForwardWindowsAuthToken, + _In_ SERVER_PROCESS* pServerProcess, + _Out_ PCWSTR * ppszHeaders, + _Inout_ DWORD * pcchHeaders + ); + + VOID + RemoveRequest( + VOID + ); + + DWORD m_Signature; + // + // WinHTTP request handle is protected using a read-write lock. + // + SRWLOCK m_RequestLock; + HINTERNET m_hRequest; + FORWARDING_REQUEST_STATUS m_RequestStatus; + + BOOL m_fWebSocketEnabled; + BOOL m_fWebSocketSupported; + BOOL m_fResponseHeadersReceivedAndSet; + BOOL m_fResetConnection; + BOOL m_fDoReverseRewriteHeaders; + BOOL m_fServerResetConn; + volatile BOOL m_fClientDisconnected; + // + // A safety guard flag indicating no more IIS PostCompletion is allowed + // + volatile BOOL m_fFinishRequest; + // + // A safety guard flag to prevent from unexpect callback which may signal IIS pipeline + // more than once with non-pending status + // + volatile BOOL m_fDoneAsyncCompletion; + volatile BOOL m_fHasError; + // + // WinHttp may hit AV under race if handle got closed more than once simultaneously + // Use two bool variables to guard + // + volatile BOOL m_fHttpHandleInClose; + volatile BOOL m_fWebSocketHandleInClose; + + PCSTR m_pszOriginalHostHeader; + PCWSTR m_pszHeaders; + // + // Record the number of winhttp handles in use + // release IIS pipeline only after all handles got closed + // + volatile LONG m_dwHandlers; + DWORD m_cchHeaders; + DWORD m_BytesToReceive; + DWORD m_BytesToSend; + DWORD m_cchLastSend; + DWORD m_cEntityBuffers; + DWORD m_cBytesBuffered; + DWORD m_cMinBufferLimit; + ULONGLONG m_cContentLength; + WEBSOCKET_HANDLER * m_pWebSocket; + + BYTE * m_pEntityBuffer; + static const SIZE_T INLINE_ENTITY_BUFFERS = 8; + BUFFER_T m_buffEntityBuffers; + + static ALLOC_CACHE_HANDLER * sm_pAlloc; + static PROTOCOL_CONFIG sm_ProtocolConfig; + static RESPONSE_HEADER_HASH * sm_pResponseHeaderHash; + // + // Reference cout tracing for debugging purposes. + // + static TRACE_LOG * sm_pTraceLog; + + static STRA sm_pStra502ErrorMsg; + + mutable LONG m_cRefs; + IHttpContext* m_pW3Context; + std::unique_ptr m_pApplication; + bool m_fReactToDisconnect; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/outofprocessrequesthandler.rc b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/outofprocessrequesthandler.rc new file mode 100644 index 0000000000..654496a8c5 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/outofprocessrequesthandler.rc @@ -0,0 +1,119 @@ +// Microsoft Visual C++ generated resource script. +// +#include +#include "version.h" +#include "..\CommonLib\resources.h" +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#define FileDescription "IIS ASP.NET Core Module V2 Request Handler. Commit: " CommitHash + +///////////////////////////////////////////////////////////////////////////// +// +// 11 +// + +//1 11 +//BEGIN +// 0x0001, 0x0000, 0x03e8, 0x0000, 0x03ed, 0x0000, 0x0010, 0x0000, 0x0010, +// 0x0001, 0x0025, 0x0031, 0x000d, 0x000a, 0x0000, 0x0000, 0x0010, 0x0001, +// 0x0025, 0x0031, 0x000d, 0x000a, 0x0000, 0x0000, 0x0010, 0x0001, 0x0025, +// 0x0031, 0x000d, 0x000a, 0x0000, 0x0000, 0x0010, 0x0001, 0x0025, 0x0031, +// 0x000d, 0x000a, 0x0000, 0x0000, 0x0010, 0x0001, 0x0025, 0x0031, 0x000d, +// 0x000a, 0x0000, 0x0000, 0x0010, 0x0001, 0x0025, 0x0031, 0x000d, 0x000a, +// 0x0000, 0x0000 +//END + + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "..\CommonLib\resources.h\0" +END + +2 TEXTINCLUDE +BEGIN + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION FileVersion + PRODUCTVERSION ProductVersion + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x40004L + FILETYPE 0x2L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", "Microsoft" + VALUE "FileDescription", FileDescription + VALUE "FileVersion", FileVersionStr + VALUE "InternalName", "aspnetcorev2_outofprocess.dll" + VALUE "LegalCopyright", "Copyright (C) Microsoft Corporation" + VALUE "OriginalFilename", "aspnetcorev2_outofprocess.dll" + VALUE "ProductName", "ASP.NET Core Module Request Handler" + VALUE "ProductVersion", ProductVersionStr + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END + + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + +STRINGTABLE +BEGIN + IDS_INVALID_PROPERTY "Property name '%s' in system.webServer/aspNetCore section has invalid value '%s' which does not conform to the prescribed format" + IDS_SERVER_ERROR "There was a connection error while trying to route the request." +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/outprocessapplication.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/outprocessapplication.cpp new file mode 100644 index 0000000000..22d0da50c9 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/outprocessapplication.cpp @@ -0,0 +1,104 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "outprocessapplication.h" + +#include "SRWExclusiveLock.h" +#include "exceptions.h" + +OUT_OF_PROCESS_APPLICATION::OUT_OF_PROCESS_APPLICATION( + IHttpApplication& pApplication, + std::unique_ptr pConfig) : + AppOfflineTrackingApplication(pApplication), + m_fWebSocketSupported(WEBSOCKET_STATUS::WEBSOCKET_UNKNOWN), + m_pConfig(std::move(pConfig)) +{ + m_pProcessManager = NULL; +} + +OUT_OF_PROCESS_APPLICATION::~OUT_OF_PROCESS_APPLICATION() +{ + SRWExclusiveLock lock(m_stateLock); + if (m_pProcessManager != NULL) + { + m_pProcessManager->Shutdown(); + m_pProcessManager->DereferenceProcessManager(); + m_pProcessManager = NULL; + } +} + +HRESULT +OUT_OF_PROCESS_APPLICATION::Initialize( +) +{ + if (m_pProcessManager == NULL) + { + m_pProcessManager = new PROCESS_MANAGER(); + RETURN_IF_FAILED(m_pProcessManager->Initialize()); + } + return S_OK; +} + +HRESULT +OUT_OF_PROCESS_APPLICATION::GetProcess( + _Out_ SERVER_PROCESS **ppServerProcess +) +{ + return m_pProcessManager->GetProcess(m_pConfig.get(), QueryWebsocketStatus(), ppServerProcess); +} + +__override +VOID +OUT_OF_PROCESS_APPLICATION::StopInternal(bool fServerInitiated) +{ + AppOfflineTrackingApplication::StopInternal(fServerInitiated); + + if (m_pProcessManager != NULL) + { + m_pProcessManager->Shutdown(); + } +} + +HRESULT +OUT_OF_PROCESS_APPLICATION::CreateHandler( + _In_ IHttpContext *pHttpContext, + _Out_ IREQUEST_HANDLER **pRequestHandler) +{ + IREQUEST_HANDLER* pHandler = NULL; + + //add websocket check here + if (m_fWebSocketSupported == WEBSOCKET_STATUS::WEBSOCKET_UNKNOWN) + { + SetWebsocketStatus(pHttpContext); + } + + pHandler = new FORWARDING_HANDLER(pHttpContext, ::ReferenceApplication(this)); + *pRequestHandler = pHandler; + return S_OK; +} + +VOID +OUT_OF_PROCESS_APPLICATION::SetWebsocketStatus( + IHttpContext* pHttpContext +) +{ + // Even though the applicationhost.config file contains the websocket element, + // the websocket module may still not be enabled. + PCWSTR pszTempWebsocketValue; + DWORD cbLength; + + if (FAILED_LOG(pHttpContext->GetServerVariable("WEBSOCKET_VERSION", &pszTempWebsocketValue, &cbLength))) + { + m_fWebSocketSupported = WEBSOCKET_STATUS::WEBSOCKET_NOT_SUPPORTED; + } + else + { + m_fWebSocketSupported = WEBSOCKET_STATUS::WEBSOCKET_SUPPORTED; + } +} + +BOOL +OUT_OF_PROCESS_APPLICATION::QueryWebsocketStatus() const +{ + return m_fWebSocketSupported == WEBSOCKET_STATUS::WEBSOCKET_SUPPORTED; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/outprocessapplication.h b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/outprocessapplication.h new file mode 100644 index 0000000000..43eca31f95 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/outprocessapplication.h @@ -0,0 +1,63 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#pragma once + +#include "AppOfflineTrackingApplication.h" + +class OUT_OF_PROCESS_APPLICATION : public AppOfflineTrackingApplication +{ + enum WEBSOCKET_STATUS + { + WEBSOCKET_UNKNOWN = 0, + WEBSOCKET_NOT_SUPPORTED, + WEBSOCKET_SUPPORTED, + }; + +public: + OUT_OF_PROCESS_APPLICATION( + IHttpApplication& pApplication, + std::unique_ptr pConfig); + + __override + ~OUT_OF_PROCESS_APPLICATION() override; + + HRESULT + Initialize(); + + HRESULT + GetProcess( + _Out_ SERVER_PROCESS **ppServerProcess + ); + + __override + VOID + StopInternal(bool fServerInitiated) + override; + + __override + HRESULT + CreateHandler( + _In_ IHttpContext *pHttpContext, + _Out_ IREQUEST_HANDLER **pRequestHandler) + override; + + BOOL + QueryWebsocketStatus() + const; + + REQUESTHANDLER_CONFIG* QueryConfig() + { + return m_pConfig.get(); + } + +private: + + VOID SetWebsocketStatus(IHttpContext *pHttpContext); + + PROCESS_MANAGER * m_pProcessManager; + IHttpServer *m_pHttpServer; + + WEBSOCKET_STATUS m_fWebSocketSupported; + std::unique_ptr m_pConfig; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/processmanager.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/processmanager.cpp new file mode 100644 index 0000000000..71d106fb77 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/processmanager.cpp @@ -0,0 +1,180 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "processmanager.h" +#include "EventLog.h" +#include "exceptions.h" +#include "SRWSharedLock.h" + +volatile BOOL PROCESS_MANAGER::sm_fWSAStartupDone = FALSE; + +HRESULT +PROCESS_MANAGER::Initialize( + VOID +) +{ + WSADATA wsaData; + int result; + + if( !sm_fWSAStartupDone ) + { + auto lock = SRWExclusiveLock(m_srwLock); + + if( !sm_fWSAStartupDone ) + { + if( (result = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0 ) + { + RETURN_HR(HRESULT_FROM_WIN32( result )); + } + sm_fWSAStartupDone = TRUE; + } + } + + m_dwRapidFailTickStart = GetTickCount(); + + if( m_hNULHandle == NULL ) + { + SECURITY_ATTRIBUTES saAttr; + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = TRUE; + saAttr.lpSecurityDescriptor = NULL; + + m_hNULHandle = CreateFileW( L"NUL", + FILE_WRITE_DATA, + FILE_SHARE_READ, + &saAttr, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + NULL ); + RETURN_LAST_ERROR_IF( m_hNULHandle == INVALID_HANDLE_VALUE ); + } + + return S_OK; +} + +PROCESS_MANAGER::~PROCESS_MANAGER() +{ +} + +HRESULT +PROCESS_MANAGER::GetProcess( + _In_ REQUESTHANDLER_CONFIG *pConfig, + _In_ BOOL fWebsocketSupported, + _Out_ SERVER_PROCESS **ppServerProcess +) +{ + DWORD dwProcessIndex = 0; + std::unique_ptr pSelectedServerProcess; + + if (InterlockedCompareExchange(&m_lStopping, 1L, 1L) == 1L) + { + RETURN_IF_FAILED(E_APPLICATION_EXITING); + } + + if (!m_fServerProcessListReady) + { + auto lock = SRWExclusiveLock(m_srwLock); + + if (!m_fServerProcessListReady) + { + m_dwProcessesPerApplication = pConfig->QueryProcessesPerApplication(); + m_ppServerProcessList = new SERVER_PROCESS*[m_dwProcessesPerApplication]; + + for (DWORD i = 0; i < m_dwProcessesPerApplication; ++i) + { + m_ppServerProcessList[i] = NULL; + } + } + m_fServerProcessListReady = TRUE; + } + + { + auto lock = SRWSharedLock(m_srwLock); + + // + // round robin through to the next available process. + // + dwProcessIndex = (DWORD)InterlockedIncrement64((LONGLONG*)&m_dwRouteToProcessIndex); + dwProcessIndex = dwProcessIndex % m_dwProcessesPerApplication; + + if (m_ppServerProcessList[dwProcessIndex] != NULL && + m_ppServerProcessList[dwProcessIndex]->IsReady()) + { + *ppServerProcess = m_ppServerProcessList[dwProcessIndex]; + return S_OK; + } + } + + // should make the lock per process so that we can start processes simultaneously ? + if (m_ppServerProcessList[dwProcessIndex] == NULL || + !m_ppServerProcessList[dwProcessIndex]->IsReady()) + { + auto lock = SRWExclusiveLock(m_srwLock); + + if (m_ppServerProcessList[dwProcessIndex] != NULL) + { + if (!m_ppServerProcessList[dwProcessIndex]->IsReady()) + { + // + // terminate existing process that is not ready + // before creating new one. + // + ShutdownProcessNoLock( m_ppServerProcessList[dwProcessIndex] ); + } + else + { + // server is already up and ready to serve requests. + //m_ppServerProcessList[dwProcessIndex]->ReferenceServerProcess(); + *ppServerProcess = m_ppServerProcessList[dwProcessIndex]; + return S_OK; + } + } + + if (RapidFailsPerMinuteExceeded(pConfig->QueryRapidFailsPerMinute())) + { + // + // rapid fails per minute exceeded, do not create new process. + // + EventLog::Info( + ASPNETCORE_EVENT_RAPID_FAIL_COUNT_EXCEEDED, + ASPNETCORE_EVENT_RAPID_FAIL_COUNT_EXCEEDED_MSG, + pConfig->QueryRapidFailsPerMinute()); + + RETURN_HR(HRESULT_FROM_WIN32(ERROR_SERVER_DISABLED)); + } + + if (m_ppServerProcessList[dwProcessIndex] == NULL) + { + + pSelectedServerProcess = std::make_unique(); + RETURN_IF_FAILED(pSelectedServerProcess->Initialize( + this, //ProcessManager + pConfig->QueryProcessPath(), // + pConfig->QueryArguments(), // + pConfig->QueryStartupTimeLimitInMS(), + pConfig->QueryShutdownTimeLimitInMS(), + pConfig->QueryWindowsAuthEnabled(), + pConfig->QueryBasicAuthEnabled(), + pConfig->QueryAnonymousAuthEnabled(), + pConfig->QueryEnvironmentVariables(), + pConfig->QueryStdoutLogEnabled(), + fWebsocketSupported, + pConfig->QueryStdoutLogFile(), + pConfig->QueryApplicationPhysicalPath(), // physical path + pConfig->QueryApplicationPath(), // app path + pConfig->QueryApplicationVirtualPath() // App relative virtual path + )); + RETURN_IF_FAILED(pSelectedServerProcess->StartProcess()); + } + + if (!pSelectedServerProcess->IsReady()) + { + RETURN_HR(HRESULT_FROM_WIN32(ERROR_CREATE_FAILED)); + } + + m_ppServerProcessList[dwProcessIndex] = pSelectedServerProcess.release(); + } + *ppServerProcess = m_ppServerProcessList[dwProcessIndex]; + + return S_OK; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/processmanager.h b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/processmanager.h new file mode 100644 index 0000000000..764e77d595 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/processmanager.h @@ -0,0 +1,208 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#define ONE_MINUTE_IN_MILLISECONDS 60000 +class SERVER_PROCESS; + +class PROCESS_MANAGER +{ +public: + + virtual + ~PROCESS_MANAGER(); + + VOID + ReferenceProcessManager() const + { + InterlockedIncrement(&m_cRefs); + } + + VOID + DereferenceProcessManager() const + { + if (InterlockedDecrement(&m_cRefs) == 0) + { + delete this; + } + } + + HRESULT + GetProcess( + _In_ REQUESTHANDLER_CONFIG *pConfig, + _In_ BOOL fWebsocketEnabled, + _Out_ SERVER_PROCESS **ppServerProcess + ); + + HANDLE + QueryNULHandle() + { + return m_hNULHandle; + } + + HRESULT + Initialize( + VOID + ); + + VOID + SendShutdownSignal() + { + AcquireSRWLockExclusive( &m_srwLock ); + + for(DWORD i = 0; i < m_dwProcessesPerApplication; ++i ) + { + if( m_ppServerProcessList != NULL && + m_ppServerProcessList[i] != NULL ) + { + m_ppServerProcessList[i]->SendSignal(); + m_ppServerProcessList[i]->DereferenceServerProcess(); + m_ppServerProcessList[i] = NULL; + } + } + + ReleaseSRWLockExclusive( &m_srwLock ); + } + + VOID + ShutdownProcess( + SERVER_PROCESS* pServerProcess + ) + { + AcquireSRWLockExclusive( &m_srwLock ); + + ShutdownProcessNoLock( pServerProcess ); + + ReleaseSRWLockExclusive( &m_srwLock ); + } + + VOID + ShutdownAllProcesses( + ) + { + AcquireSRWLockExclusive( &m_srwLock ); + + ShutdownAllProcessesNoLock(); + + ReleaseSRWLockExclusive( &m_srwLock ); + } + + VOID + Shutdown( + ) + { + if (InterlockedCompareExchange(&m_lStopping, 1L, 0L) == 0L) + { + ShutdownAllProcesses(); + } + } + + VOID + IncrementRapidFailCount( + VOID + ) + { + InterlockedIncrement(&m_cRapidFailCount); + } + + PROCESS_MANAGER() : + m_ppServerProcessList( NULL ), + m_hNULHandle( NULL ), + m_cRapidFailCount( 0 ), + m_dwProcessesPerApplication( 1 ), + m_dwRouteToProcessIndex( 0 ), + m_fServerProcessListReady(FALSE), + m_lStopping(0), + m_cRefs( 1 ) + { + m_ppServerProcessList = NULL; + m_fServerProcessListReady = FALSE; + InitializeSRWLock( &m_srwLock ); + } + +private: + + BOOL + RapidFailsPerMinuteExceeded( + LONG dwRapidFailsPerMinute + ) + { + DWORD dwCurrentTickCount = GetTickCount(); + + if( (dwCurrentTickCount - m_dwRapidFailTickStart) + >= ONE_MINUTE_IN_MILLISECONDS ) + { + // + // reset counters every minute. + // + + InterlockedExchange(&m_cRapidFailCount, 0); + m_dwRapidFailTickStart = dwCurrentTickCount; + } + + return m_cRapidFailCount > dwRapidFailsPerMinute; + } + + VOID + ShutdownProcessNoLock( + SERVER_PROCESS* pServerProcess + ) + { + for(DWORD i = 0; i < m_dwProcessesPerApplication; ++i ) + { + if( m_ppServerProcessList != NULL && + m_ppServerProcessList[i] != NULL && + m_ppServerProcessList[i]->GetPort() == pServerProcess->GetPort() ) + { + // shutdown pServerProcess if not already shutdown. + m_ppServerProcessList[i]->StopProcess(); + m_ppServerProcessList[i]->DereferenceServerProcess(); + m_ppServerProcessList[i] = NULL; + } + } + } + + VOID + ShutdownAllProcessesNoLock( + VOID + ) + { + for(DWORD i = 0; i < m_dwProcessesPerApplication; ++i ) + { + if( m_ppServerProcessList != NULL && + m_ppServerProcessList[i] != NULL ) + { + // shutdown pServerProcess if not already shutdown. + m_ppServerProcessList[i]->SendSignal(); + m_ppServerProcessList[i]->DereferenceServerProcess(); + m_ppServerProcessList[i] = NULL; + } + } + } + + volatile LONG m_cRapidFailCount; + DWORD m_dwRapidFailTickStart; + DWORD m_dwProcessesPerApplication; + volatile DWORD m_dwRouteToProcessIndex; + + SRWLOCK m_srwLock; + SERVER_PROCESS **m_ppServerProcessList; + + // + // m_hNULHandle is used to redirect stdout/stderr to NUL. + // If Createprocess is called to launch a batch file for example, + // it tries to write to the console buffer by default. It fails to + // start if the console buffer is owned by the parent process i.e + // in our case w3wp.exe. So we have to redirect the stdout/stderr + // of the child process to NUL or to a file (anything other than + // the console buffer of the parent process). + // + + HANDLE m_hNULHandle; + mutable LONG m_cRefs; + + volatile static BOOL sm_fWSAStartupDone; + volatile BOOL m_fServerProcessListReady; + volatile LONG m_lStopping; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/protocolconfig.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/protocolconfig.cpp new file mode 100644 index 0000000000..d0e44b5bd3 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/protocolconfig.cpp @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "protocolconfig.h" +#include "exceptions.h" + +HRESULT +PROTOCOL_CONFIG::Initialize() +{ + m_fKeepAlive = TRUE; + m_msTimeout = 120000; + m_fPreserveHostHeader = TRUE; + m_fReverseRewriteHeaders = FALSE; + + RETURN_IF_FAILED(m_strXForwardedForName.CopyW(L"X-Forwarded-For")); + RETURN_IF_FAILED(m_strSslHeaderName.CopyW(L"X-Forwarded-Proto")); + RETURN_IF_FAILED(m_strClientCertName.CopyW(L"MS-ASPNETCORE-CLIENTCERT")); + + m_fIncludePortInXForwardedFor = TRUE; + m_dwMinResponseBuffer = 0; // no response buffering + m_dwResponseBufferLimit = 4096*1024; + m_dwMaxResponseHeaderSize = 65536; + return S_OK; +} + +VOID +PROTOCOL_CONFIG::OverrideConfig( + REQUESTHANDLER_CONFIG *pAspNetCoreConfig +) +{ + m_msTimeout = pAspNetCoreConfig->QueryRequestTimeoutInMS(); +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/protocolconfig.h b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/protocolconfig.h new file mode 100644 index 0000000000..a31ca1ee33 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/protocolconfig.h @@ -0,0 +1,103 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +class PROTOCOL_CONFIG +{ + public: + + PROTOCOL_CONFIG() + { + } + + HRESULT + Initialize(); + + VOID + OverrideConfig( + REQUESTHANDLER_CONFIG *pAspNetCoreConfig + ); + + BOOL + QueryDoKeepAlive() const + { + return m_fKeepAlive; + } + + DWORD + QueryTimeout() const + { + return m_msTimeout; + } + + BOOL + QueryPreserveHostHeader() const + { + return m_fPreserveHostHeader; + } + + BOOL + QueryReverseRewriteHeaders() const + { + return m_fReverseRewriteHeaders; + } + + const STRA * + QueryXForwardedForName() const + { + return &m_strXForwardedForName; + } + + BOOL + QueryIncludePortInXForwardedFor() const + { + return m_fIncludePortInXForwardedFor; + } + + DWORD + QueryMinResponseBuffer() const + { + return m_dwMinResponseBuffer; + } + + DWORD + QueryResponseBufferLimit() const + { + return m_dwResponseBufferLimit; + } + + DWORD + QueryMaxResponseHeaderSize() const + { + return m_dwMaxResponseHeaderSize; + } + + const STRA* + QuerySslHeaderName() const + { + return &m_strSslHeaderName; + } + + const STRA * + QueryClientCertName() const + { + return &m_strClientCertName; + } + + private: + + BOOL m_fKeepAlive; + BOOL m_fPreserveHostHeader; + BOOL m_fReverseRewriteHeaders; + BOOL m_fIncludePortInXForwardedFor; + + DWORD m_msTimeout; + DWORD m_dwMinResponseBuffer; + DWORD m_dwResponseBufferLimit; + DWORD m_dwMaxResponseHeaderSize; + + STRA m_strXForwardedForName; + STRA m_strSslHeaderName; + STRA m_strClientCertName; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/resource.h b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/resource.h new file mode 100644 index 0000000000..4d54425300 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by HtmlResponses.rc +// +#define OUT_OF_PROCESS_RH_STATIC_HTML 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/responseheaderhash.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/responseheaderhash.cpp new file mode 100644 index 0000000000..55dcc2fbd4 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/responseheaderhash.cpp @@ -0,0 +1,90 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "responseheaderhash.h" +#include "exceptions.h" + +HEADER_RECORD RESPONSE_HEADER_HASH::sm_rgHeaders[] = +{ + { "Cache-Control", HttpHeaderCacheControl }, + { "Connection", HttpHeaderConnection }, + { "Date", HttpHeaderDate }, + { "Keep-Alive", HttpHeaderKeepAlive }, + { "Pragma", HttpHeaderPragma }, + { "Trailer", HttpHeaderTrailer }, + { "Transfer-Encoding", HttpHeaderTransferEncoding }, + { "Upgrade", HttpHeaderUpgrade }, + { "Via", HttpHeaderVia }, + { "Warning", HttpHeaderWarning }, + { "Allow", HttpHeaderAllow }, + { "Content-Length", HttpHeaderContentLength }, + { "Content-Type", HttpHeaderContentType }, + { "Content-Encoding", HttpHeaderContentEncoding }, + { "Content-Language", HttpHeaderContentLanguage }, + { "Content-Location", HttpHeaderContentLocation }, + { "Content-MD5", HttpHeaderContentMd5 }, + { "Content-Range", HttpHeaderContentRange }, + { "Expires", HttpHeaderExpires }, + { "Last-Modified", HttpHeaderLastModified }, + { "Accept-Ranges", HttpHeaderAcceptRanges }, + { "Age", HttpHeaderAge }, + { "ETag", HttpHeaderEtag }, + { "Location", HttpHeaderLocation }, + { "Proxy-Authenticate", HttpHeaderProxyAuthenticate }, + { "Retry-After", HttpHeaderRetryAfter }, + { "Server", HttpHeaderServer }, + // Set it to something which cannot be a header name, in effect + // making Server an unknown header. w:w is used to avoid collision with Keep-Alive. + { "w:w\r\n", HttpHeaderServer }, + // Set it to something which cannot be a header name, in effect + // making Set-Cookie an unknown header + { "y:y\r\n", HttpHeaderSetCookie }, + { "Vary", HttpHeaderVary }, + // Set it to something which cannot be a header name, in effect + // making WWW-Authenticate an unknown header + { "z:z\r\n", HttpHeaderWwwAuthenticate } + +}; + +HRESULT +RESPONSE_HEADER_HASH::Initialize( + VOID +) +/*++ + +Routine Description: + + Initialize global header hash table + +Arguments: + + None + +Return Value: + + HRESULT + +--*/ +{ + // + // 31 response headers. + // Make sure to update the number of buckets it new headers + // are added. Test it to avoid collisions. + // + C_ASSERT(_countof(sm_rgHeaders) == 31); + + // + // 79 buckets will have less collisions for the 31 response headers. + // Known collisions are "Age" colliding with "Expire" and "Location" + // colliding with both "Expire" and "Age". + // + RETURN_IF_FAILED(HASH_TABLE::Initialize(79)); + + for ( DWORD Index = 0; Index < _countof(sm_rgHeaders); ++Index ) + { + RETURN_IF_FAILED(InsertRecord(&sm_rgHeaders[Index])); + } + + return S_OK; +} + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/responseheaderhash.h b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/responseheaderhash.h new file mode 100644 index 0000000000..54f9c82954 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/responseheaderhash.h @@ -0,0 +1,108 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +// +// *_HEADER_HASH maps strings to UlHeader* values +// + +#define UNKNOWN_INDEX (0xFFFFFFFF) + +struct HEADER_RECORD +{ + PCSTR _pszName; + ULONG _ulHeaderIndex; +}; + +class RESPONSE_HEADER_HASH: public HASH_TABLE +{ +public: + RESPONSE_HEADER_HASH() + {} + + VOID + ReferenceRecord( + HEADER_RECORD * + ) + {} + + VOID + DereferenceRecord( + HEADER_RECORD * + ) + {} + + PCSTR + ExtractKey( + HEADER_RECORD * pRecord + ) + { + return pRecord->_pszName; + } + + DWORD + CalcKeyHash( + PCSTR key + ) + { + return HashStringNoCase(key); + } + + BOOL + EqualKeys( + PCSTR key1, + PCSTR key2 + ) + { + return (_stricmp(key1, key2) == 0); + } + + HRESULT + Initialize( + VOID + ); + + VOID + Terminate( + VOID + ); + + DWORD + GetIndex( + PCSTR pszName + ) + { + HEADER_RECORD * pRecord = NULL; + + FindKey(pszName, &pRecord); + if (pRecord != NULL) + { + return pRecord->_ulHeaderIndex; + } + + return UNKNOWN_INDEX; + } + + static + PCSTR + GetString( + ULONG ulIndex + ) + { + if (ulIndex < HttpHeaderResponseMaximum) + { + DBG_ASSERT(sm_rgHeaders[ulIndex]._ulHeaderIndex == ulIndex); + return sm_rgHeaders[ulIndex]._pszName; + } + + return NULL; + } + +private: + + static HEADER_RECORD sm_rgHeaders[]; + + RESPONSE_HEADER_HASH(const RESPONSE_HEADER_HASH &); + void operator=(const RESPONSE_HEADER_HASH &); +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/serverprocess.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/serverprocess.cpp new file mode 100644 index 0000000000..494911472a --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/serverprocess.cpp @@ -0,0 +1,2151 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "serverprocess.h" + +#include +#include "EventLog.h" +#include "file_utility.h" +#include "exceptions.h" +//#include + +//extern BOOL g_fNsiApiNotSupported; + +#define STARTUP_TIME_LIMIT_INCREMENT_IN_MILLISECONDS 5000 + + +HRESULT +SERVER_PROCESS::Initialize( + PROCESS_MANAGER *pProcessManager, + STRU *pszProcessExePath, + STRU *pszArguments, + DWORD dwStartupTimeLimitInMS, + DWORD dwShtudownTimeLimitInMS, + BOOL fWindowsAuthEnabled, + BOOL fBasicAuthEnabled, + BOOL fAnonymousAuthEnabled, + ENVIRONMENT_VAR_HASH *pEnvironmentVariables, + BOOL fStdoutLogEnabled, + BOOL fWebSocketSupported, + STRU *pstruStdoutLogFile, + STRU *pszAppPhysicalPath, + STRU *pszAppPath, + STRU *pszAppVirtualPath +) +{ + HRESULT hr = S_OK; + + m_pProcessManager = pProcessManager; + m_dwStartupTimeLimitInMS = dwStartupTimeLimitInMS; + m_dwShutdownTimeLimitInMS = dwShtudownTimeLimitInMS; + m_fStdoutLogEnabled = fStdoutLogEnabled; + m_fWebSocketSupported = fWebSocketSupported; + m_fWindowsAuthEnabled = fWindowsAuthEnabled; + m_fBasicAuthEnabled = fBasicAuthEnabled; + m_fAnonymousAuthEnabled = fAnonymousAuthEnabled; + m_pProcessManager->ReferenceProcessManager(); + m_fDebuggerAttached = FALSE; + + if (FAILED_LOG(hr = m_ProcessPath.Copy(*pszProcessExePath)) || + FAILED_LOG(hr = m_struLogFile.Copy(*pstruStdoutLogFile))|| + FAILED_LOG(hr = m_struPhysicalPath.Copy(*pszAppPhysicalPath))|| + FAILED_LOG(hr = m_struAppFullPath.Copy(*pszAppPath))|| + FAILED_LOG(hr = m_struAppVirtualPath.Copy(*pszAppVirtualPath))|| + FAILED_LOG(hr = m_Arguments.Copy(*pszArguments)) || + FAILED_LOG(hr = SetupJobObject())) + { + goto Finished; + } + + m_pEnvironmentVarTable = pEnvironmentVariables; + +Finished: + return hr; +} + +HRESULT +SERVER_PROCESS::SetupJobObject(VOID) +{ + HRESULT hr = S_OK; + JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo = { 0 }; + + if (m_hJobObject == NULL) + { + m_hJobObject = CreateJobObject(NULL, // LPSECURITY_ATTRIBUTES + NULL); // LPCTSTR lpName +#pragma warning( disable : 4312) + // 0xdeadbeef is used by Antares + if (m_hJobObject == NULL || m_hJobObject == (HANDLE)0xdeadbeef) + { + m_hJobObject = NULL; + // ignore job object creation error. + } +#pragma warning( error : 4312) + if (m_hJobObject != NULL) + { + jobInfo.BasicLimitInformation.LimitFlags = + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + + if (!SetInformationJobObject(m_hJobObject, + JobObjectExtendedLimitInformation, + &jobInfo, + sizeof jobInfo)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + } + } + } + + return hr; +} + +HRESULT +SERVER_PROCESS::GetRandomPort +( + DWORD* pdwPickedPort, + DWORD dwExcludedPort = 0 +) +{ + HRESULT hr = S_OK; + BOOL fPortInUse = FALSE; + DWORD dwActualProcessId = 0; + + std::uniform_int_distribution<> dist(MIN_PORT, MAX_PORT); + + if (g_fNsiApiNotSupported) + { + // + // the default value for optional parameter dwExcludedPort is 0 which is reserved + // a random number between MIN_PORT and MAX_PORT + // + while ((*pdwPickedPort = dist(m_randomGenerator)) == dwExcludedPort); + } + else + { + DWORD cRetry = 0; + do + { + // + // ignore dwActualProcessId because here we are + // determing whether the randomly generated port is + // in use by any other process. + // + while ((*pdwPickedPort = dist(m_randomGenerator)) == dwExcludedPort); + hr = CheckIfServerIsUp(*pdwPickedPort, &dwActualProcessId, &fPortInUse); + } while (fPortInUse && ++cRetry < MAX_RETRY); + + if (cRetry >= MAX_RETRY) + { + hr = HRESULT_FROM_WIN32(ERROR_PORT_NOT_SET); + } + } + + return hr; +} + +HRESULT +SERVER_PROCESS::SetupListenPort( + ENVIRONMENT_VAR_HASH *pEnvironmentVarTable, + BOOL* pfCriticalError +) +{ + HRESULT hr = S_OK; + ENVIRONMENT_VAR_ENTRY *pEntry = NULL; + *pfCriticalError = FALSE; + + pEnvironmentVarTable->FindKey(ASPNETCORE_PORT_ENV_STR, &pEntry); + if (pEntry != NULL) + { + if (pEntry->QueryValue() != NULL && pEntry->QueryValue()[0] != L'\0') + { + m_dwPort = (DWORD)_wtoi(pEntry->QueryValue()); + if (m_dwPort >MAX_PORT || m_dwPort < MIN_PORT) + { + hr = E_INVALIDARG; + *pfCriticalError = TRUE; + goto Finished; + // need add log for this one + } + hr = m_struPort.Copy(pEntry->QueryValue()); + goto Finished; + } + else + { + // + // user set the env variable but did not give value, let's set it up + // + pEnvironmentVarTable->DeleteKey(ASPNETCORE_PORT_ENV_STR); + pEntry->Dereference(); + pEntry = NULL; + } + } + + WCHAR buffer[15]; + if (FAILED_LOG(hr = GetRandomPort(&m_dwPort))) + { + goto Finished; + } + + if (swprintf_s(buffer, 15, L"%d", m_dwPort) <= 0) + { + hr = E_INVALIDARG; + goto Finished; + } + + pEntry = new ENVIRONMENT_VAR_ENTRY(); + if (pEntry == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + if (FAILED_LOG(hr = pEntry->Initialize(ASPNETCORE_PORT_ENV_STR, buffer)) || + FAILED_LOG(hr = pEnvironmentVarTable->InsertRecord(pEntry)) || + FAILED_LOG(hr = m_struPort.Copy(buffer))) + { + goto Finished; + } + +Finished: + if (pEntry != NULL) + { + pEntry->Dereference(); + pEntry = NULL; + } + + if (FAILED_LOG(hr)) + { + EventLog::Error( + ASPNETCORE_EVENT_PROCESS_START_SUCCESS, + ASPNETCORE_EVENT_PROCESS_START_PORTSETUP_ERROR_MSG, + m_struAppFullPath.QueryStr(), + m_struPhysicalPath.QueryStr(), + m_dwPort, + MIN_PORT, + MAX_PORT, + hr); + } + + return hr; +} + +HRESULT +SERVER_PROCESS::SetupAppPath( + ENVIRONMENT_VAR_HASH* pEnvironmentVarTable +) +{ + HRESULT hr = S_OK; + ENVIRONMENT_VAR_ENTRY* pEntry = NULL; + + pEnvironmentVarTable->FindKey(ASPNETCORE_APP_PATH_ENV_STR, &pEntry); + if (pEntry != NULL) + { + // user should not set this environment variable in configuration + pEnvironmentVarTable->DeleteKey(ASPNETCORE_APP_PATH_ENV_STR); + pEntry->Dereference(); + pEntry = NULL; + } + + pEntry = new ENVIRONMENT_VAR_ENTRY(); + if (pEntry == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + if (FAILED_LOG(hr = pEntry->Initialize(ASPNETCORE_APP_PATH_ENV_STR, m_struAppVirtualPath.QueryStr())) || + FAILED_LOG(hr = pEnvironmentVarTable->InsertRecord(pEntry))) + { + goto Finished; + } + +Finished: + if (pEntry != NULL) + { + pEntry->Dereference(); + pEntry = NULL; + } + return hr; +} + +HRESULT +SERVER_PROCESS::SetupAppToken( + ENVIRONMENT_VAR_HASH *pEnvironmentVarTable +) +{ + HRESULT hr = S_OK; + UUID logUuid; + PSTR pszLogUuid = NULL; + BOOL fRpcStringAllocd = FALSE; + RPC_STATUS rpcStatus; + STRU strAppToken; + ENVIRONMENT_VAR_ENTRY* pEntry = NULL; + + pEnvironmentVarTable->FindKey(ASPNETCORE_APP_TOKEN_ENV_STR, &pEntry); + if (pEntry != NULL) + { + // user sets the environment variable + m_straGuid.Reset(); + hr = m_straGuid.CopyW(pEntry->QueryValue()); + pEntry->Dereference(); + pEntry = NULL; + goto Finished; + } + else + { + if (m_straGuid.IsEmpty()) + { + // the GUID has not been set yet + rpcStatus = UuidCreate(&logUuid); + if (rpcStatus != RPC_S_OK) + { + hr = rpcStatus; + goto Finished; + } + + rpcStatus = UuidToStringA(&logUuid, (BYTE **)&pszLogUuid); + if (rpcStatus != RPC_S_OK) + { + hr = rpcStatus; + goto Finished; + } + + fRpcStringAllocd = TRUE; + + if (FAILED_LOG(hr = m_straGuid.Copy(pszLogUuid))) + { + goto Finished; + } + } + + pEntry = new ENVIRONMENT_VAR_ENTRY(); + if (pEntry == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + if (FAILED_LOG(strAppToken.CopyA(m_straGuid.QueryStr())) || + FAILED_LOG(hr = pEntry->Initialize(ASPNETCORE_APP_TOKEN_ENV_STR, strAppToken.QueryStr())) || + FAILED_LOG(hr = pEnvironmentVarTable->InsertRecord(pEntry))) + { + goto Finished; + } + } + +Finished: + + if (fRpcStringAllocd) + { + RpcStringFreeA((BYTE **)&pszLogUuid); + pszLogUuid = NULL; + } + if (pEntry != NULL) + { + pEntry->Dereference(); + pEntry = NULL; + } + return hr; +} + +HRESULT +SERVER_PROCESS::OutputEnvironmentVariables +( + MULTISZ* pmszOutput, + ENVIRONMENT_VAR_HASH* pEnvironmentVarTable +) +{ + HRESULT hr = S_OK; + LPWSTR pszEnvironmentVariables = NULL; + LPWSTR pszCurrentVariable = NULL; + LPWSTR pszNextVariable = NULL; + LPWSTR pszEqualChar = NULL; + STRU strEnvVar; + ENVIRONMENT_VAR_ENTRY* pEntry = NULL; + + DBG_ASSERT(pmszOutput); + DBG_ASSERT(pEnvironmentVarTable); // We added some startup variables + DBG_ASSERT(pEnvironmentVarTable->Count() >0); + + // cleanup, as we may in retry logic + pmszOutput->Reset(); + + pszEnvironmentVariables = GetEnvironmentStringsW(); + if (pszEnvironmentVariables == NULL) + { + hr = HRESULT_FROM_WIN32(ERROR_INVALID_ENVIRONMENT); + goto Finished; + } + pszCurrentVariable = pszEnvironmentVariables; + while (*pszCurrentVariable != L'\0') + { + pszNextVariable = pszCurrentVariable + wcslen(pszCurrentVariable) + 1; + pszEqualChar = wcschr(pszCurrentVariable, L'='); + if (pszEqualChar != NULL) + { + if (FAILED_LOG(hr = strEnvVar.Copy(pszCurrentVariable, (DWORD)(pszEqualChar - pszCurrentVariable) + 1))) + { + goto Finished; + } + pEnvironmentVarTable->FindKey(strEnvVar.QueryStr(), &pEntry); + if (pEntry != NULL) + { + // same env variable is defined in configuration, use it + if (FAILED_LOG(hr = strEnvVar.Append(pEntry->QueryValue()))) + { + goto Finished; + } + pmszOutput->Append(strEnvVar); //should we check the returned bool + // remove the record from hash table as we already output it + pEntry->Dereference(); + pEnvironmentVarTable->DeleteKey(pEntry->QueryName()); + strEnvVar.Reset(); + pEntry = NULL; + } + else + { + pmszOutput->Append(pszCurrentVariable); + } + } + else + { + // env varaible is not well formated + hr = HRESULT_FROM_WIN32(ERROR_INVALID_ENVIRONMENT); + goto Finished; + } + // move to next env variable + pszCurrentVariable = pszNextVariable; + } + // append the remaining env variable in hash table + pEnvironmentVarTable->Apply(ENVIRONMENT_VAR_HELPERS::CopyToMultiSz, pmszOutput); + +Finished: + if (pszEnvironmentVariables != NULL) + { + FreeEnvironmentStringsW(pszEnvironmentVariables); + pszEnvironmentVariables = NULL; + } + return hr; +} + +HRESULT +SERVER_PROCESS::SetupCommandLine( + STRU* pstrCommandLine +) +{ + HRESULT hr = S_OK; + LPWSTR pszPath = NULL; + LPWSTR pszFullPath = NULL; + STRU strRelativePath; + DWORD dwBufferSize = 0; + FILE *file = NULL; + + DBG_ASSERT(pstrCommandLine); + + if (!m_struCommandLine.IsEmpty() && + pstrCommandLine == (&m_struCommandLine)) + { + // already set up the commandline string, skip + goto Finished; + } + + pszPath = m_ProcessPath.QueryStr(); + + if ((wcsstr(pszPath, L":") == NULL) && (wcsstr(pszPath, L"%") == NULL)) + { + // let's check whether it is a relative path + if (FAILED_LOG(hr = strRelativePath.Copy(m_struPhysicalPath.QueryStr())) || + FAILED_LOG(hr = strRelativePath.Append(L"\\")) || + FAILED_LOG(hr = strRelativePath.Append(pszPath))) + { + goto Finished; + } + + dwBufferSize = strRelativePath.QueryCCH() + 1; + pszFullPath = new WCHAR[dwBufferSize]; + if (pszFullPath == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + if (_wfullpath(pszFullPath, + strRelativePath.QueryStr(), + dwBufferSize) == NULL) + { + hr = HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER); + goto Finished; + } + + if ((file = _wfsopen(pszFullPath, L"r", _SH_DENYNO)) != NULL) + { + fclose(file); + pszPath = pszFullPath; + } + } + if (FAILED_LOG(hr = pstrCommandLine->Copy(pszPath)) || + FAILED_LOG(hr = pstrCommandLine->Append(L" ")) || + FAILED_LOG(hr = pstrCommandLine->Append(m_Arguments.QueryStr()))) + { + goto Finished; + } + +Finished: + if (pszFullPath != NULL) + { + delete pszFullPath; + } + return hr; +} + +HRESULT +SERVER_PROCESS::PostStartCheck( + VOID +) +{ + HRESULT hr = S_OK; + + BOOL fReady = FALSE; + BOOL fProcessMatch = FALSE; + BOOL fDebuggerAttached = FALSE; + DWORD dwTickCount = 0; + DWORD dwTimeDifference = 0; + DWORD dwActualProcessId = 0; + INT iChildProcessIndex = -1; + STACK_STRU(strEventMsg, 256); + + if (CheckRemoteDebuggerPresent(m_hProcessHandle, &fDebuggerAttached) == 0) + { + // some error occurred - assume debugger is not attached; + fDebuggerAttached = FALSE; + } + + dwTickCount = GetTickCount(); + do + { + DWORD processStatus = 0; + if (GetExitCodeProcess(m_hProcessHandle, &processStatus)) + { + // make sure the process is still running + if (processStatus != STILL_ACTIVE) + { + // double check + if (GetExitCodeProcess(m_hProcessHandle, &processStatus) && processStatus != STILL_ACTIVE) + { + hr = E_APPLICATION_ACTIVATION_EXEC_FAILURE; + strEventMsg.SafeSnwprintf( + ASPNETCORE_EVENT_PROCESS_START_STATUS_ERROR_MSG, + m_struAppFullPath.QueryStr(), + m_struPhysicalPath.QueryStr(), + m_struCommandLine.QueryStr(), + hr, + m_dwProcessId, + processStatus); + goto Finished; + } + } + } + // + // dwActualProcessId will be set only when NsiAPI(GetExtendedTcpTable) is supported + // + hr = CheckIfServerIsUp(m_dwPort, &dwActualProcessId, &fReady); + fDebuggerAttached = IsDebuggerIsAttached(); + + if (!fReady) + { + Sleep(250); + } + + dwTimeDifference = (GetTickCount() - dwTickCount); + } while (fReady == FALSE && + ((dwTimeDifference < m_dwStartupTimeLimitInMS) || fDebuggerAttached)); + + if (!fReady) + { + hr = E_APPLICATION_ACTIVATION_TIMED_OUT; + goto Finished; + } + + // register call back with the created process + if (FAILED_LOG(hr = RegisterProcessWait(&m_hProcessWaitHandle, m_hProcessHandle))) + { + goto Finished; + } + + // + // check if debugger is attached after startupTimeout. + // + if (!fDebuggerAttached && + CheckRemoteDebuggerPresent(m_hProcessHandle, &fDebuggerAttached) == 0) + { + // some error occurred - assume debugger is not attached; + fDebuggerAttached = FALSE; + } + + if (!g_fNsiApiNotSupported) + { + // + // NsiAPI(GetExtendedTcpTable) is supported. we should check whether processIds matche + // + if (dwActualProcessId == m_dwProcessId) + { + m_dwListeningProcessId = m_dwProcessId; + fProcessMatch = TRUE; + } + + if (!fProcessMatch) + { + // could be the scenario that backend creates child process + if (FAILED_LOG(hr = GetChildProcessHandles())) + { + goto Finished; + } + + for (DWORD i = 0; i < m_cChildProcess; ++i) + { + // a child process listen on the assigned port + if (dwActualProcessId == m_dwChildProcessIds[i]) + { + m_dwListeningProcessId = m_dwChildProcessIds[i]; + fProcessMatch = TRUE; + + if (m_hChildProcessHandles[i] != NULL) + { + if (fDebuggerAttached == FALSE && + CheckRemoteDebuggerPresent(m_hChildProcessHandles[i], &fDebuggerAttached) == 0) + { + // some error occurred - assume debugger is not attached; + fDebuggerAttached = FALSE; + } + + if (FAILED_LOG(hr = RegisterProcessWait(&m_hChildProcessWaitHandles[i], + m_hChildProcessHandles[i]))) + { + goto Finished; + } + iChildProcessIndex = i; + } + break; + } + } + } + + if(!fProcessMatch) + { + // + // process that we created is not listening + // on the port we specified. + // + fReady = FALSE; + hr = HRESULT_FROM_WIN32(ERROR_CREATE_FAILED); + strEventMsg.SafeSnwprintf( + ASPNETCORE_EVENT_PROCESS_START_WRONGPORT_ERROR_MSG, + m_struAppFullPath.QueryStr(), + m_struPhysicalPath.QueryStr(), + m_struCommandLine.QueryStr(), + m_dwPort, + hr); + goto Finished; + } + } + + if (!fReady) + { + // + // hr is already set by CheckIfServerIsUp + // + if (dwTimeDifference >= m_dwStartupTimeLimitInMS) + { + hr = HRESULT_FROM_WIN32(ERROR_TIMEOUT); + strEventMsg.SafeSnwprintf( + ASPNETCORE_EVENT_PROCESS_START_NOTREADY_ERROR_MSG, + m_struAppFullPath.QueryStr(), + m_struPhysicalPath.QueryStr(), + m_struCommandLine.QueryStr(), + m_dwPort, + hr); + } + goto Finished; + } + + if (iChildProcessIndex >= 0) + { + // + // final check to make sure child process listening on HTTP is still UP + // This is needed because, the child process might have crashed/exited between + // the previous call to checkIfServerIsUp and RegisterProcessWait + // and we would not know about it. + // + + hr = CheckIfServerIsUp(m_dwPort, &dwActualProcessId, &fReady); + + if ((FAILED_LOG(hr) || fReady == FALSE)) + { + strEventMsg.SafeSnwprintf( + ASPNETCORE_EVENT_PROCESS_START_NOTREADY_ERROR_MSG, + m_struAppFullPath.QueryStr(), + m_struPhysicalPath.QueryStr(), + m_struCommandLine.QueryStr(), + m_dwPort, + hr); + goto Finished; + } + } + + // + // ready to mark the server process ready but before this, + // create and initialize the FORWARDER_CONNECTION + // + if (m_pForwarderConnection == NULL) + { + m_pForwarderConnection = new FORWARDER_CONNECTION(); + if (m_pForwarderConnection == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + hr = m_pForwarderConnection->Initialize(m_dwPort); + if (FAILED_LOG(hr)) + { + goto Finished; + } + } + + if (!g_fNsiApiNotSupported) + { + m_hListeningProcessHandle = OpenProcess(SYNCHRONIZE | PROCESS_TERMINATE | PROCESS_DUP_HANDLE, + FALSE, + m_dwListeningProcessId); + } + + // + // mark server process as Ready + // + m_fReady = TRUE; + +Finished: + m_fDebuggerAttached = fDebuggerAttached; + + if (FAILED_LOG(hr)) + { + if (m_pForwarderConnection != NULL) + { + m_pForwarderConnection->DereferenceForwarderConnection(); + m_pForwarderConnection = NULL; + } + + if (!strEventMsg.IsEmpty()) + { + EventLog::Warn( + ASPNETCORE_EVENT_PROCESS_START_ERROR, + strEventMsg.QueryStr()); + } + } + return hr; +} + +HRESULT +SERVER_PROCESS::StartProcess( + VOID +) +{ + HRESULT hr = S_OK; + PROCESS_INFORMATION processInformation = {0}; + STARTUPINFOW startupInfo = {0}; + DWORD dwRetryCount = 2; // should we allow customer to config it + DWORD dwCreationFlags = 0; + MULTISZ mszNewEnvironment; + ENVIRONMENT_VAR_HASH *pHashTable = NULL; + PWSTR pStrStage = NULL; + BOOL fCriticalError = FALSE; + GetStartupInfoW(&startupInfo); + + // + // setup stdout and stderr handles to our stdout handle only if + // the handle is valid. + // + SetupStdHandles(&startupInfo); + + while (dwRetryCount > 0) + { + m_dwPort = 0; + dwRetryCount--; + // + // generate process command line. + // + if (FAILED_LOG(hr = SetupCommandLine(&m_struCommandLine))) + { + pStrStage = L"SetupCommandLine"; + goto Failure; + } + + if (FAILED_LOG(hr = ENVIRONMENT_VAR_HELPERS::InitEnvironmentVariablesTable( + m_pEnvironmentVarTable, + m_fWindowsAuthEnabled, + m_fBasicAuthEnabled, + m_fAnonymousAuthEnabled, + &pHashTable))) + { + pStrStage = L"InitEnvironmentVariablesTable"; + goto Failure; + } + + if (FAILED_LOG(hr = ENVIRONMENT_VAR_HELPERS::AddWebsocketEnabledToEnvironmentVariables( + pHashTable, + m_fWebSocketSupported + ))) + { + pStrStage = L"AddWebsocketEnabledToEnvironmentVariables"; + goto Failure; + + } + + // + // setup the the port that the backend process will listen on + // + if (FAILED_LOG(hr = SetupListenPort(pHashTable, &fCriticalError))) + { + pStrStage = L"SetupListenPort"; + goto Failure; + } + + // + // get app path + // + if (FAILED_LOG(hr = SetupAppPath(pHashTable))) + { + pStrStage = L"SetupAppPath"; + goto Failure; + } + + // + // generate new guid for each process + // + if (FAILED_LOG(hr = SetupAppToken(pHashTable))) + { + pStrStage = L"SetupAppToken"; + goto Failure; + } + + // + // setup environment variables for new process + // + if (FAILED_LOG(hr = OutputEnvironmentVariables(&mszNewEnvironment, pHashTable))) + { + pStrStage = L"OutputEnvironmentVariables"; + goto Failure; + } + + dwCreationFlags = CREATE_NO_WINDOW | + CREATE_UNICODE_ENVIRONMENT | + CREATE_SUSPENDED | + CREATE_NEW_PROCESS_GROUP; + + if (!CreateProcessW( + NULL, // applicationName + m_struCommandLine.QueryStr(), + NULL, // processAttr + NULL, // threadAttr + TRUE, // inheritHandles + dwCreationFlags, + mszNewEnvironment.QueryStr(), + m_struPhysicalPath.QueryStr(), // currentDir + &startupInfo, + &processInformation)) + { + pStrStage = L"CreateProcessW"; + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Failure; + } + + m_hProcessHandle = processInformation.hProcess; + m_dwProcessId = processInformation.dwProcessId; + + if (FAILED_LOG(hr = SetupJobObject())) + { + pStrStage = L"SetupJobObject"; + goto Failure; + } + + if (m_hJobObject != NULL) + { + if (!AssignProcessToJobObject(m_hJobObject, m_hProcessHandle)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + if (hr != HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)) + { + pStrStage = L"AssignProcessToJobObject"; + goto Failure; + } + } + } + + if (ResumeThread(processInformation.hThread) == -1) + { + pStrStage = L"ResumeThread"; + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Failure; + } + + // + // need to make sure the server is up and listening on the port specified. + // + if (FAILED_LOG(hr = PostStartCheck())) + { + pStrStage = L"PostStartCheck"; + goto Failure; + } + + // Backend process starts successfully. Set retry counter to 0 + dwRetryCount = 0; + + EventLog::Info( + ASPNETCORE_EVENT_PROCESS_START_SUCCESS, + ASPNETCORE_EVENT_PROCESS_START_SUCCESS_MSG, + m_struAppFullPath.QueryStr(), + m_dwProcessId, + m_dwListeningProcessId, + m_dwPort); + + goto Finished; + + Failure: + if (fCriticalError) + { + // Critical error, no retry need to avoid wasting resource and polluting log + dwRetryCount = 0; + } + + EventLog::Warn( + ASPNETCORE_EVENT_PROCESS_START_ERROR, + ASPNETCORE_EVENT_PROCESS_START_ERROR_MSG, + m_struAppFullPath.QueryStr(), + m_struPhysicalPath.QueryStr(), + m_struCommandLine.QueryStr(), + pStrStage, + hr, + m_dwPort, + dwRetryCount); + + if (processInformation.hThread != NULL) + { + CloseHandle(processInformation.hThread); + processInformation.hThread = NULL; + } + + if (pHashTable != NULL) + { + pHashTable->Clear(); + delete pHashTable; + pHashTable = NULL; + } + + CleanUp(); + } + +Finished: + if (FAILED_LOG(hr) || m_fReady == FALSE) + { + if (m_hStdoutHandle != NULL) + { + if (m_hStdoutHandle != INVALID_HANDLE_VALUE) + { + CloseHandle(m_hStdoutHandle); + } + m_hStdoutHandle = NULL; + } + + if (m_fStdoutLogEnabled) + { + m_Timer.CancelTimer(); + } + + EventLog::Error( + ASPNETCORE_EVENT_PROCESS_START_FAILURE, + ASPNETCORE_EVENT_PROCESS_START_FAILURE_MSG, + m_struAppFullPath.QueryStr(), + m_struPhysicalPath.QueryStr(), + m_struCommandLine.QueryStr(), + m_dwPort); + } + return hr; +} + +HRESULT +SERVER_PROCESS::SetWindowsAuthToken( + HANDLE hToken, + LPHANDLE pTargetTokenHandle +) +{ + HRESULT hr = S_OK; + + if (m_hListeningProcessHandle != NULL && m_hListeningProcessHandle != INVALID_HANDLE_VALUE) + { + if (!DuplicateHandle( GetCurrentProcess(), + hToken, + m_hListeningProcessHandle, + pTargetTokenHandle, + 0, + FALSE, + DUPLICATE_SAME_ACCESS )) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + } + +Finished: + + return hr; +} + +HRESULT +SERVER_PROCESS::SetupStdHandles( + LPSTARTUPINFOW pStartupInfo +) +{ + HRESULT hr = S_OK; + SYSTEMTIME systemTime; + SECURITY_ATTRIBUTES saAttr = { 0 }; + + STRU struPath; + + DBG_ASSERT(pStartupInfo); + + if (!m_fStdoutLogEnabled) + { + pStartupInfo->dwFlags = STARTF_USESTDHANDLES; + pStartupInfo->hStdInput = INVALID_HANDLE_VALUE; + pStartupInfo->hStdError = INVALID_HANDLE_VALUE; + pStartupInfo->hStdOutput = INVALID_HANDLE_VALUE; + return hr; + } + if (m_hStdoutHandle != NULL && m_hStdoutHandle != INVALID_HANDLE_VALUE) + { + if (!CloseHandle(m_hStdoutHandle)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + m_hStdoutHandle = NULL; + } + + hr = FILE_UTILITY::ConvertPathToFullPath( + m_struLogFile.QueryStr(), + m_struPhysicalPath.QueryStr(), + &struPath); + if (FAILED_LOG(hr)) + { + goto Finished; + } + + GetSystemTime(&systemTime); + hr = m_struFullLogFile.SafeSnwprintf(L"%s_%d%02d%02d%02d%02d%02d_%d.log", + struPath.QueryStr(), + systemTime.wYear, + systemTime.wMonth, + systemTime.wDay, + systemTime.wHour, + systemTime.wMinute, + systemTime.wSecond, + GetCurrentProcessId()); + if (FAILED_LOG(hr)) + { + goto Finished; + } + + hr = FILE_UTILITY::EnsureDirectoryPathExist(struPath.QueryStr()); + if (FAILED_LOG(hr)) + { + goto Finished; + } + + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = TRUE; + saAttr.lpSecurityDescriptor = NULL; + + m_hStdoutHandle = CreateFileW(m_struFullLogFile.QueryStr(), + FILE_WRITE_DATA, + FILE_SHARE_READ, + &saAttr, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + NULL); + + if (m_hStdoutHandle == INVALID_HANDLE_VALUE) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + pStartupInfo->dwFlags = STARTF_USESTDHANDLES; + pStartupInfo->hStdInput = INVALID_HANDLE_VALUE; + pStartupInfo->hStdError = m_hStdoutHandle; + pStartupInfo->hStdOutput = m_hStdoutHandle; + // start timer to open and close handles regularly. + m_Timer.InitializeTimer(STTIMER::TimerCallback, &m_struFullLogFile, 3000, 3000); + +Finished: + if (FAILED_LOG(hr)) + { + pStartupInfo->dwFlags = STARTF_USESTDHANDLES; + pStartupInfo->hStdInput = INVALID_HANDLE_VALUE; + pStartupInfo->hStdError = INVALID_HANDLE_VALUE; + pStartupInfo->hStdOutput = INVALID_HANDLE_VALUE; + + if (m_fStdoutLogEnabled) + { + // Log the error + EventLog::Warn( + ASPNETCORE_EVENT_CONFIG_ERROR, + ASPNETCORE_EVENT_INVALID_STDOUT_LOG_FILE_MSG, + m_struFullLogFile.IsEmpty()? m_struLogFile.QueryStr() : m_struFullLogFile.QueryStr(), + hr); + } + // The log file was not created yet in case of failure. No need to clean it + m_struFullLogFile.Reset(); + } + return hr; +} + +HRESULT +SERVER_PROCESS::CheckIfServerIsUp( + _In_ DWORD dwPort, + _Out_ DWORD * pdwProcessId, + _Out_ BOOL * pfReady +) +{ + HRESULT hr = S_OK; + DWORD dwResult = ERROR_INSUFFICIENT_BUFFER; + MIB_TCPTABLE_OWNER_PID *pTCPInfo = NULL; + MIB_TCPROW_OWNER_PID *pOwner = NULL; + DWORD dwSize = 1000; // Initial size for pTCPInfo buffer + int iResult = 0; + SOCKADDR_IN sockAddr; + SOCKET socketCheck = INVALID_SOCKET; + + DBG_ASSERT(pfReady); + DBG_ASSERT(pdwProcessId); + + *pfReady = FALSE; + // + // it's OK for us to return processID 0 in case we cannot detect the real one + // + *pdwProcessId = 0; + + if (!g_fNsiApiNotSupported) + { + while (dwResult == ERROR_INSUFFICIENT_BUFFER) + { + // Increase the buffer size with additional space, MIB_TCPROW 20 bytes + // New entries may be added by other processes before calling GetExtendedTcpTable + dwSize += 200; + + if (pTCPInfo != NULL) + { + HeapFree(GetProcessHeap(), 0, pTCPInfo); + } + + pTCPInfo = (MIB_TCPTABLE_OWNER_PID*)HeapAlloc(GetProcessHeap(), 0, dwSize); + if (pTCPInfo == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + dwResult = GetExtendedTcpTable(pTCPInfo, + &dwSize, + FALSE, + AF_INET, + TCP_TABLE_OWNER_PID_LISTENER, + 0); + + if (dwResult != NO_ERROR && dwResult != ERROR_INSUFFICIENT_BUFFER) + { + hr = HRESULT_FROM_WIN32(dwResult); + goto Finished; + } + } + + // iterate pTcpInfo struct to find PID/PORT entry + for (DWORD dwLoop = 0; dwLoop < pTCPInfo->dwNumEntries; dwLoop++) + { + pOwner = &pTCPInfo->table[dwLoop]; + if (ntohs((USHORT)pOwner->dwLocalPort) == dwPort) + { + *pdwProcessId = pOwner->dwOwningPid; + *pfReady = TRUE; + break; + } + } + } + else + { + // + // We have to open socket to ping the service + // + socketCheck = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + + if (socketCheck == INVALID_SOCKET) + { + hr = HRESULT_FROM_WIN32(WSAGetLastError()); + goto Finished; + } + + sockAddr.sin_family = AF_INET; + if (!inet_pton(AF_INET, LOCALHOST, &(sockAddr.sin_addr))) + { + hr = HRESULT_FROM_WIN32(WSAGetLastError()); + goto Finished; + } + + //sockAddr.sin_addr.s_addr = inet_addr( LOCALHOST ); + sockAddr.sin_port = htons((u_short)dwPort); + + // + // Connect to server. + // if connection fails, socket is not closed, we reuse the same socket + // while retrying + // + iResult = connect(socketCheck, (SOCKADDR *)&sockAddr, sizeof(sockAddr)); + if (iResult == SOCKET_ERROR) + { + hr = HRESULT_FROM_WIN32(WSAGetLastError()); + if (hr == HRESULT_FROM_WIN32(WSAECONNREFUSED)) + { + // WSAECONNREFUSED means no application listen on the given port. + // This is not a failure. Reset the hresult to S_OK and return fReady to false + hr = S_OK; + } + goto Finished; + } + *pfReady = TRUE; + } + +Finished: + + if (socketCheck != INVALID_SOCKET) + { + iResult = closesocket(socketCheck); + if (iResult == SOCKET_ERROR) + { + hr = HRESULT_FROM_WIN32(WSAGetLastError()); + } + socketCheck = INVALID_SOCKET; + } + + if (pTCPInfo != NULL) + { + HeapFree(GetProcessHeap(), 0, pTCPInfo); + pTCPInfo = NULL; + } + + return hr; +} + +// send signal to the process to let it gracefully shutdown +// if the process cannot shutdown within given time, terminate it +VOID +SERVER_PROCESS::SendSignal( + VOID +) +{ + HRESULT hr = S_OK; + HANDLE hThread = NULL; + + ReferenceServerProcess(); + + m_hShutdownHandle = OpenProcess(SYNCHRONIZE | PROCESS_TERMINATE, FALSE, m_dwProcessId); + + if (m_hShutdownHandle == NULL) + { + // since we cannot open the process. let's terminate the process + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + hThread = CreateThread( + NULL, // default security attributes + 0, // default stack size + (LPTHREAD_START_ROUTINE)SendShutDownSignal, + this, // thread function arguments + 0, // default creation flags + NULL); // receive thread identifier + + if (hThread == NULL) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + // + // Reset the shutdown timeout if debugger is attached. + // Do it only for the case that debugger is attached during process creation + // as IsDebuggerIsAttached call is too heavy + // + if (WaitForSingleObject(m_hShutdownHandle, m_fDebuggerAttached ? INFINITE : m_dwShutdownTimeLimitInMS) != WAIT_OBJECT_0) + { + hr = HRESULT_FROM_WIN32(ERROR_TIMEOUT); + goto Finished; + } + // thread should already exit + CloseHandle(hThread); + hThread = NULL; + +Finished: + if (hThread != NULL) + { + // if the send shutdown message thread is still running, terminate it + DWORD dwThreadStatus = 0; + if (GetExitCodeThread(hThread, &dwThreadStatus)!= 0 && dwThreadStatus == STILL_ACTIVE) + { + TerminateThread(hThread, STATUS_CONTROL_C_EXIT); + } + CloseHandle(hThread); + hThread = NULL; + } + + if (FAILED_LOG(hr)) + { + TerminateBackendProcess(); + } + + if (m_hShutdownHandle != NULL && m_hShutdownHandle != INVALID_HANDLE_VALUE) + { + CloseHandle(m_hShutdownHandle); + m_hShutdownHandle = NULL; + } + + DereferenceServerProcess(); +} + + +// +// StopProcess is only called if process crashes OR if the process +// creation failed and calling this counts towards RapidFailCounts. +// +VOID +SERVER_PROCESS::StopProcess( + VOID +) +{ + m_fReady = FALSE; + + m_pProcessManager->IncrementRapidFailCount(); + + for (INT i=0; iNumberOfAssignedProcesses > processList->NumberOfProcessIdsInList || + processList->NumberOfProcessIdsInList == 0)); + + if (dwError == ERROR_MORE_DATA) + { + hr = E_OUTOFMEMORY; + // some error + goto Finished; + } + + if (processList == NULL || + (processList->NumberOfAssignedProcesses > processList->NumberOfProcessIdsInList || + processList->NumberOfProcessIdsInList == 0)) + { + hr = HRESULT_FROM_WIN32(ERROR_PROCESS_ABORTED); + // some error + goto Finished; + } + + if (processList->NumberOfProcessIdsInList > MAX_ACTIVE_CHILD_PROCESSES) + { + hr = HRESULT_FROM_WIN32(ERROR_CREATE_FAILED); + goto Finished; + } + + for (DWORD i=0; iNumberOfProcessIdsInList; i++) + { + dwPid = (DWORD)processList->ProcessIdList[i]; + if (dwPid != dwWorkerProcessPid) + { + HANDLE hProcess = OpenProcess( + PROCESS_QUERY_INFORMATION | SYNCHRONIZE | PROCESS_TERMINATE | PROCESS_DUP_HANDLE, + FALSE, + dwPid); + + BOOL returnValue = CheckRemoteDebuggerPresent(hProcess, &fDebuggerPresent); + if (hProcess != NULL) + { + CloseHandle(hProcess); + hProcess = NULL; + } + + if (!returnValue) + { + goto Finished; + } + + if (fDebuggerPresent) + { + break; + } + } + } + +Finished: + + if (processList != NULL) + { + HeapFree(GetProcessHeap(), 0, processList); + } + + return fDebuggerPresent; +} + +HRESULT +SERVER_PROCESS::GetChildProcessHandles( + VOID +) +{ + HRESULT hr = S_OK; + PJOBOBJECT_BASIC_PROCESS_ID_LIST processList = NULL; + DWORD dwPid = 0; + DWORD dwWorkerProcessPid = 0; + DWORD cbNumBytes = 1024; + DWORD dwRetries = 0; + DWORD dwError = NO_ERROR; + + dwWorkerProcessPid = GetCurrentProcessId(); + + do + { + dwError = NO_ERROR; + + if (processList != NULL) + { + HeapFree(GetProcessHeap(), 0, processList); + processList = NULL; + + // resize + cbNumBytes = cbNumBytes * 2; + } + + processList = (PJOBOBJECT_BASIC_PROCESS_ID_LIST) HeapAlloc( + GetProcessHeap(), + 0, + cbNumBytes + ); + if (processList == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + RtlZeroMemory(processList, cbNumBytes); + + if (!QueryInformationJobObject( + m_hJobObject, + JobObjectBasicProcessIdList, + processList, + cbNumBytes, + NULL)) + { + dwError = GetLastError(); + if (dwError != ERROR_MORE_DATA) + { + hr = HRESULT_FROM_WIN32(dwError); + goto Finished; + } + } + + } while (dwRetries++ < 5 && + processList != NULL && + (processList->NumberOfAssignedProcesses > processList->NumberOfProcessIdsInList || processList->NumberOfProcessIdsInList == 0)); + + if (dwError == ERROR_MORE_DATA) + { + hr = E_OUTOFMEMORY; + // some error + goto Finished; + } + + if (processList == NULL || (processList->NumberOfAssignedProcesses > processList->NumberOfProcessIdsInList || processList->NumberOfProcessIdsInList == 0)) + { + hr = HRESULT_FROM_WIN32(ERROR_PROCESS_ABORTED); + // some error + goto Finished; + } + + if (processList->NumberOfProcessIdsInList > MAX_ACTIVE_CHILD_PROCESSES) + { + hr = HRESULT_FROM_WIN32(ERROR_CREATE_FAILED); + goto Finished; + } + + for (DWORD i=0; iNumberOfProcessIdsInList; i++) + { + dwPid = (DWORD)processList->ProcessIdList[i]; + if (dwPid != m_dwProcessId && + dwPid != dwWorkerProcessPid ) + { + m_hChildProcessHandles[m_cChildProcess] = OpenProcess( + PROCESS_QUERY_INFORMATION | SYNCHRONIZE | PROCESS_TERMINATE | PROCESS_DUP_HANDLE, + FALSE, + dwPid + ); + m_dwChildProcessIds[m_cChildProcess] = dwPid; + m_cChildProcess ++; + } + } + +Finished: + + if (processList != NULL) + { + HeapFree(GetProcessHeap(), 0, processList); + } + + return hr; +} + +HRESULT +SERVER_PROCESS::StopAllProcessesInJobObject( + VOID +) +{ + HRESULT hr = S_OK; + PJOBOBJECT_BASIC_PROCESS_ID_LIST processList = NULL; + HANDLE hProcess = NULL; + DWORD dwWorkerProcessPid = 0; + DWORD cbNumBytes = 1024; + DWORD dwRetries = 0; + + dwWorkerProcessPid = GetCurrentProcessId(); + + do + { + if (processList != NULL) + { + HeapFree(GetProcessHeap(), 0, processList); + processList = NULL; + + // resize + cbNumBytes = cbNumBytes * 2; + } + + processList = (PJOBOBJECT_BASIC_PROCESS_ID_LIST) HeapAlloc( + GetProcessHeap(), + 0, + cbNumBytes + ); + if (processList == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + RtlZeroMemory(processList, cbNumBytes); + + if (!QueryInformationJobObject( + m_hJobObject, + JobObjectBasicProcessIdList, + processList, + cbNumBytes, + NULL)) + { + DWORD dwError = GetLastError(); + if (dwError != ERROR_MORE_DATA) + { + hr = HRESULT_FROM_WIN32(dwError); + goto Finished; + } + } + + } while (dwRetries++ < 5 && + processList != NULL && + (processList->NumberOfAssignedProcesses > processList->NumberOfProcessIdsInList || processList->NumberOfProcessIdsInList == 0)); + + if (processList == NULL || (processList->NumberOfAssignedProcesses > processList->NumberOfProcessIdsInList || processList->NumberOfProcessIdsInList == 0)) + { + hr = HRESULT_FROM_WIN32(ERROR_NOT_ENOUGH_MEMORY); + // some error + goto Finished; + } + + for (DWORD i=0; iNumberOfProcessIdsInList; i++) + { + if (dwWorkerProcessPid != (DWORD)processList->ProcessIdList[i]) + { + hProcess = OpenProcess(PROCESS_TERMINATE, + FALSE, + (DWORD)processList->ProcessIdList[i]); + if (hProcess != NULL) + { + if (!TerminateProcess(hProcess, 1)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + } + else + { + WaitForSingleObject(hProcess, INFINITE); + } + + if (hProcess != NULL) + { + CloseHandle(hProcess); + hProcess = NULL; + } + } + } + } + +Finished: + + if (processList != NULL) + { + HeapFree(GetProcessHeap(), 0, processList); + } + + return hr; +} + +SERVER_PROCESS::SERVER_PROCESS() : + m_cRefs(1), + m_hProcessHandle(NULL), + m_hProcessWaitHandle(NULL), + m_dwProcessId(0), + m_cChildProcess(0), + m_fReady(FALSE), + m_lStopping(0L), + m_hStdoutHandle(NULL), + m_fStdoutLogEnabled(FALSE), + m_hJobObject(NULL), + m_pForwarderConnection(NULL), + m_dwListeningProcessId(0), + m_hListeningProcessHandle(NULL), + m_hShutdownHandle(NULL), + m_randomGenerator(std::random_device()()) +{ + //InterlockedIncrement(&g_dwActiveServerProcesses); + + for (INT i=0; iDereferenceForwarderConnection(); + m_pForwarderConnection = NULL; + } + +} + +SERVER_PROCESS::~SERVER_PROCESS() +{ + + CleanUp(); + + m_pEnvironmentVarTable = NULL; + // no need to free m_pEnvironmentVarTable, as it references to + // the same hash table hold by configuration. + // the hashtable memory will be freed once onfiguration got recycled + + if (m_pProcessManager != NULL) + { + m_pProcessManager->DereferenceProcessManager(); + m_pProcessManager = NULL; + } + + if (m_hStdoutHandle != NULL) + { + if (m_hStdoutHandle != INVALID_HANDLE_VALUE) + { + CloseHandle(m_hStdoutHandle); + } + m_hStdoutHandle = NULL; + } + + if (m_fStdoutLogEnabled) + { + m_Timer.CancelTimer(); + } + + if (!m_fStdoutLogEnabled && !m_struFullLogFile.IsEmpty()) + { + WIN32_FIND_DATA fileData; + HANDLE handle = FindFirstFile(m_struFullLogFile.QueryStr(), &fileData); + if (handle != INVALID_HANDLE_VALUE && + fileData.nFileSizeHigh == 0 && + fileData.nFileSizeLow == 0) + { + FindClose(handle); + // no need to check whether the deletion succeeds + // as nothing can be done + DeleteFile(m_struFullLogFile.QueryStr()); + } + } +} + +//static +VOID +CALLBACK +SERVER_PROCESS::ProcessHandleCallback( + _In_ PVOID pContext, + _In_ BOOL +) +{ + SERVER_PROCESS *pServerProcess = (SERVER_PROCESS*) pContext; + pServerProcess->HandleProcessExit(); +} + +HRESULT +SERVER_PROCESS::RegisterProcessWait( + PHANDLE phWaitHandle, + HANDLE hProcessToWaitOn +) +{ + HRESULT hr = S_OK; + NTSTATUS status = 0; + + _ASSERT(phWaitHandle != NULL && *phWaitHandle == NULL); + + *phWaitHandle = NULL; + + // wait thread will dereference. + ReferenceServerProcess(); + + status = RegisterWaitForSingleObject( + phWaitHandle, + hProcessToWaitOn, + (WAITORTIMERCALLBACKFUNC)&ProcessHandleCallback, + this, + INFINITE, + WT_EXECUTEONLYONCE | WT_EXECUTEINWAITTHREAD + ); + + if (status < 0) + { + hr = HRESULT_FROM_NT(status); + goto Finished; + } + +Finished: + + if (FAILED_LOG(hr)) + { + *phWaitHandle = NULL; + DereferenceServerProcess(); + } + + return hr; +} + +VOID +SERVER_PROCESS::HandleProcessExit( VOID ) +{ + BOOL fReady = FALSE; + DWORD dwProcessId = 0; + + if (InterlockedCompareExchange(&m_lStopping, 1L, 0L) == 0L) + { + CheckIfServerIsUp(m_dwPort, &dwProcessId, &fReady); + + if (!fReady) + { + EventLog::Info( + ASPNETCORE_EVENT_PROCESS_SHUTDOWN, + ASPNETCORE_EVENT_PROCESS_SHUTDOWN_MSG, + m_struAppFullPath.QueryStr(), + m_struPhysicalPath.QueryStr(), + m_dwProcessId, + m_dwPort); + + m_pProcessManager->ShutdownProcess(this); + } + + DereferenceServerProcess(); + } +} + +HRESULT +SERVER_PROCESS::SendShutdownHttpMessage( VOID ) +{ + HRESULT hr = S_OK; + HINTERNET hSession = NULL; + HINTERNET hConnect = NULL; + HINTERNET hRequest = NULL; + + STACK_STRU(strHeaders, 256); + STRU strAppToken; + STRU strUrl; + DWORD dwStatusCode = 0; + DWORD dwSize = sizeof(dwStatusCode); + + hSession = WinHttpOpen(L"", + WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, + WINHTTP_NO_PROXY_NAME, + WINHTTP_NO_PROXY_BYPASS, + 0); + + if (hSession == NULL) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + hConnect = WinHttpConnect(hSession, + L"127.0.0.1", + (USHORT)m_dwPort, + 0); + + if (hConnect == NULL) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + if (m_struAppVirtualPath.QueryCCH() > 1) + { + // app path size is 1 means site root, i.e., "/" + // we don't want to add duplicated '/' to the request url + // otherwise the request will fail + strUrl.Copy(m_struAppVirtualPath); + } + strUrl.Append(L"/iisintegration"); + + hRequest = WinHttpOpenRequest(hConnect, + L"POST", + strUrl.QueryStr(), + NULL, + WINHTTP_NO_REFERER, + NULL, + 0); + + if (hRequest == NULL) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + // set timeout + if (!WinHttpSetTimeouts(hRequest, + m_dwShutdownTimeLimitInMS, // dwResolveTimeout + m_dwShutdownTimeLimitInMS, // dwConnectTimeout + m_dwShutdownTimeLimitInMS, // dwSendTimeout + m_dwShutdownTimeLimitInMS)) // dwReceiveTimeout + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + // set up the shutdown headers + if (FAILED_LOG(hr = strHeaders.Append(L"MS-ASPNETCORE-EVENT:shutdown \r\n")) || + FAILED_LOG(hr = strAppToken.Append(L"MS-ASPNETCORE-TOKEN:")) || + FAILED_LOG(hr = strAppToken.AppendA(m_straGuid.QueryStr())) || + FAILED_LOG(hr = strHeaders.Append(strAppToken.QueryStr()))) + { + goto Finished; + } + + if (!WinHttpSendRequest(hRequest, + strHeaders.QueryStr(), // pwszHeaders + strHeaders.QueryCCH(), // dwHeadersLength + WINHTTP_NO_REQUEST_DATA, + 0, // dwOptionalLength + 0, // dwTotalLength + 0)) // dwContext + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + if (!WinHttpReceiveResponse(hRequest , NULL)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + if (!WinHttpQueryHeaders(hRequest, + WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, + WINHTTP_HEADER_NAME_BY_INDEX, + &dwStatusCode, + &dwSize, + WINHTTP_NO_HEADER_INDEX)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + if (dwStatusCode != 202) + { + // not expected http status + hr = E_FAIL; + } + + // log + EventLog::Info( + ASPNETCORE_EVENT_SENT_SHUTDOWN_HTTP_REQUEST, + ASPNETCORE_EVENT_SENT_SHUTDOWN_HTTP_REQUEST_MSG, + m_dwProcessId, + dwStatusCode); + +Finished: + if (hRequest) + { + WinHttpCloseHandle(hRequest); + hRequest = NULL; + } + if (hConnect) + { + WinHttpCloseHandle(hConnect); + hConnect = NULL; + } + if (hSession) + { + WinHttpCloseHandle(hSession); + hSession = NULL; + } + return hr; +} + +//static +VOID +SERVER_PROCESS::SendShutDownSignal( + LPVOID lpParam +) +{ + SERVER_PROCESS* pThis = static_cast(lpParam); + DBG_ASSERT(pThis); + pThis->SendShutDownSignalInternal(); +} + +// +// send shutdown message first, if fail then send +// ctrl-c to the backend process to let it gracefully shutdown +// +VOID +SERVER_PROCESS::SendShutDownSignalInternal( + VOID +) +{ + ReferenceServerProcess(); + + if (FAILED_LOG(SendShutdownHttpMessage())) + { + // + // failed to send shutdown http message + // try send ctrl signal + // + HWND hCurrentConsole = NULL; + BOOL fFreeConsole = FALSE; + hCurrentConsole = GetConsoleWindow(); + if (hCurrentConsole) + { + // free current console first, as we may have one, e.g., hostedwebcore case + fFreeConsole = FreeConsole(); + } + + if (AttachConsole(m_dwProcessId)) + { + // As we called CreateProcess with CREATE_NEW_PROCESS_GROUP + // call ctrl-break instead of ctrl-c as child process ignores ctrl-c + if (!GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, m_dwProcessId)) + { + // failed to send the ctrl signal. terminate the backend process immediately instead of waiting for timeout + TerminateBackendProcess(); + } + FreeConsole(); + + if (fFreeConsole) + { + // IISExpress and hostedwebcore w3wp run as background process + // have to attach console back to ensure post app_offline scenario still works + AttachConsole(ATTACH_PARENT_PROCESS); + } + } + else + { + // terminate the backend process immediately instead of waiting for timeout + TerminateBackendProcess(); + } + } + + DereferenceServerProcess(); +} + +VOID +SERVER_PROCESS::TerminateBackendProcess( + VOID +) +{ + if (InterlockedCompareExchange(&m_lStopping, 1L, 0L) == 0L) + { + // backend process will be terminated, remove the waitcallback + if (m_hProcessWaitHandle != NULL) + { + UnregisterWait(m_hProcessWaitHandle); + + // as we skipped process exit callback (ProcessHandleCallback), + // need to dereference the object otherwise memory leak + DereferenceServerProcess(); + + m_hProcessWaitHandle = NULL; + } + + // cannot gracefully shutdown or timeout, terminate the process + if (m_hProcessHandle != NULL && m_hProcessHandle != INVALID_HANDLE_VALUE) + { + TerminateProcess(m_hProcessHandle, 0); + m_hProcessHandle = NULL; + } + + // log a warning for ungraceful shutdown + EventLog::Warn( + ASPNETCORE_EVENT_GRACEFUL_SHUTDOWN_FAILURE, + ASPNETCORE_EVENT_GRACEFUL_SHUTDOWN_FAILURE_MSG, + m_dwProcessId); + } +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/serverprocess.h b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/serverprocess.h new file mode 100644 index 0000000000..3073835e05 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/serverprocess.h @@ -0,0 +1,292 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include + +#define MIN_PORT 1025 +#define MAX_PORT 48000 +#define MAX_RETRY 10 +#define MAX_ACTIVE_CHILD_PROCESSES 16 +#define LOCALHOST "127.0.0.1" +#define ASPNETCORE_PORT_STR L"ASPNETCORE_PORT" +#define ASPNETCORE_PORT_ENV_STR L"ASPNETCORE_PORT=" +#define ASPNETCORE_APP_PATH_ENV_STR L"ASPNETCORE_APPL_PATH=" +#define ASPNETCORE_APP_TOKEN_ENV_STR L"ASPNETCORE_TOKEN=" +#define ASPNETCORE_APP_PATH_ENV_STR L"ASPNETCORE_APPL_PATH=" + +class PROCESS_MANAGER; + +class SERVER_PROCESS +{ +public: + SERVER_PROCESS(); + + HRESULT + Initialize( + _In_ PROCESS_MANAGER *pProcessManager, + _In_ STRU *pszProcessExePath, + _In_ STRU *pszArguments, + _In_ DWORD dwStartupTimeLimitInMS, + _In_ DWORD dwShtudownTimeLimitInMS, + _In_ BOOL fWindowsAuthEnabled, + _In_ BOOL fBasicAuthEnabled, + _In_ BOOL fAnonymousAuthEnabled, + _In_ ENVIRONMENT_VAR_HASH* pEnvironmentVariables, + _In_ BOOL fStdoutLogEnabled, + _In_ BOOL fWebSocketSupported, + _In_ STRU *pstruStdoutLogFile, + _In_ STRU *pszAppPhysicalPath, + _In_ STRU *pszAppPath, + _In_ STRU *pszAppVirtualPath + ); + + HRESULT + StartProcess( VOID ); + + HRESULT + SetWindowsAuthToken( + _In_ HANDLE hToken, + _Out_ LPHANDLE pTargeTokenHandle + ); + + BOOL + IsReady( + VOID + ) + { + return m_fReady; + } + + BOOL + IsDebuggerAttached( + VOID + ) + { + return m_fDebuggerAttached; + } + + VOID + StopProcess( + VOID + ); + + DWORD + GetPort() + { + return m_dwPort; + } + + VOID + ReferenceServerProcess( + VOID + ) + { + InterlockedIncrement(&m_cRefs); + } + + VOID + DereferenceServerProcess( + VOID + ) + { + _ASSERT(m_cRefs != 0 ); + if (InterlockedDecrement(&m_cRefs) == 0) + { + delete this; + } + } + + virtual + ~SERVER_PROCESS(); + + static + VOID + CALLBACK + ProcessHandleCallback( + _In_ PVOID pContext, + _In_ BOOL + ); + + VOID + HandleProcessExit( + VOID + ); + + FORWARDER_CONNECTION* + QueryWinHttpConnection( + VOID + ) + { + return m_pForwarderConnection; + } + + LPCSTR + QueryGuid() + { + return m_straGuid.QueryStr(); + }; + + VOID + SendSignal( + VOID + ); + +private: + VOID + CleanUp(); + + HRESULT + SetupJobObject( + VOID + ); + + BOOL + IsDebuggerIsAttached( + VOID + ); + + HRESULT + StopAllProcessesInJobObject( + VOID + ); + + HRESULT + SetupStdHandles( + _Inout_ LPSTARTUPINFOW pStartupInfo + ); + + HRESULT + CheckIfServerIsUp( + _In_ DWORD dwPort, + _Out_ DWORD * pdwProcessId, + _Out_ BOOL * pfReady + ); + + HRESULT + RegisterProcessWait( + _In_ PHANDLE phWaitHandle, + _In_ HANDLE hProcessToWaitOn + ); + + HRESULT + GetChildProcessHandles( + VOID + ); + + HRESULT + SetupListenPort( + ENVIRONMENT_VAR_HASH *pEnvironmentVarTable, + BOOL *pfCriticalError + ); + + HRESULT + SetupAppPath( + ENVIRONMENT_VAR_HASH* pEnvironmentVarTable + ); + + HRESULT + SetupAppToken( + ENVIRONMENT_VAR_HASH* pEnvironmentVarTable + ); + + HRESULT + OutputEnvironmentVariables( + MULTISZ* pmszOutput, + ENVIRONMENT_VAR_HASH* pEnvironmentVarTable + ); + + HRESULT + SetupCommandLine( + STRU* pstrCommandLine + ); + + HRESULT + PostStartCheck( + VOID + ); + + HRESULT + GetRandomPort( + DWORD* pdwPickedPort, + DWORD dwExcludedPort + ); + + static + VOID + SendShutDownSignal( + LPVOID lpParam + ); + + VOID + SendShutDownSignalInternal( + VOID + ); + + HRESULT + SendShutdownHttpMessage( + VOID + ); + + VOID + TerminateBackendProcess( + VOID + ); + + FORWARDER_CONNECTION *m_pForwarderConnection; + BOOL m_fStdoutLogEnabled; + BOOL m_fWebSocketSupported; + BOOL m_fWindowsAuthEnabled; + BOOL m_fBasicAuthEnabled; + BOOL m_fAnonymousAuthEnabled; + BOOL m_fDebuggerAttached; + + STTIMER m_Timer; + SOCKET m_socket; + + STRU m_struLogFile; + STRU m_struFullLogFile; + STRU m_ProcessPath; + STRU m_Arguments; + STRU m_struAppVirtualPath; // e.g., '/' for site + STRU m_struAppFullPath; // e.g., /LM/W3SVC/4/ROOT/Inproc + STRU m_struPhysicalPath; // e.g., c:/test/mysite + STRU m_struPort; + STRU m_struCommandLine; + + volatile LONG m_lStopping; + volatile BOOL m_fReady; + mutable LONG m_cRefs; + + std::mt19937 m_randomGenerator; + + DWORD m_dwPort; + DWORD m_dwStartupTimeLimitInMS; + DWORD m_dwShutdownTimeLimitInMS; + DWORD m_cChildProcess; + DWORD m_dwChildProcessIds[MAX_ACTIVE_CHILD_PROCESSES]; + DWORD m_dwProcessId; + DWORD m_dwListeningProcessId; + + STRA m_straGuid; + + HANDLE m_hJobObject; + HANDLE m_hStdoutHandle; + // + // m_hProcessHandle is the handle to process this object creates. + // + HANDLE m_hProcessHandle; + HANDLE m_hListeningProcessHandle; + HANDLE m_hProcessWaitHandle; + HANDLE m_hShutdownHandle; + // + // m_hChildProcessHandle is the handle to process created by + // m_hProcessHandle process if it does. + // + HANDLE m_hChildProcessHandles[MAX_ACTIVE_CHILD_PROCESSES]; + HANDLE m_hChildProcessWaitHandles[MAX_ACTIVE_CHILD_PROCESSES]; + + PROCESS_MANAGER *m_pProcessManager; + ENVIRONMENT_VAR_HASH *m_pEnvironmentVarTable ; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/stdafx.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/stdafx.cpp new file mode 100644 index 0000000000..12fcb1d436 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/stdafx.cpp @@ -0,0 +1,4 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +// Do not remove this file. It is used for precompiled header generation diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/stdafx.h b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/stdafx.h new file mode 100644 index 0000000000..91b6b002dc --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/stdafx.h @@ -0,0 +1,120 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once +#pragma warning( disable : 4091) + +// +// System related headers +// +#define _WINSOCKAPI_ + +#define NTDDI_VERSION 0x06010000 +#define WINVER 0x0601 +#define _WIN32_WINNT 0x0601 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// This should remove our issue of compiling for win7 without header files. +// We force the Windows 8 version check logic in iiswebsocket.h to succeed even though we're compiling for Windows 7. +// Then, we set the version defines back to Windows 7 to for the remainder of the compilation. +#undef NTDDI_VERSION +#undef WINVER +#undef _WIN32_WINNT +#define NTDDI_VERSION 0x06020000 +#define WINVER 0x0602 +#define _WIN32_WINNT 0x0602 +#include +#undef NTDDI_VERSION +#undef WINVER +#undef _WIN32_WINNT + +#define NTDDI_VERSION 0x06010000 +#define WINVER 0x0601 +#define _WIN32_WINNT 0x0601 + +// IIS Lib +#include "acache.h" +#include "multisz.h" +#include "multisza.h" +#include "base64.h" +#include "listentry.h" +#include "debugutil.h" + +// Common lib +#include "requesthandler.h" +#include "application.h" +#include "resources.h" +#include "EventTracing.h" +#include "aspnetcore_msg.h" +#include "requesthandler_config.h" + +#include "sttimer.h" +#include "websockethandler.h" +#include "responseheaderhash.h" +#include "protocolconfig.h" +#include "forwarderconnection.h" +#include "serverprocess.h" +#include "processmanager.h" +#include "forwardinghandler.h" +#include "outprocessapplication.h" +#include "winhttphelper.h" + +#include "environmentvariablehelpers.h" + +#ifdef max +#undef max +template inline T max(T a, T b) +{ + return a > b ? a : b; +} +#endif + +#ifdef min +#undef min +template inline T min(T a, T b) +{ + return a < b ? a : b; +} +#endif + +inline bool IsSpace(char ch) +{ + switch (ch) + { + case 32: // ' ' + case 9: // '\t' + case 10: // '\n' + case 13: // '\r' + case 11: // '\v' + case 12: // '\f' + return true; + default: + return false; + } +} + +extern BOOL g_fAsyncDisconnectAvailable; +extern BOOL g_fWinHttpNonBlockingCallbackAvailable; +extern BOOL g_fWebSocketStaticInitialize; +extern BOOL g_fNsiApiNotSupported; +extern BOOL g_fEnableReferenceCountTracing; +extern BOOL g_fProcessDetach; +extern DWORD g_dwActiveServerProcesses; +extern DWORD g_OptionalWinHttpFlags; +extern SRWLOCK g_srwLockRH; +extern HINTERNET g_hWinhttpSession; +extern DWORD g_dwTlsIndex; +extern HANDLE g_hEventLog; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/url_utility.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/url_utility.cpp new file mode 100644 index 0000000000..64118b61b6 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/url_utility.cpp @@ -0,0 +1,122 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "url_utility.h" + +#include +#include "debugutil.h" +#include "exceptions.h" + +// static +HRESULT +URL_UTILITY::SplitUrl( + PCWSTR pszDestinationUrl, + BOOL *pfSecure, + STRU *pstrDestination, + STRU *pstrUrl +) +/*++ + +Routine Description: + + Split the URL specified for forwarding into its specific components + The format of the URL looks like + http[s]://destination[:port]/path + when port is omitted, the default port for that specific protocol is used + when host is omitted, it gets the same value as the destination + +Arguments: + + pszDestinationUrl - the url to be split up + pfSecure - SSL to be used in forwarding? + pstrDestination - destination + pDestinationPort - port + pstrUrl - URL + +Return Value: + + HRESULT + +--*/ +{ + // + // First determine if the target is secure + // + if (_wcsnicmp(pszDestinationUrl, L"http://", 7) == 0) + { + *pfSecure = FALSE; + pszDestinationUrl += 7; + } + else if (_wcsnicmp(pszDestinationUrl, L"https://", 8) == 0) + { + *pfSecure = TRUE; + pszDestinationUrl += 8; + } + else + { + RETURN_HR(HRESULT_FROM_WIN32(ERROR_INVALID_DATA)); + } + + if (*pszDestinationUrl == L'\0') + { + RETURN_HR(HRESULT_FROM_WIN32(ERROR_INVALID_DATA)); + } + + // + // Find the 3rd slash corresponding to the url + // + LPCWSTR pszSlash = wcschr(pszDestinationUrl, L'/'); + if (pszSlash == NULL) + { + RETURN_IF_FAILED(pstrUrl->Copy(L"/", 1)); + RETURN_IF_FAILED(pstrDestination->Copy(pszDestinationUrl)); + } + else + { + RETURN_IF_FAILED(pstrUrl->Copy(pszSlash)); + RETURN_IF_FAILED(pstrDestination->Copy(pszDestinationUrl, + (DWORD)(pszSlash - pszDestinationUrl))); + } + + return S_OK; +} + +// Change a hexadecimal digit to its numerical equivalent +#define TOHEX( ch ) \ + ((ch) > L'9' ? \ + (ch) >= L'a' ? \ + (ch) - L'a' + 10 : \ + (ch) - L'A' + 10 \ + : (ch) - L'0') + +HRESULT +URL_UTILITY::EscapeAbsPath( + IHttpRequest * pRequest, + STRU * strEscapedUrl +) +{ + STRU strAbsPath; + LPCWSTR pszAbsPath = NULL; + LPCWSTR pszFindStr = NULL; + + RETURN_IF_FAILED(strAbsPath.Copy( pRequest->GetRawHttpRequest()->CookedUrl.pAbsPath, + pRequest->GetRawHttpRequest()->CookedUrl.AbsPathLength / sizeof(WCHAR) )); + + pszAbsPath = strAbsPath.QueryStr(); + pszFindStr = wcschr(pszAbsPath, L'?'); + + while(pszFindStr != NULL) + { + RETURN_IF_FAILED(strEscapedUrl->Append( pszAbsPath, pszFindStr - pszAbsPath)); + RETURN_IF_FAILED(strEscapedUrl->Append(L"%3F")); + pszAbsPath = pszFindStr + 1; + pszFindStr = wcschr(pszAbsPath, L'?'); + } + + RETURN_IF_FAILED(strEscapedUrl->Append(pszAbsPath)); + RETURN_IF_FAILED(strEscapedUrl->Append(pRequest->GetRawHttpRequest()->CookedUrl.pQueryString, + pRequest->GetRawHttpRequest()->CookedUrl.QueryStringLength / sizeof(WCHAR))); + + return S_OK; +} + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/url_utility.h b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/url_utility.h new file mode 100644 index 0000000000..a096a96cf0 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/url_utility.h @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#include "stdafx.h" + +#include +#include "stringu.h" + +class URL_UTILITY +{ +public: + + static + HRESULT + SplitUrl( + PCWSTR pszDestinationUrl, + BOOL *pfSecure, + STRU *pstrDestination, + STRU *pstrUrl + ); + + static HRESULT + EscapeAbsPath( + IHttpRequest * pRequest, + STRU * strEscapedUrl + ); +}; + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/websockethandler.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/websockethandler.cpp new file mode 100644 index 0000000000..ecd97737fe --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/websockethandler.cpp @@ -0,0 +1,1128 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +/*++ + +Abstract: + + Main Handler for websocket requests. + + Initiates websocket connection to backend. + Uses WinHttp API's for backend connections, + and IIS Websocket API's for sending/receiving + websocket traffic. + + Transfers data between the two IO endpoints. + +----------------- +Read Loop Design +----------------- +When a read IO completes successfully on any endpoints, Asp.Net Core Module doesn't +immediately issue the next read. The next read is initiated only after +the read data is sent to the other endpoint. As soon as this send completes, +we initiate the next IO. It should be noted that the send complete merely +indicates the API completion from HTTP, and not necessarily over the network. + +This prevents the need for data buffering at the Asp.Net Core Module level. + +--*/ + +#include "websockethandler.h" +#include "exceptions.h" + +SRWLOCK WEBSOCKET_HANDLER::sm_RequestsListLock; + +LIST_ENTRY WEBSOCKET_HANDLER::sm_RequestsListHead; + +TRACE_LOG * WEBSOCKET_HANDLER::sm_pTraceLog; + +WEBSOCKET_HANDLER::WEBSOCKET_HANDLER() : + _pHttpContext(NULL), + _pWebSocketContext(NULL), + _hWebSocketRequest(NULL), + _pHandler(NULL), + _dwOutstandingIo(0), + _fCleanupInProgress(FALSE), + _fIndicateCompletionToIis(FALSE), + _fHandleClosed(FALSE), + _fReceivedCloseMsg(FALSE) +{ + LOG_TRACE(L"WEBSOCKET_HANDLER::WEBSOCKET_HANDLER"); + + InitializeCriticalSectionAndSpinCount(&_RequestLock, 1000); + InsertRequest(); +} + +VOID +WEBSOCKET_HANDLER::Terminate( + VOID + ) +{ + LOG_TRACE(L"WEBSOCKET_HANDLER::Terminate"); + if (!_fHandleClosed) + { + RemoveRequest(); + _fCleanupInProgress = TRUE; + + if (_pHttpContext != NULL) + { + _pHttpContext->CancelIo(); + _pHttpContext = NULL; + } + if (_hWebSocketRequest) + { + WinHttpCloseHandle(_hWebSocketRequest); + _hWebSocketRequest = NULL; + } + + _pWebSocketContext = NULL; + DeleteCriticalSection(&_RequestLock); + + delete this; + } +} + +//static +HRESULT +WEBSOCKET_HANDLER::StaticInitialize( + BOOL fEnableReferenceCountTracing + ) +/*++ + + Routine Description: + + Initialize structures required for idle connection cleanup. + +--*/ +{ + if (!g_fWebSocketStaticInitialize) + { + return S_OK; + } + + if (fEnableReferenceCountTracing) + { + // + // If tracing is enabled, keep track of all websocket requests + // for debugging purposes. + // + InitializeListHead (&sm_RequestsListHead); + sm_pTraceLog = CreateRefTraceLog( 10000, 0 ); + } + + InitializeSRWLock(&sm_RequestsListLock); + + return S_OK; +} + +//static +VOID +WEBSOCKET_HANDLER::StaticTerminate( + VOID + ) +{ + if (!g_fWebSocketStaticInitialize) + { + return; + } + + if (sm_pTraceLog) + { + DestroyRefTraceLog(sm_pTraceLog); + sm_pTraceLog = NULL; + } +} + +VOID +WEBSOCKET_HANDLER::InsertRequest( + VOID + ) +{ + if (g_fEnableReferenceCountTracing) + { + AcquireSRWLockExclusive(&sm_RequestsListLock); + InsertTailList(&sm_RequestsListHead, &_listEntry); + ReleaseSRWLockExclusive( &sm_RequestsListLock); + } +} + +//static +VOID +WEBSOCKET_HANDLER::RemoveRequest( + VOID + ) +{ + if (g_fEnableReferenceCountTracing) + { + AcquireSRWLockExclusive(&sm_RequestsListLock); + RemoveEntryList(&_listEntry); + ReleaseSRWLockExclusive( &sm_RequestsListLock); + } +} + +VOID +WEBSOCKET_HANDLER::IncrementOutstandingIo( + VOID + ) +{ + LONG dwOutstandingIo = InterlockedIncrement(&_dwOutstandingIo); + if (sm_pTraceLog) + { + WriteRefTraceLog(sm_pTraceLog, dwOutstandingIo, this); + } +} + +VOID +WEBSOCKET_HANDLER::DecrementOutstandingIo( + VOID + ) +/*++ + Routine Description: + Decrements outstanding IO count. + + This indicates completion to IIS if all outstanding IO + has been completed, and a Cleanup was triggered for this + connection (denoted by _fIndicateCompletionToIis). + +--*/ +{ + LONG dwOutstandingIo = InterlockedDecrement (&_dwOutstandingIo); + + if (sm_pTraceLog) + { + WriteRefTraceLog(sm_pTraceLog, dwOutstandingIo, this); + } + + if (dwOutstandingIo == 0 && _fIndicateCompletionToIis) + { + IndicateCompletionToIIS(); + } +} + +VOID +WEBSOCKET_HANDLER::IndicateCompletionToIIS( + VOID + ) +/*++ + Routine Description: + Indicates completion to IIS. + + This returns a Pending Status, so that forwarding handler has a chance + to do book keeping when request is finally done. + +--*/ +{ + LOG_TRACEF(L"WEBSOCKET_HANDLER::IndicateCompletionToIIS called %d", _dwOutstandingIo); + + // + // close Websocket handle. This will triger a WinHttp callback + // on handle close, then let IIS pipeline continue. + // Make sure no pending IO as there is no IIS websocket cancelation, + // any unexpected callback will lead to AV. Revisit it once CanelOutGoingIO works + // + if (_hWebSocketRequest != NULL && _dwOutstandingIo == 0) + { + LOG_TRACE(L"WEBSOCKET_HANDLER::IndicateCompletionToIIS"); + + _pHandler->SetStatus(FORWARDER_DONE); + _fHandleClosed = TRUE; + WinHttpCloseHandle(_hWebSocketRequest); + _hWebSocketRequest = NULL; + } +} + +HRESULT +WEBSOCKET_HANDLER::ProcessRequest( + FORWARDING_HANDLER *pHandler, + IHttpContext *pHttpContext, + HINTERNET hRequest, + BOOL* pfHandleCreated +) +/*++ + +Routine Description: + + Entry point to WebSocket Handler: + + This routine is called after the 101 response was successfully sent to + the client. + This routine get's a websocket handle to winhttp, + websocket handle to IIS's websocket context, and initiates IO + in these two endpoints. + + +--*/ +{ + HRESULT hr = S_OK; + //DWORD dwBuffSize = RECEIVE_BUFFER_SIZE; + + *pfHandleCreated = FALSE; + _pHandler = pHandler; + + EnterCriticalSection(&_RequestLock); + LOG_TRACEF(L"WEBSOCKET_HANDLER::ProcessRequest"); + + // + // Cache the points to IHttpContext3 + // + hr = HttpGetExtendedInterface(g_pHttpServer, + pHttpContext, + &_pHttpContext); + if (FAILED_LOG(hr)) + { + goto Finished; + } + + // + // Get pointer to IWebSocketContext for IIS websocket IO. + // + + _pWebSocketContext = (IWebSocketContext *) _pHttpContext-> + GetNamedContextContainer()->GetNamedContext(IIS_WEBSOCKET); + if ( _pWebSocketContext == NULL ) + { + hr = HRESULT_FROM_WIN32( ERROR_FILE_NOT_FOUND ); + goto Finished; + } + + // + // Get Handle to Winhttp's websocket context. + // + _hWebSocketRequest = WINHTTP_HELPER::sm_pfnWinHttpWebSocketCompleteUpgrade( + hRequest, + (DWORD_PTR) pHandler); + + if (_hWebSocketRequest == NULL) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + *pfHandleCreated = TRUE; + + // + // Resize the send & receive buffers to be more conservative (and avoid DoS attacks). + // NOTE: The two WinHTTP options below were added for WinBlue, so we can't + // rely on their existence. + // + + //if (!WinHttpSetOption(_hWebSocketRequest, + // WINHTTP_OPTION_WEB_SOCKET_RECEIVE_BUFFER_SIZE, + // &dwBuffSize, + // sizeof(dwBuffSize))) + //{ + // DWORD dwRet = GetLastError(); + // if ( dwRet != ERROR_WINHTTP_INVALID_OPTION ) + // { + // hr = HRESULT_FROM_WIN32(dwRet); + // goto Finished; + // } + //} + + //if (!WinHttpSetOption(_hWebSocketRequest, + // WINHTTP_OPTION_WEB_SOCKET_SEND_BUFFER_SIZE, + // &dwBuffSize, + // sizeof(dwBuffSize))) + //{ + // DWORD dwRet = GetLastError(); + // if ( dwRet != ERROR_WINHTTP_INVALID_OPTION ) + // { + // hr = HRESULT_FROM_WIN32(dwRet); + // goto Finished; + // } + //} + + // + // Initiate Read on IIS + // + hr = DoIisWebSocketReceive(); + if (FAILED_LOG(hr)) + { + goto Finished; + } + + // + // Initiate Read on WinHttp + // + + hr = DoWinHttpWebSocketReceive(); + if (FAILED_LOG(hr)) + { + goto Finished; + } + +Finished: + LeaveCriticalSection(&_RequestLock); + + if (FAILED_LOG(hr)) + { + LOG_ERRORF(L"Process Request Failed with HR=%08x", hr); + } + + return hr; +} + +HRESULT +WEBSOCKET_HANDLER::DoIisWebSocketReceive( + VOID +) +/*++ + +Routine Description: + + Initiates a websocket receive on the IIS Websocket Context. + + +--*/ +{ + HRESULT hr = S_OK; + DWORD dwBufferSize = RECEIVE_BUFFER_SIZE; + BOOL fUtf8Encoded; + BOOL fFinalFragment; + BOOL fClose; + + LOG_TRACE(L"WEBSOCKET_HANDLER::DoIisWebSocketReceive"); + + IncrementOutstandingIo(); + + hr = _pWebSocketContext->ReadFragment( + &_IisReceiveBuffer, + &dwBufferSize, + TRUE, + &fUtf8Encoded, + &fFinalFragment, + &fClose, + OnReadIoCompletion, + this, + NULL); + if (FAILED_LOG(hr)) + { + DecrementOutstandingIo(); + LOG_ERRORF(L"WEBSOCKET_HANDLER::DoIisWebSocketSend failed with %08x", hr); + } + + return hr; +} + +HRESULT +WEBSOCKET_HANDLER::DoWinHttpWebSocketReceive( + VOID +) +/*++ + +Routine Description: + + Initiates a websocket receive on WinHttp + + +--*/ +{ + HRESULT hr = S_OK; + DWORD dwError = NO_ERROR; + + LOG_TRACE(L"WEBSOCKET_HANDLER::DoWinHttpWebSocketReceive"); + + IncrementOutstandingIo(); + + dwError = WINHTTP_HELPER::sm_pfnWinHttpWebSocketReceive( + _hWebSocketRequest, + &_WinHttpReceiveBuffer, + RECEIVE_BUFFER_SIZE, + NULL, + NULL); + + if (dwError != NO_ERROR) + { + DecrementOutstandingIo(); + hr = HRESULT_FROM_WIN32(dwError); + LOG_ERRORF(L"WEBSOCKET_HANDLER::DoWinHttpWebSocketReceive failed with %08x", hr); + } + + return hr; +} + +HRESULT +WEBSOCKET_HANDLER::DoIisWebSocketSend( + DWORD cbData, + WINHTTP_WEB_SOCKET_BUFFER_TYPE eBufferType +) +/*++ + +Routine Description: + + Initiates a websocket send on IIS + +--*/ +{ + HRESULT hr = S_OK; + BOOL fUtf8Encoded = FALSE; + BOOL fFinalFragment = FALSE; + BOOL fClose = FALSE; + + LOG_TRACEF(L"WEBSOCKET_HANDLER::DoIisWebSocketSend %d", eBufferType); + + if (eBufferType == WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE) + { + // + // Query Close Status from WinHttp + // + + DWORD dwError = NO_ERROR; + USHORT uStatus; + DWORD dwReceived = 0; + STACK_STRU(strCloseReason, 128); + + dwError = WINHTTP_HELPER::sm_pfnWinHttpWebSocketQueryCloseStatus( + _hWebSocketRequest, + &uStatus, + &_WinHttpReceiveBuffer, + RECEIVE_BUFFER_SIZE, + &dwReceived); + + if (dwError != NO_ERROR) + { + hr = HRESULT_FROM_WIN32(dwError); + goto Finished; + } + + // + // Convert close reason to WCHAR + // + hr = strCloseReason.CopyA((PCSTR)&_WinHttpReceiveBuffer, + dwReceived); + if (FAILED_LOG(hr)) + { + goto Finished; + } + + IncrementOutstandingIo(); + // + // Backend end may start close hand shake first + // Need to indicate no more receive should be called on WinHttp connection + // + _fReceivedCloseMsg = TRUE; + _fIndicateCompletionToIis = TRUE; + + // + // Send close to IIS. + // + hr = _pWebSocketContext->SendConnectionClose( + TRUE, + uStatus, + uStatus == 1005 ? NULL : strCloseReason.QueryStr(), + OnWriteIoCompletion, + this, + NULL); + } + else + { + // + // Get equivalant flags for IIS API from buffer type. + // + + WINHTTP_HELPER::GetFlagsFromBufferType(eBufferType, + &fUtf8Encoded, + &fFinalFragment, + &fClose); + + IncrementOutstandingIo(); + + // + // Do the Send. + // + hr = _pWebSocketContext->WriteFragment( + &_WinHttpReceiveBuffer, + &cbData, + TRUE, + fUtf8Encoded, + fFinalFragment, + OnWriteIoCompletion, + this, + NULL); + } + + if (FAILED_LOG(hr)) + { + DecrementOutstandingIo(); + } + +Finished: + if (FAILED_LOG(hr)) + { + LOG_ERRORF(L"WEBSOCKET_HANDLER::DoIisWebSocketSend failed with %08x", hr); + } + + return hr; +} + +HRESULT +WEBSOCKET_HANDLER::DoWinHttpWebSocketSend( + DWORD cbData, + WINHTTP_WEB_SOCKET_BUFFER_TYPE eBufferType +) +/*++ + +Routine Description: + + Initiates a websocket send on WinHttp + +--*/ +{ + DWORD dwError = NO_ERROR; + HRESULT hr = S_OK; + + LOG_TRACEF(L"WEBSOCKET_HANDLER::DoWinHttpWebSocketSend, %d", eBufferType); + + if (eBufferType == WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE) + { + USHORT uStatus; + LPCWSTR pszReason; + STACK_STRA(strCloseReason, 128); + + // + // Get Close status from IIS. + // + hr = _pWebSocketContext->GetCloseStatus(&uStatus, + &pszReason); + + if (FAILED_LOG(hr)) + { + goto Finished; + } + + // + // Convert status to UTF8 + // + hr = strCloseReason.CopyWToUTF8Unescaped(pszReason); + if (FAILED_LOG(hr)) + { + goto Finished; + } + + IncrementOutstandingIo(); + + // + // Send Close. + // + dwError = WINHTTP_HELPER::sm_pfnWinHttpWebSocketShutdown( + _hWebSocketRequest, + uStatus, + strCloseReason.QueryCCH() == 0 ? NULL : (PVOID) strCloseReason.QueryStr(), + strCloseReason.QueryCCH()); + + if (dwError == ERROR_IO_PENDING) + { + // + // Call will complete asynchronously, return. + // ignore error. + // + LOG_TRACE(L"WEBSOCKET_HANDLER::DoWinhttpWebSocketSend IO_PENDING"); + + dwError = NO_ERROR; + } + else + { + if (dwError == NO_ERROR) + { + // + // Call completed synchronously. + // + LOG_TRACE(L"WEBSOCKET_HANDLER::DoWinhttpWebSocketSend Shutdown successful."); + } + } + } + else + { + IncrementOutstandingIo(); + + dwError = WINHTTP_HELPER::sm_pfnWinHttpWebSocketSend( + _hWebSocketRequest, + eBufferType, + cbData == 0 ? NULL : &_IisReceiveBuffer, + cbData + ); + } + + if (dwError != NO_ERROR) + { + hr = HRESULT_FROM_WIN32(dwError); + DecrementOutstandingIo(); + goto Finished; + } + +Finished: + if (FAILED_LOG(hr)) + { + LOG_ERRORF(L"WEBSOCKET_HANDLER::DoWinHttpWebSocketSend failed with %08x", hr); + } + + return hr; +} + +//static +VOID +WINAPI +WEBSOCKET_HANDLER::OnReadIoCompletion( + HRESULT hrError, + VOID * pvCompletionContext, + DWORD cbIO, + BOOL fUTF8Encoded, + BOOL fFinalFragment, + BOOL fClose + ) +/*++ + + Routine Description: + + Completion routine for Read's from IIS pipeline. + +--*/ +{ + WEBSOCKET_HANDLER * pHandler = (WEBSOCKET_HANDLER *) + pvCompletionContext; + + pHandler->OnIisReceiveComplete( + hrError, + cbIO, + fUTF8Encoded, + fFinalFragment, + fClose + ); +} + +//static +VOID +WINAPI +WEBSOCKET_HANDLER::OnWriteIoCompletion( + HRESULT hrError, + VOID * pvCompletionContext, + DWORD cbIO, + BOOL fUTF8Encoded, + BOOL fFinalFragment, + BOOL fClose + ) +/*++ + Routine Description: + + Completion routine for Write's from IIS pipeline. + +--*/ +{ + WEBSOCKET_HANDLER * pHandler = (WEBSOCKET_HANDLER *) + pvCompletionContext; + + UNREFERENCED_PARAMETER(fUTF8Encoded); + UNREFERENCED_PARAMETER(fFinalFragment); + UNREFERENCED_PARAMETER(fClose); + + pHandler->OnIisSendComplete( + hrError, + cbIO + ); +} + + +HRESULT +WEBSOCKET_HANDLER::OnWinHttpSendComplete( + WINHTTP_WEB_SOCKET_STATUS * + ) +/*++ + +Routine Description: + Completion callback executed when a send to backend + server completes. + + If the send was successful, issue the next read + on the client's endpoint. + +++*/ +{ + HRESULT hr = S_OK; + BOOL fLocked = FALSE; + CleanupReason cleanupReason = CleanupReasonUnknown; + + LOG_TRACE(L"WEBSOCKET_HANDLER::OnWinHttpSendComplete"); + + if (_fCleanupInProgress) + { + goto Finished; + } + + EnterCriticalSection (&_RequestLock); + + fLocked = TRUE; + + if (_fCleanupInProgress) + { + goto Finished; + } + // + // Data was successfully sent to backend. + // Initiate next receive from IIS. + // + + hr = DoIisWebSocketReceive(); + if (FAILED_LOG(hr)) + { + goto Finished; + } + +Finished: + if (fLocked) + { + LeaveCriticalSection(&_RequestLock); + } + + if (FAILED_LOG(hr)) + { + Cleanup (cleanupReason); + + LOG_ERRORF(L"WEBSOCKET_HANDLER::OnWinsockSendComplete failed with HR=%08x", hr); + } + + // + // The handler object can be gone after this call. + // do not reference it after this statement. + // + DecrementOutstandingIo(); + + return hr; +} + +HRESULT +WEBSOCKET_HANDLER::OnWinHttpShutdownComplete( + VOID + ) +{ + LOG_TRACEF(L"WEBSOCKET_HANDLER::OnWinHttpShutdownComplete --%p", _pHandler); + + DecrementOutstandingIo(); + + return S_OK; +} + +HRESULT +WEBSOCKET_HANDLER::OnWinHttpIoError( + WINHTTP_WEB_SOCKET_ASYNC_RESULT *pCompletionStatus +) +{ + HRESULT hr = HRESULT_FROM_WIN32(pCompletionStatus->AsyncResult.dwError); + + LOG_ERRORF(L"WEBSOCKET_HANDLER::OnWinHttpIoError HR = %08x, Operation = %d", + hr, pCompletionStatus->AsyncResult.dwResult); + + Cleanup(ServerDisconnect); + DecrementOutstandingIo(); + + return hr; +} + +HRESULT +WEBSOCKET_HANDLER::OnWinHttpReceiveComplete( + WINHTTP_WEB_SOCKET_STATUS * pCompletionStatus + ) +/*++ + +Routine Description: + + Completion callback executed when a receive completes + on the backend server winhttp endpoint. + + Issue send on the Client(IIS) if the receive was + successful. + + If the receive completed with zero bytes, that + indicates that the server has disconnected the connection. + Issue cleanup for the websocket handler. +--*/ +{ + HRESULT hr = S_OK; + BOOL fLocked = FALSE; + CleanupReason cleanupReason = CleanupReasonUnknown; + + LOG_TRACEF(L"WEBSOCKET_HANDLER::OnWinHttpReceiveComplete --%p", _pHandler); + + if (_fCleanupInProgress) + { + goto Finished; + } + + EnterCriticalSection(&_RequestLock); + + fLocked = TRUE; + if (_fCleanupInProgress) + { + goto Finished; + } + hr = DoIisWebSocketSend( + pCompletionStatus->dwBytesTransferred, + pCompletionStatus->eBufferType + ); + + if (FAILED_LOG(hr)) + { + cleanupReason = ClientDisconnect; + goto Finished; + } + +Finished: + if (fLocked) + { + LeaveCriticalSection(&_RequestLock); + } + if (FAILED_LOG(hr)) + { + Cleanup (cleanupReason); + + LOG_ERRORF(L"WEBSOCKET_HANDLER::OnWinsockReceiveComplete failed with HR=%08x", hr); + } + + // + // The handler object can be gone after this call. + // do not reference it after this statement. + // + + DecrementOutstandingIo(); + + return hr; +} + +HRESULT +WEBSOCKET_HANDLER::OnIisSendComplete( + HRESULT hrCompletion, + DWORD cbIo + ) +/*++ +Routine Description: + + Completion callback executed when a send + completes from the client. + + If send was successful,issue read on the + server endpoint, to continue the readloop. + +--*/ +{ + HRESULT hr = S_OK; + BOOL fLocked = FALSE; + CleanupReason cleanupReason = CleanupReasonUnknown; + + UNREFERENCED_PARAMETER(cbIo); + + LOG_TRACE(L"WEBSOCKET_HANDLER::OnIisSendComplete"); + + if (FAILED_LOG(hrCompletion)) + { + hr = hrCompletion; + cleanupReason = ClientDisconnect; + goto Finished; + } + + if (_fCleanupInProgress) + { + goto Finished; + } + EnterCriticalSection(&_RequestLock); + fLocked = TRUE; + if (_fCleanupInProgress) + { + goto Finished; + } + + // + // Only call read if no close hand shake was received from backend + // + if (!_fReceivedCloseMsg) + { + // + // Write Completed, initiate next read from backend server. + // + hr = DoWinHttpWebSocketReceive(); + if (FAILED_LOG(hr)) + { + cleanupReason = ServerDisconnect; + goto Finished; + } + } + +Finished: + if (fLocked) + { + LeaveCriticalSection(&_RequestLock); + } + if (FAILED_LOG(hr)) + { + Cleanup (cleanupReason); + + LOG_ERRORF(L"WEBSOCKET_HANDLER::OnIisSendComplete failed with HR=%08x", hr); + } + + // + // The handler object can be gone after this call. + // do not reference it after this statement. + // + + DecrementOutstandingIo(); + + return hr; +} + +HRESULT +WEBSOCKET_HANDLER::OnIisReceiveComplete( + HRESULT hrCompletion, + DWORD cbIO, + BOOL fUTF8Encoded, + BOOL fFinalFragment, + BOOL fClose + ) +/*++ +Routine Description: + + Completion routine executed when a receive completes + from the client (IIS endpoint). + + If the receive was successful, initiate a send on + the backend server (winhttp) endpoint. + + If the receive failed, initiate cleanup. + +--*/ +{ + HRESULT hr = S_OK; + BOOL fLocked = FALSE; + CleanupReason cleanupReason = CleanupReasonUnknown; + WINHTTP_WEB_SOCKET_BUFFER_TYPE BufferType; + + LOG_TRACE(L"WEBSOCKET_HANDLER::OnIisReceiveComplete"); + + if (FAILED_LOG(hrCompletion)) + { + cleanupReason = ClientDisconnect; + hr = hrCompletion; + goto Finished; + } + + if (_fCleanupInProgress) + { + goto Finished; + } + + EnterCriticalSection(&_RequestLock); + + fLocked = TRUE; + if (_fCleanupInProgress) + { + goto Finished; + } + // + // Get Buffer Type from flags. + // + + WINHTTP_HELPER::GetBufferTypeFromFlags(fUTF8Encoded, + fFinalFragment, + fClose, + &BufferType); + + // + // Initiate Send. + // + + hr = DoWinHttpWebSocketSend(cbIO, BufferType); + if (FAILED_LOG(hr)) + { + cleanupReason = ServerDisconnect; + goto Finished; + } + +Finished: + if (fLocked) + { + LeaveCriticalSection(&_RequestLock); + } + if (FAILED_LOG(hr)) + { + Cleanup (cleanupReason); + + LOG_ERRORF(L"WEBSOCKET_HANDLER::OnIisReceiveComplete failed with HR=%08x", hr); + } + + // + // The handler object can be gone after this call. + // do not reference it after this statement. + // + + DecrementOutstandingIo(); + + return hr; +} + +VOID +WEBSOCKET_HANDLER::Cleanup( + CleanupReason reason +) +/*++ + +Routine Description: + + Cleanup function for the websocket handler. + + Initiates cancelIo on the two IO endpoints: + IIS, WinHttp client. + +Arguments: + CleanupReason +--*/ +{ + BOOL fLocked = FALSE; + LOG_TRACEF(L"WEBSOCKET_HANDLER::Cleanup Initiated with reason %d", reason); + + if (_fCleanupInProgress) + { + goto Finished; + } + + EnterCriticalSection(&_RequestLock); + + fLocked = TRUE; + if (_fCleanupInProgress) + { + goto Finished; + } + + _fCleanupInProgress = TRUE; + + _fIndicateCompletionToIis = TRUE; + + // + // TODO:: Raise FREB event with cleanup reason. + // + if (reason == ClientDisconnect || reason == ServerStateUnavailable) + { + // + // Calling shutdown to notify the backend about disonnect + // + WINHTTP_HELPER::sm_pfnWinHttpWebSocketShutdown( + _hWebSocketRequest, + 1011, // indicate that a server is terminating the connection because it encountered + // an unexpected condition that prevent it from fulfilling the request + NULL, // Reason + 0); // length og Reason + + } + + if (reason == ServerDisconnect || reason == ServerStateUnavailable) + { + _pHttpContext->CancelIo(); + // + // CancelIo sometime may not be able to cannel pending websocket IO + // ResetConnection to force IISWebsocket module to release the pipeline + // + _pHttpContext->GetResponse()->ResetConnection(); + } + +Finished: + if (fLocked) + { + LeaveCriticalSection(&_RequestLock); + } +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/websockethandler.h b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/websockethandler.h new file mode 100644 index 0000000000..2256e5d70e --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/websockethandler.h @@ -0,0 +1,225 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +extern IHttpServer * g_pHttpServer; +class FORWARDING_HANDLER; + +class WEBSOCKET_HANDLER +{ +public: + WEBSOCKET_HANDLER(); + + static + HRESULT + StaticInitialize( + BOOL fEnableReferenceTraceLogging + ); + + static + VOID + StaticTerminate( + VOID + ); + + VOID + Terminate( + VOID + ); + + VOID + TerminateRequest( + VOID + ) + { + Cleanup(ServerStateUnavailable); + } + + HRESULT + ProcessRequest( + FORWARDING_HANDLER *pHandler, + IHttpContext * pHttpContext, + HINTERNET hRequest, + BOOL* pfHandleCreated + ); + + REQUEST_NOTIFICATION_STATUS + OnAsyncCompletion( + VOID + ); + + HRESULT + OnWinHttpSendComplete( + WINHTTP_WEB_SOCKET_STATUS * pCompletionStatus + ); + + HRESULT + OnWinHttpShutdownComplete( + VOID + ); + + HRESULT + OnWinHttpReceiveComplete( + WINHTTP_WEB_SOCKET_STATUS * pCompletionStatus + ); + + HRESULT + OnWinHttpIoError( + WINHTTP_WEB_SOCKET_ASYNC_RESULT *pCompletionStatus + ); + + +private: + enum CleanupReason + { + CleanupReasonUnknown = 0, + IdleTimeout = 1, + ConnectFailed = 2, + ClientDisconnect = 3, + ServerDisconnect = 4, + ServerStateUnavailable = 5 + }; + + virtual + ~WEBSOCKET_HANDLER() + { + } + + WEBSOCKET_HANDLER(const WEBSOCKET_HANDLER &); + void operator=(const WEBSOCKET_HANDLER &); + + VOID + InsertRequest( + VOID + ); + + VOID + RemoveRequest( + VOID + ); + + static + VOID + WINAPI + OnReadIoCompletion( + HRESULT hrError, + VOID * pvCompletionContext, + DWORD cbIO, + BOOL fUTF8Encoded, + BOOL fFinalFragment, + BOOL fClose + ); + + static + VOID + WINAPI + OnWriteIoCompletion( + HRESULT hrError, + VOID * pvCompletionContext, + DWORD cbIO, + BOOL fUTF8Encoded, + BOOL fFinalFragment, + BOOL fClose + ); + + VOID + Cleanup( + CleanupReason reason + ); + + HRESULT + DoIisWebSocketReceive( + VOID + ); + + HRESULT + DoWinHttpWebSocketReceive( + VOID + ); + + HRESULT + DoIisWebSocketSend( + DWORD cbData, + WINHTTP_WEB_SOCKET_BUFFER_TYPE eBufferType + ); + + HRESULT + DoWinHttpWebSocketSend( + DWORD cbData, + WINHTTP_WEB_SOCKET_BUFFER_TYPE eBufferType + ); + + HRESULT + OnIisSendComplete( + HRESULT hrError, + DWORD cbIO + ); + + HRESULT + OnIisReceiveComplete( + HRESULT hrError, + DWORD cbIO, + BOOL fUTF8Encoded, + BOOL fFinalFragment, + BOOL fClose + ); + + VOID + IncrementOutstandingIo( + VOID + ); + + VOID + DecrementOutstandingIo( + VOID + ); + + VOID + IndicateCompletionToIIS( + VOID + ); + +private: + static const + DWORD RECEIVE_BUFFER_SIZE = 4*1024; + + LIST_ENTRY _listEntry; + + IHttpContext3 * _pHttpContext; + + IWebSocketContext * _pWebSocketContext; + + FORWARDING_HANDLER *_pHandler; + + HINTERNET _hWebSocketRequest; + + BYTE _WinHttpReceiveBuffer[RECEIVE_BUFFER_SIZE]; + + BYTE _IisReceiveBuffer[RECEIVE_BUFFER_SIZE]; + + CRITICAL_SECTION _RequestLock; + + LONG _dwOutstandingIo; + + volatile + BOOL _fCleanupInProgress; + + volatile + BOOL _fIndicateCompletionToIis; + + volatile + BOOL _fHandleClosed; + + volatile + BOOL _fReceivedCloseMsg; + + static + LIST_ENTRY sm_RequestsListHead; + + static + SRWLOCK sm_RequestsListLock; + + static + TRACE_LOG * sm_pTraceLog; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/winhttphelper.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/winhttphelper.cpp new file mode 100644 index 0000000000..8f6c5d3720 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/winhttphelper.cpp @@ -0,0 +1,147 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "winhttphelper.h" +#include "exceptions.h" + +PFN_WINHTTP_WEBSOCKET_COMPLETE_UPGRADE +WINHTTP_HELPER::sm_pfnWinHttpWebSocketCompleteUpgrade; + +PFN_WINHTTP_WEBSOCKET_SEND +WINHTTP_HELPER::sm_pfnWinHttpWebSocketSend; + +PFN_WINHTTP_WEBSOCKET_RECEIVE +WINHTTP_HELPER::sm_pfnWinHttpWebSocketReceive; + +PFN_WINHTTP_WEBSOCKET_SHUTDOWN +WINHTTP_HELPER::sm_pfnWinHttpWebSocketShutdown; + +PFN_WINHTTP_WEBSOCKET_QUERY_CLOSE_STATUS +WINHTTP_HELPER::sm_pfnWinHttpWebSocketQueryCloseStatus; + +//static +HRESULT +WINHTTP_HELPER::StaticInitialize( + VOID +) +{ + // + // Initialize the function pointers for WinHttp Websocket API's. + // + if (!g_fWebSocketStaticInitialize) + { + return S_OK; + } + + HMODULE hWinHttp = GetModuleHandleA("winhttp.dll"); + RETURN_LAST_ERROR_IF (hWinHttp == NULL); + + sm_pfnWinHttpWebSocketCompleteUpgrade = (PFN_WINHTTP_WEBSOCKET_COMPLETE_UPGRADE) + GetProcAddress(hWinHttp, "WinHttpWebSocketCompleteUpgrade"); + RETURN_LAST_ERROR_IF (sm_pfnWinHttpWebSocketCompleteUpgrade == NULL); + + sm_pfnWinHttpWebSocketQueryCloseStatus = (PFN_WINHTTP_WEBSOCKET_QUERY_CLOSE_STATUS) + GetProcAddress(hWinHttp, "WinHttpWebSocketQueryCloseStatus"); + RETURN_LAST_ERROR_IF (sm_pfnWinHttpWebSocketQueryCloseStatus == NULL); + + sm_pfnWinHttpWebSocketReceive = (PFN_WINHTTP_WEBSOCKET_RECEIVE) + GetProcAddress(hWinHttp, "WinHttpWebSocketReceive"); + RETURN_LAST_ERROR_IF (sm_pfnWinHttpWebSocketReceive == NULL); + + sm_pfnWinHttpWebSocketSend = (PFN_WINHTTP_WEBSOCKET_SEND) + GetProcAddress(hWinHttp, "WinHttpWebSocketSend"); + RETURN_LAST_ERROR_IF (sm_pfnWinHttpWebSocketSend == NULL); + + sm_pfnWinHttpWebSocketShutdown = (PFN_WINHTTP_WEBSOCKET_SHUTDOWN) + GetProcAddress(hWinHttp, "WinHttpWebSocketShutdown"); + RETURN_LAST_ERROR_IF (sm_pfnWinHttpWebSocketShutdown == NULL); + + return S_OK; +} + + +//static +VOID +WINHTTP_HELPER::GetFlagsFromBufferType( + __in WINHTTP_WEB_SOCKET_BUFFER_TYPE BufferType, + __out BOOL * pfUtf8Encoded, + __out BOOL * pfFinalFragment, + __out BOOL * pfClose +) +{ + switch (BufferType) + { + case WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE: + *pfUtf8Encoded = FALSE; + *pfFinalFragment = TRUE; + *pfClose = FALSE; + + break; + + case WINHTTP_WEB_SOCKET_BINARY_FRAGMENT_BUFFER_TYPE: + *pfUtf8Encoded = FALSE; + *pfFinalFragment = FALSE; + *pfClose = FALSE; + + break; + + case WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE: + *pfUtf8Encoded = TRUE; + *pfFinalFragment = TRUE; + *pfClose = FALSE; + + break; + + case WINHTTP_WEB_SOCKET_UTF8_FRAGMENT_BUFFER_TYPE: + *pfUtf8Encoded = TRUE; + *pfFinalFragment = FALSE; + *pfClose = FALSE; + + break; + + case WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE: + *pfUtf8Encoded = FALSE; + *pfFinalFragment = FALSE; + *pfClose = TRUE; + + break; + } +} + +//static +VOID +WINHTTP_HELPER::GetBufferTypeFromFlags( + __in BOOL fUtf8Encoded, + __in BOOL fFinalFragment, + __in BOOL fClose, + __out WINHTTP_WEB_SOCKET_BUFFER_TYPE* pBufferType +) +{ + if (fClose) + { + *pBufferType = WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE; + } + else + if (fUtf8Encoded) + { + if (fFinalFragment) + { + *pBufferType = WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE; + } + else + { + *pBufferType = WINHTTP_WEB_SOCKET_UTF8_FRAGMENT_BUFFER_TYPE; + } + } + else + { + if (fFinalFragment) + { + *pBufferType = WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE; + } + else + { + *pBufferType = WINHTTP_WEB_SOCKET_BINARY_FRAGMENT_BUFFER_TYPE; + } + } +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/winhttphelper.h b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/winhttphelper.h new file mode 100644 index 0000000000..d583f6fb10 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/winhttphelper.h @@ -0,0 +1,91 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +typedef +HINTERNET +(WINAPI * PFN_WINHTTP_WEBSOCKET_COMPLETE_UPGRADE)( + _In_ HINTERNET hRequest, + _In_opt_ DWORD_PTR pContext +); + + +typedef +DWORD +(WINAPI * PFN_WINHTTP_WEBSOCKET_SEND)( + _In_ HINTERNET hWebSocket, + _In_ WINHTTP_WEB_SOCKET_BUFFER_TYPE eBufferType, + _In_reads_opt_(dwBufferLength) PVOID pvBuffer, + _In_ DWORD dwBufferLength +); + +typedef +DWORD +(WINAPI * PFN_WINHTTP_WEBSOCKET_RECEIVE)( + _In_ HINTERNET hWebSocket, + _Out_writes_bytes_to_(dwBufferLength, *pdwBytesRead) PVOID pvBuffer, + _In_ DWORD dwBufferLength, + _Out_range_(0, dwBufferLength) DWORD *pdwBytesRead, + _Out_ WINHTTP_WEB_SOCKET_BUFFER_TYPE *peBufferType +); + +typedef +DWORD +(WINAPI * PFN_WINHTTP_WEBSOCKET_SHUTDOWN)( + _In_ HINTERNET hWebSocket, + _In_ USHORT usStatus, + _In_reads_bytes_opt_(dwReasonLength) PVOID pvReason, + _In_range_(0, WINHTTP_WEB_SOCKET_MAX_CLOSE_REASON_LENGTH) DWORD dwReasonLength +); + +typedef +DWORD +(WINAPI * PFN_WINHTTP_WEBSOCKET_QUERY_CLOSE_STATUS)( + _In_ HINTERNET hWebSocket, + _Out_ USHORT *pusStatus, + _Out_writes_bytes_to_opt_(dwReasonLength, *pdwReasonLengthConsumed) PVOID pvReason, + _In_range_(0, WINHTTP_WEB_SOCKET_MAX_CLOSE_REASON_LENGTH) DWORD dwReasonLength, + _Out_range_(0, WINHTTP_WEB_SOCKET_MAX_CLOSE_REASON_LENGTH) DWORD *pdwReasonLengthConsumed +); + +class WINHTTP_HELPER +{ +public: + static + HRESULT + StaticInitialize(); + + static + VOID + GetFlagsFromBufferType( + __in WINHTTP_WEB_SOCKET_BUFFER_TYPE BufferType, + __out BOOL * pfUtf8Encoded, + __out BOOL * pfFinalFragment, + __out BOOL * pfClose + ); + + static + VOID + GetBufferTypeFromFlags( + __in BOOL fUtf8Encoded, + __in BOOL fFinalFragment, + __in BOOL fClose, + __out WINHTTP_WEB_SOCKET_BUFFER_TYPE* pBufferType + ); + + static + PFN_WINHTTP_WEBSOCKET_COMPLETE_UPGRADE sm_pfnWinHttpWebSocketCompleteUpgrade; + + static + PFN_WINHTTP_WEBSOCKET_SEND sm_pfnWinHttpWebSocketSend; + + static + PFN_WINHTTP_WEBSOCKET_RECEIVE sm_pfnWinHttpWebSocketReceive; + + static + PFN_WINHTTP_WEBSOCKET_SHUTDOWN sm_pfnWinHttpWebSocketShutdown; + + static + PFN_WINHTTP_WEBSOCKET_QUERY_CLOSE_STATUS sm_pfnWinHttpWebSocketQueryCloseStatus; +}; \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/AppOfflineTrackingApplication.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/AppOfflineTrackingApplication.cpp new file mode 100644 index 0000000000..a946e7c2dc --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/AppOfflineTrackingApplication.cpp @@ -0,0 +1,66 @@ +// 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. + +#include "stdafx.h" +#include "AppOfflineTrackingApplication.h" +#include "EventLog.h" +#include "exceptions.h" + +HRESULT AppOfflineTrackingApplication::StartMonitoringAppOffline() +{ + LOG_INFOF(L"Starting app_offline monitoring in application '%ls'", m_applicationPath.c_str()); + HRESULT hr = StartMonitoringAppOflineImpl(); + + if (FAILED_LOG(hr)) + { + EventLog::Warn( + ASPNETCORE_EVENT_MONITOR_APPOFFLINE_ERROR, + ASPNETCORE_EVENT_MONITOR_APPOFFLINE_ERROR_MSG, + m_applicationPath.c_str(), + hr); + } + + return hr; +} + +void AppOfflineTrackingApplication::StopInternal(bool fServerInitiated) +{ + APPLICATION::StopInternal(fServerInitiated); + + if (m_fileWatcher) + { + m_fileWatcher->StopMonitor(); + m_fileWatcher = nullptr; + } +} + +HRESULT AppOfflineTrackingApplication::StartMonitoringAppOflineImpl() +{ + if (m_fileWatcher) + { + RETURN_HR(E_UNEXPECTED); + } + + m_fileWatcher = std::make_unique(); + RETURN_IF_FAILED(m_fileWatcher->Create(m_applicationPath.c_str(), + L"app_offline.htm", + this)); + + return S_OK; +} + +void AppOfflineTrackingApplication::OnAppOffline() +{ + if (m_fAppOfflineProcessed.exchange(true)) + { + return; + } + + LOG_INFOF(L"Received app_offline notification in application '%ls'", m_applicationPath.c_str()); + EventLog::Info( + ASPNETCORE_EVENT_RECYCLE_APPOFFLINE, + ASPNETCORE_EVENT_RECYCLE_APPOFFLINE_MSG, + m_applicationPath.c_str()); + + Stop(/*fServerInitiated*/ false); +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/AppOfflineTrackingApplication.h b/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/AppOfflineTrackingApplication.h new file mode 100644 index 0000000000..b504a730fd --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/AppOfflineTrackingApplication.h @@ -0,0 +1,47 @@ +// 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. + +#pragma once + +#include +#include "application.h" +#include "filewatcher.h" +#include + +class AppOfflineTrackingApplication: public APPLICATION +{ +public: + AppOfflineTrackingApplication(const IHttpApplication& application) + : APPLICATION(application), + m_applicationPath(application.GetApplicationPhysicalPath()), + m_fileWatcher(nullptr), + m_fAppOfflineProcessed(false) + { + } + + ~AppOfflineTrackingApplication() override + { + if (m_fileWatcher) + { + m_fileWatcher->StopMonitor(); + } + }; + + HRESULT + StartMonitoringAppOffline(); + + VOID + StopInternal(bool fServerInitiated) override; + + virtual + VOID + OnAppOffline(); + +private: + HRESULT + StartMonitoringAppOflineImpl(); + + std::wstring m_applicationPath; + std::unique_ptr m_fileWatcher; + std::atomic_bool m_fAppOfflineProcessed; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/RequestHandlerLib.vcxproj b/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/RequestHandlerLib.vcxproj new file mode 100644 index 0000000000..2519c101b1 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/RequestHandlerLib.vcxproj @@ -0,0 +1,220 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {1533E271-F61B-441B-8B74-59FB61DF0552} + Win32Proj + NewCommon + 10.0.15063.0 + + + + StaticLibrary + true + v141 + Unicode + + + StaticLibrary + false + v141 + true + Unicode + + + StaticLibrary + true + v141 + Unicode + + + StaticLibrary + false + v141 + false + Unicode + + + + + + + + + + + + + + + + + + + + + true + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + true + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + false + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + false + C:\AspNetCoreModule\src\IISLib;$(IncludePath) + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + + NotUsing + Level4 + true + Disabled + false + WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + $(IntDir)$(TargetName).pch + stdafx.h + true + MultiThreadedDebug + false + ProgramDatabase + ..\iislib;..\CommonLib; + true + stdcpp17 + + + Windows + true + + + + + NotUsing + Level4 + true + Disabled + false + _DEBUG;_LIB;%(PreprocessorDefinitions) + $(IntDir)$(TargetName).pch + stdafx.h + true + ProgramDatabase + false + MultiThreadedDebug + false + ..\iislib;..\CommonLib; + true + stdcpp17 + + + Windows + true + + + + + NotUsing + Level4 + true + MaxSpeed + true + true + false + WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + $(IntDir)$(TargetName).pch + stdafx.h + true + MultiThreaded + false + ..\iislib;..\CommonLib; + true + stdcpp17 + + + Windows + true + /NODEFAULTLIB:libucrt.lib /DEFAULTLIB:ucrt.lib %(AdditionalOptions) + true + true + + + + + NotUsing + Level4 + true + MaxSpeed + true + true + false + NDEBUG;_LIB;%(PreprocessorDefinitions) + $(IntDir)$(TargetName).pch + stdafx.h + true + ..\iislib;..\CommonLib; + true + + + MultiThreaded + false + stdcpp17 + + + Windows + true + /NODEFAULTLIB:libucrt.lib /DEFAULTLIB:ucrt.lib %(AdditionalOptions) + true + true + + + ..\iislib + + + + + + + + + + + + + + + + + + {55494e58-e061-4c4c-a0a8-837008e72f85} + + + {4787a64f-9a3e-4867-a55a-70cb4b2b2ffe} + + + + + + \ No newline at end of file diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/environmentvariablehash.h b/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/environmentvariablehash.h new file mode 100644 index 0000000000..c5f63f6fde --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/environmentvariablehash.h @@ -0,0 +1,149 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#define HOSTING_STARTUP_ASSEMBLIES_ENV_STR L"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES" +#define HOSTING_STARTUP_ASSEMBLIES_NAME L"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES=" +#define HOSTING_STARTUP_ASSEMBLIES_VALUE L"Microsoft.AspNetCore.Server.IISIntegration" +#define ASPNETCORE_IIS_AUTH_ENV_STR L"ASPNETCORE_IIS_HTTPAUTH=" +#define ASPNETCORE_IIS_WEBSOCKETS_SUPPORTED_ENV_STR L"ASPNETCORE_IIS_WEBSOCKETS_SUPPORTED=" +#define ASPNETCORE_IIS_AUTH_WINDOWS L"windows;" +#define ASPNETCORE_IIS_AUTH_BASIC L"basic;" +#define ASPNETCORE_IIS_AUTH_ANONYMOUS L"anonymous;" +#define ASPNETCORE_IIS_AUTH_NONE L"none" + +// +// The key used for hash-table lookups, consists of the port on which the http process is created. +// + +class ENVIRONMENT_VAR_ENTRY +{ +public: + ENVIRONMENT_VAR_ENTRY(): + _cRefs(1) + { + } + + HRESULT + Initialize( + PCWSTR pszName, + PCWSTR pszValue + ) + { + HRESULT hr = S_OK; + if (FAILED(hr = _strName.Copy(pszName)) || + FAILED(hr = _strValue.Copy(pszValue))) + { + } + return hr; + } + + VOID + Reference() const + { + InterlockedIncrement(&_cRefs); + } + + VOID + Dereference() const + { + if (InterlockedDecrement(&_cRefs) == 0) + { + delete this; + } + } + + PWSTR const + QueryName() + { + return _strName.QueryStr(); + } + + PWSTR const + QueryValue() + { + return _strValue.QueryStr(); + } + +private: + ~ENVIRONMENT_VAR_ENTRY() + { + } + + STRU _strName; + STRU _strValue; + mutable LONG _cRefs; +}; + + +class ENVIRONMENT_VAR_HASH : public HASH_TABLE +{ +public: + ENVIRONMENT_VAR_HASH() + { + } + + PWSTR + ExtractKey( + ENVIRONMENT_VAR_ENTRY * pEntry + ) + { + return pEntry->QueryName(); + } + + DWORD + CalcKeyHash( + PWSTR pszName + ) + { + return HashStringNoCase(pszName); + } + + BOOL + EqualKeys( + PWSTR pszName1, + PWSTR pszName2 + ) + { + return (_wcsicmp(pszName1, pszName2) == 0); + } + + VOID + ReferenceRecord( + ENVIRONMENT_VAR_ENTRY * pEntry + ) + { + pEntry->Reference(); + } + + VOID + DereferenceRecord( + ENVIRONMENT_VAR_ENTRY * pEntry + ) + { + pEntry->Dereference(); + } + + +private: + ENVIRONMENT_VAR_HASH(const ENVIRONMENT_VAR_HASH &); + void operator=(const ENVIRONMENT_VAR_HASH &); +}; + +struct ENVIRONMENT_VAR_HASH_DELETER +{ + void operator ()(ENVIRONMENT_VAR_HASH* hashTable) const + { + hashTable->Clear(); + delete hashTable; + } +}; + +struct ENVIRONMENT_VAR_ENTRY_DELETER +{ + void operator ()(ENVIRONMENT_VAR_ENTRY* entry) const + { + entry->Dereference(); + } +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/environmentvariablehelpers.h b/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/environmentvariablehelpers.h new file mode 100644 index 0000000000..b65c32ee29 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/environmentvariablehelpers.h @@ -0,0 +1,405 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +class ENVIRONMENT_VAR_HELPERS +{ + +public: + static + VOID + CopyToMultiSz( + ENVIRONMENT_VAR_ENTRY * pEntry, + PVOID pvData + ) + { + STRU strTemp; + MULTISZ *pMultiSz = static_cast(pvData); + DBG_ASSERT(pMultiSz); + DBG_ASSERT(pEntry); + strTemp.Copy(pEntry->QueryName()); + strTemp.Append(pEntry->QueryValue()); + pMultiSz->Append(strTemp.QueryStr()); + } + + static + VOID + CopyToTable( + ENVIRONMENT_VAR_ENTRY * pEntry, + PVOID pvData + ) + { + // best effort copy, ignore the failure + ENVIRONMENT_VAR_ENTRY * pNewEntry = new ENVIRONMENT_VAR_ENTRY(); + if (pNewEntry != NULL) + { + pNewEntry->Initialize(pEntry->QueryName(), pEntry->QueryValue()); + ENVIRONMENT_VAR_HASH *pHash = static_cast(pvData); + DBG_ASSERT(pHash); + pHash->InsertRecord(pNewEntry); + // Need to dereference as InsertRecord references it now + pNewEntry->Dereference(); + } + } + + static + VOID + AppendEnvironmentVariables + ( + ENVIRONMENT_VAR_ENTRY * pEntry, + PVOID pvData + ) + { + HRESULT hr = S_OK; + DWORD dwResult = 0; + DWORD dwError = 0; + STRU struNameBuffer; + STACK_STRU(struValueBuffer, 300); + BOOL fFound = FALSE; + + HRESULT* pHr = static_cast(pvData); + + // pEntry->QueryName includes the trailing =, remove it before calling stru + if (FAILED(hr = struNameBuffer.Copy(pEntry->QueryName()))) + { + goto Finished; + } + dwResult = struNameBuffer.LastIndexOf(L'='); + if (dwResult != -1) + { + struNameBuffer.QueryStr()[dwResult] = L'\0'; + if (FAILED(hr = struNameBuffer.SyncWithBuffer())) + { + goto Finished; + } + } + + dwResult = GetEnvironmentVariable(struNameBuffer.QueryStr(), struValueBuffer.QueryStr(), struValueBuffer.QuerySizeCCH()); + if (dwResult == 0) + { + dwError = GetLastError(); + // Windows API (e.g., CreateProcess) allows variable with empty string value + // in such case dwResult will be 0 and dwError will also be 0 + // As UI and CMD does not allow empty value, ignore this environment var + if (dwError != ERROR_ENVVAR_NOT_FOUND && dwError != ERROR_SUCCESS) + { + hr = HRESULT_FROM_WIN32(dwError); + goto Finished; + } + } + else if (dwResult > struValueBuffer.QuerySizeCCH()) + { + // have to increase the buffer and try get environment var again + struValueBuffer.Reset(); + struValueBuffer.Resize(dwResult + (DWORD)wcslen(pEntry->QueryValue()) + 2); // for null char and semicolon + dwResult = GetEnvironmentVariable(struNameBuffer.QueryStr(), + struValueBuffer.QueryStr(), + struValueBuffer.QuerySizeCCH()); + + if (dwResult <= 0) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + fFound = TRUE; + } + else + { + fFound = TRUE; + } + + if (FAILED(hr = struValueBuffer.SyncWithBuffer())) + { + goto Finished; + } + + if (fFound) + { + if (FAILED(hr = struValueBuffer.Append(L";"))) + { + goto Finished; + } + } + if (FAILED(hr = struValueBuffer.Append(pEntry->QueryValue()))) + { + goto Finished; + } + + if (FAILED(hr = pEntry->Initialize(pEntry->QueryName(), struValueBuffer.QueryStr()))) + { + goto Finished; + } + + Finished: + if (FAILED(hr)) + { + *pHr = hr; + } + return; + } + + static + VOID + SetEnvironmentVariables + ( + ENVIRONMENT_VAR_ENTRY * pEntry, + PVOID pvData + ) + { + UNREFERENCED_PARAMETER(pvData); + HRESULT hr = S_OK; + DWORD dwResult = 0; + STRU struNameBuffer; + + HRESULT* pHr = static_cast(pvData); + + // pEntry->QueryName includes the trailing =, remove it before calling SetEnvironmentVariable. + if (FAILED(hr = struNameBuffer.Copy(pEntry->QueryName()))) + { + goto Finished; + } + dwResult = struNameBuffer.LastIndexOf(L'='); + if (dwResult != -1) + { + struNameBuffer.QueryStr()[dwResult] = L'\0'; + if (FAILED(hr = struNameBuffer.SyncWithBuffer())) + { + goto Finished; + } + } + + dwResult = SetEnvironmentVariable(struNameBuffer.QueryStr(), pEntry->QueryValue()); + if (dwResult == 0) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + } + + Finished: + if (FAILED(hr)) + { + *pHr = hr; + } + return; + } + + static + HRESULT + InitEnvironmentVariablesTable + ( + _In_ ENVIRONMENT_VAR_HASH* pInEnvironmentVarTable, + _In_ BOOL fWindowsAuthEnabled, + _In_ BOOL fBasicAuthEnabled, + _In_ BOOL fAnonymousAuthEnabled, + _Out_ ENVIRONMENT_VAR_HASH** ppEnvironmentVarTable + ) + { + HRESULT hr = S_OK; + BOOL fFound = FALSE; + DWORD dwResult, dwError; + STRU strIisAuthEnvValue; + STACK_STRU(strStartupAssemblyEnv, 1024); + ENVIRONMENT_VAR_ENTRY* pHostingEntry = NULL; + ENVIRONMENT_VAR_ENTRY* pIISAuthEntry = NULL; + ENVIRONMENT_VAR_HASH* pEnvironmentVarTable = NULL; + + pEnvironmentVarTable = new ENVIRONMENT_VAR_HASH(); + + // + // few environment variables expected, small bucket size for hash table + // + if (FAILED(hr = pEnvironmentVarTable->Initialize(37 /*prime*/))) + { + goto Finished; + } + + // copy the envirable hash table (from configuration) to a temp one as we may need to remove elements + pInEnvironmentVarTable->Apply(ENVIRONMENT_VAR_HELPERS::CopyToTable, pEnvironmentVarTable); + if (pEnvironmentVarTable->Count() != pInEnvironmentVarTable->Count()) + { + // hash table copy failed + hr = E_UNEXPECTED; + goto Finished; + } + + pEnvironmentVarTable->FindKey((PWSTR)ASPNETCORE_IIS_AUTH_ENV_STR, &pIISAuthEntry); + if (pIISAuthEntry != NULL) + { + // user defined ASPNETCORE_IIS_HTTPAUTH in configuration, wipe it off + pIISAuthEntry->Dereference(); + pEnvironmentVarTable->DeleteKey((PWSTR)ASPNETCORE_IIS_AUTH_ENV_STR); + } + + if (fWindowsAuthEnabled) + { + strIisAuthEnvValue.Copy(ASPNETCORE_IIS_AUTH_WINDOWS); + } + + if (fBasicAuthEnabled) + { + strIisAuthEnvValue.Append(ASPNETCORE_IIS_AUTH_BASIC); + } + + if (fAnonymousAuthEnabled) + { + strIisAuthEnvValue.Append(ASPNETCORE_IIS_AUTH_ANONYMOUS); + } + + if (strIisAuthEnvValue.IsEmpty()) + { + strIisAuthEnvValue.Copy(ASPNETCORE_IIS_AUTH_NONE); + } + + pIISAuthEntry = new ENVIRONMENT_VAR_ENTRY(); + + if (FAILED(hr = pIISAuthEntry->Initialize(ASPNETCORE_IIS_AUTH_ENV_STR, strIisAuthEnvValue.QueryStr())) || + FAILED(hr = pEnvironmentVarTable->InsertRecord(pIISAuthEntry))) + { + goto Finished; + } + + // Compiler is complaining about conversion between PCWSTR and PWSTR here. + // Explictly casting. + pEnvironmentVarTable->FindKey((PWSTR)HOSTING_STARTUP_ASSEMBLIES_NAME, &pHostingEntry); + if (pHostingEntry != NULL) + { + // user defined ASPNETCORE_HOSTINGSTARTUPASSEMBLIES in configuration + // the value will be used in OutputEnvironmentVariables. Do nothing here + pHostingEntry->Dereference(); + pHostingEntry = NULL; + goto Skipped; + } + + //check whether ASPNETCORE_HOSTINGSTARTUPASSEMBLIES is defined in system + dwResult = GetEnvironmentVariable(HOSTING_STARTUP_ASSEMBLIES_ENV_STR, + strStartupAssemblyEnv.QueryStr(), + strStartupAssemblyEnv.QuerySizeCCH()); + if (dwResult == 0) + { + dwError = GetLastError(); + // Windows API (e.g., CreateProcess) allows variable with empty string value + // in such case dwResult will be 0 and dwError will also be 0 + // As UI and CMD does not allow empty value, ignore this environment var + if (dwError != ERROR_ENVVAR_NOT_FOUND && dwError != ERROR_SUCCESS) + { + hr = HRESULT_FROM_WIN32(dwError); + goto Finished; + } + } + else if (dwResult > strStartupAssemblyEnv.QuerySizeCCH()) + { + // have to increase the buffer and try get environment var again + strStartupAssemblyEnv.Reset(); + strStartupAssemblyEnv.Resize(dwResult + (DWORD)wcslen(HOSTING_STARTUP_ASSEMBLIES_VALUE) + 1); + dwResult = GetEnvironmentVariable(HOSTING_STARTUP_ASSEMBLIES_ENV_STR, + strStartupAssemblyEnv.QueryStr(), + strStartupAssemblyEnv.QuerySizeCCH()); + if (dwResult <= 0) + { + hr = E_UNEXPECTED; + goto Finished; + } + fFound = TRUE; + } + else + { + fFound = TRUE; + } + + strStartupAssemblyEnv.SyncWithBuffer(); + if (strStartupAssemblyEnv.IndexOf(HOSTING_STARTUP_ASSEMBLIES_VALUE) == -1) + { + if (fFound) + { + strStartupAssemblyEnv.Append(L";"); + } + strStartupAssemblyEnv.Append(HOSTING_STARTUP_ASSEMBLIES_VALUE); + } + + // the environment variable was not defined, create it and add to hashtable + pHostingEntry = new ENVIRONMENT_VAR_ENTRY(); + + if (FAILED(hr = pHostingEntry->Initialize(HOSTING_STARTUP_ASSEMBLIES_NAME, strStartupAssemblyEnv.QueryStr())) || + FAILED(hr = pEnvironmentVarTable->InsertRecord(pHostingEntry))) + { + goto Finished; + } + + Skipped: + *ppEnvironmentVarTable = pEnvironmentVarTable; + pEnvironmentVarTable = NULL; + + Finished: + if (pHostingEntry != NULL) + { + pHostingEntry->Dereference(); + pHostingEntry = NULL; + } + + if (pIISAuthEntry != NULL) + { + pIISAuthEntry->Dereference(); + pIISAuthEntry = NULL; + } + + if (pEnvironmentVarTable != NULL) + { + pEnvironmentVarTable->Clear(); + delete pEnvironmentVarTable; + pEnvironmentVarTable = NULL; + } + return hr; + } + + static + HRESULT + AddWebsocketEnabledToEnvironmentVariables + ( + _Inout_ ENVIRONMENT_VAR_HASH* pInEnvironmentVarTable, + _In_ BOOL fWebsocketsEnabled + ) + { + HRESULT hr = S_OK; + ENVIRONMENT_VAR_ENTRY* pIISWebsocketEntry = NULL; + STACK_STRU(strIISWebsocketEnvValue, 40); + + // We only need to set the WEBSOCKET_SUPPORTED environment variable for out of process + pInEnvironmentVarTable->FindKey((PWSTR)ASPNETCORE_IIS_WEBSOCKETS_SUPPORTED_ENV_STR, &pIISWebsocketEntry); + if (pIISWebsocketEntry != NULL) + { + // user defined ASPNETCORE_IIS_WEBSOCKETS in configuration, wipe it off + pIISWebsocketEntry->Dereference(); + pInEnvironmentVarTable->DeleteKey((PWSTR)ASPNETCORE_IIS_WEBSOCKETS_SUPPORTED_ENV_STR); + } + // Set either true or false for the WebsocketEnvValue. + if (fWebsocketsEnabled) + { + if (FAILED(hr = strIISWebsocketEnvValue.Copy(L"true"))) + { + goto Finished; + } + } + else + { + if (FAILED(hr = strIISWebsocketEnvValue.Copy(L"false"))) + { + goto Finished; + } + } + + pIISWebsocketEntry = new ENVIRONMENT_VAR_ENTRY(); + + if (FAILED(hr = pIISWebsocketEntry->Initialize(ASPNETCORE_IIS_WEBSOCKETS_SUPPORTED_ENV_STR, strIISWebsocketEnvValue.QueryStr())) || + FAILED(hr = pInEnvironmentVarTable->InsertRecord(pIISWebsocketEntry))) + { + goto Finished; + } + + Finished: + return hr; + } +public: + ENVIRONMENT_VAR_HELPERS(); + ~ENVIRONMENT_VAR_HELPERS(); +}; + diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/filewatcher.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/filewatcher.cpp new file mode 100644 index 0000000000..9d17f95b37 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/filewatcher.cpp @@ -0,0 +1,327 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "stdafx.h" +#include "filewatcher.h" +#include "debugutil.h" +#include "AppOfflineTrackingApplication.h" +#include "exceptions.h" + +FILE_WATCHER::FILE_WATCHER() : + m_hCompletionPort(NULL), + m_hChangeNotificationThread(NULL), + m_fThreadExit(FALSE) +{ +} + +FILE_WATCHER::~FILE_WATCHER() +{ + StopMonitor(); + + if (m_hChangeNotificationThread != NULL) + { + DWORD dwRetryCounter = 20; // totally wait for 1s + DWORD dwExitCode = STILL_ACTIVE; + + while (!m_fThreadExit && dwRetryCounter > 0) + { + if (GetExitCodeThread(m_hChangeNotificationThread, &dwExitCode)) + { + if (dwExitCode == STILL_ACTIVE) + { + // the file watcher thread will set m_fThreadExit before exit + WaitForSingleObject(m_hChangeNotificationThread, 50); + } + } + else + { + // fail to get thread status + // call terminitethread + TerminateThread(m_hChangeNotificationThread, 1); + m_fThreadExit = TRUE; + } + dwRetryCounter--; + } + + if (!m_fThreadExit) + { + TerminateThread(m_hChangeNotificationThread, 1); + } + } +} + +HRESULT +FILE_WATCHER::Create( + _In_ PCWSTR pszDirectoryToMonitor, + _In_ PCWSTR pszFileNameToMonitor, + _In_ AppOfflineTrackingApplication *pApplication +) +{ + + RETURN_LAST_ERROR_IF_NULL(m_hCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0)); + + RETURN_LAST_ERROR_IF_NULL(m_hChangeNotificationThread = CreateThread(NULL, + 0, + (LPTHREAD_START_ROUTINE)ChangeNotificationThread, + this, + 0, + NULL)); + + if (pszDirectoryToMonitor == NULL || + pszFileNameToMonitor == NULL || + pApplication == NULL) + { + DBG_ASSERT(FALSE); + return HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER); + } + + _pApplication = ReferenceApplication(pApplication); + + RETURN_IF_FAILED(_strFileName.Copy(pszFileNameToMonitor)); + RETURN_IF_FAILED(_strDirectoryName.Copy(pszDirectoryToMonitor)); + RETURN_IF_FAILED(_strFullName.Append(_strDirectoryName)); + RETURN_IF_FAILED(_strFullName.Append(_strFileName)); + + // + // Resize change buffer to something "reasonable" + // + RETURN_LAST_ERROR_IF(!_buffDirectoryChanges.Resize(FILE_WATCHER_ENTRY_BUFFER_SIZE)); + + _hDirectory = CreateFileW( + _strDirectoryName.QueryStr(), + FILE_LIST_DIRECTORY, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, + NULL); + + RETURN_LAST_ERROR_IF_NULL(_hDirectory); + + RETURN_LAST_ERROR_IF_NULL(CreateIoCompletionPort( + _hDirectory, + m_hCompletionPort, + NULL, + 0)); + + RETURN_IF_FAILED(Monitor()); + + return S_OK; +} + +DWORD +WINAPI +FILE_WATCHER::ChangeNotificationThread( + LPVOID pvArg +) +/*++ + +Routine Description: + +IO completion thread + +Arguments: + +None + +Return Value: + +Win32 error + +--*/ +{ + FILE_WATCHER * pFileMonitor; + BOOL fSuccess = FALSE; + DWORD cbCompletion = 0; + OVERLAPPED * pOverlapped = NULL; + DWORD dwErrorStatus; + ULONG_PTR completionKey; + + LOG_INFO(L"Starting file watcher thread"); + pFileMonitor = (FILE_WATCHER*)pvArg; + DBG_ASSERT(pFileMonitor != NULL); + + while (TRUE) + { + fSuccess = GetQueuedCompletionStatus( + pFileMonitor->m_hCompletionPort, + &cbCompletion, + &completionKey, + &pOverlapped, + INFINITE); + + DBG_ASSERT(fSuccess); + dwErrorStatus = fSuccess ? ERROR_SUCCESS : GetLastError(); + + if (completionKey == FILE_WATCHER_SHUTDOWN_KEY) + { + break; + } + + DBG_ASSERT(pOverlapped != NULL); + if (pOverlapped != NULL) + { + pFileMonitor->HandleChangeCompletion(cbCompletion); + + if (!pFileMonitor->_lStopMonitorCalled) + { + // + // Continue monitoring + // + pFileMonitor->Monitor(); + } + } + pOverlapped = NULL; + cbCompletion = 0; + } + + pFileMonitor->m_fThreadExit = TRUE; + + LOG_INFO(L"Stopping file watcher thread"); + ExitThread(0); +} + + +HRESULT +FILE_WATCHER::HandleChangeCompletion( + _In_ DWORD cbCompletion +) +/*++ + +Routine Description: + +Handle change notification (see if any of associated config files +need to be flushed) + +Arguments: + +dwCompletionStatus - Completion status +cbCompletion - Bytes of completion + +Return Value: + +HRESULT + +--*/ +{ + BOOL fFileChanged = FALSE; + + // When directory handle is closed then HandleChangeCompletion + // happens with cbCompletion = 0 and dwCompletionStatus = 0 + // From documentation it is not clear if that combination + // of return values is specific to closing handles or whether + // it could also mean an error condition. Hence we will maintain + // explicit flag that will help us determine if entry + // is being shutdown (StopMonitor() called) + // + if (_lStopMonitorCalled) + { + return S_OK; + } + + // + // There could be a FCN overflow + // Let assume the file got changed instead of checking files + // Othersie we have to cache the file info + // + if (cbCompletion == 0) + { + fFileChanged = TRUE; + } + else + { + auto pNotificationInfo = (FILE_NOTIFY_INFORMATION*)_buffDirectoryChanges.QueryPtr(); + DBG_ASSERT(pNotificationInfo != NULL); + + while (pNotificationInfo != NULL) + { + // + // check whether the monitored file got changed + // + if (_wcsnicmp(pNotificationInfo->FileName, + _strFileName.QueryStr(), + pNotificationInfo->FileNameLength / sizeof(WCHAR)) == 0) + { + fFileChanged = TRUE; + break; + } + // + // Advance to next notification + // + if (pNotificationInfo->NextEntryOffset == 0) + { + pNotificationInfo = NULL; + } + else + { + pNotificationInfo = (FILE_NOTIFY_INFORMATION*) + ((PBYTE)pNotificationInfo + + pNotificationInfo->NextEntryOffset); + } + } + } + + if (fFileChanged && !_lStopMonitorCalled) + { + // Reference application before + _pApplication->ReferenceApplication(); + RETURN_LAST_ERROR_IF(!QueueUserWorkItem(RunNotificationCallback, _pApplication.get(), WT_EXECUTEDEFAULT)); + } + + return S_OK; +} + +DWORD +WINAPI +FILE_WATCHER::RunNotificationCallback( + LPVOID pvArg +) +{ + // Recapture application instance into unique_ptr + auto pApplication = std::unique_ptr(static_cast(pvArg)); + DBG_ASSERT(pFileMonitor != NULL); + pApplication->OnAppOffline(); + + return 0; +} + +HRESULT +FILE_WATCHER::Monitor(VOID) +{ + HRESULT hr = S_OK; + DWORD cbRead; + + ZeroMemory(&_overlapped, sizeof(_overlapped)); + + RETURN_LAST_ERROR_IF(!ReadDirectoryChangesW(_hDirectory, + _buffDirectoryChanges.QueryPtr(), + _buffDirectoryChanges.QuerySize(), + FALSE, // Watching sub dirs. Set to False now as only monitoring app_offline + FILE_NOTIFY_VALID_MASK & ~FILE_NOTIFY_CHANGE_LAST_ACCESS, + &cbRead, + &_overlapped, + NULL)); + + // Check if file exist because ReadDirectoryChangesW would not fire events for existing files + if (GetFileAttributes(_strFullName.QueryStr()) != INVALID_FILE_ATTRIBUTES) + { + PostQueuedCompletionStatus(m_hCompletionPort, 0, 0, &_overlapped); + } + + return hr; +} + +VOID +FILE_WATCHER::StopMonitor() +{ + // + // Flag that monitoring is being stopped so that + // we know that HandleChangeCompletion() call + // can be ignored + // + InterlockedExchange(&_lStopMonitorCalled, 1); + // signal the file watch thread to exit + PostQueuedCompletionStatus(m_hCompletionPort, 0, FILE_WATCHER_SHUTDOWN_KEY, NULL); + // Release application reference + _pApplication.reset(nullptr); +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/filewatcher.h b/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/filewatcher.h new file mode 100644 index 0000000000..46ea744533 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/filewatcher.h @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + + +#pragma once + +#include +#include +#include "iapplication.h" +#include "HandleWrapper.h" + +#define FILE_WATCHER_SHUTDOWN_KEY (ULONG_PTR)(-1) +#define FILE_WATCHER_ENTRY_BUFFER_SIZE 4096 +#define FILE_NOTIFY_VALID_MASK 0x00000fff + +class AppOfflineTrackingApplication; + +class FILE_WATCHER{ +public: + + FILE_WATCHER(); + + ~FILE_WATCHER(); + + HRESULT Create( + _In_ PCWSTR pszDirectoryToMonitor, + _In_ PCWSTR pszFileNameToMonitor, + _In_ AppOfflineTrackingApplication *pApplication + ); + + static + DWORD + WINAPI ChangeNotificationThread(LPVOID); + + static + DWORD + WINAPI RunNotificationCallback(LPVOID); + + HRESULT HandleChangeCompletion(DWORD cbCompletion); + + HRESULT Monitor(); + void StopMonitor(); + +private: + HandleWrapper m_hCompletionPort; + HandleWrapper m_hChangeNotificationThread; + HandleWrapper _hDirectory; + volatile BOOL m_fThreadExit; + + BUFFER _buffDirectoryChanges; + STRU _strFileName; + STRU _strDirectoryName; + STRU _strFullName; + LONG _lStopMonitorCalled {}; + OVERLAPPED _overlapped; + std::unique_ptr _pApplication; +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/requesthandler_config.cpp b/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/requesthandler_config.cpp new file mode 100644 index 0000000000..1a087d5c95 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/requesthandler_config.cpp @@ -0,0 +1,489 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "stdafx.h" +#include "requesthandler_config.h" +#include "debugutil.h" +#include "environmentvariablehash.h" +#include "exceptions.h" +#include "config_utility.h" + + +REQUESTHANDLER_CONFIG::~REQUESTHANDLER_CONFIG() +{ + if (m_ppStrArguments != NULL) + { + delete[] m_ppStrArguments; + m_ppStrArguments = NULL; + } + + if (m_pEnvironmentVariables != NULL) + { + m_pEnvironmentVariables->Clear(); + delete m_pEnvironmentVariables; + m_pEnvironmentVariables = NULL; + } +} + +HRESULT +REQUESTHANDLER_CONFIG::CreateRequestHandlerConfig( + _In_ IHttpServer *pHttpServer, + _In_ IHttpApplication *pHttpApplication, + _Out_ REQUESTHANDLER_CONFIG **ppAspNetCoreConfig +) +{ + HRESULT hr = S_OK; + REQUESTHANDLER_CONFIG *pRequestHandlerConfig = NULL; + STRU struHostFxrDllLocation; + STRU struExeLocation; + + try + { + if (ppAspNetCoreConfig == NULL) + { + hr = E_INVALIDARG; + goto Finished; + } + + *ppAspNetCoreConfig = NULL; + + pRequestHandlerConfig = new REQUESTHANDLER_CONFIG; + + hr = pRequestHandlerConfig->Populate(pHttpServer, pHttpApplication); + if (FAILED(hr)) + { + goto Finished; + } + + // set appliction info here instead of inside Populate() + // as the destructor will delete the backend process + hr = pRequestHandlerConfig->QueryApplicationPath()->Copy(pHttpApplication->GetApplicationId()); + if (FAILED(hr)) + { + goto Finished; + } + + *ppAspNetCoreConfig = pRequestHandlerConfig; + pRequestHandlerConfig = NULL; + } + catch (std::bad_alloc&) + { + hr = E_OUTOFMEMORY; + } + +Finished: + + if (pRequestHandlerConfig != NULL) + { + delete pRequestHandlerConfig; + pRequestHandlerConfig = NULL; + } + + return hr; +} + +HRESULT +REQUESTHANDLER_CONFIG::Populate( + IHttpServer *pHttpServer, + IHttpApplication *pHttpApplication +) +{ + STACK_STRU(strHostingModel, 300); + HRESULT hr = S_OK; + STRU strEnvName; + STRU strEnvValue; + STRU strExpandedEnvValue; + STRU strApplicationFullPath; + IAppHostAdminManager *pAdminManager = NULL; + IAppHostElement *pAspNetCoreElement = NULL; + IAppHostElement *pWindowsAuthenticationElement = NULL; + IAppHostElement *pBasicAuthenticationElement = NULL; + IAppHostElement *pAnonymousAuthenticationElement = NULL; + IAppHostElement *pEnvVarList = NULL; + IAppHostElement *pEnvVar = NULL; + IAppHostElementCollection *pEnvVarCollection = NULL; + ULONGLONG ullRawTimeSpan = 0; + ENUM_INDEX index; + ENVIRONMENT_VAR_ENTRY* pEntry = NULL; + DWORD dwCounter = 0; + DWORD dwPosition = 0; + WCHAR* pszPath = NULL; + BSTR bstrWindowAuthSection = NULL; + BSTR bstrBasicAuthSection = NULL; + BSTR bstrAnonymousAuthSection = NULL; + BSTR bstrAspNetCoreSection = NULL; + + m_pEnvironmentVariables = new ENVIRONMENT_VAR_HASH(); + if (FAILED(hr = m_pEnvironmentVariables->Initialize(37 /*prime*/))) + { + delete m_pEnvironmentVariables; + m_pEnvironmentVariables = NULL; + goto Finished; + } + + pAdminManager = pHttpServer->GetAdminManager(); + hr = m_struConfigPath.Copy(pHttpApplication->GetAppConfigPath()); + if (FAILED(hr)) + { + goto Finished; + } + + hr = m_struApplicationPhysicalPath.Copy(pHttpApplication->GetApplicationPhysicalPath()); + if (FAILED(hr)) + { + goto Finished; + } + + pszPath = m_struConfigPath.QueryStr(); + while (pszPath[dwPosition] != NULL) + { + if (pszPath[dwPosition] == '/') + { + dwCounter++; + if (dwCounter == 4) + break; + } + dwPosition++; + } + + if (dwCounter == 4) + { + hr = m_struApplicationVirtualPath.Copy(pszPath + dwPosition); + } + else + { + hr = m_struApplicationVirtualPath.Copy(L"/"); + } + + // Will setup the application virtual path. + if (FAILED(hr)) + { + goto Finished; + } + + bstrWindowAuthSection = SysAllocString(CS_WINDOWS_AUTHENTICATION_SECTION); + if (bstrWindowAuthSection == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + hr = pAdminManager->GetAdminSection(bstrWindowAuthSection, + m_struConfigPath.QueryStr(), + &pWindowsAuthenticationElement); + if (FAILED(hr)) + { + // assume the corresponding authen was not enabled + // as the section may get deleted by user in some HWC case + // ToDo: log a warning to event log + m_fWindowsAuthEnabled = FALSE; + } + else + { + hr = GetElementBoolProperty(pWindowsAuthenticationElement, + CS_ENABLED, + &m_fWindowsAuthEnabled); + if (FAILED(hr)) + { + goto Finished; + } + } + + bstrBasicAuthSection = SysAllocString(CS_BASIC_AUTHENTICATION_SECTION); + if (bstrBasicAuthSection == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + hr = pAdminManager->GetAdminSection(bstrBasicAuthSection, + m_struConfigPath.QueryStr(), + &pBasicAuthenticationElement); + if (FAILED(hr)) + { + m_fBasicAuthEnabled = FALSE; + } + else + { + hr = GetElementBoolProperty(pBasicAuthenticationElement, + CS_ENABLED, + &m_fBasicAuthEnabled); + if (FAILED(hr)) + { + goto Finished; + } + } + + bstrAnonymousAuthSection = SysAllocString(CS_ANONYMOUS_AUTHENTICATION_SECTION); + if (bstrAnonymousAuthSection == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + + hr = pAdminManager->GetAdminSection(bstrAnonymousAuthSection, + m_struConfigPath.QueryStr(), + &pAnonymousAuthenticationElement); + if (FAILED(hr)) + { + m_fAnonymousAuthEnabled = FALSE; + } + else + { + hr = GetElementBoolProperty(pAnonymousAuthenticationElement, + CS_ENABLED, + &m_fAnonymousAuthEnabled); + if (FAILED(hr)) + { + goto Finished; + } + } + + bstrAspNetCoreSection = SysAllocString(CS_ASPNETCORE_SECTION); + if (bstrAspNetCoreSection == NULL) + { + hr = E_OUTOFMEMORY; + goto Finished; + } + hr = pAdminManager->GetAdminSection(bstrAspNetCoreSection, + m_struConfigPath.QueryStr(), + &pAspNetCoreElement); + if (FAILED(hr)) + { + goto Finished; + } + + hr = GetElementStringProperty(pAspNetCoreElement, + CS_ASPNETCORE_PROCESS_EXE_PATH, + &m_struProcessPath); + if (FAILED(hr)) + { + goto Finished; + } + + hr = GetElementStringProperty(pAspNetCoreElement, + CS_ASPNETCORE_HOSTING_MODEL, + &strHostingModel); + if (FAILED(hr)) + { + // Swallow this error for backward compatability + // Use default behavior for empty string + hr = S_OK; + } + + if (strHostingModel.IsEmpty() || strHostingModel.Equals(L"outofprocess", TRUE)) + { + m_hostingModel = HOSTING_OUT_PROCESS; + } + else if (strHostingModel.Equals(L"inprocess", TRUE)) + { + m_hostingModel = HOSTING_IN_PROCESS; + } + else + { + // block unknown hosting value + hr = HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED); + goto Finished; + } + + hr = GetElementStringProperty(pAspNetCoreElement, + CS_ASPNETCORE_PROCESS_ARGUMENTS, + &m_struArguments); + if (FAILED(hr)) + { + goto Finished; + } + + hr = GetElementDWORDProperty(pAspNetCoreElement, + CS_ASPNETCORE_RAPID_FAILS_PER_MINUTE, + &m_dwRapidFailsPerMinute); + if (FAILED(hr)) + { + goto Finished; + } + + // + // rapidFailsPerMinute cannot be greater than 100. + // + if (m_dwRapidFailsPerMinute > MAX_RAPID_FAILS_PER_MINUTE) + { + m_dwRapidFailsPerMinute = MAX_RAPID_FAILS_PER_MINUTE; + } + + hr = GetElementDWORDProperty(pAspNetCoreElement, + CS_ASPNETCORE_PROCESSES_PER_APPLICATION, + &m_dwProcessesPerApplication); + if (FAILED(hr)) + { + goto Finished; + } + + hr = GetElementDWORDProperty( + pAspNetCoreElement, + CS_ASPNETCORE_PROCESS_STARTUP_TIME_LIMIT, + &m_dwStartupTimeLimitInMS + ); + if (FAILED(hr)) + { + goto Finished; + } + + m_dwStartupTimeLimitInMS *= MILLISECONDS_IN_ONE_SECOND; + + hr = GetElementDWORDProperty( + pAspNetCoreElement, + CS_ASPNETCORE_PROCESS_SHUTDOWN_TIME_LIMIT, + &m_dwShutdownTimeLimitInMS + ); + if (FAILED(hr)) + { + goto Finished; + } + m_dwShutdownTimeLimitInMS *= MILLISECONDS_IN_ONE_SECOND; + + hr = GetElementBoolProperty(pAspNetCoreElement, + CS_ASPNETCORE_FORWARD_WINDOWS_AUTH_TOKEN, + &m_fForwardWindowsAuthToken); + if (FAILED(hr)) + { + goto Finished; + } + + hr = GetElementBoolProperty(pAspNetCoreElement, + CS_ASPNETCORE_DISABLE_START_UP_ERROR_PAGE, + &m_fDisableStartUpErrorPage); + if (FAILED(hr)) + { + goto Finished; + } + + hr = GetElementRawTimeSpanProperty( + pAspNetCoreElement, + CS_ASPNETCORE_WINHTTP_REQUEST_TIMEOUT, + &ullRawTimeSpan + ); + if (FAILED(hr)) + { + goto Finished; + } + + m_dwRequestTimeoutInMS = (DWORD)TIMESPAN_IN_MILLISECONDS(ullRawTimeSpan); + + hr = GetElementBoolProperty(pAspNetCoreElement, + CS_ASPNETCORE_STDOUT_LOG_ENABLED, + &m_fStdoutLogEnabled); + if (FAILED(hr)) + { + goto Finished; + } + hr = GetElementStringProperty(pAspNetCoreElement, + CS_ASPNETCORE_STDOUT_LOG_FILE, + &m_struStdoutLogFile); + if (FAILED(hr)) + { + goto Finished; + } + + hr = GetElementChildByName(pAspNetCoreElement, + CS_ASPNETCORE_ENVIRONMENT_VARIABLES, + &pEnvVarList); + if (FAILED(hr)) + { + goto Finished; + } + + hr = pEnvVarList->get_Collection(&pEnvVarCollection); + if (FAILED(hr)) + { + goto Finished; + } + + for (hr = FindFirstElement(pEnvVarCollection, &index, &pEnvVar); + SUCCEEDED(hr); + hr = FindNextElement(pEnvVarCollection, &index, &pEnvVar)) + { + if (hr == S_FALSE) + { + hr = S_OK; + break; + } + + if (FAILED(hr = GetElementStringProperty(pEnvVar, + CS_ASPNETCORE_ENVIRONMENT_VARIABLE_NAME, + &strEnvName)) || + FAILED(hr = GetElementStringProperty(pEnvVar, + CS_ASPNETCORE_ENVIRONMENT_VARIABLE_VALUE, + &strEnvValue)) || + FAILED(hr = strEnvName.Append(L"=")) || + FAILED(hr = STRU::ExpandEnvironmentVariables(strEnvValue.QueryStr(), &strExpandedEnvValue))) + { + goto Finished; + } + + pEntry = new ENVIRONMENT_VAR_ENTRY(); + + if (FAILED(hr = pEntry->Initialize(strEnvName.QueryStr(), strExpandedEnvValue.QueryStr())) || + FAILED(hr = m_pEnvironmentVariables->InsertRecord(pEntry))) + { + goto Finished; + } + strEnvName.Reset(); + strEnvValue.Reset(); + strExpandedEnvValue.Reset(); + pEnvVar->Release(); + pEnvVar = NULL; + pEntry->Dereference(); + pEntry = NULL; + } + +Finished: + + if (pAspNetCoreElement != NULL) + { + pAspNetCoreElement->Release(); + pAspNetCoreElement = NULL; + } + + if (pWindowsAuthenticationElement != NULL) + { + pWindowsAuthenticationElement->Release(); + pWindowsAuthenticationElement = NULL; + } + + if (pAnonymousAuthenticationElement != NULL) + { + pAnonymousAuthenticationElement->Release(); + pAnonymousAuthenticationElement = NULL; + } + + if (pBasicAuthenticationElement != NULL) + { + pBasicAuthenticationElement->Release(); + pBasicAuthenticationElement = NULL; + } + + if (pEnvVarList != NULL) + { + pEnvVarList->Release(); + pEnvVarList = NULL; + } + + if (pEnvVar != NULL) + { + pEnvVar->Release(); + pEnvVar = NULL; + } + + if (pEnvVarCollection != NULL) + { + pEnvVarCollection->Release(); + pEnvVarCollection = NULL; + } + + if (pEntry != NULL) + { + pEntry->Dereference(); + pEntry = NULL; + } + + return hr; +} diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/requesthandler_config.h b/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/requesthandler_config.h new file mode 100644 index 0000000000..5ba8959317 --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/requesthandler_config.h @@ -0,0 +1,256 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once +#define CS_ROOTWEB_CONFIG L"MACHINE/WEBROOT/APPHOST/" +#define CS_ROOTWEB_CONFIG_LEN _countof(CS_ROOTWEB_CONFIG)-1 +#define CS_ASPNETCORE_SECTION L"system.webServer/aspNetCore" +#define CS_WINDOWS_AUTHENTICATION_SECTION L"system.webServer/security/authentication/windowsAuthentication" +#define CS_BASIC_AUTHENTICATION_SECTION L"system.webServer/security/authentication/basicAuthentication" +#define CS_ANONYMOUS_AUTHENTICATION_SECTION L"system.webServer/security/authentication/anonymousAuthentication" +#define CS_WEBSOCKET_SECTION L"system.webServer/webSocket" +#define CS_ENABLED L"enabled" +#define CS_ASPNETCORE_PROCESS_EXE_PATH L"processPath" +#define CS_ASPNETCORE_PROCESS_ARGUMENTS L"arguments" +#define CS_ASPNETCORE_PROCESS_STARTUP_TIME_LIMIT L"startupTimeLimit" +#define CS_ASPNETCORE_PROCESS_SHUTDOWN_TIME_LIMIT L"shutdownTimeLimit" +#define CS_ASPNETCORE_WINHTTP_REQUEST_TIMEOUT L"requestTimeout" +#define CS_ASPNETCORE_RAPID_FAILS_PER_MINUTE L"rapidFailsPerMinute" +#define CS_ASPNETCORE_STDOUT_LOG_ENABLED L"stdoutLogEnabled" +#define CS_ASPNETCORE_STDOUT_LOG_FILE L"stdoutLogFile" +#define CS_ASPNETCORE_ENVIRONMENT_VARIABLES L"environmentVariables" +#define CS_ASPNETCORE_ENVIRONMENT_VARIABLE L"environmentVariable" +#define CS_ASPNETCORE_ENVIRONMENT_VARIABLE_NAME L"name" +#define CS_ASPNETCORE_ENVIRONMENT_VARIABLE_VALUE L"value" +#define CS_ASPNETCORE_PROCESSES_PER_APPLICATION L"processesPerApplication" +#define CS_ASPNETCORE_FORWARD_WINDOWS_AUTH_TOKEN L"forwardWindowsAuthToken" +#define CS_ASPNETCORE_DISABLE_START_UP_ERROR_PAGE L"disableStartUpErrorPage" +#define CS_ASPNETCORE_RECYCLE_ON_FILE_CHANGE L"recycleOnFileChange" +#define CS_ASPNETCORE_RECYCLE_ON_FILE_CHANGE_FILE L"file" +#define CS_ASPNETCORE_RECYCLE_ON_FILE_CHANGE_FILE_PATH L"path" +#define CS_ASPNETCORE_HOSTING_MODEL L"hostingModel" + +#define MAX_RAPID_FAILS_PER_MINUTE 100 +#define MILLISECONDS_IN_ONE_SECOND 1000 +#define MIN_PORT 1025 +#define MAX_PORT 48000 + +#define TIMESPAN_IN_MILLISECONDS(x) ((x)/((LONGLONG)(10000))) +#define TIMESPAN_IN_SECONDS(x) ((TIMESPAN_IN_MILLISECONDS(x))/((LONGLONG)(1000))) +#define TIMESPAN_IN_MINUTES(x) ((TIMESPAN_IN_SECONDS(x))/((LONGLONG)(60))) + +//#define HEX_TO_ASCII(c) ((CHAR)(((c) < 10) ? ((c) + '0') : ((c) + 'a' - 10))) + +#include "stdafx.h" +#include "environmentvariablehash.h" + +enum APP_HOSTING_MODEL +{ + HOSTING_UNKNOWN = 0, + HOSTING_IN_PROCESS, + HOSTING_OUT_PROCESS +}; + +class REQUESTHANDLER_CONFIG +{ +public: + + + ~REQUESTHANDLER_CONFIG(); + + static + HRESULT + CreateRequestHandlerConfig( + _In_ IHttpServer *pHttpServer, + _In_ IHttpApplication *pHttpApplication, + _Out_ REQUESTHANDLER_CONFIG **ppAspNetCoreConfig + ); + + ENVIRONMENT_VAR_HASH* + QueryEnvironmentVariables( + VOID + ) + { + return m_pEnvironmentVariables; + } + + DWORD + QueryRapidFailsPerMinute( + VOID + ) + { + return m_dwRapidFailsPerMinute; + } + + DWORD + QueryStartupTimeLimitInMS( + VOID + ) + { + return m_dwStartupTimeLimitInMS; + } + + DWORD + QueryShutdownTimeLimitInMS( + VOID + ) + { + return m_dwShutdownTimeLimitInMS; + } + + DWORD + QueryProcessesPerApplication( + VOID + ) + { + return m_dwProcessesPerApplication; + } + + DWORD + QueryRequestTimeoutInMS( + VOID + ) + { + return m_dwRequestTimeoutInMS; + } + + STRU* + QueryArguments( + VOID + ) + { + return &m_struArguments; + } + + STRU* + QueryApplicationPath( + VOID + ) + { + return &m_struApplication; + } + + STRU* + QueryApplicationPhysicalPath( + VOID + ) + { + return &m_struApplicationPhysicalPath; + } + + STRU* + QueryApplicationVirtualPath( + VOID + ) + { + return &m_struApplicationVirtualPath; + } + + STRU* + QueryProcessPath( + VOID + ) + { + return &m_struProcessPath; + } + + APP_HOSTING_MODEL + QueryHostingModel( + VOID + ) + { + return m_hostingModel; + } + + BOOL + QueryStdoutLogEnabled() + { + return m_fStdoutLogEnabled; + } + + BOOL + QueryForwardWindowsAuthToken() + { + return m_fForwardWindowsAuthToken; + } + + BOOL + QueryWindowsAuthEnabled() + { + return m_fWindowsAuthEnabled; + } + + BOOL + QueryBasicAuthEnabled() + { + return m_fBasicAuthEnabled; + } + + BOOL + QueryAnonymousAuthEnabled() + { + return m_fAnonymousAuthEnabled; + } + + BOOL + QueryDisableStartUpErrorPage() + { + return m_fDisableStartUpErrorPage; + } + + STRU* + QueryStdoutLogFile() + { + return &m_struStdoutLogFile; + } + + STRU* + QueryConfigPath() + { + return &m_struConfigPath; + } + +protected: + + // + // protected constructor + // + REQUESTHANDLER_CONFIG() : + m_fStdoutLogEnabled(FALSE), + m_pEnvironmentVariables(NULL), + m_hostingModel(HOSTING_UNKNOWN), + m_ppStrArguments(NULL) + { + } + + HRESULT + Populate( + IHttpServer *pHttpServer, + IHttpApplication *pHttpApplication + ); + + DWORD m_dwRequestTimeoutInMS; + DWORD m_dwStartupTimeLimitInMS; + DWORD m_dwShutdownTimeLimitInMS; + DWORD m_dwRapidFailsPerMinute; + DWORD m_dwProcessesPerApplication; + STRU m_struArguments; + STRU m_struProcessPath; + STRU m_struStdoutLogFile; + STRU m_struApplication; + STRU m_struApplicationPhysicalPath; + STRU m_struApplicationVirtualPath; + STRU m_struConfigPath; + BOOL m_fStdoutLogEnabled; + BOOL m_fForwardWindowsAuthToken; + BOOL m_fDisableStartUpErrorPage; + BOOL m_fWindowsAuthEnabled; + BOOL m_fBasicAuthEnabled; + BOOL m_fAnonymousAuthEnabled; + APP_HOSTING_MODEL m_hostingModel; + ENVIRONMENT_VAR_HASH* m_pEnvironmentVariables; + STRU m_struHostFxrLocation; + PWSTR* m_ppStrArguments; + DWORD m_dwArgc; + +}; diff --git a/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/stdafx.h b/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/stdafx.h new file mode 100644 index 0000000000..78a02ce72b --- /dev/null +++ b/src/IISIntegration/src/AspNetCoreModuleV2/RequestHandlerLib/stdafx.h @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Shlwapi.h" +#include + +#include "hashtable.h" +#include "stringu.h" +#include "stringa.h" +#include "multisz.h" +#include "dbgutil.h" +#include "ahutil.h" +#include "hashfn.h" diff --git a/src/IISIntegration/src/Directory.Build.props b/src/IISIntegration/src/Directory.Build.props new file mode 100644 index 0000000000..4b89a431e7 --- /dev/null +++ b/src/IISIntegration/src/Directory.Build.props @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/AppHostConfig/IIS.config b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/AppHostConfig/IIS.config new file mode 100644 index 0000000000..bc24ef9639 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/AppHostConfig/IIS.config @@ -0,0 +1,740 @@ + + + + + + + + +
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+ + +
+
+
+
+ +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/AppOfflineTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/AppOfflineTests.cs new file mode 100644 index 0000000000..7d8d661ef0 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/AppOfflineTests.cs @@ -0,0 +1,287 @@ +// 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.Net; +using System.Net.Http; +using System.Net.Sockets; +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.Testing.xunit; +using Microsoft.Extensions.Logging; +using Xunit; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; + +namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class AppOfflineTests : IISFunctionalTestBase + { + private static readonly TimeSpan RetryDelay = TimeSpan.FromMilliseconds(100); + + private readonly PublishedSitesFixture _fixture; + + public AppOfflineTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalTheory] + [InlineData(HostingModel.InProcess)] + [InlineData(HostingModel.OutOfProcess)] + public async Task AppOfflineDroppedWhileSiteIsDown_SiteReturns503(HostingModel hostingModel) + { + var deploymentResult = await DeployApp(hostingModel); + + AddAppOffline(deploymentResult.ContentRoot); + + await AssertAppOffline(deploymentResult); + DeletePublishOutput(deploymentResult); + } + + [ConditionalTheory] + [InlineData(HostingModel.InProcess)] + [InlineData(HostingModel.OutOfProcess)] + public async Task LockedAppOfflineDroppedWhileSiteIsDown_SiteReturns503(HostingModel hostingModel) + { + var deploymentResult = await DeployApp(hostingModel); + + // Add app_offline without shared access + using (var stream = File.Open(Path.Combine(deploymentResult.ContentRoot, "app_offline.htm"), FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None)) + using (var writer = new StreamWriter(stream)) + { + await writer.WriteLineAsync("App if offline but you wouldn't see this message"); + await writer.FlushAsync(); + await AssertAppOffline(deploymentResult, ""); + } + + DeletePublishOutput(deploymentResult); + } + + [ConditionalTheory] + [InlineData(HostingModel.InProcess, 500, "500.0")] + [InlineData(HostingModel.OutOfProcess, 502, "502.5")] + public async Task AppOfflineDroppedWhileSiteFailedToStartInShim_AppOfflineServed(HostingModel hostingModel, int statusCode, string content) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(hostingModel: hostingModel, publish: true); + deploymentParameters.WebConfigActionList.Add(WebConfigHelpers.AddOrModifyAspNetCoreSection("processPath", "nonexistent")); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var result = await deploymentResult.HttpClient.GetAsync("/"); + Assert.Equal(statusCode, (int)result.StatusCode); + Assert.Contains(content, await result.Content.ReadAsStringAsync()); + + AddAppOffline(deploymentResult.ContentRoot); + + await AssertAppOffline(deploymentResult); + DeletePublishOutput(deploymentResult); + } + + [ConditionalFact(Skip = "https://github.com/aspnet/IISIntegration/issues/933")] + public async Task AppOfflineDroppedWhileSiteFailedToStartInRequestHandler_SiteStops_InProcess() + { + var deploymentResult = await DeployApp(HostingModel.InProcess); + + // Set file content to empty so it fails at runtime + File.WriteAllText(Path.Combine(deploymentResult.ContentRoot, "Microsoft.AspNetCore.Server.IIS.dll"), ""); + + var result = await deploymentResult.HttpClient.GetAsync("/"); + Assert.Equal(500, (int)result.StatusCode); + Assert.Contains("500.30", await result.Content.ReadAsStringAsync()); + + AddAppOffline(deploymentResult.ContentRoot); + + await deploymentResult.AssertRecycledAsync(() => AssertAppOffline(deploymentResult)); + } + + [ConditionalFact] + [RequiresIIS(IISCapability.ShutdownToken)] + public async Task AppOfflineDroppedWhileSiteStarting_SiteShutsDown_InProcess() + { + // This test often hits a race between debug logging and stdout redirection closing the handle + // we are fine having this race + using (AppVerifier.Disable(DeployerSelector.ServerType, 0x300)) + { + var deploymentResult = await DeployApp(HostingModel.InProcess); + + for (int i = 0; i < 10; i++) + { + // send first request and add app_offline while app is starting + var runningTask = AssertAppOffline(deploymentResult); + + // This test tries to hit a race where we drop app_offline file while + // in process application is starting, application start takes at least 400ms + // so we back off for 100ms to allow request to reach request handler + // Test itself is racy and can result in two scenarios + // 1. ANCM detects app_offline before it starts the request - if AssertAppOffline succeeds we've hit it + // 2. Intended scenario where app starts and then shuts down + // In first case we remove app_offline and try again + await Task.Delay(RetryDelay); + + AddAppOffline(deploymentResult.ContentRoot); + + try + { + await runningTask.DefaultTimeout(); + + // if AssertAppOffline succeeded ANCM have picked up app_offline before starting the app + // try again + RemoveAppOffline(deploymentResult.ContentRoot); + } + catch + { + deploymentResult.AssertWorkerProcessStop(); + return; + } + } + + Assert.True(false); + + } + } + + [ConditionalFact] + public async Task AppOfflineDroppedWhileSiteRunning_SiteShutsDown_InProcess() + { + var deploymentResult = await AssertStarts(HostingModel.InProcess); + + AddAppOffline(deploymentResult.ContentRoot); + + await deploymentResult.AssertRecycledAsync(() => AssertAppOffline(deploymentResult)); + } + + [ConditionalFact] + public async Task AppOfflineDroppedWhileSiteRunning_SiteShutsDown_OutOfProcess() + { + var deploymentResult = await AssertStarts(HostingModel.OutOfProcess); + + // Repeat dropping file and restarting multiple times + for (int i = 0; i < 5; i++) + { + AddAppOffline(deploymentResult.ContentRoot); + await AssertAppOffline(deploymentResult); + RemoveAppOffline(deploymentResult.ContentRoot); + await AssertRunning(deploymentResult); + } + + AddAppOffline(deploymentResult.ContentRoot); + await AssertAppOffline(deploymentResult); + DeletePublishOutput(deploymentResult); + } + + [ConditionalTheory] + [InlineData(HostingModel.InProcess)] + [InlineData(HostingModel.OutOfProcess)] + public async Task AppOfflineDropped_CanRemoveAppOfflineAfterAddingAndSiteWorks(HostingModel hostingModel) + { + var deploymentResult = await DeployApp(hostingModel); + + AddAppOffline(deploymentResult.ContentRoot); + + await AssertAppOffline(deploymentResult); + + RemoveAppOffline(deploymentResult.ContentRoot); + + await AssertRunning(deploymentResult); + } + + [ConditionalTheory] + [InlineData(HostingModel.InProcess)] + [InlineData(HostingModel.OutOfProcess)] + public async Task AppOfflineAddedAndRemovedStress(HostingModel hostingModel) + { + var deploymentResult = await AssertStarts(hostingModel); + + var load = Helpers.StressLoad(deploymentResult.HttpClient, "/HelloWorld", response => { + var statusCode = (int)response.StatusCode; + Assert.True(statusCode == 200 || statusCode == 503, "Status code was " + statusCode); + }); + + for (int i = 0; i < 100; i++) + { + // AddAppOffline might fail if app_offline is being read by ANCM and deleted at the same time + RetryHelper.RetryOperation( + () => AddAppOffline(deploymentResult.ContentRoot), + e => Logger.LogError($"Failed to create app_offline : {e.Message}"), + retryCount: 3, + retryDelayMilliseconds: RetryDelay.Milliseconds); + RemoveAppOffline(deploymentResult.ContentRoot); + } + + try + { + await load; + } + catch (HttpRequestException ex) when (ex.InnerException is IOException | ex.InnerException is SocketException) + { + // IOException in InProcess is fine, just means process stopped + if (hostingModel != HostingModel.InProcess) + { + throw; + } + } + } + + private async Task DeployApp(HostingModel hostingModel = HostingModel.InProcess) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(hostingModel: hostingModel, publish: true); + + return await DeployAsync(deploymentParameters); + } + + private void AddAppOffline(string appPath, string content = "The app is offline.") + { + File.WriteAllText(Path.Combine(appPath, "app_offline.htm"), content); + } + + private void RemoveAppOffline(string appPath) + { + RetryHelper.RetryOperation( + () => File.Delete(Path.Combine(appPath, "app_offline.htm")), + e => Logger.LogError($"Failed to remove app_offline : {e.Message}"), + retryCount: 3, + retryDelayMilliseconds: RetryDelay.Milliseconds); + } + + private async Task AssertAppOffline(IISDeploymentResult deploymentResult, string expectedResponse = "The app is offline.") + { + var response = await deploymentResult.HttpClient.RetryRequestAsync("HelloWorld", r => r.StatusCode == HttpStatusCode.ServiceUnavailable); + Assert.Equal(expectedResponse, await response.Content.ReadAsStringAsync()); + } + + private async Task AssertStarts(HostingModel hostingModel) + { + var deploymentResult = await DeployApp(hostingModel); + + await AssertRunning(deploymentResult); + + return deploymentResult; + } + + private static async Task AssertRunning(IISDeploymentResult deploymentResult) + { + var response = await deploymentResult.HttpClient.RetryRequestAsync("HelloWorld", r => r.IsSuccessStatusCode); + var responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal("Hello World", responseText); + } + + private void DeletePublishOutput(IISDeploymentResult deploymentResult) + { + foreach (var file in Directory.GetFiles(deploymentResult.ContentRoot, "*", SearchOption.AllDirectories)) + { + // Out of process module dll is allowed to be locked + var name = Path.GetFileName(file); + if (name == "aspnetcore.dll" || name == "aspnetcorev2.dll" || name == "aspnetcorev2_outofprocess.dll") + { + continue; + } + File.Delete(file); + } + } + + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/BasicAuthTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/BasicAuthTests.cs new file mode 100644 index 0000000000..d8607db21e --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/BasicAuthTests.cs @@ -0,0 +1,70 @@ +// 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.Http; +using System.Net.Http.Headers; +using System.Text; +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; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class BasicAuthTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public BasicAuthTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + public static TestMatrix TestVariants + => TestMatrix.ForServers(DeployerSelector.ServerType) + .WithTfms(Tfm.NetCoreApp22) + .WithApplicationTypes(ApplicationType.Portable) + .WithAllAncmVersions() + .WithAllHostingModels(); + + [ConditionalTheory] + [RequiresEnvironmentVariable("ASPNETCORE_MODULE_TEST_USER")] + [RequiresIIS(IISCapability.BasicAuthentication)] + [MemberData(nameof(TestVariants))] + public async Task BasicAuthTest(TestVariant variant) + { + var username = Environment.GetEnvironmentVariable("ASPNETCORE_MODULE_TEST_USER"); + var password = Environment.GetEnvironmentVariable("ASPNETCORE_MODULE_TEST_PASSWORD"); + + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + deploymentParameters.SetAnonymousAuth(enabled: false); + deploymentParameters.SetWindowsAuth(enabled: false); + deploymentParameters.SetBasicAuth(enabled: true); + + // The default in hosting sets windows auth to true. + var deploymentResult = await DeployAsync(deploymentParameters); + var request = new HttpRequestMessage(HttpMethod.Get, "/Auth"); + var byteArray = new UTF8Encoding().GetBytes(username + ":" + password); + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray)); + + var response = await deploymentResult.HttpClient.SendAsync(request); + + var responseText = await response.Content.ReadAsStringAsync(); + + if (variant.HostingModel == HostingModel.InProcess) + { + Assert.StartsWith("Windows", responseText); + Assert.Contains(username, responseText); + } + else + { + // We expect out-of-proc not allowing basic auth + Assert.Equal("Windows", responseText); + } + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/ClientCertificateFixture.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/ClientCertificateFixture.cs new file mode 100644 index 0000000000..40b6f2265e --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/ClientCertificateFixture.cs @@ -0,0 +1,104 @@ +// 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 + { + private X509Certificate2 _certificate; + private const string _certIssuerPrefix = "CN=IISIntegrationTest_Root"; + + public X509Certificate2 GetOrCreateCertificate() + { + if (_certificate != null) + { + return _certificate; + } + + using (var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine)) + { + store.Open(OpenFlags.ReadWrite); + var parentKey = CreateKeyMaterial(2048); + + // Create a cert name with a random guid to avoid name conflicts + var parentRequest = new CertificateRequest( + _certIssuerPrefix + Guid.NewGuid().ToString(), + 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(); + return imported; + } + } + + public void Dispose() + { + if (_certificate == null) + { + return; + } + + using (var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine)) + { + store.Open(OpenFlags.ReadWrite); + store.Remove(_certificate); + + // Remove any extra certs that were left by previous tests. + for (var i = store.Certificates.Count - 1; i >= 0; i--) + { + var cert = store.Certificates[i]; + if (cert.Issuer.StartsWith(_certIssuerPrefix)) + { + store.Remove(cert); + } + } + 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/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/ClientCertificateTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/ClientCertificateTests.cs new file mode 100644 index 0000000000..43ccc83eff --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/ClientCertificateTests.cs @@ -0,0 +1,108 @@ +// 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.Http; +using System.Security.Cryptography.X509Certificates; +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 Microsoft.Extensions.Logging; +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 handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (a, b, c, d) => true, + ClientCertificateOptions = ClientCertificateOption.Manual, + }; + + X509Certificate2 cert = null; + if (sendClientCert) + { + cert = _certFixture.GetOrCreateCertificate(); + handler.ClientCertificates.Add(cert); + } + + var deploymentResult = await DeployAsync(deploymentParameters); + + var client = deploymentResult.CreateClient(handler); + var response = await client.GetAsync("GetClientCert"); + + var responseText = await response.Content.ReadAsStringAsync(); + + try + { + if (sendClientCert) + { + Assert.Equal($"Enabled;{cert.GetCertHashString()}", responseText); + } + else + { + Assert.Equal("Disabled", responseText); + } + } + catch (Exception ex) + { + Logger.LogError($"Certificate is invalid. Issuer name: {cert.Issuer}"); + using (var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine)) + { + Logger.LogError($"List of current certificates in root store:"); + store.Open(OpenFlags.ReadWrite); + foreach (var otherCert in store.Certificates) + { + Logger.LogError(otherCert.Issuer); + } + store.Close(); + } + throw ex; + } + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/ClientDisconnectStress.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/ClientDisconnectStress.cs new file mode 100644 index 0000000000..9deeae3f92 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/ClientDisconnectStress.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class ClientDisconnectStressTests: FunctionalTestsBase + { + private readonly PublishedSitesFixture _fixture; + + public ClientDisconnectStressTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalTheory] + [InlineData(HostingModel.InProcess)] + [InlineData(HostingModel.OutOfProcess)] + public async Task ClientDisconnectStress(HostingModel hostingModel) + { + var site = await StartAsync(_fixture.GetBaseDeploymentParameters(hostingModel)); + var maxRequestSize = 1000; + var blockSize = 40; + var random = new Random(); + async Task RunRequests() + { + using (var connection = new TestConnection(site.HttpClient.BaseAddress.Port)) + { + await connection.Send( + "POST /ReadAndFlushEcho HTTP/1.1", + $"Content-Length: {maxRequestSize}", + "Host: localhost", + "Connection: close", + "", + ""); + + var disconnectAfter = random.Next(maxRequestSize); + var data = new byte[blockSize]; + for (int i = 0; i < disconnectAfter / blockSize; i++) + { + await connection.Stream.WriteAsync(data); + } + } + } + + List tasks = new List(); + for (int i = 0; i < 100; i++) + { + tasks.Add(Task.Run(RunRequests)); + } + + await Task.WhenAll(tasks); + + StopServer(); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/CommonStartupTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/CommonStartupTests.cs new file mode 100644 index 0000000000..e2bcf2a8f9 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/CommonStartupTests.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.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class CommonStartupTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public CommonStartupTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + public static TestMatrix TestVariants + => TestMatrix.ForServers(DeployerSelector.ServerType) + .WithTfms(Tfm.NetCoreApp22) + .WithAllApplicationTypes() + .WithAllAncmVersions() + .WithAllHostingModels(); + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task StartupStress(TestVariant variant) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + + var deploymentResult = await DeployAsync(deploymentParameters); + + await Helpers.StressLoad(deploymentResult.HttpClient, "/HelloWorld", response => { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Hello World", response.Content.ReadAsStringAsync().GetAwaiter().GetResult()); + }); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/CompressionTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/CompressionTests.cs new file mode 100644 index 0000000000..c2d0277c4c --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/CompressionTests.cs @@ -0,0 +1,58 @@ +// 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.Linq; +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 abstract class CompressionTests : FixtureLoggedTest + { + private readonly IISTestSiteFixture _fixture; + + [Collection(IISTestSiteCollection.Name)] + public class InProc: CompressionTests + { + public InProc(IISTestSiteFixture fixture) : base(fixture) { } + } + + [Collection(OutOfProcessTestSiteCollection.Name)] + public class OutOfProcess: CompressionTests + { + public OutOfProcess(OutOfProcessTestSiteFixture fixture) : base(fixture) { } + } + + [Collection(OutOfProcessV1TestSiteCollection.Name)] + public class OutOfProcessV1: CompressionTests + { + public OutOfProcessV1(OutOfProcessV1TestSiteFixture fixture) : base(fixture) { } + } + + protected CompressionTests(IISTestSiteFixture fixture) : base(fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task PassesThroughCompression() + { + var request = new HttpRequestMessage(HttpMethod.Get, "/CompressedData"); + + request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip")); + + var response = await _fixture.Client.SendAsync(request); + Assert.Equal("gzip", response.Content.Headers.ContentEncoding.Single()); + Assert.Equal( + new byte[] { + 0x1F, 0x8B, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x0B, 0x63, 0x60, 0xA0, 0x3D, 0x00, 0x00, + 0xCA, 0xC6, 0x88, 0x99, 0x64, 0x00, 0x00, 0x00 }, + await response.Content.ReadAsByteArrayAsync()); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/ConfigurationChangeTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/ConfigurationChangeTests.cs new file mode 100644 index 0000000000..099cefb97a --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/ConfigurationChangeTests.cs @@ -0,0 +1,126 @@ +// 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.Net.Http; +using System.Net.Sockets; +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 Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class ConfigurationChangeTests : IISFunctionalTestBase + { + private static readonly TimeSpan RetryDelay = TimeSpan.FromMilliseconds(100); + private readonly PublishedSitesFixture _fixture; + + public ConfigurationChangeTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task ConfigurationChangeStopsInProcess() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(HostingModel.InProcess, publish: true); + + var deploymentResult = await DeployAsync(deploymentParameters); + + await deploymentResult.AssertStarts(); + + // Just "touching" web.config should be enough + deploymentResult.ModifyWebConfig(element => {}); + + await deploymentResult.AssertRecycledAsync(); + } + + [ConditionalTheory] + [InlineData(AncmVersion.AspNetCoreModule)] + [InlineData(AncmVersion.AspNetCoreModuleV2)] + public async Task ConfigurationChangeForcesChildProcessRestart(AncmVersion version) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(HostingModel.OutOfProcess, publish: true); + deploymentParameters.AncmVersion = version; + + var deploymentResult = await DeployAsync(deploymentParameters); + + var processBefore = await deploymentResult.HttpClient.GetStringAsync("/ProcessId"); + + // Just "touching" web.config should be enough + deploymentResult.ModifyWebConfig(element => {}); + + // Have to retry here to allow ANCM to receive notification and react to it + // Verify that worker process gets restarted with new process id + await deploymentResult.HttpClient.RetryRequestAsync("/ProcessId", async r => await r.Content.ReadAsStringAsync() != processBefore); + } + + [ConditionalFact] + public async Task OutOfProcessToInProcessHostingModelSwitchWorks() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(HostingModel.OutOfProcess, publish: true); + + var deploymentResult = await DeployAsync(deploymentParameters); + + await deploymentResult.AssertStarts(); + + deploymentResult.ModifyWebConfig(element => element + .Descendants("system.webServer") + .Single() + .GetOrAdd("aspNetCore") + .SetAttributeValue("hostingModel", "inprocess")); + + // Have to retry here to allow ANCM to receive notification and react to it + // Verify that inprocess application was created and tried to start + await deploymentResult.HttpClient.RetryRequestAsync("/HelloWorld", r => r.StatusCode == HttpStatusCode.InternalServerError); + + StopServer(); + EventLogHelpers.VerifyEventLogEvent(deploymentResult, "Could not find the assembly 'aspnetcorev2_inprocess.dll'", Logger); + } + + [ConditionalTheory] + [InlineData(HostingModel.InProcess)] + [InlineData(HostingModel.OutOfProcess)] + public async Task ConfigurationTouchedStress(HostingModel hostingModel) + { + var deploymentResult = await DeployAsync(_fixture.GetBaseDeploymentParameters(hostingModel, publish: true)); + + await deploymentResult.AssertStarts(); + var load = Helpers.StressLoad(deploymentResult.HttpClient, "/HelloWorld", response => { + var statusCode = (int)response.StatusCode; + Assert.True(statusCode == 200 || statusCode == 503, "Status code was " + statusCode); + }); + + for (int i = 0; i < 100; i++) + { + // ModifyWebConfig might fail if web.config is being read by IIS + RetryHelper.RetryOperation( + () => deploymentResult.ModifyWebConfig(element => {}), + e => Logger.LogError($"Failed to touch web.config : {e.Message}"), + retryCount: 3, + retryDelayMilliseconds: RetryDelay.Milliseconds); + } + + try + { + await load; + } + catch (HttpRequestException ex) when (ex.InnerException is IOException | ex.InnerException is SocketException) + { + // IOException in InProcess is fine, just means process stopped + if (hostingModel != HostingModel.InProcess) + { + throw; + } + } + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ClientDisconnectTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ClientDisconnectTests.cs new file mode 100644 index 0000000000..6e2d1bc752 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ClientDisconnectTests.cs @@ -0,0 +1,108 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class ClientDisconnectTests: FixtureLoggedTest + { + private readonly IISTestSiteFixture _fixture; + + public ClientDisconnectTests(IISTestSiteFixture fixture): base(fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task ServerWorksAfterClientDisconnect() + { + using (var connection = _fixture.CreateTestConnection()) + { + var message = "Hello"; + await connection.Send( + "POST /ReadAndWriteSynchronously HTTP/1.1", + $"Content-Length: {100000}", + "Host: localhost", + "Connection: close", + "", + ""); + + await connection.Send(message); + + await connection.Receive( + "HTTP/1.1 200 OK", + ""); + } + + var response = await _fixture.Client.GetAsync("HelloWorld"); + + var responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal("Hello World", responseText); + } + + [ConditionalFact] + public async Task RequestAbortedTokenFires() + { + using (var connection = _fixture.CreateTestConnection()) + { + await connection.Send( + "GET /WaitForAbort HTTP/1.1", + "Host: localhost", + "Connection: close", + "", + ""); + + await _fixture.Client.RetryRequestAsync("/WaitingRequestCount", async message => await message.Content.ReadAsStringAsync() == "1"); + } + + await _fixture.Client.RetryRequestAsync("/WaitingRequestCount", async message => await message.Content.ReadAsStringAsync() == "0"); + } + + [ConditionalFact] + public async Task ClientDisconnectCallbackStress() + { + // Fixture initialization fails if inside of the Task.Run, so send an + // initial request to initialize the fixture. + var response = await _fixture.Client.GetAsync("HelloWorld"); + var numTotalRequests = 0; + for (var j = 0; j < 20; j++) + { + // Windows has a max connection limit of 10 for the IIS server, + // so setting limit fairly low. + const int numRequests = 5; + async Task RunRequests() + { + using (var connection = _fixture.CreateTestConnection()) + { + await connection.Send( + "GET /WaitForAbort HTTP/1.1", + "Host: localhost", + "Connection: close", + "", + ""); + await _fixture.Client.RetryRequestAsync("/WaitingRequestCount", async message => await message.Content.ReadAsStringAsync() != "0"); + Interlocked.Increment(ref numTotalRequests); + } + } + + List tasks = new List(); + for (int i = 0; i < numRequests; i++) + { + tasks.Add(Task.Run(RunRequests)); + } + + await Task.WhenAll(tasks); + + await _fixture.Client.RetryRequestAsync("/WaitingRequestCount", async message => await message.Content.ReadAsStringAsync() == "0"); + } + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/CompressionTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/CompressionTests.cs new file mode 100644 index 0000000000..ce1c84e609 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/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 CompressionModuleTests : FixtureLoggedTest + { + private readonly IISCompressionSiteFixture _fixture; + + public CompressionModuleTests(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/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/EnvironmentVariableTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/EnvironmentVariableTests.cs new file mode 100644 index 0000000000..ae8fde39ed --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/EnvironmentVariableTests.cs @@ -0,0 +1,51 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class EnvironmentVariableTests: FixtureLoggedTest + { + private readonly IISTestSiteFixture _fixture; + + public EnvironmentVariableTests(IISTestSiteFixture fixture): base(fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task GetUniqueEnvironmentVariable() + { + Assert.Equal("foobar", await _fixture.Client.GetStringAsync("/CheckEnvironmentVariable")); + } + + [ConditionalFact] + public async Task GetLongEnvironmentVariable() + { + Assert.Equal( + "AReallyLongValueThatIsGreaterThan300CharactersToForceResizeInNative" + + "AReallyLongValueThatIsGreaterThan300CharactersToForceResizeInNative" + + "AReallyLongValueThatIsGreaterThan300CharactersToForceResizeInNative" + + "AReallyLongValueThatIsGreaterThan300CharactersToForceResizeInNative" + + "AReallyLongValueThatIsGreaterThan300CharactersToForceResizeInNative" + + "AReallyLongValueThatIsGreaterThan300CharactersToForceResizeInNative", + await _fixture.Client.GetStringAsync("/CheckEnvironmentLongValueVariable")); + } + + [ConditionalFact] + public async Task GetExistingEnvironmentVariable() + { + Assert.Contains(";foobarbaz", await _fixture.Client.GetStringAsync("/CheckAppendedEnvironmentVariable")); + } + + [ConditionalFact] + public async Task AuthHeaderEnvironmentVariableRemoved() + { + Assert.DoesNotContain("shouldberemoved", await _fixture.Client.GetStringAsync("/CheckRemoveAuthEnvironmentVariable")); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ErrorPagesTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ErrorPagesTests.cs new file mode 100644 index 0000000000..bf45949491 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ErrorPagesTests.cs @@ -0,0 +1,131 @@ +// 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; +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.IIS; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class ErrorPagesTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public ErrorPagesTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + public async Task IncludesAdditionalErrorPageTextInProcessHandlerLoadFailure_CorrectString() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + var response = await DeployAppWithStartupFailure(deploymentParameters); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + StopServer(); + + var responseString = await response.Content.ReadAsStringAsync(); + Assert.Contains("HTTP Error 500.0 - ANCM In-Process Handler Load Failure", responseString); + VerifyNoExtraTrailingBytes(responseString); + + await AssertLink(response); + } + + [ConditionalFact] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + public async Task IncludesAdditionalErrorPageTextOutOfProcessStartupFailure_CorrectString() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(HostingModel.OutOfProcess, publish: true); + var response = await DeployAppWithStartupFailure(deploymentParameters); + + Assert.Equal(HttpStatusCode.BadGateway, response.StatusCode); + + StopServer(); + + var responseString = await response.Content.ReadAsStringAsync(); + Assert.Contains("HTTP Error 502.5 - ANCM Out-Of-Process Startup Failure", responseString); + VerifyNoExtraTrailingBytes(responseString); + + await AssertLink(response); + } + + [ConditionalFact] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + public async Task IncludesAdditionalErrorPageTextOutOfProcessHandlerLoadFailure_CorrectString() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(HostingModel.OutOfProcess, publish: true); + deploymentParameters.HandlerSettings["handlerVersion"] = "88.93"; + deploymentParameters.EnvironmentVariables["ANCM_ADDITIONAL_ERROR_PAGE_LINK"] = "http://example"; + + var deploymentResult = await DeployAsync(deploymentParameters); + var response = await deploymentResult.HttpClient.GetAsync("HelloWorld"); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + StopServer(); + + var responseString = await response.Content.ReadAsStringAsync(); + Assert.Contains("HTTP Error 500.0 - ANCM Out-Of-Process Handler Load Failure", responseString); + VerifyNoExtraTrailingBytes(responseString); + + await AssertLink(response); + } + + [ConditionalFact] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + [RequiresNewHandler] + public async Task IncludesAdditionalErrorPageTextInProcessStartupFailure_CorrectString() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} EarlyReturn"); + deploymentParameters.EnvironmentVariables["ANCM_ADDITIONAL_ERROR_PAGE_LINK"] = "http://example"; + + var deploymentResult = await DeployAsync(deploymentParameters); + var response = await deploymentResult.HttpClient.GetAsync("HelloWorld"); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + StopServer(); + + var responseString = await response.Content.ReadAsStringAsync(); + Assert.Contains("HTTP Error 500.30 - ANCM In-Process Start Failure", responseString); + VerifyNoExtraTrailingBytes(responseString); + + await AssertLink(response); + } + + private static void VerifyNoExtraTrailingBytes(string responseString) + { + if (!DeployerSelector.IsBackwardsCompatiblityTest) + { + Assert.EndsWith("\r\n", responseString); + } + } + + private static async Task AssertLink(HttpResponseMessage response) + { + Assert.Contains(" http://example and ", await response.Content.ReadAsStringAsync()); + } + + private async Task DeployAppWithStartupFailure(IISDeploymentParameters deploymentParameters) + { + deploymentParameters.WebConfigActionList.Add(WebConfigHelpers.AddOrModifyAspNetCoreSection("processPath", "doesnot")); + deploymentParameters.WebConfigActionList.Add(WebConfigHelpers.AddOrModifyAspNetCoreSection("arguments", "start")); + + deploymentParameters.EnvironmentVariables["ANCM_ADDITIONAL_ERROR_PAGE_LINK"] = "http://example"; + + var deploymentResult = await DeployAsync(deploymentParameters); + + return await deploymentResult.HttpClient.GetAsync("HelloWorld"); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/EventLogTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/EventLogTests.cs new file mode 100644 index 0000000000..1df7f3c077 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/EventLogTests.cs @@ -0,0 +1,46 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class EventLogTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public EventLogTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task CheckStartupEventLogMessage() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + + var deploymentResult = await DeployAsync(deploymentParameters); + await deploymentResult.AssertStarts(); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvent(deploymentResult, "Application '.+' started the coreclr in-process successfully."); + } + + [ConditionalFact] + public async Task CheckShutdownEventLogMessage() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + var deploymentResult = await DeployAsync(deploymentParameters); + await deploymentResult.AssertStarts(); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvent(deploymentResult, "Application '.+' has shutdown."); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/FeatureCollectionTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/FeatureCollectionTests.cs new file mode 100644 index 0000000000..e31dc3dbaa --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/FeatureCollectionTests.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class FeatureCollectionTest + { + private readonly IISTestSiteFixture _fixture; + + public FeatureCollectionTest(IISTestSiteFixture fixture) + { + _fixture = fixture; + } + + [ConditionalTheory] + [InlineData("FeatureCollectionSetRequestFeatures")] + [InlineData("FeatureCollectionSetResponseFeatures")] + [InlineData("FeatureCollectionSetConnectionFeatures")] + public async Task FeatureCollectionTest_SetHttpContextFeatures(string path) + { + Assert.Equal("Success", await _fixture.Client.GetStringAsync(path + "/path" + "?query")); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/FixtureLoggedTest.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/FixtureLoggedTest.cs new file mode 100644 index 0000000000..705af2b213 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/FixtureLoggedTest.cs @@ -0,0 +1,31 @@ +// 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.Reflection; +using Microsoft.Extensions.Logging.Testing; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + public class FixtureLoggedTest: LoggedTest + { + private readonly IISTestSiteFixture _fixture; + + public FixtureLoggedTest(IISTestSiteFixture fixture) + { + _fixture = fixture; + } + + public override void Initialize(MethodInfo methodInfo, object[] testMethodArguments, ITestOutputHelper testOutputHelper) + { + base.Initialize(methodInfo, testMethodArguments, testOutputHelper); + _fixture.Attach(this); + } + + public override void Dispose() + { + _fixture.Detach(this); + base.Dispose(); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/FrebTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/FrebTests.cs new file mode 100644 index 0000000000..12e2f6ae53 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/FrebTests.cs @@ -0,0 +1,181 @@ +// 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.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class FrebTests : LogFileTestBase + { + private readonly PublishedSitesFixture _fixture; + public FrebTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + public static IList FrebChecks() + { + var list = new List(); + list.Add(new FrebLogItem("ANCM_INPROC_EXECUTE_REQUEST_START")); + list.Add(new FrebLogItem("ANCM_INPROC_EXECUTE_REQUEST_COMPLETION", "1")); + list.Add(new FrebLogItem("ANCM_INPROC_ASYNC_COMPLETION_START")); + list.Add(new FrebLogItem("ANCM_INPROC_ASYNC_COMPLETION_COMPLETION", "0")); + list.Add(new FrebLogItem("ANCM_INPROC_MANAGED_REQUEST_COMPLETION")); + return list; + } + + [ConditionalFact] + [RequiresIIS(IISCapability.FailedRequestTracingModule)] + public async Task CheckCommonFrebEvents() + { + var result = await SetupFrebApp(); + + await result.HttpClient.GetAsync("HelloWorld"); + + StopServer(); + + AssertFrebLogs(result, FrebChecks()); + } + + [ConditionalFact] + [RequiresNewShim] + [RequiresIIS(IISCapability.FailedRequestTracingModule)] + public async Task FrebIncludesHResultFailures() + { + var parameters = _fixture.GetBaseDeploymentParameters(publish: true); + parameters.TransformArguments((args, _) => string.Empty); + var result = await SetupFrebApp(parameters); + + await result.HttpClient.GetAsync("HelloWorld"); + + StopServer(); + + AssertFrebLogs(result, new FrebLogItem("ANCM_HRESULT_FAILED"), new FrebLogItem("ANCM_EXCEPTION_CAUGHT")); + } + + [ConditionalFact] + [RequiresIIS(IISCapability.FailedRequestTracingModule)] + public async Task CheckFailedRequestEvents() + { + var result = await SetupFrebApp(); + + await result.HttpClient.GetAsync("Throw"); + + StopServer(); + + AssertFrebLogs(result, new FrebLogItem("ANCM_INPROC_ASYNC_COMPLETION_COMPLETION", "2")); + } + + [ConditionalFact] + [RequiresIIS(IISCapability.FailedRequestTracingModule)] + public async Task CheckFrebDisconnect() + { + var result = await SetupFrebApp(); + + using (var connection = new TestConnection(result.HttpClient.BaseAddress.Port)) + { + await connection.Send( + "GET /WaitForAbort HTTP/1.1", + "Host: localhost", + "Connection: close", + "", + ""); + await result.HttpClient.RetryRequestAsync("/WaitingRequestCount", async message => await message.Content.ReadAsStringAsync() == "1"); + } + + StopServer(); + + // The order of freb logs is based on when the requests are complete. + // This is non-deterministic here, so we need to check both freb files for a request that was disconnected. + AssertFrebLogs(result, new FrebLogItem("ANCM_INPROC_REQUEST_DISCONNECT"), new FrebLogItem("ANCM_INPROC_MANAGED_REQUEST_COMPLETION")); + } + + private async Task SetupFrebApp(IISDeploymentParameters parameters = null) + { + parameters = parameters ?? _fixture.GetBaseDeploymentParameters(publish: true); + parameters.EnableFreb("Verbose", _logFolderPath); + + Directory.CreateDirectory(_logFolderPath); + var result = await DeployAsync(parameters); + return result; + } + + private void AssertFrebLogs(IISDeploymentResult result, params FrebLogItem[] expectedFrebEvents) + { + AssertFrebLogs(result, (IEnumerable)expectedFrebEvents); + } + + private void AssertFrebLogs(IISDeploymentResult result, IEnumerable expectedFrebEvents) + { + var frebEvent = GetFrebLogItems(result); + foreach (var expectedEvent in expectedFrebEvents) + { + Assert.Contains(expectedEvent, frebEvent); + } + } + + private IEnumerable GetFrebLogItems(IISDeploymentResult result) + { + var folderPath = Helpers.GetFrebFolder(_logFolderPath, result); + var xmlFiles = Directory.GetFiles(folderPath).Where(f => f.EndsWith("xml")); + var frebEvents = new List(); + + foreach (var xmlFile in xmlFiles) + { + var xDocument = XDocument.Load(xmlFile).Root; + var nameSpace = (XNamespace)"http://schemas.microsoft.com/win/2004/08/events/event"; + var eventElements = xDocument.Descendants(nameSpace + "Event"); + foreach (var eventElement in eventElements) + { + var eventElementWithOpCode = eventElement.Descendants(nameSpace + "RenderingInfo").Single().Descendants(nameSpace + "Opcode").Single(); + var requestStatus = eventElement.Element(nameSpace + "EventData").Descendants().Where(el => el.Attribute("Name").Value == "requestStatus").SingleOrDefault(); + frebEvents.Add(new FrebLogItem(eventElementWithOpCode.Value, requestStatus?.Value)); + } + } + + return frebEvents; + } + + public class FrebLogItem + { + private string _opCode; + private string _requestStatus; + + public FrebLogItem(string opCode) + { + _opCode = opCode; + } + + public FrebLogItem(string opCode, string requestStatus) + { + _opCode = opCode; + _requestStatus = requestStatus; + } + + public override bool Equals(object obj) + { + var item = obj as FrebLogItem; + return item != null && + _opCode == item._opCode && + _requestStatus == item._requestStatus; + } + + public override int GetHashCode() + { + return HashCode.Combine(_opCode, _requestStatus); + } + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/HelloWorldTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/HelloWorldTests.cs new file mode 100644 index 0000000000..1b2ad70600 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/HelloWorldTests.cs @@ -0,0 +1,30 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class HelloWorldInProcessTests + { + private readonly IISTestSiteFixture _fixture; + + public HelloWorldInProcessTests(IISTestSiteFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task HelloWorld_InProcess() + { + Assert.Equal("Hello World", await _fixture.Client.GetStringAsync("/HelloWorld")); + + Assert.Equal("/Path??", await _fixture.Client.GetStringAsync("/HelloWorld/Path%3F%3F?query")); + + Assert.Equal("?query", await _fixture.Client.GetStringAsync("/HelloWorld/Query%3F%3F?query")); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/HostingEnvironmentTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/HostingEnvironmentTests.cs new file mode 100644 index 0000000000..061b828a6c --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/HostingEnvironmentTests.cs @@ -0,0 +1,33 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class HostingEnvironmentTests: FixtureLoggedTest + { + private readonly IISTestSiteFixture _fixture; + + public HostingEnvironmentTests(IISTestSiteFixture fixture): base(fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + [RequiresIIS(IISCapability.ShutdownToken)] + public async Task HostingEnvironmentIsCorrect() + { + Assert.Equal( + $"ContentRootPath {_fixture.DeploymentResult.ContentRoot}" + Environment.NewLine + + $"WebRootPath {_fixture.DeploymentResult.ContentRoot}\\wwwroot" + Environment.NewLine + + $"CurrentDirectory {Path.GetDirectoryName(_fixture.DeploymentResult.HostProcess.MainModule.FileName)}", + await _fixture.Client.GetStringAsync("/HostingEnvironment")); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/InvalidReadWriteOperationTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/InvalidReadWriteOperationTests.cs new file mode 100644 index 0000000000..95c05308bd --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/InvalidReadWriteOperationTests.cs @@ -0,0 +1,84 @@ +// 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.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class InvalidReadWriteOperationTests + { + private readonly IISTestSiteFixture _fixture; + + public InvalidReadWriteOperationTests(IISTestSiteFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task TestReadOffsetWorks() + { + var result = await _fixture.Client.PostAsync($"/TestReadOffsetWorks", new StringContent("Hello World")); + Assert.Equal("Hello World", await result.Content.ReadAsStringAsync()); + } + + [ConditionalTheory] + [InlineData("/InvalidOffsetSmall")] + [InlineData("/InvalidOffsetLarge")] + [InlineData("/InvalidCountSmall")] + [InlineData("/InvalidCountLarge")] + [InlineData("/InvalidCountWithOffset")] + public async Task TestInvalidReadOperations(string operation) + { + var result = await _fixture.Client.GetStringAsync($"/TestInvalidReadOperations{operation}"); + Assert.Equal("Success", result); + } + + [ConditionalTheory] + [InlineData("/NullBuffer")] + [InlineData("/InvalidCountZeroRead")] + public async Task TestValidReadOperations(string operation) + { + var result = await _fixture.Client.GetStringAsync($"/TestValidReadOperations{operation}"); + Assert.Equal("Success", result); + } + + [ConditionalTheory] + [InlineData("/NullBufferPost")] + [InlineData("/InvalidCountZeroReadPost")] + public async Task TestValidReadOperationsPost(string operation) + { + var result = await _fixture.Client.PostAsync($"/TestValidReadOperations{operation}", new StringContent("hello")); + Assert.Equal("Success", await result.Content.ReadAsStringAsync()); + } + + [ConditionalTheory] + [InlineData("/InvalidOffsetSmall")] + [InlineData("/InvalidOffsetLarge")] + [InlineData("/InvalidCountSmall")] + [InlineData("/InvalidCountLarge")] + [InlineData("/InvalidCountWithOffset")] + public async Task TestInvalidWriteOperations(string operation) + { + var result = await _fixture.Client.GetStringAsync($"/TestInvalidWriteOperations{operation}"); + Assert.Equal("Success", result); + } + + [ConditionalFact] + public async Task TestValidWriteOperations() + { + var result = await _fixture.Client.GetStringAsync($"/TestValidWriteOperations/NullBuffer"); + Assert.Equal("Success", result); + } + + [ConditionalFact] + public async Task TestValidWriteOperationsPost() + { + var result = await _fixture.Client.PostAsync($"/TestValidWriteOperations/NullBufferPost", new StringContent("hello")); + Assert.Equal("Success", await result.Content.ReadAsStringAsync()); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/LargeResponseBodyTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/LargeResponseBodyTests.cs new file mode 100644 index 0000000000..40db2cfdb8 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/LargeResponseBodyTests.cs @@ -0,0 +1,36 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class LargeResponseBodyTests + { + private readonly IISTestSiteFixture _fixture; + + public LargeResponseBodyTests(IISTestSiteFixture fixture) + { + _fixture = fixture; + } + + [ConditionalTheory] + [InlineData(65000)] + [InlineData(1000000)] + [InlineData(10000000)] + [InlineData(100000000)] + public async Task LargeResponseBodyTest_CheckAllResponseBodyBytesWritten(int query) + { + Assert.Equal(new string('a', query), await _fixture.Client.GetStringAsync($"/LargeResponseBody?length={query}")); + } + + [ConditionalFact] + public async Task LargeResponseBodyFromFile_CheckAllResponseBodyBytesWritten() + { + Assert.Equal(200000000, (await _fixture.Client.GetStringAsync($"/LargeResponseFile")).Length); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/LogPipeTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/LogPipeTests.cs new file mode 100644 index 0000000000..4d34c3154f --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/LogPipeTests.cs @@ -0,0 +1,83 @@ +// 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; +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; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class LogPipeTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public LogPipeTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalTheory] + [InlineData("ConsoleErrorWrite")] + [InlineData("ConsoleWrite")] + public async Task CheckStdoutLoggingToPipe_DoesNotCrashProcess(string path) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + var deploymentResult = await DeployAsync(deploymentParameters); + + await Helpers.AssertStarts(deploymentResult, path); + + StopServer(); + + if (deploymentParameters.ServerType == ServerType.IISExpress) + { + Assert.Contains(TestSink.Writes, context => context.Message.Contains("TEST MESSAGE")); + } + } + + [ConditionalTheory] + [InlineData("ConsoleErrorWriteStartServer")] + [InlineData("ConsoleWriteStartServer")] + public async Task CheckStdoutLoggingToPipeWithFirstWrite(string path) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + + var firstWriteString = "TEST MESSAGE"; + + deploymentParameters.TransformArguments((a, _) => $"{a} {path}"); + + var deploymentResult = await DeployAsync(deploymentParameters); + + await Helpers.AssertStarts(deploymentResult); + + StopServer(); + + if (deploymentParameters.ServerType == ServerType.IISExpress) + { + // We can't read stdout logs from IIS as they aren't redirected. + Assert.Contains(TestSink.Writes, context => context.Message.Contains(firstWriteString)); + } + } + + [ConditionalFact] + public async Task CheckUnicodePipe() + { + var path = "CheckConsoleFunctions"; + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} {path}"); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync(path); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + StopServer(); + EventLogHelpers.VerifyEventLogEvent(deploymentResult, EventLogHelpers.InProcessThreadExitStdOut(deploymentResult, "12", "(.*)彡⾔(.*)")); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ResponseHeaderTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ResponseHeaderTests.cs new file mode 100644 index 0000000000..fec5c227ec --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ResponseHeaderTests.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.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class ResponseHeaders + { + private readonly IISTestSiteFixture _fixture; + + public ResponseHeaders(IISTestSiteFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task AddResponseHeaders_HeaderValuesAreSetCorrectly() + { + var response = await _fixture.Client.GetAsync("ResponseHeaders"); + var responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Request Complete", responseText); + + Assert.True(response.Headers.TryGetValues("UnknownHeader", out var headerValues)); + Assert.Equal("test123=foo", headerValues.First()); + + Assert.True(response.Content.Headers.TryGetValues(HeaderNames.ContentType, out headerValues)); + Assert.Equal("text/plain", headerValues.First()); + + Assert.True(response.Headers.TryGetValues("MultiHeader", out headerValues)); + Assert.Equal(2, headerValues.Count()); + Assert.Equal("1", headerValues.First()); + Assert.Equal("2", headerValues.Last()); + } + + [ConditionalFact] + public async Task ErrorCodeIsSetForExceptionDuringRequest() + { + var response = await _fixture.Client.GetAsync("Throw"); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.Equal("Internal Server Error", response.ReasonPhrase); + } + + [ConditionalTheory] + [InlineData(200, "custom", "custom", null)] + [InlineData(200, "custom", "custom", "Custom body")] + [InlineData(200, "custom", "custom", "")] + + + [InlineData(500, "", "Internal Server Error", null)] + [InlineData(500, "", "Internal Server Error", "Custom body")] + [InlineData(500, "", "Internal Server Error", "")] + + [InlineData(400, "custom", "custom", null)] + [InlineData(400, "", "Bad Request", "Custom body")] + [InlineData(400, "", "Bad Request", "")] + + [InlineData(999, "", "", null)] + [InlineData(999, "", "", "Custom body")] + [InlineData(999, "", "", "")] + public async Task CustomErrorCodeWorks(int code, string reason, string expectedReason, string body) + { + var response = await _fixture.Client.GetAsync($"SetCustomErorCode?code={code}&reason={reason}&writeBody={body != null}&body={body}"); + Assert.Equal((HttpStatusCode)code, response.StatusCode); + Assert.Equal(expectedReason, response.ReasonPhrase); + + // ReadAsStringAsync returns empty string for empty results + Assert.Equal(body ?? string.Empty, await response.Content.ReadAsStringAsync()); + } + + [ConditionalFact] + public async Task ServerHeaderIsOverriden() + { + var response = await _fixture.Client.GetAsync("OverrideServer"); + Assert.Equal("MyServer/7.8", response.Headers.Server.Single().Product.ToString()); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ResponseInvalidOrderingTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ResponseInvalidOrderingTests.cs new file mode 100644 index 0000000000..c39e155e77 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ResponseInvalidOrderingTests.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class ResponseInvalidOrderingTest + { + private readonly IISTestSiteFixture _fixture; + + public ResponseInvalidOrderingTest(IISTestSiteFixture fixture) + { + _fixture = fixture; + } + + [ConditionalTheory] + [InlineData("SetStatusCodeAfterWrite")] + [InlineData("SetHeaderAfterWrite")] + public async Task ResponseInvalidOrderingTests_ExpectFailure(string path) + { + Assert.Equal($"Started_{path}Threw_Finished", await _fixture.Client.GetStringAsync("/ResponseInvalidOrdering/" + path)); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ServerVariablesTest.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ServerVariablesTest.cs new file mode 100644 index 0000000000..d18c4c7f09 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ServerVariablesTest.cs @@ -0,0 +1,59 @@ +// 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.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class ServerVariablesTest + { + private readonly IISTestSiteFixture _fixture; + + public ServerVariablesTest(IISTestSiteFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task ProvidesAccessToServerVariables() + { + var port = _fixture.Client.BaseAddress.Port; + Assert.Equal("SERVER_PORT: " + port, await _fixture.Client.GetStringAsync("/ServerVariable?q=SERVER_PORT")); + Assert.Equal("QUERY_STRING: q=QUERY_STRING", await _fixture.Client.GetStringAsync("/ServerVariable?q=QUERY_STRING")); + } + + [ConditionalFact] + public async Task ReturnsNullForUndefinedServerVariable() + { + 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() + { + Assert.DoesNotContain(@"\\?\", await _fixture.Client.GetStringAsync("/BasePath")); + } + + [ConditionalFact] + public async Task GetServerVariableDoesNotCrash() + { + await Helpers.StressLoad(_fixture.Client, "/GetServerVariableStress", response => { + var text = response.Content.ReadAsStringAsync().Result; + Assert.StartsWith("Response Begin", text); + Assert.EndsWith("Response End", text); + }); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/StartupExceptionTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/StartupExceptionTests.cs new file mode 100644 index 0000000000..cadfed9781 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/StartupExceptionTests.cs @@ -0,0 +1,100 @@ +// 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; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class StartupExceptionTests : LogFileTestBase + { + private readonly PublishedSitesFixture _fixture; + + public StartupExceptionTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalTheory] + [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); + + 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] + [InlineData("CheckLargeStdErrWrites")] + [InlineData("CheckLargeStdOutWrites")] + [InlineData("CheckOversizedStdErrWrites")] + [InlineData("CheckOversizedStdOutWrites")] + public async Task CheckStdoutWithLargeWrites_LogFile(string mode) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} {mode}"); + deploymentParameters.EnableLogging(_logFolderPath); + + var deploymentResult = await DeployAsync(deploymentParameters); + + 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] + public async Task CheckValidConsoleFunctions() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} CheckConsoleFunctions"); + + var deploymentResult = await DeployAsync(deploymentParameters); + + await AssertFailsToStart(deploymentResult); + + Assert.Contains(TestSink.Writes, context => context.Message.Contains("Is Console redirection: True")); + } + + private async Task AssertFailsToStart(IISDeploymentResult deploymentResult) + { + var response = await deploymentResult.HttpClient.GetAsync("/HelloWorld"); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + StopServer(); + } + + [ConditionalFact] + public async Task Gets500_30_ErrorPage() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} EarlyReturn"); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("/HelloWorld"); + Assert.False(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + var responseText = await response.Content.ReadAsStringAsync(); + Assert.Contains("500.30 - ANCM In-Process Start Failure", responseText); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/StartupTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/StartupTests.cs new file mode 100644 index 0000000000..3bcc134cb2 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/StartupTests.cs @@ -0,0 +1,479 @@ +// 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.IO; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.AspNetCore.Testing.xunit; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class StartupTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public StartupTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + private readonly string _dotnetLocation = DotNetCommands.GetDotNetExecutable(RuntimeArchitecture.x64); + + [ConditionalFact] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + public async Task ExpandEnvironmentVariableInWebConfig() + { + // Point to dotnet installed in user profile. + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + deploymentParameters.EnvironmentVariables["DotnetPath"] = _dotnetLocation; + deploymentParameters.WebConfigActionList.Add(WebConfigHelpers.AddOrModifyAspNetCoreSection("processPath", "%DotnetPath%")); + await StartAsync(deploymentParameters); + } + + [ConditionalTheory] + [InlineData("bogus", "", @"Executable was not found at '.*?\\bogus.exe")] + [InlineData("c:\\random files\\dotnet.exe", "something.dll", @"Could not find dotnet.exe at '.*?\\dotnet.exe'")] + [InlineData(".\\dotnet.exe", "something.dll", @"Could not find dotnet.exe at '.*?\\.\\dotnet.exe'")] + [InlineData("dotnet.exe", "", @"Application arguments are empty.")] + [InlineData("dotnet.zip", "", @"Process path 'dotnet.zip' doesn't have '.exe' extension.")] + public async Task InvalidProcessPath_ExpectServerError(string path, string arguments, string subError) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + deploymentParameters.WebConfigActionList.Add(WebConfigHelpers.AddOrModifyAspNetCoreSection("processPath", path)); + deploymentParameters.WebConfigActionList.Add(WebConfigHelpers.AddOrModifyAspNetCoreSection("arguments", arguments)); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("HelloWorld"); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvent(deploymentResult, $@"Application '{Regex.Escape(deploymentResult.ContentRoot)}\\' wasn't able to start. {subError}"); + + Assert.Contains("HTTP Error 500.0 - ANCM In-Process Handler Load Failure", await response.Content.ReadAsStringAsync()); + } + + [ConditionalFact] + public async Task StartsWithDotnetLocationWithoutExe() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + + var dotnetLocationWithoutExtension = _dotnetLocation.Substring(0, _dotnetLocation.LastIndexOf(".")); + deploymentParameters.WebConfigActionList.Add(WebConfigHelpers.AddOrModifyAspNetCoreSection("processPath", dotnetLocationWithoutExtension)); + + await StartAsync(deploymentParameters); + } + + [ConditionalFact] + public async Task StartsWithDotnetLocationUppercase() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + + var dotnetLocationWithoutExtension = _dotnetLocation.Substring(0, _dotnetLocation.LastIndexOf(".")).ToUpperInvariant(); + deploymentParameters.WebConfigActionList.Add(WebConfigHelpers.AddOrModifyAspNetCoreSection("processPath", dotnetLocationWithoutExtension)); + + await StartAsync(deploymentParameters); + } + + [ConditionalTheory] + [InlineData("dotnet")] + [InlineData("dotnet.EXE")] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + public async Task StartsWithDotnetOnThePath(string path) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + + deploymentParameters.EnvironmentVariables["PATH"] = Path.GetDirectoryName(_dotnetLocation); + deploymentParameters.WebConfigActionList.Add(WebConfigHelpers.AddOrModifyAspNetCoreSection("processPath", path)); + + var deploymentResult = await DeployAsync(deploymentParameters); + await deploymentResult.AssertStarts(); + + StopServer(); + // Verify that in this scenario where.exe was invoked only once by shim and request handler uses cached value + Assert.Equal(1, TestSink.Writes.Count(w => w.Message.Contains("Invoking where.exe to find dotnet.exe"))); + } + + public static TestMatrix TestVariants + => TestMatrix.ForServers(DeployerSelector.ServerType) + .WithTfms(Tfm.NetCoreApp22) + .WithAllApplicationTypes() + .WithAncmV2InProcess(); + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task HelloWorld(TestVariant variant) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + await StartAsync(deploymentParameters); + } + + [ConditionalFact] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + public async Task StartsWithPortableAndBootstraperExe() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + // We need the right dotnet on the path in IIS + deploymentParameters.EnvironmentVariables["PATH"] = Path.GetDirectoryName(DotNetCommands.GetDotNetExecutable(deploymentParameters.RuntimeArchitecture)); + + // rest publisher as it doesn't support additional parameters + deploymentParameters.ApplicationPublisher = null; + // ReferenceTestTasks is workaround for https://github.com/dotnet/sdk/issues/2482 + deploymentParameters.AdditionalPublishParameters = "-p:RuntimeIdentifier=win7-x64 -p:UseAppHost=true -p:SelfContained=false -p:ReferenceTestTasks=false"; + var deploymentResult = await DeployAsync(deploymentParameters); + + Assert.True(File.Exists(Path.Combine(deploymentResult.ContentRoot, "InProcessWebSite.exe"))); + Assert.False(File.Exists(Path.Combine(deploymentResult.ContentRoot, "hostfxr.dll"))); + Assert.Contains("InProcessWebSite.exe", Helpers.ReadAllTextFromFile(Path.Combine(deploymentResult.ContentRoot, "web.config"), Logger)); + + await deploymentResult.AssertStarts(); + } + + [ConditionalFact] + public async Task DetectsOverriddenServer() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} OverriddenServer"); + + var deploymentResult = await DeployAsync(deploymentParameters); + var response = await deploymentResult.HttpClient.GetAsync("/"); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvents(deploymentResult, + EventLogHelpers.InProcessFailedToStart(deploymentResult, "CLR worker thread exited prematurely"), + EventLogHelpers.InProcessThreadException(deploymentResult, ".*?Application is running inside IIS process but is not configured to use IIS server")); + } + + [ConditionalFact] + public async Task LogsStartupExceptionExitError() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} Throw"); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("/"); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvents(deploymentResult, + EventLogHelpers.InProcessFailedToStart(deploymentResult, "CLR worker thread exited prematurely"), + EventLogHelpers.InProcessThreadException(deploymentResult, ", exception code = '0xe0434352'")); + } + + [ConditionalFact] + public async Task LogsUnexpectedThreadExitError() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} EarlyReturn"); + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("/HelloWorld"); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvents(deploymentResult, + EventLogHelpers.InProcessFailedToStart(deploymentResult, "CLR worker thread exited prematurely"), + EventLogHelpers.InProcessThreadExit(deploymentResult, "12")); + } + + [ConditionalFact] + public async Task RemoveHostfxrFromApp_InProcessHostfxrInvalid() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.ApplicationType = ApplicationType.Standalone; + var deploymentResult = await DeployAsync(deploymentParameters); + + File.Copy( + Path.Combine(deploymentResult.ContentRoot, "aspnetcorev2_inprocess.dll"), + Path.Combine(deploymentResult.ContentRoot, "hostfxr.dll"), + true); + await AssertSiteFailsToStartWithInProcessStaticContent(deploymentResult); + + EventLogHelpers.VerifyEventLogEvent(deploymentResult, EventLogHelpers.InProcessHostfxrInvalid(deploymentResult), Logger); + } + + [ConditionalFact] + public async Task TargedDifferenceSharedFramework_FailedToFindNativeDependencies() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + var deploymentResult = await DeployAsync(deploymentParameters); + + Helpers.ModifyFrameworkVersionInRuntimeConfig(deploymentResult); + await AssertSiteFailsToStartWithInProcessStaticContent(deploymentResult); + + EventLogHelpers.VerifyEventLogEvent(deploymentResult, EventLogHelpers.InProcessFailedToFindNativeDependencies(deploymentResult), Logger); + } + + [ConditionalFact] + public async Task RemoveInProcessReference_FailedToFindRequestHandler() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.ApplicationType = ApplicationType.Standalone; + var deploymentResult = await DeployAsync(deploymentParameters); + + File.Delete(Path.Combine(deploymentResult.ContentRoot, "aspnetcorev2_inprocess.dll")); + + await AssertSiteFailsToStartWithInProcessStaticContent(deploymentResult); + + if (DeployerSelector.IsForwardsCompatibilityTest) + { + EventLogHelpers.VerifyEventLogEvent(deploymentResult, EventLogHelpers.InProcessFailedToFindNativeDependencies(deploymentResult), Logger); + } + else + { + EventLogHelpers.VerifyEventLogEvent(deploymentResult, EventLogHelpers.InProcessFailedToFindRequestHandler(deploymentResult), Logger); + } + } + + [ConditionalFact] + public async Task StartupTimeoutIsApplied() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} Hang"); + deploymentParameters.WebConfigActionList.Add( + WebConfigHelpers.AddOrModifyAspNetCoreSection("startupTimeLimit", "1")); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("/"); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvents(deploymentResult, + EventLogHelpers.InProcessFailedToStart(deploymentResult, "Managed server didn't initialize after 1000 ms.") + ); + } + + [ConditionalFact] + public async Task ShutdownTimeoutIsApplied() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} HangOnStop"); + deploymentParameters.WebConfigActionList.Add( + WebConfigHelpers.AddOrModifyAspNetCoreSection("shutdownTimeLimit", "1")); + + var deploymentResult = await DeployAsync(deploymentParameters); + + Assert.Equal("Hello World", await deploymentResult.HttpClient.GetStringAsync("/HelloWorld")); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvents(deploymentResult, + EventLogHelpers.InProcessStarted(deploymentResult), + EventLogHelpers.InProcessFailedToStop(deploymentResult, "")); + } + + [ConditionalFact] + public async Task CheckInvalidHostingModelParameter() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + deploymentParameters.WebConfigActionList.Add(WebConfigHelpers.AddOrModifyAspNetCoreSection("hostingModel", "bogus")); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("HelloWorld"); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvents(deploymentResult, + EventLogHelpers.ConfigurationLoadError(deploymentResult, "Unknown hosting model 'bogus'. Please specify either hostingModel=\"inprocess\" or hostingModel=\"outofprocess\" in the web.config file.") + ); + } + + private static Dictionary)> InvalidConfigTransformations = InitInvalidConfigTransformations(); + public static IEnumerable InvalidConfigTransformationsScenarios => InvalidConfigTransformations.ToTheoryData(); + + [ConditionalTheory] + [MemberData(nameof(InvalidConfigTransformationsScenarios))] + public async Task ReportsWebConfigAuthoringErrors(string scenario) + { + var (expectedError, action) = InvalidConfigTransformations[scenario]; + var iisDeploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + iisDeploymentParameters.WebConfigActionList.Add((element, _) => action(element)); + var deploymentResult = await DeployAsync(iisDeploymentParameters); + var result = await deploymentResult.HttpClient.GetAsync("/HelloWorld"); + Assert.Equal(HttpStatusCode.InternalServerError, result.StatusCode); + + // Config load errors might not allow us to initialize log file + deploymentResult.AllowNoLogs(); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvents(deploymentResult, + EventLogHelpers.ConfigurationLoadError(deploymentResult, expectedError) + ); + } + + public static Dictionary)> InitInvalidConfigTransformations() + { + var dictionary = new Dictionary)>(); + dictionary.Add("Empty process path", + ( + "Attribute 'processPath' is required.", + element => element.Descendants("aspNetCore").Single().SetAttributeValue("processPath", "") + )); + dictionary.Add("Unknown hostingModel", + ( + "Unknown hosting model 'asdf'.", + element => element.Descendants("aspNetCore").Single().SetAttributeValue("hostingModel", "asdf") + )); + dictionary.Add("environmentVariables with add", + ( + "Unable to get required configuration section 'system.webServer/aspNetCore'. Possible reason is web.config authoring error.", + element => element.Descendants("aspNetCore").Single().GetOrAdd("environmentVariables").GetOrAdd("add") + )); + return dictionary; + } + + private static Dictionary> PortableConfigTransformations = InitPortableWebConfigTransformations(); + public static IEnumerable PortableConfigTransformationsScenarios => PortableConfigTransformations.ToTheoryData(); + + [ConditionalTheory] + [MemberData(nameof(PortableConfigTransformationsScenarios))] + public async Task StartsWithWebConfigVariationsPortable(string scenario) + { + var action = PortableConfigTransformations[scenario]; + var iisDeploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + var expectedArguments = action(iisDeploymentParameters); + var result = await DeployAsync(iisDeploymentParameters); + Assert.Equal(expectedArguments, await result.HttpClient.GetStringAsync("/CommandLineArgs")); + } + + public static Dictionary> InitPortableWebConfigTransformations() + { + var dictionary = new Dictionary>(); + var pathWithSpace = "\u03c0 \u2260 3\u00b714"; + + dictionary.Add("App in bin subdirectory full path to dll using exec and quotes", + parameters => { + MoveApplication(parameters, "bin"); + parameters.TransformArguments((arguments, root) => "exec " + Path.Combine(root, "bin", arguments)); + return ""; + }); + + dictionary.Add("App in subdirectory with space", + parameters => { + MoveApplication(parameters, pathWithSpace); + parameters.TransformArguments((arguments, root) => Path.Combine(pathWithSpace, arguments)); + return ""; + }); + + dictionary.Add("App in subdirectory with space and full path to dll", + parameters => { + MoveApplication(parameters, pathWithSpace); + parameters.TransformArguments((arguments, root) => Path.Combine(root, pathWithSpace, arguments)); + return ""; + }); + + dictionary.Add("App in bin subdirectory with space full path to dll using exec and quotes", + parameters => { + MoveApplication(parameters, pathWithSpace); + parameters.TransformArguments((arguments, root) => "exec \"" + Path.Combine(root, pathWithSpace, arguments) + "\" extra arguments"); + return "extra|arguments"; + }); + + dictionary.Add("App in bin subdirectory and quoted argument", + parameters => { + MoveApplication(parameters, "bin"); + parameters.TransformArguments((arguments, root) => Path.Combine("bin", arguments) + " \"extra argument\""); + return "extra argument"; + }); + + dictionary.Add("App in bin subdirectory full path to dll", + parameters => { + MoveApplication(parameters, "bin"); + parameters.TransformArguments((arguments, root) => Path.Combine(root, "bin", arguments) + " extra arguments"); + return "extra|arguments"; + }); + return dictionary; + } + + + private static Dictionary> StandaloneConfigTransformations = InitStandaloneConfigTransformations(); + public static IEnumerable StandaloneConfigTransformationsScenarios => StandaloneConfigTransformations.ToTheoryData(); + + [ConditionalTheory] + [MemberData(nameof(StandaloneConfigTransformationsScenarios))] + public async Task StartsWithWebConfigVariationsStandalone(string scenario) + { + var action = StandaloneConfigTransformations[scenario]; + var iisDeploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + iisDeploymentParameters.ApplicationType = ApplicationType.Standalone; + var expectedArguments = action(iisDeploymentParameters); + var result = await DeployAsync(iisDeploymentParameters); + Assert.Equal(expectedArguments, await result.HttpClient.GetStringAsync("/CommandLineArgs")); + } + + public static Dictionary> InitStandaloneConfigTransformations() + { + var dictionary = new Dictionary>(); + var pathWithSpace = "\u03c0 \u2260 3\u00b714"; + + dictionary.Add("App in subdirectory", + parameters => { + MoveApplication(parameters, pathWithSpace); + parameters.TransformPath((path, root) => Path.Combine(pathWithSpace, path)); + parameters.TransformArguments((arguments, root) => "\"additional argument\""); + return "additional argument"; + }); + + dictionary.Add("App in bin subdirectory full path", + parameters => { + MoveApplication(parameters, pathWithSpace); + parameters.TransformPath((path, root) => Path.Combine(root, pathWithSpace, path)); + parameters.TransformArguments((arguments, root) => "additional arguments"); + return "additional|arguments"; + }); + + return dictionary; + } + + private static void MoveApplication( + IISDeploymentParameters parameters, + string subdirectory) + { + parameters.WebConfigActionList.Add((config, contentRoot) => + { + var source = new DirectoryInfo(contentRoot); + var subDirectoryPath = source.CreateSubdirectory(subdirectory); + + // Copy everything into a subfolder + Helpers.CopyFiles(source, subDirectoryPath, null); + // Cleanup files + foreach (var fileSystemInfo in source.GetFiles()) + { + fileSystemInfo.Delete(); + } + }); + } + + private async Task AssertSiteFailsToStartWithInProcessStaticContent(IISDeploymentResult deploymentResult) + { + var response = await deploymentResult.HttpClient.GetAsync("/HelloWorld"); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.Contains("HTTP Error 500.0 - ANCM In-Process Handler Load Failure", await response.Content.ReadAsStringAsync()); + StopServer(); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/SynchronousReadAndWriteTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/SynchronousReadAndWriteTests.cs new file mode 100644 index 0000000000..03aa0e16e6 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Inprocess/SynchronousReadAndWriteTests.cs @@ -0,0 +1,197 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class SynchronousReadAndWriteTests: FixtureLoggedTest + { + private readonly IISTestSiteFixture _fixture; + + public SynchronousReadAndWriteTests(IISTestSiteFixture fixture): base(fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task ReadAndWriteSynchronously() + { + for (int i = 0; i < 100; i++) + { + var content = new StringContent(new string('a', 100000)); + var response = await _fixture.Client.PostAsync("ReadAndWriteSynchronously", content); + var responseText = await response.Content.ReadAsStringAsync(); + + Assert.Equal(expected: 110000, actual: responseText.Length); + } + } + + [ConditionalFact] + public async Task ReadAndWriteEcho() + { + var body = new string('a', 100000); + var content = new StringContent(body); + var response = await _fixture.Client.PostAsync("ReadAndWriteEcho", content); + var responseText = await response.Content.ReadAsStringAsync(); + + Assert.Equal(body, responseText); + } + + [ConditionalFact] + public async Task ReadAndWriteCopyToAsync() + { + var body = new string('a', 100000); + var content = new StringContent(body); + var response = await _fixture.Client.PostAsync("ReadAndWriteCopyToAsync", content); + var responseText = await response.Content.ReadAsStringAsync(); + + Assert.Equal(body, responseText); + } + + [ConditionalFact] + public async Task ReadAndWriteEchoTwice() + { + var requestBody = new string('a', 10000); + var content = new StringContent(requestBody); + var response = await _fixture.Client.PostAsync("ReadAndWriteEchoTwice", content); + var responseText = await response.Content.ReadAsStringAsync(); + + Assert.Equal(requestBody.Length * 2, responseText.Length); + } + + [ConditionalFact] + public async Task ReadSetHeaderWrite() + { + var body = "Body text"; + var content = new StringContent(body); + var response = await _fixture.Client.PostAsync("SetHeaderFromBody", content); + var responseText = await response.Content.ReadAsStringAsync(); + + Assert.Equal(body, response.Headers.GetValues("BodyAsString").Single()); + Assert.Equal(body, responseText); + } + + [ConditionalFact] + public async Task ReadAndWriteSlowConnection() + { + using (var connection = _fixture.CreateTestConnection()) + { + var testString = "hello world"; + var request = $"POST /ReadAndWriteSlowConnection HTTP/1.0\r\n" + + $"Content-Length: {testString.Length}\r\n" + + "Host: " + "localhost\r\n" + + "\r\n" + testString; + + foreach (var c in request) + { + await connection.Send(c.ToString()); + await Task.Delay(10); + } + + await connection.Receive( + "HTTP/1.1 200 OK", + ""); + await connection.ReceiveHeaders(); + + for (int i = 0; i < 100; i++) + { + foreach (var c in testString) + { + await connection.Receive(c.ToString()); + } + await Task.Delay(10); + } + await connection.WaitForConnectionClose(); + } + } + + [ConditionalFact] + public async Task ReadAndWriteInterleaved() + { + 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 /ReadAndWriteEchoLines HTTP/1.0", + $"Content-Length: {requestLength}", + "Host: localhost", + "", + ""); + + await connection.Receive( + "HTTP/1.1 200 OK", + ""); + await connection.ReceiveHeaders(); + + foreach (var message in messages) + { + await connection.Send(message); + await connection.Receive(message); + } + + await connection.Send("\r\n"); + await connection.WaitForConnectionClose(); + } + } + + [ConditionalFact] + public async Task ConsumePartialBody() + { + using (var connection = _fixture.CreateTestConnection()) + { + var message = "Hello"; + await connection.Send( + "POST /ReadPartialBody HTTP/1.1", + $"Content-Length: {100}", + "Host: localhost", + "Connection: close", + "", + ""); + + await connection.Send(message); + + await connection.Receive( + "HTTP/1.1 200 OK", + ""); + + // This test can return both content length or chunked response + // depending on if appfunc managed to complete before write was + // issued + var headers = await connection.ReceiveHeaders(); + if (headers.Contains("Content-Length: 5")) + { + await connection.Receive("Hello"); + } + else + { + await connection.Receive( + "5", + message, + ""); + await connection.Receive( + "0", + "", + ""); + } + + await connection.WaitForConnectionClose(); + } + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/LogFileTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/LogFileTests.cs new file mode 100644 index 0000000000..1e15cde5c2 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/LogFileTests.cs @@ -0,0 +1,242 @@ +// 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.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; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class LoggingTests : LogFileTestBase + { + private readonly PublishedSitesFixture _fixture; + + public LoggingTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + public static TestMatrix TestVariants + => TestMatrix.ForServers(DeployerSelector.ServerType) + .WithTfms(Tfm.NetCoreApp22) + .WithAllApplicationTypes() + .WithAncmVersions(AncmVersion.AspNetCoreModuleV2) + .WithAllHostingModels(); + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task CheckStdoutLoggingToFile(TestVariant variant) + { + await CheckStdoutToFile(variant, "ConsoleWrite"); + } + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task CheckStdoutErrLoggingToFile(TestVariant variant) + { + await CheckStdoutToFile(variant, "ConsoleErrorWrite"); + } + + private async Task CheckStdoutToFile(TestVariant variant, string path) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + deploymentParameters.EnableLogging(_logFolderPath); + + var deploymentResult = await DeployAsync(deploymentParameters); + + await Helpers.AssertStarts(deploymentResult, path); + + StopServer(); + + var contents = Helpers.ReadAllTextFromFile(Helpers.GetExpectedLogName(deploymentResult, _logFolderPath), Logger); + + Assert.Contains("TEST MESSAGE", contents); + } + + // Move to separate file + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task InvalidFilePathForLogs_ServerStillRuns(TestVariant variant) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + + deploymentParameters.WebConfigActionList.Add( + WebConfigHelpers.AddOrModifyAspNetCoreSection("stdoutLogEnabled", "true")); + deploymentParameters.WebConfigActionList.Add( + WebConfigHelpers.AddOrModifyAspNetCoreSection("stdoutLogFile", Path.Combine("Q:", "std"))); + + var deploymentResult = await DeployAsync(deploymentParameters); + + await Helpers.AssertStarts(deploymentResult, "HelloWorld"); + + StopServer(); + if (variant.HostingModel == HostingModel.InProcess) + { + EventLogHelpers.VerifyEventLogEvent(deploymentResult, "Could not start stdout redirection in (.*)aspnetcorev2.dll. Exception message: HRESULT 0x80070003"); + EventLogHelpers.VerifyEventLogEvent(deploymentResult, "Could not stop stdout redirection in (.*)aspnetcorev2.dll. Exception message: HRESULT 0x80070002"); + } + } + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task StartupMessagesAreLoggedIntoDebugLogFile(TestVariant variant) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + deploymentParameters.HandlerSettings["debugLevel"] = "file"; + deploymentParameters.HandlerSettings["debugFile"] = "debug.txt"; + + var deploymentResult = await DeployAsync(deploymentParameters); + + await deploymentResult.HttpClient.GetAsync("/"); + + AssertLogs(Path.Combine(deploymentResult.ContentRoot, "debug.txt")); + } + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task StartupMessagesAreLoggedIntoDefaultDebugLogFile(TestVariant variant) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + deploymentParameters.HandlerSettings["debugLevel"] = "file"; + + var deploymentResult = await DeployAsync(deploymentParameters); + + await deploymentResult.HttpClient.GetAsync("/"); + + AssertLogs(Path.Combine(deploymentResult.ContentRoot, "aspnetcore-debug.log")); + } + + [ConditionalTheory] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + [MemberData(nameof(TestVariants))] + public async Task StartupMessagesAreLoggedIntoDefaultDebugLogFileWhenEnabledWithEnvVar(TestVariant variant) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + deploymentParameters.EnvironmentVariables["ASPNETCORE_MODULE_DEBUG"] = "file"; + // Add empty debugFile handler setting to prevent IIS deployer from overriding debug settings + deploymentParameters.HandlerSettings["debugFile"] = ""; + var deploymentResult = await DeployAsync(deploymentParameters); + + await deploymentResult.HttpClient.GetAsync("/"); + + AssertLogs(Path.Combine(deploymentResult.ContentRoot, "aspnetcore-debug.log")); + } + + [ConditionalTheory] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + [MemberData(nameof(TestVariants))] + public async Task StartupMessagesLogFileSwitchedWhenLogFilePresentInWebConfig(TestVariant variant) + { + var firstTempFile = Path.GetTempFileName(); + var secondTempFile = Path.GetTempFileName(); + + try + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + deploymentParameters.EnvironmentVariables["ASPNETCORE_MODULE_DEBUG_FILE"] = firstTempFile; + deploymentParameters.AddDebugLogToWebConfig(secondTempFile); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("/"); + + StopServer(); + var logContents = Helpers.ReadAllTextFromFile(firstTempFile, Logger); + + Assert.Contains("Switching debug log files to", logContents); + + AssertLogs(secondTempFile); + } + finally + { + File.Delete(firstTempFile); + File.Delete(secondTempFile); + } + } + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + + public async Task DebugLogsAreWrittenToEventLog(TestVariant variant) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + deploymentParameters.HandlerSettings["debugLevel"] = "file,eventlog"; + var deploymentResult = await StartAsync(deploymentParameters); + StopServer(); + EventLogHelpers.VerifyEventLogEvent(deploymentResult, @"\[aspnetcorev2.dll\] Initializing logs for .*?Description: IIS ASP.NET Core Module V2"); + } + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task CheckUTF8File(TestVariant variant) + { + var path = "CheckConsoleFunctions"; + + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, variant.HostingModel, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} {path}"); // For standalone this will need to remove space + + var logFolderPath = _logFolderPath + "\\彡⾔"; + deploymentParameters.EnableLogging(logFolderPath); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync(path); + + Assert.False(response.IsSuccessStatusCode); + + StopServer(); + + var contents = Helpers.ReadAllTextFromFile(Helpers.GetExpectedLogName(deploymentResult, logFolderPath), Logger); + Assert.Contains("彡⾔", contents); + + if (variant.HostingModel == HostingModel.InProcess) + { + EventLogHelpers.VerifyEventLogEvent(deploymentResult, EventLogHelpers.InProcessThreadExitStdOut(deploymentResult, "12", "(.*)彡⾔(.*)")); + } + else + { + EventLogHelpers.VerifyEventLogEvent(deploymentResult, EventLogHelpers.OutOfProcessFailedToStart(deploymentResult)); + } + } + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task OnlyOneFileCreatedWithProcessStartTime(TestVariant variant) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + + deploymentParameters.EnableLogging(_logFolderPath); + + var deploymentResult = await DeployAsync(deploymentParameters); + await Helpers.AssertStarts(deploymentResult, "ConsoleWrite"); + + StopServer(); + + Assert.Single(Directory.GetFiles(_logFolderPath), Helpers.GetExpectedLogName(deploymentResult, _logFolderPath)); + } + + private static string ReadLogs(string logPath) + { + using (var stream = File.Open(logPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + using (var streamReader = new StreamReader(stream)) + { + return streamReader.ReadToEnd(); + } + } + + private static void AssertLogs(string logPath) + { + var logContents = ReadLogs(logPath); + Assert.Contains("[aspnetcorev2.dll]", logContents); + Assert.True(logContents.Contains("[aspnetcorev2_inprocess.dll]") || logContents.Contains("[aspnetcorev2_outofprocess.dll]")); + Assert.Contains("Description: IIS ASP.NET Core Module V2. Commit:", logContents); + Assert.Contains("Description: IIS ASP.NET Core Module V2 Request Handler. Commit:", logContents); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/MultiApplicationTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/MultiApplicationTests.cs new file mode 100644 index 0000000000..3723369c89 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/MultiApplicationTests.cs @@ -0,0 +1,144 @@ +// 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.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class MultiApplicationTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + private PublishedApplication _publishedApplication; + private PublishedApplication _rootApplication; + + public MultiApplicationTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task RunsTwoOutOfProcessApps() + { + var parameters = _fixture.GetBaseDeploymentParameters(HostingModel.OutOfProcess, publish: true); + parameters.ServerConfigActionList.Add(DuplicateApplication); + var result = await DeployAsync(parameters); + var id1 = await result.HttpClient.GetStringAsync("/app1/ProcessId"); + var id2 = await result.HttpClient.GetStringAsync("/app2/ProcessId"); + Assert.NotEqual(id2, id1); + } + + [ConditionalFact] + public async Task FailsAndLogsWhenRunningTwoInProcessApps() + { + var parameters = _fixture.GetBaseDeploymentParameters(HostingModel.InProcess, publish: true); + parameters.ServerConfigActionList.Add(DuplicateApplication); + + var result = await DeployAsync(parameters); + var result1 = await result.HttpClient.GetAsync("/app1/HelloWorld"); + var result2 = await result.HttpClient.GetAsync("/app2/HelloWorld"); + Assert.Equal(200, (int)result1.StatusCode); + Assert.Equal(500, (int)result2.StatusCode); + StopServer(); + EventLogHelpers.VerifyEventLogEvent(result, "Only one inprocess application is allowed per IIS application pool"); + } + + [ConditionalTheory] + [InlineData(HostingModel.OutOfProcess)] + [InlineData(HostingModel.InProcess)] + public async Task FailsAndLogsEventLogForMixedHostingModel(HostingModel firstApp) + { + var parameters = _fixture.GetBaseDeploymentParameters(firstApp, publish: true); + parameters.ServerConfigActionList.Add(DuplicateApplication); + var result = await DeployAsync(parameters); + + // Modify hosting model of other app to be the opposite + var otherApp = firstApp == HostingModel.InProcess ? HostingModel.OutOfProcess : HostingModel.InProcess; + SetHostingModel(_publishedApplication.Path, otherApp); + + var result1 = await result.HttpClient.GetAsync("/app1/HelloWorld"); + var result2 = await result.HttpClient.GetAsync("/app2/HelloWorld"); + Assert.Equal(200, (int)result1.StatusCode); + Assert.Equal(500, (int)result2.StatusCode); + StopServer(); + EventLogHelpers.VerifyEventLogEvent(result, "Mixed hosting model is not supported."); + } + + private void SetHostingModel(string directory, HostingModel model) + { + var webConfigLocation = GetWebConfigLocation(directory); + XDocument webConfig = XDocument.Load(webConfigLocation); + webConfig.Root + .Descendants("system.webServer") + .Single() + .GetOrAdd("aspNetCore") + .SetAttributeValue("hostingModel", model.ToString()); + webConfig.Save(webConfigLocation); + } + + private void DuplicateApplication(XElement config, string contentRoot) + { + var siteElement = config + .RequiredElement("system.applicationHost") + .RequiredElement("sites") + .RequiredElement("site"); + + var application = siteElement + .RequiredElement("application"); + + application.SetAttributeValue("path", "/app1"); + + var source = new DirectoryInfo(contentRoot); + + var destination = new DirectoryInfo(contentRoot + "anotherApp"); + destination.Create(); + Helpers.CopyFiles(source, destination, Logger); + + _publishedApplication = new PublishedApplication(destination.FullName, Logger); + + var newApplication = new XElement(application); + newApplication.SetAttributeValue("path", "/app2"); + newApplication.RequiredElement("virtualDirectory") + .SetAttributeValue("physicalPath", destination.FullName); + + siteElement.Add(newApplication); + + // IIS Express requires root application to exist + var rootApplicationDirectory = new DirectoryInfo(contentRoot + "rootApp"); + rootApplicationDirectory.Create(); + + _rootApplication = new PublishedApplication(rootApplicationDirectory.FullName, Logger); + File.WriteAllText(GetWebConfigLocation(rootApplicationDirectory.FullName), ""); + + var rootApplication = new XElement(application); + rootApplication.SetAttributeValue("path", "/"); + rootApplication.RequiredElement("virtualDirectory") + .SetAttributeValue("physicalPath", rootApplicationDirectory.FullName); + + siteElement.Add(rootApplication); + } + + private static string GetWebConfigLocation(string siteRoot) + { + return Path.Combine(siteRoot, "web.config"); + } + + public override void Dispose() + { + base.Dispose(); + _rootApplication.Dispose(); + _publishedApplication.Dispose(); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/OutOfProcess/AspNetCorePortTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/OutOfProcess/AspNetCorePortTests.cs new file mode 100644 index 0000000000..3b86662717 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/OutOfProcess/AspNetCorePortTests.cs @@ -0,0 +1,125 @@ +// 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; +using System.Net.Sockets; +using System.Threading.Tasks; +using Xunit; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class AspNetCorePortTests : IISFunctionalTestBase + { + // Port range allowed by ANCM config + private const int _minPort = 1025; + private const int _maxPort = 48000; + + private static readonly Random _random = new Random(); + + private readonly PublishedSitesFixture _fixture; + + public AspNetCorePortTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + public static TestMatrix TestVariants + => TestMatrix.ForServers(DeployerSelector.ServerType) + .WithTfms(Tfm.NetCoreApp22) + .WithApplicationTypes(ApplicationType.Portable) + .WithAllAncmVersions(); + + public static IEnumerable InvalidTestVariants + => from v in TestVariants.Select(v => v.Single()) + from s in new string[] { (_minPort - 1).ToString(), (_maxPort + 1).ToString(), "noninteger" } + select new object[] { v, s }; + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task EnvVarInWebConfig_Valid(TestVariant variant) + { + // Must publish to set env vars in web.config + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + var port = GetUnusedRandomPort(); + deploymentParameters.WebConfigBasedEnvironmentVariables["ASPNETCORE_PORT"] = port.ToString(); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var responseText = await deploymentResult.HttpClient.GetStringAsync("/ServerAddresses"); + + Assert.Equal(port, new Uri(responseText).Port); + } + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task EnvVarInWebConfig_Empty(TestVariant variant) + { + // Must publish to set env vars in web.config + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + deploymentParameters.WebConfigBasedEnvironmentVariables["ASPNETCORE_PORT"] = string.Empty; + + var deploymentResult = await DeployAsync(deploymentParameters); + + var responseText = await deploymentResult.HttpClient.GetStringAsync("/ServerAddresses"); + + // If env var is empty, ANCM should assign a random port (same as no env var) + Assert.InRange(new Uri(responseText).Port, _minPort, _maxPort); + } + + [ConditionalTheory] + [MemberData(nameof(InvalidTestVariants))] + public async Task EnvVarInWebConfig_Invalid(TestVariant variant, string port) + { + // Must publish to set env vars in web.config + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + deploymentParameters.WebConfigBasedEnvironmentVariables["ASPNETCORE_PORT"] = port; + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("/ServerAddresses"); + + Assert.Equal(HttpStatusCode.BadGateway, response.StatusCode); + } + + private static int GetUnusedRandomPort() + { + // Large number of retries to prevent test failures due to port collisions, but not infinite + // to prevent infinite loop in case Bind() fails repeatedly for some other reason. + const int retries = 100; + + List exceptions = null; + + for (var i = 0; i < retries; i++) + { + var port = _random.Next(_minPort, _maxPort); + + using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + { + try + { + socket.Bind(new IPEndPoint(IPAddress.Loopback, port)); + return port; + } + catch (Exception e) + { + // Bind failed, most likely because port is in use. Save exception and retry. + if (exceptions == null) + { + exceptions = new List(retries); + } + exceptions.Add(e); + } + } + } + + throw new AggregateException($"Unable to find unused random port after {retries} retries.", exceptions); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/OutOfProcess/GlobalVersionTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/OutOfProcess/GlobalVersionTests.cs new file mode 100644 index 0000000000..f9e6836b6f --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/OutOfProcess/GlobalVersionTests.cs @@ -0,0 +1,252 @@ +// 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 System.Xml.Linq; +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; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class GlobalVersionTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public GlobalVersionTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + private const string _handlerVersion20 = "2.0.0"; + private const string _helloWorldRequest = "HelloWorld"; + private const string _helloWorldResponse = "Hello World"; + + [ConditionalFact] + public async Task GlobalVersion_DefaultWorks() + { + var deploymentParameters = GetGlobalVersionBaseDeploymentParameters(); + + deploymentParameters.HandlerSettings["handlerVersion"] = _handlerVersion20; + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync(_helloWorldRequest); + var responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(_helloWorldResponse, responseText); + } + + [ConditionalFact] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + [RequiresNewShim] + public async Task GlobalVersion_EnvironmentVariableWorks() + { + var temporaryFile = Path.GetTempFileName(); + try + { + var deploymentParameters = GetGlobalVersionBaseDeploymentParameters(); + CopyShimToOutput(deploymentParameters); + deploymentParameters.PublishApplicationBeforeDeployment = true; + deploymentParameters.EnvironmentVariables["ASPNETCORE_MODULE_OUTOFPROCESS_HANDLER"] = temporaryFile; + + var deploymentResult = await DeployAsync(deploymentParameters); + var requestHandlerPath = Path.Combine(GetANCMRequestHandlerPath(deploymentResult, _handlerVersion20), "aspnetcorev2_outofprocess.dll"); + + File.Delete(temporaryFile); + File.Move(requestHandlerPath, temporaryFile); + + var response = await deploymentResult.HttpClient.GetAsync(_helloWorldRequest); + var responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(_helloWorldResponse, responseText); + StopServer(); + } + finally + { + File.Delete(temporaryFile); + } + } + + [ConditionalTheory] + [InlineData("2.1.0")] + [InlineData("2.1.0-preview")] + public async Task GlobalVersion_NewVersionNumber_Fails(string version) + { + var deploymentParameters = GetGlobalVersionBaseDeploymentParameters(); + deploymentParameters.HandlerSettings["handlerVersion"] = version; + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync(_helloWorldRequest); + Assert.False(response.IsSuccessStatusCode); + var responseString = await response.Content.ReadAsStringAsync(); + Assert.Contains("HTTP Error 500.0 - ANCM Out-Of-Process Handler Load Failure", responseString); + } + + [ConditionalTheory] + [InlineData("2.1.0")] + [InlineData("2.1.0-preview")] + public async Task GlobalVersion_NewVersionNumber(string version) + { + var deploymentParameters = GetGlobalVersionBaseDeploymentParameters(); + CopyShimToOutput(deploymentParameters); + deploymentParameters.HandlerSettings["handlerVersion"] = version; + + var deploymentResult = await DeployAsync(deploymentParameters); + + var originalANCMPath = GetANCMRequestHandlerPath(deploymentResult, _handlerVersion20); + var newANCMPath = GetANCMRequestHandlerPath(deploymentResult, version); + Directory.Move(originalANCMPath, newANCMPath); + + var response = await deploymentResult.HttpClient.GetAsync(_helloWorldRequest); + var responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(_helloWorldResponse, responseText); + AssertLoadedVersion(version); + } + + [ConditionalTheory] + [InlineData("2.1.0")] + [InlineData("2.1.0-preview")] + public async Task GlobalVersion_MultipleRequestHandlers_PicksHighestOne(string version) + { + var deploymentParameters = GetGlobalVersionBaseDeploymentParameters(); + CopyShimToOutput(deploymentParameters); + var deploymentResult = await DeployAsync(deploymentParameters); + + var originalANCMPath = GetANCMRequestHandlerPath(deploymentResult, _handlerVersion20); + + var newANCMPath = GetANCMRequestHandlerPath(deploymentResult, version); + + CopyDirectory(originalANCMPath, newANCMPath); + + deploymentResult.HttpClient.DefaultRequestHeaders.Add("ANCMRHPath", newANCMPath); + var response = await deploymentResult.HttpClient.GetAsync("CheckRequestHandlerVersion"); + var responseText = await response.Content.ReadAsStringAsync(); + + Assert.Equal(_helloWorldResponse, responseText); + AssertLoadedVersion(version); + } + + [ConditionalTheory] + [InlineData("2.1.0")] + [InlineData("2.1.0-preview")] + public async Task GlobalVersion_MultipleRequestHandlers_UpgradeWorks(string version) + { + var deploymentParameters = GetGlobalVersionBaseDeploymentParameters(); + CopyShimToOutput(deploymentParameters); + var deploymentResult = await DeployAsync(deploymentParameters); + + var originalANCMPath = GetANCMRequestHandlerPath(deploymentResult, _handlerVersion20); + + deploymentResult.HttpClient.DefaultRequestHeaders.Add("ANCMRHPath", originalANCMPath); + var response = await deploymentResult.HttpClient.GetAsync("CheckRequestHandlerVersion"); + var responseText = await response.Content.ReadAsStringAsync(); + + Assert.Equal(_helloWorldResponse, responseText); + + StopServer(); + + deploymentResult = await DeployAsync(deploymentParameters); + + originalANCMPath = GetANCMRequestHandlerPath(deploymentResult, _handlerVersion20); + + var newANCMPath = GetANCMRequestHandlerPath(deploymentResult, version); + + CopyDirectory(originalANCMPath, newANCMPath); + + deploymentResult.HttpClient.DefaultRequestHeaders.Add("ANCMRHPath", newANCMPath); + response = await deploymentResult.HttpClient.GetAsync("CheckRequestHandlerVersion"); + responseText = await response.Content.ReadAsStringAsync(); + + Assert.Equal(_helloWorldResponse, responseText); + AssertLoadedVersion(version); + } + + [ConditionalFact] + public async Task DoesNotCrashWhenNoVersionsAvailable() + { + var deploymentParameters = GetGlobalVersionBaseDeploymentParameters(); + CopyShimToOutput(deploymentParameters); + var deploymentResult = await DeployAsync(deploymentParameters); + + var originalANCMPath = GetANCMRequestHandlerPath(deploymentResult, _handlerVersion20); + Directory.Delete(originalANCMPath, true); + var response = await deploymentResult.HttpClient.GetAsync("HelloWorld"); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + } + + private IISDeploymentParameters GetGlobalVersionBaseDeploymentParameters() + { + return _fixture.GetBaseDeploymentParameters(HostingModel.OutOfProcess, publish: true); + } + + private void CopyDirectory(string from, string to) + { + var toInfo = new DirectoryInfo(to); + toInfo.Create(); + + foreach (var file in new DirectoryInfo(from).GetFiles()) + { + file.CopyTo(Path.Combine(toInfo.FullName, file.Name)); + } + } + + private string GetANCMRequestHandlerPath(IISDeploymentResult deploymentResult, string version) + { + return Path.Combine(deploymentResult.ContentRoot, + deploymentResult.DeploymentParameters.RuntimeArchitecture.ToString(), + version); + } + + private void AssertLoadedVersion(string version) + { + StopServer(); + Assert.Contains(TestSink.Writes, context => context.Message.Contains(version + @"\aspnetcorev2_outofprocess.dll")); + } + + private static void CopyShimToOutput(IISDeploymentParameters parameters) + { + parameters.AddServerConfigAction( + (config, contentRoot) => { + var moduleNodes = config.DescendantNodesAndSelf() + .OfType() + .Where(element => + element.Name == "add" && + element.Attribute("name")?.Value.StartsWith("AspNetCoreModule") == true && + element.Attribute("image") != null); + + var sourceDirectory = new DirectoryInfo(Path.GetDirectoryName(moduleNodes.First().Attribute("image").Value)); + var destinationDirectory = new DirectoryInfo(Path.Combine(contentRoot, sourceDirectory.Name)); + destinationDirectory.Create(); + foreach (var element in moduleNodes) + { + var imageAttribute = element.Attribute("image"); + imageAttribute.Value = imageAttribute.Value.Replace(sourceDirectory.FullName, destinationDirectory.FullName); + } + CopyFiles(sourceDirectory, destinationDirectory); + }); + } + + private static void CopyFiles(DirectoryInfo source, DirectoryInfo target) + { + foreach (DirectoryInfo directoryInfo in source.GetDirectories()) + { + CopyFiles(directoryInfo, target.CreateSubdirectory(directoryInfo.Name)); + } + + foreach (FileInfo fileInfo in source.GetFiles()) + { + var destFileName = Path.Combine(target.FullName, fileInfo.Name); + fileInfo.CopyTo(destFileName); + } + } + + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/OutOfProcess/HelloWorldTest.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/OutOfProcess/HelloWorldTest.cs new file mode 100644 index 0000000000..f6e36613d7 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/OutOfProcess/HelloWorldTest.cs @@ -0,0 +1,81 @@ +// 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.Diagnostics; +using System.Linq; +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; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class HelloWorldTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public HelloWorldTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + public static TestMatrix TestVariants + => TestMatrix.ForServers(DeployerSelector.ServerType) + .WithTfms(Tfm.NetCoreApp22, Tfm.Net461) + .WithAllApplicationTypes() + .WithAllAncmVersions(); + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task HelloWorld(TestVariant variant) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant); + deploymentParameters.ServerConfigActionList.Add( + (element, _) => { + element + .RequiredElement("system.webServer") + .RequiredElement("security") + .RequiredElement("authentication") + .Element("windowsAuthentication") + ?.SetAttributeValue("enabled", "false"); + }); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("/HelloWorld"); + var responseText = await response.Content.ReadAsStringAsync(); + + Assert.Equal("Hello World", responseText); + + response = await deploymentResult.HttpClient.GetAsync("/Path/%3F%3F?query"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal("/??", responseText); + + response = await deploymentResult.HttpClient.GetAsync("/Query/%3FPath?query?"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal("?query?", responseText); + + response = await deploymentResult.HttpClient.GetAsync("/BodyLimit"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal("null", responseText); + + response = await deploymentResult.HttpClient.GetAsync("/Auth"); + responseText = await response.Content.ReadAsStringAsync(); + + Assert.Equal("null", responseText); + + Assert.Equal( + $"ContentRootPath {deploymentResult.ContentRoot}" + Environment.NewLine + + $"WebRootPath {deploymentResult.ContentRoot}\\wwwroot" + Environment.NewLine + + $"CurrentDirectory {deploymentResult.ContentRoot}", + await deploymentResult.HttpClient.GetStringAsync("/HostingEnvironment")); + + var expectedDll = variant.AncmVersion == AncmVersion.AspNetCoreModule ? "aspnetcore.dll" : "aspnetcorev2.dll"; + Assert.Contains(deploymentResult.HostProcess.Modules.OfType(), m=> m.FileName.Contains(expectedDll)); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/PublishedSitesFixture.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/PublishedSitesFixture.cs new file mode 100644 index 0000000000..282ee26109 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/PublishedSitesFixture.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + /// + /// This type just maps collection names to available fixtures + /// + [CollectionDefinition(Name)] + public class PublishedSitesCollection : ICollectionFixture, ICollectionFixture + { + public const string Name = nameof(PublishedSitesCollection); + } + + public class PublishedSitesFixture : IDisposable + { + public CachingApplicationPublisher InProcessTestSite { get; } = new CachingApplicationPublisher(Helpers.GetInProcessTestSitesPath()); + public CachingApplicationPublisher OutOfProcessTestSite { get; } = new CachingApplicationPublisher(Helpers.GetOutOfProcessTestSitesPath()); + + public void Dispose() + { + InProcessTestSite.Dispose(); + OutOfProcessTestSite.Dispose(); + } + + public IISDeploymentParameters GetBaseDeploymentParameters(HostingModel hostingModel = HostingModel.InProcess, bool publish = false) + { + var publisher = hostingModel == HostingModel.InProcess ? InProcessTestSite : OutOfProcessTestSite; + return GetBaseDeploymentParameters(publisher, hostingModel, publish); + } + public IISDeploymentParameters GetBaseDeploymentParameters(TestVariant variant, bool publish = false) + { + var publisher = variant.HostingModel == HostingModel.InProcess ? InProcessTestSite : OutOfProcessTestSite; + return GetBaseDeploymentParameters(publisher, new DeploymentParameters(variant), publish); + } + + public IISDeploymentParameters GetBaseDeploymentParameters(ApplicationPublisher publisher, HostingModel hostingModel = HostingModel.InProcess, bool publish = false) + { + return GetBaseDeploymentParameters( + publisher, + new DeploymentParameters(publisher.ApplicationPath, DeployerSelector.ServerType, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64) + { + HostingModel = hostingModel, + TargetFramework = "netcoreapp2.2", + AncmVersion = AncmVersion.AspNetCoreModuleV2 + }, + publish); + } + + public IISDeploymentParameters GetBaseDeploymentParameters(ApplicationPublisher publisher, DeploymentParameters baseParameters, bool publish = false) + { + return new IISDeploymentParameters(baseParameters) + { + ApplicationPublisher = publisher, + ApplicationPath = publisher.ApplicationPath, + PublishApplicationBeforeDeployment = publish + }; + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/RequiresNewHandler.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/RequiresNewHandler.cs new file mode 100644 index 0000000000..cbe43ec0c7 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/RequiresNewHandler.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Testing.xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] + public sealed class RequiresNewHandlerAttribute : Attribute, ITestCondition + { + public bool IsMet => !DeployerSelector.IsForwardsCompatibilityTest; + + public string SkipReason => "Test verifies new behavior in the aspnetcorev2_inprocess.dll that isn't supported in earlier versions."; + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/RequiresNewShim.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/RequiresNewShim.cs new file mode 100644 index 0000000000..b0bc50a83b --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/RequiresNewShim.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Testing.xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] + public sealed class RequiresNewShimAttribute : Attribute, ITestCondition + { + public bool IsMet => !DeployerSelector.IsBackwardsCompatiblityTest; + + public string SkipReason => "Test verifies new behavior in the aspnetcorev2.dll that isn't supported in earlier versions."; + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/ServerAbortTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/ServerAbortTests.cs new file mode 100644 index 0000000000..8ebd70db12 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/ServerAbortTests.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + public abstract class ServerAbortTests: FixtureLoggedTest + { + private readonly IISTestSiteFixture _fixture; + + [Collection(IISTestSiteCollection.Name)] + public class InProc: ServerAbortTests + { + public InProc(IISTestSiteFixture fixture) : base(fixture) { } + } + + [Collection(OutOfProcessTestSiteCollection.Name)] + public class OutOfProcess: ServerAbortTests + { + public OutOfProcess(OutOfProcessTestSiteFixture fixture) : base(fixture) { } + } + + [Collection(OutOfProcessV1TestSiteCollection.Name)] + public class OutOfProcessV1: ServerAbortTests + { + public OutOfProcessV1(OutOfProcessV1TestSiteFixture fixture) : base(fixture) { } + } + + protected ServerAbortTests(IISTestSiteFixture fixture) : base(fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task ClosesConnectionOnServerAbort() + { + try + { + var response = await _fixture.Client.GetAsync("/Abort").DefaultTimeout(); + + // 502 is expected for outofproc but not for inproc + if (_fixture.DeploymentResult.DeploymentParameters.HostingModel == HostingModel.OutOfProcess) + { + Assert.Equal(HttpStatusCode.BadGateway, response.StatusCode); + // 0x80072f78 ERROR_HTTP_INVALID_SERVER_RESPONSE The server returned an invalid or unrecognized response + Assert.Contains("0x80072f78", await response.Content.ReadAsStringAsync()); + } + else + { + Assert.True(false, "Should not reach here"); + } + } + catch (HttpRequestException) + { + // Connection reset is expected both for outofproc and inproc + } + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/SkipIfNotAdminAttribute.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/SkipIfNotAdminAttribute.cs new file mode 100644 index 0000000000..d2acb70415 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/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/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/SkipVSTSAttribute.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/SkipVSTSAttribute.cs new file mode 100644 index 0000000000..8e88fc8c26 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/SkipVSTSAttribute.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] + public sealed class SkipInVSTSAttribute : Attribute, ITestCondition + { + public bool IsMet => string.IsNullOrEmpty(Environment.GetEnvironmentVariable("SYSTEM_TASKDEFINITIONSURI")); + + public string SkipReason => "Running in VSTS"; + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/AppVerifier.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/AppVerifier.cs new file mode 100644 index 0000000000..7984193e29 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/AppVerifier.cs @@ -0,0 +1,83 @@ +// 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.Diagnostics; +using System.Linq; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + public class AppVerifier + { + private static readonly TimeSpan AppVerifierCommandTimeout = TimeSpan.FromSeconds(5); + + public static IDisposable Disable(ServerType serverType, int code) + { + // Set in SetupTestEnvironment.ps1 + var enabledCodes = (Environment.GetEnvironmentVariable("APPVERIFIER_ENABLED_CODES") ?? "").Split(' '); + string processName; + switch (serverType) + { + case ServerType.IISExpress: + processName = "iisexpress.exe"; + break; + case ServerType.IIS: + processName = "w3wp.exe"; + break; + default: + throw new ArgumentOutOfRangeException(nameof(serverType), serverType, null); + } + + if (!enabledCodes.Contains(code.ToString())) + { + return null; + } + + RunProcessAndWaitForExit("appverif.exe", $"-configure {code} -for {processName} -with ErrorReport=0", AppVerifierCommandTimeout); + return new AppVerifierToken(processName, code.ToString()); + } + + private static void RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout) + { + var startInfo = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + }; + + var process = Process.Start(startInfo); + + if (!process.WaitForExit((int)timeout.TotalMilliseconds)) + { + process.Kill(); + } + + if (process.ExitCode != 0) + { + throw new InvalidOperationException($"Exit code {process.ExitCode} when running {fileName} {arguments}. Stdout: {process.StandardOutput.ReadToEnd()} Stderr: {process.StandardError.ReadToEnd()}"); + } + } + + public class AppVerifierToken : IDisposable + { + private readonly string _processName; + + private readonly string _codes; + + public AppVerifierToken(string processName, string codes) + { + _processName = processName; + _codes = codes; + } + + public void Dispose() + { + // + RunProcessAndWaitForExit("appverif.exe", $"-configure {_codes} -for {_processName} -with ErrorReport={Environment.GetEnvironmentVariable("APPVERIFIER_LEVEL")}", AppVerifierCommandTimeout); + } + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/EventLogHelpers.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/EventLogHelpers.cs new file mode 100644 index 0000000000..78c77fd2ca --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/EventLogHelpers.cs @@ -0,0 +1,210 @@ +// 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.Diagnostics; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + public class EventLogHelpers + { + public static void VerifyEventLogEvent(IISDeploymentResult deploymentResult, string expectedRegexMatchString) + { + Assert.True(deploymentResult.HostProcess.HasExited); + + var entries = GetEntries(deploymentResult); + AssertSingleEntry(expectedRegexMatchString, entries); + } + + public static void VerifyEventLogEvent(IISDeploymentResult deploymentResult, string expectedRegexMatchString, ILogger logger) + { + Assert.True(deploymentResult.HostProcess.HasExited); + + var entries = GetEntries(deploymentResult); + try + { + AssertSingleEntry(expectedRegexMatchString, entries); + } + catch (Exception ex) + { + foreach (var entry in entries) + { + logger.LogInformation(entry.Message); + } + throw ex; + } + } + + public static void VerifyEventLogEvents(IISDeploymentResult deploymentResult, params string[] expectedRegexMatchString) + { + Assert.True(deploymentResult.HostProcess.HasExited); + + var entries = GetEntries(deploymentResult).ToList(); + foreach (var regexString in expectedRegexMatchString) + { + var matchedEntries = AssertSingleEntry(regexString, entries); + + foreach (var matchedEntry in matchedEntries) + { + entries.Remove(matchedEntry); + } + } + + Assert.True(0 == entries.Count, $"Some entries were not matched by any regex {FormatEntries(entries)}"); + } + + private static EventLogEntry[] AssertSingleEntry(string regexString, IEnumerable entries) + { + var expectedRegex = new Regex(regexString, RegexOptions.Singleline); + var matchedEntries = entries.Where(entry => expectedRegex.IsMatch(entry.Message)).ToArray(); + Assert.True(matchedEntries.Length > 0, $"No entries matched by '{regexString}'"); + Assert.True(matchedEntries.Length < 2, $"Multiple entries matched by '{regexString}': {FormatEntries(matchedEntries)}"); + return matchedEntries; + } + + private static string FormatEntries(IEnumerable entries) + { + return string.Join(",", entries.Select(e => e.Message)); + } + + private static IEnumerable GetEntries(IISDeploymentResult deploymentResult) + { + var eventLog = new EventLog("Application"); + + // Eventlog is already sorted based on time of event in ascending time. + // Check results in reverse order. + var processIdString = $"Process Id: {deploymentResult.HostProcess.Id}."; + + // Event log messages round down to the nearest second, so subtract a second + var processStartTime = deploymentResult.HostProcess.StartTime.AddSeconds(-1); + for (var i = eventLog.Entries.Count - 1; i >= 0; i--) + { + var eventLogEntry = eventLog.Entries[i]; + if (eventLogEntry.TimeGenerated < processStartTime) + { + // If event logs is older than the process start time, we didn't find a match. + break; + } + + if (eventLogEntry.ReplacementStrings == null || + eventLogEntry.ReplacementStrings.Length < 3) + { + continue; + } + + // ReplacementStings == EventData collection in EventLog + // This is unaffected if event providers are not registered correctly + if (eventLogEntry.Source == AncmVersionToMatch(deploymentResult) && + processIdString == eventLogEntry.ReplacementStrings[1]) + { + yield return eventLogEntry; + } + } + } + + private static string AncmVersionToMatch(IISDeploymentResult deploymentResult) + { + return "IIS " + + (deploymentResult.DeploymentParameters.ServerType == ServerType.IISExpress ? "Express " : "") + + "AspNetCore Module" + + (deploymentResult.DeploymentParameters.AncmVersion == AncmVersion.AspNetCoreModuleV2 ? " V2" : ""); + } + + public static string Started(IISDeploymentResult deploymentResult) + { + if (deploymentResult.DeploymentParameters.HostingModel == HostingModel.InProcess) + { + return InProcessStarted(deploymentResult); + } + else + { + return OutOfProcessStarted(deploymentResult); + } + } + + public static string InProcessStarted(IISDeploymentResult deploymentResult) + { + return $"Application '{EscapedContentRoot(deploymentResult)}' started the coreclr in-process successfully"; + } + + public static string OutOfProcessStarted(IISDeploymentResult deploymentResult) + { + return $"Application '/LM/W3SVC/1/ROOT' started process '\\d+' successfully and process '\\d+' is listening on port '\\d+'."; + } + + public static string InProcessFailedToStart(IISDeploymentResult deploymentResult, string reason) + { + return $"Application '/LM/W3SVC/1/ROOT' with physical root '{EscapedContentRoot(deploymentResult)}' failed to load clr and managed application. {reason}"; + } + + public static string InProcessFailedToStop(IISDeploymentResult deploymentResult, string reason) + { + return "Failed to gracefully shutdown application 'MACHINE/WEBROOT/APPHOST/.*?'."; + } + + public static string InProcessThreadException(IISDeploymentResult deploymentResult, string reason) + { + return $"Application '/LM/W3SVC/1/ROOT' with physical root '{EscapedContentRoot(deploymentResult)}' hit unexpected managed exception{reason}"; + } + + public static string InProcessThreadExit(IISDeploymentResult deploymentResult, string code) + { + return $"Application '/LM/W3SVC/1/ROOT' with physical root '{EscapedContentRoot(deploymentResult)}' hit unexpected managed background thread exit, exit code = '{code}'."; + } + public static string InProcessThreadExitStdOut(IISDeploymentResult deploymentResult, string code, string output) + { + return $"Application '/LM/W3SVC/1/ROOT' with physical root '{EscapedContentRoot(deploymentResult)}' hit unexpected managed background thread exit, exit code = '{code}'. Last 4KB characters of captured stdout and stderr logs:\r\n{output}"; + } + + public static string FailedToStartApplication(IISDeploymentResult deploymentResult, string code) + { + return $"Failed to start application '/LM/W3SVC/1/ROOT', ErrorCode '{code}'."; + } + + public static string ConfigurationLoadError(IISDeploymentResult deploymentResult, string reason) + { + return $"Could not load configuration. Exception message: {reason}"; + } + + public static string OutOfProcessFailedToStart(IISDeploymentResult deploymentResult) + { + return $"Application '/LM/W3SVC/1/ROOT' with physical root '{EscapedContentRoot(deploymentResult)}' failed to start process with " + + $"commandline '(.*)' with multiple retries. " + + $"The last try of listening port is '(.*)'. See previous warnings for details."; + } + + public static string InProcessHostfxrInvalid(IISDeploymentResult deploymentResult) + { + return $"Hostfxr version used does not support 'hostfxr_get_native_search_directories', update the version of hostfxr to a higher version. Path to hostfxr: '(.*)'."; + } + + public static string InProcessFailedToFindNativeDependencies(IISDeploymentResult deploymentResult) + { + return "Invoking hostfxr to find the inprocess request handler failed without finding any native dependencies. " + + "This most likely means the app is misconfigured, please check the versions of Microsoft.NetCore.App and Microsoft.AspNetCore.App that " + + "are targeted by the application and are installed on the machine."; + } + + public static string InProcessFailedToFindRequestHandler(IISDeploymentResult deploymentResult) + { + return "Could not find the assembly '(.*)' referenced for the in-process application. Please confirm the Microsoft.AspNetCore.Server.IIS package is referenced in your application."; + } + + private static string EscapedContentRoot(IISDeploymentResult deploymentResult) + { + var contentRoot = deploymentResult.ContentRoot; + if (!contentRoot.EndsWith('\\')) + { + contentRoot += '\\'; + } + return Regex.Escape(contentRoot); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/FunctionalTestsBase.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/FunctionalTestsBase.cs new file mode 100644 index 0000000000..1a1a4ce490 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/FunctionalTestsBase.cs @@ -0,0 +1,59 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.Extensions.Logging.Testing; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + public class FunctionalTestsBase : LoggedTest + { + private const string DebugEnvironmentVariable = "ASPNETCORE_MODULE_DEBUG"; + + public FunctionalTestsBase(ITestOutputHelper output = null) : base(output) + { + } + + protected IISDeployerBase _deployer; + + protected ApplicationDeployer CreateDeployer(IISDeploymentParameters parameters) + { + if (parameters.ServerType == ServerType.IISExpress && + !parameters.EnvironmentVariables.ContainsKey(DebugEnvironmentVariable)) + { + parameters.EnvironmentVariables[DebugEnvironmentVariable] = "console"; + } + + return IISApplicationDeployerFactory.Create(parameters, LoggerFactory); + } + + protected virtual async Task DeployAsync(IISDeploymentParameters parameters) + { + _deployer = (IISDeployerBase)CreateDeployer(parameters); + return (IISDeploymentResult)await _deployer.DeployAsync(); + } + + protected virtual async Task StartAsync(IISDeploymentParameters parameters) + { + var result = await DeployAsync(parameters); + await result.AssertStarts(); + return result; + } + + public override void Dispose() + { + StopServer(false); + } + + public void StopServer(bool gracefulShutdown = true) + { + _deployer?.Dispose(gracefulShutdown); + _deployer = null; + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/Helpers.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/Helpers.cs new file mode 100644 index 0000000000..1e31126eab --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/Helpers.cs @@ -0,0 +1,241 @@ +// 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.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + public static class Helpers + { + private static readonly TimeSpan RetryRequestDelay = TimeSpan.FromMilliseconds(100); + private static readonly int RetryRequestCount = 10; + + public static string GetTestWebSitePath(string name) + { + return Path.Combine(TestPathUtilities.GetSolutionRootDirectory("IISIntegration"),"test", "WebSites", name); + } + + public static string GetInProcessTestSitesPath() + { + return DeployerSelector.IsForwardsCompatibilityTest ? GetTestWebSitePath("InProcessForwardsCompatWebSite") : GetTestWebSitePath("InProcessWebSite"); + } + + public static string GetOutOfProcessTestSitesPath() => GetTestWebSitePath("OutOfProcessWebSite"); + + public static async Task AssertStarts(this IISDeploymentResult deploymentResult, string path = "/HelloWorld") + { + var response = await deploymentResult.HttpClient.GetAsync(path); + + var responseText = await response.Content.ReadAsStringAsync(); + + Assert.Equal("Hello World", responseText); + } + + public static async Task StressLoad(HttpClient httpClient, string path, Action action) + { + async Task RunRequests() + { + var connection = new HttpClient() { BaseAddress = httpClient.BaseAddress }; + + for (int j = 0; j < 10; j++) + { + var response = await connection.GetAsync(path); + action(response); + } + } + + List tasks = new List(); + for (int i = 0; i < 10; i++) + { + tasks.Add(Task.Run(RunRequests)); + } + + await Task.WhenAll(tasks); + } + + public static string GetFrebFolder(string folder, IISDeploymentResult result) + { + if (result.DeploymentParameters.ServerType == ServerType.IISExpress) + { + return Path.Combine(folder, result.DeploymentParameters.SiteName); + } + else + { + return Path.Combine(folder, "W3SVC1"); + } + } + + public static void CopyFiles(DirectoryInfo source, DirectoryInfo target, ILogger logger) + { + foreach (DirectoryInfo directoryInfo in source.GetDirectories()) + { + if (directoryInfo.FullName != target.FullName) + { + CopyFiles(directoryInfo, target.CreateSubdirectory(directoryInfo.Name), logger); + } + } + logger?.LogDebug($"Processing {target.FullName}"); + foreach (FileInfo fileInfo in source.GetFiles()) + { + logger?.LogDebug($" Copying {fileInfo.Name}"); + var destFileName = Path.Combine(target.FullName, fileInfo.Name); + fileInfo.CopyTo(destFileName); + } + } + + public static void ModifyWebConfig(this DeploymentResult deploymentResult, Action action) + { + var webConfigPath = Path.Combine(deploymentResult.ContentRoot, "web.config"); + var document = XDocument.Load(webConfigPath); + action(document.Root); + document.Save(webConfigPath); + } + + public static Task RetryRequestAsync(this HttpClient client, string uri, Func predicate) + { + return RetryRequestAsync(client, uri, message => Task.FromResult(predicate(message))); + } + + public static async Task RetryRequestAsync(this HttpClient client, string uri, Func> predicate) + { + HttpResponseMessage response = await client.GetAsync(uri); + + for (var i = 0; i < RetryRequestCount && !await predicate(response); i++) + { + // Keep retrying until app_offline is present. + response = await client.GetAsync(uri); + await Task.Delay(RetryRequestDelay); + } + + if (!await predicate(response)) + { + throw new InvalidOperationException($"Didn't get response that satisfies predicate after {RetryRequestCount} retries"); + } + + return response; + } + + public static async Task Retry(Func func, int attempts, int msDelay) + { + var exceptions = new List(); + + for (var attempt = 0; attempt < attempts; attempt++) + { + try + { + await func(); + return; + } + catch (Exception e) + { + exceptions.Add(e); + } + await Task.Delay(msDelay); + } + + throw new AggregateException(exceptions); + } + + public static void AssertWorkerProcessStop(this IISDeploymentResult deploymentResult, int? timeout = null) + { + var hostProcess = deploymentResult.HostProcess; + Assert.True(hostProcess.WaitForExit(timeout ?? (int)TimeoutExtensions.DefaultTimeoutValue.TotalMilliseconds)); + + if (deploymentResult.DeploymentParameters.ServerType == ServerType.IISExpress) + { + Assert.Equal(0, hostProcess.ExitCode); + } + } + + + public static async Task AssertRecycledAsync(this IISDeploymentResult deploymentResult, Func verificationAction = null) + { + if (deploymentResult.DeploymentParameters.HostingModel != HostingModel.InProcess) + { + throw new NotSupportedException(); + } + + deploymentResult.AssertWorkerProcessStop(); + if (deploymentResult.DeploymentParameters.ServerType == ServerType.IIS) + { + verificationAction = verificationAction ?? (() => deploymentResult.AssertStarts()); + await verificationAction(); + } + } + + public static IEnumerable ToTheoryData(this Dictionary dictionary) + { + return dictionary.Keys.Select(k => new[] { k }); + } + + public static string GetExpectedLogName(IISDeploymentResult deploymentResult, string logFolderPath) + { + var startTime = deploymentResult.HostProcess.StartTime.ToUniversalTime(); + + if (deploymentResult.DeploymentParameters.HostingModel == HostingModel.InProcess) + { + return Path.Combine(logFolderPath, $"std_{startTime.Year}{startTime.Month:D2}" + + $"{startTime.Day:D2}{startTime.Hour:D2}" + + $"{startTime.Minute:D2}{startTime.Second:D2}_" + + $"{deploymentResult.HostProcess.Id}.log"); + } + else + { + return Directory.GetFiles(logFolderPath).Single(); + } + } + + public static void ModifyFrameworkVersionInRuntimeConfig(IISDeploymentResult deploymentResult) + { + var path = Path.Combine(deploymentResult.ContentRoot, "InProcessWebSite.runtimeconfig.json"); + dynamic depsFileContent = JsonConvert.DeserializeObject(File.ReadAllText(path)); + depsFileContent["runtimeOptions"]["framework"]["version"] = "2.9.9"; + var output = JsonConvert.SerializeObject(depsFileContent); + File.WriteAllText(path, output); + } + + public static void AllowNoLogs(this IISDeploymentResult deploymentResult) + { + File.AppendAllText( + Path.Combine(deploymentResult.DeploymentParameters.PublishedApplicationRootPath, "aspnetcore-debug.log"), + "Running test allowed log file to be empty." + Environment.NewLine); + } + + public static string ReadAllTextFromFile(string filename, ILogger logger) + { + try + { + return File.ReadAllText(filename); + } + catch (Exception ex) + { + // check if there is a dotnet.exe, iisexpress.exe, or w3wp.exe processes still running. + var hostingProcesses = Process.GetProcessesByName("dotnet") + .Concat(Process.GetProcessesByName("iisexpress")) + .Concat(Process.GetProcessesByName("w3wp")); + + logger.LogError($"Could not read file content. Exception message {ex.Message}"); + logger.LogError("Current hosting exes running:"); + + foreach (var hostingProcess in hostingProcesses) + { + logger.LogError($"{hostingProcess.ProcessName} pid: {hostingProcess.Id} hasExited: {hostingProcess.HasExited.ToString()}"); + } + throw ex; + } + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISCapability.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISCapability.cs new file mode 100644 index 0000000000..0a080bb702 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISCapability.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Flags] + public enum IISCapability + { + None = 0, + Websockets = 1, + WindowsAuthentication = 2, + PoolEnvironmentVariables = 4, + ShutdownToken = 8, + DynamicCompression = 16, + ApplicationInitialization = 32, + TracingModule = 64, + FailedRequestTracingModule = 128, + BasicAuthentication = 256 + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISCompressionSiteCollection.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISCompressionSiteCollection.cs new file mode 100644 index 0000000000..2c424943f3 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/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/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISCompressionSiteFixture.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISCompressionSiteFixture.cs new file mode 100644 index 0000000000..3aff68d11b --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/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/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISFunctionalTestBase.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISFunctionalTestBase.cs new file mode 100644 index 0000000000..a20a5e2e0e --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISFunctionalTestBase.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities +{ + public class IISFunctionalTestBase : FunctionalTestsBase + { + public IISFunctionalTestBase(ITestOutputHelper output = null) : base(output) + { + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISTestSiteCollection.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISTestSiteCollection.cs new file mode 100644 index 0000000000..562d63adbe --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISTestSiteCollection.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + /// + /// This type just maps collection names to available fixtures + /// + [CollectionDefinition(Name)] + public class IISTestSiteCollection : ICollectionFixture + { + public const string Name = nameof(IISTestSiteCollection); + } + + [CollectionDefinition(Name)] + public class OutOfProcessTestSiteCollection : ICollectionFixture + { + public const string Name = nameof(OutOfProcessTestSiteCollection); + } + + [CollectionDefinition(Name)] + public class OutOfProcessV1TestSiteCollection : ICollectionFixture + { + public const string Name = nameof(OutOfProcessV1TestSiteCollection); + } + +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISTestSiteFixture.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISTestSiteFixture.cs new file mode 100644 index 0000000000..e8cfd8f641 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISTestSiteFixture.cs @@ -0,0 +1,191 @@ +// 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 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 OutOfProcessTestSiteFixture : IISTestSiteFixture + { + public OutOfProcessTestSiteFixture() : base(Configure) + { + } + + private static void Configure(IISDeploymentParameters deploymentParameters) + { + deploymentParameters.ApplicationPath = Helpers.GetOutOfProcessTestSitesPath(); + deploymentParameters.HostingModel = HostingModel.OutOfProcess; + } + } + + public class OutOfProcessV1TestSiteFixture : IISTestSiteFixture + { + public OutOfProcessV1TestSiteFixture() : base(Configure) + { + } + + private static void Configure(IISDeploymentParameters deploymentParameters) + { + deploymentParameters.ApplicationPath = Helpers.GetOutOfProcessTestSitesPath(); + deploymentParameters.HostingModel = HostingModel.OutOfProcess; + deploymentParameters.AncmVersion = AncmVersion.AspNetCoreModule; + } + } + + public class IISTestSiteFixture : IDisposable + { + private ApplicationDeployer _deployer; + private ILoggerFactory _loggerFactory; + private ForwardingProvider _forwardingProvider; + private IISDeploymentResult _deploymentResult; + private readonly Action _configure; + + public IISTestSiteFixture() : this(_ => { }) + { + } + + 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() + { + return new TestConnection(Client.BaseAddress.Port); + } + + public void Dispose() + { + _deployer?.Dispose(); + } + + public void Attach(LoggedTest test) + { + if (_forwardingProvider.LoggerFactory != null) + { + throw new InvalidOperationException("Test instance is already attached to this fixture"); + } + + _forwardingProvider.LoggerFactory = test.LoggerFactory; + } + + public void Detach(LoggedTest test) + { + if (_forwardingProvider.LoggerFactory != test.LoggerFactory) + { + throw new InvalidOperationException("Different test is attached to this fixture"); + } + + _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(); + + private ILoggerFactory _loggerFactory; + + public ILoggerFactory LoggerFactory + { + get => _loggerFactory; + set + { + + lock (_loggers) + { + _loggerFactory = value; + foreach (var logger in _loggers) + { + logger.Logger = _loggerFactory?.CreateLogger("FIXTURE:" + logger.Name); + } + } + } + } + + public void Dispose() + { + lock (_loggers) + { + _loggers.Clear(); + } + } + + public ILogger CreateLogger(string categoryName) + { + lock (_loggers) + { + var logger = new ForwardingLogger(categoryName); + _loggers.Add(logger); + return logger; + } + } + } + + internal class ForwardingLogger : ILogger + { + public ForwardingLogger(string name) + { + Name = name; + } + + public ILogger Logger { get; set; } + public string Name { get; set; } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + Logger?.Log(logLevel, eventId, state, exception, formatter); + } + + public bool IsEnabled(LogLevel logLevel) => Logger?.IsEnabled(logLevel) == true; + + public IDisposable BeginScope(TState state) => Logger?.BeginScope(state); + } + } + +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/LogFileTestBase.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/LogFileTestBase.cs new file mode 100644 index 0000000000..17885f3547 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/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 Helpers.ReadAllTextFromFile(Helpers.GetExpectedLogName(deploymentResult, _logFolderPath), Logger); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/RequiresEnvironmentVariableAttribute.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/RequiresEnvironmentVariableAttribute.cs new file mode 100644 index 0000000000..d2749db547 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/RequiresEnvironmentVariableAttribute.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] + public sealed class RequiresEnvironmentVariableAttribute : Attribute, ITestCondition + { + private readonly string _name; + + public RequiresEnvironmentVariableAttribute(string name) + { + _name = name; + } + + public bool IsMet => !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(_name)); + + public string SkipReason => $"Environment variable {_name} is required to run this test."; + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/SkipIfDebugAttribute.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/SkipIfDebugAttribute.cs new file mode 100644 index 0000000000..6cdd725392 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/Utilities/SkipIfDebugAttribute.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] + public sealed class SkipIfDebugAttribute : Attribute, ITestCondition + { + public bool IsMet => + #if DEBUG + false; + #else + true; + #endif + + public string SkipReason => "Test cannot be run in Debug mode."; + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/WindowsAuthTests.cs b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/WindowsAuthTests.cs new file mode 100644 index 0000000000..8431b15801 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.FunctionalTests/WindowsAuthTests.cs @@ -0,0 +1,52 @@ +// 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.Http; +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; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class WindowsAuthTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public WindowsAuthTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + public static TestMatrix TestVariants + => TestMatrix.ForServers(DeployerSelector.ServerType) + .WithTfms(Tfm.NetCoreApp22, Tfm.Net461) + .WithApplicationTypes(ApplicationType.Portable) + .WithAllAncmVersions() + .WithAllHostingModels(); + + [ConditionalTheory] + [RequiresIIS(IISCapability.WindowsAuthentication)] + [MemberData(nameof(TestVariants))] + public async Task WindowsAuthTest(TestVariant variant) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant); + deploymentParameters.SetAnonymousAuth(enabled: false); + deploymentParameters.SetWindowsAuth(); + + // The default in hosting sets windows auth to true. + var deploymentResult = await DeployAsync(deploymentParameters); + + var client = deploymentResult.CreateClient(new HttpClientHandler { UseDefaultCredentials = true }); + var response = await client.GetAsync("/Auth"); + var responseText = await response.Content.ReadAsStringAsync(); + + Assert.StartsWith("Windows:", responseText); + Assert.Contains(Environment.UserName, responseText); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.Tests/Common.Tests.csproj b/src/IISIntegration/src/IISIntegration/test/Common.Tests/Common.Tests.csproj new file mode 100644 index 0000000000..ede80732ee --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.Tests/Common.Tests.csproj @@ -0,0 +1,22 @@ + + + + netcoreapp2.2 + false + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/src/IISIntegration/test/Common.Tests/Utilities/DisposableList.cs b/src/IISIntegration/src/IISIntegration/test/Common.Tests/Utilities/DisposableList.cs new file mode 100644 index 0000000000..78f76e41c2 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.Tests/Utilities/DisposableList.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.Collections.Generic; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + public class DisposableList : List, IDisposable where T : IDisposable + { + public DisposableList() : base() { } + + public DisposableList(IEnumerable collection) : base(collection) { } + + public DisposableList(int capacity) : base(capacity) { } + + public void Dispose() + { + foreach (var item in this) + { + item?.Dispose(); + } + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.Tests/Utilities/TestConnections.cs b/src/IISIntegration/src/IISIntegration/test/Common.Tests/Utilities/TestConnections.cs new file mode 100644 index 0000000000..3b7a870cf3 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.Tests/Utilities/TestConnections.cs @@ -0,0 +1,252 @@ +// 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.Diagnostics; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + /// + /// Summary description for TestConnection + /// + public class TestConnection : IDisposable + { + private static readonly TimeSpan Timeout = TimeSpan.FromMinutes(1); + + private readonly bool _ownsSocket; + private readonly Socket _socket; + private readonly NetworkStream _stream; + + public TestConnection(int port) + : this(port, AddressFamily.InterNetwork) + { + } + + public TestConnection(int port, AddressFamily addressFamily) + : this(CreateConnectedLoopbackSocket(port, addressFamily), ownsSocket: true) + { + } + + public TestConnection(Socket socket) + : this(socket, ownsSocket: false) + { + } + + private TestConnection(Socket socket, bool ownsSocket) + { + _ownsSocket = ownsSocket; + _socket = socket; + _stream = new NetworkStream(_socket, ownsSocket: false); + } + + public Socket Socket => _socket; + public Stream Stream => _stream; + + public void Dispose() + { + _stream.Dispose(); + + if (_ownsSocket) + { + _socket.Dispose(); + } + } + + public async Task Send(params string[] lines) + { + var bytes = Encoding.ASCII.GetBytes(string.Join("\r\n", lines)); + + for (var index = 0; index < bytes.Length; index++) + { + await _stream.WriteAsync(bytes, index, 1).ConfigureAwait(false); + await _stream.FlushAsync().ConfigureAwait(false); + // Re-add delay to help find socket input consumption bugs more consistently + //await Task.Delay(TimeSpan.FromMilliseconds(5)); + } + } + + 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 = 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 < length) + { + 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}{length} bytes of data{Environment.NewLine}{Environment.NewLine}" + + $"Actual:{Environment.NewLine}{Encoding.ASCII.GetString(actual, 0, offset)}{Environment.NewLine}", + ex); + } + + return actual.AsMemory(0, offset); + } + + public async Task ReceiveStartsWith(string prefix, int maxLineLength = 1024) + { + 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 = _stream.ReadAsync(actual, offset, 1); + if (!Debugger.IsAttached) + { + Assert.True(task.Wait(4000), "timeout"); + } + var count = await task.ConfigureAwait(false); + if (count == 0) + { + break; + } + + Assert.True(count == 1); + offset++; + + if (actual[offset - 1] == '\n') + { + break; + } + } + + var actualLine = Encoding.ASCII.GetString(actual, 0, offset); + Assert.StartsWith(prefix, actualLine); + } + + public async Task ReceiveHeaders(params string[] lines) + { + List headers = new List(); + string line; + do + { + line = await ReadLineAsync(); + headers.Add(line); + } while (line != ""); + + foreach (var s in lines) + { + Assert.Contains(s, headers); + } + + return headers.ToArray(); + } + + public Task WaitForConnectionClose() + { + var tcs = new TaskCompletionSource(); + var eventArgs = new SocketAsyncEventArgs(); + eventArgs.SetBuffer(new byte[128], 0, 128); + eventArgs.Completed += ReceiveAsyncCompleted; + eventArgs.UserToken = tcs; + + if (!_socket.ReceiveAsync(eventArgs)) + { + ReceiveAsyncCompleted(this, eventArgs); + } + + return tcs.Task; + } + + private void ReceiveAsyncCompleted(object sender, SocketAsyncEventArgs e) + { + var tcs = (TaskCompletionSource)e.UserToken; + if (e.BytesTransferred == 0) + { + tcs.SetResult(null); + } + else + { + tcs.SetException(new IOException( + $"Expected connection close, received data instead: \"{Encoding.ASCII.GetString(e.Buffer, 0, e.BytesTransferred)}\"")); + } + } + + public static Socket CreateConnectedLoopbackSocket(int port, AddressFamily addressFamily) + { + if (addressFamily != AddressFamily.InterNetwork && addressFamily != AddressFamily.InterNetworkV6) + { + throw new ArgumentException($"TestConnection does not support address family of type {addressFamily}", nameof(addressFamily)); + } + + var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp); + var address = addressFamily == AddressFamily.InterNetworkV6 + ? IPAddress.IPv6Loopback + : IPAddress.Loopback; + socket.Connect(new IPEndPoint(address, port)); + return socket; + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Common.Tests/Utilities/TimeoutExtensions.cs b/src/IISIntegration/src/IISIntegration/test/Common.Tests/Utilities/TimeoutExtensions.cs new file mode 100644 index 0000000000..ce7175dff9 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Common.Tests/Utilities/TimeoutExtensions.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + + public static class TimeoutExtensions + { + public static TimeSpan DefaultTimeoutValue = TimeSpan.FromSeconds(300); + + public static Task DefaultTimeout(this Task task, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = -1) + { + return task.TimeoutAfter(DefaultTimeoutValue, filePath, lineNumber); + } + + public static Task DefaultTimeout(this Task task, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = -1) + { + return task.TimeoutAfter(DefaultTimeoutValue, filePath, lineNumber); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/CommonLibTests/CommonLibTests.vcxproj b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/CommonLibTests.vcxproj new file mode 100644 index 0000000000..87dbd16675 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/CommonLibTests.vcxproj @@ -0,0 +1,196 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + {1eac8125-1765-4e2d-8cbe-56dc98a1c8c1} + Win32Proj + 10.0.15063.0 + Application + v141 + Unicode + + + + + + + + + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + + + + + + + + + + + + + + + + + + {ec82302f-d2f0-4727-99d1-eabc0dd9dc3b} + + + {55494e58-e061-4c4c-a0a8-837008e72f85} + + + {09d9d1d6-2951-4e14-bc35-76a23cf9391a} + + + {1533e271-f61b-441b-8b74-59fb61df0552} + + + {cac1267b-8778-4257-aac6-caf481723b01} + + + {d57ea297-6dc2-4bc0-8c91-334863327863} + + + + + + + NotUsing + Disabled + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + $(IntDir)$(TargetName).pch + stdafx.h + true + EnableFastChecks + MultiThreadedDebug + Level3 + $(MSBuildThisFileDirectory)include;%(AdditionalIncludeDirectories);..\..\src\AspNetCoreModuleV2\RequestHandlerLib;..\..\src\AspNetCoreModuleV2\IISLib;..\..\src\AspNetCoreModuleV2\CommonLib;..\gtest\googletest\googletest\include;..\gtest\googletest\googlemock\include;...\..\src\AspNetCoreModuleV2\AspNetCore\Inc;..\..\src\AspNetCoreModuleV2\InProcessRequestHandler\ + /D "_SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING" + stdcpp17 + + + true + Console + ..\..\src\AspNetCoreModuleV2\InProcessRequestHandler\$(Configuration)\; + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;inprocessapplication.obj;inprocesshandler.obj;ahadmin.lib;Rpcrt4.lib;inprocessapplicationbase.obj;stdafx.obj;version.lib;inprocessoptions.obj;%(AdditionalDependencies) + UseLinkTimeCodeGeneration + + + + + + + + + NotUsing + Disabled + X64;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + $(IntDir)$(TargetName).pch + stdafx.h + true + EnableFastChecks + MultiThreadedDebug + Level3 + $(MSBuildThisFileDirectory)include;%(AdditionalIncludeDirectories);..\..\src\AspNetCoreModuleV2\RequestHandlerLib;..\..\src\AspNetCoreModuleV2\IISLib;..\..\src\AspNetCoreModuleV2\CommonLib;..\gtest\googletest\googletest\include;..\gtest\googletest\googlemock\include;...\..\src\AspNetCoreModuleV2\AspNetCore\Inc;..\..\src\AspNetCoreModuleV2\InProcessRequestHandler\ + /D "_SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING" + stdcpp17 + + + true + Console + ..\..\src\AspNetCoreModuleV2\InProcessRequestHandler\x64\$(Configuration)\; + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;inprocessapplication.obj;inprocesshandler.obj;ahadmin.lib;Rpcrt4.lib;inprocessapplicationbase.obj;stdafx.obj;version.lib;inprocessoptions.obj;%(AdditionalDependencies) + UseLinkTimeCodeGeneration + + + + + + + + + NotUsing + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + $(IntDir)$(TargetName).pch + stdafx.h + MultiThreaded + Level3 + ProgramDatabase + $(MSBuildThisFileDirectory)include;%(AdditionalIncludeDirectories);..\..\src\AspNetCoreModuleV2\RequestHandlerLib;..\..\src\AspNetCoreModuleV2\IISLib;..\..\src\AspNetCoreModuleV2\CommonLib;..\gtest\googletest\googletest\include;..\gtest\googletest\googlemock\include;...\..\src\AspNetCoreModuleV2\AspNetCore\Inc;..\..\src\AspNetCoreModuleV2\InProcessRequestHandler\ + /D "_SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING" + stdcpp17 + + + true + Console + /NODEFAULTLIB:libucrt.lib /DEFAULTLIB:ucrt.lib %(AdditionalOptions) + true + true + ..\..\src\AspNetCoreModuleV2\InProcessRequestHandler\$(Configuration)\; + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;inprocessapplication.obj;inprocesshandler.obj;ahadmin.lib;Rpcrt4.lib;inprocessapplicationbase.obj;stdafx.obj;version.lib;inprocessoptions.obj;%(AdditionalDependencies) + UseLinkTimeCodeGeneration + + + + + + + + + NotUsing + X64;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + $(IntDir)$(TargetName).pch + stdafx.h + MultiThreaded + Level3 + ProgramDatabase + $(MSBuildThisFileDirectory)include;%(AdditionalIncludeDirectories);..\..\src\AspNetCoreModuleV2\RequestHandlerLib;..\..\src\AspNetCoreModuleV2\IISLib;..\..\src\AspNetCoreModuleV2\CommonLib;..\gtest\googletest\googletest\include;..\gtest\googletest\googlemock\include;...\..\src\AspNetCoreModuleV2\AspNetCore\Inc;..\..\src\AspNetCoreModuleV2\InProcessRequestHandler\ + /D "_SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING" + stdcpp17 + + + true + Console + /NODEFAULTLIB:libucrt.lib /DEFAULTLIB:ucrt.lib %(AdditionalOptions) + true + true + ..\..\src\AspNetCoreModuleV2\InProcessRequestHandler\x64\$(Configuration)\; + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;inprocessapplication.obj;inprocesshandler.obj;ahadmin.lib;Rpcrt4.lib;inprocessapplicationbase.obj;stdafx.obj;version.lib;inprocessoptions.obj;%(AdditionalDependencies) + UseLinkTimeCodeGeneration + + + + + + + + \ No newline at end of file diff --git a/src/IISIntegration/src/IISIntegration/test/CommonLibTests/ConfigUtilityTests.cpp b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/ConfigUtilityTests.cpp new file mode 100644 index 0000000000..9b5bf6e9e6 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/ConfigUtilityTests.cpp @@ -0,0 +1,123 @@ +// 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. + +#include "stdafx.h" +#include "gmock/gmock.h" +using ::testing::_; +using ::testing::NiceMock; + +namespace ConfigUtilityTests +{ + using ::testing::Test; + + class ConfigUtilityTest : public Test + { + protected: + void TestHandlerVersion(std::wstring key, std::wstring value, std::wstring expected, HRESULT(*func)(IAppHostElement*, STRU&)) + { + IAppHostElement* retElement = NULL; + + STRU handlerVersion; + + // NiceMock removes warnings about "uninteresting calls", + auto element = std::make_unique>(); + auto innerElement = std::make_unique>(); + auto collection = std::make_unique>(); + auto nameElement = std::make_unique>(); + auto mockProperty = std::make_unique>(); + + ON_CALL(*element, GetElementByName(_, _)) + .WillByDefault(DoAll(testing::SetArgPointee<1>(innerElement.get()), testing::Return(S_OK))); + ON_CALL(*innerElement, get_Collection(_)) + .WillByDefault(testing::DoAll(testing::SetArgPointee<0>(collection.get()), testing::Return(S_OK))); + ON_CALL(*collection, get_Count(_)) + .WillByDefault(DoAll(testing::SetArgPointee<0>(1), testing::Return(S_OK))); + ON_CALL(*collection, get_Item(_, _)) + .WillByDefault(DoAll(testing::SetArgPointee<1>(nameElement.get()), testing::Return(S_OK))); + ON_CALL(*nameElement, GetPropertyByName(_, _)) + .WillByDefault(DoAll(testing::SetArgPointee<1>(mockProperty.get()), testing::Return(S_OK))); + EXPECT_CALL(*mockProperty, get_StringValue(_)) + .WillOnce(DoAll(testing::SetArgPointee<0>(SysAllocString(key.c_str())), testing::Return(S_OK))) + .WillOnce(DoAll(testing::SetArgPointee<0>(SysAllocString(value.c_str())), testing::Return(S_OK))); + + HRESULT hr = func(element.get(), handlerVersion); + + EXPECT_EQ(hr, S_OK); + EXPECT_STREQ(handlerVersion.QueryStr(), expected.c_str()); + } + }; + + TEST_F(ConfigUtilityTest, CheckHandlerVersionKeysAndValues) + { + auto func = ConfigUtility::FindHandlerVersion; + TestHandlerVersion(L"handlerVersion", L"value", L"value", func); + TestHandlerVersion(L"handlerversion", L"value", L"value", func); + TestHandlerVersion(L"HandlerversioN", L"value", L"value", func); + TestHandlerVersion(L"randomvalue", L"value", L"", func); + TestHandlerVersion(L"", L"value", L"", func); + TestHandlerVersion(L"", L"", L"", func); + } + + TEST_F(ConfigUtilityTest, CheckDebugLogFile) + { + auto func = ConfigUtility::FindDebugFile; + + TestHandlerVersion(L"debugFile", L"value", L"value", func); + TestHandlerVersion(L"debugFILE", L"value", L"value", func); + } + + TEST_F(ConfigUtilityTest, CheckDebugLevel) + { + auto func = ConfigUtility::FindDebugLevel; + + TestHandlerVersion(L"debugLevel", L"value", L"value", func); + TestHandlerVersion(L"debugLEVEL", L"value", L"value", func); + } + + TEST(ConfigUtilityTestSingle, MultipleElements) + { + IAppHostElement* retElement = NULL; + STRU handlerVersion; + + auto element = std::make_unique>(); + auto innerElement = std::make_unique>(); + auto collection = std::make_unique>(); + auto nameElement = std::make_unique>(); + auto mockProperty = std::make_unique>(); + + ON_CALL(*element, GetElementByName(_, _)) + .WillByDefault(DoAll(testing::SetArgPointee<1>(innerElement.get()), testing::Return(S_OK))); + ON_CALL(*innerElement, get_Collection(_)) + .WillByDefault(testing::DoAll(testing::SetArgPointee<0>(collection.get()), testing::Return(S_OK))); + ON_CALL(*collection, get_Count(_)) + .WillByDefault(DoAll(testing::SetArgPointee<0>(2), testing::Return(S_OK))); + ON_CALL(*collection, get_Item(_, _)) + .WillByDefault(DoAll(testing::SetArgPointee<1>(nameElement.get()), testing::Return(S_OK))); + ON_CALL(*nameElement, GetPropertyByName(_, _)) + .WillByDefault(DoAll(testing::SetArgPointee<1>(mockProperty.get()), testing::Return(S_OK))); + EXPECT_CALL(*mockProperty, get_StringValue(_)) + .WillOnce(DoAll(testing::SetArgPointee<0>(SysAllocString(L"key")), testing::Return(S_OK))) + .WillOnce(DoAll(testing::SetArgPointee<0>(SysAllocString(L"value")), testing::Return(S_OK))) + .WillOnce(DoAll(testing::SetArgPointee<0>(SysAllocString(L"handlerVersion")), testing::Return(S_OK))) + .WillOnce(DoAll(testing::SetArgPointee<0>(SysAllocString(L"value2")), testing::Return(S_OK))); + + HRESULT hr = ConfigUtility::FindHandlerVersion(element.get(), handlerVersion); + + EXPECT_EQ(hr, S_OK); + EXPECT_STREQ(handlerVersion.QueryStr(), L"value2"); + } + + TEST(ConfigUtilityTestSingle, IgnoresFailedGetElement) + { + STRU handlerVersion; + + auto element = std::make_unique>(); + ON_CALL(*element, GetElementByName(_, _)) + .WillByDefault(DoAll(testing::SetArgPointee<1>(nullptr), testing::Return(HRESULT_FROM_WIN32( ERROR_INVALID_INDEX )))); + + HRESULT hr = ConfigUtility::FindHandlerVersion(element.get(), handlerVersion); + + EXPECT_EQ(hr, S_OK); + EXPECT_STREQ(handlerVersion.QueryStr(), L""); + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/CommonLibTests/FileOutputManagerTests.cpp b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/FileOutputManagerTests.cpp new file mode 100644 index 0000000000..66a9ff7f0b --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/FileOutputManagerTests.cpp @@ -0,0 +1,152 @@ +// 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. + +#include "stdafx.h" +#include "gtest/internal/gtest-port.h" +#include "FileOutputManager.h" + +class FileManagerWrapper +{ +public: + FileOutputManager* manager; + FileManagerWrapper(FileOutputManager* m) + : manager(m) + { + manager->Start(); + } + + ~FileManagerWrapper() + { + delete manager; + } +}; + +namespace FileOutManagerStartupTests +{ + using ::testing::Test; + class FileOutputManagerTest : public Test + { + protected: + void + Test(std::wstring fileNamePrefix, FILE* out) + { + PCWSTR expected = L"test"; + + auto tempDirectory = TempDirectory(); + FileOutputManager* pManager = new FileOutputManager(fileNamePrefix, tempDirectory.path()); + + { + FileManagerWrapper wrapper(pManager); + + wprintf(expected, out); + } + + for (auto & p : std::filesystem::directory_iterator(tempDirectory.path())) + { + std::wstring filename(p.path().filename()); + ASSERT_EQ(filename.substr(0, fileNamePrefix.size()), fileNamePrefix); + + std::wstring content = Helpers::ReadFileContent(std::wstring(p.path())); + } + } + }; + + TEST_F(FileOutputManagerTest, WriteToFileCheckContentsWritten) + { + Test(L"", stdout); + Test(L"log", stdout); + } + + TEST_F(FileOutputManagerTest, WriteToFileCheckContentsWrittenErr) + { + Test(L"", stderr); + Test(L"log", stderr); + } +} + +namespace FileOutManagerOutputTests +{ + TEST(FileOutManagerOutputTest, StdOut) + { + PCWSTR expected = L"test"; + + auto tempDirectory = TempDirectory(); + + FileOutputManager* pManager = new FileOutputManager(L"", tempDirectory.path()); + { + FileManagerWrapper wrapper(pManager); + + fwprintf(stdout, expected); + pManager->Stop(); + + auto output = pManager->GetStdOutContent(); + ASSERT_FALSE(output.empty()); + + ASSERT_STREQ(output.c_str(), expected); + } + } + + TEST(FileOutManagerOutputTest, StdErr) + { + PCWSTR expected = L"test"; + + auto tempDirectory = TempDirectory(); + + FileOutputManager* pManager = new FileOutputManager(L"", tempDirectory.path().c_str()); + { + FileManagerWrapper wrapper(pManager); + + fwprintf(stderr, expected); + pManager->Stop(); + + auto output = pManager->GetStdOutContent(); + ASSERT_FALSE(output.empty()); + + ASSERT_STREQ(output.c_str(), expected); + } + } + + TEST(FileOutManagerOutputTest, CapAt30KB) + { + PCWSTR expected = L"hello world"; + + auto tempDirectory = TempDirectory(); + + FileOutputManager* pManager = new FileOutputManager(L"", tempDirectory.path()); + { + FileManagerWrapper wrapper(pManager); + + for (int i = 0; i < 3000; i++) + { + wprintf(expected); + } + pManager->Stop(); + auto output = pManager->GetStdOutContent(); + ASSERT_FALSE(output.empty()); + + ASSERT_EQ(output.size(), 30000); + } + } + + TEST(FileOutManagerOutputTest, StartStopRestoresCorrectly) + { + PCWSTR expected = L"test"; + + auto tempDirectory = TempDirectory(); + + for (int i = 0; i < 10; i++) + { + FileOutputManager* pManager = new FileOutputManager(L"", tempDirectory.path()); + { + FileManagerWrapper wrapper(pManager); + + wprintf(expected); + pManager->Stop(); + auto output = pManager->GetStdOutContent(); + ASSERT_FALSE(output.empty()); + + ASSERT_STREQ(output.c_str(), expected); + } + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/CommonLibTests/GlobalVersionTests.cpp b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/GlobalVersionTests.cpp new file mode 100644 index 0000000000..f38c9361d2 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/GlobalVersionTests.cpp @@ -0,0 +1,153 @@ +// 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. + +#include "stdafx.h" +#include "gtest/internal/gtest-port.h" + +namespace GlobalVersionTests +{ + using ::testing::Test; + namespace fs = std::filesystem; + + class GlobalVersionTest : public Test + { + protected: + void + RemoveFileNamePath(PCWSTR dllPath, PCWSTR expected) + { + std::wstring res = GlobalVersionUtility::RemoveFileNameFromFolderPath(dllPath); + EXPECT_STREQ(res.c_str(), expected); + } + }; + + TEST_F(GlobalVersionTest, RemovesPathCorrectly) + { + RemoveFileNamePath(L"test\\log.txt", L"test"); + RemoveFileNamePath(L"test\\log", L"test"); + RemoveFileNamePath(L"C:\\Program Files\\IIS\\aspnetcorev2.dll", L"C:\\Program Files\\IIS"); + RemoveFileNamePath(L"test\\log.txt", L"test"); + } + + TEST(GetRequestHandlerVersions, GetFolders) + { + auto tempPath = TempDirectory(); + EXPECT_TRUE(fs::create_directories(tempPath.path() / L"2.0.0")); + + auto res = GlobalVersionUtility::GetRequestHandlerVersions(tempPath.path().c_str()); + EXPECT_EQ(res.size(), 1); + EXPECT_EQ(res.at(0), fx_ver_t(2, 0, 0, std::wstring())); + } + + TEST(GetRequestHandlerVersions, GetFolderPreview) + { + auto tempPath = TempDirectory(); + EXPECT_TRUE(fs::create_directories(tempPath.path() / L"2.0.0-preview")); + + auto res = GlobalVersionUtility::GetRequestHandlerVersions(tempPath.path().c_str()); + EXPECT_EQ(res.size(), 1); + EXPECT_EQ(res.at(0), fx_ver_t(2, 0, 0, std::wstring(L"-preview"))); + } + + TEST(GetRequestHandlerVersions, GetFolderManyVersions) + { + auto tempPath = TempDirectory(); + EXPECT_TRUE(fs::create_directories(tempPath.path() / + L"2.0.0")); + EXPECT_TRUE(fs::create_directories(tempPath.path() / + L"1.9.0")); + EXPECT_TRUE(fs::create_directories(tempPath.path() / + L"2.1.0")); + + auto res = GlobalVersionUtility::GetRequestHandlerVersions(tempPath.path().c_str()); + EXPECT_EQ(res.size(), 3); + EXPECT_TRUE(std::find(res.begin(), res.end(), fx_ver_t(1, 9, 0, std::wstring())) != std::end(res)); + EXPECT_TRUE(std::find(res.begin(), res.end(), fx_ver_t(2, 0, 0, std::wstring())) != std::end(res)); + EXPECT_TRUE(std::find(res.begin(), res.end(), fx_ver_t(2, 1, 0, std::wstring())) != std::end(res)); + } + + TEST(FindHighestGlobalVersion, HighestVersionWithSingleFolder) + { + auto tempPath = TempDirectory(); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.0.0")); + + auto res = GlobalVersionUtility::FindHighestGlobalVersion(tempPath.path().c_str()); + + EXPECT_STREQ(res.c_str(), L"2.0.0"); + } + + TEST(FindHighestGlobalVersion, HighestVersionWithMultipleVersions) + { + auto tempPath = TempDirectory(); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.0.0")); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.1.0")); + + auto res = GlobalVersionUtility::FindHighestGlobalVersion(tempPath.path().c_str()); + + EXPECT_STREQ(res.c_str(), L"2.1.0"); + } + + TEST(FindHighestGlobalVersion, HighestVersionWithMultipleVersionsPreview) + { + auto tempPath = TempDirectory(); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.0.0")); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.1.0")); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.2.0-preview")); + + auto res = GlobalVersionUtility::FindHighestGlobalVersion(tempPath.path().c_str()); + + EXPECT_STREQ(res.c_str(), L"2.2.0-preview"); + } + + TEST(FindHighestGlobalVersion, HighestVersionWithMultipleVersionNoPreview) + { + auto tempPath = TempDirectory(); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.0.0")); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.1.0-preview")); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.1.0")); + + auto res = GlobalVersionUtility::FindHighestGlobalVersion(tempPath.path().c_str()); + + EXPECT_STREQ(res.c_str(), L"2.1.0"); + } + + TEST(GetGlobalRequestHandlerPath, FindHighestVersionNoHandlerName) + { + auto tempPath = TempDirectory(); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.0.0")); + auto result = GlobalVersionUtility::GetGlobalRequestHandlerPath(tempPath.path().c_str(), L"", L"aspnetcorev2_outofprocess.dll"); + + EXPECT_STREQ(result.c_str(), (tempPath.path() / L"2.0.0\\aspnetcorev2_outofprocess.dll").c_str()); + } + + TEST(GetGlobalRequestHandlerPath, FindHighestVersionPreviewWins) + { + auto tempPath = TempDirectory(); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.0.0")); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.1.0-preview")); + + auto result = GlobalVersionUtility::GetGlobalRequestHandlerPath(tempPath.path().c_str(), L"", L"aspnetcorev2_outofprocess.dll"); + + EXPECT_STREQ(result.c_str(), (tempPath.path() / L"2.1.0-preview\\aspnetcorev2_outofprocess.dll").c_str()); + } + + TEST(GetGlobalRequestHandlerPath, FindHighestVersionSpecificVersion) + { + auto tempPath = TempDirectory(); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.0.0")); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.1.0-preview")); + + auto result = GlobalVersionUtility::GetGlobalRequestHandlerPath(tempPath.path().c_str(), L"2.0.0", L"aspnetcorev2_outofprocess.dll"); + + EXPECT_STREQ(result.c_str(), (tempPath.path() / L"2.0.0\\aspnetcorev2_outofprocess.dll").c_str()); + } + + TEST(GetGlobalRequestHandlerPath, FindHighestVersionSpecificPreview) + { + auto tempPath = TempDirectory(); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.0.0")); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.1.0-preview")); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.2.0")); + + + auto result = GlobalVersionUtility::GetGlobalRequestHandlerPath(tempPath.path().c_str(), L"2.1.0-preview", L"aspnetcorev2_outofprocess.dll"); + + EXPECT_STREQ(result.c_str(), (tempPath.path() / L"2.1.0-preview\\aspnetcorev2_outofprocess.dll").c_str()); + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/CommonLibTests/Helpers.cpp b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/Helpers.cpp new file mode 100644 index 0000000000..ccca6cad5b --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/Helpers.cpp @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "stdafx.h" + +std::wstring +Helpers::ReadFileContent(std::wstring file) +{ + std::wcout << file << std::endl; + + std::fstream t(file); + std::stringstream buffer; + buffer << t.rdbuf(); + + int nChars = MultiByteToWideChar(CP_ACP, 0, buffer.str().c_str(), -1, NULL, 0); + + std::wstring retVal(nChars, '\0'); + + MultiByteToWideChar(CP_UTF8, 0, buffer.str().c_str(), -1, retVal.data(), nChars); + + return retVal; +} + +TempDirectory::TempDirectory() +{ + UUID uuid; + UuidCreate(&uuid); + RPC_CSTR szUuid = NULL; + if (UuidToStringA(&uuid, &szUuid) == RPC_S_OK) + { + m_path = std::filesystem::temp_directory_path() / reinterpret_cast(szUuid); + RpcStringFreeA(&szUuid); + return; + } + throw std::exception("Cannot create temp directory"); +} + +TempDirectory::~TempDirectory() +{ + std::filesystem::remove_all(m_path); +} diff --git a/src/IISIntegration/src/IISIntegration/test/CommonLibTests/Helpers.h b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/Helpers.h new file mode 100644 index 0000000000..67256966bb --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/Helpers.h @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once +class Helpers +{ +public: + static + std::wstring + ReadFileContent(std::wstring file); +}; + +class TempDirectory +{ +public: + + TempDirectory(); + + ~TempDirectory(); + + std::filesystem::path path() const + { + return m_path; + } + +private: + std::filesystem::path m_path; +}; diff --git a/src/IISIntegration/src/IISIntegration/test/CommonLibTests/NativeTests.targets b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/NativeTests.targets new file mode 100644 index 0000000000..f3d2caf930 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/NativeTests.targets @@ -0,0 +1,8 @@ + + + $(VCIDEInstallDir)..\CommonExtensions\Microsoft\TestWindow\vstest.console.exe + + + + + \ No newline at end of file diff --git a/src/IISIntegration/src/IISIntegration/test/CommonLibTests/PipeOutputManagerTests.cpp b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/PipeOutputManagerTests.cpp new file mode 100644 index 0000000000..385d6df9e0 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/PipeOutputManagerTests.cpp @@ -0,0 +1,162 @@ +// 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. + +#include "stdafx.h" +#include "gtest/internal/gtest-port.h" +#include "PipeOutputManager.h" + +class FileManagerWrapper +{ +public: + PipeOutputManager * manager; + FileManagerWrapper(PipeOutputManager* m) + : manager(m) + { + manager->Start(); + } + + ~FileManagerWrapper() + { + delete manager; + } +}; + +namespace PipeOutputManagerTests +{ + TEST(PipeManagerOutputTest, StdOut) + { + PCWSTR expected = L"test"; + + PipeOutputManager* pManager = new PipeOutputManager(true); + + pManager->Start(); + fwprintf(stdout, expected); + pManager->Stop(); + + auto output = pManager->GetStdOutContent(); + ASSERT_STREQ(output.c_str(), expected); + delete pManager; + } + + TEST(PipeManagerOutputTest, StdOutMultiToWide) + { + PipeOutputManager* pManager = new PipeOutputManager(true); + + pManager->Start(); + fprintf(stdout, "test"); + pManager->Stop(); + + auto output = pManager->GetStdOutContent(); + ASSERT_STREQ(output.c_str(), L"test"); + delete pManager; + } + + TEST(PipeManagerOutputTest, StdErr) + { + PCWSTR expected = L"test"; + + PipeOutputManager* pManager = new PipeOutputManager(); + + pManager->Start(); + fwprintf(stderr, expected); + pManager->Stop(); + + auto output = pManager->GetStdOutContent(); + ASSERT_STREQ(output.c_str(), expected); + delete pManager; + } + + TEST(PipeManagerOutputTest, CheckMaxPipeSize) + { + std::wstring test; + for (int i = 0; i < 3000; i++) + { + test.append(L"hello world"); + } + + PipeOutputManager* pManager = new PipeOutputManager(); + + pManager->Start(); + wprintf(test.c_str()); + pManager->Stop(); + + auto output = pManager->GetStdOutContent(); + ASSERT_EQ(output.size(), (DWORD)30000); + delete pManager; + } + + TEST(PipeManagerOutputTest, SetInvalidHandlesForErrAndOut) + { + auto m_fdPreviousStdOut = _dup(_fileno(stdout)); + auto m_fdPreviousStdErr = _dup(_fileno(stderr)); + + SetStdHandle(STD_ERROR_HANDLE, INVALID_HANDLE_VALUE); + SetStdHandle(STD_OUTPUT_HANDLE, INVALID_HANDLE_VALUE); + + PCWSTR expected = L"test"; + + PipeOutputManager* pManager = new PipeOutputManager(); + pManager->Start(); + + _dup2(m_fdPreviousStdOut, _fileno(stdout)); + _dup2(m_fdPreviousStdErr, _fileno(stderr)); + + // Test will fail if we didn't redirect stdout back to a file descriptor. + // This is because gtest relies on console output to know if a test succeeded or failed. + // If the output still points to a file/pipe, the test (and all other tests after it) will fail. + delete pManager; + } + + TEST(PipeManagerOutputTest, CreateDeleteMultipleTimesStdOutWorks) + { + for (int i = 0; i < 10; i++) + { + auto stdoutBefore = _fileno(stdout); + auto stderrBefore = _fileno(stderr); + PCWSTR expected = L"test"; + + PipeOutputManager* pManager = new PipeOutputManager(); + + pManager->Start(); + fwprintf(stdout, expected); + + pManager->Stop(); + + auto output = pManager->GetStdOutContent(); + ASSERT_STREQ(output.c_str(), expected); + ASSERT_EQ(stdoutBefore, _fileno(stdout)); + ASSERT_EQ(stderrBefore, _fileno(stderr)); + delete pManager; + } + // When this returns, we get an AV from gtest. + } + + TEST(PipeManagerOutputTest, CreateDeleteKeepOriginalStdErr) + { + for (int i = 0; i < 10; i++) + { + auto stdoutBefore = _fileno(stdout); + auto stderrBefore = _fileno(stderr); + auto stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); + auto stderrHandle = GetStdHandle(STD_ERROR_HANDLE); + PCWSTR expected = L"test"; + + PipeOutputManager* pManager = new PipeOutputManager(); + + pManager->Start(); + fwprintf(stderr, expected); + pManager->Stop(); + + auto output = pManager->GetStdOutContent(); + ASSERT_STREQ(output.c_str(), expected); + ASSERT_EQ(stdoutBefore, _fileno(stdout)); + + ASSERT_EQ(stderrBefore, _fileno(stderr)); + + delete pManager; + } + + wprintf(L"Hello!"); + } +} + diff --git a/src/IISIntegration/src/IISIntegration/test/CommonLibTests/exception_handler_tests.cpp b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/exception_handler_tests.cpp new file mode 100644 index 0000000000..d0a9e97fdb --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/exception_handler_tests.cpp @@ -0,0 +1,49 @@ +// 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. + +#include "stdafx.h" + +TEST(CaughtExceptionHResult, ReturnsOutOfMemoryForBadAlloc) +{ + HRESULT hr; + try + { + throw std::bad_alloc(); + } + catch(...) + { + hr = CaughtExceptionHResult(); + } + + EXPECT_EQ(E_OUTOFMEMORY, hr); +} + +TEST(CaughtExceptionHResult, ReturnsValueForSystemError) +{ + HRESULT hr; + try + { + throw std::system_error(E_INVALIDARG, std::system_category()); + } + catch(...) + { + hr = CaughtExceptionHResult(); + } + + EXPECT_EQ(E_INVALIDARG, hr); +} + +TEST(CaughtExceptionHResult, ReturnsUhandledExceptionForOtherExceptions) +{ + HRESULT hr; + try + { + throw E_INVALIDARG; + } + catch(...) + { + hr = CaughtExceptionHResult(); + } + + EXPECT_EQ(HRESULT_FROM_WIN32(ERROR_UNHANDLED_EXCEPTION), hr); +} diff --git a/src/IISIntegration/src/IISIntegration/test/CommonLibTests/fakeclasses.h b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/fakeclasses.h new file mode 100644 index 0000000000..a6f4a3e111 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/fakeclasses.h @@ -0,0 +1,190 @@ +// 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. + +#pragma once + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "InProcessOptions.h" + +class MockProperty : public IAppHostProperty +{ +public: + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, QueryInterface, HRESULT(REFIID riid, void ** ppvObject)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, AddRef, ULONG()); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, Release, ULONG()); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_Name, HRESULT(BSTR* pbstrValue)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_Value, HRESULT(VARIANT * pVariant)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, put_Value, HRESULT(VARIANT value)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, Clear, HRESULT()); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_StringValue, HRESULT(BSTR* pbstrValue)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_Exception, HRESULT(IAppHostPropertyException ** ppException)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, GetMetadata, HRESULT(BSTR bstrMetadataType, VARIANT * pValue)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, SetMetadata, HRESULT(BSTR bstrMetadataType, VARIANT value)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_Schema, HRESULT(IAppHostPropertySchema ** ppSchema)); +}; + +class MockCollection : public IAppHostElementCollection +{ +public: + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, QueryInterface, HRESULT(REFIID riid, void ** ppvObject)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, AddRef, ULONG()); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, Release, ULONG()); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, Clear, HRESULT()); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_Schema, HRESULT(IAppHostCollectionSchema** pSchema)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_Count, HRESULT(DWORD * dwordElem)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, get_Item, HRESULT(VARIANT cIndex, IAppHostElement ** ppElement)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, AddElement, HRESULT(IAppHostElement * pElement, INT cPosition)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, DeleteElement, HRESULT(VARIANT cIndex)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, CreateNewElement, HRESULT(BSTR bstrElementName, IAppHostElement** ppElement)); +}; + +class MockElement : public IAppHostElement +{ +public: + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, QueryInterface, HRESULT(REFIID riid, void ** ppvObject)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, AddRef, ULONG()); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, Release, ULONG()); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_Name, HRESULT(BSTR * pbstrName)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_Collection, HRESULT(IAppHostElementCollection ** ppCollection)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_Properties, HRESULT(IAppHostPropertyCollection ** ppProperties)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_ChildElements, HRESULT(IAppHostChildElementCollection ** ppElements)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, GetMetadata, HRESULT(BSTR bstrMetadataType, VARIANT * pValue)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, SetMetadata, HRESULT(BSTR bstrMetadataType, VARIANT value)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_Schema, HRESULT(IAppHostElementSchema** pSchema)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, GetElementByName, HRESULT(BSTR bstrSubName, IAppHostElement ** ppElement)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, GetPropertyByName, HRESULT(BSTR bstrSubName, IAppHostProperty ** ppProperty)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, Clear, HRESULT()); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_Methods, HRESULT(IAppHostMethodCollection ** ppMethods)); +}; + +class MockHttpServer : public IHttpServer +{ + // Inherited via IHttpServer + virtual BOOL IsCommandLineLaunch(VOID) const override + { + return 0; + } + virtual PCWSTR GetAppPoolName(VOID) const override + { + return PCWSTR(); + } + virtual HRESULT AssociateWithThreadPool(HANDLE hHandle, LPOVERLAPPED_COMPLETION_ROUTINE completionRoutine) override + { + return E_NOTIMPL; + } + virtual VOID IncrementThreadCount(VOID) override + { + return VOID(); + } + virtual VOID DecrementThreadCount(VOID) override + { + return VOID(); + } + virtual VOID ReportUnhealthy(PCWSTR pszReasonString, HRESULT hrReason) override + { + return VOID(); + } + virtual VOID RecycleProcess(PCWSTR pszReason) override + { + return VOID(); + } + virtual IAppHostAdminManager * GetAdminManager(VOID) const override + { + return nullptr; + } + virtual HRESULT GetFileInfo(PCWSTR pszPhysicalPath, HANDLE hUserToken, PSID pSid, PCWSTR pszChangeNotificationPath, HANDLE hChangeNotificationToken, BOOL fCache, IHttpFileInfo ** ppFileInfo, IHttpTraceContext * pHttpTraceContext = NULL) override + { + return E_NOTIMPL; + } + virtual HRESULT FlushKernelCache(PCWSTR pszUrl) override + { + return E_NOTIMPL; + } + virtual HRESULT DoCacheOperation(CACHE_OPERATION cacheOperation, IHttpCacheKey * pCacheKey, IHttpCacheSpecificData ** ppCacheSpecificData, IHttpTraceContext * pHttpTraceContext = NULL) override + { + return E_NOTIMPL; + } + virtual GLOBAL_NOTIFICATION_STATUS NotifyCustomNotification(ICustomNotificationProvider * pCustomOutput) override + { + return GLOBAL_NOTIFICATION_STATUS(); + } + virtual IHttpPerfCounterInfo * GetPerfCounterInfo(VOID) override + { + return nullptr; + } + virtual VOID RecycleApplication(PCWSTR pszAppConfigPath) override + { + return VOID(); + } + virtual VOID NotifyConfigurationChange(PCWSTR pszPath) override + { + return VOID(); + } + virtual VOID NotifyFileChange(PCWSTR pszFileName) override + { + return VOID(); + } + virtual IDispensedHttpModuleContextContainer * DispenseContainer(VOID) override + { + return nullptr; + } + virtual HRESULT AddFragmentToCache(HTTP_DATA_CHUNK * pDataChunk, PCWSTR pszFragmentName) override + { + return E_NOTIMPL; + } + virtual HRESULT ReadFragmentFromCache(PCWSTR pszFragmentName, BYTE * pvBuffer, DWORD cbSize, DWORD * pcbCopied) override + { + return E_NOTIMPL; + } + virtual HRESULT RemoveFragmentFromCache(PCWSTR pszFragmentName) override + { + return E_NOTIMPL; + } + virtual HRESULT GetWorkerProcessSettings(IWpfSettings ** ppWorkerProcessSettings) override + { + return E_NOTIMPL; + } + virtual HRESULT GetProtocolManagerCustomInterface(PCWSTR pProtocolManagerDll, PCWSTR pProtocolManagerDllInitFunction, DWORD dwCustomInterfaceId, PVOID * ppCustomInterface) override + { + return E_NOTIMPL; + } + virtual BOOL SatisfiesPrecondition(PCWSTR pszPrecondition, BOOL * pfUnknownPrecondition = NULL) const override + { + return 0; + } + virtual IHttpTraceContext * GetTraceContext(VOID) const override + { + return nullptr; + } + virtual HRESULT RegisterFileChangeMonitor(PCWSTR pszPath, HANDLE hToken, IHttpFileMonitor ** ppFileMonitor) override + { + return E_NOTIMPL; + } + virtual HRESULT GetExtendedInterface(HTTP_SERVER_INTERFACE_VERSION version, PVOID * ppInterface) override + { + return E_NOTIMPL; + } +}; + + +class MockHttpApplication: public IHttpApplication +{ +public: + MOCK_CONST_METHOD0(GetApplicationPhysicalPath, PCWSTR ()); + MOCK_CONST_METHOD0(GetApplicationId, PCWSTR ()); + MOCK_CONST_METHOD0(GetAppConfigPath, PCWSTR ()); + MOCK_METHOD0(GetModuleContextContainer, IHttpModuleContextContainer* ()); +}; + +class MockInProcessOptions : public InProcessOptions +{ +public: + static + MockInProcessOptions* + CreateConfig() + { + return new MockInProcessOptions; + } +}; + diff --git a/src/IISIntegration/src/IISIntegration/test/CommonLibTests/hostfxr_utility_tests.cpp b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/hostfxr_utility_tests.cpp new file mode 100644 index 0000000000..01c9541429 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/hostfxr_utility_tests.cpp @@ -0,0 +1,122 @@ +// 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. + +#include "stdafx.h" +#include +#include +#include +#include "hostfxr_utility.h" +#include "Environment.h" + +TEST(ParseHostFxrArguments, BasicHostFxrArguments) +{ + std::vector bstrArray; + + HOSTFXR_UTILITY::AppendArguments( + L"exec \"test.dll\"", // args + L"invalid", // physical path to application + bstrArray); // args array. + + EXPECT_EQ(2, bstrArray.size()); + ASSERT_STREQ(L"exec", bstrArray[0].c_str()); + ASSERT_STREQ(L"test.dll", bstrArray[1].c_str()); +} + +TEST(ParseHostFxrArguments, NoExecProvided) +{ + std::vector bstrArray; + + HOSTFXR_UTILITY::AppendArguments( + L"test.dll", // args + L"ignored", // physical path to application + bstrArray); // args array. + + EXPECT_EQ(1, bstrArray.size()); + ASSERT_STREQ(L"test.dll", bstrArray[0].c_str()); +} + +TEST(ParseHostFxrArguments, ConvertDllToAbsolutePath) +{ + std::vector bstrArray; + // we need to use existing dll so let's use ntdll that we know exists everywhere + auto system32 = Environment::ExpandEnvironmentVariables(L"%WINDIR%\\System32"); + HOSTFXR_UTILITY::AppendArguments( + L"exec \"ntdll.dll\"", // args + system32, // physical path to application + bstrArray, // args array. + true); // expandDllPaths + + EXPECT_EQ(2, bstrArray.size()); + ASSERT_STREQ(L"exec", bstrArray[0].c_str()); + ASSERT_STREQ((system32 + L"\\ntdll.dll").c_str(), bstrArray[1].c_str()); +} + +TEST(ParseHostFxrArguments, ProvideNoArgs_InvalidArgs) +{ + std::vector bstrArray; + std::filesystem::path struHostFxrDllLocation; + std::filesystem::path struExeLocation; + + EXPECT_THROW(HOSTFXR_UTILITY::GetHostFxrParameters( + L"dotnet", // processPath + L"some\\path", // application physical path, ignored. + L"", //arguments + struHostFxrDllLocation, + struExeLocation, + bstrArray), // args array. + InvalidOperationException); +} + +TEST(GetAbsolutePathToDotnetFromProgramFiles, BackupWorks) +{ + STRU struAbsolutePathToDotnet; + BOOL fDotnetInProgramFiles; + BOOL is64Bit; + BOOL fIsWow64 = FALSE; + SYSTEM_INFO systemInfo; + IsWow64Process(GetCurrentProcess(), &fIsWow64); + if (fIsWow64) + { + is64Bit = FALSE; + } + else + { + GetNativeSystemInfo(&systemInfo); + is64Bit = systemInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64; + } + + if (is64Bit) + { + fDotnetInProgramFiles = std::filesystem::is_regular_file(L"C:/Program Files/dotnet/dotnet.exe"); + } + else + { + fDotnetInProgramFiles = std::filesystem::is_regular_file(L"C:/Program Files (x86)/dotnet/dotnet.exe"); + } + + auto dotnetPath = HOSTFXR_UTILITY::GetAbsolutePathToDotnetFromProgramFiles(); + if (fDotnetInProgramFiles) + { + EXPECT_TRUE(dotnetPath.has_value()); + } + else + { + EXPECT_FALSE(dotnetPath.has_value()); + } +} + +TEST(GetHostFxrArguments, InvalidParams) +{ + std::vector bstrArray; + std::filesystem::path struHostFxrDllLocation; + std::filesystem::path struExeLocation; + + EXPECT_THROW(HOSTFXR_UTILITY::GetHostFxrParameters( + L"bogus", // processPath + L"", // application physical path, ignored. + L"ignored", //arguments + struHostFxrDllLocation, + struExeLocation, + bstrArray), // args array. + InvalidOperationException); +} diff --git a/src/IISIntegration/src/IISIntegration/test/CommonLibTests/inprocess_application_tests.cpp b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/inprocess_application_tests.cpp new file mode 100644 index 0000000000..d2ec985723 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/inprocess_application_tests.cpp @@ -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. + +#include "stdafx.h" + +#include +#include "inprocessapplication.h" +#include "fakeclasses.h" + +using ::testing::_; +using ::testing::NiceMock; + +// Externals defined in inprocess +BOOL g_fProcessDetach; +HANDLE g_hEventLog; + +namespace InprocessTests +{ + TEST(InProcessTest, NoNullRefForExePath) + { + MockHttpServer server; + NiceMock application; + + ON_CALL(application, GetApplicationPhysicalPath()) + .WillByDefault(testing::Return(L"Some path")); + + ON_CALL(application, GetAppConfigPath()) + .WillByDefault(testing::Return(L"")); + + ON_CALL(application, GetApplicationId()) + .WillByDefault(testing::Return(L"")); + + auto requestHandlerConfig = std::unique_ptr(MockInProcessOptions::CreateConfig()); + + std::wstring exePath(L"hello"); + + std::array parameters{ + {"InProcessExeLocation", exePath.data()} + }; + + IN_PROCESS_APPLICATION *app = new IN_PROCESS_APPLICATION(server, application, std::move(requestHandlerConfig), parameters.data(), 1); + + ASSERT_STREQ(app->QueryExeLocation().c_str(), L"hello"); + } + + TEST(InProcessTest, GeneratesVirtualPath) + { + MockHttpServer server; + NiceMock application; + + ON_CALL(application, GetApplicationPhysicalPath()) + .WillByDefault(testing::Return(L"Some path")); + + ON_CALL(application, GetAppConfigPath()) + .WillByDefault(testing::Return(L"SECTION1/SECTION2/SECTION3/SECTION4/SECTION5")); + + ON_CALL(application, GetApplicationId()) + .WillByDefault(testing::Return(L"")); + + auto requestHandlerConfig = std::unique_ptr(MockInProcessOptions::CreateConfig()); + IN_PROCESS_APPLICATION *app = new IN_PROCESS_APPLICATION(server, application, std::move(requestHandlerConfig), nullptr, 0); + + ASSERT_STREQ(app->QueryApplicationVirtualPath().c_str(), L"/SECTION5"); + } + + TEST(InProcessTest, GeneratesVirtualPathForDefaultApp) + { + MockHttpServer server; + NiceMock application; + + ON_CALL(application, GetApplicationPhysicalPath()) + .WillByDefault(testing::Return(L"Some path")); + + ON_CALL(application, GetAppConfigPath()) + .WillByDefault(testing::Return(L"SECTION1/SECTION2/SECTION3/SECTION4")); + + ON_CALL(application, GetApplicationId()) + .WillByDefault(testing::Return(L"")); + + auto requestHandlerConfig = std::unique_ptr(MockInProcessOptions::CreateConfig()); + IN_PROCESS_APPLICATION *app = new IN_PROCESS_APPLICATION(server, application, std::move(requestHandlerConfig), nullptr, 0); + + ASSERT_STREQ(app->QueryApplicationVirtualPath().c_str(), L"/"); + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/CommonLibTests/main.cpp b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/main.cpp new file mode 100644 index 0000000000..1ad0a10ccd --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/main.cpp @@ -0,0 +1,12 @@ +// 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. + +#include "stdafx.h" + +DECLARE_DEBUG_PRINT_OBJECT2("tests", ASPNETCORE_DEBUG_FLAG_INFO | ASPNETCORE_DEBUG_FLAG_CONSOLE); + +int wmain(int argc, wchar_t* argv[]) +{ + ::testing::InitGoogleTest(&argc, argv); + RUN_ALL_TESTS(); +} diff --git a/src/IISIntegration/src/IISIntegration/test/CommonLibTests/stdafx.h b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/stdafx.h new file mode 100644 index 0000000000..4b9ac7cd27 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/stdafx.h @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#define WIN32_LEAN_AND_MEAN + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include "stringa.h" +#include "stringu.h" +#include "dbgutil.h" +#include "ahutil.h" +#include "multisz.h" +#include "multisza.h" +#include "base64.h" +#include +#include +#include +#include +#include + +#include "stringu.h" +#include "stringa.h" +#include "multisz.h" +#include "dbgutil.h" +#include "hashfn.h" + +#include "requesthandler_config.h" +#include "hostfxr_utility.h" +#include "config_utility.h" +#include "environmentvariablehash.h" +#include "iapplication.h" +#include "debugutil.h" +#include "requesthandler.h" +#include "resources.h" +#include "aspnetcore_msg.h" +#include "Helpers.h" +#include "GlobalVersionUtility.h" + +#undef assert // Macro redefinition in IISLib. +#include "gtest/gtest.h" +#include "fakeclasses.h" + diff --git a/src/IISIntegration/src/IISIntegration/test/CommonLibTests/utility_tests.cpp b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/utility_tests.cpp new file mode 100644 index 0000000000..ee69d79054 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/CommonLibTests/utility_tests.cpp @@ -0,0 +1,75 @@ +// 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. + +#include "stdafx.h" +#include "Environment.h" +#include "StringHelpers.h" + +TEST(PassUnexpandedEnvString, ExpandsResult) +{ + HRESULT hr = S_OK; + PCWSTR unexpandedString = L"ANCM_TEST_ENV_VAR"; + PCWSTR unexpandedStringValue = L"foobar"; + STRU struExpandedString; + SetEnvironmentVariable(L"ANCM_TEST_ENV_VAR", unexpandedStringValue); + + hr = struExpandedString.CopyAndExpandEnvironmentStrings(L"%ANCM_TEST_ENV_VAR%"); + EXPECT_EQ(hr, S_OK); + EXPECT_STREQ(L"foobar", struExpandedString.QueryStr()); +} + +TEST(PassUnexpandedEnvString, LongStringExpandsResults) +{ + HRESULT hr = S_OK; + PCWSTR unexpandedString = L"ANCM_TEST_ENV_VAR_LONG"; + STRU struStringValue; + STACK_STRU(struExpandedString, MAX_PATH); + + struStringValue.Append(L"TestValueThatIsLongerThan256CharactersLongToTriggerResize"); + struStringValue.Append(L"TestValueThatIsLongerThan256CharactersLongToTriggerResize"); + struStringValue.Append(L"TestValueThatIsLongerThan256CharactersLongToTriggerResize"); + struStringValue.Append(L"TestValueThatIsLongerThan256CharactersLongToTriggerResize"); + struStringValue.Append(L"TestValueThatIsLongerThan256CharactersLongToTriggerResize"); + struStringValue.Append(L"TestValueThatIsLongerThan256CharactersLongToTriggerResize"); + + SetEnvironmentVariable(unexpandedString, struStringValue.QueryStr()); + + hr = struExpandedString.CopyAndExpandEnvironmentStrings(L"%ANCM_TEST_ENV_VAR_LONG%"); + EXPECT_EQ(hr, S_OK); + EXPECT_EQ(struStringValue.QueryCCH(), struExpandedString.QueryCCH()); + // The values are exactly the same, however EXPECT_EQ is returning false. + //EXPECT_EQ(struStringValue.QueryStr(), struExpandedString.QueryStr()); + EXPECT_STREQ(struStringValue.QueryStr(), struExpandedString.QueryStr()); +} + + +TEST(GetEnvironmentVariableValue, ReturnsCorrectLenght) +{ + SetEnvironmentVariable(L"RANDOM_ENV_VAR_1", L"test"); + + auto result = Environment::GetEnvironmentVariableValue(L"RANDOM_ENV_VAR_1"); + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().length(), 4); + EXPECT_STREQ(result.value().c_str(), L"test"); +} + + +TEST(GetEnvironmentVariableValue, ReturnsNulloptWhenNotFound) +{ + auto result = Environment::GetEnvironmentVariableValue(L"RANDOM_ENV_VAR_2"); + EXPECT_FALSE(result.has_value()); +} + +TEST(CheckStringHelpers, FormatWithoutContentDoesNotIncreaseSizeString) +{ + std::string testString = "test"; + auto result = format(testString); + EXPECT_EQ(testString.size(), result.size()); +} + +TEST(CheckStringHelpers, FormatWithoutContentDoesNotIncreaseSizeWstring) +{ + std::wstring testString = L"test"; + auto result = format(testString); + EXPECT_EQ(testString.size(), result.size()); +} diff --git a/src/IISIntegration/src/IISIntegration/test/Directory.Build.props b/src/IISIntegration/src/IISIntegration/test/Directory.Build.props new file mode 100644 index 0000000000..edfd666254 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Directory.Build.props @@ -0,0 +1,16 @@ + + + + + + netcoreapp2.2 + $(DeveloperBuildTestTfms) + $(StandardTestTfms) + $(StandardTestTfms);net461 + + + + + + diff --git a/src/IISIntegration/src/IISIntegration/test/IIS.BackwardsCompatibility.FunctionalTests/BackwardsCompatibilityTests.cs b/src/IISIntegration/src/IISIntegration/test/IIS.BackwardsCompatibility.FunctionalTests/BackwardsCompatibilityTests.cs new file mode 100644 index 0000000000..2e4a7b1c40 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IIS.BackwardsCompatibility.FunctionalTests/BackwardsCompatibilityTests.cs @@ -0,0 +1,39 @@ +// 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.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; +using Xunit.Sdk; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class BackwardsCompatibilityTests : FixtureLoggedTest + { + private readonly IISTestSiteFixture _fixture; + + public BackwardsCompatibilityTests(IISTestSiteFixture fixture) : base(fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task CheckBackwardsCompatibilityIsUsed() + { + var response = await _fixture.Client.GetAsync("/HelloWorld"); + var handles = _fixture.DeploymentResult.HostProcess.Modules; + + foreach (ProcessModule handle in handles) + { + if (handle.ModuleName == "aspnetcorev2.dll") + { + Assert.Equal("12.2.18287.0", handle.FileVersionInfo.FileVersion); + return; + } + } + throw new XunitException($"Could not find aspnetcorev2.dll loaded in process {_fixture.DeploymentResult.HostProcess.ProcessName}"); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/IIS.BackwardsCompatibility.FunctionalTests/DeployerSelector.cs b/src/IISIntegration/src/IISIntegration/test/IIS.BackwardsCompatibility.FunctionalTests/DeployerSelector.cs new file mode 100644 index 0000000000..5c6f3739a4 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IIS.BackwardsCompatibility.FunctionalTests/DeployerSelector.cs @@ -0,0 +1,14 @@ +// 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.Server.IntegrationTesting; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + public static class DeployerSelector + { + public static ServerType ServerType => ServerType.IIS; + public static bool IsBackwardsCompatiblityTest => true; + public static bool IsForwardsCompatibilityTest => false; + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/IIS.BackwardsCompatibility.FunctionalTests/IIS.BackwardsCompatibility.FunctionalTests.csproj b/src/IISIntegration/src/IISIntegration/test/IIS.BackwardsCompatibility.FunctionalTests/IIS.BackwardsCompatibility.FunctionalTests.csproj new file mode 100644 index 0000000000..c819a03ab1 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IIS.BackwardsCompatibility.FunctionalTests/IIS.BackwardsCompatibility.FunctionalTests.csproj @@ -0,0 +1,46 @@ + + + + netcoreapp2.2 + IISBackwardsCompatibility.FunctionalTests + True + + + + + + + + + + + False + + + False + + + False + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/src/IISIntegration/test/IIS.ForwardsCompatibility.FunctionalTests/DeployerSelector.cs b/src/IISIntegration/src/IISIntegration/test/IIS.ForwardsCompatibility.FunctionalTests/DeployerSelector.cs new file mode 100644 index 0000000000..bd7aabbe0f --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IIS.ForwardsCompatibility.FunctionalTests/DeployerSelector.cs @@ -0,0 +1,14 @@ +// 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.Server.IntegrationTesting; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + public static class DeployerSelector + { + public static ServerType ServerType => ServerType.IIS; + public static bool IsBackwardsCompatiblityTest => false; + public static bool IsForwardsCompatibilityTest => true; + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/IIS.ForwardsCompatibility.FunctionalTests/ForwardsCompatibilityTests.cs b/src/IISIntegration/src/IISIntegration/test/IIS.ForwardsCompatibility.FunctionalTests/ForwardsCompatibilityTests.cs new file mode 100644 index 0000000000..5f4ebb5608 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IIS.ForwardsCompatibility.FunctionalTests/ForwardsCompatibilityTests.cs @@ -0,0 +1,38 @@ +// 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.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; +using Xunit.Sdk; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class ForwardsCompatibilityTests : FixtureLoggedTest + { + private readonly IISTestSiteFixture _fixture; + + public ForwardsCompatibilityTests(IISTestSiteFixture fixture) : base(fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task CheckForwardsCompatibilityIsUsed() + { + var response = await _fixture.Client.GetAsync("/HelloWorld"); + var handles = _fixture.DeploymentResult.HostProcess.Modules; + foreach (ProcessModule handle in handles) + { + if (handle.ModuleName == "aspnetcorev2_inprocess.dll") + { + Assert.Equal("12.2.18287.0", handle.FileVersionInfo.FileVersion); + return; + } + } + throw new XunitException($"Could not find aspnetcorev2_inprocess.dll loaded in process {_fixture.DeploymentResult.HostProcess.ProcessName}"); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/IIS.ForwardsCompatibility.FunctionalTests/IIS.ForwardsCompatibility.FunctionalTests.csproj b/src/IISIntegration/src/IISIntegration/test/IIS.ForwardsCompatibility.FunctionalTests/IIS.ForwardsCompatibility.FunctionalTests.csproj new file mode 100644 index 0000000000..929f6ec6b0 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IIS.ForwardsCompatibility.FunctionalTests/IIS.ForwardsCompatibility.FunctionalTests.csproj @@ -0,0 +1,45 @@ + + + + netcoreapp2.2 + IISForwardsCompatibility.FunctionalTests + True + + + + + + + + + + False + + + False + + + False + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/src/IISIntegration/test/IIS.FunctionalTests/DeployerSelector.cs b/src/IISIntegration/src/IISIntegration/test/IIS.FunctionalTests/DeployerSelector.cs new file mode 100644 index 0000000000..f2e1be321e --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IIS.FunctionalTests/DeployerSelector.cs @@ -0,0 +1,14 @@ +// 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.Server.IntegrationTesting; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + public static class DeployerSelector + { + public static ServerType ServerType => ServerType.IIS; + public static bool IsBackwardsCompatiblityTest => false; + public static bool IsForwardsCompatibilityTest => false; + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj b/src/IISIntegration/src/IISIntegration/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj new file mode 100644 index 0000000000..62dec62e60 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj @@ -0,0 +1,46 @@ + + + + netcoreapp2.2 + IIS.FunctionalTests + True + + + + + + + + + + + False + + + False + + + False + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/src/IISIntegration/test/IIS.Shared.FunctionalTests/Inprocess/StdOutRedirectionTests.cs b/src/IISIntegration/src/IISIntegration/test/IIS.Shared.FunctionalTests/Inprocess/StdOutRedirectionTests.cs new file mode 100644 index 0000000000..35f10b13ab --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IIS.Shared.FunctionalTests/Inprocess/StdOutRedirectionTests.cs @@ -0,0 +1,150 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.AspNetCore.Testing.xunit; +using Newtonsoft.Json; +using Xunit; + +namespace IIS.FunctionalTests.Inprocess +{ + [Collection(PublishedSitesCollection.Name)] + public class StdOutRedirectionTests : LogFileTestBase + { + private readonly PublishedSitesFixture _fixture; + + public StdOutRedirectionTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + [SkipIfDebug] + public async Task FrameworkNotFoundExceptionLogged_Pipe() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + + var deploymentResult = await DeployAsync(deploymentParameters); + + Helpers.ModifyFrameworkVersionInRuntimeConfig(deploymentResult); + + var response = await deploymentResult.HttpClient.GetAsync("/HelloWorld"); + Assert.False(response.IsSuccessStatusCode); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvent(deploymentResult, + "The specified framework 'Microsoft.NETCore.App', version '2.9.9' was not found."); + } + + [ConditionalFact] + [SkipIfDebug] + public async Task FrameworkNotFoundExceptionLogged_File() + { + var deploymentParameters = + _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + + deploymentParameters.EnableLogging(_logFolderPath); + + var deploymentResult = await DeployAsync(deploymentParameters); + + Helpers.ModifyFrameworkVersionInRuntimeConfig(deploymentResult); + + var response = await deploymentResult.HttpClient.GetAsync("/HelloWorld"); + Assert.False(response.IsSuccessStatusCode); + + StopServer(); + + var contents = Helpers.ReadAllTextFromFile(Helpers.GetExpectedLogName(deploymentResult, _logFolderPath), Logger); + var expectedString = "The specified framework 'Microsoft.NETCore.App', version '2.9.9' was not found."; + EventLogHelpers.VerifyEventLogEvent(deploymentResult, expectedString); + Assert.Contains(expectedString, contents); + } + + [ConditionalFact] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + [SkipIfDebug] + public async Task EnableCoreHostTraceLogging_TwoLogFilesCreated() + { + var deploymentParameters = + _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} CheckLargeStdOutWrites"); + + deploymentParameters.EnvironmentVariables["COREHOST_TRACE"] = "1"; + + deploymentParameters.EnableLogging(_logFolderPath); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("/HelloWorld"); + Assert.False(response.IsSuccessStatusCode); + + StopServer(); + + var fileInDirectory = Directory.GetFiles(_logFolderPath).Single(); + var contents = Helpers.ReadAllTextFromFile(fileInDirectory, Logger); + EventLogHelpers.VerifyEventLogEvent(deploymentResult, "Invoked hostfxr"); + Assert.Contains("Invoked hostfxr", contents); + } + + [ConditionalTheory] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + [SkipIfDebug] + [InlineData("CheckLargeStdErrWrites")] + [InlineData("CheckLargeStdOutWrites")] + [InlineData("CheckOversizedStdErrWrites")] + [InlineData("CheckOversizedStdOutWrites")] + public async Task EnableCoreHostTraceLogging_PipeCaptureNativeLogs(string path) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.EnvironmentVariables["COREHOST_TRACE"] = "1"; + deploymentParameters.TransformArguments((a, _) => $"{a} {path}"); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("/HelloWorld"); + + Assert.False(response.IsSuccessStatusCode); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvent(deploymentResult, "Invoked hostfxr"); + } + + [ConditionalTheory] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + [SkipIfDebug] + [InlineData("CheckLargeStdErrWrites")] + [InlineData("CheckLargeStdOutWrites")] + [InlineData("CheckOversizedStdErrWrites")] + [InlineData("CheckOversizedStdOutWrites")] + public async Task EnableCoreHostTraceLogging_FileCaptureNativeLogs(string path) + { + var deploymentParameters = + _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.EnvironmentVariables["COREHOST_TRACE"] = "1"; + deploymentParameters.TransformArguments((a, _) => $"{a} {path}"); + + deploymentParameters.EnableLogging(_logFolderPath); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("/HelloWorld"); + Assert.False(response.IsSuccessStatusCode); + + StopServer(); + + var fileInDirectory = Directory.GetFiles(_logFolderPath).First(); + var contents = Helpers.ReadAllTextFromFile(fileInDirectory, Logger); + + EventLogHelpers.VerifyEventLogEvent(deploymentResult, "Invoked hostfxr"); + Assert.Contains("Invoked hostfxr", contents); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/IIS.Shared.FunctionalTests/MofFileTests.cs b/src/IISIntegration/src/IISIntegration/test/IIS.Shared.FunctionalTests/MofFileTests.cs new file mode 100644 index 0000000000..a054225ac5 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IIS.Shared.FunctionalTests/MofFileTests.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics; +using System.IO; +using Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests; +using Microsoft.AspNetCore.Testing; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace IIS.FunctionalTests +{ + public class MofFileTests + { + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)] + [RequiresIIS(IISCapability.TracingModule)] + public void CheckMofFile() + { + var path = Path.Combine(TestPathUtilities.GetSolutionRootDirectory("IISIntegration"), "src", "aspnetcoremodulev2", "aspnetcore", "ancm.mof"); + var process = Process.Start("mofcomp.exe", path); + process.WaitForExit(); + Assert.Equal(0, process.ExitCode); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/IIS.Shared.FunctionalTests/Properties/AssemblyInfo.cs b/src/IISIntegration/src/IISIntegration/test/IIS.Shared.FunctionalTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..b26f48a815 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IIS.Shared.FunctionalTests/Properties/AssemblyInfo.cs @@ -0,0 +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 Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] +[assembly: RequiresIIS] +[assembly: ShortClassName] diff --git a/src/IISIntegration/src/IISIntegration/test/IIS.Shared.FunctionalTests/RequiresIISAttribute.cs b/src/IISIntegration/src/IISIntegration/test/IIS.Shared.FunctionalTests/RequiresIISAttribute.cs new file mode 100644 index 0000000000..cfbbf70486 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IIS.Shared.FunctionalTests/RequiresIISAttribute.cs @@ -0,0 +1,150 @@ +// 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.Runtime.InteropServices; +using System.Security.Principal; +using System.Xml.Linq; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Win32; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] + public sealed class RequiresIISAttribute : Attribute, ITestCondition + { + private static readonly (IISCapability Capability, string DllName)[] Modules = + { + (IISCapability.Websockets, "iiswsock.dll"), + (IISCapability.WindowsAuthentication, "authsspi.dll"), + (IISCapability.DynamicCompression, "compdyn.dll"), + (IISCapability.ApplicationInitialization, "warmup.dll"), + (IISCapability.TracingModule, "iisetw.dll"), + (IISCapability.FailedRequestTracingModule, "iisfreb.dll"), + (IISCapability.BasicAuthentication, "authbas.dll"), + }; + + private static readonly bool _isMetStatic; + private static readonly string _skipReasonStatic; + private static readonly bool _poolEnvironmentVariablesAvailable; + private static readonly IISCapability _modulesAvailable; + + static RequiresIISAttribute() + { + if (Environment.GetEnvironmentVariable("ASPNETCORE_TEST_SKIP_IIS") == "true") + { + _skipReasonStatic = "Test skipped using ASPNETCORE_TEST_SKIP_IIS environment variable"; + return; + } + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _skipReasonStatic = "IIS tests can only be run on Windows"; + return; + } + + var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + if (!principal.IsInRole(WindowsBuiltInRole.Administrator)) + { + _skipReasonStatic += "The current console is not running as admin."; + return; + } + + if (!File.Exists(Path.Combine(Environment.SystemDirectory, "inetsrv", "w3wp.exe"))) + { + _skipReasonStatic += "The machine does not have IIS installed."; + return; + } + + var ancmConfigPath = Path.Combine(Environment.SystemDirectory, "inetsrv", "config", "schema", "aspnetcore_schema_v2.xml"); + + if (!File.Exists(ancmConfigPath)) + { + _skipReasonStatic = "IIS Schema is not installed."; + return; + } + + XDocument ancmConfig; + + try + { + ancmConfig = XDocument.Load(ancmConfigPath); + } + catch + { + _skipReasonStatic = "Could not read ANCM schema configuration"; + return; + } + + _isMetStatic = ancmConfig + .Root + .Descendants("attribute") + .Any(n => "hostingModel".Equals(n.Attribute("name")?.Value, StringComparison.Ordinal)); + + _skipReasonStatic = _isMetStatic ? null : "IIS schema needs to be upgraded to support ANCM."; + + foreach (var module in Modules) + { + if (File.Exists(Path.Combine(Environment.SystemDirectory, "inetsrv", module.DllName))) + { + _modulesAvailable |= module.Capability; + } + } + + var iisRegistryKey = Registry.LocalMachine.OpenSubKey(@"Software\Microsoft\InetStp", writable: false); + if (iisRegistryKey == null) + { + _poolEnvironmentVariablesAvailable = false; + } + else + { + var majorVersion = (int)iisRegistryKey.GetValue("MajorVersion", -1); + var minorVersion = (int)iisRegistryKey.GetValue("MinorVersion", -1); + var version = new Version(majorVersion, minorVersion); + _poolEnvironmentVariablesAvailable = version >= new Version(10, 0); + } + } + + public RequiresIISAttribute() + : this(IISCapability.None) { } + + public RequiresIISAttribute(IISCapability capabilities) + { + IsMet = _isMetStatic; + SkipReason = _skipReasonStatic; + if (capabilities.HasFlag(IISCapability.PoolEnvironmentVariables)) + { + IsMet &= _poolEnvironmentVariablesAvailable; + if (!_poolEnvironmentVariablesAvailable) + { + 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"; + } + + foreach (var module in Modules) + { + if (capabilities.HasFlag(module.Capability)) + { + var available = _modulesAvailable.HasFlag(module.Capability); + IsMet &= available; + if (!available) + { + SkipReason += $"The machine does have {module.Capability} available."; + } + } + } + } + + public bool IsMet { get; } + public string SkipReason { get; } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/IIS.Shared.FunctionalTests/ServicesTests.cs b/src/IISIntegration/src/IISIntegration/test/IIS.Shared.FunctionalTests/ServicesTests.cs new file mode 100644 index 0000000000..875b5b13be --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IIS.Shared.FunctionalTests/ServicesTests.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.IO; +using System.ServiceProcess; +using System.Threading; +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.IIS; +using Microsoft.AspNetCore.Testing; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace IIS.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class ApplicationInitializationTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public ApplicationInitializationTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalTheory] + [RequiresIIS(IISCapability.ApplicationInitialization)] + [InlineData(HostingModel.InProcess)] + [InlineData(HostingModel.OutOfProcess)] + public async Task ApplicationPreloadStartsApp(HostingModel hostingModel) + { + // This test often hits a memory leak in warmup.dll module, it has been reported to IIS team + using (AppVerifier.Disable(DeployerSelector.ServerType, 0x900)) + { + var baseDeploymentParameters = _fixture.GetBaseDeploymentParameters(hostingModel, publish: true); + baseDeploymentParameters.TransformArguments( + (args, contentRoot) => $"{args} CreateFile \"{Path.Combine(contentRoot, "Started.txt")}\""); + EnablePreload(baseDeploymentParameters); + + var result = await DeployAsync(baseDeploymentParameters); + + await Helpers.Retry(async () => await File.ReadAllTextAsync(Path.Combine(result.ContentRoot, "Started.txt")), 10, 200); + StopServer(); + EventLogHelpers.VerifyEventLogEvent(result, EventLogHelpers.Started(result)); + } + } + + [ConditionalTheory] + [RequiresIIS(IISCapability.ApplicationInitialization)] + [InlineData(HostingModel.InProcess)] + [InlineData(HostingModel.OutOfProcess)] + public async Task ApplicationInitializationPageIsRequested(HostingModel hostingModel) + { + // This test often hits a memory leak in warmup.dll module, it has been reported to IIS team + using (AppVerifier.Disable(DeployerSelector.ServerType, 0x900)) + { + var baseDeploymentParameters = _fixture.GetBaseDeploymentParameters(hostingModel, publish: true); + EnablePreload(baseDeploymentParameters); + + baseDeploymentParameters.ServerConfigActionList.Add( + (config, _) => { + config + .RequiredElement("system.webServer") + .GetOrAdd("applicationInitialization") + .GetOrAdd("add", "initializationPage", "/CreateFile"); + }); + + var result = await DeployAsync(baseDeploymentParameters); + + await Helpers.Retry(async () => await File.ReadAllTextAsync(Path.Combine(result.ContentRoot, "Started.txt")), 10, 200); + StopServer(); + EventLogHelpers.VerifyEventLogEvent(result, EventLogHelpers.Started(result)); + } + } + + private static void EnablePreload(IISDeploymentParameters baseDeploymentParameters) + { + baseDeploymentParameters.EnsureSection("applicationInitialization", "system.webServer"); + baseDeploymentParameters.ServerConfigActionList.Add( + (config, _) => { + + config + .RequiredElement("system.applicationHost") + .RequiredElement("sites") + .RequiredElement("site") + .RequiredElement("application") + .SetAttributeValue("preloadEnabled", true); + }); + + baseDeploymentParameters.EnableModule("ApplicationInitializationModule", "%IIS_BIN%\\warmup.dll"); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/IIS.Tests/AppHostConfig/HostableWebCore.config b/src/IISIntegration/src/IISIntegration/test/IIS.Tests/AppHostConfig/HostableWebCore.config new file mode 100644 index 0000000000..5e994d2855 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IIS.Tests/AppHostConfig/HostableWebCore.config @@ -0,0 +1,198 @@ + + + + + + + + +
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+ + +
+
+
+
+ +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/src/IISIntegration/test/IIS.Tests/ClientDisconnectTests.cs b/src/IISIntegration/src/IISIntegration/test/IIS.Tests/ClientDisconnectTests.cs new file mode 100644 index 0000000000..dbe562aa7c --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IIS.Tests/ClientDisconnectTests.cs @@ -0,0 +1,302 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [SkipIfHostableWebCoreNotAvailable] + [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, "https://github.com/aspnet/IISIntegration/issues/866")] + public class ClientDisconnectTests : StrictTestServerTests + { + [ConditionalFact] + public async Task WritesSucceedAfterClientDisconnect() + { + var requestStartedCompletionSource = CreateTaskCompletionSource(); + var clientDisconnectedCompletionSource = CreateTaskCompletionSource(); + var requestCompletedCompletionSource = CreateTaskCompletionSource(); + + var data = new byte[1024]; + using (var testServer = await TestServer.Create( + async ctx => + { + requestStartedCompletionSource.SetResult(true); + await clientDisconnectedCompletionSource.Task; + for (var i = 0; i < 1000; i++) + { + await ctx.Response.Body.WriteAsync(data); + } + + requestCompletedCompletionSource.SetResult(true); + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + await requestStartedCompletionSource.Task.DefaultTimeout(); + } + clientDisconnectedCompletionSource.SetResult(true); + + await requestCompletedCompletionSource.Task.DefaultTimeout(); + } + + AssertConnectionDisconnectLog(); + } + + [ConditionalFact] + public async Task WritesCancelledWhenUsingAbortedToken() + { + var requestStartedCompletionSource = CreateTaskCompletionSource(); + var requestCompletedCompletionSource = CreateTaskCompletionSource(); + + Exception exception = null; + + var data = new byte[1]; + using (var testServer = await TestServer.Create(async ctx => + { + requestStartedCompletionSource.SetResult(true); + try + { + while (true) + { + await ctx.Response.Body.WriteAsync(data, ctx.RequestAborted); + } + } + catch (Exception e) + { + exception = e; + } + + requestCompletedCompletionSource.SetResult(true); + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + + await requestStartedCompletionSource.Task.DefaultTimeout(); + } + + await requestCompletedCompletionSource.Task.DefaultTimeout(); + + Assert.IsType(exception); + } + + AssertConnectionDisconnectLog(); + } + + [ConditionalFact] + public async Task ReadThrowsAfterClientDisconnect() + { + var requestStartedCompletionSource = CreateTaskCompletionSource(); + var requestCompletedCompletionSource = CreateTaskCompletionSource(); + + Exception exception = null; + + var data = new byte[1024]; + using (var testServer = await TestServer.Create(async ctx => + { + requestStartedCompletionSource.SetResult(true); + try + { + await ctx.Request.Body.ReadAsync(data); + } + catch (Exception e) + { + exception = e; + } + + requestCompletedCompletionSource.SetResult(true); + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + await requestStartedCompletionSource.Task.DefaultTimeout(); + } + + await requestCompletedCompletionSource.Task.DefaultTimeout(); + } + + Assert.IsType(exception); + Assert.Equal("The client has disconnected", exception.Message); + + AssertConnectionDisconnectLog(); + } + + [ConditionalFact] + public async Task WriterThrowsCancelledException() + { + var requestStartedCompletionSource = CreateTaskCompletionSource(); + var requestCompletedCompletionSource = CreateTaskCompletionSource(); + + Exception exception = null; + var cancellationTokenSource = new CancellationTokenSource(); + + var data = new byte[1]; + using (var testServer = await TestServer.Create(async ctx => + { + requestStartedCompletionSource.SetResult(true); + try + { + while (true) + { + await ctx.Response.Body.WriteAsync(data, cancellationTokenSource.Token); + } + } + catch (Exception e) + { + exception = e; + } + + requestCompletedCompletionSource.SetResult(true); + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + + await requestStartedCompletionSource.Task.DefaultTimeout(); + cancellationTokenSource.Cancel(); + await requestCompletedCompletionSource.Task.DefaultTimeout(); + } + + Assert.IsType(exception); + } + } + + [ConditionalFact] + public async Task ReaderThrowsCancelledException() + { + var requestStartedCompletionSource = CreateTaskCompletionSource(); + var requestCompletedCompletionSource = CreateTaskCompletionSource(); + + Exception exception = null; + var cancellationTokenSource = new CancellationTokenSource(); + + var data = new byte[1024]; + using (var testServer = await TestServer.Create(async ctx => + { + requestStartedCompletionSource.SetResult(true); + try + { + await ctx.Request.Body.ReadAsync(data, cancellationTokenSource.Token); + } + catch (Exception e) + { + exception = e; + } + + requestCompletedCompletionSource.SetResult(true); + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + await requestStartedCompletionSource.Task.DefaultTimeout(); + cancellationTokenSource.Cancel(); + await requestCompletedCompletionSource.Task.DefaultTimeout(); + } + Assert.IsType(exception); + } + } + + [ConditionalFact] + public async Task ReaderThrowsResetExceptionOnInvalidBody() + { + var requestStartedCompletionSource = CreateTaskCompletionSource(); + var requestCompletedCompletionSource = CreateTaskCompletionSource(); + + Exception exception = null; + + var data = new byte[1024]; + using (var testServer = await TestServer.Create(async ctx => + { + requestStartedCompletionSource.SetResult(true); + try + { + await ctx.Request.Body.ReadAsync(data); + } + catch (Exception e) + { + exception = e; + } + + requestCompletedCompletionSource.SetResult(true); + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Transfer-Encoding: chunked", + "Host: localhost", + "Connection: close", + "", + ""); + + await requestStartedCompletionSource.Task; + await connection.Send( + "ZZZZZZZZZZZZZ"); + + await connection.Receive( + "HTTP/1.1 400 Bad Request", + "" + ); + + } + await requestCompletedCompletionSource.Task.DefaultTimeout(); + } + + Assert.IsType(exception); + Assert.Equal("The client has disconnected", exception.Message); + AssertConnectionDisconnectLog(); + } + + [ConditionalFact] + public async Task RequestAbortedIsTrippedWithoutIO() + { + var requestStarted = CreateTaskCompletionSource(); + var requestAborted = CreateTaskCompletionSource(); + + using (var testServer = await TestServer.Create( + async ctx => { + ctx.RequestAborted.Register(() => requestAborted.SetResult(true)); + requestStarted.SetResult(true); + await requestAborted.Task; + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + await requestStarted.Task; + } + await requestAborted.Task; + } + + AssertConnectionDisconnectLog(); + } + + private void AssertConnectionDisconnectLog() + { + Assert.Single(TestSink.Writes, w => w.EventId.Name == "ConnectionDisconnect"); + } + + private static async Task SendContentLength1Post(TestConnection connection) + { + await connection.Send( + "POST / HTTP/1.1", + "Content-Length: 1", + "Host: localhost", + "Connection: close", + "", + ""); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/IIS.Tests/ConnectionIdFeatureTests.cs b/src/IISIntegration/src/IISIntegration/test/IIS.Tests/ConnectionIdFeatureTests.cs new file mode 100644 index 0000000000..37e69b3a32 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IIS.Tests/ConnectionIdFeatureTests.cs @@ -0,0 +1,54 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [SkipIfHostableWebCoreNotAvailable] + [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, "https://github.com/aspnet/IISIntegration/issues/866")] + public class HttpBodyControlFeatureTests : StrictTestServerTests + { + [ConditionalFact] + public async Task ThrowsOnSyncReadOrWrite() + { + Exception writeException = null; + Exception readException = null; + using (var testServer = await TestServer.Create( + ctx => { + var bodyControl = ctx.Features.Get(); + bodyControl.AllowSynchronousIO = false; + + try + { + ctx.Response.Body.Write(new byte[10]); + } + catch (Exception ex) + { + writeException = ex; + } + + try + { + ctx.Request.Body.Read(new byte[10]); + } + catch (Exception ex) + { + readException = ex; + } + + return Task.CompletedTask; + }, LoggerFactory)) + { + await testServer.HttpClient.GetStringAsync("/"); + } + + Assert.IsType(readException); + Assert.IsType(writeException); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/IIS.Tests/HttpBodyControlFeatureTests.cs b/src/IISIntegration/src/IISIntegration/test/IIS.Tests/HttpBodyControlFeatureTests.cs new file mode 100644 index 0000000000..3d91c445ca --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IIS.Tests/HttpBodyControlFeatureTests.cs @@ -0,0 +1,31 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [SkipIfHostableWebCoreNotAvailable] + [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, "https://github.com/aspnet/IISIntegration/issues/866")] + public class ConnectionIdFeatureTests : StrictTestServerTests + { + [ConditionalFact] + public async Task ProvidesConnectionId() + { + string connectionId = null; + using (var testServer = await TestServer.Create(ctx => { + var connectionIdFeature = ctx.Features.Get(); + connectionId = connectionIdFeature.ConnectionId; + return Task.CompletedTask; + }, LoggerFactory)) + { + await testServer.HttpClient.GetStringAsync("/"); + } + + Assert.NotNull(connectionId); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/IIS.Tests/IIS.Tests.csproj b/src/IISIntegration/src/IISIntegration/test/IIS.Tests/IIS.Tests.csproj new file mode 100644 index 0000000000..3fdb2a5363 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IIS.Tests/IIS.Tests.csproj @@ -0,0 +1,32 @@ + + + + netcoreapp2.2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/src/IISIntegration/test/IIS.Tests/ResponseAbortTests.cs b/src/IISIntegration/src/IISIntegration/test/IIS.Tests/ResponseAbortTests.cs new file mode 100644 index 0000000000..c7ec472586 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IIS.Tests/ResponseAbortTests.cs @@ -0,0 +1,151 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [SkipIfHostableWebCoreNotAvailable] + [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, "https://github.com/aspnet/IISIntegration/issues/866")] + public class ResponseAbortTests : StrictTestServerTests + { + [ConditionalFact] + public async Task ClosesWithoutSendingAnything() + { + using (var testServer = await TestServer.Create( + ctx => { + ctx.Abort(); + return Task.CompletedTask; + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + await connection.WaitForConnectionClose(); + } + } + } + + [ConditionalFact] + public async Task ClosesAfterDataSent() + { + var bodyReceived = CreateTaskCompletionSource(); + using (var testServer = await TestServer.Create( + async ctx => { + await ctx.Response.WriteAsync("Abort"); + await ctx.Response.Body.FlushAsync(); + await bodyReceived.Task.DefaultTimeout(); + ctx.Abort(); + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + await connection.Receive( + "HTTP/1.1 200 OK", + ""); + await connection.ReceiveHeaders( + "Transfer-Encoding: chunked"); + + await connection.ReceiveChunk("Abort"); + bodyReceived.SetResult(true); + await connection.WaitForConnectionClose(); + } + } + } + + [ConditionalFact] + public async Task ReadsThrowAfterAbort() + { + Exception exception = null; + + using (var testServer = await TestServer.Create( + async ctx => { + ctx.Abort(); + try + { + var a = new byte[10]; + await ctx.Request.Body.ReadAsync(a); + } + catch (Exception e) + { + exception = e; + } + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + await connection.WaitForConnectionClose(); + } + } + + Assert.IsType(exception); + } + + [ConditionalFact] + public async Task WritesNoopAfterAbort() + { + Exception exception = null; + + using (var testServer = await TestServer.Create( + async ctx => { + ctx.Abort(); + try + { + await ctx.Response.Body.WriteAsync(new byte[10]); + } + catch (Exception e) + { + exception = e; + } + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + await connection.WaitForConnectionClose(); + } + } + + Assert.Null(exception); + } + + [ConditionalFact] + public async Task RequestAbortedIsTrippedAfterAbort() + { + bool tokenAborted = false; + using (var testServer = await TestServer.Create( + ctx => { + ctx.Abort(); + tokenAborted = ctx.RequestAborted.IsCancellationRequested; + return Task.CompletedTask; + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + await connection.WaitForConnectionClose(); + } + } + + Assert.True(tokenAborted); + } + + private static async Task SendContentLength1Post(TestConnection connection) + { + await connection.Send( + "POST / HTTP/1.1", + "Content-Length: 1", + "Host: localhost", + "", + ""); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/IIS.Tests/StrictTestServerTests.cs b/src/IISIntegration/src/IISIntegration/test/IIS.Tests/StrictTestServerTests.cs new file mode 100644 index 0000000000..c8f62beedb --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IIS.Tests/StrictTestServerTests.cs @@ -0,0 +1,24 @@ +// 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.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + public class StrictTestServerTests: LoggedTest + { + public override void Dispose() + { + base.Dispose(); + Assert.DoesNotContain(TestSink.Writes, w => w.LogLevel > LogLevel.Information); + } + + protected static TaskCompletionSource CreateTaskCompletionSource() + { + return new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/IIS.Tests/TestServerTest.cs b/src/IISIntegration/src/IISIntegration/test/IIS.Tests/TestServerTest.cs new file mode 100644 index 0000000000..40b538ea52 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IIS.Tests/TestServerTest.cs @@ -0,0 +1,35 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [SkipIfHostableWebCoreNotAvailable] + [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, "https://github.com/aspnet/IISIntegration/issues/866")] + public class TestServerTest : StrictTestServerTests + { + [ConditionalFact] + public async Task SingleProcessTestServer_HelloWorld() + { + var helloWorld = "Hello World"; + var expectedPath = "/Path"; + + string path = null; + using (var testServer = await TestServer.Create(ctx => + { + path = ctx.Request.Path.ToString(); + return ctx.Response.WriteAsync(helloWorld); + }, LoggerFactory)) + { + var result = await testServer.HttpClient.GetAsync(expectedPath); + Assert.Equal(helloWorld, await result.Content.ReadAsStringAsync()); + Assert.Equal(expectedPath, path); + } + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/IIS.Tests/Utilities/SkipIfHostableWebCoreNotAvailibleAttribute.cs b/src/IISIntegration/src/IISIntegration/test/IIS.Tests/Utilities/SkipIfHostableWebCoreNotAvailibleAttribute.cs new file mode 100644 index 0000000000..b63743f106 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IIS.Tests/Utilities/SkipIfHostableWebCoreNotAvailibleAttribute.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using Microsoft.AspNetCore.Testing.xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] + public sealed class SkipIfHostableWebCoreNotAvailableAttribute : Attribute, ITestCondition + { + public bool IsMet { get; } = File.Exists(TestServer.HostableWebCoreLocation); + + public string SkipReason { get; } = $"Hostable Web Core not available, {TestServer.HostableWebCoreLocation} not found."; + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/IIS.Tests/Utilities/TestServer.cs b/src/IISIntegration/src/IISIntegration/test/IIS.Tests/Utilities/TestServer.cs new file mode 100644 index 0000000000..351b826316 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IIS.Tests/Utilities/TestServer.cs @@ -0,0 +1,211 @@ +// 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.IO; +using System.Net.Http; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.Loader; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using System.Xml.XPath; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Server.IntegrationTesting.Common; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + public class TestServer: IDisposable, IStartup + { + private const string InProcessHandlerDll = "aspnetcorev2_inprocess.dll"; + private const string AspNetCoreModuleDll = "aspnetcorev2.dll"; + private const string HWebCoreDll = "hwebcore.dll"; + + internal static string HostableWebCoreLocation => Environment.ExpandEnvironmentVariables($@"%windir%\system32\inetsrv\{HWebCoreDll}"); + internal static string BasePath => Path.GetDirectoryName(new Uri(typeof(TestServer).Assembly.CodeBase).AbsolutePath); + + internal static string AspNetCoreModuleLocation => Path.Combine(BasePath, AspNetCoreModuleDll); + + private static readonly SemaphoreSlim WebCoreLock = new SemaphoreSlim(1, 1); + + private static readonly int PortRetryCount = 10; + + private readonly TaskCompletionSource _startedTaskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + private readonly Action _appBuilder; + private readonly ILoggerFactory _loggerFactory; + private readonly hostfxr_main_fn _hostfxrMainFn; + + private Uri BaseUri => new Uri("http://localhost:" + _currentPort); + public HttpClient HttpClient { get; private set; } + public TestConnection CreateConnection() => new TestConnection(_currentPort); + + private IWebHost _host; + + private string _appHostConfigPath; + private int _currentPort; + + private TestServer(Action appBuilder, ILoggerFactory loggerFactory) + { + _hostfxrMainFn = Main; + _appBuilder = appBuilder; + _loggerFactory = loggerFactory; + } + + public static async Task Create(Action appBuilder, ILoggerFactory loggerFactory) + { + await WebCoreLock.WaitAsync(); + var server = new TestServer(appBuilder, loggerFactory); + server.Start(); + (await server.HttpClient.GetAsync("/start")).EnsureSuccessStatusCode(); + await server._startedTaskCompletionSource.Task; + return server; + } + + public static Task Create(RequestDelegate app, ILoggerFactory loggerFactory) + { + return Create(builder => builder.Run(app), loggerFactory); + } + + private void Start() + { + LoadLibrary(HostableWebCoreLocation); + _appHostConfigPath = Path.GetTempFileName(); + + set_main_handler(_hostfxrMainFn); + + Retry(() => + { + _currentPort = TestPortHelper.GetNextPort(); + + InitializeConfig(_currentPort); + + var startResult = WebCoreActivate(_appHostConfigPath, null, "Instance"); + if (startResult != 0) + { + throw new InvalidOperationException($"Error while running WebCoreActivate: {startResult} on port {_currentPort}"); + } + }, PortRetryCount); + + HttpClient = new HttpClient(new LoggingHandler(new SocketsHttpHandler(), _loggerFactory.CreateLogger())) + { + BaseAddress = BaseUri + }; + } + + private void InitializeConfig(int port) + { + var webHostConfig = XDocument.Load(Path.GetFullPath("HostableWebCore.config")); + webHostConfig.XPathSelectElement("/configuration/system.webServer/globalModules/add[@name='AspNetCoreModuleV2']") + .SetAttributeValue("image", AspNetCoreModuleLocation); + + var siteElement = webHostConfig.Root + .RequiredElement("system.applicationHost") + .RequiredElement("sites") + .RequiredElement("site"); + + siteElement + .RequiredElement("bindings") + .RequiredElement("binding") + .SetAttributeValue("bindingInformation", $":{port}:localhost"); + + webHostConfig.Save(_appHostConfigPath); + } + + private int Main(IntPtr argc, IntPtr argv) + { + _host = new WebHostBuilder() + .UseIIS() + .ConfigureServices(services => { + services.AddSingleton(this); + services.AddSingleton(_loggerFactory); + }) + .UseSetting(WebHostDefaults.ApplicationKey, typeof(TestServer).GetTypeInfo().Assembly.FullName) + .Build(); + + var doneEvent = new ManualResetEventSlim(); + var lifetime = _host.Services.GetService(); + + lifetime.ApplicationStopping.Register(() => doneEvent.Set()); + _host.Start(); + _startedTaskCompletionSource.SetResult(null); + doneEvent.Wait(); + _host.Dispose(); + return 0; + } + + public void Dispose() + { + HttpClient.Dispose(); + + // WebCoreShutdown occasionally AVs + // This causes the dotnet test process to crash + // To avoid this, we have to wait to shutdown + // and pass in true to immediately shutdown the hostable web core + // Both of these seem to be required. + Thread.Sleep(100); + WebCoreShutdown(immediate: true); + WebCoreLock.Release(); + } + + public IServiceProvider ConfigureServices(IServiceCollection services) + { + return services.BuildServiceProvider(); + } + + public void Configure(IApplicationBuilder app) + { + app.Map("/start", builder => builder.Run(context => context.Response.WriteAsync("Done"))); + _appBuilder(app); + } + + private delegate int hostfxr_main_fn(IntPtr argc, IntPtr argv); + + [DllImport(HWebCoreDll)] + private static extern int WebCoreActivate( + [In, MarshalAs(UnmanagedType.LPWStr)] + string appHostConfigPath, + [In, MarshalAs(UnmanagedType.LPWStr)] + string rootWebConfigPath, + [In, MarshalAs(UnmanagedType.LPWStr)] + string instanceName); + + [DllImport(HWebCoreDll)] + private static extern int WebCoreShutdown(bool immediate); + + [DllImport(InProcessHandlerDll)] + private static extern int set_main_handler(hostfxr_main_fn main); + + [DllImport("kernel32", SetLastError=true, CharSet = CharSet.Ansi)] + private static extern IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpFileName); + + private void Retry(Action func, int attempts) + { + var exceptions = new List(); + + for (var attempt = 0; attempt < attempts; attempt++) + { + try + { + func(); + return; + } + catch (Exception e) + { + exceptions.Add(e); + } + } + + throw new AggregateException(exceptions); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/DeployerSelector.cs b/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/DeployerSelector.cs new file mode 100644 index 0000000000..ba6a1ec6e2 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/DeployerSelector.cs @@ -0,0 +1,14 @@ +// 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.Server.IntegrationTesting; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + public static class DeployerSelector + { + public static ServerType ServerType => ServerType.IISExpress; + public static bool IsBackwardsCompatiblityTest => false; + public static bool IsForwardsCompatibilityTest => false; + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/HttpsTests.cs b/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/HttpsTests.cs new file mode 100644 index 0000000000..ac58f73c0e --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/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/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/IISExpress.FunctionalTests.csproj b/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/IISExpress.FunctionalTests.csproj new file mode 100644 index 0000000000..988c2d5943 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/IISExpress.FunctionalTests.csproj @@ -0,0 +1,38 @@ + + + + netcoreapp2.2 + True + + + + + + + + + + + False + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/InProcess/AuthenticationTests.cs b/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/InProcess/AuthenticationTests.cs new file mode 100644 index 0000000000..47fb3a6553 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/InProcess/AuthenticationTests.cs @@ -0,0 +1,70 @@ +// 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; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class AuthenticationTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public AuthenticationTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + [RequiresIIS(IISCapability.WindowsAuthentication)] + public async Task Authentication_InProcess() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + deploymentParameters.SetWindowsAuth(); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("/AuthenticationAnonymous"); + + var responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Anonymous?True", responseText); + + response = await deploymentResult.HttpClient.GetAsync("/AuthenticationRestricted"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Contains("NTLM", response.Headers.WwwAuthenticate.ToString()); + Assert.Contains("Negotiate", response.Headers.WwwAuthenticate.ToString()); + + response = await deploymentResult.HttpClient.GetAsync("/AuthenticationRestrictedNTLM"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Contains("NTLM", response.Headers.WwwAuthenticate.ToString()); + // Note we can't restrict a challenge to a specific auth type, the native auth modules always add themselves. + Assert.Contains("Negotiate", response.Headers.WwwAuthenticate.ToString()); + + response = await deploymentResult.HttpClient.GetAsync("/AuthenticationForbidden"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + + var httpClientHandler = new HttpClientHandler() { UseDefaultCredentials = true }; + var httpClient = deploymentResult.CreateHttpClient(httpClientHandler); + + response = await httpClient.GetAsync("/AuthenticationAnonymous"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Anonymous?True", responseText); + + response = await httpClient.GetAsync("/AuthenticationRestricted"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotEmpty(responseText); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/InProcess/ShutdownTests.cs b/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/InProcess/ShutdownTests.cs new file mode 100644 index 0000000000..caaf728c64 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/InProcess/ShutdownTests.cs @@ -0,0 +1,90 @@ +// 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.Net.Http; +using System.Net.Sockets; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class ShutdownTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public ShutdownTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task ServerShutsDownWhenMainExits() + { + var parameters = _fixture.GetBaseDeploymentParameters(publish: true); + var deploymentResult = await DeployAsync(parameters); + try + { + await deploymentResult.HttpClient.GetAsync("/Shutdown"); + } + catch (HttpRequestException ex) when (ex.InnerException is IOException) + { + // Server might close a connection before request completes + } + + deploymentResult.AssertWorkerProcessStop(); + } + + + [ConditionalFact] + public async Task ServerShutsDownWhenMainExitsStress() + { + var parameters = _fixture.GetBaseDeploymentParameters(publish: true); + var deploymentResult = await StartAsync(parameters); + + var load = Helpers.StressLoad(deploymentResult.HttpClient, "/HelloWorld", response => { + var statusCode = (int)response.StatusCode; + Assert.True(statusCode == 200 || statusCode == 503, "Status code was " + statusCode); + }); + + try + { + await deploymentResult.HttpClient.GetAsync("/Shutdown"); + await load; + } + catch (HttpRequestException ex) when (ex.InnerException is IOException | ex.InnerException is SocketException) + { + // Server might close a connection before request completes + } + + deploymentResult.AssertWorkerProcessStop(); + } + + [ConditionalFact] + public async Task GracefulShutdown_DoesNotCrashProcess() + { + var parameters = _fixture.GetBaseDeploymentParameters(publish: true); + var result = await DeployAsync(parameters); + + var response = await result.HttpClient.GetAsync("/HelloWorld"); + StopServer(gracefulShutdown: true); + Assert.True(result.HostProcess.ExitCode == 0); + } + + [ConditionalFact] + public async Task ForcefulShutdown_DoesCrashProcess() + { + var parameters = _fixture.GetBaseDeploymentParameters(publish: true); + var result = await DeployAsync(parameters); + + var response = await result.HttpClient.GetAsync("/HelloWorld"); + StopServer(gracefulShutdown: false); + Assert.True(result.HostProcess.ExitCode == 1); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/InProcess/WebSocketTests.cs b/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/InProcess/WebSocketTests.cs new file mode 100644 index 0000000000..826001c96d --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/InProcess/WebSocketTests.cs @@ -0,0 +1,79 @@ +// 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.Linq; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, SkipReason = "No supported on this platform")] + public class WebSocketsTests + { + private readonly string _webSocketUri; + + public WebSocketsTests(IISTestSiteFixture fixture) + { + _webSocketUri = fixture.DeploymentResult.ApplicationBaseUri.Replace("http:", "ws:"); + } + + [ConditionalFact] + public async Task OnStartedCalledForWebSocket() + { + var cws = new ClientWebSocket(); + await cws.ConnectAsync(new Uri(_webSocketUri + "WebSocketLifetimeEvents"), default); + + await ReceiveMessage(cws, "OnStarting"); + await ReceiveMessage(cws, "Upgraded"); + } + + [ConditionalFact] + public async Task WebReadBeforeUpgrade() + { + var cws = new ClientWebSocket(); + await cws.ConnectAsync(new Uri(_webSocketUri + "WebReadBeforeUpgrade"), default); + + await ReceiveMessage(cws, "Yay"); + } + + [ConditionalFact] + public async Task CanSendAndReceieveData() + { + var cws = new ClientWebSocket(); + await cws.ConnectAsync(new Uri(_webSocketUri + "WebSocketEcho"), default); + + for (int i = 0; i < 1000; i++) + { + var mesage = i.ToString(); + await SendMessage(cws, mesage); + await ReceiveMessage(cws, mesage); + } + } + + private async Task SendMessage(ClientWebSocket webSocket, string message) + { + await webSocket.SendAsync(new ArraySegment(Encoding.ASCII.GetBytes(message)), WebSocketMessageType.Text, true, default); + } + + private async Task ReceiveMessage(ClientWebSocket webSocket, string expectedMessage) + { + var received = new byte[expectedMessage.Length]; + + var offset = 0; + WebSocketReceiveResult result; + do + { + result = await webSocket.ReceiveAsync(new ArraySegment(received, offset, received.Length - offset), default); + offset += result.Count; + } while (!result.EndOfMessage); + + Assert.Equal(expectedMessage, Encoding.ASCII.GetString(received)); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/OutOfProcess/MultipleAppTests.cs b/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/OutOfProcess/MultipleAppTests.cs new file mode 100644 index 0000000000..349f72d7bf --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/OutOfProcess/MultipleAppTests.cs @@ -0,0 +1,81 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class MultipleAppTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public MultipleAppTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalTheory] + [InlineData(AncmVersion.AspNetCoreModule)] + [InlineData(AncmVersion.AspNetCoreModuleV2)] + public Task Startup(AncmVersion ancmVersion) + { + // ANCM v1 currently does *not* retry if an app fails to start the first time due to a port collision. + // So, this test is expected to fail on v1 approximately 1 in 1,000 times (probably of at least one collision + // when 10 sites choose a random port from the range 1025-48000). Adding one retry should reduce the failure + // rate from 1 in 1,000 to 1 in 1,000,000. The previous product code (with "srand(GetTickCount())") should still + // fail the test reliably. + // https://github.com/aspnet/IISIntegration/issues/1350 + // + // ANCM v2 does retry on port collisions, so no retries should be required. + var attempts = (ancmVersion == AncmVersion.AspNetCoreModule) ? 2 : 1; + + return Helpers.Retry(async () => + { + const int numApps = 10; + + using (var deployers = new DisposableList()) + { + var deploymentResults = new List(); + + // Deploy all apps + for (var i = 0; i < numApps; i++) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(hostingModel: IntegrationTesting.HostingModel.OutOfProcess); + deploymentParameters.AncmVersion = ancmVersion; + + var deployer = CreateDeployer(deploymentParameters); + deployers.Add(deployer); + deploymentResults.Add(await deployer.DeployAsync()); + } + + // Start all requests as quickly as possible, so apps are started as quickly as possible, + // to test possible race conditions when multiple apps start at the same time. + var requestTasks = new List>(); + foreach (var deploymentResult in deploymentResults) + { + requestTasks.Add(deploymentResult.HttpClient.GetAsync("/HelloWorld")); + } + + // Verify all apps started and return expected response + foreach (var requestTask in requestTasks) + { + var response = await requestTask; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal("Hello World", responseText); + } + } + }, + attempts: attempts, msDelay: 0); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/OutOfProcess/NtlmAuthentationTest.cs b/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/OutOfProcess/NtlmAuthentationTest.cs new file mode 100644 index 0000000000..3ff46bf304 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/OutOfProcess/NtlmAuthentationTest.cs @@ -0,0 +1,87 @@ +// 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; +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.IIS; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class NtlmAuthenticationTests : IISFunctionalTestBase + { + // Test only runs on IISExpress today as our CI machines do not have + // Windows auth installed globally. + // TODO either enable windows auth on our CI or use containers to test this + // behavior + + private readonly PublishedSitesFixture _fixture; + + public NtlmAuthenticationTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + public static TestMatrix TestVariants + => TestMatrix.ForServers(DeployerSelector.ServerType) + .WithTfms(Tfm.NetCoreApp22, Tfm.Net461) + .WithAllAncmVersions(); + + [ConditionalTheory] + [RequiresIIS(IISCapability.WindowsAuthentication)] + [MemberData(nameof(TestVariants))] + public async Task NtlmAuthentication(TestVariant variant) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant); + deploymentParameters.ApplicationBaseUriHint = $"https://localhost:0/"; + + deploymentParameters.SetWindowsAuth(enabled: true); + + var result = await DeployAsync(deploymentParameters); + var response = await result.HttpClient.GetAsync("/HelloWorld"); + + var responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Hello World", responseText); + + var httpClient = result.HttpClient; + response = await httpClient.GetAsync("/Anonymous"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Anonymous?True", responseText); + + response = await httpClient.GetAsync("/Restricted"); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Contains("NTLM", response.Headers.WwwAuthenticate.ToString()); + Assert.Contains("Negotiate", response.Headers.WwwAuthenticate.ToString()); + + response = await httpClient.GetAsync("/RestrictedNTLM"); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Contains("NTLM", response.Headers.WwwAuthenticate.ToString()); + // Note we can't restrict a challenge to a specific auth type, the native auth modules always add themselves. + Assert.Contains("Negotiate", response.Headers.WwwAuthenticate.ToString()); + + response = await httpClient.GetAsync("/Forbidden"); + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + + var httpClientHandler = new HttpClientHandler() { UseDefaultCredentials = true }; + httpClient = result.CreateClient(httpClientHandler); + + response = await httpClient.GetAsync("/Anonymous"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Anonymous?True", responseText); + + response = await httpClient.GetAsync("/Restricted"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotEmpty(responseText); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/Properties/AssemblyInfo.cs b/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..b26f48a815 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/Properties/AssemblyInfo.cs @@ -0,0 +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 Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] +[assembly: RequiresIIS] +[assembly: ShortClassName] diff --git a/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/RequiresIISAttribute.cs b/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/RequiresIISAttribute.cs new file mode 100644 index 0000000000..2e0a68b9f7 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/RequiresIISAttribute.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] + public sealed class RequiresIISAttribute : Attribute, ITestCondition + { + public bool IsMet => IISExpressAncmSchema.SupportsInProcessHosting; + + public string SkipReason => IISExpressAncmSchema.SkipReason; + + public RequiresIISAttribute() { } + + public RequiresIISAttribute(IISCapability capabilities) + { + // IISCapabilities aren't pertinent to IISExpress + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/UpgradeFeatureDetectionTests.cs b/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/UpgradeFeatureDetectionTests.cs new file mode 100644 index 0000000000..62d07c5fd9 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/IISExpress.FunctionalTests/UpgradeFeatureDetectionTests.cs @@ -0,0 +1,83 @@ +// 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.Linq; +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; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class UpgradeFeatureDetectionTests : IISFunctionalTestBase + { + private readonly string _isWebsocketsSupported = Environment.OSVersion.Version >= new Version(6, 2) ? "Enabled" : "Disabled"; + private readonly PublishedSitesFixture _fixture; + + public UpgradeFeatureDetectionTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public Task UpgradeFeatureDetectionDisabled_InProcess() + { + // fails due to not modifying the apphost.config file. + return UpgradeFeatureDetectionDeployer( + disableWebSocket: true, + Helpers.GetInProcessTestSitesPath(), + "Disabled", HostingModel.InProcess); + } + + [ConditionalFact] + public Task UpgradeFeatureDetectionEnabled_InProcess() + { + return UpgradeFeatureDetectionDeployer( + disableWebSocket: false, + Helpers.GetInProcessTestSitesPath(), + _isWebsocketsSupported, HostingModel.InProcess); + } + + [ConditionalFact] + public Task UpgradeFeatureDetectionDisabled_OutOfProcess() + { + return UpgradeFeatureDetectionDeployer( + disableWebSocket: true, + Helpers.GetOutOfProcessTestSitesPath(), + "Disabled", HostingModel.OutOfProcess); + } + + [ConditionalFact] + public Task UpgradeFeatureDetectionEnabled_OutOfProcess() + { + return UpgradeFeatureDetectionDeployer( + disableWebSocket: false, + Helpers.GetOutOfProcessTestSitesPath(), + _isWebsocketsSupported, HostingModel.OutOfProcess); + } + + private async Task UpgradeFeatureDetectionDeployer(bool disableWebSocket, string sitePath, string expected, HostingModel hostingModel) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(hostingModel, publish: true); + + if (disableWebSocket) + { + // For IIS, we need to modify the apphost.config file + deploymentParameters.AddServerConfigAction( + element => element.Descendants("webSocket") + .Single() + .SetAttributeValue("enabled", "false")); + } + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("UpgradeFeatureDetection"); + var responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(expected, responseText); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Microsoft.AspNetCore.Server.IISIntegration.Tests/IISExtensionTests.cs b/src/IISIntegration/src/IISIntegration/test/Microsoft.AspNetCore.Server.IISIntegration.Tests/IISExtensionTests.cs new file mode 100644 index 0000000000..772fbde2c3 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Microsoft.AspNetCore.Server.IISIntegration.Tests/IISExtensionTests.cs @@ -0,0 +1,33 @@ +// 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.Linq; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration +{ + public class IISExtensionTests + { + [Fact] + public void CallingUseIISIntegrationMultipleTimesWorks() + { + + var builder = new WebHostBuilder() + .UseSetting("TOKEN", "TestToken") + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", "/") + .UseIISIntegration() + .UseIISIntegration() + .Configure(app => { }); + var server = new TestServer(builder); + + var filters = server.Host.Services.GetServices() + .OfType(); + + Assert.Single(filters); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Microsoft.AspNetCore.Server.IISIntegration.Tests/IISMiddlewareTests.cs b/src/IISIntegration/src/IISIntegration/test/Microsoft.AspNetCore.Server.IISIntegration.Tests/IISMiddlewareTests.cs new file mode 100644 index 0000000000..0898b7ae21 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Microsoft.AspNetCore.Server.IISIntegration.Tests/IISMiddlewareTests.cs @@ -0,0 +1,420 @@ +// 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.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http.Features.Authentication; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration +{ + public class IISMiddlewareTests + { + [Fact] + public async Task MiddlewareSkippedIfTokenIsMissing() + { + var assertsExecuted = false; + + var builder = new WebHostBuilder() + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", "/") + .UseIISIntegration() + .Configure(app => + { + app.Run(context => + { + var auth = context.Features.Get(); + Assert.Null(auth); + assertsExecuted = true; + return Task.FromResult(0); + }); + }); + var server = new TestServer(builder); + + var req = new HttpRequestMessage(HttpMethod.Get, ""); + req.Headers.TryAddWithoutValidation("MS-ASPNETCORE-TOKEN", "TestToken"); + var response = await server.CreateClient().SendAsync(req); + Assert.True(assertsExecuted); + response.EnsureSuccessStatusCode(); + } + + [Fact] + public async Task MiddlewareRejectsRequestIfTokenHeaderIsMissing() + { + var assertsExecuted = false; + + var builder = new WebHostBuilder() + .UseSetting("TOKEN", "TestToken") + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", "/") + .UseIISIntegration() + .Configure(app => + { + app.Run(context => + { + var auth = context.Features.Get(); + Assert.Null(auth); + assertsExecuted = true; + return Task.FromResult(0); + }); + }); + var server = new TestServer(builder); + + var req = new HttpRequestMessage(HttpMethod.Get, ""); + var response = await server.CreateClient().SendAsync(req); + Assert.False(assertsExecuted); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Theory] + [InlineData("/", "/iisintegration", "shutdown")] + [InlineData("/", "/iisintegration", "Shutdown")] + [InlineData("/pathBase", "/pathBase/iisintegration", "shutdown")] + [InlineData("/pathBase", "/pathBase/iisintegration", "Shutdown")] + public async Task MiddlewareShutsdownGivenANCMShutdown(string pathBase, string requestPath, string shutdownEvent) + { + var requestExecuted = new ManualResetEvent(false); + var applicationStoppingFired = new ManualResetEvent(false); + var builder = new WebHostBuilder() + .UseSetting("TOKEN", "TestToken") + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", pathBase) + .UseIISIntegration() + .Configure(app => + { + var appLifetime = app.ApplicationServices.GetRequiredService(); + appLifetime.ApplicationStopping.Register(() => applicationStoppingFired.Set()); + + app.Run(context => + { + requestExecuted.Set(); + return Task.FromResult(0); + }); + }); + var server = new TestServer(builder); + + var request = new HttpRequestMessage(HttpMethod.Post, requestPath); + request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-TOKEN", "TestToken"); + request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-EVENT", shutdownEvent); + var response = await server.CreateClient().SendAsync(request); + + Assert.True(applicationStoppingFired.WaitOne(TimeSpan.FromSeconds(5))); + Assert.False(requestExecuted.WaitOne(0)); + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + } + + public static TheoryData InvalidShutdownMethods + { + get + { + return new TheoryData + { + HttpMethod.Put, + HttpMethod.Trace, + HttpMethod.Head, + HttpMethod.Get, + HttpMethod.Delete, + HttpMethod.Options + }; + } + } + + [Theory] + [MemberData(nameof(InvalidShutdownMethods))] + public async Task MiddlewareIgnoresShutdownGivenWrongMethod(HttpMethod method) + { + var requestExecuted = new ManualResetEvent(false); + var applicationStoppingFired = new ManualResetEvent(false); + var builder = new WebHostBuilder() + .UseSetting("TOKEN", "TestToken") + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", "/") + .UseIISIntegration() + .Configure(app => + { + var appLifetime = app.ApplicationServices.GetRequiredService(); + appLifetime.ApplicationStopping.Register(() => applicationStoppingFired.Set()); + + app.Run(context => + { + requestExecuted.Set(); + return Task.FromResult(0); + }); + }); + var server = new TestServer(builder); + + var request = new HttpRequestMessage(method, "/iisintegration"); + request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-TOKEN", "TestToken"); + request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-EVENT", "shutdown"); + var response = await server.CreateClient().SendAsync(request); + + Assert.False(applicationStoppingFired.WaitOne(TimeSpan.FromSeconds(1))); + Assert.True(requestExecuted.WaitOne(0)); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Theory] + [InlineData("/")] + [InlineData("/path")] + [InlineData("/path/iisintegration")] + public async Task MiddlewareIgnoresShutdownGivenWrongPath(string path) + { + var requestExecuted = new ManualResetEvent(false); + var applicationStoppingFired = new ManualResetEvent(false); + var builder = new WebHostBuilder() + .UseSetting("TOKEN", "TestToken") + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", "/") + .UseIISIntegration() + .Configure(app => + { + var appLifetime = app.ApplicationServices.GetRequiredService(); + appLifetime.ApplicationStopping.Register(() => applicationStoppingFired.Set()); + + app.Run(context => + { + requestExecuted.Set(); + return Task.FromResult(0); + }); + }); + var server = new TestServer(builder); + + var request = new HttpRequestMessage(HttpMethod.Post, path); + request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-TOKEN", "TestToken"); + request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-EVENT", "shutdown"); + var response = await server.CreateClient().SendAsync(request); + + Assert.False(applicationStoppingFired.WaitOne(TimeSpan.FromSeconds(1))); + Assert.True(requestExecuted.WaitOne(0)); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Theory] + [InlineData("event")] + [InlineData("")] + [InlineData(null)] + public async Task MiddlewareIgnoresShutdownGivenWrongEvent(string shutdownEvent) + { + var requestExecuted = new ManualResetEvent(false); + var applicationStoppingFired = new ManualResetEvent(false); + var builder = new WebHostBuilder() + .UseSetting("TOKEN", "TestToken") + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", "/") + .UseIISIntegration() + .Configure(app => + { + var appLifetime = app.ApplicationServices.GetRequiredService(); + appLifetime.ApplicationStopping.Register(() => applicationStoppingFired.Set()); + + app.Run(context => + { + requestExecuted.Set(); + return Task.FromResult(0); + }); + }); + var server = new TestServer(builder); + + var request = new HttpRequestMessage(HttpMethod.Post, "/iisintegration"); + request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-TOKEN", "TestToken"); + request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-EVENT", shutdownEvent); + var response = await server.CreateClient().SendAsync(request); + + Assert.False(applicationStoppingFired.WaitOne(TimeSpan.FromSeconds(1))); + Assert.True(requestExecuted.WaitOne(0)); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public void UrlDelayRegisteredAndPreferHostingUrlsSet() + { + var builder = new WebHostBuilder() + .UseSetting("TOKEN", "TestToken") + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", "/") + .UseIISIntegration() + .Configure(app => + { + app.Run(context => Task.FromResult(0)); + }); + + Assert.Null(builder.GetSetting(WebHostDefaults.ServerUrlsKey)); + Assert.Null(builder.GetSetting(WebHostDefaults.PreferHostingUrlsKey)); + + // Adds a server and calls Build() + var server = new TestServer(builder); + + Assert.Equal("http://127.0.0.1:12345", builder.GetSetting(WebHostDefaults.ServerUrlsKey)); + Assert.Equal("true", builder.GetSetting(WebHostDefaults.PreferHostingUrlsKey)); + } + + [Fact] + public void PathBaseHiddenFromServer() + { + var builder = new WebHostBuilder() + .UseSetting("TOKEN", "TestToken") + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", "/pathBase") + .UseIISIntegration() + .Configure(app => + { + app.Run(context => Task.FromResult(0)); + }); + new TestServer(builder); + + Assert.Equal("http://127.0.0.1:12345", builder.GetSetting(WebHostDefaults.ServerUrlsKey)); + } + + [Fact] + public async Task AddsUsePathBaseMiddlewareWhenPathBaseSpecified() + { + var requestPathBase = string.Empty; + var requestPath = string.Empty; + var builder = new WebHostBuilder() + .UseSetting("TOKEN", "TestToken") + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", "/pathbase") + .UseIISIntegration() + .Configure(app => + { + app.Run(context => + { + requestPathBase = context.Request.PathBase.Value; + requestPath = context.Request.Path.Value; + return Task.FromResult(0); + }); + }); + var server = new TestServer(builder); + + var request = new HttpRequestMessage(HttpMethod.Get, "/PathBase/Path"); + request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-TOKEN", "TestToken"); + var response = await server.CreateClient().SendAsync(request); + + Assert.Equal("/PathBase", requestPathBase); + Assert.Equal("/Path", requestPath); + } + + [Fact] + public async Task AddsAuthenticationHandlerByDefault() + { + var assertsExecuted = false; + + var builder = new WebHostBuilder() + .UseSetting("TOKEN", "TestToken") + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", "/") + .UseIISIntegration() + .Configure(app => + { + app.Run(async context => + { + var auth = context.RequestServices.GetRequiredService(); + var windows = await auth.GetSchemeAsync(IISDefaults.AuthenticationScheme); + Assert.NotNull(windows); + Assert.Null(windows.DisplayName); + Assert.Equal("Microsoft.AspNetCore.Server.IISIntegration.AuthenticationHandler", windows.HandlerType.FullName); + assertsExecuted = true; + }); + }); + var server = new TestServer(builder); + + var req = new HttpRequestMessage(HttpMethod.Get, ""); + req.Headers.TryAddWithoutValidation("MS-ASPNETCORE-TOKEN", "TestToken"); + await server.CreateClient().SendAsync(req); + + Assert.True(assertsExecuted); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task OnlyAddAuthenticationHandlerIfForwardWindowsAuthentication(bool forward) + { + var assertsExecuted = false; + + var builder = new WebHostBuilder() + .UseSetting("TOKEN", "TestToken") + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", "/") + .UseIISIntegration() + .ConfigureServices(services => + { + services.Configure(options => + { + options.ForwardWindowsAuthentication = forward; + }); + }) + .Configure(app => + { + app.Run(async context => + { + var auth = context.RequestServices.GetService(); + Assert.NotNull(auth); + var windowsAuth = await auth.GetSchemeAsync(IISDefaults.AuthenticationScheme); + if (forward) + { + Assert.NotNull(windowsAuth); + Assert.Null(windowsAuth.DisplayName); + Assert.Equal("AuthenticationHandler", windowsAuth.HandlerType.Name); + } + else + { + Assert.Null(windowsAuth); + } + assertsExecuted = true; + }); + }); + var server = new TestServer(builder); + + var req = new HttpRequestMessage(HttpMethod.Get, ""); + req.Headers.TryAddWithoutValidation("MS-ASPNETCORE-TOKEN", "TestToken"); + await server.CreateClient().SendAsync(req); + + Assert.True(assertsExecuted); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task DoesNotBlowUpWithoutAuth(bool forward) + { + var assertsExecuted = false; + + var builder = new WebHostBuilder() + .UseSetting("TOKEN", "TestToken") + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", "/") + .UseIISIntegration() + .ConfigureServices(services => + { + services.Configure(options => + { + options.ForwardWindowsAuthentication = forward; + }); + }) + .Configure(app => + { + app.Run(context => + { + assertsExecuted = true; + return Task.FromResult(0); + }); + }); + var server = new TestServer(builder); + + var req = new HttpRequestMessage(HttpMethod.Get, ""); + req.Headers.TryAddWithoutValidation("MS-ASPNETCORE-TOKEN", "TestToken"); + await server.CreateClient().SendAsync(req); + + Assert.True(assertsExecuted); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/Microsoft.AspNetCore.Server.IISIntegration.Tests/Microsoft.AspNetCore.Server.IISIntegration.Tests.csproj b/src/IISIntegration/src/IISIntegration/test/Microsoft.AspNetCore.Server.IISIntegration.Tests/Microsoft.AspNetCore.Server.IISIntegration.Tests.csproj new file mode 100644 index 0000000000..ea19c2a1fc --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/Microsoft.AspNetCore.Server.IISIntegration.Tests/Microsoft.AspNetCore.Server.IISIntegration.Tests.csproj @@ -0,0 +1,18 @@ + + + + $(StandardTestTfms) + + + + + + + + + + + + + + diff --git a/src/IISIntegration/src/IISIntegration/test/TestTasks/InjectRequestHandler.cs b/src/IISIntegration/src/IISIntegration/test/TestTasks/InjectRequestHandler.cs new file mode 100644 index 0000000000..4da8129d71 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/TestTasks/InjectRequestHandler.cs @@ -0,0 +1,62 @@ +// 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.Diagnostics; +using System.IO; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace TestTasks +{ + public class InjectRequestHandler + { + private static void Main(string[] args) + { + string rid = args[0]; + string libraryLocation = args[1]; + string depsFile = args[2]; + + JToken deps; + using (var file = File.OpenText(depsFile)) + using (JsonTextReader reader = new JsonTextReader(file)) + { + deps = JObject.ReadFrom(reader); + } + + var libraryName = "ANCMRH/1.0"; + var libraries = (JObject)deps["libraries"]; + var targetName = (JValue)deps["runtimeTarget"]["name"]; + + var target = (JObject)deps["targets"][targetName.Value]; + var targetLibrary = target.Properties().FirstOrDefault(p => p.Name == libraryName); + targetLibrary?.Remove(); + targetLibrary = + new JProperty(libraryName, new JObject( + new JProperty("runtimeTargets", new JObject( + new JProperty(libraryLocation.Replace('\\', '/'), new JObject( + new JProperty("rid", rid), + new JProperty("assetType", "native") + )))))); + target.AddFirst(targetLibrary); + + var library = libraries.Properties().FirstOrDefault(p => p.Name == libraryName); + library?.Remove(); + library = + new JProperty(libraryName, new JObject( + new JProperty("type", "package"), + new JProperty("serviceable", true), + new JProperty("sha512", ""), + new JProperty("path", libraryName), + new JProperty("hashPath", ""))); + libraries.AddFirst(library); + + using (var file = File.CreateText(depsFile)) + using (var writer = new JsonTextWriter(file) { Formatting = Formatting.Indented }) + { + deps.WriteTo(writer); + } + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/TestTasks/TestTasks.csproj b/src/IISIntegration/src/IISIntegration/test/TestTasks/TestTasks.csproj new file mode 100644 index 0000000000..aa4c144936 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/TestTasks/TestTasks.csproj @@ -0,0 +1,12 @@ + + + + Exe + $(StandardTestTfms) + + + + + + + diff --git a/src/IISIntegration/src/IISIntegration/test/WebSites/Directory.Build.props b/src/IISIntegration/src/IISIntegration/test/WebSites/Directory.Build.props new file mode 100644 index 0000000000..9b29d34f16 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/WebSites/Directory.Build.props @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessForwardsCompatWebSite/InProcessWebSite.csproj b/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessForwardsCompatWebSite/InProcessWebSite.csproj new file mode 100644 index 0000000000..4d0952108a --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessForwardsCompatWebSite/InProcessWebSite.csproj @@ -0,0 +1,32 @@ + + + + + + netcoreapp2.2 + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessForwardsCompatWebSite/Properties/launchSettings.json b/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessForwardsCompatWebSite/Properties/launchSettings.json new file mode 100644 index 0000000000..246b7a0b47 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessForwardsCompatWebSite/Properties/launchSettings.json @@ -0,0 +1,42 @@ +{ + "iisSettings": { + "windowsAuthentication": true, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5762/", + "sslPort": 0 + } + }, + "profiles": { + "ANCM IIS Express": { + "commandName": "Executable", + "executablePath": "$(IISExpressPath)", + "commandLineArgs": "$(IISExpressArguments)", + "environmentVariables": { + "IIS_SITE_PATH": "$(MSBuildThisFileDirectory)", + "ANCM_PATH": "$(AspNetCoreModuleV1ShimDll)", + "ANCMV2_PATH": "$(AspNetCoreModuleV2ShimDll)", + "ANCM_OUTOFPROCESS_HANDLER": "$(AspNetCoreModuleV2OutOfProcessHandlerDll)", + "LAUNCHER_ARGS": "$(TargetPath)", + "ASPNETCORE_ENVIRONMENT": "Development", + "LAUNCHER_PATH": "$(DotNetPath)", + "ASPNETCORE_MODULE_DEBUG": "console" + } + }, + "ANCM IIS": { + "commandName": "Executable", + "executablePath": "$(IISPath)", + "commandLineArgs": "$(IISArguments)", + "environmentVariables": { + "IIS_SITE_PATH": "$(MSBuildThisFileDirectory)", + "ANCM_PATH": "$(AspNetCoreModuleV1ShimDll)", + "ANCMV2_PATH": "$(AspNetCoreModuleV2ShimDll)", + "ASPNETCORE_MODULE_OUTOFPROCESS_HANDLER": "$(AspNetCoreModuleV2OutOfProcessHandlerDll)", + "LAUNCHER_ARGS": "$(TargetPath)", + "ASPNETCORE_ENVIRONMENT": "Development", + "LAUNCHER_PATH": "$(DotNetPath)", + "ASPNETCORE_MODULE_DEBUG": "console" + } + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessWebSite/DummyServer.cs b/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessWebSite/DummyServer.cs new file mode 100644 index 0000000000..d122f05776 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessWebSite/DummyServer.cs @@ -0,0 +1,30 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http.Features; + +namespace TestSite +{ + public class DummyServer : IServer + { + public void Dispose() + { + } + + public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) + { + return Task.Delay(TimeSpan.MaxValue); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.Delay(TimeSpan.MaxValue); + } + + public IFeatureCollection Features { get; } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessWebSite/InProcessWebSite.csproj b/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessWebSite/InProcessWebSite.csproj new file mode 100644 index 0000000000..d007d2daa1 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessWebSite/InProcessWebSite.csproj @@ -0,0 +1,33 @@ + + + + + + netcoreapp2.2 + true + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessWebSite/Program.cs b/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessWebSite/Program.cs new file mode 100644 index 0000000000..c40448ee91 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessWebSite/Program.cs @@ -0,0 +1,114 @@ +// 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.Text; +using System.Threading; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace TestSite +{ + public static class Program + { + public static int Main(string[] args) + { + var mode = args.FirstOrDefault(); + switch (mode) + { + case "CreateFile": + File.WriteAllText(args[1], ""); + return StartServer(); + case "CheckLargeStdOutWrites": + Console.WriteLine(new string('a', 30000)); + break; + case "CheckLargeStdErrWrites": + Console.Error.WriteLine(new string('a', 30000)); + Console.Error.Flush(); + break; + case "ConsoleWrite": + Console.WriteLine($"Random number: {args[1]}"); + break; + case "ConsoleErrorWrite": + Console.Error.WriteLine($"Random number: {args[1]}"); + Console.Error.Flush(); + break; + case "CheckOversizedStdErrWrites": + Console.WriteLine(new string('a', 31000)); + break; + case "CheckOversizedStdOutWrites": + Console.Error.WriteLine(new string('a', 31000)); + Console.Error.Flush(); + break; + case "Hang": + Thread.Sleep(Timeout.Infinite); + break; + case "Throw": + throw new InvalidOperationException("Program.Main exception"); + case "EarlyReturn": + return 12; + case "HangOnStop": + { + var host = new WebHostBuilder() + .UseIIS() + .UseStartup() + .Build(); + host.Run(); + + Thread.Sleep(Timeout.Infinite); + } + break; + case "CheckConsoleFunctions": + // Call a bunch of console functions and make sure none return invalid handle. + Console.OutputEncoding = Encoding.UTF8; + Console.Title = "Test"; + Console.WriteLine($"Is Console redirection: {Console.IsOutputRedirected}"); + Console.BackgroundColor = ConsoleColor.Blue; + Console.WriteLine("彡⾔"); + break; + case "OverriddenServer": + { + var host = new WebHostBuilder() + .UseIIS() + .ConfigureServices(services => services.AddSingleton()) + .Configure(builder => builder.Run(async context => { await context.Response.WriteAsync("I shouldn't work"); })) + .Build(); + host.Run(); + } + break; + case "ConsoleErrorWriteStartServer": + Console.Error.WriteLine("TEST MESSAGE"); + return StartServer(); + case "ConsoleWriteStartServer": + Console.WriteLine("TEST MESSAGE"); + return StartServer(); + default: + return StartServer(); + + } + return 12; + } + + private static int StartServer() + { + var host = new WebHostBuilder() + .ConfigureLogging((_, factory) => + { + factory.AddConsole(); + factory.AddFilter("Console", level => level >= LogLevel.Information); + }) + .UseIIS() + .UseStartup() + .Build(); + + host.Run(); + return 0; + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessWebSite/Properties/launchSettings.json b/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessWebSite/Properties/launchSettings.json new file mode 100644 index 0000000000..246b7a0b47 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessWebSite/Properties/launchSettings.json @@ -0,0 +1,42 @@ +{ + "iisSettings": { + "windowsAuthentication": true, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5762/", + "sslPort": 0 + } + }, + "profiles": { + "ANCM IIS Express": { + "commandName": "Executable", + "executablePath": "$(IISExpressPath)", + "commandLineArgs": "$(IISExpressArguments)", + "environmentVariables": { + "IIS_SITE_PATH": "$(MSBuildThisFileDirectory)", + "ANCM_PATH": "$(AspNetCoreModuleV1ShimDll)", + "ANCMV2_PATH": "$(AspNetCoreModuleV2ShimDll)", + "ANCM_OUTOFPROCESS_HANDLER": "$(AspNetCoreModuleV2OutOfProcessHandlerDll)", + "LAUNCHER_ARGS": "$(TargetPath)", + "ASPNETCORE_ENVIRONMENT": "Development", + "LAUNCHER_PATH": "$(DotNetPath)", + "ASPNETCORE_MODULE_DEBUG": "console" + } + }, + "ANCM IIS": { + "commandName": "Executable", + "executablePath": "$(IISPath)", + "commandLineArgs": "$(IISArguments)", + "environmentVariables": { + "IIS_SITE_PATH": "$(MSBuildThisFileDirectory)", + "ANCM_PATH": "$(AspNetCoreModuleV1ShimDll)", + "ANCMV2_PATH": "$(AspNetCoreModuleV2ShimDll)", + "ASPNETCORE_MODULE_OUTOFPROCESS_HANDLER": "$(AspNetCoreModuleV2OutOfProcessHandlerDll)", + "LAUNCHER_ARGS": "$(TargetPath)", + "ASPNETCORE_ENVIRONMENT": "Development", + "LAUNCHER_PATH": "$(DotNetPath)", + "ASPNETCORE_MODULE_DEBUG": "console" + } + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessWebSite/Startup.WebSockets.cs b/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessWebSite/Startup.WebSockets.cs new file mode 100644 index 0000000000..d658ebd03f --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessWebSite/Startup.WebSockets.cs @@ -0,0 +1,147 @@ +// 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.IO; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace TestSite +{ + public partial class Startup + { + + private void WebsocketRequest(IApplicationBuilder app) + { + app.Run(async context => + { + await context.Response.WriteAsync("test"); + }); + } + + private void WebReadBeforeUpgrade(IApplicationBuilder app) + { + app.Run(async context => { + + var singleByteArray = new byte[1]; + Assert.Equal(0, await context.Request.Body.ReadAsync(singleByteArray, 0, 1)); + + var ws = await Upgrade(context); + await SendMessages(ws, "Yay"); + }); + } + + private void WebSocketEcho(IApplicationBuilder app) + { + app.Run(async context => + { + var ws = await Upgrade(context); + + var appLifetime = app.ApplicationServices.GetRequiredService(); + + await Echo(ws, appLifetime.ApplicationStopping); + }); + } + + private void WebSocketLifetimeEvents(IApplicationBuilder app) + { + app.Run(async context => { + + var messages = new List(); + + context.Response.OnStarting(() => { + context.Response.Headers["custom-header"] = "value"; + messages.Add("OnStarting"); + return Task.CompletedTask; + }); + + var ws = await Upgrade(context); + messages.Add("Upgraded"); + + await SendMessages(ws, messages.ToArray()); + }); + } + + private static async Task SendMessages(WebSocket webSocket, params string[] messages) + { + foreach (var message in messages) + { + await webSocket.SendAsync(new ArraySegment(Encoding.ASCII.GetBytes(message)), WebSocketMessageType.Text, true, CancellationToken.None); + } + } + + private static async Task Upgrade(HttpContext context) + { + var upgradeFeature = context.Features.Get(); + + // Generate WebSocket response headers + string key = context.Request.Headers[Constants.Headers.SecWebSocketKey].ToString(); + var responseHeaders = HandshakeHelpers.GenerateResponseHeaders(key); + foreach (var headerPair in responseHeaders) + { + context.Response.Headers[headerPair.Key] = headerPair.Value; + } + + // Upgrade the connection + Stream opaqueTransport = await upgradeFeature.UpgradeAsync(); + + // Get the WebSocket object + var ws = WebSocketProtocol.CreateFromStream(opaqueTransport, isServer: true, subProtocol: null, keepAliveInterval: TimeSpan.FromMinutes(2)); + return ws; + } + + private async Task Echo(WebSocket webSocket, CancellationToken token) + { + var buffer = new byte[1024 * 4]; + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), token); + bool closeFromServer = false; + string closeFromServerCmd = "CloseFromServer"; + int closeFromServerLength = closeFromServerCmd.Length; + + while (!result.CloseStatus.HasValue && !token.IsCancellationRequested && !closeFromServer) + { + if (result.Count == closeFromServerLength && + Encoding.ASCII.GetString(buffer).Substring(0, result.Count) == closeFromServerCmd) + { + // The client sent "CloseFromServer" text message to request the server to close (a test scenario). + closeFromServer = true; + } + else + { + await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, token); + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), token); + } + } + + if (result.CloseStatus.HasValue) + { + // Client-initiated close handshake + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + } + else + { + // Server-initiated close handshake due to either of the two conditions: + // (1) The applicaton host is performing a graceful shutdown. + // (2) The client sent "CloseFromServer" text message to request the server to close (a test scenario). + await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, closeFromServerCmd, CancellationToken.None); + + // The server has sent the Close frame. + // Stop sending but keep receiving until we get the Close frame from the client. + while (!result.CloseStatus.HasValue) + { + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + } + } + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessWebSite/Startup.cs b/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessWebSite/Startup.cs new file mode 100644 index 0000000000..eb84272b76 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessWebSite/Startup.cs @@ -0,0 +1,699 @@ +// 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.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.IISIntegration.FunctionalTests; +using Microsoft.AspNetCore.Server.IIS; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace TestSite +{ + public partial class Startup + { + public void Configure(IApplicationBuilder app) + { + TestStartup.Register(app, this); + } + + private async Task ServerVariable(HttpContext ctx) + { + var varName = ctx.Request.Query["q"]; + 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) + { + await ctx.Response.WriteAsync("Anonymous?" + !ctx.User.Identity.IsAuthenticated); + } + + private async Task AuthenticationRestricted(HttpContext ctx) + { + if (ctx.User.Identity.IsAuthenticated) + { + await ctx.Response.WriteAsync(ctx.User.Identity.AuthenticationType); + } + else + { + await ctx.ChallengeAsync(IISServerDefaults.AuthenticationScheme); + } + } + + private async Task AuthenticationForbidden(HttpContext ctx) + { + await ctx.ForbidAsync(IISServerDefaults.AuthenticationScheme); + } + + private async Task AuthenticationRestrictedNTLM(HttpContext ctx) + { + if (string.Equals("NTLM", ctx.User.Identity.AuthenticationType, StringComparison.Ordinal)) + { + await ctx.Response.WriteAsync("NTLM"); + } + else + { + await ctx.ChallengeAsync(IISServerDefaults.AuthenticationScheme); + } + } + + private async Task FeatureCollectionSetRequestFeatures(HttpContext ctx) + { + try + { + Assert.Equal("GET", ctx.Request.Method); + ctx.Request.Method = "test"; + Assert.Equal("test", ctx.Request.Method); + + Assert.Equal("http", ctx.Request.Scheme); + ctx.Request.Scheme = "test"; + Assert.Equal("test", ctx.Request.Scheme); + + Assert.Equal("/FeatureCollectionSetRequestFeatures", ctx.Request.PathBase); + ctx.Request.PathBase = "/base"; + Assert.Equal("/base", ctx.Request.PathBase); + + Assert.Equal("/path", ctx.Request.Path); + ctx.Request.Path = "/path"; + Assert.Equal("/path", ctx.Request.Path); + + Assert.Equal("?query", ctx.Request.QueryString.Value); + ctx.Request.QueryString = QueryString.Empty; + Assert.Equal("", ctx.Request.QueryString.Value); + + Assert.Equal("HTTP/1.1", ctx.Request.Protocol); + ctx.Request.Protocol = "HTTP/1.0"; + Assert.Equal("HTTP/1.0", ctx.Request.Protocol); + + Assert.NotNull(ctx.Request.Headers); + var headers = new HeaderDictionary(); + ctx.Features.Get().Headers = headers; + Assert.Same(headers, ctx.Features.Get().Headers); + + Assert.NotNull(ctx.Request.Body); + var body = new MemoryStream(); + ctx.Request.Body = body; + Assert.Same(body, ctx.Request.Body); + + //Assert.NotNull(ctx.Features.Get().TraceIdentifier); + //Assert.NotEqual(CancellationToken.None, ctx.RequestAborted); + //var token = new CancellationTokenSource().Token; + //ctx.RequestAborted = token; + //Assert.Equal(token, ctx.RequestAborted); + + await ctx.Response.WriteAsync("Success"); + return; + } + catch (Exception exception) + { + ctx.Response.StatusCode = 500; + await ctx.Response.WriteAsync(exception.ToString()); + } + await ctx.Response.WriteAsync("_Failure"); + } + + private async Task FeatureCollectionSetResponseFeatures(HttpContext ctx) + { + try + { + Assert.Equal(200, ctx.Response.StatusCode); + ctx.Response.StatusCode = 404; + Assert.Equal(404, ctx.Response.StatusCode); + ctx.Response.StatusCode = 200; + + Assert.Null(ctx.Features.Get().ReasonPhrase); + ctx.Features.Get().ReasonPhrase = "Set Response"; + Assert.Equal("Set Response", ctx.Features.Get().ReasonPhrase); + + Assert.NotNull(ctx.Response.Headers); + var headers = new HeaderDictionary(); + ctx.Features.Get().Headers = headers; + Assert.Same(headers, ctx.Features.Get().Headers); + + var originalBody = ctx.Response.Body; + Assert.NotNull(originalBody); + var body = new MemoryStream(); + ctx.Response.Body = body; + Assert.Same(body, ctx.Response.Body); + ctx.Response.Body = originalBody; + + await ctx.Response.WriteAsync("Success"); + return; + } + catch (Exception exception) + { + ctx.Response.StatusCode = 500; + await ctx.Response.WriteAsync(exception.ToString()); + } + await ctx.Response.WriteAsync("_Failure"); + } + + private async Task FeatureCollectionSetConnectionFeatures(HttpContext ctx) + { + try + { + Assert.True(IPAddress.IsLoopback(ctx.Connection.LocalIpAddress)); + ctx.Connection.LocalIpAddress = IPAddress.IPv6Any; + Assert.Equal(IPAddress.IPv6Any, ctx.Connection.LocalIpAddress); + + Assert.True(IPAddress.IsLoopback(ctx.Connection.RemoteIpAddress)); + ctx.Connection.RemoteIpAddress = IPAddress.IPv6Any; + Assert.Equal(IPAddress.IPv6Any, ctx.Connection.RemoteIpAddress); + await ctx.Response.WriteAsync("Success"); + return; + } + catch (Exception exception) + { + ctx.Response.StatusCode = 500; + await ctx.Response.WriteAsync(exception.ToString()); + } + await ctx.Response.WriteAsync("_Failure"); + } + + private void Throw(HttpContext ctx) + { + throw new Exception(); + } + + private async Task SetCustomErorCode(HttpContext ctx) + { + var feature = ctx.Features.Get(); + feature.ReasonPhrase = ctx.Request.Query["reason"]; + feature.StatusCode = int.Parse(ctx.Request.Query["code"]); + if (ctx.Request.Query["writeBody"] == "True") + { + await ctx.Response.WriteAsync(ctx.Request.Query["body"]); + } + } + + private async Task HelloWorld(HttpContext ctx) + { + if (ctx.Request.Path.Value.StartsWith("/Path")) + { + await ctx.Response.WriteAsync(ctx.Request.Path.Value); + return; + } + if (ctx.Request.Path.Value.StartsWith("/Query")) + { + await ctx.Response.WriteAsync(ctx.Request.QueryString.Value); + return; + } + + await ctx.Response.WriteAsync("Hello World"); + } + + private async Task LargeResponseBody(HttpContext ctx) + { + if (int.TryParse(ctx.Request.Query["length"], out var length)) + { + await ctx.Response.WriteAsync(new string('a', length)); + } + } + + private async Task ResponseHeaders(HttpContext ctx) + { + ctx.Response.Headers["UnknownHeader"] = "test123=foo"; + ctx.Response.ContentType = "text/plain"; + ctx.Response.Headers["MultiHeader"] = new StringValues(new string[] { "1", "2" }); + await ctx.Response.WriteAsync("Request Complete"); + } + + private async Task ResponseInvalidOrdering(HttpContext ctx) + { + if (ctx.Request.Path.Equals("/SetStatusCodeAfterWrite")) + { + await ctx.Response.WriteAsync("Started_"); + try + { + ctx.Response.StatusCode = 200; + } + catch (InvalidOperationException) + { + await ctx.Response.WriteAsync("SetStatusCodeAfterWriteThrew_"); + } + await ctx.Response.WriteAsync("Finished"); + return; + } + else if (ctx.Request.Path.Equals("/SetHeaderAfterWrite")) + { + await ctx.Response.WriteAsync("Started_"); + try + { + ctx.Response.Headers["This will fail"] = "some value"; + } + catch (InvalidOperationException) + { + await ctx.Response.WriteAsync("SetHeaderAfterWriteThrew_"); + } + await ctx.Response.WriteAsync("Finished"); + return; + } + } + + private async Task CheckEnvironmentVariable(HttpContext ctx) + { + var variable = Environment.GetEnvironmentVariable("ASPNETCORE_INPROCESS_TESTING_VALUE"); + await ctx.Response.WriteAsync(variable); + } + + private async Task CheckEnvironmentLongValueVariable(HttpContext ctx) + { + var variable = Environment.GetEnvironmentVariable("ASPNETCORE_INPROCESS_TESTING_LONG_VALUE"); + await ctx.Response.WriteAsync(variable); + } + + private async Task CheckAppendedEnvironmentVariable(HttpContext ctx) + { + var variable = Environment.GetEnvironmentVariable("ProgramFiles"); + await ctx.Response.WriteAsync(variable); + } + + private async Task CheckRemoveAuthEnvironmentVariable(HttpContext ctx) + { + var variable = Environment.GetEnvironmentVariable("ASPNETCORE_IIS_HTTPAUTH"); + await ctx.Response.WriteAsync(variable); + } + private async Task ReadAndWriteSynchronously(HttpContext ctx) + { + var t2 = Task.Run(() => WriteManyTimesToResponseBody(ctx)); + var t1 = Task.Run(() => ReadRequestBody(ctx)); + await Task.WhenAll(t1, t2); + } + + private async Task ReadRequestBody(HttpContext ctx) + { + var readBuffer = new byte[1]; + var result = await ctx.Request.Body.ReadAsync(readBuffer, 0, 1); + while (result != 0) + { + result = await ctx.Request.Body.ReadAsync(readBuffer, 0, 1); + } + } + + private async Task WriteManyTimesToResponseBody(HttpContext ctx) + { + for (var i = 0; i < 10000; i++) + { + await ctx.Response.WriteAsync("hello world"); + } + } + + private async Task ReadAndWriteEcho(HttpContext ctx) + { + var readBuffer = new byte[4096]; + var result = await ctx.Request.Body.ReadAsync(readBuffer, 0, readBuffer.Length); + while (result != 0) + { + await ctx.Response.WriteAsync(Encoding.UTF8.GetString(readBuffer, 0, result)); + result = await ctx.Request.Body.ReadAsync(readBuffer, 0, readBuffer.Length); + } + } + private async Task ReadAndFlushEcho(HttpContext ctx) + { + var readBuffer = new byte[4096]; + var result = await ctx.Request.Body.ReadAsync(readBuffer, 0, readBuffer.Length); + while (result != 0) + { + await ctx.Response.WriteAsync(Encoding.UTF8.GetString(readBuffer, 0, result)); + await ctx.Response.Body.FlushAsync(); + result = await ctx.Request.Body.ReadAsync(readBuffer, 0, readBuffer.Length); + } + } + + 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(); + + 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); + await ctx.Response.Body.FlushAsync(); + } + } + + 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]; + var count = 0; + do + { + count += await ctx.Request.Body.ReadAsync(data, count, data.Length - count); + } while (count != data.Length); + await ctx.Response.Body.WriteAsync(data, 0, data.Length); + } + + private async Task SetHeaderFromBody(HttpContext ctx) + { + using (var reader = new StreamReader(ctx.Request.Body)) + { + var value = await reader.ReadToEndAsync(); + ctx.Response.Headers["BodyAsString"] = value; + await ctx.Response.WriteAsync(value); + } + } + + private async Task ReadAndWriteEchoTwice(HttpContext ctx) + { + var readBuffer = new byte[4096]; + var result = await ctx.Request.Body.ReadAsync(readBuffer, 0, readBuffer.Length); + while (result != 0) + { + await ctx.Response.WriteAsync(Encoding.UTF8.GetString(readBuffer, 0, result)); + await ctx.Response.Body.FlushAsync(); + await ctx.Response.WriteAsync(Encoding.UTF8.GetString(readBuffer, 0, result)); + await ctx.Response.Body.FlushAsync(); + result = await ctx.Request.Body.ReadAsync(readBuffer, 0, readBuffer.Length); + } + } + + private async Task ReadAndWriteSlowConnection(HttpContext ctx) + { + var t2 = Task.Run(() => WriteResponseBodyAFewTimes(ctx)); + var t1 = Task.Run(() => ReadRequestBody(ctx)); + await Task.WhenAll(t1, t2); + } + + private async Task WriteResponseBodyAFewTimes(HttpContext ctx) + { + for (var i = 0; i < 100; i++) + { + await ctx.Response.WriteAsync("hello world"); + } + } + + private async Task ReadAndWriteCopyToAsync(HttpContext ctx) + { + await ctx.Request.Body.CopyToAsync(ctx.Response.Body); + } + + private async Task UpgradeFeatureDetection(HttpContext ctx) + { + if (ctx.Features.Get() != null) + { + await ctx.Response.WriteAsync("Enabled"); + } + else + { + await ctx.Response.WriteAsync("Disabled"); + } + } + + private async Task TestReadOffsetWorks(HttpContext ctx) + { + var buffer = new byte[11]; + ctx.Request.Body.Read(buffer, 0, 6); + ctx.Request.Body.Read(buffer, 6, 5); + + await ctx.Response.WriteAsync(Encoding.UTF8.GetString(buffer)); + } + + private async Task TestInvalidReadOperations(HttpContext ctx) + { + var success = false; + if (ctx.Request.Path.StartsWithSegments("/NullBuffer")) + { + try + { + await ctx.Request.Body.ReadAsync(null, 0, 0); + } + catch (Exception) + { + success = true; + } + } + else if (ctx.Request.Path.StartsWithSegments("/InvalidOffsetSmall")) + { + try + { + await ctx.Request.Body.ReadAsync(new byte[1], -1, 0); + } + catch (ArgumentOutOfRangeException) + { + success = true; + } + } + else if (ctx.Request.Path.StartsWithSegments("/InvalidOffsetLarge")) + { + try + { + await ctx.Request.Body.ReadAsync(new byte[1], 2, 0); + } + catch (ArgumentOutOfRangeException) + { + success = true; + } + } + else if (ctx.Request.Path.StartsWithSegments("/InvalidCountSmall")) + { + try + { + await ctx.Request.Body.ReadAsync(new byte[1], 0, -1); + } + catch (ArgumentOutOfRangeException) + { + success = true; + } + } + else if (ctx.Request.Path.StartsWithSegments("/InvalidCountLarge")) + { + try + { + await ctx.Request.Body.ReadAsync(new byte[1], 0, -1); + } + catch (ArgumentOutOfRangeException) + { + success = true; + } + } + else if (ctx.Request.Path.StartsWithSegments("/InvalidCountWithOffset")) + { + try + { + await ctx.Request.Body.ReadAsync(new byte[3], 1, 3); + } + catch (ArgumentOutOfRangeException) + { + success = true; + } + } + + + await ctx.Response.WriteAsync(success ? "Success" : "Failure"); + } + + private async Task TestValidReadOperations(HttpContext ctx) + { + var count = -1; + + if (ctx.Request.Path.StartsWithSegments("/NullBuffer")) + { + count = await ctx.Request.Body.ReadAsync(null, 0, 0); + } + else if (ctx.Request.Path.StartsWithSegments("/NullBufferPost")) + { + count = await ctx.Request.Body.ReadAsync(null, 0, 0); + } + else if (ctx.Request.Path.StartsWithSegments("/InvalidCountZeroRead")) + { + count = await ctx.Request.Body.ReadAsync(new byte[1], 0, 0); + } + else if (ctx.Request.Path.StartsWithSegments("/InvalidCountZeroReadPost")) + { + count = await ctx.Request.Body.ReadAsync(new byte[1], 0, 0); + } + + await ctx.Response.WriteAsync(count == 0 ? "Success" : "Failure"); + } + + private async Task TestInvalidWriteOperations(HttpContext ctx) + { + var success = false; + + if (ctx.Request.Path.StartsWithSegments("/InvalidOffsetSmall")) + { + try + { + await ctx.Response.Body.WriteAsync(new byte[1], -1, 0); + } + catch (ArgumentOutOfRangeException) + { + success = true; + } + } + else if (ctx.Request.Path.StartsWithSegments("/InvalidOffsetLarge")) + { + try + { + await ctx.Response.Body.WriteAsync(new byte[1], 2, 0); + } + catch (ArgumentOutOfRangeException) + { + success = true; + } + } + else if (ctx.Request.Path.StartsWithSegments("/InvalidCountSmall")) + { + try + { + await ctx.Response.Body.WriteAsync(new byte[1], 0, -1); + } + catch (ArgumentOutOfRangeException) + { + success = true; + } + } + else if (ctx.Request.Path.StartsWithSegments("/InvalidCountLarge")) + { + try + { + await ctx.Response.Body.WriteAsync(new byte[1], 0, -1); + } + catch (ArgumentOutOfRangeException) + { + success = true; + } + } + else if (ctx.Request.Path.StartsWithSegments("/InvalidCountWithOffset")) + { + try + { + await ctx.Response.Body.WriteAsync(new byte[3], 1, 3); + } + catch (ArgumentOutOfRangeException) + { + success = true; + } + } + + await ctx.Response.WriteAsync(success ? "Success" : "Failure"); + } + + private async Task TestValidWriteOperations(HttpContext ctx) + { + if (ctx.Request.Path.StartsWithSegments("/NullBuffer")) + { + await ctx.Response.Body.WriteAsync(null, 0, 0); + } + else if (ctx.Request.Path.StartsWithSegments("/NullBufferPost")) + { + await ctx.Response.Body.WriteAsync(null, 0, 0); + } + + await ctx.Response.WriteAsync("Success"); + } + + private async Task LargeResponseFile(HttpContext ctx) + { + var tempFile = Path.GetTempFileName(); + var fileContent = new string('a', 200000); + var fileStream = File.OpenWrite(tempFile); + + for (var i = 0; i < 1000; i++) + { + await fileStream.WriteAsync(Encoding.UTF8.GetBytes(fileContent), 0, fileContent.Length); + } + fileStream.Close(); + + await ctx.Response.SendFileAsync(tempFile, 0, null); + + // Try to delete the file from the temp directory. If it fails, don't report an error + // to the application. File should eventually be cleaned up from the temp directory + // by OS. + try + { + File.Delete(tempFile); + } + catch (Exception) + { + } + } + + private async Task BasePath(HttpContext ctx) + { + await ctx.Response.WriteAsync(AppDomain.CurrentDomain.BaseDirectory); + } + + private async Task Shutdown(HttpContext ctx) + { + await ctx.Response.WriteAsync("Shutting down"); + ctx.RequestServices.GetService().StopApplication(); + } + + private async Task GetServerVariableStress(HttpContext ctx) + { + // This test simulates the scenario where native Flush call is being + // 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(serverVariableFeature["REMOTE_PORT"]); + await ctx.Response.Body.FlushAsync(); + } + await ctx.Response.WriteAsync("Response End"); + } + + private async Task CommandLineArgs(HttpContext ctx) + { + 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/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessWebSite/web.config b/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessWebSite/web.config new file mode 100644 index 0000000000..2a9bd223c3 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessWebSite/web.config @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessWebSite/wwwroot/static.txt b/src/IISIntegration/src/IISIntegration/test/WebSites/InProcessWebSite/wwwroot/static.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/IISIntegration/src/IISIntegration/test/WebSites/OutOfProcessWebSite/OutOfProcessWebSite.csproj b/src/IISIntegration/src/IISIntegration/test/WebSites/OutOfProcessWebSite/OutOfProcessWebSite.csproj new file mode 100644 index 0000000000..14beb7394e --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/WebSites/OutOfProcessWebSite/OutOfProcessWebSite.csproj @@ -0,0 +1,31 @@ + + + + + + $(StandardTestTfms) + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/src/IISIntegration/test/WebSites/OutOfProcessWebSite/Program.cs b/src/IISIntegration/src/IISIntegration/test/WebSites/OutOfProcessWebSite/Program.cs new file mode 100644 index 0000000000..90895237d2 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/WebSites/OutOfProcessWebSite/Program.cs @@ -0,0 +1,45 @@ +// 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.IO; +using System.Linq; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Logging; + +namespace TestSite +{ + public static class Program + { + public static int Main(string[] args) + { + var mode = args.FirstOrDefault(); + switch (mode) + { + case "CreateFile": + File.WriteAllText(args[1], ""); + return StartServer(); + } + + return StartServer(); + } + + private static int StartServer() + { + var host = new WebHostBuilder() + .ConfigureLogging( + (_, factory) => { + factory.AddConsole(); + factory.AddFilter("Console", level => level >= LogLevel.Information); + }) + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .UseKestrel() + .Build(); + + host.Run(); + return 0; + } + } +} + diff --git a/src/IISIntegration/src/IISIntegration/test/WebSites/OutOfProcessWebSite/Properties/launchSettings.json b/src/IISIntegration/src/IISIntegration/test/WebSites/OutOfProcessWebSite/Properties/launchSettings.json new file mode 100644 index 0000000000..246b7a0b47 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/WebSites/OutOfProcessWebSite/Properties/launchSettings.json @@ -0,0 +1,42 @@ +{ + "iisSettings": { + "windowsAuthentication": true, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5762/", + "sslPort": 0 + } + }, + "profiles": { + "ANCM IIS Express": { + "commandName": "Executable", + "executablePath": "$(IISExpressPath)", + "commandLineArgs": "$(IISExpressArguments)", + "environmentVariables": { + "IIS_SITE_PATH": "$(MSBuildThisFileDirectory)", + "ANCM_PATH": "$(AspNetCoreModuleV1ShimDll)", + "ANCMV2_PATH": "$(AspNetCoreModuleV2ShimDll)", + "ANCM_OUTOFPROCESS_HANDLER": "$(AspNetCoreModuleV2OutOfProcessHandlerDll)", + "LAUNCHER_ARGS": "$(TargetPath)", + "ASPNETCORE_ENVIRONMENT": "Development", + "LAUNCHER_PATH": "$(DotNetPath)", + "ASPNETCORE_MODULE_DEBUG": "console" + } + }, + "ANCM IIS": { + "commandName": "Executable", + "executablePath": "$(IISPath)", + "commandLineArgs": "$(IISArguments)", + "environmentVariables": { + "IIS_SITE_PATH": "$(MSBuildThisFileDirectory)", + "ANCM_PATH": "$(AspNetCoreModuleV1ShimDll)", + "ANCMV2_PATH": "$(AspNetCoreModuleV2ShimDll)", + "ASPNETCORE_MODULE_OUTOFPROCESS_HANDLER": "$(AspNetCoreModuleV2OutOfProcessHandlerDll)", + "LAUNCHER_ARGS": "$(TargetPath)", + "ASPNETCORE_ENVIRONMENT": "Development", + "LAUNCHER_PATH": "$(DotNetPath)", + "ASPNETCORE_MODULE_DEBUG": "console" + } + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/WebSites/OutOfProcessWebSite/Startup.cs b/src/IISIntegration/src/IISIntegration/test/WebSites/OutOfProcessWebSite/Startup.cs new file mode 100644 index 0000000000..de54a85a8a --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/WebSites/OutOfProcessWebSite/Startup.cs @@ -0,0 +1,105 @@ +// 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.Diagnostics; +using System.IO; +using System.Linq; +using System.Security.Principal; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.IISIntegration.FunctionalTests; +using Microsoft.AspNetCore.Server.IISIntegration; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace TestSite +{ + public partial class Startup + { + private IServerAddressesFeature _serverAddresses; + + public void Configure(IApplicationBuilder app) + { + TestStartup.Register(app, this); + + _serverAddresses = app.ServerFeatures.Get(); + } + + public Task Path(HttpContext ctx) => ctx.Response.WriteAsync(ctx.Request.Path.Value); + + public Task Query(HttpContext ctx) => ctx.Response.WriteAsync(ctx.Request.QueryString.Value); + + public Task BodyLimit(HttpContext ctx) => ctx.Response.WriteAsync(ctx.Features.Get()?.MaxRequestBodySize?.ToString() ?? "null"); + + + public Task HelloWorld(HttpContext ctx) => ctx.Response.WriteAsync("Hello World"); + + public Task HttpsHelloWorld(HttpContext ctx) => + ctx.Response.WriteAsync("Scheme:" + ctx.Request.Scheme + "; Original:" + ctx.Request.Headers["x-original-proto"]); + + public Task Anonymous(HttpContext context) => context.Response.WriteAsync("Anonymous?" + !context.User.Identity.IsAuthenticated); + + public Task Restricted(HttpContext context) + { + if (context.User.Identity.IsAuthenticated) + { + Assert.IsType(context.User); + return context.Response.WriteAsync(context.User.Identity.AuthenticationType); + } + else + { + return context.ChallengeAsync(IISDefaults.AuthenticationScheme); + } + } + + public Task Forbidden(HttpContext context) => context.ForbidAsync(IISDefaults.AuthenticationScheme); + + public Task RestrictedNTLM(HttpContext context) + { + if (string.Equals("NTLM", context.User.Identity.AuthenticationType, StringComparison.Ordinal)) + { + return context.Response.WriteAsync("NTLM"); + } + else + { + return context.ChallengeAsync(IISDefaults.AuthenticationScheme); + } + } + + public Task UpgradeFeatureDetection(HttpContext context) => + context.Response.WriteAsync(context.Features.Get() != null? "Enabled": "Disabled"); + + public Task CheckRequestHandlerVersion(HttpContext context) + { + // We need to check if the aspnetcorev2_outofprocess dll is loaded by iisexpress.exe + // As they aren't in the same process, we will try to delete the file and expect a file + // in use error + try + { + File.Delete(context.Request.Headers["ANCMRHPath"]); + } + catch(UnauthorizedAccessException) + { + // TODO calling delete on the file will succeed when running with IIS + return context.Response.WriteAsync("Hello World"); + } + + return context.Response.WriteAsync(context.Request.Headers["ANCMRHPath"]); + } + + private async Task ProcessId(HttpContext context) + { + await context.Response.WriteAsync(Process.GetCurrentProcess().Id.ToString()); + } + + private async Task ServerAddresses(HttpContext context) + { + await context.Response.WriteAsync(string.Join(",", _serverAddresses.Addresses)); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/WebSites/OutOfProcessWebSite/wwwroot/static.txt b/src/IISIntegration/src/IISIntegration/test/WebSites/OutOfProcessWebSite/wwwroot/static.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/IISIntegration/src/IISIntegration/test/WebSites/StressTestWebSite/Program.cs b/src/IISIntegration/src/IISIntegration/test/WebSites/StressTestWebSite/Program.cs new file mode 100644 index 0000000000..e8e5392c2c --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/WebSites/StressTestWebSite/Program.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Logging; + +namespace ANCMStressTestApp +{ + public class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .ConfigureLogging((_, factory) => + { + factory.AddConsole(); + }) + .UseKestrel() + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/WebSites/StressTestWebSite/Properties/launchSettings.json b/src/IISIntegration/src/IISIntegration/test/WebSites/StressTestWebSite/Properties/launchSettings.json new file mode 100644 index 0000000000..246b7a0b47 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/WebSites/StressTestWebSite/Properties/launchSettings.json @@ -0,0 +1,42 @@ +{ + "iisSettings": { + "windowsAuthentication": true, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5762/", + "sslPort": 0 + } + }, + "profiles": { + "ANCM IIS Express": { + "commandName": "Executable", + "executablePath": "$(IISExpressPath)", + "commandLineArgs": "$(IISExpressArguments)", + "environmentVariables": { + "IIS_SITE_PATH": "$(MSBuildThisFileDirectory)", + "ANCM_PATH": "$(AspNetCoreModuleV1ShimDll)", + "ANCMV2_PATH": "$(AspNetCoreModuleV2ShimDll)", + "ANCM_OUTOFPROCESS_HANDLER": "$(AspNetCoreModuleV2OutOfProcessHandlerDll)", + "LAUNCHER_ARGS": "$(TargetPath)", + "ASPNETCORE_ENVIRONMENT": "Development", + "LAUNCHER_PATH": "$(DotNetPath)", + "ASPNETCORE_MODULE_DEBUG": "console" + } + }, + "ANCM IIS": { + "commandName": "Executable", + "executablePath": "$(IISPath)", + "commandLineArgs": "$(IISArguments)", + "environmentVariables": { + "IIS_SITE_PATH": "$(MSBuildThisFileDirectory)", + "ANCM_PATH": "$(AspNetCoreModuleV1ShimDll)", + "ANCMV2_PATH": "$(AspNetCoreModuleV2ShimDll)", + "ASPNETCORE_MODULE_OUTOFPROCESS_HANDLER": "$(AspNetCoreModuleV2OutOfProcessHandlerDll)", + "LAUNCHER_ARGS": "$(TargetPath)", + "ASPNETCORE_ENVIRONMENT": "Development", + "LAUNCHER_PATH": "$(DotNetPath)", + "ASPNETCORE_MODULE_DEBUG": "console" + } + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/WebSites/StressTestWebSite/Startup.cs b/src/IISIntegration/src/IISIntegration/test/WebSites/StressTestWebSite/Startup.cs new file mode 100644 index 0000000000..68f6eb1f77 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/WebSites/StressTestWebSite/Startup.cs @@ -0,0 +1,232 @@ +// 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.Threading.Tasks; +using System.Threading; +using System.Text; +using System.Net.WebSockets; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Net.Http.Headers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; + +namespace ANCMStressTestApp +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + } + + public void Configure(IApplicationBuilder app) + { + app.Map("/HelloWorld", HelloWorld); + app.Map("/ConnectionClose", ConnectionClose); + app.Map("/EchoPostData", EchoPostData); + app.Map("/LargeResponseBody", LargeResponseBody); + app.Map("/ResponseHeaders", ResponseHeaders); + app.Map("/EnvironmentVariables", EnvironmentVariables); + app.Map("/RequestInformation", RequestInformation); + app.Map("/WebSocket", WebSocket); + + app.Run(async context => + { + await context.Response.WriteAsync("Default Page"); + }); + } + + private void HelloWorld(IApplicationBuilder app) + { + app.Run(async context => + { + await context.Response.WriteAsync("Hello World"); + }); + } + + private void ConnectionClose(IApplicationBuilder app) + { + app.Run(async context => + { + context.Response.Headers[HeaderNames.Connection] = "close"; + await context.Response.WriteAsync("Connnection Close"); + await context.Response.Body.FlushAsync(); + }); + } + + private void EchoPostData(IApplicationBuilder app) + { + app.Run(async context => + { + string responseBody = string.Empty; + + if (string.Equals(context.Request.Method, "POST", StringComparison.OrdinalIgnoreCase)) + { + using (StreamReader reader = new StreamReader(context.Request.Body, Encoding.UTF8)) + { + responseBody = await reader.ReadToEndAsync(); + } + } + else + { + responseBody = "NoAction"; + } + + await context.Response.WriteAsync(responseBody); + }); + } + + private void LargeResponseBody(IApplicationBuilder app) + { + app.Run(async context => + { + if (int.TryParse(context.Request.Query["length"], out var length)) + { + await context.Response.WriteAsync(new string('a', length)); + } + }); + } + + private void ResponseHeaders(IApplicationBuilder app) + { + app.Run(async context => + { + context.Response.Headers["UnknownHeader"] = "test123=foo"; + context.Response.ContentType = "text/plain"; + context.Response.Headers["MultiHeader"] = new StringValues(new string[] { "1", "2" }); + await context.Response.WriteAsync("Request Complete"); + }); + } + + private void EnvironmentVariables(IApplicationBuilder app) + { + app.Run(async context => + { + context.Response.ContentType = "text/plain"; + await context.Response.WriteAsync("Environment Variables:" + Environment.NewLine); + var vars = Environment.GetEnvironmentVariables(); + foreach (var key in vars.Keys.Cast().OrderBy(key => key, StringComparer.OrdinalIgnoreCase)) + { + var value = vars[key]; + await context.Response.WriteAsync(key + ": " + value + Environment.NewLine); + } + await context.Response.WriteAsync(Environment.NewLine); + }); + } + + private void RequestInformation(IApplicationBuilder app) + { + app.Run(async context => + { + context.Response.ContentType = "text/plain"; + + await context.Response.WriteAsync("Address:" + Environment.NewLine); + await context.Response.WriteAsync("Scheme: " + context.Request.Scheme + Environment.NewLine); + await context.Response.WriteAsync("Host: " + context.Request.Headers["Host"] + Environment.NewLine); + await context.Response.WriteAsync("PathBase: " + context.Request.PathBase.Value + Environment.NewLine); + await context.Response.WriteAsync("Path: " + context.Request.Path.Value + Environment.NewLine); + await context.Response.WriteAsync("Query: " + context.Request.QueryString.Value + Environment.NewLine); + await context.Response.WriteAsync(Environment.NewLine); + + await context.Response.WriteAsync("Connection:" + Environment.NewLine); + await context.Response.WriteAsync("RemoteIp: " + context.Connection.RemoteIpAddress + Environment.NewLine); + await context.Response.WriteAsync("RemotePort: " + context.Connection.RemotePort + Environment.NewLine); + await context.Response.WriteAsync("LocalIp: " + context.Connection.LocalIpAddress + Environment.NewLine); + await context.Response.WriteAsync("LocalPort: " + context.Connection.LocalPort + Environment.NewLine); + await context.Response.WriteAsync(Environment.NewLine); + + await context.Response.WriteAsync("Headers:" + Environment.NewLine); + foreach (var header in context.Request.Headers) + { + await context.Response.WriteAsync(header.Key + ": " + header.Value + Environment.NewLine); + } + await context.Response.WriteAsync(Environment.NewLine); + }); + } + + private void WebSocket(IApplicationBuilder app) + { + app.Run(async context => + { + var upgradeFeature = context.Features.Get(); + + // Generate WebSocket response headers + string key = context.Request.Headers[Constants.Headers.SecWebSocketKey].ToString(); + var responseHeaders = HandshakeHelpers.GenerateResponseHeaders(key); + foreach (var headerPair in responseHeaders) + { + context.Response.Headers[headerPair.Key] = headerPair.Value; + } + + // Upgrade the connection + Stream opaqueTransport = await upgradeFeature.UpgradeAsync(); + + // Get the WebSocket object + var ws = WebSocketProtocol.CreateFromStream(opaqueTransport, isServer: true, subProtocol: null, keepAliveInterval: TimeSpan.FromMinutes(2)); + + var appLifetime = app.ApplicationServices.GetRequiredService(); + + await Echo(ws, appLifetime.ApplicationStopping); + }); + } + + private async Task Echo(WebSocket webSocket, CancellationToken token) + { + try + { + var buffer = new byte[1024 * 4]; + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), token); + bool closeFromServer = false; + string closeFromServerCmd = "CloseFromServer"; + int closeFromServerLength = closeFromServerCmd.Length; + + while (!result.CloseStatus.HasValue && !token.IsCancellationRequested && !closeFromServer) + { + if (result.Count == closeFromServerLength && + Encoding.ASCII.GetString(buffer).Substring(0, result.Count) == closeFromServerCmd) + { + // The client sent "CloseFromServer" text message to request the server to close (a test scenario). + closeFromServer = true; + } + else + { + await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, token); + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), token); + } + } + + if (result.CloseStatus.HasValue) + { + // Client-initiated close handshake + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + } + else + { + // Server-initiated close handshake due to either of the two conditions: + // (1) The applicaton host is performing a graceful shutdown. + // (2) The client sent "CloseFromServer" text message to request the server to close (a test scenario). + await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, closeFromServerCmd, CancellationToken.None); + + // The server has sent the Close frame. + // Stop sending but keep receiving until we get the Close frame from the client. + while (!result.CloseStatus.HasValue) + { + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + } + } + } + catch (Exception e) + { + Console.WriteLine("{0} Exception caught!", e); + } + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/WebSites/StressTestWebSite/StressTestWebSite.csproj b/src/IISIntegration/src/IISIntegration/test/WebSites/StressTestWebSite/StressTestWebSite.csproj new file mode 100644 index 0000000000..25ae032221 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/WebSites/StressTestWebSite/StressTestWebSite.csproj @@ -0,0 +1,25 @@ + + + + + + $(StandardTestTfms) + true + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/src/IISIntegration/test/WebSites/shared/SharedStartup/Startup.shared.cs b/src/IISIntegration/src/IISIntegration/test/WebSites/shared/SharedStartup/Startup.shared.cs new file mode 100644 index 0000000000..b5d7f0305f --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/WebSites/shared/SharedStartup/Startup.shared.cs @@ -0,0 +1,117 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace TestSite +{ + public partial class Startup + { + public void ConfigureServices(IServiceCollection serviceCollection) + { + serviceCollection.AddResponseCompression(); + } + + private async Task HostingEnvironment(HttpContext ctx) + { + var hostingEnv = ctx.RequestServices.GetService(); + + await ctx.Response.WriteAsync("ContentRootPath " + hostingEnv.ContentRootPath + Environment.NewLine); + await ctx.Response.WriteAsync("WebRootPath " + hostingEnv.WebRootPath + Environment.NewLine); + await ctx.Response.WriteAsync("CurrentDirectory " + Environment.CurrentDirectory); + } + + private async Task ConsoleWrite(HttpContext ctx) + { + Console.WriteLine("TEST MESSAGE"); + + await ctx.Response.WriteAsync("Hello World"); + } + + private async Task ConsoleErrorWrite(HttpContext ctx) + { + Console.Error.WriteLine("TEST MESSAGE"); + + 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"); + if (ctx.User.Identity.Name != null) + { + await ctx.Response.WriteAsync(":" + ctx.User.Identity.Name); + } + } + + public async Task GetClientCert(HttpContext context) + { + var clientCert = context.Connection.ClientCertificate; + await context.Response.WriteAsync(clientCert != null ? $"Enabled;{clientCert.GetCertHashString()}" : "Disabled"); + } + + private static int _waitingRequestCount; + + public Task WaitForAbort(HttpContext context) + { + Interlocked.Increment(ref _waitingRequestCount); + try + { + context.RequestAborted.WaitHandle.WaitOne(); + return Task.CompletedTask; + } + finally + { + Interlocked.Decrement(ref _waitingRequestCount); + } + } + + public Task Abort(HttpContext context) + { + context.Abort(); + return Task.CompletedTask; + } + + public async Task WaitingRequestCount(HttpContext context) + { + await context.Response.WriteAsync(_waitingRequestCount.ToString()); + } + + public Task CreateFile(HttpContext context) + { + var hostingEnv = context.RequestServices.GetService(); + File.WriteAllText(System.IO.Path.Combine(hostingEnv.ContentRootPath, "Started.txt"), ""); + return Task.CompletedTask; + } + + public Task OverrideServer(HttpContext context) + { + context.Response.Headers["Server"] = "MyServer/7.8"; + return Task.CompletedTask; + } + + public void CompressedData(IApplicationBuilder builder) + { + builder.UseResponseCompression(); + // write random bytes to check that compressed data is passed through + builder.Run( + async context => + { + context.Response.ContentType = "text/html"; + await context.Response.Body.WriteAsync(new byte[100], 0, 100); + }); + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/WebSites/shared/WebSockets/Constants.cs b/src/IISIntegration/src/IISIntegration/test/WebSites/shared/WebSockets/Constants.cs new file mode 100644 index 0000000000..95b53003cc --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/WebSites/shared/WebSockets/Constants.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests +{ + public static class Constants + { + public static class Headers + { + public const string Upgrade = "Upgrade"; + public const string UpgradeWebSocket = "websocket"; + public const string Connection = "Connection"; + public const string SecWebSocketKey = "Sec-WebSocket-Key"; + public const string SecWebSocketAccept = "Sec-WebSocket-Accept"; + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/WebSites/shared/WebSockets/HandshakeHelpers.cs b/src/IISIntegration/src/IISIntegration/test/WebSites/shared/WebSockets/HandshakeHelpers.cs new file mode 100644 index 0000000000..b079ea3f85 --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/WebSites/shared/WebSockets/HandshakeHelpers.cs @@ -0,0 +1,41 @@ +// 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.Security.Cryptography; +using System.Text; + +namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests +{ + internal static class HandshakeHelpers + { + public static IEnumerable> GenerateResponseHeaders(string key) + { + yield return new KeyValuePair(Constants.Headers.Connection, Constants.Headers.Upgrade); + yield return new KeyValuePair(Constants.Headers.Upgrade, Constants.Headers.UpgradeWebSocket); + yield return new KeyValuePair(Constants.Headers.SecWebSocketAccept, CreateResponseKey(key)); + } + + public static string CreateResponseKey(string requestKey) + { + // "The value of this header field is constructed by concatenating /key/, defined above in step 4 + // in Section 4.2.2, with the string "258EAFA5- E914-47DA-95CA-C5AB0DC85B11", taking the SHA-1 hash of + // this concatenated value to obtain a 20-byte value and base64-encoding" + // https://tools.ietf.org/html/rfc6455#section-4.2.2 + + if (requestKey == null) + { + throw new ArgumentNullException(nameof(requestKey)); + } + + using (var algorithm = SHA1.Create()) + { + string merged = requestKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + byte[] mergedBytes = Encoding.UTF8.GetBytes(merged); + byte[] hashedBytes = algorithm.ComputeHash(mergedBytes); + return Convert.ToBase64String(hashedBytes); + } + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/WebSites/shared/WebSockets/TestStartup.cs b/src/IISIntegration/src/IISIntegration/test/WebSites/shared/WebSockets/TestStartup.cs new file mode 100644 index 0000000000..b1604e367a --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/WebSites/shared/WebSockets/TestStartup.cs @@ -0,0 +1,38 @@ +using System; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.IISIntegration.FunctionalTests +{ + public static class TestStartup + { + public static void Register(IApplicationBuilder app, object startup) + { + var type = startup.GetType(); + foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + { + var parameters = method.GetParameters(); + if (method.Name != "Configure" && + parameters.Length == 1) + { + Action appfunc = null; + if (parameters[0].ParameterType == typeof(IApplicationBuilder)) + { + appfunc = innerAppBuilder => method.Invoke(startup, new[] { innerAppBuilder }); + } + else if (parameters[0].ParameterType == typeof(HttpContext)) + { + appfunc = innerAppBuilder => innerAppBuilder.Run(ctx => (Task)method.Invoke(startup, new[] { ctx })); + } + + if (appfunc != null) + { + app.Map("/" + method.Name, appfunc); + } + } + } + } + } +} diff --git a/src/IISIntegration/src/IISIntegration/test/gtest/gtest.vcxproj b/src/IISIntegration/src/IISIntegration/test/gtest/gtest.vcxproj new file mode 100644 index 0000000000..924f26337d --- /dev/null +++ b/src/IISIntegration/src/IISIntegration/test/gtest/gtest.vcxproj @@ -0,0 +1,180 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + + + + + 15.0 + {CAC1267B-8778-4257-AAC6-CAF481723B01} + Win32Proj + gtest + 10.0.15063.0 + + + + StaticLibrary + true + v141 + Unicode + gtestd + + + StaticLibrary + false + v141 + true + Unicode + gtest + + + StaticLibrary + true + v141 + Unicode + gtestd + + + StaticLibrary + false + v141 + true + Unicode + gtest + + + + + + + + + + + + + + + + + + + + + true + $(VC_IncludePath);$(WindowsSDK_IncludePath); + $(VC_SourcePath); + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + true + $(VC_IncludePath);$(WindowsSDK_IncludePath); + $(VC_SourcePath); + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + false + $(VC_IncludePath);$(WindowsSDK_IncludePath); + $(VC_SourcePath); + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + false + $(VC_IncludePath);$(WindowsSDK_IncludePath); + $(VC_SourcePath); + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + + NotUsing + Level3 + Disabled + true + WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + true + googletest\googletest\include;googletest\googletest;googletest\googlemock;googletest\googlemock\include;%(AdditionalIncludeDirectories) + MultiThreadedDebug + + + Windows + true + + + + + NotUsing + Level3 + Disabled + true + _DEBUG;_LIB;%(PreprocessorDefinitions) + true + googletest\googletest\include;googletest\googletest;googletest\googlemock;googletest\googlemock\include;%(AdditionalIncludeDirectories) + MultiThreadedDebug + + + Windows + true + + + + + NotUsing + Level3 + MaxSpeed + true + true + true + WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + true + googletest\googletest\include;googletest\googletest;googletest\googlemock;googletest\googlemock\include;%(AdditionalIncludeDirectories) + MultiThreaded + + + Windows + true + true + true + + + + + NotUsing + Level3 + MaxSpeed + true + true + true + NDEBUG;_LIB;%(PreprocessorDefinitions) + true + googletest\googletest\include;googletest\googletest;googletest\googlemock;googletest\googlemock\include;%(AdditionalIncludeDirectories) + MultiThreaded + + + Windows + true + true + true + + + + + + \ No newline at end of file diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/AssemblyInfo.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/AssemblyInfo.cs new file mode 100644 index 0000000000..f08839f842 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/AssemblyInfo.cs @@ -0,0 +1,7 @@ +// 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.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.IISIntegration.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] + diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/DuplexStream.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/DuplexStream.cs new file mode 100644 index 0000000000..8ff01c778f --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/DuplexStream.cs @@ -0,0 +1,68 @@ +// 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.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + // See https://github.com/aspnet/IISIntegration/issues/426 + internal class DuplexStream : Stream + { + private Stream _requestBody; + private Stream _responseBody; + + public DuplexStream(Stream requestBody, Stream responseBody) + { + _requestBody = requestBody; + _responseBody = responseBody; + } + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => true; + + public override long Length => throw new NotSupportedException(); + + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + + public override void Flush() + { + _responseBody.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _requestBody.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + _responseBody.Write(buffer, offset, count); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _requestBody.ReadAsync(buffer, offset, count, cancellationToken); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _responseBody.WriteAsync(buffer, offset, count, cancellationToken); + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/EmptyStream.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/EmptyStream.cs new file mode 100644 index 0000000000..dfc804df42 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/EmptyStream.cs @@ -0,0 +1,94 @@ +// 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.Runtime.ExceptionServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal class EmptyStream : ReadOnlyStream + { + private readonly IHttpBodyControlFeature _bodyControl; + private HttpStreamState _state; + private Exception _error; + + public EmptyStream(IHttpBodyControlFeature bodyControl) + { + _bodyControl = bodyControl; + _state = HttpStreamState.Open; + } + + public override void Flush() + { + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (!_bodyControl.AllowSynchronousIO) + { + throw new InvalidOperationException(CoreStrings.SynchronousReadsDisallowed); + } + + return 0; + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ValidateState(cancellationToken); + + return Task.FromResult(0); + } + + public void StopAcceptingReads() + { + // Can't use dispose (or close) as can be disposed too early by user code + // As exampled in EngineTests.ZeroContentLengthNotSetAutomaticallyForCertainStatusCodes + _state = HttpStreamState.Closed; + } + + public void Abort(Exception error = null) + { + // We don't want to throw an ODE until the app func actually completes. + // If the request is aborted, we throw a TaskCanceledException instead, + // unless error is not null, in which case we throw it. + if (_state != HttpStreamState.Closed) + { + _state = HttpStreamState.Aborted; + _error = error; + } + } + + private void ValidateState(CancellationToken cancellationToken) + { + switch (_state) + { + case HttpStreamState.Open: + if (cancellationToken.IsCancellationRequested) + { + cancellationToken.ThrowIfCancellationRequested(); + } + break; + case HttpStreamState.Closed: + throw new ObjectDisposedException(nameof(HttpRequestStream)); + case HttpStreamState.Aborted: + if (_error != null) + { + ExceptionDispatchInfo.Capture(_error).Throw(); + } + else + { + throw new TaskCanceledException(); + } + break; + } + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/HttpRequestStream.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/HttpRequestStream.cs new file mode 100644 index 0000000000..8a9d37b146 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/HttpRequestStream.cs @@ -0,0 +1,160 @@ +// 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.Runtime.ExceptionServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal class HttpRequestStream : ReadOnlyStream + { + private readonly IHttpBodyControlFeature _bodyControl; + private IISHttpContext _body; + private HttpStreamState _state; + private Exception _error; + + public HttpRequestStream(IHttpBodyControlFeature bodyControl) + { + _bodyControl = bodyControl; + _state = HttpStreamState.Closed; + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (!_bodyControl.AllowSynchronousIO) + { + throw new InvalidOperationException(CoreStrings.SynchronousReadsDisallowed); + } + + return ReadAsync(buffer, offset, count).GetAwaiter().GetResult(); + } + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + var task = ReadAsync(buffer, offset, count, default(CancellationToken), state); + if (callback != null) + { + task.ContinueWith(t => callback.Invoke(t)); + } + return task; + } + + public override int EndRead(IAsyncResult asyncResult) + { + return ((Task)asyncResult).GetAwaiter().GetResult(); + } + + private Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state) + { + var tcs = new TaskCompletionSource(state); + var task = ReadAsync(buffer, offset, count, cancellationToken); + task.ContinueWith((task2, state2) => + { + var tcs2 = (TaskCompletionSource)state2; + if (task2.IsCanceled) + { + tcs2.SetCanceled(); + } + else if (task2.IsFaulted) + { + tcs2.SetException(task2.Exception); + } + else + { + tcs2.SetResult(task2.Result); + } + }, tcs, cancellationToken); + return tcs.Task; + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ValidateState(cancellationToken); + + return ReadAsyncInternal(new Memory(buffer, offset, count), cancellationToken).AsTask(); + } + +#if NETCOREAPP2_1 + public override ValueTask ReadAsync(Memory destination, CancellationToken cancellationToken = default) + { + ValidateState(cancellationToken); + + return ReadAsyncInternal(destination, cancellationToken); + } +#elif NETSTANDARD2_0 +#else +#error TFMs need to be updated +#endif + + private async ValueTask ReadAsyncInternal(Memory buffer, CancellationToken cancellationToken) + { + try + { + return await _body.ReadAsync(buffer, cancellationToken); + } + catch (ConnectionAbortedException ex) + { + throw new TaskCanceledException("The request was aborted", ex); + } + } + + public void StartAcceptingReads(IISHttpContext body) + { + // Only start if not aborted + if (_state == HttpStreamState.Closed) + { + _state = HttpStreamState.Open; + _body = body; + } + } + + public void StopAcceptingReads() + { + // Can't use dispose (or close) as can be disposed too early by user code + // As exampled in EngineTests.ZeroContentLengthNotSetAutomaticallyForCertainStatusCodes + _state = HttpStreamState.Closed; + _body = null; + } + + public void Abort(Exception error = null) + { + // We don't want to throw an ODE until the app func actually completes. + // If the request is aborted, we throw a TaskCanceledException instead, + // unless error is not null, in which case we throw it. + if (_state != HttpStreamState.Closed) + { + _state = HttpStreamState.Aborted; + _error = error; + } + } + + private void ValidateState(CancellationToken cancellationToken) + { + switch (_state) + { + case HttpStreamState.Open: + if (cancellationToken.IsCancellationRequested) + { + cancellationToken.ThrowIfCancellationRequested(); + } + break; + case HttpStreamState.Closed: + throw new ObjectDisposedException(nameof(HttpRequestStream)); + case HttpStreamState.Aborted: + if (_error != null) + { + ExceptionDispatchInfo.Capture(_error).Throw(); + } + else + { + throw new TaskCanceledException(); + } + break; + } + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/HttpResponseStream.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/HttpResponseStream.cs new file mode 100644 index 0000000000..1a04535448 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/HttpResponseStream.cs @@ -0,0 +1,151 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal class HttpResponseStream : WriteOnlyStream + { + private readonly IHttpBodyControlFeature _bodyControl; + private readonly IISHttpContext _context; + private HttpStreamState _state; + + public HttpResponseStream(IHttpBodyControlFeature bodyControl, IISHttpContext context) + { + _bodyControl = bodyControl; + _context = context; + _state = HttpStreamState.Closed; + } + + public override void Flush() + { + FlushAsync(default(CancellationToken)).GetAwaiter().GetResult(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + ValidateState(cancellationToken); + + return _context.FlushAsync(cancellationToken); + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (!_bodyControl.AllowSynchronousIO) + { + throw new InvalidOperationException(CoreStrings.SynchronousWritesDisallowed); + } + + WriteAsync(buffer, offset, count, default(CancellationToken)).GetAwaiter().GetResult(); + } + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + var task = WriteAsync(buffer, offset, count, default(CancellationToken), state); + if (callback != null) + { + task.ContinueWith(t => callback.Invoke(t)); + } + return task; + } + + public override void EndWrite(IAsyncResult asyncResult) + { + ((Task)asyncResult).GetAwaiter().GetResult(); + } + + private Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state) + { + var tcs = new TaskCompletionSource(state); + var task = WriteAsync(buffer, offset, count, cancellationToken); + task.ContinueWith((task2, state2) => + { + var tcs2 = (TaskCompletionSource)state2; + if (task2.IsCanceled) + { + tcs2.SetCanceled(); + } + else if (task2.IsFaulted) + { + tcs2.SetException(task2.Exception); + } + else + { + tcs2.SetResult(null); + } + }, tcs, cancellationToken); + return tcs.Task; + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ValidateState(cancellationToken); + + return _context.WriteAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken); + } + +#if NETCOREAPP2_1 + public override ValueTask WriteAsync(ReadOnlyMemory source, CancellationToken cancellationToken = default) + { + ValidateState(cancellationToken); + + return new ValueTask(_httpResponseControl.WriteAsync(source, cancellationToken)); + } +#elif NETSTANDARD2_0 +#else +#error TFMs need to be updated +#endif + + public void StartAcceptingWrites() + { + // Only start if not aborted + if (_state == HttpStreamState.Closed) + { + _state = HttpStreamState.Open; + } + } + + public void StopAcceptingWrites() + { + // Can't use dispose (or close) as can be disposed too early by user code + // As exampled in EngineTests.ZeroContentLengthNotSetAutomaticallyForCertainStatusCodes + _state = HttpStreamState.Closed; + } + + public void Abort() + { + // We don't want to throw an ODE until the app func actually completes. + if (_state != HttpStreamState.Closed) + { + _state = HttpStreamState.Aborted; + } + } + + private void ValidateState(CancellationToken cancellationToken) + { + switch (_state) + { + case HttpStreamState.Open: + if (cancellationToken.IsCancellationRequested) + { + cancellationToken.ThrowIfCancellationRequested(); + } + break; + case HttpStreamState.Closed: + throw new ObjectDisposedException(nameof(HttpResponseStream), CoreStrings.WritingToResponseBodyAfterResponseCompleted); + case HttpStreamState.Aborted: + if (cancellationToken.IsCancellationRequested) + { + // Aborted state only throws on write if cancellationToken requests it + cancellationToken.ThrowIfCancellationRequested(); + } + break; + } + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/HttpStreamState.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/HttpStreamState.cs new file mode 100644 index 0000000000..231db99409 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/HttpStreamState.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + enum HttpStreamState + { + Open, + Closed, + Aborted + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/HttpUpgradeStream.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/HttpUpgradeStream.cs new file mode 100644 index 0000000000..c8b481f948 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/HttpUpgradeStream.cs @@ -0,0 +1,208 @@ +// 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.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal class HttpUpgradeStream : Stream + { + private readonly Stream _requestStream; + private readonly Stream _responseStream; + + public HttpUpgradeStream(Stream requestStream, Stream responseStream) + { + _requestStream = requestStream; + _responseStream = responseStream; + } + + public override bool CanRead + { + get + { + return _requestStream.CanRead; + } + } + + public override bool CanSeek + { + get + { + return _requestStream.CanSeek; + } + } + + public override bool CanTimeout + { + get + { + return _responseStream.CanTimeout || _requestStream.CanTimeout; + } + } + + public override bool CanWrite + { + get + { + return _responseStream.CanWrite; + } + } + + public override long Length + { + get + { + return _requestStream.Length; + } + } + + public override long Position + { + get + { + return _requestStream.Position; + } + set + { + _requestStream.Position = value; + } + } + + public override int ReadTimeout + { + get + { + return _requestStream.ReadTimeout; + } + set + { + _requestStream.ReadTimeout = value; + } + } + + public override int WriteTimeout + { + get + { + return _responseStream.WriteTimeout; + } + set + { + _responseStream.WriteTimeout = value; + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _requestStream.Dispose(); + _responseStream.Dispose(); + } + } + + public override void Flush() + { + _responseStream.Flush(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _responseStream.FlushAsync(cancellationToken); + } + + public override void Close() + { + _requestStream.Close(); + _responseStream.Close(); + } + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + return _requestStream.BeginRead(buffer, offset, count, callback, state); + } + + public override int EndRead(IAsyncResult asyncResult) + { + return _requestStream.EndRead(asyncResult); + } + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + return _responseStream.BeginWrite(buffer, offset, count, callback, state); + } + + public override void EndWrite(IAsyncResult asyncResult) + { + _responseStream.EndWrite(asyncResult); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _requestStream.ReadAsync(buffer, offset, count, cancellationToken); + } + +#if NETCOREAPP2_1 + public override ValueTask ReadAsync(Memory destination, CancellationToken cancellationToken = default) + { + return _requestStream.ReadAsync(destination, cancellationToken); + } +#elif NETSTANDARD2_0 +#else +#error TFMs need to be updated +#endif + + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + return _requestStream.CopyToAsync(destination, bufferSize, cancellationToken); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _responseStream.WriteAsync(buffer, offset, count, cancellationToken); + } + +#if NETCOREAPP2_1 + public override ValueTask WriteAsync(ReadOnlyMemory source, CancellationToken cancellationToken = default) + { + return _responseStream.WriteAsync(source, cancellationToken); + } +#elif NETSTANDARD2_0 +#else +#error TFMs need to be updated +#endif + + public override long Seek(long offset, SeekOrigin origin) + { + return _requestStream.Seek(offset, origin); + } + + public override void SetLength(long value) + { + _requestStream.SetLength(value); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _requestStream.Read(buffer, offset, count); + } + + public override int ReadByte() + { + return _requestStream.ReadByte(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + _responseStream.Write(buffer, offset, count); + } + + public override void WriteByte(byte value) + { + _responseStream.WriteByte(value); + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISConfigurationData.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISConfigurationData.cs new file mode 100644 index 0000000000..5c55267756 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISConfigurationData.cs @@ -0,0 +1,21 @@ +// 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.Runtime.InteropServices; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + [StructLayout(LayoutKind.Sequential)] + internal struct IISConfigurationData + { + public IntPtr pNativeApplication; + [MarshalAs(UnmanagedType.BStr)] + public string pwzFullApplicationPath; + [MarshalAs(UnmanagedType.BStr)] + public string pwzVirtualApplicationPath; + public bool fWindowsAuthEnabled; + public bool fBasicAuthEnabled; + public bool fAnonymousAuthEnable; + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.FeatureCollection.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.FeatureCollection.cs new file mode 100644 index 0000000000..6e1656a487 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.FeatureCollection.cs @@ -0,0 +1,343 @@ +// 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; +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; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Features.Authentication; +using Microsoft.AspNetCore.Server.IIS.Core.IO; +using Microsoft.AspNetCore.WebUtilities; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal partial class IISHttpContext : IFeatureCollection, + IHttpRequestFeature, + IHttpResponseFeature, + IHttpUpgradeFeature, + IHttpRequestLifetimeFeature, + IHttpAuthenticationFeature, + IServerVariablesFeature, + IHttpBufferingFeature, + ITlsConnectionFeature, + IHttpBodyControlFeature + { + // 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() + { + Initialize(); + MaybeExtra?.Clear(); + _featureRevision++; + } + + private object ExtraFeatureGet(Type key) + { + if (MaybeExtra == null) + { + return null; + } + for (var i = 0; i < MaybeExtra.Count; i++) + { + var kv = MaybeExtra[i]; + if (kv.Key == key) + { + return kv.Value; + } + } + return null; + } + + private void ExtraFeatureSet(Type key, object value) + { + if (MaybeExtra == null) + { + MaybeExtra = new List>(2); + } + + for (var i = 0; i < MaybeExtra.Count; i++) + { + if (MaybeExtra[i].Key == key) + { + MaybeExtra[i] = new KeyValuePair(key, value); + return; + } + } + MaybeExtra.Add(new KeyValuePair(key, value)); + } + + string IHttpRequestFeature.Protocol + { + get + { + if (_httpProtocolVersion == null) + { + var protocol = HttpVersion; + if (protocol.Major == 1 && protocol.Minor == 1) + { + _httpProtocolVersion = "HTTP/1.1"; + } + else if (protocol.Major == 1 && protocol.Minor == 0) + { + _httpProtocolVersion = "HTTP/1.0"; + } + else + { + _httpProtocolVersion = "HTTP/" + protocol.ToString(2); + } + } + return _httpProtocolVersion; + } + set + { + _httpProtocolVersion = value; + } + } + + string IHttpRequestFeature.Scheme + { + get => Scheme; + set => Scheme = value; + } + + string IHttpRequestFeature.Method + { + get => Method; + set => Method = value; + } + + string IHttpRequestFeature.PathBase + { + get => PathBase; + set => PathBase = value; + } + + string IHttpRequestFeature.Path + { + get => Path; + set => Path = value; + } + + string IHttpRequestFeature.QueryString + { + get => QueryString; + set => QueryString = value; + } + + string IHttpRequestFeature.RawTarget + { + get => RawTarget; + set => RawTarget = value; + } + + IHeaderDictionary IHttpRequestFeature.Headers + { + get => RequestHeaders; + set => RequestHeaders = value; + } + + Stream IHttpRequestFeature.Body + { + get => RequestBody; + set => RequestBody = value; + } + + int IHttpResponseFeature.StatusCode + { + get => StatusCode; + set => StatusCode = value; + } + + string IHttpResponseFeature.ReasonPhrase + { + get => ReasonPhrase; + set => ReasonPhrase = value; + } + + IHeaderDictionary IHttpResponseFeature.Headers + { + get => ResponseHeaders; + set => ResponseHeaders = value; + } + + Stream IHttpResponseFeature.Body + { + get => ResponseBody; + set => ResponseBody = value; + } + + bool IHttpResponseFeature.HasStarted => HasResponseStarted; + + bool IHttpUpgradeFeature.IsUpgradableRequest => true; + + bool IFeatureCollection.IsReadOnly => false; + + int IFeatureCollection.Revision => _featureRevision; + + ClaimsPrincipal IHttpAuthenticationFeature.User + { + get => User; + set => User = value; + } + + public IAuthenticationHandler Handler { get; set; } + + string IServerVariablesFeature.this[string variableName] + { + get + { + 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) + { + 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] + { + get => FastFeatureGet(key); + set => FastFeatureSet(key, value); + } + + TFeature IFeatureCollection.Get() + { + return (TFeature)FastFeatureGet(typeof(TFeature)); + } + + void IFeatureCollection.Set(TFeature instance) + { + FastFeatureSet(typeof(TFeature), instance); + } + + void IHttpResponseFeature.OnStarting(Func callback, object state) + { + OnStarting(callback, state); + } + + void IHttpResponseFeature.OnCompleted(Func callback, object state) + { + OnCompleted(callback, state); + } + + async Task IHttpUpgradeFeature.UpgradeAsync() + { + if (!((IHttpUpgradeFeature)this).IsUpgradableRequest) + { + throw new InvalidOperationException(CoreStrings.CannotUpgradeNonUpgradableRequest); + } + + if (_wasUpgraded) + { + throw new InvalidOperationException(CoreStrings.UpgradeCannotBeCalledMultipleTimes); + } + if (HasResponseStarted) + { + throw new InvalidOperationException(CoreStrings.UpgradeCannotBeCalledMultipleTimes); + } + + _wasUpgraded = true; + + StatusCode = StatusCodes.Status101SwitchingProtocols; + ReasonPhrase = ReasonPhrases.GetReasonPhrase(StatusCodes.Status101SwitchingProtocols); + + // If we started reading before calling Upgrade Task should be completed at this point + // because read would return 0 synchronously + Debug.Assert(_readBodyTask == null || _readBodyTask.IsCompleted); + + // Reset reading status to allow restarting with new IO + _hasRequestReadingStarted = false; + + // Upgrade async will cause the stream processing to go into duplex mode + AsyncIO = new WebSocketsAsyncIOEngine(_contextLock, _pInProcessHandler); + + await InitializeResponse(flushHeaders: true); + + return _streams.Upgrade(); + } + + 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(); + + bool IHttpBodyControlFeature.AllowSynchronousIO { get; set; } = true; + + 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/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.Features.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.Features.cs new file mode 100644 index 0000000000..6e107e03f0 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.Features.cs @@ -0,0 +1,347 @@ +// 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; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal partial class IISHttpContext + { + private static readonly Type IHttpRequestFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpRequestFeature); + private static readonly Type IHttpResponseFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpResponseFeature); + private static readonly Type IHttpRequestIdentifierFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpRequestIdentifierFeature); + private static readonly Type IServiceProvidersFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IServiceProvidersFeature); + private static readonly Type IHttpRequestLifetimeFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpRequestLifetimeFeature); + private static readonly Type IHttpConnectionFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature); + private static readonly Type IHttpAuthenticationFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.Authentication.IHttpAuthenticationFeature); + private static readonly Type IQueryFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IQueryFeature); + private static readonly Type IFormFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IFormFeature); + private static readonly Type IHttpUpgradeFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpUpgradeFeature); + private static readonly Type IResponseCookiesFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IResponseCookiesFeature); + private static readonly Type IItemsFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IItemsFeature); + private static readonly Type ITlsConnectionFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.ITlsConnectionFeature); + private static readonly Type IHttpWebSocketFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpWebSocketFeature); + private static readonly Type ISessionFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.ISessionFeature); + private static readonly Type IHttpBodyControlFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpBodyControlFeature); + 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; + private object _currentIHttpRequestIdentifierFeature; + private object _currentIServiceProvidersFeature; + private object _currentIHttpRequestLifetimeFeature; + private object _currentIHttpConnectionFeature; + private object _currentIHttpAuthenticationFeature; + private object _currentIQueryFeature; + private object _currentIFormFeature; + private object _currentIHttpUpgradeFeature; + private object _currentIResponseCookiesFeature; + private object _currentIItemsFeature; + private object _currentITlsConnectionFeature; + private object _currentIHttpWebSocketFeature; + private object _currentISessionFeature; + private object _currentIHttpBodyControlFeature; + private object _currentIHttpSendFileFeature; + private object _currentIServerVariablesFeature; + private object _currentIHttpBufferingFeature; + + private void Initialize() + { + _currentIHttpRequestFeature = this; + _currentIHttpResponseFeature = this; + _currentIHttpUpgradeFeature = this; + _currentIHttpRequestIdentifierFeature = this; + _currentIHttpRequestLifetimeFeature = this; + _currentIHttpConnectionFeature = this; + _currentIHttpBodyControlFeature = this; + _currentIHttpAuthenticationFeature = this; + _currentIServerVariablesFeature = this; + _currentIHttpBufferingFeature = this; + _currentITlsConnectionFeature = this; + } + + internal object FastFeatureGet(Type key) + { + if (key == IHttpRequestFeatureType) + { + return _currentIHttpRequestFeature; + } + if (key == IHttpResponseFeatureType) + { + return _currentIHttpResponseFeature; + } + if (key == IHttpRequestIdentifierFeatureType) + { + return _currentIHttpRequestIdentifierFeature; + } + if (key == IServiceProvidersFeatureType) + { + return _currentIServiceProvidersFeature; + } + if (key == IHttpRequestLifetimeFeatureType) + { + return _currentIHttpRequestLifetimeFeature; + } + if (key == IHttpConnectionFeatureType) + { + return _currentIHttpConnectionFeature; + } + if (key == IHttpAuthenticationFeatureType) + { + return _currentIHttpAuthenticationFeature; + } + if (key == IQueryFeatureType) + { + return _currentIQueryFeature; + } + if (key == IFormFeatureType) + { + return _currentIFormFeature; + } + if (key == IHttpUpgradeFeatureType) + { + return _currentIHttpUpgradeFeature; + } + if (key == IResponseCookiesFeatureType) + { + return _currentIResponseCookiesFeature; + } + if (key == IItemsFeatureType) + { + return _currentIItemsFeature; + } + if (key == ITlsConnectionFeatureType) + { + return _currentITlsConnectionFeature; + } + if (key == IHttpWebSocketFeatureType) + { + return _currentIHttpWebSocketFeature; + } + if (key == ISessionFeatureType) + { + return _currentISessionFeature; + } + if (key == IHttpBodyControlFeatureType) + { + return _currentIHttpBodyControlFeature; + } + if (key == IHttpSendFileFeatureType) + { + return _currentIHttpSendFileFeature; + } + if (key == IISHttpContextType) + { + return this; + } + if (key == IServerVariablesFeature) + { + return _currentIServerVariablesFeature; + } + if (key == IHttpBufferingFeature) + { + return _currentIHttpBufferingFeature; + } + + return ExtraFeatureGet(key); + } + + internal void FastFeatureSet(Type key, object feature) + { + _featureRevision++; + + if (key == IHttpRequestFeatureType) + { + _currentIHttpRequestFeature = feature; + return; + } + if (key == IHttpResponseFeatureType) + { + _currentIHttpResponseFeature = feature; + return; + } + if (key == IHttpRequestIdentifierFeatureType) + { + _currentIHttpRequestIdentifierFeature = feature; + return; + } + if (key == IServiceProvidersFeatureType) + { + _currentIServiceProvidersFeature = feature; + return; + } + if (key == IHttpRequestLifetimeFeatureType) + { + _currentIHttpRequestLifetimeFeature = feature; + return; + } + if (key == IHttpConnectionFeatureType) + { + _currentIHttpConnectionFeature = feature; + return; + } + if (key == IHttpAuthenticationFeatureType) + { + _currentIHttpAuthenticationFeature = feature; + return; + } + if (key == IQueryFeatureType) + { + _currentIQueryFeature = feature; + return; + } + if (key == IFormFeatureType) + { + _currentIFormFeature = feature; + return; + } + if (key == IHttpUpgradeFeatureType) + { + _currentIHttpUpgradeFeature = feature; + return; + } + if (key == IResponseCookiesFeatureType) + { + _currentIResponseCookiesFeature = feature; + return; + } + if (key == IItemsFeatureType) + { + _currentIItemsFeature = feature; + return; + } + if (key == ITlsConnectionFeatureType) + { + _currentITlsConnectionFeature = feature; + return; + } + if (key == IHttpWebSocketFeatureType) + { + _currentIHttpWebSocketFeature = feature; + return; + } + if (key == ISessionFeatureType) + { + _currentISessionFeature = feature; + return; + } + if (key == IHttpBodyControlFeatureType) + { + _currentIHttpBodyControlFeature = feature; + return; + } + if (key == IHttpSendFileFeatureType) + { + _currentIHttpSendFileFeature = feature; + return; + } + if (key == IServerVariablesFeature) + { + _currentIServerVariablesFeature = feature; + return; + } + if (key == IHttpBufferingFeature) + { + _currentIHttpBufferingFeature = feature; + return; + } + if (key == IISHttpContextType) + { + throw new InvalidOperationException("Cannot set IISHttpContext in feature collection"); + }; + ExtraFeatureSet(key, feature); + } + + private IEnumerable> FastEnumerable() + { + if (_currentIHttpRequestFeature != null) + { + yield return new KeyValuePair(IHttpRequestFeatureType, _currentIHttpRequestFeature as global::Microsoft.AspNetCore.Http.Features.IHttpRequestFeature); + } + if (_currentIHttpResponseFeature != null) + { + yield return new KeyValuePair(IHttpResponseFeatureType, _currentIHttpResponseFeature as global::Microsoft.AspNetCore.Http.Features.IHttpResponseFeature); + } + if (_currentIHttpRequestIdentifierFeature != null) + { + yield return new KeyValuePair(IHttpRequestIdentifierFeatureType, _currentIHttpRequestIdentifierFeature as global::Microsoft.AspNetCore.Http.Features.IHttpRequestIdentifierFeature); + } + if (_currentIServiceProvidersFeature != null) + { + yield return new KeyValuePair(IServiceProvidersFeatureType, _currentIServiceProvidersFeature as global::Microsoft.AspNetCore.Http.Features.IServiceProvidersFeature); + } + if (_currentIHttpRequestLifetimeFeature != null) + { + yield return new KeyValuePair(IHttpRequestLifetimeFeatureType, _currentIHttpRequestLifetimeFeature as global::Microsoft.AspNetCore.Http.Features.IHttpRequestLifetimeFeature); + } + if (_currentIHttpConnectionFeature != null) + { + yield return new KeyValuePair(IHttpConnectionFeatureType, _currentIHttpConnectionFeature as global::Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature); + } + if (_currentIHttpAuthenticationFeature != null) + { + yield return new KeyValuePair(IHttpAuthenticationFeatureType, _currentIHttpAuthenticationFeature as global::Microsoft.AspNetCore.Http.Features.Authentication.IHttpAuthenticationFeature); + } + if (_currentIQueryFeature != null) + { + yield return new KeyValuePair(IQueryFeatureType, _currentIQueryFeature as global::Microsoft.AspNetCore.Http.Features.IQueryFeature); + } + if (_currentIFormFeature != null) + { + yield return new KeyValuePair(IFormFeatureType, _currentIFormFeature as global::Microsoft.AspNetCore.Http.Features.IFormFeature); + } + if (_currentIHttpUpgradeFeature != null) + { + yield return new KeyValuePair(IHttpUpgradeFeatureType, _currentIHttpUpgradeFeature as global::Microsoft.AspNetCore.Http.Features.IHttpUpgradeFeature); + } + if (_currentIResponseCookiesFeature != null) + { + yield return new KeyValuePair(IResponseCookiesFeatureType, _currentIResponseCookiesFeature as global::Microsoft.AspNetCore.Http.Features.IResponseCookiesFeature); + } + if (_currentIItemsFeature != null) + { + yield return new KeyValuePair(IItemsFeatureType, _currentIItemsFeature as global::Microsoft.AspNetCore.Http.Features.IItemsFeature); + } + if (_currentITlsConnectionFeature != null) + { + yield return new KeyValuePair(ITlsConnectionFeatureType, _currentITlsConnectionFeature as global::Microsoft.AspNetCore.Http.Features.ITlsConnectionFeature); + } + if (_currentIHttpWebSocketFeature != null) + { + yield return new KeyValuePair(IHttpWebSocketFeatureType, _currentIHttpWebSocketFeature as global::Microsoft.AspNetCore.Http.Features.IHttpWebSocketFeature); + } + if (_currentISessionFeature != null) + { + yield return new KeyValuePair(ISessionFeatureType, _currentISessionFeature as global::Microsoft.AspNetCore.Http.Features.ISessionFeature); + } + if (_currentIHttpBodyControlFeature != null) + { + yield return new KeyValuePair(IHttpBodyControlFeatureType, _currentIHttpBodyControlFeature as global::Microsoft.AspNetCore.Http.Features.IHttpBodyControlFeature); + } + if (_currentIHttpSendFileFeature != null) + { + yield return new KeyValuePair(IHttpSendFileFeatureType, _currentIHttpSendFileFeature as global::Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature); + } + if (_currentIServerVariablesFeature != null) + { + 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) + { + foreach (var item in MaybeExtra) + { + yield return item; + } + } + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.IHttpConnectionFeature.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.IHttpConnectionFeature.cs new file mode 100644 index 0000000000..40b70a2b90 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.IHttpConnectionFeature.cs @@ -0,0 +1,100 @@ +// 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.Globalization; +using System.Net; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal partial class IISHttpContext : IHttpConnectionFeature + { + IPAddress IHttpConnectionFeature.RemoteIpAddress + { + get + { + if (RemoteIpAddress == null) + { + InitializeRemoteEndpoint(); + } + + return RemoteIpAddress; + } + set => RemoteIpAddress = value; + } + + IPAddress IHttpConnectionFeature.LocalIpAddress + { + get + { + if (LocalIpAddress == null) + { + InitializeLocalEndpoint(); + } + return LocalIpAddress; + } + set => LocalIpAddress = value; + } + + int IHttpConnectionFeature.RemotePort + { + get + { + if (RemoteIpAddress == null) + { + InitializeRemoteEndpoint(); + } + + return RemotePort; + } + set => RemotePort = value; + } + + int IHttpConnectionFeature.LocalPort + { + get + { + if (LocalIpAddress == null) + { + InitializeLocalEndpoint(); + } + + return LocalPort; + } + set => LocalPort = value; + } + + string IHttpConnectionFeature.ConnectionId + { + get + { + if (RequestConnectionId == null) + { + InitializeConnectionId(); + } + + return RequestConnectionId; + } + set => RequestConnectionId = value; + } + + private void InitializeLocalEndpoint() + { + var localEndPoint = GetLocalEndPoint(); + LocalIpAddress = localEndPoint.GetIPAddress(); + LocalPort = localEndPoint.GetPort(); + } + + private void InitializeRemoteEndpoint() + { + var remoteEndPoint = GetRemoteEndPoint(); + RemoteIpAddress = remoteEndPoint.GetIPAddress(); + RemotePort = remoteEndPoint.GetPort(); + } + + private void InitializeConnectionId() + { + RequestConnectionId = ConnectionId.ToString(CultureInfo.InvariantCulture); + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.IHttpRequestIdentifierFeature.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.IHttpRequestIdentifierFeature.cs new file mode 100644 index 0000000000..2d735a7d8f --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.IHttpRequestIdentifierFeature.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal partial class IISHttpContext : IHttpRequestIdentifierFeature + { + string IHttpRequestIdentifierFeature.TraceIdentifier + { + get + { + if (TraceIdentifier == null) + { + InitializeHttpRequestIdentifierFeature(); + } + + return TraceIdentifier; + } + set => TraceIdentifier = value; + } + + private unsafe void InitializeHttpRequestIdentifierFeature() + { + // Copied from WebListener + // This is the base GUID used by HTTP.SYS for generating the activity ID. + // HTTP.SYS overwrites the first 8 bytes of the base GUID with RequestId to generate ETW activity ID. + // The requestId should be set by the NativeRequestContext + var guid = new Guid(0xffcb4c93, 0xa57f, 0x453c, 0xb6, 0x3f, 0x84, 0x71, 0xc, 0x79, 0x67, 0xbb); + *((ulong*)&guid) = RequestId; + + // TODO: Also make this not slow + TraceIdentifier = guid.ToString(); + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.IHttpRequestLifetimeFeature.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.IHttpRequestLifetimeFeature.cs new file mode 100644 index 0000000000..ab46f8df5c --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.IHttpRequestLifetimeFeature.cs @@ -0,0 +1,71 @@ +// 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.Threading; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal partial class IISHttpContext : IHttpRequestLifetimeFeature + { + private CancellationTokenSource _abortedCts; + private CancellationToken? _manuallySetRequestAbortToken; + + CancellationToken IHttpRequestLifetimeFeature.RequestAborted + { + get + { + // If a request abort token was previously explicitly set, return it. + if (_manuallySetRequestAbortToken.HasValue) + { + return _manuallySetRequestAbortToken.Value; + } + // Otherwise, get the abort CTS. If we have one, which would mean that someone previously + // asked for the RequestAborted token, simply return its token. If we don't, + // check to see whether we've already aborted, in which case just return an + // already canceled token. Finally, force a source into existence if we still + // don't have one, and return its token. + var cts = _abortedCts; + return + cts != null ? cts.Token : + (_requestAborted == 1) ? new CancellationToken(true) : + RequestAbortedSource.Token; + } + set + { + // Set an abort token, overriding one we create internally. This setter and associated + // field exist purely to support IHttpRequestLifetimeFeature.set_RequestAborted. + _manuallySetRequestAbortToken = value; + } + } + + private CancellationTokenSource RequestAbortedSource + { + get + { + // Get the abort token, lazily-initializing it if necessary. + // Make sure it's canceled if an abort request already came in. + + // EnsureInitialized can return null since _abortedCts is reset to null + // after it's already been initialized to a non-null value. + // If EnsureInitialized does return null, this property was accessed between + // requests so it's safe to return an ephemeral CancellationTokenSource. + var cts = LazyInitializer.EnsureInitialized(ref _abortedCts, () => new CancellationTokenSource()) + ?? new CancellationTokenSource(); + + if (_requestAborted == 1) + { + cts.Cancel(); + } + return cts; + } + } + + void IHttpRequestLifetimeFeature.Abort() + { + Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByApplication)); + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.IO.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.IO.cs new file mode 100644 index 0000000000..e945500672 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.IO.cs @@ -0,0 +1,232 @@ +// 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.Buffers; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal partial class IISHttpContext + { + /// + /// Reads data from the Input pipe to the user. + /// + /// + /// + /// + internal async Task ReadAsync(Memory memory, CancellationToken cancellationToken) + { + if (!_hasRequestReadingStarted) + { + InitializeRequestIO(); + } + + while (true) + { + var result = await _bodyInputPipe.Reader.ReadAsync(cancellationToken); + var readableBuffer = result.Buffer; + try + { + if (!readableBuffer.IsEmpty) + { + var actual = Math.Min(readableBuffer.Length, memory.Length); + readableBuffer = readableBuffer.Slice(0, actual); + readableBuffer.CopyTo(memory.Span); + return (int)actual; + } + else if (result.IsCompleted) + { + return 0; + } + } + finally + { + _bodyInputPipe.Reader.AdvanceTo(readableBuffer.End, readableBuffer.End); + } + } + } + + /// + /// Writes data to the output pipe. + /// + /// + /// + /// + internal Task WriteAsync(ReadOnlyMemory memory, CancellationToken cancellationToken = default(CancellationToken)) + { + async Task WriteFirstAsync() + { + await InitializeResponse(flushHeaders: false); + await _bodyOutput.WriteAsync(memory, cancellationToken); + } + + return !HasResponseStarted ? WriteFirstAsync() : _bodyOutput.WriteAsync(memory, cancellationToken); + } + + /// + /// Flushes the data in the output pipe + /// + /// + /// + internal Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + async Task FlushFirstAsync() + { + await InitializeResponse(flushHeaders: true); + await _bodyOutput.FlushAsync(cancellationToken); + } + + return !HasResponseStarted ? FlushFirstAsync() : _bodyOutput.FlushAsync(cancellationToken); + } + + private async Task ReadBody() + { + Exception error = null; + try + { + while (true) + { + var memory = _bodyInputPipe.Writer.GetMemory(); + + var read = await AsyncIO.ReadAsync(memory); + + // End of body + if (read == 0) + { + break; + } + + // Read was not canceled because of incoming write or IO stopping + if (read != -1) + { + _bodyInputPipe.Writer.Advance(read); + } + + var result = await _bodyInputPipe.Writer.FlushAsync(); + + if (result.IsCompleted || result.IsCanceled) + { + break; + } + } + } + catch (ConnectionResetException ex) + { + ConnectionReset(); + error = ex; + } + catch (Exception ex) + { + error = ex; + Log.UnexpectedError(_logger, nameof(IISHttpContext), ex); + } + finally + { + _bodyInputPipe.Writer.Complete(error); + } + } + + private async Task WriteBody(bool flush = false) + { + Exception error = null; + try + { + while (true) + { + var result = await _bodyOutput.Reader.ReadAsync(); + + var buffer = result.Buffer; + + try + { + if (!buffer.IsEmpty) + { + await AsyncIO.WriteAsync(buffer); + } + + // if request is done no need to flush, http.sys would do it for us + if (result.IsCompleted) + { + break; + } + + flush = flush | result.IsCanceled; + + if (flush) + { + await AsyncIO.FlushAsync(); + flush = false; + } + } + finally + { + _bodyOutput.Reader.AdvanceTo(buffer.End); + } + } + } + // We want to swallow IO exception and allow app to finish writing + catch (ConnectionResetException) + { + ConnectionReset(); + } + catch (Exception ex) + { + error = ex; + Log.UnexpectedError(_logger, nameof(IISHttpContext), ex); + } + finally + { + _bodyOutput.Reader.Complete(error); + } + } + + private bool AbortIO() + { + if (Interlocked.CompareExchange(ref _requestAborted, 1, 0) != 0) + { + return false; + } + + _bodyOutput.Dispose(); + + var cts = _abortedCts; + if (cts != null) + { + ThreadPool.QueueUserWorkItem(t => + { + try + { + cts.Cancel(); + } + catch (Exception ex) + { + Log.ApplicationError(_logger, ((IHttpConnectionFeature)this).ConnectionId, TraceIdentifier, ex); + } + }); + } + + return true; + } + + public void Abort(Exception reason) + { + _bodyOutput.Abort(reason); + _streams.Abort(reason); + NativeMethods.HttpCloseConnection(_pInProcessHandler); + + AbortIO(); + } + + internal void ConnectionReset() + { + if (AbortIO()) + { + Log.ConnectionDisconnect(_logger, ((IHttpConnectionFeature)this).ConnectionId); + } + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.Log.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.Log.cs new file mode 100644 index 0000000000..1056ac1479 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.Log.cs @@ -0,0 +1,39 @@ +// 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.Runtime.CompilerServices; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal abstract partial class IISHttpContext + { + private static class Log + { + private static readonly Action _connectionDisconnect = + LoggerMessage.Define(LogLevel.Debug, new EventId(1, "ConnectionDisconnect"), @"Connection ID ""{ConnectionId}"" disconnecting."); + + private static readonly Action _applicationError = + LoggerMessage.Define(LogLevel.Error, new EventId(2, "ApplicationError"), @"Connection ID ""{ConnectionId}"", Request ID ""{TraceIdentifier}"": An unhandled exception was thrown by the application."); + + private static readonly Action _unexpectedError = + LoggerMessage.Define(LogLevel.Error, new EventId(3, "UnexpectedError"), @"Unexpected exception in ""{ClassName}.{MethodName}""."); + + public static void ConnectionDisconnect(ILogger logger, string connectionId) + { + _connectionDisconnect(logger, connectionId, null); + } + + public static void ApplicationError(ILogger logger, string connectionId, string traceIdentifier, Exception ex) + { + _applicationError(logger, connectionId, traceIdentifier, ex); + } + + public static void UnexpectedError(ILogger logger, string className, Exception ex, [CallerMemberName] string methodName = null) + { + _unexpectedError(logger, className, methodName, ex); + } + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.cs new file mode 100644 index 0000000000..09e478a989 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.cs @@ -0,0 +1,522 @@ +// 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.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Pipelines; +using System.Net; +using System.Runtime.InteropServices; +using System.Security.Claims; +using System.Security.Principal; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.HttpSys.Internal; +using Microsoft.AspNetCore.Server.IIS.Core.IO; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal abstract partial class IISHttpContext : NativeRequestContext, IDisposable + { + private const int MinAllocBufferSize = 2048; + private const int PauseWriterThreshold = 65536; + private const int ResumeWriterTheshold = PauseWriterThreshold / 2; + + protected readonly IntPtr _pInProcessHandler; + + private readonly IISServerOptions _options; + + protected Streams _streams; + + private volatile bool _hasResponseStarted; + private volatile bool _hasRequestReadingStarted; + + private int _statusCode; + private string _reasonPhrase; + // Used to synchronize callback registration and native method calls + private readonly object _contextLock = new object(); + + protected Stack, object>> _onStarting; + protected Stack, object>> _onCompleted; + + protected Exception _applicationException; + private readonly MemoryPool _memoryPool; + private readonly IISHttpServer _server; + + private readonly ILogger _logger; + + private GCHandle _thisHandle; + protected Task _readBodyTask; + protected Task _writeBodyTask; + + private bool _wasUpgraded; + protected int _requestAborted; + + protected Pipe _bodyInputPipe; + protected OutputProducer _bodyOutput; + + private const string NtlmString = "NTLM"; + private const string NegotiateString = "Negotiate"; + private const string BasicString = "Basic"; + + + internal unsafe IISHttpContext( + MemoryPool memoryPool, + IntPtr pInProcessHandler, + IISServerOptions options, + IISHttpServer server, + ILogger logger) + : base((HttpApiTypes.HTTP_REQUEST*)NativeMethods.HttpGetRawRequest(pInProcessHandler)) + { + _memoryPool = memoryPool; + _pInProcessHandler = pInProcessHandler; + _options = options; + _server = server; + _logger = logger; + } + + public Version HttpVersion { get; set; } + public string Scheme { get; set; } + public string Method { get; set; } + public string PathBase { get; set; } + public string Path { get; set; } + public string QueryString { get; set; } + public string RawTarget { get; set; } + + public bool HasResponseStarted => _hasResponseStarted; + public IPAddress RemoteIpAddress { get; set; } + public int RemotePort { get; set; } + public IPAddress LocalIpAddress { get; set; } + public int LocalPort { get; set; } + public string RequestConnectionId { get; set; } + public string TraceIdentifier { get; set; } + public ClaimsPrincipal User { get; set; } + internal WindowsPrincipal WindowsUser { get; set; } + public Stream RequestBody { get; set; } + public Stream ResponseBody { get; set; } + + protected IAsyncIOEngine AsyncIO { get; set; } + + public IHeaderDictionary RequestHeaders { get; set; } + public IHeaderDictionary ResponseHeaders { get; set; } + private HeaderCollection HttpResponseHeaders { get; set; } + internal HttpApiTypes.HTTP_VERB KnownMethod { get; private set; } + + protected void InitializeContext() + { + _thisHandle = GCHandle.Alloc(this); + + Method = GetVerb(); + + RawTarget = GetRawUrl(); + // TODO version is slow. + HttpVersion = GetVersion(); + Scheme = SslStatus != SslStatus.Insecure ? Constants.HttpsScheme : Constants.HttpScheme; + KnownMethod = VerbId; + StatusCode = 200; + + var originalPath = GetOriginalPath(); + + if (KnownMethod == HttpApiTypes.HTTP_VERB.HttpVerbOPTIONS && string.Equals(RawTarget, "*", StringComparison.Ordinal)) + { + PathBase = string.Empty; + Path = string.Empty; + } + else + { + // Path and pathbase are unescaped by RequestUriBuilder + // The UsePathBase middleware will modify the pathbase and path correctly + PathBase = string.Empty; + Path = originalPath; + } + + var cookedUrl = GetCookedUrl(); + QueryString = cookedUrl.GetQueryString() ?? string.Empty; + + RequestHeaders = new RequestHeaders(this); + HttpResponseHeaders = new HeaderCollection(); + ResponseHeaders = HttpResponseHeaders; + + if (_options.ForwardWindowsAuthentication) + { + WindowsUser = GetWindowsPrincipal(); + if (_options.AutomaticAuthentication) + { + User = WindowsUser; + } + } + + ResetFeatureCollection(); + + if (!_server.IsWebSocketAvailable(_pInProcessHandler)) + { + _currentIHttpUpgradeFeature = null; + } + + _streams = new Streams(this); + + (RequestBody, ResponseBody) = _streams.Start(); + + var pipe = new Pipe( + new PipeOptions( + _memoryPool, + readerScheduler: PipeScheduler.ThreadPool, + pauseWriterThreshold: PauseWriterThreshold, + resumeWriterThreshold: ResumeWriterTheshold, + minimumSegmentSize: MinAllocBufferSize)); + _bodyOutput = new OutputProducer(pipe); + + NativeMethods.HttpSetManagedContext(_pInProcessHandler, (IntPtr)_thisHandle); + } + + private string GetOriginalPath() + { + // applicationInitialization request might have trailing \0 character included in the length + // check and skip it + var rawUrlInBytes = GetRawUrlInBytes(); + if (rawUrlInBytes.Length > 0 && rawUrlInBytes[rawUrlInBytes.Length - 1] == 0) + { + var newRawUrlInBytes = new byte[rawUrlInBytes.Length - 1]; + Array.Copy(rawUrlInBytes, newRawUrlInBytes, newRawUrlInBytes.Length); + rawUrlInBytes = newRawUrlInBytes; + } + + var originalPath = RequestUriBuilder.DecodeAndUnescapePath(rawUrlInBytes); + return originalPath; + } + + public int StatusCode + { + get { return _statusCode; } + set + { + if (HasResponseStarted) + { + ThrowResponseAlreadyStartedException(nameof(StatusCode)); + } + _statusCode = (ushort)value; + } + } + + public string ReasonPhrase + { + get { return _reasonPhrase; } + set + { + if (HasResponseStarted) + { + ThrowResponseAlreadyStartedException(nameof(ReasonPhrase)); + } + _reasonPhrase = value; + } + } + + internal IISHttpServer Server => _server; + + private async Task InitializeResponse(bool flushHeaders) + { + await FireOnStarting(); + + if (_applicationException != null) + { + ThrowResponseAbortedException(); + } + + await ProduceStart(flushHeaders); + } + + private async Task ProduceStart(bool flushHeaders) + { + Debug.Assert(_hasResponseStarted == false); + + _hasResponseStarted = true; + + SetResponseHeaders(); + + EnsureIOInitialized(); + + if (flushHeaders) + { + try + { + await AsyncIO.FlushAsync(); + } + // Client might be disconnected at this point + // don't leak the exception + catch (ConnectionResetException) + { + ConnectionReset(); + } + } + + _writeBodyTask = WriteBody(!flushHeaders); + } + + private void InitializeRequestIO() + { + Debug.Assert(!_hasRequestReadingStarted); + + _hasRequestReadingStarted = true; + + EnsureIOInitialized(); + + _bodyInputPipe = new Pipe(new PipeOptions(_memoryPool, readerScheduler: PipeScheduler.ThreadPool, minimumSegmentSize: MinAllocBufferSize)); + _readBodyTask = ReadBody(); + } + + private void EnsureIOInitialized() + { + // If at this point request was not upgraded just start a normal IO engine + if (AsyncIO == null) + { + AsyncIO = new AsyncIOEngine(_contextLock, _pInProcessHandler); + } + } + + private void ThrowResponseAbortedException() + { + throw new ObjectDisposedException(CoreStrings.UnhandledApplicationException, _applicationException); + } + + protected Task ProduceEnd() + { + if (_applicationException != null) + { + if (HasResponseStarted) + { + // We can no longer change the response, so we simply close the connection. + return Task.CompletedTask; + } + + // If the request was rejected, the error state has already been set by SetBadRequestState and + // that should take precedence. + else + { + // 500 Internal Server Error + SetErrorResponseHeaders(statusCode: StatusCodes.Status500InternalServerError); + } + } + + if (!HasResponseStarted) + { + return ProduceEndAwaited(); + } + + return Task.CompletedTask; + } + + private void SetErrorResponseHeaders(int statusCode) + { + StatusCode = statusCode; + ReasonPhrase = string.Empty; + HttpResponseHeaders.Clear(); + } + + private async Task ProduceEndAwaited() + { + await ProduceStart(flushHeaders: true); + await _bodyOutput.FlushAsync(default); + } + + public unsafe void SetResponseHeaders() + { + // Verifies we have sent the statuscode before writing a header + var reasonPhrase = string.IsNullOrEmpty(ReasonPhrase) ? ReasonPhrases.GetReasonPhrase(StatusCode) : ReasonPhrase; + + // This copies data into the underlying buffer + NativeMethods.HttpSetResponseStatusCode(_pInProcessHandler, (ushort)StatusCode, reasonPhrase); + + HttpResponseHeaders.IsReadOnly = true; + foreach (var headerPair in HttpResponseHeaders) + { + var headerValues = headerPair.Value; + var knownHeaderIndex = HttpApiTypes.HTTP_RESPONSE_HEADER_ID.IndexOfKnownHeader(headerPair.Key); + for (var i = 0; i < headerValues.Count; i++) + { + var isFirst = i == 0; + var headerValueBytes = Encoding.UTF8.GetBytes(headerValues[i]); + fixed (byte* pHeaderValue = headerValueBytes) + { + if (knownHeaderIndex == -1) + { + var headerNameBytes = Encoding.UTF8.GetBytes(headerPair.Key); + fixed (byte* pHeaderName = headerNameBytes) + { + NativeMethods.HttpResponseSetUnknownHeader(_pInProcessHandler, pHeaderName, pHeaderValue, (ushort)headerValueBytes.Length, fReplace: isFirst); + } + } + else + { + NativeMethods.HttpResponseSetKnownHeader(_pInProcessHandler, knownHeaderIndex, pHeaderValue, (ushort)headerValueBytes.Length, fReplace: isFirst); + } + } + } + } + } + + public abstract Task ProcessRequestAsync(); + + public void OnStarting(Func callback, object state) + { + lock (_contextLock) + { + if (HasResponseStarted) + { + throw new InvalidOperationException("Response already started"); + } + + if (_onStarting == null) + { + _onStarting = new Stack, object>>(); + } + _onStarting.Push(new KeyValuePair, object>(callback, state)); + } + } + + public void OnCompleted(Func callback, object state) + { + lock (_contextLock) + { + if (_onCompleted == null) + { + _onCompleted = new Stack, object>>(); + } + _onCompleted.Push(new KeyValuePair, object>(callback, state)); + } + } + + protected async Task FireOnStarting() + { + Stack, object>> onStarting = null; + lock (_contextLock) + { + onStarting = _onStarting; + _onStarting = null; + } + if (onStarting != null) + { + try + { + foreach (var entry in onStarting) + { + await entry.Key.Invoke(entry.Value); + } + } + catch (Exception ex) + { + ReportApplicationError(ex); + } + } + } + + protected async Task FireOnCompleted() + { + Stack, object>> onCompleted = null; + lock (_contextLock) + { + onCompleted = _onCompleted; + _onCompleted = null; + } + if (onCompleted != null) + { + foreach (var entry in onCompleted) + { + try + { + await entry.Key.Invoke(entry.Value); + } + catch (Exception ex) + { + ReportApplicationError(ex); + } + } + } + } + + protected void ReportApplicationError(Exception ex) + { + if (_applicationException == null) + { + _applicationException = ex; + } + else if (_applicationException is AggregateException) + { + _applicationException = new AggregateException(_applicationException, ex).Flatten(); + } + else + { + _applicationException = new AggregateException(_applicationException, ex); + } + + Log.ApplicationError(_logger, ((IHttpConnectionFeature)this).ConnectionId, TraceIdentifier, ex); + } + + public void PostCompletion(NativeMethods.REQUEST_NOTIFICATION_STATUS requestNotificationStatus) + { + NativeMethods.HttpSetCompletionStatus(_pInProcessHandler, requestNotificationStatus); + NativeMethods.HttpPostCompletion(_pInProcessHandler, 0); + } + + internal void OnAsyncCompletion(int hr, int bytes) + { + AsyncIO.NotifyCompletion(hr, bytes); + } + + private bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + _thisHandle.Free(); + } + + if (WindowsUser?.Identity is WindowsIdentity wi) + { + wi.Dispose(); + } + + _abortedCts?.Dispose(); + + disposedValue = true; + } + } + + public override void Dispose() + { + Dispose(disposing: true); + } + + private void ThrowResponseAlreadyStartedException(string name) + { + throw new InvalidOperationException(CoreStrings.FormatParameterReadOnlyAfterResponseStarted(name)); + } + + private WindowsPrincipal GetWindowsPrincipal() + { + NativeMethods.HttpGetAuthenticationInformation(_pInProcessHandler, out var authenticationType, out var token); + + if (token != IntPtr.Zero && authenticationType != null) + { + if ((authenticationType.Equals(NtlmString, StringComparison.OrdinalIgnoreCase) + || authenticationType.Equals(NegotiateString, StringComparison.OrdinalIgnoreCase) + || authenticationType.Equals(BasicString, StringComparison.OrdinalIgnoreCase))) + { + return new WindowsPrincipal(new WindowsIdentity(token, authenticationType)); + } + } + return null; + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContextOfT.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContextOfT.cs new file mode 100644 index 0000000000..bb104f5ac1 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContextOfT.cs @@ -0,0 +1,107 @@ +// 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.Buffers; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal class IISHttpContextOfT : IISHttpContext + { + private readonly IHttpApplication _application; + + public IISHttpContextOfT(MemoryPool memoryPool, IHttpApplication application, IntPtr pInProcessHandler, IISServerOptions options, IISHttpServer server, ILogger logger) + : base(memoryPool, pInProcessHandler, options, server, logger) + { + _application = application; + } + + public override async Task ProcessRequestAsync() + { + InitializeContext(); + + var context = default(TContext); + var success = true; + + try + { + context = _application.CreateContext(this); + await _application.ProcessRequestAsync(context); + // TODO Verification of Response + //if (Volatile.Read(ref _requestAborted) == 0) + //{ + // VerifyResponseContentLength(); + //} + } + catch (Exception ex) + { + ReportApplicationError(ex); + success = false; + } + finally + { + _streams.Stop(); + + if (!HasResponseStarted && _applicationException == null && _onStarting != null) + { + await FireOnStarting(); + // Dispose + } + + if (_onCompleted != null) + { + await FireOnCompleted(); + } + } + + if (Volatile.Read(ref _requestAborted) == 0) + { + await ProduceEnd(); + } + else if (!HasResponseStarted) + { + // If the request was aborted and no response was sent, there's no + // meaningful status code to log. + StatusCode = 0; + success = false; + } + + try + { + _application.DisposeContext(context, _applicationException); + } + catch (Exception ex) + { + // TODO Log this + _applicationException = _applicationException ?? ex; + success = false; + } + finally + { + // Complete response writer and request reader pipe sides + _bodyOutput.Dispose(); + _bodyInputPipe?.Reader.Complete(); + + // Allow writes to drain + if (_writeBodyTask != null) + { + await _writeBodyTask; + } + + // Cancel all remaining IO, there might be reads pending if not entire request body was sent by client + AsyncIO?.Dispose(); + + if (_readBodyTask != null) + { + await _readBodyTask; + } + } + return success; + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpServer.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpServer.cs new file mode 100644 index 0000000000..bfabf61bd7 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpServer.cs @@ -0,0 +1,286 @@ +// 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.Buffers; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal class IISHttpServer : IServer + { + private const string WebSocketVersionString = "WEBSOCKET_VERSION"; + + private static readonly NativeMethods.PFN_REQUEST_HANDLER _requestHandler = HandleRequest; + private static readonly NativeMethods.PFN_SHUTDOWN_HANDLER _shutdownHandler = HandleShutdown; + private static readonly NativeMethods.PFN_DISCONNECT_HANDLER _onDisconnect = OnDisconnect; + private static readonly NativeMethods.PFN_ASYNC_COMPLETION _onAsyncCompletion = OnAsyncCompletion; + + private IISContextFactory _iisContextFactory; + private readonly MemoryPool _memoryPool = new SlabMemoryPool(); + private GCHandle _httpServerHandle; + private readonly IApplicationLifetime _applicationLifetime; + private readonly ILogger _logger; + private readonly IISServerOptions _options; + private readonly IISNativeApplication _nativeApplication; + + private volatile int _stopping; + private bool Stopping => _stopping == 1; + private int _outstandingRequests; + private readonly TaskCompletionSource _shutdownSignal = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + private bool? _websocketAvailable; + + public IFeatureCollection Features { get; } = new FeatureCollection(); + + // TODO: Remove pInProcessHandler argument + public bool IsWebSocketAvailable(IntPtr pInProcessHandler) + { + // Check if the Http upgrade feature is available in IIS. + // To check this, we can look at the server variable WEBSOCKET_VERSION + // And see if there is a version. Same check that Katana did: + // https://github.com/aspnet/AspNetKatana/blob/9f6e09af6bf203744feb5347121fe25f6eec06d8/src/Microsoft.Owin.Host.SystemWeb/OwinAppContext.cs#L125 + // Actively not locking here as acquiring a lock on every request will hurt perf more than checking the + // server variables a few extra times if a bunch of requests hit the server at the same time. + if (!_websocketAvailable.HasValue) + { + _websocketAvailable = NativeMethods.HttpTryGetServerVariable(pInProcessHandler, WebSocketVersionString, out var webSocketsSupported) + && !string.IsNullOrEmpty(webSocketsSupported); + } + + return _websocketAvailable.Value; + } + + public IISHttpServer( + IISNativeApplication nativeApplication, + IApplicationLifetime applicationLifetime, + IAuthenticationSchemeProvider authentication, + IOptions options, + ILogger logger + ) + { + _nativeApplication = nativeApplication; + _applicationLifetime = applicationLifetime; + _logger = logger; + _options = options.Value; + + if (_options.ForwardWindowsAuthentication) + { + authentication.AddScheme(new AuthenticationScheme(IISServerDefaults.AuthenticationScheme, _options.AuthenticationDisplayName, typeof(IISServerAuthenticationHandler))); + } + } + + public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) + { + _httpServerHandle = GCHandle.Alloc(this); + + _iisContextFactory = new IISContextFactory(_memoryPool, application, _options, this, _logger); + _nativeApplication.RegisterCallbacks(_requestHandler, _shutdownHandler, _onDisconnect, _onAsyncCompletion, (IntPtr)_httpServerHandle, (IntPtr)_httpServerHandle); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + void RegisterCancelation() + { + cancellationToken.Register(() => + { + _nativeApplication.StopCallsIntoManaged(); + _shutdownSignal.TrySetResult(null); + }); + } + if (Interlocked.Exchange(ref _stopping, 1) == 1) + { + RegisterCancelation(); + + return _shutdownSignal.Task; + } + + // First call back into native saying "DON'T SEND ME ANY MORE REQUESTS" + _nativeApplication.StopIncomingRequests(); + + try + { + // Wait for active requests to drain + if (_outstandingRequests > 0) + { + RegisterCancelation(); + } + else + { + // We have drained all requests. Block any callbacks into managed at this point. + _nativeApplication.StopCallsIntoManaged(); + _shutdownSignal.TrySetResult(null); + } + } + catch (Exception ex) + { + _shutdownSignal.TrySetException(ex); + } + + return _shutdownSignal.Task; + } + + public void Dispose() + { + _stopping = 1; + + // Block any more calls into managed from native as we are unloading. + _nativeApplication.StopCallsIntoManaged(); + _shutdownSignal.TrySetResult(null); + + if (_httpServerHandle.IsAllocated) + { + _httpServerHandle.Free(); + } + + _memoryPool.Dispose(); + _nativeApplication.Dispose(); + } + + private static NativeMethods.REQUEST_NOTIFICATION_STATUS HandleRequest(IntPtr pInProcessHandler, IntPtr pvRequestContext) + { + IISHttpServer server = null; + try + { + // Unwrap the server so we can create an http context and process the request + server = (IISHttpServer)GCHandle.FromIntPtr(pvRequestContext).Target; + Interlocked.Increment(ref server._outstandingRequests); + + var context = server._iisContextFactory.CreateHttpContext(pInProcessHandler); + + ThreadPool.QueueUserWorkItem(state => _ = HandleRequest((IISHttpContext)state), context); + return NativeMethods.REQUEST_NOTIFICATION_STATUS.RQ_NOTIFICATION_PENDING; + } + catch (Exception ex) + { + server?._logger.LogError(0, ex, $"Unexpected exception in static {nameof(IISHttpServer)}.{nameof(HandleRequest)}."); + + return NativeMethods.REQUEST_NOTIFICATION_STATUS.RQ_NOTIFICATION_FINISH_REQUEST; + } + } + + private static async Task HandleRequest(IISHttpContext context) + { + bool successfulRequest = false; + try + { + successfulRequest = await context.ProcessRequestAsync(); + } + catch (Exception ex) + { + context.Server._logger.LogError(0, ex, $"Unexpected exception in {nameof(IISHttpServer)}.{nameof(HandleRequest)}."); + } + finally + { + CompleteRequest(context, successfulRequest); + } + } + + private static bool HandleShutdown(IntPtr pvRequestContext) + { + IISHttpServer server = null; + try + { + server = (IISHttpServer)GCHandle.FromIntPtr(pvRequestContext).Target; + server._applicationLifetime.StopApplication(); + } + catch (Exception ex) + { + server?._logger.LogError(0, ex, $"Unexpected exception in {nameof(IISHttpServer)}.{nameof(HandleShutdown)}."); + } + return true; + } + + + private static void OnDisconnect(IntPtr pvManagedHttpContext) + { + IISHttpContext context = null; + try + { + context = (IISHttpContext)GCHandle.FromIntPtr(pvManagedHttpContext).Target; + context.ConnectionReset(); + } + catch (Exception ex) + { + context?.Server._logger.LogError(0, ex, $"Unexpected exception in {nameof(IISHttpServer)}.{nameof(OnDisconnect)}."); + } + } + + private static NativeMethods.REQUEST_NOTIFICATION_STATUS OnAsyncCompletion(IntPtr pvManagedHttpContext, int hr, int bytes) + { + IISHttpContext context = null; + try + { + context = (IISHttpContext)GCHandle.FromIntPtr(pvManagedHttpContext).Target; + context.OnAsyncCompletion(hr, bytes); + return NativeMethods.REQUEST_NOTIFICATION_STATUS.RQ_NOTIFICATION_PENDING; + } + catch (Exception ex) + { + context?.Server._logger.LogError(0, ex, $"Unexpected exception in {nameof(IISHttpServer)}.{nameof(OnAsyncCompletion)}."); + + return NativeMethods.REQUEST_NOTIFICATION_STATUS.RQ_NOTIFICATION_FINISH_REQUEST; + } + } + + private static void CompleteRequest(IISHttpContext context, bool result) + { + // Post completion after completing the request to resume the state machine + context.PostCompletion(ConvertRequestCompletionResults(result)); + + if (Interlocked.Decrement(ref context.Server._outstandingRequests) == 0 && context.Server.Stopping) + { + // All requests have been drained. + context.Server._nativeApplication.StopCallsIntoManaged(); + context.Server._shutdownSignal.TrySetResult(null); + } + + // Dispose the context + context.Dispose(); + } + + private static NativeMethods.REQUEST_NOTIFICATION_STATUS ConvertRequestCompletionResults(bool success) + { + return success ? NativeMethods.REQUEST_NOTIFICATION_STATUS.RQ_NOTIFICATION_CONTINUE + : NativeMethods.REQUEST_NOTIFICATION_STATUS.RQ_NOTIFICATION_FINISH_REQUEST; + } + + private class IISContextFactory : IISContextFactory + { + private readonly IHttpApplication _application; + private readonly MemoryPool _memoryPool; + private readonly IISServerOptions _options; + private readonly IISHttpServer _server; + private readonly ILogger _logger; + + public IISContextFactory(MemoryPool memoryPool, IHttpApplication application, IISServerOptions options, IISHttpServer server, ILogger logger) + { + _application = application; + _memoryPool = memoryPool; + _options = options; + _server = server; + _logger = logger; + } + + public IISHttpContext CreateHttpContext(IntPtr pInProcessHandler) + { + return new IISHttpContextOfT(_memoryPool, _application, pInProcessHandler, _options, _server, _logger); + } + } + } + + // Over engineering to avoid allocations... + internal interface IISContextFactory + { + IISHttpContext CreateHttpContext(IntPtr pInProcessHandler); + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISNativeApplication.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISNativeApplication.cs new file mode 100644 index 0000000000..1be8e888fd --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISNativeApplication.cs @@ -0,0 +1,57 @@ +// 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; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal class IISNativeApplication + { + private readonly IntPtr _nativeApplication; + + public IISNativeApplication(IntPtr nativeApplication) + { + _nativeApplication = nativeApplication; + } + + public void StopIncomingRequests() + { + NativeMethods.HttpStopIncomingRequests(_nativeApplication); + } + + public void StopCallsIntoManaged() + { + NativeMethods.HttpStopCallsIntoManaged(_nativeApplication); + } + + public void RegisterCallbacks( + NativeMethods.PFN_REQUEST_HANDLER requestHandler, + NativeMethods.PFN_SHUTDOWN_HANDLER shutdownHandler, + NativeMethods.PFN_DISCONNECT_HANDLER disconnectHandler, + NativeMethods.PFN_ASYNC_COMPLETION onAsyncCompletion, + IntPtr requestContext, + IntPtr shutdownContext) + { + NativeMethods.HttpRegisterCallbacks( + _nativeApplication, + requestHandler, + shutdownHandler, + disconnectHandler, + onAsyncCompletion, + requestContext, + shutdownContext); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + } + + ~IISNativeApplication() + { + // If this finalize is invoked, try our best to block all calls into managed. + StopCallsIntoManaged(); + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISServerAuthenticationHandler.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISServerAuthenticationHandler.cs new file mode 100644 index 0000000000..7031df34ea --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISServerAuthenticationHandler.cs @@ -0,0 +1,58 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + public class IISServerAuthenticationHandler : IAuthenticationHandler + { + private HttpContext _context; + private IISHttpContext _iisHttpContext; + + internal AuthenticationScheme Scheme { get; private set; } + + public Task AuthenticateAsync() + { + var user = _iisHttpContext.WindowsUser; + if (user != null && user.Identity != null && user.Identity.IsAuthenticated) + { + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(user, Scheme.Name))); + } + else + { + return Task.FromResult(AuthenticateResult.NoResult()); + } + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + // We would normally set the www-authenticate header here, but IIS does that for us. + _context.Response.StatusCode = 401; + return Task.CompletedTask; + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + _context.Response.StatusCode = 403; + return Task.CompletedTask; + } + + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + _iisHttpContext = context.Features.Get(); + if (_iisHttpContext == null) + { + throw new InvalidOperationException("No IISHttpContext found."); + } + + Scheme = scheme; + _context = context; + + return Task.CompletedTask; + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISServerSetupFilter.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISServerSetupFilter.cs new file mode 100644 index 0000000000..f3c3613565 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IISServerSetupFilter.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal class IISServerSetupFilter : IStartupFilter + { + private string _virtualPath; + + public IISServerSetupFilter(string virtualPath) + { + _virtualPath = virtualPath; + } + + public Action Configure(Action next) + { + return app => + { + var server = app.ApplicationServices.GetService(); + if (server?.GetType() != typeof(IISHttpServer)) + { + throw new InvalidOperationException("Application is running inside IIS process but is not configured to use IIS server."); + } + + app.UsePathBase(_virtualPath); + next(app); + }; + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/AsyncIOEngine.Flush.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/AsyncIOEngine.Flush.cs new file mode 100644 index 0000000000..1187c5b3f2 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/AsyncIOEngine.Flush.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Server.IIS.Core.IO +{ + internal partial class AsyncIOEngine + { + internal class AsyncFlushOperation : AsyncIOOperation + { + private readonly AsyncIOEngine _engine; + + private IntPtr _requestHandler; + + public AsyncFlushOperation(AsyncIOEngine engine) + { + _engine = engine; + } + + public void Initialize(IntPtr requestHandler) + { + _requestHandler = requestHandler; + } + + protected override bool InvokeOperation(out int hr, out int bytes) + { + bytes = 0; + hr = NativeMethods.HttpFlushResponseBytes(_requestHandler, out var fCompletionExpected); + + return !fCompletionExpected; + } + + protected override void ResetOperation() + { + base.ResetOperation(); + + _requestHandler = default; + _engine.ReturnOperation(this); + } + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/AsyncIOEngine.Read.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/AsyncIOEngine.Read.cs new file mode 100644 index 0000000000..00c6709453 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/AsyncIOEngine.Read.cs @@ -0,0 +1,63 @@ +// 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.Buffers; + +namespace Microsoft.AspNetCore.Server.IIS.Core.IO +{ + internal partial class AsyncIOEngine + { + internal class AsyncReadOperation : AsyncIOOperation + { + private readonly AsyncIOEngine _engine; + + private MemoryHandle _inputHandle; + + private IntPtr _requestHandler; + + private Memory _memory; + + public AsyncReadOperation(AsyncIOEngine engine) + { + _engine = engine; + } + + public void Initialize(IntPtr requestHandler, Memory memory) + { + _requestHandler = requestHandler; + _memory = memory; + } + + protected override unsafe bool InvokeOperation(out int hr, out int bytes) + { + _inputHandle = _memory.Pin(); + hr = NativeMethods.HttpReadRequestBytes( + _requestHandler, + (byte*)_inputHandle.Pointer, + _memory.Length, + out bytes, + out bool completionExpected); + + return !completionExpected; + } + + protected override void ResetOperation() + { + base.ResetOperation(); + + _memory = default; + _inputHandle.Dispose(); + _inputHandle = default; + _requestHandler = default; + + _engine.ReturnOperation(this); + } + + public override void FreeOperationResources(int hr, int bytes) + { + _inputHandle.Dispose(); + } + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/AsyncIOEngine.Write.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/AsyncIOEngine.Write.cs new file mode 100644 index 0000000000..72be9f107b --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/AsyncIOEngine.Write.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 Microsoft.AspNetCore.HttpSys.Internal; + +namespace Microsoft.AspNetCore.Server.IIS.Core.IO +{ + internal partial class AsyncIOEngine + { + private class AsyncWriteOperation : AsyncWriteOperationBase + { + private readonly AsyncIOEngine _engine; + + public AsyncWriteOperation(AsyncIOEngine engine) + { + _engine = engine; + } + + protected override unsafe int WriteChunks(IntPtr requestHandler, int chunkCount, HttpApiTypes.HTTP_DATA_CHUNK* dataChunks, + out bool completionExpected) + { + return NativeMethods.HttpWriteResponseBytes(requestHandler, dataChunks, chunkCount, out completionExpected); + } + + protected override void ResetOperation() + { + base.ResetOperation(); + + _engine.ReturnOperation(this); + } + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/AsyncIOEngine.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/AsyncIOEngine.cs new file mode 100644 index 0000000000..f169cebf77 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/AsyncIOEngine.cs @@ -0,0 +1,176 @@ +// 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.Buffers; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Server.IIS.Core.IO +{ + internal partial class AsyncIOEngine : IAsyncIOEngine + { + private readonly object _contextSync; + private readonly IntPtr _handler; + + private bool _stopped; + + private AsyncIOOperation _nextOperation; + private AsyncIOOperation _runningOperation; + + private AsyncReadOperation _cachedAsyncReadOperation; + private AsyncWriteOperation _cachedAsyncWriteOperation; + private AsyncFlushOperation _cachedAsyncFlushOperation; + + public AsyncIOEngine(object contextSync, IntPtr handler) + { + _contextSync = contextSync; + _handler = handler; + } + + public ValueTask ReadAsync(Memory memory) + { + var read = GetReadOperation(); + read.Initialize(_handler, memory); + Run(read); + return new ValueTask(read, 0); + } + + public ValueTask WriteAsync(ReadOnlySequence data) + { + var write = GetWriteOperation(); + write.Initialize(_handler, data); + Run(write); + return new ValueTask(write, 0); + } + + private void Run(AsyncIOOperation ioOperation) + { + lock (_contextSync) + { + if (_stopped) + { + // Abort all operation after IO was stopped + ioOperation.Complete(NativeMethods.ERROR_OPERATION_ABORTED, 0); + return; + } + + if (_runningOperation != null) + { + if (_nextOperation == null) + { + _nextOperation = ioOperation; + + // If there is an active read cancel it + if (_runningOperation is AsyncReadOperation) + { + NativeMethods.HttpTryCancelIO(_handler); + } + } + else + { + throw new InvalidOperationException("Only one queued operation is allowed"); + } + } + else + { + // we are just starting operation so there would be no + // continuation registered + var completed = ioOperation.Invoke() != null; + + // operation went async + if (!completed) + { + _runningOperation = ioOperation; + } + } + } + } + + + public ValueTask FlushAsync() + { + var flush = GetFlushOperation(); + flush.Initialize(_handler); + Run(flush); + return new ValueTask(flush, 0); + } + + public void NotifyCompletion(int hr, int bytes) + { + AsyncIOOperation.AsyncContinuation continuation; + AsyncIOOperation.AsyncContinuation? nextContinuation = null; + + lock (_contextSync) + { + Debug.Assert(_runningOperation != null); + + continuation = _runningOperation.Complete(hr, bytes); + + var next = _nextOperation; + _nextOperation = null; + _runningOperation = null; + + if (next != null) + { + if (_stopped) + { + // Abort next operation if IO is stopped + nextContinuation = next.Complete(NativeMethods.ERROR_OPERATION_ABORTED, 0); + } + else + { + nextContinuation = next.Invoke(); + + // operation went async + if (nextContinuation == null) + { + _runningOperation = next; + } + } + } + } + + continuation.Invoke(); + nextContinuation?.Invoke(); + } + + public void Dispose() + { + lock (_contextSync) + { + _stopped = true; + NativeMethods.HttpTryCancelIO(_handler); + } + } + + private AsyncReadOperation GetReadOperation() => + Interlocked.Exchange(ref _cachedAsyncReadOperation, null) ?? + new AsyncReadOperation(this); + + private AsyncWriteOperation GetWriteOperation() => + Interlocked.Exchange(ref _cachedAsyncWriteOperation, null) ?? + new AsyncWriteOperation(this); + + private AsyncFlushOperation GetFlushOperation() => + Interlocked.Exchange(ref _cachedAsyncFlushOperation, null) ?? + new AsyncFlushOperation(this); + + private void ReturnOperation(AsyncReadOperation operation) + { + Volatile.Write(ref _cachedAsyncReadOperation, operation); + } + + private void ReturnOperation(AsyncWriteOperation operation) + { + Volatile.Write(ref _cachedAsyncWriteOperation, operation); + } + + private void ReturnOperation(AsyncFlushOperation operation) + { + Volatile.Write(ref _cachedAsyncFlushOperation, operation); + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/AsyncIOOperation.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/AsyncIOOperation.cs new file mode 100644 index 0000000000..80e9234ea2 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/AsyncIOOperation.cs @@ -0,0 +1,162 @@ +// 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.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks.Sources; +using Microsoft.AspNetCore.Connections; + +namespace Microsoft.AspNetCore.Server.IIS.Core.IO +{ + internal abstract class AsyncIOOperation: IValueTaskSource, IValueTaskSource + { + private static readonly Action CallbackCompleted = _ => { Debug.Assert(false, "Should not be invoked"); }; + + private Action _continuation; + private object _state; + private int _result; + + private Exception _exception; + + public ValueTaskSourceStatus GetStatus(short token) + { + if (ReferenceEquals(Volatile.Read(ref _continuation), null)) + { + return ValueTaskSourceStatus.Pending; + } + + return _exception != null ? ValueTaskSourceStatus.Succeeded : ValueTaskSourceStatus.Faulted; + } + + public void OnCompleted(Action continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) + { + if (_state != null) + { + ThrowMultipleContinuations(); + } + + _state = state; + + var previousContinuation = Interlocked.CompareExchange(ref _continuation, continuation, null); + + if (previousContinuation != null) + { + if (!ReferenceEquals(previousContinuation, CallbackCompleted)) + { + ThrowMultipleContinuations(); + } + + new AsyncContinuation(continuation, state).Invoke(); + } + } + + private static void ThrowMultipleContinuations() + { + throw new InvalidOperationException("Multiple awaiters are not allowed"); + } + + void IValueTaskSource.GetResult(short token) + { + var exception = _exception; + + ResetOperation(); + + if (exception != null) + { + throw exception; + } + } + + public int GetResult(short token) + { + var exception = _exception; + var result = _result; + + ResetOperation(); + + if (exception != null) + { + throw exception; + } + + return result; + } + + public AsyncContinuation? Invoke() + { + if (InvokeOperation(out var hr, out var bytes)) + { + return Complete(hr, bytes); + } + return null; + } + + protected abstract bool InvokeOperation(out int hr, out int bytes); + + public AsyncContinuation Complete(int hr, int bytes) + { + if (hr != NativeMethods.ERROR_OPERATION_ABORTED) + { + _result = bytes; + if (hr != NativeMethods.HR_OK) + { + // Treat all errors as the client disconnect + _exception = new ConnectionResetException("The client has disconnected", Marshal.GetExceptionForHR(hr)); + } + } + else + { + _result = -1; + _exception = null; + } + + AsyncContinuation asyncContinuation = default; + var continuation = Interlocked.CompareExchange(ref _continuation, CallbackCompleted, null); + if (continuation != null) + { + asyncContinuation = new AsyncContinuation(continuation, _state); + } + + FreeOperationResources(hr, bytes); + + return asyncContinuation; + } + + public virtual void FreeOperationResources(int hr, int bytes) { } + + protected virtual void ResetOperation() + { + _exception = null; + _result = int.MinValue; + _state = null; + _continuation = null; + } + + public readonly struct AsyncContinuation + { + public Action Continuation { get; } + public object State { get; } + + public AsyncContinuation(Action continuation, object state) + { + Continuation = continuation; + State = state; + } + + public void Invoke() + { + if (Continuation != null) + { + // TODO: use generic overload when code moved to be netcoreapp only + var continuation = Continuation; + var state = State; + ThreadPool.QueueUserWorkItem(_ => continuation(state)); + } + } + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/AsyncWriteOperationBase.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/AsyncWriteOperationBase.cs new file mode 100644 index 0000000000..3be67095fd --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/AsyncWriteOperationBase.cs @@ -0,0 +1,119 @@ +// 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.Buffers; +using Microsoft.AspNetCore.HttpSys.Internal; + +namespace Microsoft.AspNetCore.Server.IIS.Core.IO +{ + internal abstract class AsyncWriteOperationBase : AsyncIOOperation + { + private const int HttpDataChunkStackLimit = 128; // 16 bytes per HTTP_DATA_CHUNK + + private IntPtr _requestHandler; + private ReadOnlySequence _buffer; + private MemoryHandle[] _handles; + + public void Initialize(IntPtr requestHandler, ReadOnlySequence buffer) + { + _requestHandler = requestHandler; + _buffer = buffer; + } + + protected override unsafe bool InvokeOperation(out int hr, out int bytes) + { + if (_buffer.Length > int.MaxValue) + { + throw new InvalidOperationException($"Writes larger then {int.MaxValue} are not supported."); + } + + bool completionExpected; + var chunkCount = GetChunkCount(); + + var bufferLength = (int)_buffer.Length; + + if (chunkCount < HttpDataChunkStackLimit) + { + // To avoid stackoverflows, we will only stackalloc if the write size is less than the StackChunkLimit + // The stack size is IIS is by default 128/256 KB, so we are generous with this threshold. + var chunks = stackalloc HttpApiTypes.HTTP_DATA_CHUNK[chunkCount]; + hr = WriteSequence(chunkCount, _buffer, chunks, out completionExpected); + } + else + { + // Otherwise allocate the chunks on the heap. + var chunks = new HttpApiTypes.HTTP_DATA_CHUNK[chunkCount]; + fixed (HttpApiTypes.HTTP_DATA_CHUNK* pDataChunks = chunks) + { + hr = WriteSequence(chunkCount, _buffer, pDataChunks, out completionExpected); + } + } + + bytes = bufferLength; + return !completionExpected; + } + + public override void FreeOperationResources(int hr, int bytes) + { + // Free the handles + foreach (var handle in _handles) + { + handle.Dispose(); + } + } + + protected override void ResetOperation() + { + base.ResetOperation(); + + _requestHandler = default; + _buffer = default; + _handles.AsSpan().Clear(); + } + + private int GetChunkCount() + { + if (_buffer.IsSingleSegment) + { + return 1; + } + + var count = 0; + + foreach (var _ in _buffer) + { + count++; + } + + return count; + } + + private unsafe int WriteSequence(int nChunks, ReadOnlySequence buffer, HttpApiTypes.HTTP_DATA_CHUNK* pDataChunks, out bool fCompletionExpected) + { + var currentChunk = 0; + + if (_handles == null || _handles.Length < nChunks) + { + _handles = new MemoryHandle[nChunks]; + } + + foreach (var readOnlyMemory in buffer) + { + ref var handle = ref _handles[currentChunk]; + ref var chunk = ref pDataChunks[currentChunk]; + handle = readOnlyMemory.Pin(); + + chunk.DataChunkType = HttpApiTypes.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory; + chunk.fromMemory.BufferLength = (uint)readOnlyMemory.Length; + chunk.fromMemory.pBuffer = (IntPtr)handle.Pointer; + + currentChunk++; + } + + return WriteChunks(_requestHandler, nChunks, pDataChunks, out fCompletionExpected); + } + + protected abstract unsafe int WriteChunks(IntPtr requestHandler, int chunkCount, HttpApiTypes.HTTP_DATA_CHUNK* dataChunks, out bool completionExpected); + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/IAsyncIOEngine.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/IAsyncIOEngine.cs new file mode 100644 index 0000000000..6e6896b8c4 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/IAsyncIOEngine.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Server.IIS.Core.IO +{ + internal interface IAsyncIOEngine: IDisposable + { + ValueTask ReadAsync(Memory memory); + ValueTask WriteAsync(ReadOnlySequence data); + ValueTask FlushAsync(); + void NotifyCompletion(int hr, int bytes); + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/WebSocketsAsyncIOEngine.Initialize.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/WebSocketsAsyncIOEngine.Initialize.cs new file mode 100644 index 0000000000..10e1f0da7d --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/WebSocketsAsyncIOEngine.Initialize.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Server.IIS.Core.IO +{ + internal partial class WebSocketsAsyncIOEngine + { + internal class AsyncInitializeOperation : AsyncIOOperation + { + private readonly WebSocketsAsyncIOEngine _engine; + + private IntPtr _requestHandler; + + public AsyncInitializeOperation(WebSocketsAsyncIOEngine engine) + { + _engine = engine; + } + + public void Initialize(IntPtr requestHandler) + { + _requestHandler = requestHandler; + } + + protected override bool InvokeOperation(out int hr, out int bytes) + { + hr = NativeMethods.HttpFlushResponseBytes(_requestHandler, out var completionExpected); + bytes = 0; + return !completionExpected; + } + + protected override void ResetOperation() + { + base.ResetOperation(); + + _requestHandler = default; + _engine.ReturnOperation(this); + } + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/WebSocketsAsyncIOEngine.Read.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/WebSocketsAsyncIOEngine.Read.cs new file mode 100644 index 0000000000..413fa77703 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/WebSocketsAsyncIOEngine.Read.cs @@ -0,0 +1,79 @@ +// 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.Buffers; +using System.Runtime.InteropServices; + +namespace Microsoft.AspNetCore.Server.IIS.Core.IO +{ + internal partial class WebSocketsAsyncIOEngine + { + internal class WebSocketReadOperation : AsyncIOOperation + { + public static readonly NativeMethods.PFN_WEBSOCKET_ASYNC_COMPLETION ReadCallback = (httpContext, completionInfo, completionContext) => + { + var context = (WebSocketReadOperation)GCHandle.FromIntPtr(completionContext).Target; + + NativeMethods.HttpGetCompletionInfo(completionInfo, out var cbBytes, out var hr); + + var continuation = context.Complete(hr, cbBytes); + + continuation.Invoke(); + + return NativeMethods.REQUEST_NOTIFICATION_STATUS.RQ_NOTIFICATION_PENDING; + }; + + private readonly WebSocketsAsyncIOEngine _engine; + private readonly GCHandle _thisHandle; + private MemoryHandle _inputHandle; + private IntPtr _requestHandler; + private Memory _memory; + + public WebSocketReadOperation(WebSocketsAsyncIOEngine engine) + { + _engine = engine; + _thisHandle = GCHandle.Alloc(this); + } + + protected override unsafe bool InvokeOperation(out int hr, out int bytes) + { + _inputHandle = _memory.Pin(); + + hr = NativeMethods.HttpWebsocketsReadBytes( + _requestHandler, + (byte*)_inputHandle.Pointer, + _memory.Length, + ReadCallback, + (IntPtr)_thisHandle, + out bytes, + out var completionExpected); + + return !completionExpected; + } + + public void Initialize(IntPtr requestHandler, Memory memory) + { + _requestHandler = requestHandler; + _memory = memory; + } + + public override void FreeOperationResources(int hr, int bytes) + { + _inputHandle.Dispose(); + } + + protected override void ResetOperation() + { + base.ResetOperation(); + + _memory = default; + _inputHandle.Dispose(); + _inputHandle = default; + _requestHandler = default; + + _engine.ReturnOperation(this); + } + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/WebSocketsAsyncIOEngine.Write.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/WebSocketsAsyncIOEngine.Write.cs new file mode 100644 index 0000000000..3eff3bba46 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/WebSocketsAsyncIOEngine.Write.cs @@ -0,0 +1,49 @@ +// 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.Runtime.InteropServices; +using Microsoft.AspNetCore.HttpSys.Internal; + +namespace Microsoft.AspNetCore.Server.IIS.Core.IO +{ + internal partial class WebSocketsAsyncIOEngine + { + internal sealed class WebSocketWriteOperation : AsyncWriteOperationBase + { + + private static readonly NativeMethods.PFN_WEBSOCKET_ASYNC_COMPLETION WriteCallback = (httpContext, completionInfo, completionContext) => + { + var context = (WebSocketWriteOperation)GCHandle.FromIntPtr(completionContext).Target; + + NativeMethods.HttpGetCompletionInfo(completionInfo, out var cbBytes, out var hr); + + var continuation = context.Complete(hr, cbBytes); + continuation.Invoke(); + + return NativeMethods.REQUEST_NOTIFICATION_STATUS.RQ_NOTIFICATION_PENDING; + }; + + private readonly WebSocketsAsyncIOEngine _engine; + private readonly GCHandle _thisHandle; + + public WebSocketWriteOperation(WebSocketsAsyncIOEngine engine) + { + _engine = engine; + _thisHandle = GCHandle.Alloc(this); + } + + protected override unsafe int WriteChunks(IntPtr requestHandler, int chunkCount, HttpApiTypes.HTTP_DATA_CHUNK* dataChunks, out bool completionExpected) + { + return NativeMethods.HttpWebsocketsWriteBytes(requestHandler, dataChunks, chunkCount, WriteCallback, (IntPtr)_thisHandle, out completionExpected); + } + + protected override void ResetOperation() + { + base.ResetOperation(); + + _engine.ReturnOperation(this); + } + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/WebSocketsAsyncIOEngine.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/WebSocketsAsyncIOEngine.cs new file mode 100644 index 0000000000..38c737dbce --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/IO/WebSocketsAsyncIOEngine.cs @@ -0,0 +1,142 @@ +// 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.Buffers; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Server.IIS.Core.IO +{ + internal partial class WebSocketsAsyncIOEngine: IAsyncIOEngine + { + private readonly object _contextLock; + + private readonly IntPtr _handler; + + private bool _isInitialized; + + private AsyncInitializeOperation _initializationFlush; + + private WebSocketWriteOperation _cachedWebSocketWriteOperation; + + private WebSocketReadOperation _cachedWebSocketReadOperation; + + private AsyncInitializeOperation _cachedAsyncInitializeOperation; + + public WebSocketsAsyncIOEngine(object contextLock, IntPtr handler) + { + _contextLock = contextLock; + _handler = handler; + } + + public ValueTask ReadAsync(Memory memory) + { + lock (_contextLock) + { + ThrowIfNotInitialized(); + + var read = GetReadOperation(); + read.Initialize(_handler, memory); + read.Invoke(); + return new ValueTask(read, 0); + } + } + + public ValueTask WriteAsync(ReadOnlySequence data) + { + lock (_contextLock) + { + ThrowIfNotInitialized(); + + var write = GetWriteOperation(); + write.Initialize(_handler, data); + write.Invoke(); + return new ValueTask(write, 0); + } + } + + public ValueTask FlushAsync() + { + lock (_contextLock) + { + if (_isInitialized) + { + return new ValueTask(Task.CompletedTask); + } + + NativeMethods.HttpEnableWebsockets(_handler); + + var init = GetInitializeOperation(); + init.Initialize(_handler); + + var continuation = init.Invoke(); + + if (continuation != null) + { + _isInitialized = true; + } + else + { + _initializationFlush = init; + } + + return new ValueTask(init, 0); + } + } + + public void NotifyCompletion(int hr, int bytes) + { + _isInitialized = true; + + var init = _initializationFlush; + if (init == null) + { + throw new InvalidOperationException("Unexpected completion for WebSocket operation"); + } + + var continuation = init.Complete(hr, bytes); + + _initializationFlush = null; + + continuation.Invoke(); + } + + private void ThrowIfNotInitialized() + { + if (!_isInitialized) + { + throw new InvalidOperationException("Socket IO not initialized yet"); + } + } + + public void Dispose() + { + lock (_contextLock) + { + NativeMethods.HttpTryCancelIO(_handler); + } + } + + private WebSocketReadOperation GetReadOperation() => + Interlocked.Exchange(ref _cachedWebSocketReadOperation, null) ?? + new WebSocketReadOperation(this); + + private WebSocketWriteOperation GetWriteOperation() => + Interlocked.Exchange(ref _cachedWebSocketWriteOperation, null) ?? + new WebSocketWriteOperation(this); + + private AsyncInitializeOperation GetInitializeOperation() => + Interlocked.Exchange(ref _cachedAsyncInitializeOperation, null) ?? + new AsyncInitializeOperation(this); + + private void ReturnOperation(AsyncInitializeOperation operation) => + Volatile.Write(ref _cachedAsyncInitializeOperation, operation); + + private void ReturnOperation(WebSocketWriteOperation operation) => + Volatile.Write(ref _cachedWebSocketWriteOperation, operation); + + private void ReturnOperation(WebSocketReadOperation operation) => + Volatile.Write(ref _cachedWebSocketReadOperation, operation); + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/OutputProducer.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/OutputProducer.cs new file mode 100644 index 0000000000..431f5c809a --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/OutputProducer.cs @@ -0,0 +1,152 @@ +// 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.Buffers; +using System.Diagnostics; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal class OutputProducer + { + // This locks access to to all of the below fields + private readonly object _contextLock = new object(); + + private ValueTask _flushTask; + private bool _completed = false; + + private readonly Pipe _pipe; + + // https://github.com/dotnet/corefxlab/issues/1334 + // Pipelines don't support multiple awaiters on flush + // this is temporary until it does + private TaskCompletionSource _flushTcs; + private readonly object _flushLock = new object(); + private Action _flushCompleted; + + public OutputProducer(Pipe pipe) + { + _pipe = pipe; + _flushCompleted = OnFlushCompleted; + } + + public PipeReader Reader => _pipe.Reader; + + public Task FlushAsync(CancellationToken cancellationToken) + { + _pipe.Reader.CancelPendingRead(); + // Await backpressure + return FlushAsync(_pipe.Writer, cancellationToken); + } + + public void Dispose() + { + lock (_contextLock) + { + if (_completed) + { + return; + } + + _completed = true; + _pipe.Writer.Complete(); + } + } + + public void Abort(Exception error) + { + lock (_contextLock) + { + if (_completed) + { + return; + } + + _completed = true; + + _pipe.Reader.CancelPendingRead(); + _pipe.Writer.Complete(); + } + } + + public Task WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) + { + lock (_contextLock) + { + if (_completed) + { + return Task.CompletedTask; + } + + _pipe.Writer.Write(buffer.Span); + } + + return FlushAsync(_pipe.Writer, cancellationToken); + } + + private Task FlushAsync(PipeWriter pipeWriter, CancellationToken cancellationToken) + { + var awaitable = pipeWriter.FlushAsync(cancellationToken); + if (awaitable.IsCompleted) + { + // The flush task can't fail today + return Task.CompletedTask; + } + return FlushAsyncAwaited(awaitable, cancellationToken); + } + + private async Task FlushAsyncAwaited(ValueTask awaitable, CancellationToken cancellationToken) + { + // https://github.com/dotnet/corefxlab/issues/1334 + // Since the flush awaitable doesn't currently support multiple awaiters + // we need to use a task to track the callbacks. + // All awaiters get the same task + lock (_flushLock) + { + _flushTask = awaitable; + if (_flushTcs == null || _flushTcs.Task.IsCompleted) + { + _flushTcs = new TaskCompletionSource(); + + _flushTask.GetAwaiter().OnCompleted(_flushCompleted); + } + } + + try + { + await _flushTcs.Task; + cancellationToken.ThrowIfCancellationRequested(); + } + catch (OperationCanceledException ex) + { + Abort(new ConnectionAbortedException(CoreStrings.ConnectionOrStreamAbortedByCancellationToken, ex)); + } + catch + { + // A canceled token is the only reason flush should ever throw. + Debug.Assert(false); + } + } + + private void OnFlushCompleted() + { + try + { + _flushTask.GetAwaiter().GetResult(); + _flushTcs.TrySetResult(null); + } + catch (Exception exception) + { + _flushTcs.TrySetResult(exception); + } + finally + { + _flushTask = default; + } + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/ReadOnlyStream.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/ReadOnlyStream.cs new file mode 100644 index 0000000000..b5bbbf6199 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/ReadOnlyStream.cs @@ -0,0 +1,63 @@ +// 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.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal abstract class ReadOnlyStream : Stream + { + public override bool CanRead => true; + + public override bool CanWrite => false; + + public override int WriteTimeout + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override bool CanSeek => false; + + public override long Length + => throw new NotSupportedException(); + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override void Flush() + { + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/Streams.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/Streams.cs new file mode 100644 index 0000000000..bc50edd01b --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/Streams.cs @@ -0,0 +1,68 @@ +// 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.Http.Features; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal class Streams + { + private static readonly ThrowingWasUpgradedWriteOnlyStream _throwingResponseStream + = new ThrowingWasUpgradedWriteOnlyStream(); + + private readonly IISHttpContext _context; + private readonly HttpResponseStream _response; + private readonly HttpRequestStream _request; + private readonly WrappingStream _upgradeableRequest; + private readonly WrappingStream _upgradeableResponse; + private EmptyStream _emptyRequest; + private Stream _upgradeStream; + + public Streams(IISHttpContext context) + { + _context = context; + _request = new HttpRequestStream(_context); + _response = new HttpResponseStream(_context, _context); + _upgradeableResponse = new WrappingStream(_response); + _upgradeableRequest = new WrappingStream(_request); + } + + public Stream Upgrade() + { + _upgradeStream = new HttpUpgradeStream(_request, _response); + + // causes writes to context.Response.Body to throw + _upgradeableResponse.SetInnerStream(_throwingResponseStream); + + _emptyRequest = new EmptyStream(_context); + + _upgradeableRequest.SetInnerStream(_emptyRequest); + // _upgradeStream always uses _response + return _upgradeStream; + } + + public (Stream request, Stream response) Start() + { + _request.StartAcceptingReads(_context); + _response.StartAcceptingWrites(); + + return (_upgradeableRequest, _upgradeableResponse); + } + + public void Stop() + { + _request.StopAcceptingReads(); + _emptyRequest?.StopAcceptingReads(); + _response.StopAcceptingWrites(); + } + + public void Abort(Exception error) + { + _request.Abort(error); + _emptyRequest?.Abort(error); + _response.Abort(); + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/ThrowingWasUpgradedWriteOnlyStream.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/ThrowingWasUpgradedWriteOnlyStream.cs new file mode 100644 index 0000000000..3990cd9865 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/ThrowingWasUpgradedWriteOnlyStream.cs @@ -0,0 +1,28 @@ +// 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.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + public class ThrowingWasUpgradedWriteOnlyStream : WriteOnlyStream + { + public override void Write(byte[] buffer, int offset, int count) + => throw new InvalidOperationException(CoreStrings.ResponseStreamWasUpgraded); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => throw new InvalidOperationException(CoreStrings.ResponseStreamWasUpgraded); + + public override void Flush() + => throw new InvalidOperationException(CoreStrings.ResponseStreamWasUpgraded); + + public override long Seek(long offset, SeekOrigin origin) + => throw new NotSupportedException(); + + public override void SetLength(long value) + => throw new NotSupportedException(); + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/WrappingStream.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/WrappingStream.cs new file mode 100644 index 0000000000..18ae443711 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/WrappingStream.cs @@ -0,0 +1,144 @@ +// 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.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal class WrappingStream : Stream + { + private Stream _inner; + private bool _disposed; + + public WrappingStream(Stream inner) + { + _inner = inner; + } + + public void SetInnerStream(Stream inner) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(WrappingStream)); + } + + _inner = inner; + } + + public override bool CanRead => _inner.CanRead; + + public override bool CanSeek => _inner.CanSeek; + + public override bool CanWrite => _inner.CanWrite; + + public override bool CanTimeout => _inner.CanTimeout; + + public override long Length => _inner.Length; + + public override long Position + { + get => _inner.Position; + set => _inner.Position = value; + } + + public override int ReadTimeout + { + get => _inner.ReadTimeout; + set => _inner.ReadTimeout = value; + } + + public override int WriteTimeout + { + get => _inner.WriteTimeout; + set => _inner.WriteTimeout = value; + } + + public override void Flush() + => _inner.Flush(); + + public override Task FlushAsync(CancellationToken cancellationToken) + => _inner.FlushAsync(cancellationToken); + + public override int Read(byte[] buffer, int offset, int count) + => _inner.Read(buffer, offset, count); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => _inner.ReadAsync(buffer, offset, count, cancellationToken); + +#if NETCOREAPP2_1 + public override ValueTask ReadAsync(Memory destination, CancellationToken cancellationToken = default) + => _inner.ReadAsync(destination, cancellationToken); +#elif NETSTANDARD2_0 +#else +#error TFMs need to be updated +#endif + + public override int ReadByte() + => _inner.ReadByte(); + + public override long Seek(long offset, SeekOrigin origin) + => _inner.Seek(offset, origin); + + public override void SetLength(long value) + => _inner.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) + => _inner.Write(buffer, offset, count); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => _inner.WriteAsync(buffer, offset, count, cancellationToken); + +#if NETCOREAPP2_1 + public override ValueTask WriteAsync(ReadOnlyMemory source, CancellationToken cancellationToken = default) + => _inner.WriteAsync(source, cancellationToken); +#elif NETSTANDARD2_0 +#else +#error TFMs need to be updated +#endif + + public override void WriteByte(byte value) + => _inner.WriteByte(value); + + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + => _inner.CopyToAsync(destination, bufferSize, cancellationToken); + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + => _inner.BeginRead(buffer, offset, count, callback, state); + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + => _inner.BeginWrite(buffer, offset, count, callback, state); + + public override int EndRead(IAsyncResult asyncResult) + => _inner.EndRead(asyncResult); + + public override void EndWrite(IAsyncResult asyncResult) + => _inner.EndWrite(asyncResult); + + public override object InitializeLifetimeService() + => _inner.InitializeLifetimeService(); + + public override void Close() + => _inner.Close(); + + public override bool Equals(object obj) + => _inner.Equals(obj); + + public override int GetHashCode() + => _inner.GetHashCode(); + + public override string ToString() + => _inner.ToString(); + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _disposed = true; + _inner.Dispose(); + } + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/WriteOnlyStream.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/WriteOnlyStream.cs new file mode 100644 index 0000000000..9ccf9c9cd1 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Core/WriteOnlyStream.cs @@ -0,0 +1,53 @@ +// 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.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + public abstract class WriteOnlyStream : Stream + { + public override bool CanRead => false; + + public override bool CanWrite => true; + + public override int ReadTimeout + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override bool CanSeek => false; + + public override long Length => throw new NotSupportedException(); + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/CoreStrings.resx b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/CoreStrings.resx new file mode 100644 index 0000000000..d6c2e7b974 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/CoreStrings.resx @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Cannot write to response body after connection has been upgraded. + + + The response has been aborted due to an unhandled application exception. + + + Cannot upgrade a non-upgradable request. Check IHttpUpgradeFeature.IsUpgradableRequest to determine if a request can be upgraded. + + + IHttpUpgradeFeature.UpgradeAsync was already called and can only be called once per connection. + + + Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead. + + + Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead. + + + Cannot write to the response body, the response has completed. + + + The connection was aborted by the application. + + + The connection or stream was aborted because a write operation was aborted with a CancellationToken. + + + {name} cannot be set because the response has already started. + + diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/HttpContextExtensions.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/HttpContextExtensions.cs new file mode 100644 index 0000000000..9c0092d3fe --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/HttpContextExtensions.cs @@ -0,0 +1,38 @@ +// 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 Microsoft.AspNetCore.Server.IIS +{ + /// + /// Extensions to that enable access to IIS features. + /// + public static class HttpContextExtensions + { + /// + /// Gets the value of a server variable for the current request. + /// + /// The http context for the request. + /// The name of the variable. + /// + /// null if the feature does not support the feature. + /// May return null or empty if the variable does not exist or is not set. + /// + /// + /// For a list of common server variables available in IIS, see http://go.microsoft.com/fwlink/?LinkId=52471. + /// + public static string GetIISServerVariable(this HttpContext context, string variableName) + { + var feature = context.Features.Get(); + + if (feature == null) + { + return null; + } + + return feature[variableName]; + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/IISServerDefaults.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/IISServerDefaults.cs new file mode 100644 index 0000000000..5ac1296597 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/IISServerDefaults.cs @@ -0,0 +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. + +namespace Microsoft.AspNetCore.Server.IIS +{ + public class IISServerDefaults + { + public const string AuthenticationScheme = "Windows"; + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/IISServerOptions.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/IISServerOptions.cs new file mode 100644 index 0000000000..feed9180a5 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/IISServerOptions.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Builder +{ + public class IISServerOptions + { + /// + /// If true the server should set HttpContext.User. If false the server will only provide an + /// identity when explicitly requested by the AuthenticationScheme. + /// Note Windows Authentication must also be enabled in IIS for this to work. + /// + public bool AutomaticAuthentication { get; set; } = true; + + /// + /// Sets the display name shown to users on login pages. The default is null. + /// + public string AuthenticationDisplayName { get; set; } + + /// + /// Used to indicate if the authentication handler should be registered. This is only done if ANCM indicates + /// IIS has a non-anonymous authentication enabled, or for back compat with ANCMs that did not provide this information. + /// + internal bool ForwardWindowsAuthentication { get; set; } = true; + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/IServerVariableFeature.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/IServerVariableFeature.cs new file mode 100644 index 0000000000..44f667cb41 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/IServerVariableFeature.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Http.Features +{ + /// + /// This feature provides access to request server variables set. + /// + /// This feature is only available when hosting ASP.NET Core in-process with IIS or IIS Express. + /// + /// + /// + /// For a list of common server variables available in IIS, see http://go.microsoft.com/fwlink/?LinkId=52471. + /// + public interface IServerVariablesFeature + { + /// + /// 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; set; } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Microsoft.AspNetCore.Server.IIS.csproj b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Microsoft.AspNetCore.Server.IIS.csproj new file mode 100644 index 0000000000..53145c2b29 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Microsoft.AspNetCore.Server.IIS.csproj @@ -0,0 +1,74 @@ + + + + netstandard2.0 + Microsoft.AspNetCore.Server.IIS + Provides support for hosting ASP.NET Core in IIS using the AspNetCoreModule. + $(NoWarn);CS1591 + true + aspnetcore;iis + true + true + netcoreapp2.2 + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + False + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Microsoft.AspNetCore.Server.IIS.targets b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Microsoft.AspNetCore.Server.IIS.targets new file mode 100644 index 0000000000..a25bbf3b4f --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Microsoft.AspNetCore.Server.IIS.targets @@ -0,0 +1,13 @@ + + + + + + + + AspNetCoreModuleV2 + + + diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/NativeMethods.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/NativeMethods.cs new file mode 100644 index 0000000000..4d24b3dd77 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/NativeMethods.cs @@ -0,0 +1,305 @@ +// 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.Runtime.InteropServices; +using Microsoft.AspNetCore.HttpSys.Internal; +using Microsoft.AspNetCore.Server.IIS.Core; + +namespace Microsoft.AspNetCore.Server.IIS +{ + internal static class NativeMethods + { + internal const int HR_OK = 0; + internal const int ERROR_NOT_FOUND = unchecked((int)0x80070490); + internal const int ERROR_OPERATION_ABORTED = unchecked((int)0x800703E3); + internal const int ERROR_INVALID_PARAMETER = unchecked((int)0x80070057); + internal const int COR_E_IO = unchecked((int)0x80131620); + + private const string KERNEL32 = "kernel32.dll"; + + internal const string AspNetCoreModuleDll = "aspnetcorev2_inprocess.dll"; + + [DllImport(KERNEL32, ExactSpelling = true, SetLastError = true)] + + public static extern bool CloseHandle(IntPtr handle); + + [DllImport("kernel32.dll")] + private static extern IntPtr GetModuleHandle(string lpModuleName); + + public static bool IsAspNetCoreModuleLoaded() + { + return GetModuleHandle(AspNetCoreModuleDll) != IntPtr.Zero; + } + + public enum REQUEST_NOTIFICATION_STATUS + { + RQ_NOTIFICATION_CONTINUE, + RQ_NOTIFICATION_PENDING, + RQ_NOTIFICATION_FINISH_REQUEST + } + + public delegate REQUEST_NOTIFICATION_STATUS PFN_REQUEST_HANDLER(IntPtr pInProcessHandler, IntPtr pvRequestContext); + public delegate void PFN_DISCONNECT_HANDLER(IntPtr pvManagedHttpContext); + public delegate bool PFN_SHUTDOWN_HANDLER(IntPtr pvRequestContext); + public delegate REQUEST_NOTIFICATION_STATUS PFN_ASYNC_COMPLETION(IntPtr pvManagedHttpContext, int hr, int bytes); + public delegate REQUEST_NOTIFICATION_STATUS PFN_WEBSOCKET_ASYNC_COMPLETION(IntPtr pInProcessHandler, IntPtr completionInfo, IntPtr pvCompletionContext); + + [DllImport(AspNetCoreModuleDll)] + private static extern int http_post_completion(IntPtr pInProcessHandler, int cbBytes); + + [DllImport(AspNetCoreModuleDll)] + private static extern int http_set_completion_status(IntPtr pInProcessHandler, REQUEST_NOTIFICATION_STATUS rquestNotificationStatus); + + [DllImport(AspNetCoreModuleDll)] + private static extern void http_indicate_completion(IntPtr pInProcessHandler, REQUEST_NOTIFICATION_STATUS notificationStatus); + + [DllImport(AspNetCoreModuleDll)] + private static extern int register_callbacks(IntPtr pInProcessApplication, + PFN_REQUEST_HANDLER requestCallback, + PFN_SHUTDOWN_HANDLER shutdownCallback, + PFN_DISCONNECT_HANDLER disconnectCallback, + PFN_ASYNC_COMPLETION asyncCallback, + IntPtr pvRequestContext, + IntPtr pvShutdownContext); + + [DllImport(AspNetCoreModuleDll)] + private static extern unsafe int http_write_response_bytes(IntPtr pInProcessHandler, HttpApiTypes.HTTP_DATA_CHUNK* pDataChunks, int nChunks, out bool fCompletionExpected); + + [DllImport(AspNetCoreModuleDll)] + private static extern int http_flush_response_bytes(IntPtr pInProcessHandler, out bool fCompletionExpected); + + [DllImport(AspNetCoreModuleDll)] + private static extern unsafe HttpApiTypes.HTTP_REQUEST_V2* http_get_raw_request(IntPtr pInProcessHandler); + + [DllImport(AspNetCoreModuleDll)] + private static extern int http_stop_calls_into_managed(IntPtr pInProcessApplication); + + [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); + + [DllImport(AspNetCoreModuleDll)] + private static extern unsafe int http_read_request_bytes(IntPtr pInProcessHandler, byte* pvBuffer, int cbBuffer, out int dwBytesReceived, out bool fCompletionExpected); + + [DllImport(AspNetCoreModuleDll)] + private static extern void http_get_completion_info(IntPtr pCompletionInfo, out int cbBytes, out int hr); + + [DllImport(AspNetCoreModuleDll)] + private static extern int http_set_managed_context(IntPtr pInProcessHandler, IntPtr pvManagedContext); + + [DllImport(AspNetCoreModuleDll)] + private static extern int http_get_application_properties(ref IISConfigurationData iiConfigData); + + [DllImport(AspNetCoreModuleDll)] + private static extern int http_get_server_variable( + IntPtr pInProcessHandler, + [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, + byte* pvBuffer, + int cbBuffer, + PFN_WEBSOCKET_ASYNC_COMPLETION pfnCompletionCallback, + IntPtr pvCompletionContext, + out int dwBytesReceived, + out bool fCompletionExpected); + + [DllImport(AspNetCoreModuleDll)] + private static extern unsafe int http_websockets_write_bytes( + IntPtr pInProcessHandler, + HttpApiTypes.HTTP_DATA_CHUNK* pDataChunks, + int nChunks, + PFN_WEBSOCKET_ASYNC_COMPLETION pfnCompletionCallback, + IntPtr pvCompletionContext, + out bool fCompletionExpected); + + [DllImport(AspNetCoreModuleDll)] + private static extern int http_enable_websockets(IntPtr pInProcessHandler); + + [DllImport(AspNetCoreModuleDll)] + private static extern int http_cancel_io(IntPtr pInProcessHandler); + + [DllImport(AspNetCoreModuleDll)] + private static extern int http_close_connection(IntPtr pInProcessHandler); + + [DllImport(AspNetCoreModuleDll)] + private static extern unsafe int http_response_set_unknown_header(IntPtr pInProcessHandler, byte* pszHeaderName, byte* pszHeaderValue, ushort usHeaderValueLength, bool fReplace); + + [DllImport(AspNetCoreModuleDll)] + private static extern unsafe int http_response_set_known_header(IntPtr pInProcessHandler, int headerId, byte* pHeaderValue, ushort length, bool fReplace); + + [DllImport(AspNetCoreModuleDll)] + private static extern int http_get_authentication_information(IntPtr pInProcessHandler, [MarshalAs(UnmanagedType.BStr)] out string authType, out IntPtr token); + + public static void HttpPostCompletion(IntPtr pInProcessHandler, int cbBytes) + { + Validate(http_post_completion(pInProcessHandler, cbBytes)); + } + + public static void HttpSetCompletionStatus(IntPtr pInProcessHandler, REQUEST_NOTIFICATION_STATUS rquestNotificationStatus) + { + Validate(http_set_completion_status(pInProcessHandler, rquestNotificationStatus)); + } + + public static void HttpRegisterCallbacks(IntPtr pInProcessApplication, + PFN_REQUEST_HANDLER requestCallback, + PFN_SHUTDOWN_HANDLER shutdownCallback, + PFN_DISCONNECT_HANDLER disconnectCallback, + PFN_ASYNC_COMPLETION asyncCallback, + IntPtr pvRequestContext, + IntPtr pvShutdownContext) + { + Validate(register_callbacks(pInProcessApplication, requestCallback, shutdownCallback, disconnectCallback, asyncCallback, pvRequestContext, pvShutdownContext)); + } + + public static unsafe int HttpWriteResponseBytes(IntPtr pInProcessHandler, HttpApiTypes.HTTP_DATA_CHUNK* pDataChunks, int nChunks, out bool fCompletionExpected) + { + return http_write_response_bytes(pInProcessHandler, pDataChunks, nChunks, out fCompletionExpected); + } + + public static int HttpFlushResponseBytes(IntPtr pInProcessHandler, out bool fCompletionExpected) + { + return http_flush_response_bytes(pInProcessHandler, out fCompletionExpected); + } + + public static unsafe HttpApiTypes.HTTP_REQUEST_V2* HttpGetRawRequest(IntPtr pInProcessHandler) + { + return http_get_raw_request(pInProcessHandler); + } + + public static void HttpStopCallsIntoManaged(IntPtr pInProcessApplication) + { + Validate(http_stop_calls_into_managed(pInProcessApplication)); + } + + public static void HttpStopIncomingRequests(IntPtr pInProcessApplication) + { + 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)); + } + + public static unsafe int HttpReadRequestBytes(IntPtr pInProcessHandler, byte* pvBuffer, int cbBuffer, out int dwBytesReceived, out bool fCompletionExpected) + { + return http_read_request_bytes(pInProcessHandler, pvBuffer, cbBuffer, out dwBytesReceived, out fCompletionExpected); + } + + public static void HttpGetCompletionInfo(IntPtr pCompletionInfo, out int cbBytes, out int hr) + { + http_get_completion_info(pCompletionInfo, out cbBytes, out hr); + } + + public static void HttpSetManagedContext(IntPtr pInProcessHandler, IntPtr pvManagedContext) + { + Validate(http_set_managed_context(pInProcessHandler, pvManagedContext)); + } + + public static IISConfigurationData HttpGetApplicationProperties() + { + var iisConfigurationData = new IISConfigurationData(); + Validate(http_get_application_properties(ref iisConfigurationData)); + return iisConfigurationData; + } + + public static bool HttpTryGetServerVariable(IntPtr pInProcessHandler, string variableName, out string value) + { + 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, + int cbBuffer, + PFN_WEBSOCKET_ASYNC_COMPLETION pfnCompletionCallback, + IntPtr pvCompletionContext, out int dwBytesReceived, + out bool fCompletionExpected) + { + return http_websockets_read_bytes(pInProcessHandler, pvBuffer, cbBuffer, pfnCompletionCallback, pvCompletionContext, out dwBytesReceived, out fCompletionExpected); + } + + public static unsafe int HttpWebsocketsWriteBytes( + IntPtr pInProcessHandler, + HttpApiTypes.HTTP_DATA_CHUNK* pDataChunks, + int nChunks, + PFN_WEBSOCKET_ASYNC_COMPLETION pfnCompletionCallback, + IntPtr pvCompletionContext, + out bool fCompletionExpected) + { + return http_websockets_write_bytes(pInProcessHandler, pDataChunks, nChunks, pfnCompletionCallback, pvCompletionContext, out fCompletionExpected); + } + + public static void HttpEnableWebsockets(IntPtr pInProcessHandler) + { + Validate(http_enable_websockets(pInProcessHandler)); + } + + public static bool HttpTryCancelIO(IntPtr pInProcessHandler) + { + var hr = http_cancel_io(pInProcessHandler); + // ERROR_NOT_FOUND is expected if async operation finished + // https://msdn.microsoft.com/en-us/library/windows/desktop/aa363792(v=vs.85).aspx + // ERROR_INVALID_PARAMETER is expected for "fake" requests like applicationInitialization ones + if (hr == ERROR_NOT_FOUND || hr == ERROR_INVALID_PARAMETER) + { + return false; + } + Validate(hr); + return true; + } + + public static void HttpCloseConnection(IntPtr pInProcessHandler) + { + Validate(http_close_connection(pInProcessHandler)); + } + + public static unsafe void HttpResponseSetUnknownHeader(IntPtr pInProcessHandler, byte* pszHeaderName, byte* pszHeaderValue, ushort usHeaderValueLength, bool fReplace) + { + Validate(http_response_set_unknown_header(pInProcessHandler, pszHeaderName, pszHeaderValue, usHeaderValueLength, fReplace)); + } + + public static unsafe void HttpResponseSetKnownHeader(IntPtr pInProcessHandler, int headerId, byte* pHeaderValue, ushort length, bool fReplace) + { + Validate(http_response_set_known_header(pInProcessHandler, headerId, pHeaderValue, length, fReplace)); + } + + public static void HttpGetAuthenticationInformation(IntPtr pInProcessHandler, out string authType, out IntPtr token) + { + Validate(http_get_authentication_information(pInProcessHandler, out authType, out token)); + } + + private static void Validate(int hr) + { + if (hr != HR_OK) + { + throw Marshal.GetExceptionForHR(hr); + } + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Properties/CoreStrings.Designer.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Properties/CoreStrings.Designer.cs new file mode 100644 index 0000000000..639480f4a5 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/Properties/CoreStrings.Designer.cs @@ -0,0 +1,170 @@ +// +namespace Microsoft.AspNetCore.Server.IIS +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class CoreStrings + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Server.IIS.CoreStrings", typeof(CoreStrings).GetTypeInfo().Assembly); + + /// + /// Cannot write to response body after connection has been upgraded. + /// + internal static string ResponseStreamWasUpgraded + { + get => GetString("ResponseStreamWasUpgraded"); + } + + /// + /// Cannot write to response body after connection has been upgraded. + /// + internal static string FormatResponseStreamWasUpgraded() + => GetString("ResponseStreamWasUpgraded"); + + /// + /// The response has been aborted due to an unhandled application exception. + /// + internal static string UnhandledApplicationException + { + get => GetString("UnhandledApplicationException"); + } + + /// + /// The response has been aborted due to an unhandled application exception. + /// + internal static string FormatUnhandledApplicationException() + => GetString("UnhandledApplicationException"); + + /// + /// Cannot upgrade a non-upgradable request. Check IHttpUpgradeFeature.IsUpgradableRequest to determine if a request can be upgraded. + /// + internal static string CannotUpgradeNonUpgradableRequest + { + get => GetString("CannotUpgradeNonUpgradableRequest"); + } + + /// + /// Cannot upgrade a non-upgradable request. Check IHttpUpgradeFeature.IsUpgradableRequest to determine if a request can be upgraded. + /// + internal static string FormatCannotUpgradeNonUpgradableRequest() + => GetString("CannotUpgradeNonUpgradableRequest"); + + /// + /// IHttpUpgradeFeature.UpgradeAsync was already called and can only be called once per connection. + /// + internal static string UpgradeCannotBeCalledMultipleTimes + { + get => GetString("UpgradeCannotBeCalledMultipleTimes"); + } + + /// + /// IHttpUpgradeFeature.UpgradeAsync was already called and can only be called once per connection. + /// + internal static string FormatUpgradeCannotBeCalledMultipleTimes() + => GetString("UpgradeCannotBeCalledMultipleTimes"); + + /// + /// Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead. + /// + internal static string SynchronousReadsDisallowed + { + get => GetString("SynchronousReadsDisallowed"); + } + + /// + /// Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead. + /// + internal static string FormatSynchronousReadsDisallowed() + => GetString("SynchronousReadsDisallowed"); + + /// + /// Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead. + /// + internal static string SynchronousWritesDisallowed + { + get => GetString("SynchronousWritesDisallowed"); + } + + /// + /// Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead. + /// + internal static string FormatSynchronousWritesDisallowed() + => GetString("SynchronousWritesDisallowed"); + + /// + /// Cannot write to the response body, the response has completed. + /// + internal static string WritingToResponseBodyAfterResponseCompleted + { + get => GetString("WritingToResponseBodyAfterResponseCompleted"); + } + + /// + /// Cannot write to the response body, the response has completed. + /// + internal static string FormatWritingToResponseBodyAfterResponseCompleted() + => GetString("WritingToResponseBodyAfterResponseCompleted"); + + /// + /// The connection was aborted by the application. + /// + internal static string ConnectionAbortedByApplication + { + get => GetString("ConnectionAbortedByApplication"); + } + + /// + /// The connection was aborted by the application. + /// + internal static string FormatConnectionAbortedByApplication() + => GetString("ConnectionAbortedByApplication"); + + /// + /// The connection or stream was aborted because a write operation was aborted with a CancellationToken. + /// + internal static string ConnectionOrStreamAbortedByCancellationToken + { + get => GetString("ConnectionOrStreamAbortedByCancellationToken"); + } + + /// + /// The connection or stream was aborted because a write operation was aborted with a CancellationToken. + /// + internal static string FormatConnectionOrStreamAbortedByCancellationToken() + => GetString("ConnectionOrStreamAbortedByCancellationToken"); + + /// + /// {name} cannot be set because the response has already started. + /// + internal static string ParameterReadOnlyAfterResponseStarted + { + get => GetString("ParameterReadOnlyAfterResponseStarted"); + } + + /// + /// {name} cannot be set because the response has already started. + /// + internal static string FormatParameterReadOnlyAfterResponseStarted(object name) + => string.Format(CultureInfo.CurrentCulture, GetString("ParameterReadOnlyAfterResponseStarted", "name"), name); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/WebHostBuilderIISExtensions.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/WebHostBuilderIISExtensions.cs new file mode 100644 index 0000000000..146f9a50f5 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/WebHostBuilderIISExtensions.cs @@ -0,0 +1,54 @@ +// 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.Runtime.InteropServices; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Server.IIS; +using Microsoft.AspNetCore.Server.IIS.Core; + +namespace Microsoft.AspNetCore.Hosting +{ + public static class WebHostBuilderIISExtensions + { + /// + /// Configures the port and base path the server should listen on when running behind AspNetCoreModule. + /// The app will also be configured to capture startup errors. + /// + /// + /// + public static IWebHostBuilder UseIIS(this IWebHostBuilder hostBuilder) + { + if (hostBuilder == null) + { + throw new ArgumentNullException(nameof(hostBuilder)); + } + + // Check if in process + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && NativeMethods.IsAspNetCoreModuleLoaded()) + { + hostBuilder.CaptureStartupErrors(true); + + var iisConfigData = NativeMethods.HttpGetApplicationProperties(); + // Trim trailing slash to be consistent with other servers + var contentRoot = iisConfigData.pwzFullApplicationPath.TrimEnd(Path.DirectorySeparatorChar); + hostBuilder.UseContentRoot(contentRoot); + return hostBuilder.ConfigureServices( + services => { + services.AddSingleton(new IISNativeApplication(iisConfigData.pNativeApplication)); + services.AddSingleton(); + services.AddSingleton(new IISServerSetupFilter(iisConfigData.pwzVirtualApplicationPath)); + services.AddAuthenticationCore(); + services.Configure( + options => { options.ForwardWindowsAuthentication = iisConfigData.fWindowsAuthEnabled || iisConfigData.fBasicAuthEnabled; } + ); + }); + } + + return hostBuilder; + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/_._ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/_._ new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/AuthenticationHandler.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/AuthenticationHandler.cs new file mode 100644 index 0000000000..1139969004 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/AuthenticationHandler.cs @@ -0,0 +1,95 @@ +// 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.Claims; +using System.Globalization; +using System.Security.Principal; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Server.IISIntegration +{ + internal class AuthenticationHandler : IAuthenticationHandler + { + private const string MSAspNetCoreWinAuthToken = "MS-ASPNETCORE-WINAUTHTOKEN"; + private static readonly Func ClearUserDelegate = ClearUser; + private WindowsPrincipal _user; + private HttpContext _context; + + internal AuthenticationScheme Scheme { get; private set; } + + public Task AuthenticateAsync() + { + var user = GetUser(); + if (user != null) + { + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(user, Scheme.Name))); + } + else + { + return Task.FromResult(AuthenticateResult.NoResult()); + } + } + + private WindowsPrincipal GetUser() + { + if (_user == null) + { + var tokenHeader = _context.Request.Headers[MSAspNetCoreWinAuthToken]; + + int hexHandle; + if (!StringValues.IsNullOrEmpty(tokenHeader) + && int.TryParse(tokenHeader, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out hexHandle)) + { + // Always create the identity if the handle exists, we need to dispose it so it does not leak. + var handle = new IntPtr(hexHandle); + var winIdentity = new WindowsIdentity(handle); + + // WindowsIdentity just duplicated the handle so we need to close the original. + NativeMethods.CloseHandle(handle); + + _context.Response.RegisterForDispose(winIdentity); + // We don't want loggers accessing a disposed identity. + // https://github.com/aspnet/Logging/issues/543#issuecomment-321907828 + _context.Response.OnCompleted(ClearUserDelegate, _context); + _user = new WindowsPrincipal(winIdentity); + } + } + + return _user; + } + + private static Task ClearUser(object arg) + { + var context = (HttpContext)arg; + if (context.User is WindowsPrincipal) + { + context.User = null; + } + return Task.CompletedTask; + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + // We would normally set the www-authenticate header here, but IIS does that for us. + _context.Response.StatusCode = 401; + return Task.CompletedTask; + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + _context.Response.StatusCode = 403; + return Task.CompletedTask; + } + + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + Scheme = scheme; + _context = context; + return Task.CompletedTask; + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/ForwardedTlsConnectionFeature.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/ForwardedTlsConnectionFeature.cs new file mode 100644 index 0000000000..c40ab415c7 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/ForwardedTlsConnectionFeature.cs @@ -0,0 +1,56 @@ +// 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.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Server.IISIntegration +{ + internal class ForwardedTlsConnectionFeature : ITlsConnectionFeature + { + private StringValues _header; + private X509Certificate2 _certificate; + private ILogger _logger; + + public ForwardedTlsConnectionFeature(ILogger logger, StringValues header) + { + _logger = logger; + _header = header; + } + + public X509Certificate2 ClientCertificate + { + get + { + if (_certificate == null && _header != StringValues.Empty) + { + try + { + var bytes = Convert.FromBase64String(_header); + _certificate = new X509Certificate2(bytes); + } + catch (Exception ex) + { + _logger.LogWarning(0, ex, "Failed to read the client certificate."); + } + } + return _certificate; + } + set + { + _certificate = value; + _header = StringValues.Empty; + } + } + + public Task GetClientCertificateAsync(CancellationToken cancellationToken) + { + return Task.FromResult(ClientCertificate); + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/IISDefaults.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/IISDefaults.cs new file mode 100644 index 0000000000..957273c094 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/IISDefaults.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Server.IISIntegration +{ + public class IISDefaults + { + public static readonly string AuthenticationScheme = "Windows"; + public const string Negotiate = "Negotiate"; + public const string Ntlm = "NTLM"; + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/IISHostingStartup.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/IISHostingStartup.cs new file mode 100644 index 0000000000..d701dc3181 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/IISHostingStartup.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Hosting; + +[assembly: HostingStartup(typeof(Microsoft.AspNetCore.Server.IISIntegration.IISHostingStartup))] + +namespace Microsoft.AspNetCore.Server.IISIntegration +{ + public class IISHostingStartup : IHostingStartup + { + public void Configure(IWebHostBuilder builder) + { + builder.UseIISIntegration(); + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/IISMiddleware.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/IISMiddleware.cs new file mode 100644 index 0000000000..013b15adfc --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/IISMiddleware.cs @@ -0,0 +1,150 @@ +// 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.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Server.IISIntegration +{ + public class IISMiddleware + { + private const string MSAspNetCoreClientCert = "MS-ASPNETCORE-CLIENTCERT"; + private const string MSAspNetCoreToken = "MS-ASPNETCORE-TOKEN"; + private const string MSAspNetCoreEvent = "MS-ASPNETCORE-EVENT"; + private const string ANCMShutdownEventHeaderValue = "shutdown"; + private static readonly PathString ANCMRequestPath = new PathString("/iisintegration"); + + private readonly RequestDelegate _next; + private readonly IISOptions _options; + private readonly ILogger _logger; + private readonly string _pairingToken; + private readonly IApplicationLifetime _applicationLifetime; + private readonly bool _isWebsocketsSupported; + + // Can't break public API, so creating a second constructor to propagate the isWebsocketsSupported flag. + public IISMiddleware(RequestDelegate next, + ILoggerFactory loggerFactory, + IOptions options, + string pairingToken, + IAuthenticationSchemeProvider authentication, + IApplicationLifetime applicationLifetime) + : this(next, loggerFactory, options, pairingToken, isWebsocketsSupported: true, authentication, applicationLifetime) + { + } + + public IISMiddleware(RequestDelegate next, + ILoggerFactory loggerFactory, + IOptions options, + string pairingToken, + bool isWebsocketsSupported, + IAuthenticationSchemeProvider authentication, + IApplicationLifetime applicationLifetime) + { + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + if (loggerFactory == null) + { + throw new ArgumentNullException(nameof(loggerFactory)); + } + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + if (applicationLifetime == null) + { + throw new ArgumentNullException(nameof(applicationLifetime)); + } + if (string.IsNullOrEmpty(pairingToken)) + { + throw new ArgumentException("Missing or empty pairing token."); + } + + _next = next; + _options = options.Value; + + if (_options.ForwardWindowsAuthentication) + { + authentication.AddScheme(new AuthenticationScheme(IISDefaults.AuthenticationScheme, _options.AuthenticationDisplayName, typeof(AuthenticationHandler))); + } + + _pairingToken = pairingToken; + _applicationLifetime = applicationLifetime; + _logger = loggerFactory.CreateLogger(); + _isWebsocketsSupported = isWebsocketsSupported; + } + + public async Task Invoke(HttpContext httpContext) + { + if (!string.Equals(_pairingToken, httpContext.Request.Headers[MSAspNetCoreToken], StringComparison.Ordinal)) + { + _logger.LogError($"'{MSAspNetCoreToken}' does not match the expected pairing token '{_pairingToken}', request rejected."); + httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return; + } + + // Handle shutdown from ANCM + if (HttpMethods.IsPost(httpContext.Request.Method) && + httpContext.Request.Path.Equals(ANCMRequestPath) && + string.Equals(ANCMShutdownEventHeaderValue, httpContext.Request.Headers[MSAspNetCoreEvent], StringComparison.OrdinalIgnoreCase)) + { + // Execute shutdown task on background thread without waiting for completion + var shutdownTask = Task.Run(() => _applicationLifetime.StopApplication()); + httpContext.Response.StatusCode = StatusCodes.Status202Accepted; + return; + } + + if (Debugger.IsAttached && string.Equals("DEBUG", httpContext.Request.Method, StringComparison.OrdinalIgnoreCase)) + { + // The Visual Studio debugger tooling sends a DEBUG request to make IIS & AspNetCoreModule launch the process + // so the debugger can attach. Filter out this request from the app. + return; + } + + var bodySizeFeature = httpContext.Features.Get(); + if (bodySizeFeature != null && !bodySizeFeature.IsReadOnly) + { + // IIS already limits this, no need to do it twice. + bodySizeFeature.MaxRequestBodySize = null; + } + + if (_options.ForwardClientCertificate) + { + var header = httpContext.Request.Headers[MSAspNetCoreClientCert]; + if (!StringValues.IsNullOrEmpty(header)) + { + httpContext.Features.Set(new ForwardedTlsConnectionFeature(_logger, header)); + } + } + + if (_options.ForwardWindowsAuthentication) + { + // We must always process and clean up the windows identity, even if we don't assign the User. + var result = await httpContext.AuthenticateAsync(IISDefaults.AuthenticationScheme); + if (result.Succeeded && _options.AutomaticAuthentication) + { + httpContext.User = result.Principal; + } + } + + // Remove the upgrade feature if websockets are not supported by ANCM. + // The feature must be removed on a per request basis as the Upgrade feature exists per request. + if (!_isWebsocketsSupported) + { + httpContext.Features.Set(null); + } + + await _next(httpContext); + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/IISOptions.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/IISOptions.cs new file mode 100644 index 0000000000..efd9eec1f3 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/IISOptions.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Builder +{ + public class IISOptions + { + /// + /// If true the middleware should set HttpContext.User. If false the middleware will only provide an + /// identity when explicitly requested by the AuthenticationScheme. + /// Note Windows Authentication must also be enabled in IIS for this to work. + /// + public bool AutomaticAuthentication { get; set; } = true; + + /// + /// Sets the display name shown to users on login pages. The default is null. + /// + public string AuthenticationDisplayName { get; set; } + + /// + /// Used to indicate if the authentication handler should be registered. This is only done if ANCM indicates + /// IIS has a non-anonymous authentication enabled, or for back compat with ANCMs that did not provide this information. + /// + internal bool ForwardWindowsAuthentication { get; set; } = true; + + /// + /// Populates the ITLSConnectionFeature if the MS-ASPNETCORE-CLIENTCERT request header is present. + /// + public bool ForwardClientCertificate { get; set; } = true; + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/IISSetupFilter.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/IISSetupFilter.cs new file mode 100644 index 0000000000..cb2d97f0f7 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/IISSetupFilter.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Server.IISIntegration +{ + internal class IISSetupFilter : IStartupFilter + { + private readonly string _pairingToken; + private readonly PathString _pathBase; + private readonly bool _isWebsocketsSupported; + + internal IISSetupFilter(string pairingToken, PathString pathBase, bool isWebsocketsSupported) + { + _pairingToken = pairingToken; + _pathBase = pathBase; + _isWebsocketsSupported = isWebsocketsSupported; + } + + public Action Configure(Action next) + { + return app => + { + app.UsePathBase(_pathBase); + app.UseForwardedHeaders(); + app.UseMiddleware(_pairingToken, _isWebsocketsSupported); + next(app); + }; + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/Microsoft.AspNetCore.Server.IISIntegration.csproj b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/Microsoft.AspNetCore.Server.IISIntegration.csproj new file mode 100644 index 0000000000..faae91cdbf --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/Microsoft.AspNetCore.Server.IISIntegration.csproj @@ -0,0 +1,29 @@ + + + + ASP.NET Core components for working with the IIS AspNetCoreModule. + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore;iis + true + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/Microsoft.AspNetCore.Server.IISIntegration.targets b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/Microsoft.AspNetCore.Server.IISIntegration.targets new file mode 100644 index 0000000000..a5f7fc6088 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/Microsoft.AspNetCore.Server.IISIntegration.targets @@ -0,0 +1,7 @@ + + + + AspNetCoreModuleV2 + + + diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/NativeMethods.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/NativeMethods.cs new file mode 100644 index 0000000000..02fc967f1a --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/NativeMethods.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.AspNetCore.Server.IISIntegration +{ + internal static class NativeMethods + { + private const string KERNEL32 = "kernel32.dll"; + + [DllImport(KERNEL32, ExactSpelling = true, SetLastError = true)] + + public static extern bool CloseHandle(IntPtr handle); + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/Properties/AssemblyInfo.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..f08839f842 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +// 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.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.IISIntegration.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] + diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/WebHostBuilderIISExtensions.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/WebHostBuilderIISExtensions.cs new file mode 100644 index 0000000000..b57ad38b00 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/WebHostBuilderIISExtensions.cs @@ -0,0 +1,104 @@ +// 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.Runtime.InteropServices; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.Server.IISIntegration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Hosting.Server; + +namespace Microsoft.AspNetCore.Hosting +{ + public static class WebHostBuilderIISExtensions + { + // These are defined as ASPNETCORE_ environment variables by IIS's AspNetCoreModule. + private static readonly string ServerPort = "PORT"; + private static readonly string ServerPath = "APPL_PATH"; + private static readonly string PairingToken = "TOKEN"; + private static readonly string IISAuth = "IIS_HTTPAUTH"; + private static readonly string IISWebSockets = "IIS_WEBSOCKETS_SUPPORTED"; + + /// + /// Configures the port and base path the server should listen on when running behind AspNetCoreModule. + /// The app will also be configured to capture startup errors. + /// + /// + /// + public static IWebHostBuilder UseIISIntegration(this IWebHostBuilder hostBuilder) + { + if (hostBuilder == null) + { + throw new ArgumentNullException(nameof(hostBuilder)); + } + + // Check if `UseIISIntegration` was called already + if (hostBuilder.GetSetting(nameof(UseIISIntegration)) != null) + { + return hostBuilder; + } + + var port = hostBuilder.GetSetting(ServerPort) ?? Environment.GetEnvironmentVariable($"ASPNETCORE_{ServerPort}"); + var path = hostBuilder.GetSetting(ServerPath) ?? Environment.GetEnvironmentVariable($"ASPNETCORE_{ServerPath}"); + var pairingToken = hostBuilder.GetSetting(PairingToken) ?? Environment.GetEnvironmentVariable($"ASPNETCORE_{PairingToken}"); + var iisAuth = hostBuilder.GetSetting(IISAuth) ?? Environment.GetEnvironmentVariable($"ASPNETCORE_{IISAuth}"); + var websocketsSupported = hostBuilder.GetSetting(IISWebSockets) ?? Environment.GetEnvironmentVariable($"ASPNETCORE_{IISWebSockets}"); + + bool isWebSocketsSupported; + if (!bool.TryParse(websocketsSupported, out isWebSocketsSupported)) + { + // If the websocket support variable is not set, we will always fallback to assuming websockets are enabled. + isWebSocketsSupported = (Environment.OSVersion.Version >= new Version(6, 2)); + } + + if (!string.IsNullOrEmpty(port) && !string.IsNullOrEmpty(path) && !string.IsNullOrEmpty(pairingToken)) + { + // Set flag to prevent double service configuration + hostBuilder.UseSetting(nameof(UseIISIntegration), true.ToString()); + + var enableAuth = false; + if (string.IsNullOrEmpty(iisAuth)) + { + // back compat with older ANCM versions + enableAuth = true; + } + else + { + // Lightup a new ANCM variable that tells us if auth is enabled. + foreach (var authType in iisAuth.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) + { + if (!string.Equals(authType, "anonymous", StringComparison.OrdinalIgnoreCase)) + { + enableAuth = true; + break; + } + } + } + + var address = "http://127.0.0.1:" + port; + hostBuilder.CaptureStartupErrors(true); + + hostBuilder.ConfigureServices(services => + { + // Delay register the url so users don't accidently overwrite it. + hostBuilder.UseSetting(WebHostDefaults.ServerUrlsKey, address); + hostBuilder.PreferHostingUrls(true); + services.AddSingleton(new IISSetupFilter(pairingToken, new PathString(path), isWebSocketsSupported)); + services.Configure(options => + { + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + }); + services.Configure(options => + { + options.ForwardWindowsAuthentication = enableAuth; + }); + services.AddAuthenticationCore(); + }); + } + + return hostBuilder; + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/baseline.netcore.json b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/baseline.netcore.json new file mode 100644 index 0000000000..8cb928a411 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IISIntegration/baseline.netcore.json @@ -0,0 +1,247 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Server.IISIntegration, Version=2.0.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Hosting.WebHostBuilderIISExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseIISIntegration", + "Parameters": [ + { + "Name": "hostBuilder", + "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.IISOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_AutomaticAuthentication", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AutomaticAuthentication", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AuthenticationDisplayName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AuthenticationDisplayName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ForwardClientCertificate", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ForwardClientCertificate", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Server.IISIntegration.IISDefaults", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Negotiate", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Negotiate\"" + }, + { + "Kind": "Field", + "Name": "Ntlm", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"NTLM\"" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Server.IISIntegration.IISHostingStartup", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Hosting.IHostingStartup" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Configure", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Hosting.IHostingStartup", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Server.IISIntegration.IISMiddleware", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Invoke", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "loggerFactory", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptions" + }, + { + "Name": "pairingToken", + "Type": "System.String" + }, + { + "Name": "authentication", + "Type": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider" + }, + { + "Name": "applicationLifetime", + "Type": "Microsoft.AspNetCore.Hosting.IApplicationLifetime" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/ApplicationDeployerFactory.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/ApplicationDeployerFactory.cs new file mode 100644 index 0000000000..8fddbaede6 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/ApplicationDeployerFactory.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 Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + /// + /// Factory to create an appropriate deployer based on . + /// + public class IISApplicationDeployerFactory + { + /// + /// Creates a deployer instance based on settings in . + /// + /// + /// + /// + public static ApplicationDeployer Create(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory) + { + if (deploymentParameters == null) + { + throw new ArgumentNullException(nameof(deploymentParameters)); + } + + if (loggerFactory == null) + { + throw new ArgumentNullException(nameof(loggerFactory)); + } + + switch (deploymentParameters.ServerType) + { + case ServerType.IISExpress: + return new IISExpressDeployer(deploymentParameters, loggerFactory); + case ServerType.IIS: + return new IISDeployer(deploymentParameters, loggerFactory); + default: + return ApplicationDeployerFactory.Create(deploymentParameters, loggerFactory); + } + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/Http.config b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/Http.config new file mode 100644 index 0000000000..8c74496e12 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/Http.config @@ -0,0 +1,1026 @@ + + + + + + + + +
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeployer.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeployer.cs new file mode 100644 index 0000000000..4f2e525e72 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeployer.cs @@ -0,0 +1,450 @@ +// 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.Diagnostics; +using System.IO; +using System.Linq; +using System.ServiceProcess; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNetCore.Server.IntegrationTesting.Common; +using Microsoft.Extensions.Logging; +using Microsoft.Web.Administration; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS +{ + /// + /// Deployer for IIS. + /// + public class IISDeployer : IISDeployerBase + { + private const string DetailedErrorsEnvironmentVariable = "ASPNETCORE_DETAILEDERRORS"; + + private static readonly TimeSpan _timeout = TimeSpan.FromSeconds(60); + private static readonly TimeSpan _retryDelay = TimeSpan.FromMilliseconds(200); + + private CancellationTokenSource _hostShutdownToken = new CancellationTokenSource(); + + private string _configPath; + private string _debugLogFile; + + public Process HostProcess { get; set; } + + public IISDeployer(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory) + : base(new IISDeploymentParameters(deploymentParameters), loggerFactory) + { + } + + public IISDeployer(IISDeploymentParameters deploymentParameters, ILoggerFactory loggerFactory) + : base(deploymentParameters, loggerFactory) + { + } + + public override void Dispose() + { + Dispose(gracefulShutdown: false); + } + + public override void Dispose(bool gracefulShutdown) + { + Stop(); + + TriggerHostShutdown(_hostShutdownToken); + + GetLogsFromFile(); + + CleanPublishedOutput(); + InvokeUserApplicationCleanup(); + + StopTimer(); + } + + public override Task DeployAsync() + { + using (Logger.BeginScope("Deployment")) + { + StartTimer(); + + if (string.IsNullOrEmpty(DeploymentParameters.ServerConfigTemplateContent)) + { + DeploymentParameters.ServerConfigTemplateContent = File.ReadAllText("IIS.config"); + } + + // For now, only support using published output + DeploymentParameters.PublishApplicationBeforeDeployment = true; + // Move ASPNETCORE_DETAILEDERRORS to web config env variables + if (IISDeploymentParameters.EnvironmentVariables.ContainsKey(DetailedErrorsEnvironmentVariable)) + { + IISDeploymentParameters.WebConfigBasedEnvironmentVariables[DetailedErrorsEnvironmentVariable] = + IISDeploymentParameters.EnvironmentVariables[DetailedErrorsEnvironmentVariable]; + + IISDeploymentParameters.EnvironmentVariables.Remove(DetailedErrorsEnvironmentVariable); + } + // Do not override settings set on parameters + if (!IISDeploymentParameters.HandlerSettings.ContainsKey("debugLevel") && + !IISDeploymentParameters.HandlerSettings.ContainsKey("debugFile")) + { + _debugLogFile = Path.GetTempFileName(); + IISDeploymentParameters.HandlerSettings["debugLevel"] = "file"; + IISDeploymentParameters.HandlerSettings["debugFile"] = _debugLogFile; + } + + DotnetPublish(); + var contentRoot = DeploymentParameters.PublishedApplicationRootPath; + + RunWebConfigActions(contentRoot); + + var uri = TestUriHelper.BuildTestUri(ServerType.IIS, DeploymentParameters.ApplicationBaseUriHint); + StartIIS(uri, contentRoot); + + // Warm up time for IIS setup. + Logger.LogInformation("Successfully finished IIS application directory setup."); + return Task.FromResult(new IISDeploymentResult( + LoggerFactory, + IISDeploymentParameters, + applicationBaseUri: uri.ToString(), + contentRoot: contentRoot, + hostShutdownToken: _hostShutdownToken.Token, + hostProcess: HostProcess + )); + } + } + + protected override IEnumerable> GetWebConfigActions() + { + yield return WebConfigHelpers.AddOrModifyAspNetCoreSection( + key: "hostingModel", + value: DeploymentParameters.HostingModel.ToString()); + + yield return (element, _) => { + var aspNetCore = element + .Descendants("system.webServer") + .Single() + .GetOrAdd("aspNetCore"); + + // Expand path to dotnet because IIS process would not inherit PATH variable + if (aspNetCore.Attribute("processPath")?.Value.StartsWith("dotnet") == true) + { + aspNetCore.SetAttributeValue("processPath", DotNetCommands.GetDotNetExecutable(DeploymentParameters.RuntimeArchitecture)); + } + }; + + yield return WebConfigHelpers.AddOrModifyHandlerSection( + key: "modules", + value: DeploymentParameters.AncmVersion.ToString()); + + foreach (var action in base.GetWebConfigActions()) + { + yield return action; + } + } + + private void GetLogsFromFile() + { + try + { + + // Handle cases where debug file is redirected by test + var debugLogLocations = new List(); + if (IISDeploymentParameters.HandlerSettings.ContainsKey("debugFile")) + { + debugLogLocations.Add(IISDeploymentParameters.HandlerSettings["debugFile"]); + } + + if (DeploymentParameters.EnvironmentVariables.ContainsKey("ASPNETCORE_MODULE_DEBUG_FILE")) + { + debugLogLocations.Add(DeploymentParameters.EnvironmentVariables["ASPNETCORE_MODULE_DEBUG_FILE"]); + } + + // default debug file name + debugLogLocations.Add("aspnetcore-debug.log"); + + foreach (var debugLogLocation in debugLogLocations) + { + if (string.IsNullOrEmpty(debugLogLocation)) + { + continue; + } + + var file = Path.Combine(DeploymentParameters.PublishedApplicationRootPath, debugLogLocation); + if (File.Exists(file)) + { + var lines = File.ReadAllLines(file); + if (!lines.Any()) + { + Logger.LogInformation($"Debug log file {file} found but was empty"); + continue; + } + + foreach (var line in lines) + { + Logger.LogInformation(line); + } + return; + } + } + // ANCM V1 does not support logs + if (DeploymentParameters.AncmVersion == AncmVersion.AspNetCoreModuleV2) + { + throw new InvalidOperationException($"Unable to find non-empty debug log files. Tried: {string.Join(", ", debugLogLocations)}"); + } + } + finally + { + if (File.Exists(_debugLogFile)) + { + File.Delete(_debugLogFile); + } + } + } + + public void StartIIS(Uri uri, string contentRoot) + { + // Backup currently deployed apphost.config file + using (Logger.BeginScope("StartIIS")) + { + var port = uri.Port; + if (port == 0) + { + throw new NotSupportedException("Cannot set port 0 for IIS."); + } + + AddTemporaryAppHostConfig(contentRoot, port); + + WaitUntilSiteStarted(); + } + } + + private void WaitUntilSiteStarted() + { + ServiceController serviceController = new ServiceController("w3svc"); + Logger.LogInformation("W3SVC status " + serviceController.Status); + + if (serviceController.Status != ServiceControllerStatus.Running && + serviceController.Status != ServiceControllerStatus.StartPending) + { + Logger.LogInformation("Starting W3SVC"); + + serviceController.Start(); + serviceController.WaitForStatus(ServiceControllerStatus.Running, _timeout); + } + + RetryServerManagerAction(serverManager => + { + var site = serverManager.Sites.Single(); + var appPool = serverManager.ApplicationPools.Single(); + + if (appPool.State != ObjectState.Started && appPool.State != ObjectState.Starting) + { + var state = appPool.Start(); + Logger.LogInformation($"Starting pool, state: {state.ToString()}"); + } + + if (site.State != ObjectState.Started && site.State != ObjectState.Starting) + { + var state = site.Start(); + Logger.LogInformation($"Starting site, state: {state.ToString()}"); + } + + if (site.State != ObjectState.Started) + { + throw new InvalidOperationException("Site not started yet"); + } + + var workerProcess = appPool.WorkerProcesses.SingleOrDefault(); + if (workerProcess == null) + { + throw new InvalidOperationException("Site is started but no worked process found"); + } + + HostProcess = Process.GetProcessById(workerProcess.ProcessId); + + // Ensure w3wp.exe is killed if test process termination is non-graceful. + // Prevents locked files when stop debugging unit test. + ProcessTracker.Add(HostProcess); + + // cache the process start time for verifying log file name. + var _ = HostProcess.StartTime; + + Logger.LogInformation("Site has started."); + }); + } + + private void AddTemporaryAppHostConfig(string contentRoot, int port) + { + _configPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("D")); + var appHostConfigPath = Path.Combine(_configPath, "applicationHost.config"); + Directory.CreateDirectory(_configPath); + var config = XDocument.Parse(DeploymentParameters.ServerConfigTemplateContent ?? File.ReadAllText("IIS.config")); + + ConfigureAppHostConfig(config.Root, contentRoot, port); + + config.Save(appHostConfigPath); + + RetryServerManagerAction(serverManager => + { + var redirectionConfiguration = serverManager.GetRedirectionConfiguration(); + var redirectionSection = redirectionConfiguration.GetSection("configurationRedirection"); + + redirectionSection.Attributes["enabled"].Value = true; + redirectionSection.Attributes["path"].Value = _configPath; + + serverManager.CommitChanges(); + }); + } + + private void ConfigureAppHostConfig(XElement config, string contentRoot, int port) + { + ConfigureModuleAndBinding(config, contentRoot, port); + + // In IISExpress system.webServer/modules in under location element + config + .RequiredElement("system.webServer") + .RequiredElement("modules") + .GetOrAdd("add", "name", DeploymentParameters.AncmVersion.ToString()); + + var pool = config + .RequiredElement("system.applicationHost") + .RequiredElement("applicationPools") + .RequiredElement("add"); + + if (DeploymentParameters.EnvironmentVariables.Any()) + { + var environmentVariables = pool + .GetOrAdd("environmentVariables"); + + foreach (var tuple in DeploymentParameters.EnvironmentVariables) + { + environmentVariables + .GetOrAdd("add", "name", tuple.Key) + .SetAttributeValue("value", tuple.Value); + } + + } + + RunServerConfigActions(config, contentRoot); + } + + private void Stop() + { + try + { + RetryServerManagerAction(serverManager => + { + var site = serverManager.Sites.SingleOrDefault(); + if (site == null) + { + throw new InvalidOperationException("Site not found"); + } + + if (site.State != ObjectState.Stopped && site.State != ObjectState.Stopping) + { + var state = site.Stop(); + Logger.LogInformation($"Stopping site, state: {state.ToString()}"); + } + + var appPool = serverManager.ApplicationPools.SingleOrDefault(); + if (appPool == null) + { + throw new InvalidOperationException("Application pool not found"); + } + + if (appPool.State != ObjectState.Stopped && appPool.State != ObjectState.Stopping) + { + var state = appPool.Stop(); + Logger.LogInformation($"Stopping pool, state: {state.ToString()}"); + } + + if (site.State != ObjectState.Stopped) + { + throw new InvalidOperationException("Site not stopped yet"); + } + + try + { + if (appPool.WorkerProcesses != null && + appPool.WorkerProcesses.Any(wp => + wp.State == WorkerProcessState.Running || + wp.State == WorkerProcessState.Stopping)) + { + throw new InvalidOperationException("WorkerProcess not stopped yet"); + } + + } + // If WAS was stopped for some reason appPool.WorkerProcesses + // would throw UnauthorizedAccessException. + // check if it's the case and continue shutting down deployer + catch (UnauthorizedAccessException) + { + var serviceController = new ServiceController("was"); + if (serviceController.Status != ServiceControllerStatus.Stopped) + { + throw; + } + } + + if (!HostProcess.HasExited) + { + throw new InvalidOperationException("Site is stopped but host process is not"); + } + + Logger.LogInformation($"Site has stopped successfully."); + }); + } + finally + { + // Undo redirection.config changes unconditionally + RetryServerManagerAction(serverManager => + { + var redirectionConfiguration = serverManager.GetRedirectionConfiguration(); + var redirectionSection = redirectionConfiguration.GetSection("configurationRedirection"); + + redirectionSection.Attributes["enabled"].Value = false; + + serverManager.CommitChanges(); + if (Directory.Exists(_configPath)) + { + Directory.Delete(_configPath, true); + } + }); + } + } + + private void RetryServerManagerAction(Action action) + { + List exceptions = null; + var sw = Stopwatch.StartNew(); + int retryCount = 0; + + while (sw.Elapsed < _timeout) + { + try + { + using (var serverManager = new ServerManager()) + { + action(serverManager); + } + + return; + } + catch (Exception ex) + { + if (exceptions == null) + { + exceptions = new List(); + } + + exceptions.Add(ex); + } + + retryCount++; + Thread.Sleep(_retryDelay); + } + + throw new AggregateException($"Operation did not succeed after {retryCount} retries", exceptions.ToArray()); + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeployerBase.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeployerBase.cs new file mode 100644 index 0000000000..d0745b4f01 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeployerBase.cs @@ -0,0 +1,157 @@ +// 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.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS +{ + public abstract class IISDeployerBase : ApplicationDeployer + { + public IISDeploymentParameters IISDeploymentParameters { get; } + + public IISDeployerBase(IISDeploymentParameters deploymentParameters, ILoggerFactory loggerFactory) + : base(deploymentParameters, loggerFactory) + { + IISDeploymentParameters = deploymentParameters; + } + + protected void RunWebConfigActions(string contentRoot) + { + var actions = GetWebConfigActions(); + if (!actions.Any()) + { + return; + } + + if (!DeploymentParameters.PublishApplicationBeforeDeployment) + { + throw new InvalidOperationException("Cannot modify web.config file if no published output."); + } + + var path = Path.Combine(DeploymentParameters.PublishedApplicationRootPath, "web.config"); + var webconfig = XDocument.Load(path); + + foreach (var action in actions) + { + action.Invoke(webconfig.Root, contentRoot); + } + + webconfig.Save(path); + } + + protected virtual IEnumerable> GetWebConfigActions() + { + if (IISDeploymentParameters.HandlerSettings.Any()) + { + yield return AddHandlerSettings; + } + + if (IISDeploymentParameters.WebConfigBasedEnvironmentVariables.Any()) + { + yield return AddWebConfigEnvironmentVariables; + } + + foreach (var action in IISDeploymentParameters.WebConfigActionList) + { + yield return action; + } + } + + protected virtual IEnumerable> GetServerConfigActions() + { + foreach (var action in IISDeploymentParameters.ServerConfigActionList) + { + yield return action; + } + } + + public void RunServerConfigActions(XElement config, string contentRoot) + { + foreach (var action in GetServerConfigActions()) + { + action.Invoke(config, contentRoot); + } + } + + protected string GetAncmLocation(AncmVersion version) + { + var ancmDllName = version == AncmVersion.AspNetCoreModuleV2 ? "aspnetcorev2.dll" : "aspnetcore.dll"; + var arch = DeploymentParameters.RuntimeArchitecture == RuntimeArchitecture.x64 ? $@"x64\{ancmDllName}" : $@"x86\{ancmDllName}"; + var ancmFile = Path.Combine(AppContext.BaseDirectory, arch); + if (!File.Exists(Environment.ExpandEnvironmentVariables(ancmFile))) + { + ancmFile = Path.Combine(AppContext.BaseDirectory, ancmDllName); + if (!File.Exists(Environment.ExpandEnvironmentVariables(ancmFile))) + { + throw new FileNotFoundException("AspNetCoreModule could not be found.", ancmFile); + } + } + + return ancmFile; + } + + private void AddWebConfigEnvironmentVariables(XElement element, string contentRoot) + { + var environmentVariables = element + .Descendants("system.webServer") + .Single() + .RequiredElement("aspNetCore") + .GetOrAdd("environmentVariables"); + + foreach (var envVar in IISDeploymentParameters.WebConfigBasedEnvironmentVariables) + { + environmentVariables.GetOrAdd("environmentVariable", "name", envVar.Key) + .SetAttributeValue("value", envVar.Value); + } + } + + private void AddHandlerSettings(XElement element, string contentRoot) + { + var handlerSettings = element + .Descendants("system.webServer") + .Single() + .RequiredElement("aspNetCore") + .GetOrAdd("handlerSettings"); + + foreach (var handlerSetting in IISDeploymentParameters.HandlerSettings) + { + handlerSettings.GetOrAdd("handlerSetting", "name", handlerSetting.Key) + .SetAttributeValue("value", handlerSetting.Value); + } + } + + protected void ConfigureModuleAndBinding(XElement config, string contentRoot, int port) + { + var siteElement = config + .RequiredElement("system.applicationHost") + .RequiredElement("sites") + .RequiredElement("site"); + + siteElement + .RequiredElement("application") + .RequiredElement("virtualDirectory") + .SetAttributeValue("physicalPath", contentRoot); + + siteElement + .RequiredElement("bindings") + .RequiredElement("binding") + .SetAttributeValue("bindingInformation", $":{port}:localhost"); + + var ancmVersion = DeploymentParameters.AncmVersion.ToString(); + config + .RequiredElement("system.webServer") + .RequiredElement("globalModules") + .GetOrAdd("add", "name", ancmVersion) + .SetAttributeValue("image", GetAncmLocation(DeploymentParameters.AncmVersion)); + } + + public abstract void Dispose(bool gracefulShutdown); + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeploymentParameterExtensions.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeploymentParameterExtensions.cs new file mode 100644 index 0000000000..90f76fd8c2 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeploymentParameterExtensions.cs @@ -0,0 +1,240 @@ +// 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.Xml.Linq; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS +{ + public static class IISDeploymentParameterExtensions + { + public static void AddDebugLogToWebConfig(this IISDeploymentParameters parameters, string filename) + { + parameters.HandlerSettings["debugLevel"] = "file"; + parameters.HandlerSettings["debugFile"] = filename; + } + + public static void AddServerConfigAction(this IISDeploymentParameters parameters, Action action) + { + parameters.ServerConfigActionList.Add((config, _) => action(config)); + } + + public static void AddServerConfigAction(this IISDeploymentParameters parameters, Action action) + { + parameters.ServerConfigActionList.Add(action); + } + + public static void AddHttpsToServerConfig(this IISDeploymentParameters parameters) + { + parameters.AddServerConfigAction( + element => + { + element.Descendants("binding") + .Single() + .SetAttributeValue("protocol", "https"); + + element.Descendants("access") + .Single() + .SetAttributeValue("sslFlags", "Ssl, SslNegotiateCert"); + }); + } + + public static void SetWindowsAuth(this IISDeploymentParameters parameters, bool enabled = true) + { + parameters.EnsureSection("windowsAuthentication", "system.webServer", "security", "windowsAuthentication"); + parameters.EnableModule("WindowsAuthenticationModule", "%IIS_BIN%\\authsspi.dll"); + + parameters.AddServerConfigAction( + element => + { + var windowsAuthentication = element + .RequiredElement("system.webServer") + .RequiredElement("security") + .RequiredElement("authentication") + .GetOrAdd("windowsAuthentication"); + + windowsAuthentication.SetAttributeValue("enabled", enabled); + var providers = windowsAuthentication.GetOrAdd("providers"); + providers.GetOrAdd("add", "value", "Negotiate"); + providers.GetOrAdd("add", "value", "NTLM"); + }); + } + + public static void SetAnonymousAuth(this IISDeploymentParameters parameters, bool enabled = true) + { + parameters.AddServerConfigAction( + element => + { + element + .RequiredElement("system.webServer") + .RequiredElement("security") + .RequiredElement("authentication") + .GetOrAdd("anonymousAuthentication") + .SetAttributeValue("enabled", enabled); + }); + } + + public static void SetBasicAuth(this IISDeploymentParameters parameters, bool enabled = true) + { + parameters.EnableModule("BasicAuthenticationModule", "%IIS_BIN%\\authbas.dll"); + + parameters.AddServerConfigAction( + element => + { + element + .RequiredElement("system.webServer") + .RequiredElement("security") + .RequiredElement("authentication") + .GetOrAdd("basicAuthentication") + .SetAttributeValue("enabled", enabled); + }); + } + + public static void EnsureSection(this IISDeploymentParameters parameters, string name, params string[] path) + { + parameters.ServerConfigActionList.Add( + (config, _) => { + + var element = config + .RequiredElement("configSections"); + + foreach (var s in path) + { + element = element.GetOrAdd("sectionGroup", "name", s); + } + + element.GetOrAdd("section", "name", "applicationInitialization") + .SetAttributeValue("overrideModeDefault", "Allow"); + }); + } + + public static void EnableLogging(this IISDeploymentParameters deploymentParameters, string path) + { + deploymentParameters.WebConfigActionList.Add( + WebConfigHelpers.AddOrModifyAspNetCoreSection("stdoutLogEnabled", "true")); + + deploymentParameters.WebConfigActionList.Add( + WebConfigHelpers.AddOrModifyAspNetCoreSection("stdoutLogFile", Path.Combine(path, "std"))); + } + + public static void EnableFreb(this IISDeploymentParameters deploymentParameters, string verbosity, string folderPath) + { + if (!deploymentParameters.PublishApplicationBeforeDeployment) + { + throw new InvalidOperationException("Testing freb requires site to be published."); + } + + deploymentParameters.EnableModule("FailedRequestsTracingModule", "%IIS_BIN%\\iisfreb.dll"); + + // Set the TraceFailedRequestsSection to listend to ANCM events + deploymentParameters.ServerConfigActionList.Add( + (element, _) => + { + var webServerElement = element + .RequiredElement("system.webServer"); + + var addElement = webServerElement + .GetOrAdd("tracing") + .GetOrAdd("traceFailedRequests") + .GetOrAdd("add"); + + addElement.SetAttributeValue("path", "*"); + + addElement.GetOrAdd("failureDefinitions") + .SetAttributeValue("statusCodes", "200-999"); + + var traceAreasElement = addElement + .GetOrAdd("traceAreas"); + var innerAddElement = traceAreasElement.GetOrAdd("add", "provider", "WWW Server"); + + innerAddElement.SetAttributeValue("areas", "ANCM"); + innerAddElement.SetAttributeValue("verbosity", verbosity); + }); + + // Set the ANCM traceProviderDefinition to 65536 + deploymentParameters.ServerConfigActionList.Add( + (element, _) => + { + var webServerElement = element + .RequiredElement("system.webServer"); + + var traceProviderDefinitionsElement = webServerElement + .GetOrAdd("tracing") + .GetOrAdd("traceProviderDefinitions"); + + var innerAddElement = traceProviderDefinitionsElement.GetOrAdd("add", "name", "WWW Server"); + + innerAddElement.SetAttributeValue("name", "WWW Server"); + innerAddElement.SetAttributeValue("guid", "{3a2a4e84-4c21-4981-ae10-3fda0d9b0f83}"); + + var areasElement = innerAddElement.GetOrAdd("areas"); + var iae = areasElement.GetOrAdd("add", "name", "ANCM"); + + iae.SetAttributeValue("value", "65536"); + }); + + // Set the freb directory to the published app directory. + deploymentParameters.ServerConfigActionList.Add( + (element, contentRoot) => + { + var traceFailedRequestsElement = element + .RequiredElement("system.applicationHost") + .Element("sites") + .Element("siteDefaults") + .Element("traceFailedRequestsLogging"); + traceFailedRequestsElement.SetAttributeValue("directory", folderPath); + traceFailedRequestsElement.SetAttributeValue("enabled", "true"); + traceFailedRequestsElement.SetAttributeValue("maxLogFileSizeKB", "1024"); + }); + } + + public static void TransformPath(this IISDeploymentParameters parameters, Func transformation) + { + parameters.WebConfigActionList.Add( + (config, contentRoot) => + { + var aspNetCoreElement = config.Descendants("aspNetCore").Single(); + aspNetCoreElement.SetAttributeValue("processPath", transformation((string)aspNetCoreElement.Attribute("processPath"), contentRoot)); + }); + } + + public static void TransformArguments(this IISDeploymentParameters parameters, Func transformation) + { + parameters.WebConfigActionList.Add( + (config, contentRoot) => + { + var aspNetCoreElement = config.Descendants("aspNetCore").Single(); + 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/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeploymentParameters.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeploymentParameters.cs new file mode 100644 index 0000000000..3b2dfc4804 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeploymentParameters.cs @@ -0,0 +1,52 @@ +// 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.Xml.Linq; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS +{ + public class IISDeploymentParameters : DeploymentParameters + { + public IISDeploymentParameters() : base() + { + } + + public IISDeploymentParameters(TestVariant variant) + : base(variant) + { + } + + public IISDeploymentParameters( + string applicationPath, + ServerType serverType, + RuntimeFlavor runtimeFlavor, + RuntimeArchitecture runtimeArchitecture) + : base(applicationPath, serverType, runtimeFlavor, runtimeArchitecture) + { + } + + public IISDeploymentParameters(DeploymentParameters parameters) + : base(parameters) + { + if (parameters is IISDeploymentParameters) + { + var tempParameters = (IISDeploymentParameters)parameters; + WebConfigActionList = tempParameters.WebConfigActionList; + ServerConfigActionList = tempParameters.ServerConfigActionList; + WebConfigBasedEnvironmentVariables = tempParameters.WebConfigBasedEnvironmentVariables; + HandlerSettings = tempParameters.HandlerSettings; + } + } + + public IList> WebConfigActionList { get; } = new List>(); + + public IList> ServerConfigActionList { get; } = new List>(); + + public IDictionary WebConfigBasedEnvironmentVariables { get; set; } = new Dictionary(); + + public IDictionary HandlerSettings { get; set; } = new Dictionary(); + + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeploymentResult.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeploymentResult.cs new file mode 100644 index 0000000000..814a778c16 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeploymentResult.cs @@ -0,0 +1,43 @@ +// 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.Diagnostics; +using System.Net.Http; +using System.Threading; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS +{ + public class IISDeploymentResult : DeploymentResult + { + public ILogger Logger { get; set; } + public Process HostProcess { get; } + + public IISDeploymentResult(ILoggerFactory loggerFactory, + IISDeploymentParameters deploymentParameters, + string applicationBaseUri, + string contentRoot, + CancellationToken hostShutdownToken, + Process hostProcess) + : base(loggerFactory, + deploymentParameters, + applicationBaseUri, + contentRoot, + hostShutdownToken) + { + HostProcess = hostProcess; + Logger = loggerFactory.CreateLogger(deploymentParameters.SiteName); + HttpClient = CreateClient(new HttpClientHandler()); + } + + public HttpClient CreateClient(HttpMessageHandler messageHandler) + { + return new HttpClient(new LoggingHandler(messageHandler, Logger)) + { + BaseAddress = base.HttpClient.BaseAddress + }; + } + + public new HttpClient HttpClient { get; set; } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISExpressDeployer.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISExpressDeployer.cs new file mode 100644 index 0000000000..8fe8c66585 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISExpressDeployer.cs @@ -0,0 +1,497 @@ +// 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.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNetCore.Server.IntegrationTesting.Common; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS +{ + /// + /// Deployment helper for IISExpress. + /// + public class IISExpressDeployer : IISDeployerBase + { + private const string IISExpressRunningMessage = "IIS Express is running."; + private const string FailedToInitializeBindingsMessage = "Failed to initialize site bindings"; + private const string UnableToStartIISExpressMessage = "Unable to start iisexpress."; + private const int MaximumAttempts = 5; + private readonly TimeSpan ShutdownTimeSpan = Debugger.IsAttached ? TimeSpan.FromMinutes(60) : TimeSpan.FromMinutes(1); + private static readonly Regex UrlDetectorRegex = new Regex(@"^\s*Successfully registered URL ""(?[^""]+)"" for site.*$"); + + private Process _hostProcess; + + public IISExpressDeployer(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory) + : base(new IISDeploymentParameters(deploymentParameters), loggerFactory) + { + } + + public IISExpressDeployer(IISDeploymentParameters deploymentParameters, ILoggerFactory loggerFactory) + : base(deploymentParameters, loggerFactory) + { + } + + public override async Task DeployAsync() + { + using (Logger.BeginScope("Deployment")) + { + // Start timer + StartTimer(); + + // For an unpublished application the dllroot points pre-built dlls like projectdir/bin/debug/net461/ + // and contentRoot points to the project directory so you get things like static assets. + // For a published app both point to the publish directory. + var dllRoot = CheckIfPublishIsRequired(); + var contentRoot = string.Empty; + if (DeploymentParameters.PublishApplicationBeforeDeployment) + { + DotnetPublish(); + contentRoot = DeploymentParameters.PublishedApplicationRootPath; + } + else + { + // Core+Standalone always publishes. This must be Clr+Standalone or Core+Portable. + // Update processPath and arguments for our current scenario + contentRoot = DeploymentParameters.ApplicationPath; + + var executableExtension = DeploymentParameters.ApplicationType == ApplicationType.Portable ? ".dll" : ".exe"; + var entryPoint = Path.Combine(dllRoot, DeploymentParameters.ApplicationName + executableExtension); + + var executableName = string.Empty; + var executableArgs = string.Empty; + + if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.CoreClr && DeploymentParameters.ApplicationType == ApplicationType.Portable) + { + executableName = GetDotNetExeForArchitecture(); + executableArgs = entryPoint; + } + else + { + executableName = entryPoint; + } + + Logger.LogInformation("Executing: {exe} {args}", executableName, executableArgs); + DeploymentParameters.EnvironmentVariables["LAUNCHER_PATH"] = executableName; + DeploymentParameters.EnvironmentVariables["LAUNCHER_ARGS"] = executableArgs; + + // CurrentDirectory will point to bin/{config}/{tfm}, but the config and static files aren't copied, point to the app base instead. + Logger.LogInformation("ContentRoot: {path}", DeploymentParameters.ApplicationPath); + DeploymentParameters.EnvironmentVariables["ASPNETCORE_CONTENTROOT"] = DeploymentParameters.ApplicationPath; + } + + RunWebConfigActions(contentRoot); + + var testUri = TestUriHelper.BuildTestUri(ServerType.IISExpress, DeploymentParameters.ApplicationBaseUriHint); + + // Launch the host process. + var (actualUri, hostExitToken) = await StartIISExpressAsync(testUri, contentRoot); + + Logger.LogInformation("Application ready at URL: {appUrl}", actualUri); + + // Right now this works only for urls like http://localhost:5001/. Does not work for http://localhost:5001/subpath. + + return new IISDeploymentResult( + LoggerFactory, + IISDeploymentParameters, + applicationBaseUri: actualUri.ToString(), + contentRoot: contentRoot, + hostShutdownToken: hostExitToken, + hostProcess: _hostProcess); + } + } + + private string CheckIfPublishIsRequired() + { + var targetFramework = DeploymentParameters.TargetFramework; + + // IISIntegration uses this layout + var dllRoot = Path.Combine(DeploymentParameters.ApplicationPath, "bin", DeploymentParameters.RuntimeArchitecture.ToString(), + DeploymentParameters.Configuration, targetFramework); + + if (!Directory.Exists(dllRoot)) + { + // Most repos use this layout + dllRoot = Path.Combine(DeploymentParameters.ApplicationPath, "bin", DeploymentParameters.Configuration, targetFramework); + + if (!Directory.Exists(dllRoot)) + { + // The bits we need weren't pre-compiled, compile on publish + DeploymentParameters.PublishApplicationBeforeDeployment = true; + } + else if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.Clr + && DeploymentParameters.RuntimeArchitecture == RuntimeArchitecture.x86) + { + // x64 is the default. Publish to rebuild for the right bitness + DeploymentParameters.PublishApplicationBeforeDeployment = true; + } + } + + if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.CoreClr + && DeploymentParameters.ApplicationType == ApplicationType.Standalone) + { + // Publish is always required to get the correct standalone files in the output directory + DeploymentParameters.PublishApplicationBeforeDeployment = true; + } + + return dllRoot; + } + + private async Task<(Uri url, CancellationToken hostExitToken)> StartIISExpressAsync(Uri uri, string contentRoot) + { + using (Logger.BeginScope("StartIISExpress")) + { + var port = uri.Port; + if (port == 0) + { + port = (uri.Scheme == "https") ? TestPortHelper.GetNextSSLPort() : TestPortHelper.GetNextPort(); + } + + Logger.LogInformation("Attempting to start IIS Express on port: {port}", port); + PrepareConfig(contentRoot, port); + + var parameters = string.IsNullOrEmpty(DeploymentParameters.ServerConfigLocation) ? + string.Format("/port:{0} /path:\"{1}\" /trace:error /systray:false", uri.Port, contentRoot) : + string.Format("/site:{0} /config:{1} /trace:error /systray:false", DeploymentParameters.SiteName, DeploymentParameters.ServerConfigLocation); + + var iisExpressPath = GetIISExpressPath(); + + for (var attempt = 0; attempt < MaximumAttempts; attempt++) + { + Logger.LogInformation("Executing command : {iisExpress} {parameters}", iisExpressPath, parameters); + + var startInfo = new ProcessStartInfo + { + FileName = iisExpressPath, + Arguments = parameters, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + // VS sets current directory to C:\Program Files\IIS Express + WorkingDirectory = Path.GetDirectoryName(iisExpressPath) + }; + + AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.EnvironmentVariables); + + Uri url = null; + var started = new TaskCompletionSource(); + + var process = new Process() { StartInfo = startInfo }; + process.OutputDataReceived += (sender, dataArgs) => + { + if (string.Equals(dataArgs.Data, UnableToStartIISExpressMessage)) + { + // We completely failed to start and we don't really know why + started.TrySetException(new InvalidOperationException("Failed to start IIS Express")); + } + else if (string.Equals(dataArgs.Data, FailedToInitializeBindingsMessage)) + { + started.TrySetResult(false); + } + else if (string.Equals(dataArgs.Data, IISExpressRunningMessage)) + { + started.TrySetResult(true); + } + else if (!string.IsNullOrEmpty(dataArgs.Data)) + { + var m = UrlDetectorRegex.Match(dataArgs.Data); + if (m.Success) + { + url = new Uri(m.Groups["url"].Value); + } + } + }; + + process.EnableRaisingEvents = true; + var hostExitTokenSource = new CancellationTokenSource(); + process.Exited += (sender, e) => + { + Logger.LogInformation("iisexpress Process {pid} shut down", process.Id); + + // If TrySetResult was called above, this will just silently fail to set the new state, which is what we want + started.TrySetException(new Exception($"Command exited unexpectedly with exit code: {process.ExitCode}")); + + TriggerHostShutdown(hostExitTokenSource); + }; + process.StartAndCaptureOutAndErrToLogger("iisexpress", Logger); + Logger.LogInformation("iisexpress Process {pid} started", process.Id); + + if (process.HasExited) + { + Logger.LogError("Host process {processName} {pid} exited with code {exitCode} or failed to start.", startInfo.FileName, process.Id, process.ExitCode); + throw new Exception("Failed to start host"); + } + + // Wait for the app to start + // The timeout here is large, because we don't know how long the test could need + // We cover a lot of error cases above, but I want to make sure we eventually give up and don't hang the build + // just in case we missed one -anurse + if (!await started.Task.TimeoutAfter(TimeSpan.FromMinutes(10))) + { + Logger.LogInformation("iisexpress Process {pid} failed to bind to port {port}, trying again", process.Id, port); + + // Wait for the process to exit and try again + process.WaitForExit(30 * 1000); + await Task.Delay(1000); // Wait a second to make sure the socket is completely cleaned up + } + else + { + _hostProcess = process; + + // Ensure iisexpress.exe is killed if test process termination is non-graceful. + // Prevents locked files when stop debugging unit test. + ProcessTracker.Add(_hostProcess); + + // cache the process start time for verifying log file name. + var _ = _hostProcess.StartTime; + + Logger.LogInformation("Started iisexpress successfully. Process Id : {processId}, Port: {port}", _hostProcess.Id, port); + return (url: url, hostExitToken: hostExitTokenSource.Token); + } + } + + var message = $"Failed to initialize IIS Express after {MaximumAttempts} attempts to select a port"; + Logger.LogError(message); + throw new TimeoutException(message); + } + } + + private void PrepareConfig(string contentRoot, int port) + { + var serverConfig = DeploymentParameters.ServerConfigTemplateContent;; + // Config is required. If not present then fall back to one we carry with us. + if (string.IsNullOrEmpty(serverConfig)) + { + using (var stream = GetType().Assembly.GetManifestResourceStream("Microsoft.AspNetCore.Server.IntegrationTesting.IIS.Http.config")) + using (var reader = new StreamReader(stream)) + { + serverConfig = reader.ReadToEnd(); + } + } + + XDocument config = XDocument.Parse(serverConfig); + // Pass on the applicationhost.config to iis express. With this don't need to pass in the /path /port switches as they are in the applicationHost.config + // We take a copy of the original specified applicationHost.Config to prevent modifying the one in the repo. + + config.Root + .RequiredElement("location") + .RequiredElement("system.webServer") + .RequiredElement("modules") + .GetOrAdd("add", "name", DeploymentParameters.AncmVersion.ToString()); + + ConfigureModuleAndBinding(config.Root, contentRoot, port); + + 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); + } + + RunServerConfigActions(config.Root, contentRoot); + serverConfig = config.ToString(); + + DeploymentParameters.ServerConfigLocation = Path.GetTempFileName(); + Logger.LogDebug("Saving Config to {configPath}", DeploymentParameters.ServerConfigLocation); + + File.WriteAllText(DeploymentParameters.ServerConfigLocation, serverConfig); + } + + private void AddAspNetCoreElement(XElement config) + { + var aspNetCore = config + .RequiredElement("system.webServer") + .GetOrAdd("aspNetCore"); + + aspNetCore.SetAttributeValue("hostingModel", DeploymentParameters.HostingModel.ToString()); + aspNetCore.SetAttributeValue("arguments", "%LAUNCHER_ARGS%"); + aspNetCore.SetAttributeValue("processPath", "%LAUNCHER_PATH%"); + + var handlers = config + .RequiredElement("location") + .RequiredElement("system.webServer") + .RequiredElement("handlers"); + + var aspNetCoreHandler = handlers + .GetOrAdd("add", "name", "aspNetCore"); + + aspNetCoreHandler.SetAttributeValue("path", "*"); + aspNetCoreHandler.SetAttributeValue("verb", "*"); + aspNetCoreHandler.SetAttributeValue("modules", DeploymentParameters.AncmVersion.ToString()); + aspNetCoreHandler.SetAttributeValue("resourceType", "Unspecified"); + // Make aspNetCore handler first + aspNetCoreHandler.Remove(); + handlers.AddFirst(aspNetCoreHandler); + } + + protected override IEnumerable> GetWebConfigActions() + { + if (IISDeploymentParameters.PublishApplicationBeforeDeployment) + { + // For published apps, prefer the content in the web.config, but update it. + yield return WebConfigHelpers.AddOrModifyAspNetCoreSection( + key: "hostingModel", + value: DeploymentParameters.HostingModel.ToString()); + + yield return WebConfigHelpers.AddOrModifyHandlerSection( + key: "modules", + value: DeploymentParameters.AncmVersion.ToString()); + + // We assume the x64 dotnet.exe is on the path so we need to provide an absolute path for x86 scenarios. + // Only do it for scenarios that rely on dotnet.exe (Core, portable, etc.). + if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.CoreClr + && DeploymentParameters.ApplicationType == ApplicationType.Portable + && DotNetCommands.IsRunningX86OnX64(DeploymentParameters.RuntimeArchitecture)) + { + var executableName = DotNetCommands.GetDotNetExecutable(DeploymentParameters.RuntimeArchitecture); + if (!File.Exists(executableName)) + { + throw new Exception($"Unable to find '{executableName}'.'"); + } + yield return WebConfigHelpers.AddOrModifyAspNetCoreSection("processPath", executableName); + } + } + + foreach (var action in base.GetWebConfigActions()) + { + yield return action; + } + } + + private string GetIISExpressPath() + { + var programFiles = "Program Files"; + if (DotNetCommands.IsRunningX86OnX64(DeploymentParameters.RuntimeArchitecture)) + { + programFiles = "Program Files (x86)"; + } + + // Get path to program files + var iisExpressPath = Path.Combine(Environment.GetEnvironmentVariable("SystemDrive") + "\\", programFiles, "IIS Express", "iisexpress.exe"); + + if (!File.Exists(iisExpressPath)) + { + throw new Exception("Unable to find IISExpress on the machine: " + iisExpressPath); + } + + return iisExpressPath; + } + + public override void Dispose() + { + Dispose(gracefulShutdown: false); + } + + public override void Dispose(bool gracefulShutdown) + { + using (Logger.BeginScope("Dispose")) + { + if (gracefulShutdown) + { + GracefullyShutdownProcess(_hostProcess); + } + else + { + ShutDownIfAnyHostProcess(_hostProcess); + } + + if (!string.IsNullOrEmpty(DeploymentParameters.ServerConfigLocation) + && File.Exists(DeploymentParameters.ServerConfigLocation)) + { + // Delete the temp applicationHostConfig that we created. + Logger.LogDebug("Deleting applicationHost.config file from {configLocation}", DeploymentParameters.ServerConfigLocation); + try + { + File.Delete(DeploymentParameters.ServerConfigLocation); + } + catch (Exception exception) + { + // Ignore delete failures - just write a log. + Logger.LogWarning("Failed to delete '{config}'. Exception : {exception}", DeploymentParameters.ServerConfigLocation, exception.Message); + } + } + + if (DeploymentParameters.PublishApplicationBeforeDeployment) + { + CleanPublishedOutput(); + } + + InvokeUserApplicationCleanup(); + + StopTimer(); + } + + // If by this point, the host process is still running (somehow), throw an error. + // A test failure is better than a silent hang and unknown failure later on + if (_hostProcess != null && !_hostProcess.HasExited) + { + throw new Exception($"iisexpress Process {_hostProcess.Id} failed to shutdown"); + } + } + + private class WindowsNativeMethods + { + [DllImport("user32.dll")] + internal static extern IntPtr GetTopWindow(IntPtr hWnd); + [DllImport("user32.dll")] + internal static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd); + [DllImport("user32.dll")] + internal static extern uint GetWindowThreadProcessId(IntPtr hwnd, out uint lpdwProcessId); + [DllImport("user32.dll")] + internal static extern bool PostMessage(HandleRef hWnd, uint Msg, IntPtr wParam, IntPtr lParam); + } + + private static void SendStopMessageToProcess(int pid) + { + for (var ptr = WindowsNativeMethods.GetTopWindow(IntPtr.Zero); ptr != IntPtr.Zero; ptr = WindowsNativeMethods.GetWindow(ptr, 2)) + { + uint num; + WindowsNativeMethods.GetWindowThreadProcessId(ptr, out num); + if (pid == num) + { + var hWnd = new HandleRef(null, ptr); + WindowsNativeMethods.PostMessage(hWnd, 0x12, IntPtr.Zero, IntPtr.Zero); + return; + } + } + } + + private void GracefullyShutdownProcess(Process hostProcess) + { + if (hostProcess != null && !hostProcess.HasExited) + { + // Calling hostProcess.StandardInput.WriteLine("q") with StandardInput redirected + // for the process does not work when stopping IISExpress + // Also, hostProcess.CloseMainWindow() doesn't work either. + // Instead we have to send WM_QUIT to the iisexpress process via pInvokes. + // See: https://stackoverflow.com/questions/4772092/starting-and-stopping-iis-express-programmatically + + SendStopMessageToProcess(hostProcess.Id); + if (!hostProcess.WaitForExit((int)ShutdownTimeSpan.TotalMilliseconds)) + { + throw new InvalidOperationException($"iisexpress Process {hostProcess.Id} failed to gracefully shutdown."); + } + if (hostProcess.ExitCode != 0) + { + Logger.LogWarning($"IISExpress exit code is non-zero after graceful shutdown. Exit code: {hostProcess.ExitCode}"); + } + } + else + { + throw new InvalidOperationException($"iisexpress Process {hostProcess?.Id} crashed before shutdown was triggered."); + } + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/LoggingHandler.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/LoggingHandler.cs new file mode 100644 index 0000000000..d9067c038e --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/LoggingHandler.cs @@ -0,0 +1,52 @@ +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS +{ + public class LoggingHandler: DelegatingHandler + { + private readonly int _maxBodyLogSize = 16 * 1024; + private readonly ILogger _logger; + + public LoggingHandler(HttpMessageHandler innerHandler, ILogger logger) + : base(innerHandler) + { + _logger = logger; + } + + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + _logger.LogDebug(request.ToString()); + var response = await base.SendAsync(request, cancellationToken); + + await LogResponse(response.IsSuccessStatusCode ? LogLevel.Debug : LogLevel.Warning, response); + return response; + } + + private async Task LogResponse(LogLevel logLevel, HttpResponseMessage response) + { + _logger.Log(logLevel, response.ToString()); + if (response.Content != null) + { + await response.Content.LoadIntoBufferAsync(); + var readAsStreamAsync = await response.Content.ReadAsStreamAsync(); + var buffer = new byte[_maxBodyLogSize]; + var offset = 0; + var count = 0; + do + { + count = await readAsStreamAsync.ReadAsync(buffer, offset, buffer.Length - offset); + offset += count; + } while (count != 0 && offset != buffer.Length); + + readAsStreamAsync.Position = 0; + _logger.Log(logLevel, Encoding.ASCII.GetString(buffer, 0, offset)); + } + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/Microsoft.AspNetCore.Server.IntegrationTesting.IIS.csproj b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/Microsoft.AspNetCore.Server.IntegrationTesting.IIS.csproj new file mode 100644 index 0000000000..bdfe8abfa9 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/Microsoft.AspNetCore.Server.IntegrationTesting.IIS.csproj @@ -0,0 +1,47 @@ + + + + netstandard2.0 + Microsoft.AspNetCore.Server.IntegrationTesting.IIS + Provides support for integration testing using IIS based servers. + $(NoWarn);CS1591 + true + aspnetcore;iis + True + + + + + + + + + + + + + + + + + + False + + + False + + + False + + + + + + + + + + + + + diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/ProcessTracker.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/ProcessTracker.cs new file mode 100644 index 0000000000..be2e6cc62b --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/ProcessTracker.cs @@ -0,0 +1,128 @@ +// 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.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS +{ + // Uses Windows Job Objects to ensure external processes are killed if the current process is terminated non-gracefully. + internal static class ProcessTracker + { + private static readonly IntPtr _jobHandle; + + static ProcessTracker() + { + // Requires Win8 or later + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || Environment.OSVersion.Version < new Version(6, 2)) + { + return; + } + + // Job name is optional but helps with diagnostics. Job name must be unique if non-null. + _jobHandle = CreateJobObject(IntPtr.Zero, name: $"ProcessTracker{Process.GetCurrentProcess().Id}"); + + var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION + { + BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION + { + LimitFlags = JOBOBJECTLIMIT.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE + } + }; + + var length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); + var extendedInfoPtr = Marshal.AllocHGlobal(length); + try + { + Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false); + + if (!SetInformationJobObject(_jobHandle, JobObjectInfoType.ExtendedLimitInformation, + extendedInfoPtr, (uint)length)) + { + throw new Win32Exception(); + } + } + finally + { + Marshal.FreeHGlobal(extendedInfoPtr); + } + } + + public static void Add(Process process) + { + if (_jobHandle != IntPtr.Zero) + { + var success = AssignProcessToJobObject(_jobHandle, process.Handle); + if (!success && !process.HasExited) + { + throw new Win32Exception(); + } + } + } + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string name); + + [DllImport("kernel32.dll")] + static extern bool SetInformationJobObject(IntPtr job, JobObjectInfoType infoType, + IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength); + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process); + + private enum JobObjectInfoType + { + AssociateCompletionPortInformation = 7, + BasicLimitInformation = 2, + BasicUIRestrictions = 4, + EndOfJobTimeInformation = 6, + ExtendedLimitInformation = 9, + SecurityLimitInformation = 5, + GroupInformation = 11 + } + + [StructLayout(LayoutKind.Sequential)] + private struct JOBOBJECT_BASIC_LIMIT_INFORMATION + { + public Int64 PerProcessUserTimeLimit; + public Int64 PerJobUserTimeLimit; + public JOBOBJECTLIMIT LimitFlags; + public UIntPtr MinimumWorkingSetSize; + public UIntPtr MaximumWorkingSetSize; + public UInt32 ActiveProcessLimit; + public Int64 Affinity; + public UInt32 PriorityClass; + public UInt32 SchedulingClass; + } + + [Flags] + private enum JOBOBJECTLIMIT : uint + { + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000 + } + + [StructLayout(LayoutKind.Sequential)] + private struct IO_COUNTERS + { + public UInt64 ReadOperationCount; + public UInt64 WriteOperationCount; + public UInt64 OtherOperationCount; + public UInt64 ReadTransferCount; + public UInt64 WriteTransferCount; + public UInt64 OtherTransferCount; + } + + [StructLayout(LayoutKind.Sequential)] + private struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION + { + public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; + public IO_COUNTERS IoInfo; + public UIntPtr ProcessMemoryLimit; + public UIntPtr JobMemoryLimit; + public UIntPtr PeakProcessMemoryUsed; + public UIntPtr PeakJobMemoryUsed; + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/RetryHandler.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/RetryHandler.cs new file mode 100644 index 0000000000..4282c3afca --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/RetryHandler.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; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS +{ + public class RetryHandler : DelegatingHandler + { + private static readonly int MaxRetries = 5; + private static readonly TimeSpan RetryDelay = TimeSpan.FromSeconds(1); + + private readonly ILogger _logger; + + public RetryHandler(HttpMessageHandler innerHandler, ILogger logger) + : base(innerHandler) + { + _logger = logger; + } + + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + HttpResponseMessage response = null; + for (int i = 0; i < MaxRetries; i++) + { + response = null; + try + { + response = await base.SendAsync(request, cancellationToken); + } + catch (Exception ex) + { + _logger.LogWarning("Error sending request", ex); + if (i == MaxRetries - 1) + { + throw; + } + } + + // Retry only on 503 that is expected during IIS startup + if (response != null && + (response.IsSuccessStatusCode || response.StatusCode != (HttpStatusCode)503)) + { + break; + } + + _logger.LogDebug($"Retrying {i+1}th time after {RetryDelay.Seconds} sec."); + await Task.Delay(RetryDelay, cancellationToken); + } + return response; + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/WebConfigHelpers.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/WebConfigHelpers.cs new file mode 100644 index 0000000000..f920622568 --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/WebConfigHelpers.cs @@ -0,0 +1,36 @@ +// 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.Linq; +using System.Xml.Linq; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS +{ + public static class WebConfigHelpers + { + public static Action AddOrModifyAspNetCoreSection(string key, string value) + { + return (element, _) => { + element + .Descendants("system.webServer") + .Single() + .GetOrAdd("aspNetCore") + .SetAttributeValue(key, value); + }; + } + + public static Action AddOrModifyHandlerSection(string key, string value) + { + return (element, _) => + { + element + .Descendants("system.webServer") + .Single() + .GetOrAdd("handlers") + .GetOrAdd("add") + .SetAttributeValue(key, value); + }; + } + } +} diff --git a/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/XElementExtensions.cs b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/XElementExtensions.cs new file mode 100644 index 0000000000..35d1b013cd --- /dev/null +++ b/src/IISIntegration/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/XElementExtensions.cs @@ -0,0 +1,47 @@ +// 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.Linq; +using System.Xml.Linq; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS +{ + public static class XElementExtensions + { + public static XElement RequiredElement(this XElement element, string name) + { + var existing = element.Element(name); + if (existing == null) + { + throw new InvalidOperationException($"Element with name {name} not found in {element}"); + } + + return existing; + } + + public static XElement GetOrAdd(this XElement element, string name) + { + var existing = element.Element(name); + if (existing == null) + { + existing = new XElement(name); + element.Add(existing); + } + + return existing; + } + + public static XElement GetOrAdd(this XElement element, string name, string attribute, string attributeValue) + { + var existing = element.Elements(name).FirstOrDefault(e => e.Attribute(attribute)?.Value == attributeValue); + if (existing == null) + { + existing = new XElement(name, new XAttribute(attribute, attributeValue)); + element.Add(existing); + } + + return existing; + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/AppHostConfig/IIS.config b/src/IISIntegration/test/Common.FunctionalTests/AppHostConfig/IIS.config new file mode 100644 index 0000000000..bc24ef9639 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/AppHostConfig/IIS.config @@ -0,0 +1,740 @@ + + + + + + + + +
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+ + +
+
+
+
+ +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/test/Common.FunctionalTests/AppOfflineTests.cs b/src/IISIntegration/test/Common.FunctionalTests/AppOfflineTests.cs new file mode 100644 index 0000000000..7d8d661ef0 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/AppOfflineTests.cs @@ -0,0 +1,287 @@ +// 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.Net; +using System.Net.Http; +using System.Net.Sockets; +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.Testing.xunit; +using Microsoft.Extensions.Logging; +using Xunit; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; + +namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class AppOfflineTests : IISFunctionalTestBase + { + private static readonly TimeSpan RetryDelay = TimeSpan.FromMilliseconds(100); + + private readonly PublishedSitesFixture _fixture; + + public AppOfflineTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalTheory] + [InlineData(HostingModel.InProcess)] + [InlineData(HostingModel.OutOfProcess)] + public async Task AppOfflineDroppedWhileSiteIsDown_SiteReturns503(HostingModel hostingModel) + { + var deploymentResult = await DeployApp(hostingModel); + + AddAppOffline(deploymentResult.ContentRoot); + + await AssertAppOffline(deploymentResult); + DeletePublishOutput(deploymentResult); + } + + [ConditionalTheory] + [InlineData(HostingModel.InProcess)] + [InlineData(HostingModel.OutOfProcess)] + public async Task LockedAppOfflineDroppedWhileSiteIsDown_SiteReturns503(HostingModel hostingModel) + { + var deploymentResult = await DeployApp(hostingModel); + + // Add app_offline without shared access + using (var stream = File.Open(Path.Combine(deploymentResult.ContentRoot, "app_offline.htm"), FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None)) + using (var writer = new StreamWriter(stream)) + { + await writer.WriteLineAsync("App if offline but you wouldn't see this message"); + await writer.FlushAsync(); + await AssertAppOffline(deploymentResult, ""); + } + + DeletePublishOutput(deploymentResult); + } + + [ConditionalTheory] + [InlineData(HostingModel.InProcess, 500, "500.0")] + [InlineData(HostingModel.OutOfProcess, 502, "502.5")] + public async Task AppOfflineDroppedWhileSiteFailedToStartInShim_AppOfflineServed(HostingModel hostingModel, int statusCode, string content) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(hostingModel: hostingModel, publish: true); + deploymentParameters.WebConfigActionList.Add(WebConfigHelpers.AddOrModifyAspNetCoreSection("processPath", "nonexistent")); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var result = await deploymentResult.HttpClient.GetAsync("/"); + Assert.Equal(statusCode, (int)result.StatusCode); + Assert.Contains(content, await result.Content.ReadAsStringAsync()); + + AddAppOffline(deploymentResult.ContentRoot); + + await AssertAppOffline(deploymentResult); + DeletePublishOutput(deploymentResult); + } + + [ConditionalFact(Skip = "https://github.com/aspnet/IISIntegration/issues/933")] + public async Task AppOfflineDroppedWhileSiteFailedToStartInRequestHandler_SiteStops_InProcess() + { + var deploymentResult = await DeployApp(HostingModel.InProcess); + + // Set file content to empty so it fails at runtime + File.WriteAllText(Path.Combine(deploymentResult.ContentRoot, "Microsoft.AspNetCore.Server.IIS.dll"), ""); + + var result = await deploymentResult.HttpClient.GetAsync("/"); + Assert.Equal(500, (int)result.StatusCode); + Assert.Contains("500.30", await result.Content.ReadAsStringAsync()); + + AddAppOffline(deploymentResult.ContentRoot); + + await deploymentResult.AssertRecycledAsync(() => AssertAppOffline(deploymentResult)); + } + + [ConditionalFact] + [RequiresIIS(IISCapability.ShutdownToken)] + public async Task AppOfflineDroppedWhileSiteStarting_SiteShutsDown_InProcess() + { + // This test often hits a race between debug logging and stdout redirection closing the handle + // we are fine having this race + using (AppVerifier.Disable(DeployerSelector.ServerType, 0x300)) + { + var deploymentResult = await DeployApp(HostingModel.InProcess); + + for (int i = 0; i < 10; i++) + { + // send first request and add app_offline while app is starting + var runningTask = AssertAppOffline(deploymentResult); + + // This test tries to hit a race where we drop app_offline file while + // in process application is starting, application start takes at least 400ms + // so we back off for 100ms to allow request to reach request handler + // Test itself is racy and can result in two scenarios + // 1. ANCM detects app_offline before it starts the request - if AssertAppOffline succeeds we've hit it + // 2. Intended scenario where app starts and then shuts down + // In first case we remove app_offline and try again + await Task.Delay(RetryDelay); + + AddAppOffline(deploymentResult.ContentRoot); + + try + { + await runningTask.DefaultTimeout(); + + // if AssertAppOffline succeeded ANCM have picked up app_offline before starting the app + // try again + RemoveAppOffline(deploymentResult.ContentRoot); + } + catch + { + deploymentResult.AssertWorkerProcessStop(); + return; + } + } + + Assert.True(false); + + } + } + + [ConditionalFact] + public async Task AppOfflineDroppedWhileSiteRunning_SiteShutsDown_InProcess() + { + var deploymentResult = await AssertStarts(HostingModel.InProcess); + + AddAppOffline(deploymentResult.ContentRoot); + + await deploymentResult.AssertRecycledAsync(() => AssertAppOffline(deploymentResult)); + } + + [ConditionalFact] + public async Task AppOfflineDroppedWhileSiteRunning_SiteShutsDown_OutOfProcess() + { + var deploymentResult = await AssertStarts(HostingModel.OutOfProcess); + + // Repeat dropping file and restarting multiple times + for (int i = 0; i < 5; i++) + { + AddAppOffline(deploymentResult.ContentRoot); + await AssertAppOffline(deploymentResult); + RemoveAppOffline(deploymentResult.ContentRoot); + await AssertRunning(deploymentResult); + } + + AddAppOffline(deploymentResult.ContentRoot); + await AssertAppOffline(deploymentResult); + DeletePublishOutput(deploymentResult); + } + + [ConditionalTheory] + [InlineData(HostingModel.InProcess)] + [InlineData(HostingModel.OutOfProcess)] + public async Task AppOfflineDropped_CanRemoveAppOfflineAfterAddingAndSiteWorks(HostingModel hostingModel) + { + var deploymentResult = await DeployApp(hostingModel); + + AddAppOffline(deploymentResult.ContentRoot); + + await AssertAppOffline(deploymentResult); + + RemoveAppOffline(deploymentResult.ContentRoot); + + await AssertRunning(deploymentResult); + } + + [ConditionalTheory] + [InlineData(HostingModel.InProcess)] + [InlineData(HostingModel.OutOfProcess)] + public async Task AppOfflineAddedAndRemovedStress(HostingModel hostingModel) + { + var deploymentResult = await AssertStarts(hostingModel); + + var load = Helpers.StressLoad(deploymentResult.HttpClient, "/HelloWorld", response => { + var statusCode = (int)response.StatusCode; + Assert.True(statusCode == 200 || statusCode == 503, "Status code was " + statusCode); + }); + + for (int i = 0; i < 100; i++) + { + // AddAppOffline might fail if app_offline is being read by ANCM and deleted at the same time + RetryHelper.RetryOperation( + () => AddAppOffline(deploymentResult.ContentRoot), + e => Logger.LogError($"Failed to create app_offline : {e.Message}"), + retryCount: 3, + retryDelayMilliseconds: RetryDelay.Milliseconds); + RemoveAppOffline(deploymentResult.ContentRoot); + } + + try + { + await load; + } + catch (HttpRequestException ex) when (ex.InnerException is IOException | ex.InnerException is SocketException) + { + // IOException in InProcess is fine, just means process stopped + if (hostingModel != HostingModel.InProcess) + { + throw; + } + } + } + + private async Task DeployApp(HostingModel hostingModel = HostingModel.InProcess) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(hostingModel: hostingModel, publish: true); + + return await DeployAsync(deploymentParameters); + } + + private void AddAppOffline(string appPath, string content = "The app is offline.") + { + File.WriteAllText(Path.Combine(appPath, "app_offline.htm"), content); + } + + private void RemoveAppOffline(string appPath) + { + RetryHelper.RetryOperation( + () => File.Delete(Path.Combine(appPath, "app_offline.htm")), + e => Logger.LogError($"Failed to remove app_offline : {e.Message}"), + retryCount: 3, + retryDelayMilliseconds: RetryDelay.Milliseconds); + } + + private async Task AssertAppOffline(IISDeploymentResult deploymentResult, string expectedResponse = "The app is offline.") + { + var response = await deploymentResult.HttpClient.RetryRequestAsync("HelloWorld", r => r.StatusCode == HttpStatusCode.ServiceUnavailable); + Assert.Equal(expectedResponse, await response.Content.ReadAsStringAsync()); + } + + private async Task AssertStarts(HostingModel hostingModel) + { + var deploymentResult = await DeployApp(hostingModel); + + await AssertRunning(deploymentResult); + + return deploymentResult; + } + + private static async Task AssertRunning(IISDeploymentResult deploymentResult) + { + var response = await deploymentResult.HttpClient.RetryRequestAsync("HelloWorld", r => r.IsSuccessStatusCode); + var responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal("Hello World", responseText); + } + + private void DeletePublishOutput(IISDeploymentResult deploymentResult) + { + foreach (var file in Directory.GetFiles(deploymentResult.ContentRoot, "*", SearchOption.AllDirectories)) + { + // Out of process module dll is allowed to be locked + var name = Path.GetFileName(file); + if (name == "aspnetcore.dll" || name == "aspnetcorev2.dll" || name == "aspnetcorev2_outofprocess.dll") + { + continue; + } + File.Delete(file); + } + } + + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/BasicAuthTests.cs b/src/IISIntegration/test/Common.FunctionalTests/BasicAuthTests.cs new file mode 100644 index 0000000000..d8607db21e --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/BasicAuthTests.cs @@ -0,0 +1,70 @@ +// 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.Http; +using System.Net.Http.Headers; +using System.Text; +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; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class BasicAuthTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public BasicAuthTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + public static TestMatrix TestVariants + => TestMatrix.ForServers(DeployerSelector.ServerType) + .WithTfms(Tfm.NetCoreApp22) + .WithApplicationTypes(ApplicationType.Portable) + .WithAllAncmVersions() + .WithAllHostingModels(); + + [ConditionalTheory] + [RequiresEnvironmentVariable("ASPNETCORE_MODULE_TEST_USER")] + [RequiresIIS(IISCapability.BasicAuthentication)] + [MemberData(nameof(TestVariants))] + public async Task BasicAuthTest(TestVariant variant) + { + var username = Environment.GetEnvironmentVariable("ASPNETCORE_MODULE_TEST_USER"); + var password = Environment.GetEnvironmentVariable("ASPNETCORE_MODULE_TEST_PASSWORD"); + + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + deploymentParameters.SetAnonymousAuth(enabled: false); + deploymentParameters.SetWindowsAuth(enabled: false); + deploymentParameters.SetBasicAuth(enabled: true); + + // The default in hosting sets windows auth to true. + var deploymentResult = await DeployAsync(deploymentParameters); + var request = new HttpRequestMessage(HttpMethod.Get, "/Auth"); + var byteArray = new UTF8Encoding().GetBytes(username + ":" + password); + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray)); + + var response = await deploymentResult.HttpClient.SendAsync(request); + + var responseText = await response.Content.ReadAsStringAsync(); + + if (variant.HostingModel == HostingModel.InProcess) + { + Assert.StartsWith("Windows", responseText); + Assert.Contains(username, responseText); + } + else + { + // We expect out-of-proc not allowing basic auth + Assert.Equal("Windows", responseText); + } + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/ClientCertificateFixture.cs b/src/IISIntegration/test/Common.FunctionalTests/ClientCertificateFixture.cs new file mode 100644 index 0000000000..40b6f2265e --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/ClientCertificateFixture.cs @@ -0,0 +1,104 @@ +// 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 + { + private X509Certificate2 _certificate; + private const string _certIssuerPrefix = "CN=IISIntegrationTest_Root"; + + public X509Certificate2 GetOrCreateCertificate() + { + if (_certificate != null) + { + return _certificate; + } + + using (var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine)) + { + store.Open(OpenFlags.ReadWrite); + var parentKey = CreateKeyMaterial(2048); + + // Create a cert name with a random guid to avoid name conflicts + var parentRequest = new CertificateRequest( + _certIssuerPrefix + Guid.NewGuid().ToString(), + 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(); + return imported; + } + } + + public void Dispose() + { + if (_certificate == null) + { + return; + } + + using (var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine)) + { + store.Open(OpenFlags.ReadWrite); + store.Remove(_certificate); + + // Remove any extra certs that were left by previous tests. + for (var i = store.Certificates.Count - 1; i >= 0; i--) + { + var cert = store.Certificates[i]; + if (cert.Issuer.StartsWith(_certIssuerPrefix)) + { + store.Remove(cert); + } + } + 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/src/IISIntegration/test/Common.FunctionalTests/ClientCertificateTests.cs b/src/IISIntegration/test/Common.FunctionalTests/ClientCertificateTests.cs new file mode 100644 index 0000000000..43ccc83eff --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/ClientCertificateTests.cs @@ -0,0 +1,108 @@ +// 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.Http; +using System.Security.Cryptography.X509Certificates; +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 Microsoft.Extensions.Logging; +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 handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (a, b, c, d) => true, + ClientCertificateOptions = ClientCertificateOption.Manual, + }; + + X509Certificate2 cert = null; + if (sendClientCert) + { + cert = _certFixture.GetOrCreateCertificate(); + handler.ClientCertificates.Add(cert); + } + + var deploymentResult = await DeployAsync(deploymentParameters); + + var client = deploymentResult.CreateClient(handler); + var response = await client.GetAsync("GetClientCert"); + + var responseText = await response.Content.ReadAsStringAsync(); + + try + { + if (sendClientCert) + { + Assert.Equal($"Enabled;{cert.GetCertHashString()}", responseText); + } + else + { + Assert.Equal("Disabled", responseText); + } + } + catch (Exception ex) + { + Logger.LogError($"Certificate is invalid. Issuer name: {cert.Issuer}"); + using (var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine)) + { + Logger.LogError($"List of current certificates in root store:"); + store.Open(OpenFlags.ReadWrite); + foreach (var otherCert in store.Certificates) + { + Logger.LogError(otherCert.Issuer); + } + store.Close(); + } + throw ex; + } + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/ClientDisconnectStress.cs b/src/IISIntegration/test/Common.FunctionalTests/ClientDisconnectStress.cs new file mode 100644 index 0000000000..9deeae3f92 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/ClientDisconnectStress.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class ClientDisconnectStressTests: FunctionalTestsBase + { + private readonly PublishedSitesFixture _fixture; + + public ClientDisconnectStressTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalTheory] + [InlineData(HostingModel.InProcess)] + [InlineData(HostingModel.OutOfProcess)] + public async Task ClientDisconnectStress(HostingModel hostingModel) + { + var site = await StartAsync(_fixture.GetBaseDeploymentParameters(hostingModel)); + var maxRequestSize = 1000; + var blockSize = 40; + var random = new Random(); + async Task RunRequests() + { + using (var connection = new TestConnection(site.HttpClient.BaseAddress.Port)) + { + await connection.Send( + "POST /ReadAndFlushEcho HTTP/1.1", + $"Content-Length: {maxRequestSize}", + "Host: localhost", + "Connection: close", + "", + ""); + + var disconnectAfter = random.Next(maxRequestSize); + var data = new byte[blockSize]; + for (int i = 0; i < disconnectAfter / blockSize; i++) + { + await connection.Stream.WriteAsync(data); + } + } + } + + List tasks = new List(); + for (int i = 0; i < 100; i++) + { + tasks.Add(Task.Run(RunRequests)); + } + + await Task.WhenAll(tasks); + + StopServer(); + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/CommonStartupTests.cs b/src/IISIntegration/test/Common.FunctionalTests/CommonStartupTests.cs new file mode 100644 index 0000000000..e2bcf2a8f9 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/CommonStartupTests.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.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class CommonStartupTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public CommonStartupTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + public static TestMatrix TestVariants + => TestMatrix.ForServers(DeployerSelector.ServerType) + .WithTfms(Tfm.NetCoreApp22) + .WithAllApplicationTypes() + .WithAllAncmVersions() + .WithAllHostingModels(); + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task StartupStress(TestVariant variant) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + + var deploymentResult = await DeployAsync(deploymentParameters); + + await Helpers.StressLoad(deploymentResult.HttpClient, "/HelloWorld", response => { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Hello World", response.Content.ReadAsStringAsync().GetAwaiter().GetResult()); + }); + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/CompressionTests.cs b/src/IISIntegration/test/Common.FunctionalTests/CompressionTests.cs new file mode 100644 index 0000000000..c2d0277c4c --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/CompressionTests.cs @@ -0,0 +1,58 @@ +// 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.Linq; +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 abstract class CompressionTests : FixtureLoggedTest + { + private readonly IISTestSiteFixture _fixture; + + [Collection(IISTestSiteCollection.Name)] + public class InProc: CompressionTests + { + public InProc(IISTestSiteFixture fixture) : base(fixture) { } + } + + [Collection(OutOfProcessTestSiteCollection.Name)] + public class OutOfProcess: CompressionTests + { + public OutOfProcess(OutOfProcessTestSiteFixture fixture) : base(fixture) { } + } + + [Collection(OutOfProcessV1TestSiteCollection.Name)] + public class OutOfProcessV1: CompressionTests + { + public OutOfProcessV1(OutOfProcessV1TestSiteFixture fixture) : base(fixture) { } + } + + protected CompressionTests(IISTestSiteFixture fixture) : base(fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task PassesThroughCompression() + { + var request = new HttpRequestMessage(HttpMethod.Get, "/CompressedData"); + + request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip")); + + var response = await _fixture.Client.SendAsync(request); + Assert.Equal("gzip", response.Content.Headers.ContentEncoding.Single()); + Assert.Equal( + new byte[] { + 0x1F, 0x8B, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x0B, 0x63, 0x60, 0xA0, 0x3D, 0x00, 0x00, + 0xCA, 0xC6, 0x88, 0x99, 0x64, 0x00, 0x00, 0x00 }, + await response.Content.ReadAsByteArrayAsync()); + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/ConfigurationChangeTests.cs b/src/IISIntegration/test/Common.FunctionalTests/ConfigurationChangeTests.cs new file mode 100644 index 0000000000..099cefb97a --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/ConfigurationChangeTests.cs @@ -0,0 +1,126 @@ +// 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.Net.Http; +using System.Net.Sockets; +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 Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class ConfigurationChangeTests : IISFunctionalTestBase + { + private static readonly TimeSpan RetryDelay = TimeSpan.FromMilliseconds(100); + private readonly PublishedSitesFixture _fixture; + + public ConfigurationChangeTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task ConfigurationChangeStopsInProcess() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(HostingModel.InProcess, publish: true); + + var deploymentResult = await DeployAsync(deploymentParameters); + + await deploymentResult.AssertStarts(); + + // Just "touching" web.config should be enough + deploymentResult.ModifyWebConfig(element => {}); + + await deploymentResult.AssertRecycledAsync(); + } + + [ConditionalTheory] + [InlineData(AncmVersion.AspNetCoreModule)] + [InlineData(AncmVersion.AspNetCoreModuleV2)] + public async Task ConfigurationChangeForcesChildProcessRestart(AncmVersion version) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(HostingModel.OutOfProcess, publish: true); + deploymentParameters.AncmVersion = version; + + var deploymentResult = await DeployAsync(deploymentParameters); + + var processBefore = await deploymentResult.HttpClient.GetStringAsync("/ProcessId"); + + // Just "touching" web.config should be enough + deploymentResult.ModifyWebConfig(element => {}); + + // Have to retry here to allow ANCM to receive notification and react to it + // Verify that worker process gets restarted with new process id + await deploymentResult.HttpClient.RetryRequestAsync("/ProcessId", async r => await r.Content.ReadAsStringAsync() != processBefore); + } + + [ConditionalFact] + public async Task OutOfProcessToInProcessHostingModelSwitchWorks() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(HostingModel.OutOfProcess, publish: true); + + var deploymentResult = await DeployAsync(deploymentParameters); + + await deploymentResult.AssertStarts(); + + deploymentResult.ModifyWebConfig(element => element + .Descendants("system.webServer") + .Single() + .GetOrAdd("aspNetCore") + .SetAttributeValue("hostingModel", "inprocess")); + + // Have to retry here to allow ANCM to receive notification and react to it + // Verify that inprocess application was created and tried to start + await deploymentResult.HttpClient.RetryRequestAsync("/HelloWorld", r => r.StatusCode == HttpStatusCode.InternalServerError); + + StopServer(); + EventLogHelpers.VerifyEventLogEvent(deploymentResult, "Could not find the assembly 'aspnetcorev2_inprocess.dll'", Logger); + } + + [ConditionalTheory] + [InlineData(HostingModel.InProcess)] + [InlineData(HostingModel.OutOfProcess)] + public async Task ConfigurationTouchedStress(HostingModel hostingModel) + { + var deploymentResult = await DeployAsync(_fixture.GetBaseDeploymentParameters(hostingModel, publish: true)); + + await deploymentResult.AssertStarts(); + var load = Helpers.StressLoad(deploymentResult.HttpClient, "/HelloWorld", response => { + var statusCode = (int)response.StatusCode; + Assert.True(statusCode == 200 || statusCode == 503, "Status code was " + statusCode); + }); + + for (int i = 0; i < 100; i++) + { + // ModifyWebConfig might fail if web.config is being read by IIS + RetryHelper.RetryOperation( + () => deploymentResult.ModifyWebConfig(element => {}), + e => Logger.LogError($"Failed to touch web.config : {e.Message}"), + retryCount: 3, + retryDelayMilliseconds: RetryDelay.Milliseconds); + } + + try + { + await load; + } + catch (HttpRequestException ex) when (ex.InnerException is IOException | ex.InnerException is SocketException) + { + // IOException in InProcess is fine, just means process stopped + if (hostingModel != HostingModel.InProcess) + { + throw; + } + } + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ClientDisconnectTests.cs b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ClientDisconnectTests.cs new file mode 100644 index 0000000000..6e2d1bc752 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ClientDisconnectTests.cs @@ -0,0 +1,108 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class ClientDisconnectTests: FixtureLoggedTest + { + private readonly IISTestSiteFixture _fixture; + + public ClientDisconnectTests(IISTestSiteFixture fixture): base(fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task ServerWorksAfterClientDisconnect() + { + using (var connection = _fixture.CreateTestConnection()) + { + var message = "Hello"; + await connection.Send( + "POST /ReadAndWriteSynchronously HTTP/1.1", + $"Content-Length: {100000}", + "Host: localhost", + "Connection: close", + "", + ""); + + await connection.Send(message); + + await connection.Receive( + "HTTP/1.1 200 OK", + ""); + } + + var response = await _fixture.Client.GetAsync("HelloWorld"); + + var responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal("Hello World", responseText); + } + + [ConditionalFact] + public async Task RequestAbortedTokenFires() + { + using (var connection = _fixture.CreateTestConnection()) + { + await connection.Send( + "GET /WaitForAbort HTTP/1.1", + "Host: localhost", + "Connection: close", + "", + ""); + + await _fixture.Client.RetryRequestAsync("/WaitingRequestCount", async message => await message.Content.ReadAsStringAsync() == "1"); + } + + await _fixture.Client.RetryRequestAsync("/WaitingRequestCount", async message => await message.Content.ReadAsStringAsync() == "0"); + } + + [ConditionalFact] + public async Task ClientDisconnectCallbackStress() + { + // Fixture initialization fails if inside of the Task.Run, so send an + // initial request to initialize the fixture. + var response = await _fixture.Client.GetAsync("HelloWorld"); + var numTotalRequests = 0; + for (var j = 0; j < 20; j++) + { + // Windows has a max connection limit of 10 for the IIS server, + // so setting limit fairly low. + const int numRequests = 5; + async Task RunRequests() + { + using (var connection = _fixture.CreateTestConnection()) + { + await connection.Send( + "GET /WaitForAbort HTTP/1.1", + "Host: localhost", + "Connection: close", + "", + ""); + await _fixture.Client.RetryRequestAsync("/WaitingRequestCount", async message => await message.Content.ReadAsStringAsync() != "0"); + Interlocked.Increment(ref numTotalRequests); + } + } + + List tasks = new List(); + for (int i = 0; i < numRequests; i++) + { + tasks.Add(Task.Run(RunRequests)); + } + + await Task.WhenAll(tasks); + + await _fixture.Client.RetryRequestAsync("/WaitingRequestCount", async message => await message.Content.ReadAsStringAsync() == "0"); + } + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Inprocess/CompressionTests.cs b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/CompressionTests.cs new file mode 100644 index 0000000000..ce1c84e609 --- /dev/null +++ b/src/IISIntegration/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 CompressionModuleTests : FixtureLoggedTest + { + private readonly IISCompressionSiteFixture _fixture; + + public CompressionModuleTests(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/src/IISIntegration/test/Common.FunctionalTests/Inprocess/EnvironmentVariableTests.cs b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/EnvironmentVariableTests.cs new file mode 100644 index 0000000000..ae8fde39ed --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/EnvironmentVariableTests.cs @@ -0,0 +1,51 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class EnvironmentVariableTests: FixtureLoggedTest + { + private readonly IISTestSiteFixture _fixture; + + public EnvironmentVariableTests(IISTestSiteFixture fixture): base(fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task GetUniqueEnvironmentVariable() + { + Assert.Equal("foobar", await _fixture.Client.GetStringAsync("/CheckEnvironmentVariable")); + } + + [ConditionalFact] + public async Task GetLongEnvironmentVariable() + { + Assert.Equal( + "AReallyLongValueThatIsGreaterThan300CharactersToForceResizeInNative" + + "AReallyLongValueThatIsGreaterThan300CharactersToForceResizeInNative" + + "AReallyLongValueThatIsGreaterThan300CharactersToForceResizeInNative" + + "AReallyLongValueThatIsGreaterThan300CharactersToForceResizeInNative" + + "AReallyLongValueThatIsGreaterThan300CharactersToForceResizeInNative" + + "AReallyLongValueThatIsGreaterThan300CharactersToForceResizeInNative", + await _fixture.Client.GetStringAsync("/CheckEnvironmentLongValueVariable")); + } + + [ConditionalFact] + public async Task GetExistingEnvironmentVariable() + { + Assert.Contains(";foobarbaz", await _fixture.Client.GetStringAsync("/CheckAppendedEnvironmentVariable")); + } + + [ConditionalFact] + public async Task AuthHeaderEnvironmentVariableRemoved() + { + Assert.DoesNotContain("shouldberemoved", await _fixture.Client.GetStringAsync("/CheckRemoveAuthEnvironmentVariable")); + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ErrorPagesTests.cs b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ErrorPagesTests.cs new file mode 100644 index 0000000000..bf45949491 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ErrorPagesTests.cs @@ -0,0 +1,131 @@ +// 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; +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.IIS; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class ErrorPagesTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public ErrorPagesTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + public async Task IncludesAdditionalErrorPageTextInProcessHandlerLoadFailure_CorrectString() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + var response = await DeployAppWithStartupFailure(deploymentParameters); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + StopServer(); + + var responseString = await response.Content.ReadAsStringAsync(); + Assert.Contains("HTTP Error 500.0 - ANCM In-Process Handler Load Failure", responseString); + VerifyNoExtraTrailingBytes(responseString); + + await AssertLink(response); + } + + [ConditionalFact] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + public async Task IncludesAdditionalErrorPageTextOutOfProcessStartupFailure_CorrectString() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(HostingModel.OutOfProcess, publish: true); + var response = await DeployAppWithStartupFailure(deploymentParameters); + + Assert.Equal(HttpStatusCode.BadGateway, response.StatusCode); + + StopServer(); + + var responseString = await response.Content.ReadAsStringAsync(); + Assert.Contains("HTTP Error 502.5 - ANCM Out-Of-Process Startup Failure", responseString); + VerifyNoExtraTrailingBytes(responseString); + + await AssertLink(response); + } + + [ConditionalFact] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + public async Task IncludesAdditionalErrorPageTextOutOfProcessHandlerLoadFailure_CorrectString() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(HostingModel.OutOfProcess, publish: true); + deploymentParameters.HandlerSettings["handlerVersion"] = "88.93"; + deploymentParameters.EnvironmentVariables["ANCM_ADDITIONAL_ERROR_PAGE_LINK"] = "http://example"; + + var deploymentResult = await DeployAsync(deploymentParameters); + var response = await deploymentResult.HttpClient.GetAsync("HelloWorld"); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + StopServer(); + + var responseString = await response.Content.ReadAsStringAsync(); + Assert.Contains("HTTP Error 500.0 - ANCM Out-Of-Process Handler Load Failure", responseString); + VerifyNoExtraTrailingBytes(responseString); + + await AssertLink(response); + } + + [ConditionalFact] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + [RequiresNewHandler] + public async Task IncludesAdditionalErrorPageTextInProcessStartupFailure_CorrectString() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} EarlyReturn"); + deploymentParameters.EnvironmentVariables["ANCM_ADDITIONAL_ERROR_PAGE_LINK"] = "http://example"; + + var deploymentResult = await DeployAsync(deploymentParameters); + var response = await deploymentResult.HttpClient.GetAsync("HelloWorld"); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + StopServer(); + + var responseString = await response.Content.ReadAsStringAsync(); + Assert.Contains("HTTP Error 500.30 - ANCM In-Process Start Failure", responseString); + VerifyNoExtraTrailingBytes(responseString); + + await AssertLink(response); + } + + private static void VerifyNoExtraTrailingBytes(string responseString) + { + if (!DeployerSelector.IsBackwardsCompatiblityTest) + { + Assert.EndsWith("\r\n", responseString); + } + } + + private static async Task AssertLink(HttpResponseMessage response) + { + Assert.Contains(" http://example and ", await response.Content.ReadAsStringAsync()); + } + + private async Task DeployAppWithStartupFailure(IISDeploymentParameters deploymentParameters) + { + deploymentParameters.WebConfigActionList.Add(WebConfigHelpers.AddOrModifyAspNetCoreSection("processPath", "doesnot")); + deploymentParameters.WebConfigActionList.Add(WebConfigHelpers.AddOrModifyAspNetCoreSection("arguments", "start")); + + deploymentParameters.EnvironmentVariables["ANCM_ADDITIONAL_ERROR_PAGE_LINK"] = "http://example"; + + var deploymentResult = await DeployAsync(deploymentParameters); + + return await deploymentResult.HttpClient.GetAsync("HelloWorld"); + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Inprocess/EventLogTests.cs b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/EventLogTests.cs new file mode 100644 index 0000000000..1df7f3c077 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/EventLogTests.cs @@ -0,0 +1,46 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class EventLogTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public EventLogTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task CheckStartupEventLogMessage() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + + var deploymentResult = await DeployAsync(deploymentParameters); + await deploymentResult.AssertStarts(); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvent(deploymentResult, "Application '.+' started the coreclr in-process successfully."); + } + + [ConditionalFact] + public async Task CheckShutdownEventLogMessage() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + var deploymentResult = await DeployAsync(deploymentParameters); + await deploymentResult.AssertStarts(); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvent(deploymentResult, "Application '.+' has shutdown."); + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Inprocess/FeatureCollectionTests.cs b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/FeatureCollectionTests.cs new file mode 100644 index 0000000000..e31dc3dbaa --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/FeatureCollectionTests.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class FeatureCollectionTest + { + private readonly IISTestSiteFixture _fixture; + + public FeatureCollectionTest(IISTestSiteFixture fixture) + { + _fixture = fixture; + } + + [ConditionalTheory] + [InlineData("FeatureCollectionSetRequestFeatures")] + [InlineData("FeatureCollectionSetResponseFeatures")] + [InlineData("FeatureCollectionSetConnectionFeatures")] + public async Task FeatureCollectionTest_SetHttpContextFeatures(string path) + { + Assert.Equal("Success", await _fixture.Client.GetStringAsync(path + "/path" + "?query")); + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Inprocess/FixtureLoggedTest.cs b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/FixtureLoggedTest.cs new file mode 100644 index 0000000000..705af2b213 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/FixtureLoggedTest.cs @@ -0,0 +1,31 @@ +// 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.Reflection; +using Microsoft.Extensions.Logging.Testing; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + public class FixtureLoggedTest: LoggedTest + { + private readonly IISTestSiteFixture _fixture; + + public FixtureLoggedTest(IISTestSiteFixture fixture) + { + _fixture = fixture; + } + + public override void Initialize(MethodInfo methodInfo, object[] testMethodArguments, ITestOutputHelper testOutputHelper) + { + base.Initialize(methodInfo, testMethodArguments, testOutputHelper); + _fixture.Attach(this); + } + + public override void Dispose() + { + _fixture.Detach(this); + base.Dispose(); + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Inprocess/FrebTests.cs b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/FrebTests.cs new file mode 100644 index 0000000000..12e2f6ae53 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/FrebTests.cs @@ -0,0 +1,181 @@ +// 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.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class FrebTests : LogFileTestBase + { + private readonly PublishedSitesFixture _fixture; + public FrebTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + public static IList FrebChecks() + { + var list = new List(); + list.Add(new FrebLogItem("ANCM_INPROC_EXECUTE_REQUEST_START")); + list.Add(new FrebLogItem("ANCM_INPROC_EXECUTE_REQUEST_COMPLETION", "1")); + list.Add(new FrebLogItem("ANCM_INPROC_ASYNC_COMPLETION_START")); + list.Add(new FrebLogItem("ANCM_INPROC_ASYNC_COMPLETION_COMPLETION", "0")); + list.Add(new FrebLogItem("ANCM_INPROC_MANAGED_REQUEST_COMPLETION")); + return list; + } + + [ConditionalFact] + [RequiresIIS(IISCapability.FailedRequestTracingModule)] + public async Task CheckCommonFrebEvents() + { + var result = await SetupFrebApp(); + + await result.HttpClient.GetAsync("HelloWorld"); + + StopServer(); + + AssertFrebLogs(result, FrebChecks()); + } + + [ConditionalFact] + [RequiresNewShim] + [RequiresIIS(IISCapability.FailedRequestTracingModule)] + public async Task FrebIncludesHResultFailures() + { + var parameters = _fixture.GetBaseDeploymentParameters(publish: true); + parameters.TransformArguments((args, _) => string.Empty); + var result = await SetupFrebApp(parameters); + + await result.HttpClient.GetAsync("HelloWorld"); + + StopServer(); + + AssertFrebLogs(result, new FrebLogItem("ANCM_HRESULT_FAILED"), new FrebLogItem("ANCM_EXCEPTION_CAUGHT")); + } + + [ConditionalFact] + [RequiresIIS(IISCapability.FailedRequestTracingModule)] + public async Task CheckFailedRequestEvents() + { + var result = await SetupFrebApp(); + + await result.HttpClient.GetAsync("Throw"); + + StopServer(); + + AssertFrebLogs(result, new FrebLogItem("ANCM_INPROC_ASYNC_COMPLETION_COMPLETION", "2")); + } + + [ConditionalFact] + [RequiresIIS(IISCapability.FailedRequestTracingModule)] + public async Task CheckFrebDisconnect() + { + var result = await SetupFrebApp(); + + using (var connection = new TestConnection(result.HttpClient.BaseAddress.Port)) + { + await connection.Send( + "GET /WaitForAbort HTTP/1.1", + "Host: localhost", + "Connection: close", + "", + ""); + await result.HttpClient.RetryRequestAsync("/WaitingRequestCount", async message => await message.Content.ReadAsStringAsync() == "1"); + } + + StopServer(); + + // The order of freb logs is based on when the requests are complete. + // This is non-deterministic here, so we need to check both freb files for a request that was disconnected. + AssertFrebLogs(result, new FrebLogItem("ANCM_INPROC_REQUEST_DISCONNECT"), new FrebLogItem("ANCM_INPROC_MANAGED_REQUEST_COMPLETION")); + } + + private async Task SetupFrebApp(IISDeploymentParameters parameters = null) + { + parameters = parameters ?? _fixture.GetBaseDeploymentParameters(publish: true); + parameters.EnableFreb("Verbose", _logFolderPath); + + Directory.CreateDirectory(_logFolderPath); + var result = await DeployAsync(parameters); + return result; + } + + private void AssertFrebLogs(IISDeploymentResult result, params FrebLogItem[] expectedFrebEvents) + { + AssertFrebLogs(result, (IEnumerable)expectedFrebEvents); + } + + private void AssertFrebLogs(IISDeploymentResult result, IEnumerable expectedFrebEvents) + { + var frebEvent = GetFrebLogItems(result); + foreach (var expectedEvent in expectedFrebEvents) + { + Assert.Contains(expectedEvent, frebEvent); + } + } + + private IEnumerable GetFrebLogItems(IISDeploymentResult result) + { + var folderPath = Helpers.GetFrebFolder(_logFolderPath, result); + var xmlFiles = Directory.GetFiles(folderPath).Where(f => f.EndsWith("xml")); + var frebEvents = new List(); + + foreach (var xmlFile in xmlFiles) + { + var xDocument = XDocument.Load(xmlFile).Root; + var nameSpace = (XNamespace)"http://schemas.microsoft.com/win/2004/08/events/event"; + var eventElements = xDocument.Descendants(nameSpace + "Event"); + foreach (var eventElement in eventElements) + { + var eventElementWithOpCode = eventElement.Descendants(nameSpace + "RenderingInfo").Single().Descendants(nameSpace + "Opcode").Single(); + var requestStatus = eventElement.Element(nameSpace + "EventData").Descendants().Where(el => el.Attribute("Name").Value == "requestStatus").SingleOrDefault(); + frebEvents.Add(new FrebLogItem(eventElementWithOpCode.Value, requestStatus?.Value)); + } + } + + return frebEvents; + } + + public class FrebLogItem + { + private string _opCode; + private string _requestStatus; + + public FrebLogItem(string opCode) + { + _opCode = opCode; + } + + public FrebLogItem(string opCode, string requestStatus) + { + _opCode = opCode; + _requestStatus = requestStatus; + } + + public override bool Equals(object obj) + { + var item = obj as FrebLogItem; + return item != null && + _opCode == item._opCode && + _requestStatus == item._requestStatus; + } + + public override int GetHashCode() + { + return HashCode.Combine(_opCode, _requestStatus); + } + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Inprocess/HelloWorldTests.cs b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/HelloWorldTests.cs new file mode 100644 index 0000000000..1b2ad70600 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/HelloWorldTests.cs @@ -0,0 +1,30 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class HelloWorldInProcessTests + { + private readonly IISTestSiteFixture _fixture; + + public HelloWorldInProcessTests(IISTestSiteFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task HelloWorld_InProcess() + { + Assert.Equal("Hello World", await _fixture.Client.GetStringAsync("/HelloWorld")); + + Assert.Equal("/Path??", await _fixture.Client.GetStringAsync("/HelloWorld/Path%3F%3F?query")); + + Assert.Equal("?query", await _fixture.Client.GetStringAsync("/HelloWorld/Query%3F%3F?query")); + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Inprocess/HostingEnvironmentTests.cs b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/HostingEnvironmentTests.cs new file mode 100644 index 0000000000..061b828a6c --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/HostingEnvironmentTests.cs @@ -0,0 +1,33 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class HostingEnvironmentTests: FixtureLoggedTest + { + private readonly IISTestSiteFixture _fixture; + + public HostingEnvironmentTests(IISTestSiteFixture fixture): base(fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + [RequiresIIS(IISCapability.ShutdownToken)] + public async Task HostingEnvironmentIsCorrect() + { + Assert.Equal( + $"ContentRootPath {_fixture.DeploymentResult.ContentRoot}" + Environment.NewLine + + $"WebRootPath {_fixture.DeploymentResult.ContentRoot}\\wwwroot" + Environment.NewLine + + $"CurrentDirectory {Path.GetDirectoryName(_fixture.DeploymentResult.HostProcess.MainModule.FileName)}", + await _fixture.Client.GetStringAsync("/HostingEnvironment")); + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Inprocess/InvalidReadWriteOperationTests.cs b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/InvalidReadWriteOperationTests.cs new file mode 100644 index 0000000000..95c05308bd --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/InvalidReadWriteOperationTests.cs @@ -0,0 +1,84 @@ +// 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.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class InvalidReadWriteOperationTests + { + private readonly IISTestSiteFixture _fixture; + + public InvalidReadWriteOperationTests(IISTestSiteFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task TestReadOffsetWorks() + { + var result = await _fixture.Client.PostAsync($"/TestReadOffsetWorks", new StringContent("Hello World")); + Assert.Equal("Hello World", await result.Content.ReadAsStringAsync()); + } + + [ConditionalTheory] + [InlineData("/InvalidOffsetSmall")] + [InlineData("/InvalidOffsetLarge")] + [InlineData("/InvalidCountSmall")] + [InlineData("/InvalidCountLarge")] + [InlineData("/InvalidCountWithOffset")] + public async Task TestInvalidReadOperations(string operation) + { + var result = await _fixture.Client.GetStringAsync($"/TestInvalidReadOperations{operation}"); + Assert.Equal("Success", result); + } + + [ConditionalTheory] + [InlineData("/NullBuffer")] + [InlineData("/InvalidCountZeroRead")] + public async Task TestValidReadOperations(string operation) + { + var result = await _fixture.Client.GetStringAsync($"/TestValidReadOperations{operation}"); + Assert.Equal("Success", result); + } + + [ConditionalTheory] + [InlineData("/NullBufferPost")] + [InlineData("/InvalidCountZeroReadPost")] + public async Task TestValidReadOperationsPost(string operation) + { + var result = await _fixture.Client.PostAsync($"/TestValidReadOperations{operation}", new StringContent("hello")); + Assert.Equal("Success", await result.Content.ReadAsStringAsync()); + } + + [ConditionalTheory] + [InlineData("/InvalidOffsetSmall")] + [InlineData("/InvalidOffsetLarge")] + [InlineData("/InvalidCountSmall")] + [InlineData("/InvalidCountLarge")] + [InlineData("/InvalidCountWithOffset")] + public async Task TestInvalidWriteOperations(string operation) + { + var result = await _fixture.Client.GetStringAsync($"/TestInvalidWriteOperations{operation}"); + Assert.Equal("Success", result); + } + + [ConditionalFact] + public async Task TestValidWriteOperations() + { + var result = await _fixture.Client.GetStringAsync($"/TestValidWriteOperations/NullBuffer"); + Assert.Equal("Success", result); + } + + [ConditionalFact] + public async Task TestValidWriteOperationsPost() + { + var result = await _fixture.Client.PostAsync($"/TestValidWriteOperations/NullBufferPost", new StringContent("hello")); + Assert.Equal("Success", await result.Content.ReadAsStringAsync()); + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Inprocess/LargeResponseBodyTests.cs b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/LargeResponseBodyTests.cs new file mode 100644 index 0000000000..40db2cfdb8 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/LargeResponseBodyTests.cs @@ -0,0 +1,36 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class LargeResponseBodyTests + { + private readonly IISTestSiteFixture _fixture; + + public LargeResponseBodyTests(IISTestSiteFixture fixture) + { + _fixture = fixture; + } + + [ConditionalTheory] + [InlineData(65000)] + [InlineData(1000000)] + [InlineData(10000000)] + [InlineData(100000000)] + public async Task LargeResponseBodyTest_CheckAllResponseBodyBytesWritten(int query) + { + Assert.Equal(new string('a', query), await _fixture.Client.GetStringAsync($"/LargeResponseBody?length={query}")); + } + + [ConditionalFact] + public async Task LargeResponseBodyFromFile_CheckAllResponseBodyBytesWritten() + { + Assert.Equal(200000000, (await _fixture.Client.GetStringAsync($"/LargeResponseFile")).Length); + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Inprocess/LogPipeTests.cs b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/LogPipeTests.cs new file mode 100644 index 0000000000..4d34c3154f --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/LogPipeTests.cs @@ -0,0 +1,83 @@ +// 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; +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; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class LogPipeTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public LogPipeTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalTheory] + [InlineData("ConsoleErrorWrite")] + [InlineData("ConsoleWrite")] + public async Task CheckStdoutLoggingToPipe_DoesNotCrashProcess(string path) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + var deploymentResult = await DeployAsync(deploymentParameters); + + await Helpers.AssertStarts(deploymentResult, path); + + StopServer(); + + if (deploymentParameters.ServerType == ServerType.IISExpress) + { + Assert.Contains(TestSink.Writes, context => context.Message.Contains("TEST MESSAGE")); + } + } + + [ConditionalTheory] + [InlineData("ConsoleErrorWriteStartServer")] + [InlineData("ConsoleWriteStartServer")] + public async Task CheckStdoutLoggingToPipeWithFirstWrite(string path) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + + var firstWriteString = "TEST MESSAGE"; + + deploymentParameters.TransformArguments((a, _) => $"{a} {path}"); + + var deploymentResult = await DeployAsync(deploymentParameters); + + await Helpers.AssertStarts(deploymentResult); + + StopServer(); + + if (deploymentParameters.ServerType == ServerType.IISExpress) + { + // We can't read stdout logs from IIS as they aren't redirected. + Assert.Contains(TestSink.Writes, context => context.Message.Contains(firstWriteString)); + } + } + + [ConditionalFact] + public async Task CheckUnicodePipe() + { + var path = "CheckConsoleFunctions"; + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} {path}"); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync(path); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + StopServer(); + EventLogHelpers.VerifyEventLogEvent(deploymentResult, EventLogHelpers.InProcessThreadExitStdOut(deploymentResult, "12", "(.*)彡⾔(.*)")); + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ResponseHeaderTests.cs b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ResponseHeaderTests.cs new file mode 100644 index 0000000000..fec5c227ec --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ResponseHeaderTests.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.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class ResponseHeaders + { + private readonly IISTestSiteFixture _fixture; + + public ResponseHeaders(IISTestSiteFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task AddResponseHeaders_HeaderValuesAreSetCorrectly() + { + var response = await _fixture.Client.GetAsync("ResponseHeaders"); + var responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Request Complete", responseText); + + Assert.True(response.Headers.TryGetValues("UnknownHeader", out var headerValues)); + Assert.Equal("test123=foo", headerValues.First()); + + Assert.True(response.Content.Headers.TryGetValues(HeaderNames.ContentType, out headerValues)); + Assert.Equal("text/plain", headerValues.First()); + + Assert.True(response.Headers.TryGetValues("MultiHeader", out headerValues)); + Assert.Equal(2, headerValues.Count()); + Assert.Equal("1", headerValues.First()); + Assert.Equal("2", headerValues.Last()); + } + + [ConditionalFact] + public async Task ErrorCodeIsSetForExceptionDuringRequest() + { + var response = await _fixture.Client.GetAsync("Throw"); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.Equal("Internal Server Error", response.ReasonPhrase); + } + + [ConditionalTheory] + [InlineData(200, "custom", "custom", null)] + [InlineData(200, "custom", "custom", "Custom body")] + [InlineData(200, "custom", "custom", "")] + + + [InlineData(500, "", "Internal Server Error", null)] + [InlineData(500, "", "Internal Server Error", "Custom body")] + [InlineData(500, "", "Internal Server Error", "")] + + [InlineData(400, "custom", "custom", null)] + [InlineData(400, "", "Bad Request", "Custom body")] + [InlineData(400, "", "Bad Request", "")] + + [InlineData(999, "", "", null)] + [InlineData(999, "", "", "Custom body")] + [InlineData(999, "", "", "")] + public async Task CustomErrorCodeWorks(int code, string reason, string expectedReason, string body) + { + var response = await _fixture.Client.GetAsync($"SetCustomErorCode?code={code}&reason={reason}&writeBody={body != null}&body={body}"); + Assert.Equal((HttpStatusCode)code, response.StatusCode); + Assert.Equal(expectedReason, response.ReasonPhrase); + + // ReadAsStringAsync returns empty string for empty results + Assert.Equal(body ?? string.Empty, await response.Content.ReadAsStringAsync()); + } + + [ConditionalFact] + public async Task ServerHeaderIsOverriden() + { + var response = await _fixture.Client.GetAsync("OverrideServer"); + Assert.Equal("MyServer/7.8", response.Headers.Server.Single().Product.ToString()); + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ResponseInvalidOrderingTests.cs b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ResponseInvalidOrderingTests.cs new file mode 100644 index 0000000000..c39e155e77 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ResponseInvalidOrderingTests.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class ResponseInvalidOrderingTest + { + private readonly IISTestSiteFixture _fixture; + + public ResponseInvalidOrderingTest(IISTestSiteFixture fixture) + { + _fixture = fixture; + } + + [ConditionalTheory] + [InlineData("SetStatusCodeAfterWrite")] + [InlineData("SetHeaderAfterWrite")] + public async Task ResponseInvalidOrderingTests_ExpectFailure(string path) + { + Assert.Equal($"Started_{path}Threw_Finished", await _fixture.Client.GetStringAsync("/ResponseInvalidOrdering/" + path)); + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ServerVariablesTest.cs b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ServerVariablesTest.cs new file mode 100644 index 0000000000..d18c4c7f09 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/ServerVariablesTest.cs @@ -0,0 +1,59 @@ +// 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.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class ServerVariablesTest + { + private readonly IISTestSiteFixture _fixture; + + public ServerVariablesTest(IISTestSiteFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task ProvidesAccessToServerVariables() + { + var port = _fixture.Client.BaseAddress.Port; + Assert.Equal("SERVER_PORT: " + port, await _fixture.Client.GetStringAsync("/ServerVariable?q=SERVER_PORT")); + Assert.Equal("QUERY_STRING: q=QUERY_STRING", await _fixture.Client.GetStringAsync("/ServerVariable?q=QUERY_STRING")); + } + + [ConditionalFact] + public async Task ReturnsNullForUndefinedServerVariable() + { + 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() + { + Assert.DoesNotContain(@"\\?\", await _fixture.Client.GetStringAsync("/BasePath")); + } + + [ConditionalFact] + public async Task GetServerVariableDoesNotCrash() + { + await Helpers.StressLoad(_fixture.Client, "/GetServerVariableStress", response => { + var text = response.Content.ReadAsStringAsync().Result; + Assert.StartsWith("Response Begin", text); + Assert.EndsWith("Response End", text); + }); + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Inprocess/StartupExceptionTests.cs b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/StartupExceptionTests.cs new file mode 100644 index 0000000000..cadfed9781 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/StartupExceptionTests.cs @@ -0,0 +1,100 @@ +// 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; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class StartupExceptionTests : LogFileTestBase + { + private readonly PublishedSitesFixture _fixture; + + public StartupExceptionTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalTheory] + [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); + + 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] + [InlineData("CheckLargeStdErrWrites")] + [InlineData("CheckLargeStdOutWrites")] + [InlineData("CheckOversizedStdErrWrites")] + [InlineData("CheckOversizedStdOutWrites")] + public async Task CheckStdoutWithLargeWrites_LogFile(string mode) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} {mode}"); + deploymentParameters.EnableLogging(_logFolderPath); + + var deploymentResult = await DeployAsync(deploymentParameters); + + 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] + public async Task CheckValidConsoleFunctions() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} CheckConsoleFunctions"); + + var deploymentResult = await DeployAsync(deploymentParameters); + + await AssertFailsToStart(deploymentResult); + + Assert.Contains(TestSink.Writes, context => context.Message.Contains("Is Console redirection: True")); + } + + private async Task AssertFailsToStart(IISDeploymentResult deploymentResult) + { + var response = await deploymentResult.HttpClient.GetAsync("/HelloWorld"); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + StopServer(); + } + + [ConditionalFact] + public async Task Gets500_30_ErrorPage() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} EarlyReturn"); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("/HelloWorld"); + Assert.False(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + var responseText = await response.Content.ReadAsStringAsync(); + Assert.Contains("500.30 - ANCM In-Process Start Failure", responseText); + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Inprocess/StartupTests.cs b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/StartupTests.cs new file mode 100644 index 0000000000..3bcc134cb2 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/StartupTests.cs @@ -0,0 +1,479 @@ +// 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.IO; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.AspNetCore.Testing.xunit; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class StartupTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public StartupTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + private readonly string _dotnetLocation = DotNetCommands.GetDotNetExecutable(RuntimeArchitecture.x64); + + [ConditionalFact] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + public async Task ExpandEnvironmentVariableInWebConfig() + { + // Point to dotnet installed in user profile. + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + deploymentParameters.EnvironmentVariables["DotnetPath"] = _dotnetLocation; + deploymentParameters.WebConfigActionList.Add(WebConfigHelpers.AddOrModifyAspNetCoreSection("processPath", "%DotnetPath%")); + await StartAsync(deploymentParameters); + } + + [ConditionalTheory] + [InlineData("bogus", "", @"Executable was not found at '.*?\\bogus.exe")] + [InlineData("c:\\random files\\dotnet.exe", "something.dll", @"Could not find dotnet.exe at '.*?\\dotnet.exe'")] + [InlineData(".\\dotnet.exe", "something.dll", @"Could not find dotnet.exe at '.*?\\.\\dotnet.exe'")] + [InlineData("dotnet.exe", "", @"Application arguments are empty.")] + [InlineData("dotnet.zip", "", @"Process path 'dotnet.zip' doesn't have '.exe' extension.")] + public async Task InvalidProcessPath_ExpectServerError(string path, string arguments, string subError) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + deploymentParameters.WebConfigActionList.Add(WebConfigHelpers.AddOrModifyAspNetCoreSection("processPath", path)); + deploymentParameters.WebConfigActionList.Add(WebConfigHelpers.AddOrModifyAspNetCoreSection("arguments", arguments)); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("HelloWorld"); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvent(deploymentResult, $@"Application '{Regex.Escape(deploymentResult.ContentRoot)}\\' wasn't able to start. {subError}"); + + Assert.Contains("HTTP Error 500.0 - ANCM In-Process Handler Load Failure", await response.Content.ReadAsStringAsync()); + } + + [ConditionalFact] + public async Task StartsWithDotnetLocationWithoutExe() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + + var dotnetLocationWithoutExtension = _dotnetLocation.Substring(0, _dotnetLocation.LastIndexOf(".")); + deploymentParameters.WebConfigActionList.Add(WebConfigHelpers.AddOrModifyAspNetCoreSection("processPath", dotnetLocationWithoutExtension)); + + await StartAsync(deploymentParameters); + } + + [ConditionalFact] + public async Task StartsWithDotnetLocationUppercase() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + + var dotnetLocationWithoutExtension = _dotnetLocation.Substring(0, _dotnetLocation.LastIndexOf(".")).ToUpperInvariant(); + deploymentParameters.WebConfigActionList.Add(WebConfigHelpers.AddOrModifyAspNetCoreSection("processPath", dotnetLocationWithoutExtension)); + + await StartAsync(deploymentParameters); + } + + [ConditionalTheory] + [InlineData("dotnet")] + [InlineData("dotnet.EXE")] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + public async Task StartsWithDotnetOnThePath(string path) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + + deploymentParameters.EnvironmentVariables["PATH"] = Path.GetDirectoryName(_dotnetLocation); + deploymentParameters.WebConfigActionList.Add(WebConfigHelpers.AddOrModifyAspNetCoreSection("processPath", path)); + + var deploymentResult = await DeployAsync(deploymentParameters); + await deploymentResult.AssertStarts(); + + StopServer(); + // Verify that in this scenario where.exe was invoked only once by shim and request handler uses cached value + Assert.Equal(1, TestSink.Writes.Count(w => w.Message.Contains("Invoking where.exe to find dotnet.exe"))); + } + + public static TestMatrix TestVariants + => TestMatrix.ForServers(DeployerSelector.ServerType) + .WithTfms(Tfm.NetCoreApp22) + .WithAllApplicationTypes() + .WithAncmV2InProcess(); + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task HelloWorld(TestVariant variant) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + await StartAsync(deploymentParameters); + } + + [ConditionalFact] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + public async Task StartsWithPortableAndBootstraperExe() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + // We need the right dotnet on the path in IIS + deploymentParameters.EnvironmentVariables["PATH"] = Path.GetDirectoryName(DotNetCommands.GetDotNetExecutable(deploymentParameters.RuntimeArchitecture)); + + // rest publisher as it doesn't support additional parameters + deploymentParameters.ApplicationPublisher = null; + // ReferenceTestTasks is workaround for https://github.com/dotnet/sdk/issues/2482 + deploymentParameters.AdditionalPublishParameters = "-p:RuntimeIdentifier=win7-x64 -p:UseAppHost=true -p:SelfContained=false -p:ReferenceTestTasks=false"; + var deploymentResult = await DeployAsync(deploymentParameters); + + Assert.True(File.Exists(Path.Combine(deploymentResult.ContentRoot, "InProcessWebSite.exe"))); + Assert.False(File.Exists(Path.Combine(deploymentResult.ContentRoot, "hostfxr.dll"))); + Assert.Contains("InProcessWebSite.exe", Helpers.ReadAllTextFromFile(Path.Combine(deploymentResult.ContentRoot, "web.config"), Logger)); + + await deploymentResult.AssertStarts(); + } + + [ConditionalFact] + public async Task DetectsOverriddenServer() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} OverriddenServer"); + + var deploymentResult = await DeployAsync(deploymentParameters); + var response = await deploymentResult.HttpClient.GetAsync("/"); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvents(deploymentResult, + EventLogHelpers.InProcessFailedToStart(deploymentResult, "CLR worker thread exited prematurely"), + EventLogHelpers.InProcessThreadException(deploymentResult, ".*?Application is running inside IIS process but is not configured to use IIS server")); + } + + [ConditionalFact] + public async Task LogsStartupExceptionExitError() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} Throw"); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("/"); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvents(deploymentResult, + EventLogHelpers.InProcessFailedToStart(deploymentResult, "CLR worker thread exited prematurely"), + EventLogHelpers.InProcessThreadException(deploymentResult, ", exception code = '0xe0434352'")); + } + + [ConditionalFact] + public async Task LogsUnexpectedThreadExitError() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} EarlyReturn"); + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("/HelloWorld"); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvents(deploymentResult, + EventLogHelpers.InProcessFailedToStart(deploymentResult, "CLR worker thread exited prematurely"), + EventLogHelpers.InProcessThreadExit(deploymentResult, "12")); + } + + [ConditionalFact] + public async Task RemoveHostfxrFromApp_InProcessHostfxrInvalid() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.ApplicationType = ApplicationType.Standalone; + var deploymentResult = await DeployAsync(deploymentParameters); + + File.Copy( + Path.Combine(deploymentResult.ContentRoot, "aspnetcorev2_inprocess.dll"), + Path.Combine(deploymentResult.ContentRoot, "hostfxr.dll"), + true); + await AssertSiteFailsToStartWithInProcessStaticContent(deploymentResult); + + EventLogHelpers.VerifyEventLogEvent(deploymentResult, EventLogHelpers.InProcessHostfxrInvalid(deploymentResult), Logger); + } + + [ConditionalFact] + public async Task TargedDifferenceSharedFramework_FailedToFindNativeDependencies() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + var deploymentResult = await DeployAsync(deploymentParameters); + + Helpers.ModifyFrameworkVersionInRuntimeConfig(deploymentResult); + await AssertSiteFailsToStartWithInProcessStaticContent(deploymentResult); + + EventLogHelpers.VerifyEventLogEvent(deploymentResult, EventLogHelpers.InProcessFailedToFindNativeDependencies(deploymentResult), Logger); + } + + [ConditionalFact] + public async Task RemoveInProcessReference_FailedToFindRequestHandler() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.ApplicationType = ApplicationType.Standalone; + var deploymentResult = await DeployAsync(deploymentParameters); + + File.Delete(Path.Combine(deploymentResult.ContentRoot, "aspnetcorev2_inprocess.dll")); + + await AssertSiteFailsToStartWithInProcessStaticContent(deploymentResult); + + if (DeployerSelector.IsForwardsCompatibilityTest) + { + EventLogHelpers.VerifyEventLogEvent(deploymentResult, EventLogHelpers.InProcessFailedToFindNativeDependencies(deploymentResult), Logger); + } + else + { + EventLogHelpers.VerifyEventLogEvent(deploymentResult, EventLogHelpers.InProcessFailedToFindRequestHandler(deploymentResult), Logger); + } + } + + [ConditionalFact] + public async Task StartupTimeoutIsApplied() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} Hang"); + deploymentParameters.WebConfigActionList.Add( + WebConfigHelpers.AddOrModifyAspNetCoreSection("startupTimeLimit", "1")); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("/"); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvents(deploymentResult, + EventLogHelpers.InProcessFailedToStart(deploymentResult, "Managed server didn't initialize after 1000 ms.") + ); + } + + [ConditionalFact] + public async Task ShutdownTimeoutIsApplied() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} HangOnStop"); + deploymentParameters.WebConfigActionList.Add( + WebConfigHelpers.AddOrModifyAspNetCoreSection("shutdownTimeLimit", "1")); + + var deploymentResult = await DeployAsync(deploymentParameters); + + Assert.Equal("Hello World", await deploymentResult.HttpClient.GetStringAsync("/HelloWorld")); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvents(deploymentResult, + EventLogHelpers.InProcessStarted(deploymentResult), + EventLogHelpers.InProcessFailedToStop(deploymentResult, "")); + } + + [ConditionalFact] + public async Task CheckInvalidHostingModelParameter() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + deploymentParameters.WebConfigActionList.Add(WebConfigHelpers.AddOrModifyAspNetCoreSection("hostingModel", "bogus")); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("HelloWorld"); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvents(deploymentResult, + EventLogHelpers.ConfigurationLoadError(deploymentResult, "Unknown hosting model 'bogus'. Please specify either hostingModel=\"inprocess\" or hostingModel=\"outofprocess\" in the web.config file.") + ); + } + + private static Dictionary)> InvalidConfigTransformations = InitInvalidConfigTransformations(); + public static IEnumerable InvalidConfigTransformationsScenarios => InvalidConfigTransformations.ToTheoryData(); + + [ConditionalTheory] + [MemberData(nameof(InvalidConfigTransformationsScenarios))] + public async Task ReportsWebConfigAuthoringErrors(string scenario) + { + var (expectedError, action) = InvalidConfigTransformations[scenario]; + var iisDeploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + iisDeploymentParameters.WebConfigActionList.Add((element, _) => action(element)); + var deploymentResult = await DeployAsync(iisDeploymentParameters); + var result = await deploymentResult.HttpClient.GetAsync("/HelloWorld"); + Assert.Equal(HttpStatusCode.InternalServerError, result.StatusCode); + + // Config load errors might not allow us to initialize log file + deploymentResult.AllowNoLogs(); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvents(deploymentResult, + EventLogHelpers.ConfigurationLoadError(deploymentResult, expectedError) + ); + } + + public static Dictionary)> InitInvalidConfigTransformations() + { + var dictionary = new Dictionary)>(); + dictionary.Add("Empty process path", + ( + "Attribute 'processPath' is required.", + element => element.Descendants("aspNetCore").Single().SetAttributeValue("processPath", "") + )); + dictionary.Add("Unknown hostingModel", + ( + "Unknown hosting model 'asdf'.", + element => element.Descendants("aspNetCore").Single().SetAttributeValue("hostingModel", "asdf") + )); + dictionary.Add("environmentVariables with add", + ( + "Unable to get required configuration section 'system.webServer/aspNetCore'. Possible reason is web.config authoring error.", + element => element.Descendants("aspNetCore").Single().GetOrAdd("environmentVariables").GetOrAdd("add") + )); + return dictionary; + } + + private static Dictionary> PortableConfigTransformations = InitPortableWebConfigTransformations(); + public static IEnumerable PortableConfigTransformationsScenarios => PortableConfigTransformations.ToTheoryData(); + + [ConditionalTheory] + [MemberData(nameof(PortableConfigTransformationsScenarios))] + public async Task StartsWithWebConfigVariationsPortable(string scenario) + { + var action = PortableConfigTransformations[scenario]; + var iisDeploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + var expectedArguments = action(iisDeploymentParameters); + var result = await DeployAsync(iisDeploymentParameters); + Assert.Equal(expectedArguments, await result.HttpClient.GetStringAsync("/CommandLineArgs")); + } + + public static Dictionary> InitPortableWebConfigTransformations() + { + var dictionary = new Dictionary>(); + var pathWithSpace = "\u03c0 \u2260 3\u00b714"; + + dictionary.Add("App in bin subdirectory full path to dll using exec and quotes", + parameters => { + MoveApplication(parameters, "bin"); + parameters.TransformArguments((arguments, root) => "exec " + Path.Combine(root, "bin", arguments)); + return ""; + }); + + dictionary.Add("App in subdirectory with space", + parameters => { + MoveApplication(parameters, pathWithSpace); + parameters.TransformArguments((arguments, root) => Path.Combine(pathWithSpace, arguments)); + return ""; + }); + + dictionary.Add("App in subdirectory with space and full path to dll", + parameters => { + MoveApplication(parameters, pathWithSpace); + parameters.TransformArguments((arguments, root) => Path.Combine(root, pathWithSpace, arguments)); + return ""; + }); + + dictionary.Add("App in bin subdirectory with space full path to dll using exec and quotes", + parameters => { + MoveApplication(parameters, pathWithSpace); + parameters.TransformArguments((arguments, root) => "exec \"" + Path.Combine(root, pathWithSpace, arguments) + "\" extra arguments"); + return "extra|arguments"; + }); + + dictionary.Add("App in bin subdirectory and quoted argument", + parameters => { + MoveApplication(parameters, "bin"); + parameters.TransformArguments((arguments, root) => Path.Combine("bin", arguments) + " \"extra argument\""); + return "extra argument"; + }); + + dictionary.Add("App in bin subdirectory full path to dll", + parameters => { + MoveApplication(parameters, "bin"); + parameters.TransformArguments((arguments, root) => Path.Combine(root, "bin", arguments) + " extra arguments"); + return "extra|arguments"; + }); + return dictionary; + } + + + private static Dictionary> StandaloneConfigTransformations = InitStandaloneConfigTransformations(); + public static IEnumerable StandaloneConfigTransformationsScenarios => StandaloneConfigTransformations.ToTheoryData(); + + [ConditionalTheory] + [MemberData(nameof(StandaloneConfigTransformationsScenarios))] + public async Task StartsWithWebConfigVariationsStandalone(string scenario) + { + var action = StandaloneConfigTransformations[scenario]; + var iisDeploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + iisDeploymentParameters.ApplicationType = ApplicationType.Standalone; + var expectedArguments = action(iisDeploymentParameters); + var result = await DeployAsync(iisDeploymentParameters); + Assert.Equal(expectedArguments, await result.HttpClient.GetStringAsync("/CommandLineArgs")); + } + + public static Dictionary> InitStandaloneConfigTransformations() + { + var dictionary = new Dictionary>(); + var pathWithSpace = "\u03c0 \u2260 3\u00b714"; + + dictionary.Add("App in subdirectory", + parameters => { + MoveApplication(parameters, pathWithSpace); + parameters.TransformPath((path, root) => Path.Combine(pathWithSpace, path)); + parameters.TransformArguments((arguments, root) => "\"additional argument\""); + return "additional argument"; + }); + + dictionary.Add("App in bin subdirectory full path", + parameters => { + MoveApplication(parameters, pathWithSpace); + parameters.TransformPath((path, root) => Path.Combine(root, pathWithSpace, path)); + parameters.TransformArguments((arguments, root) => "additional arguments"); + return "additional|arguments"; + }); + + return dictionary; + } + + private static void MoveApplication( + IISDeploymentParameters parameters, + string subdirectory) + { + parameters.WebConfigActionList.Add((config, contentRoot) => + { + var source = new DirectoryInfo(contentRoot); + var subDirectoryPath = source.CreateSubdirectory(subdirectory); + + // Copy everything into a subfolder + Helpers.CopyFiles(source, subDirectoryPath, null); + // Cleanup files + foreach (var fileSystemInfo in source.GetFiles()) + { + fileSystemInfo.Delete(); + } + }); + } + + private async Task AssertSiteFailsToStartWithInProcessStaticContent(IISDeploymentResult deploymentResult) + { + var response = await deploymentResult.HttpClient.GetAsync("/HelloWorld"); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.Contains("HTTP Error 500.0 - ANCM In-Process Handler Load Failure", await response.Content.ReadAsStringAsync()); + StopServer(); + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Inprocess/SynchronousReadAndWriteTests.cs b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/SynchronousReadAndWriteTests.cs new file mode 100644 index 0000000000..03aa0e16e6 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Inprocess/SynchronousReadAndWriteTests.cs @@ -0,0 +1,197 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class SynchronousReadAndWriteTests: FixtureLoggedTest + { + private readonly IISTestSiteFixture _fixture; + + public SynchronousReadAndWriteTests(IISTestSiteFixture fixture): base(fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task ReadAndWriteSynchronously() + { + for (int i = 0; i < 100; i++) + { + var content = new StringContent(new string('a', 100000)); + var response = await _fixture.Client.PostAsync("ReadAndWriteSynchronously", content); + var responseText = await response.Content.ReadAsStringAsync(); + + Assert.Equal(expected: 110000, actual: responseText.Length); + } + } + + [ConditionalFact] + public async Task ReadAndWriteEcho() + { + var body = new string('a', 100000); + var content = new StringContent(body); + var response = await _fixture.Client.PostAsync("ReadAndWriteEcho", content); + var responseText = await response.Content.ReadAsStringAsync(); + + Assert.Equal(body, responseText); + } + + [ConditionalFact] + public async Task ReadAndWriteCopyToAsync() + { + var body = new string('a', 100000); + var content = new StringContent(body); + var response = await _fixture.Client.PostAsync("ReadAndWriteCopyToAsync", content); + var responseText = await response.Content.ReadAsStringAsync(); + + Assert.Equal(body, responseText); + } + + [ConditionalFact] + public async Task ReadAndWriteEchoTwice() + { + var requestBody = new string('a', 10000); + var content = new StringContent(requestBody); + var response = await _fixture.Client.PostAsync("ReadAndWriteEchoTwice", content); + var responseText = await response.Content.ReadAsStringAsync(); + + Assert.Equal(requestBody.Length * 2, responseText.Length); + } + + [ConditionalFact] + public async Task ReadSetHeaderWrite() + { + var body = "Body text"; + var content = new StringContent(body); + var response = await _fixture.Client.PostAsync("SetHeaderFromBody", content); + var responseText = await response.Content.ReadAsStringAsync(); + + Assert.Equal(body, response.Headers.GetValues("BodyAsString").Single()); + Assert.Equal(body, responseText); + } + + [ConditionalFact] + public async Task ReadAndWriteSlowConnection() + { + using (var connection = _fixture.CreateTestConnection()) + { + var testString = "hello world"; + var request = $"POST /ReadAndWriteSlowConnection HTTP/1.0\r\n" + + $"Content-Length: {testString.Length}\r\n" + + "Host: " + "localhost\r\n" + + "\r\n" + testString; + + foreach (var c in request) + { + await connection.Send(c.ToString()); + await Task.Delay(10); + } + + await connection.Receive( + "HTTP/1.1 200 OK", + ""); + await connection.ReceiveHeaders(); + + for (int i = 0; i < 100; i++) + { + foreach (var c in testString) + { + await connection.Receive(c.ToString()); + } + await Task.Delay(10); + } + await connection.WaitForConnectionClose(); + } + } + + [ConditionalFact] + public async Task ReadAndWriteInterleaved() + { + 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 /ReadAndWriteEchoLines HTTP/1.0", + $"Content-Length: {requestLength}", + "Host: localhost", + "", + ""); + + await connection.Receive( + "HTTP/1.1 200 OK", + ""); + await connection.ReceiveHeaders(); + + foreach (var message in messages) + { + await connection.Send(message); + await connection.Receive(message); + } + + await connection.Send("\r\n"); + await connection.WaitForConnectionClose(); + } + } + + [ConditionalFact] + public async Task ConsumePartialBody() + { + using (var connection = _fixture.CreateTestConnection()) + { + var message = "Hello"; + await connection.Send( + "POST /ReadPartialBody HTTP/1.1", + $"Content-Length: {100}", + "Host: localhost", + "Connection: close", + "", + ""); + + await connection.Send(message); + + await connection.Receive( + "HTTP/1.1 200 OK", + ""); + + // This test can return both content length or chunked response + // depending on if appfunc managed to complete before write was + // issued + var headers = await connection.ReceiveHeaders(); + if (headers.Contains("Content-Length: 5")) + { + await connection.Receive("Hello"); + } + else + { + await connection.Receive( + "5", + message, + ""); + await connection.Receive( + "0", + "", + ""); + } + + await connection.WaitForConnectionClose(); + } + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/LogFileTests.cs b/src/IISIntegration/test/Common.FunctionalTests/LogFileTests.cs new file mode 100644 index 0000000000..1e15cde5c2 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/LogFileTests.cs @@ -0,0 +1,242 @@ +// 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.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; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class LoggingTests : LogFileTestBase + { + private readonly PublishedSitesFixture _fixture; + + public LoggingTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + public static TestMatrix TestVariants + => TestMatrix.ForServers(DeployerSelector.ServerType) + .WithTfms(Tfm.NetCoreApp22) + .WithAllApplicationTypes() + .WithAncmVersions(AncmVersion.AspNetCoreModuleV2) + .WithAllHostingModels(); + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task CheckStdoutLoggingToFile(TestVariant variant) + { + await CheckStdoutToFile(variant, "ConsoleWrite"); + } + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task CheckStdoutErrLoggingToFile(TestVariant variant) + { + await CheckStdoutToFile(variant, "ConsoleErrorWrite"); + } + + private async Task CheckStdoutToFile(TestVariant variant, string path) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + deploymentParameters.EnableLogging(_logFolderPath); + + var deploymentResult = await DeployAsync(deploymentParameters); + + await Helpers.AssertStarts(deploymentResult, path); + + StopServer(); + + var contents = Helpers.ReadAllTextFromFile(Helpers.GetExpectedLogName(deploymentResult, _logFolderPath), Logger); + + Assert.Contains("TEST MESSAGE", contents); + } + + // Move to separate file + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task InvalidFilePathForLogs_ServerStillRuns(TestVariant variant) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + + deploymentParameters.WebConfigActionList.Add( + WebConfigHelpers.AddOrModifyAspNetCoreSection("stdoutLogEnabled", "true")); + deploymentParameters.WebConfigActionList.Add( + WebConfigHelpers.AddOrModifyAspNetCoreSection("stdoutLogFile", Path.Combine("Q:", "std"))); + + var deploymentResult = await DeployAsync(deploymentParameters); + + await Helpers.AssertStarts(deploymentResult, "HelloWorld"); + + StopServer(); + if (variant.HostingModel == HostingModel.InProcess) + { + EventLogHelpers.VerifyEventLogEvent(deploymentResult, "Could not start stdout redirection in (.*)aspnetcorev2.dll. Exception message: HRESULT 0x80070003"); + EventLogHelpers.VerifyEventLogEvent(deploymentResult, "Could not stop stdout redirection in (.*)aspnetcorev2.dll. Exception message: HRESULT 0x80070002"); + } + } + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task StartupMessagesAreLoggedIntoDebugLogFile(TestVariant variant) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + deploymentParameters.HandlerSettings["debugLevel"] = "file"; + deploymentParameters.HandlerSettings["debugFile"] = "debug.txt"; + + var deploymentResult = await DeployAsync(deploymentParameters); + + await deploymentResult.HttpClient.GetAsync("/"); + + AssertLogs(Path.Combine(deploymentResult.ContentRoot, "debug.txt")); + } + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task StartupMessagesAreLoggedIntoDefaultDebugLogFile(TestVariant variant) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + deploymentParameters.HandlerSettings["debugLevel"] = "file"; + + var deploymentResult = await DeployAsync(deploymentParameters); + + await deploymentResult.HttpClient.GetAsync("/"); + + AssertLogs(Path.Combine(deploymentResult.ContentRoot, "aspnetcore-debug.log")); + } + + [ConditionalTheory] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + [MemberData(nameof(TestVariants))] + public async Task StartupMessagesAreLoggedIntoDefaultDebugLogFileWhenEnabledWithEnvVar(TestVariant variant) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + deploymentParameters.EnvironmentVariables["ASPNETCORE_MODULE_DEBUG"] = "file"; + // Add empty debugFile handler setting to prevent IIS deployer from overriding debug settings + deploymentParameters.HandlerSettings["debugFile"] = ""; + var deploymentResult = await DeployAsync(deploymentParameters); + + await deploymentResult.HttpClient.GetAsync("/"); + + AssertLogs(Path.Combine(deploymentResult.ContentRoot, "aspnetcore-debug.log")); + } + + [ConditionalTheory] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + [MemberData(nameof(TestVariants))] + public async Task StartupMessagesLogFileSwitchedWhenLogFilePresentInWebConfig(TestVariant variant) + { + var firstTempFile = Path.GetTempFileName(); + var secondTempFile = Path.GetTempFileName(); + + try + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + deploymentParameters.EnvironmentVariables["ASPNETCORE_MODULE_DEBUG_FILE"] = firstTempFile; + deploymentParameters.AddDebugLogToWebConfig(secondTempFile); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("/"); + + StopServer(); + var logContents = Helpers.ReadAllTextFromFile(firstTempFile, Logger); + + Assert.Contains("Switching debug log files to", logContents); + + AssertLogs(secondTempFile); + } + finally + { + File.Delete(firstTempFile); + File.Delete(secondTempFile); + } + } + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + + public async Task DebugLogsAreWrittenToEventLog(TestVariant variant) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + deploymentParameters.HandlerSettings["debugLevel"] = "file,eventlog"; + var deploymentResult = await StartAsync(deploymentParameters); + StopServer(); + EventLogHelpers.VerifyEventLogEvent(deploymentResult, @"\[aspnetcorev2.dll\] Initializing logs for .*?Description: IIS ASP.NET Core Module V2"); + } + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task CheckUTF8File(TestVariant variant) + { + var path = "CheckConsoleFunctions"; + + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, variant.HostingModel, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} {path}"); // For standalone this will need to remove space + + var logFolderPath = _logFolderPath + "\\彡⾔"; + deploymentParameters.EnableLogging(logFolderPath); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync(path); + + Assert.False(response.IsSuccessStatusCode); + + StopServer(); + + var contents = Helpers.ReadAllTextFromFile(Helpers.GetExpectedLogName(deploymentResult, logFolderPath), Logger); + Assert.Contains("彡⾔", contents); + + if (variant.HostingModel == HostingModel.InProcess) + { + EventLogHelpers.VerifyEventLogEvent(deploymentResult, EventLogHelpers.InProcessThreadExitStdOut(deploymentResult, "12", "(.*)彡⾔(.*)")); + } + else + { + EventLogHelpers.VerifyEventLogEvent(deploymentResult, EventLogHelpers.OutOfProcessFailedToStart(deploymentResult)); + } + } + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task OnlyOneFileCreatedWithProcessStartTime(TestVariant variant) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + + deploymentParameters.EnableLogging(_logFolderPath); + + var deploymentResult = await DeployAsync(deploymentParameters); + await Helpers.AssertStarts(deploymentResult, "ConsoleWrite"); + + StopServer(); + + Assert.Single(Directory.GetFiles(_logFolderPath), Helpers.GetExpectedLogName(deploymentResult, _logFolderPath)); + } + + private static string ReadLogs(string logPath) + { + using (var stream = File.Open(logPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + using (var streamReader = new StreamReader(stream)) + { + return streamReader.ReadToEnd(); + } + } + + private static void AssertLogs(string logPath) + { + var logContents = ReadLogs(logPath); + Assert.Contains("[aspnetcorev2.dll]", logContents); + Assert.True(logContents.Contains("[aspnetcorev2_inprocess.dll]") || logContents.Contains("[aspnetcorev2_outofprocess.dll]")); + Assert.Contains("Description: IIS ASP.NET Core Module V2. Commit:", logContents); + Assert.Contains("Description: IIS ASP.NET Core Module V2 Request Handler. Commit:", logContents); + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/MultiApplicationTests.cs b/src/IISIntegration/test/Common.FunctionalTests/MultiApplicationTests.cs new file mode 100644 index 0000000000..3723369c89 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/MultiApplicationTests.cs @@ -0,0 +1,144 @@ +// 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.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class MultiApplicationTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + private PublishedApplication _publishedApplication; + private PublishedApplication _rootApplication; + + public MultiApplicationTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task RunsTwoOutOfProcessApps() + { + var parameters = _fixture.GetBaseDeploymentParameters(HostingModel.OutOfProcess, publish: true); + parameters.ServerConfigActionList.Add(DuplicateApplication); + var result = await DeployAsync(parameters); + var id1 = await result.HttpClient.GetStringAsync("/app1/ProcessId"); + var id2 = await result.HttpClient.GetStringAsync("/app2/ProcessId"); + Assert.NotEqual(id2, id1); + } + + [ConditionalFact] + public async Task FailsAndLogsWhenRunningTwoInProcessApps() + { + var parameters = _fixture.GetBaseDeploymentParameters(HostingModel.InProcess, publish: true); + parameters.ServerConfigActionList.Add(DuplicateApplication); + + var result = await DeployAsync(parameters); + var result1 = await result.HttpClient.GetAsync("/app1/HelloWorld"); + var result2 = await result.HttpClient.GetAsync("/app2/HelloWorld"); + Assert.Equal(200, (int)result1.StatusCode); + Assert.Equal(500, (int)result2.StatusCode); + StopServer(); + EventLogHelpers.VerifyEventLogEvent(result, "Only one inprocess application is allowed per IIS application pool"); + } + + [ConditionalTheory] + [InlineData(HostingModel.OutOfProcess)] + [InlineData(HostingModel.InProcess)] + public async Task FailsAndLogsEventLogForMixedHostingModel(HostingModel firstApp) + { + var parameters = _fixture.GetBaseDeploymentParameters(firstApp, publish: true); + parameters.ServerConfigActionList.Add(DuplicateApplication); + var result = await DeployAsync(parameters); + + // Modify hosting model of other app to be the opposite + var otherApp = firstApp == HostingModel.InProcess ? HostingModel.OutOfProcess : HostingModel.InProcess; + SetHostingModel(_publishedApplication.Path, otherApp); + + var result1 = await result.HttpClient.GetAsync("/app1/HelloWorld"); + var result2 = await result.HttpClient.GetAsync("/app2/HelloWorld"); + Assert.Equal(200, (int)result1.StatusCode); + Assert.Equal(500, (int)result2.StatusCode); + StopServer(); + EventLogHelpers.VerifyEventLogEvent(result, "Mixed hosting model is not supported."); + } + + private void SetHostingModel(string directory, HostingModel model) + { + var webConfigLocation = GetWebConfigLocation(directory); + XDocument webConfig = XDocument.Load(webConfigLocation); + webConfig.Root + .Descendants("system.webServer") + .Single() + .GetOrAdd("aspNetCore") + .SetAttributeValue("hostingModel", model.ToString()); + webConfig.Save(webConfigLocation); + } + + private void DuplicateApplication(XElement config, string contentRoot) + { + var siteElement = config + .RequiredElement("system.applicationHost") + .RequiredElement("sites") + .RequiredElement("site"); + + var application = siteElement + .RequiredElement("application"); + + application.SetAttributeValue("path", "/app1"); + + var source = new DirectoryInfo(contentRoot); + + var destination = new DirectoryInfo(contentRoot + "anotherApp"); + destination.Create(); + Helpers.CopyFiles(source, destination, Logger); + + _publishedApplication = new PublishedApplication(destination.FullName, Logger); + + var newApplication = new XElement(application); + newApplication.SetAttributeValue("path", "/app2"); + newApplication.RequiredElement("virtualDirectory") + .SetAttributeValue("physicalPath", destination.FullName); + + siteElement.Add(newApplication); + + // IIS Express requires root application to exist + var rootApplicationDirectory = new DirectoryInfo(contentRoot + "rootApp"); + rootApplicationDirectory.Create(); + + _rootApplication = new PublishedApplication(rootApplicationDirectory.FullName, Logger); + File.WriteAllText(GetWebConfigLocation(rootApplicationDirectory.FullName), ""); + + var rootApplication = new XElement(application); + rootApplication.SetAttributeValue("path", "/"); + rootApplication.RequiredElement("virtualDirectory") + .SetAttributeValue("physicalPath", rootApplicationDirectory.FullName); + + siteElement.Add(rootApplication); + } + + private static string GetWebConfigLocation(string siteRoot) + { + return Path.Combine(siteRoot, "web.config"); + } + + public override void Dispose() + { + base.Dispose(); + _rootApplication.Dispose(); + _publishedApplication.Dispose(); + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/OutOfProcess/AspNetCorePortTests.cs b/src/IISIntegration/test/Common.FunctionalTests/OutOfProcess/AspNetCorePortTests.cs new file mode 100644 index 0000000000..3b86662717 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/OutOfProcess/AspNetCorePortTests.cs @@ -0,0 +1,125 @@ +// 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; +using System.Net.Sockets; +using System.Threading.Tasks; +using Xunit; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class AspNetCorePortTests : IISFunctionalTestBase + { + // Port range allowed by ANCM config + private const int _minPort = 1025; + private const int _maxPort = 48000; + + private static readonly Random _random = new Random(); + + private readonly PublishedSitesFixture _fixture; + + public AspNetCorePortTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + public static TestMatrix TestVariants + => TestMatrix.ForServers(DeployerSelector.ServerType) + .WithTfms(Tfm.NetCoreApp22) + .WithApplicationTypes(ApplicationType.Portable) + .WithAllAncmVersions(); + + public static IEnumerable InvalidTestVariants + => from v in TestVariants.Select(v => v.Single()) + from s in new string[] { (_minPort - 1).ToString(), (_maxPort + 1).ToString(), "noninteger" } + select new object[] { v, s }; + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task EnvVarInWebConfig_Valid(TestVariant variant) + { + // Must publish to set env vars in web.config + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + var port = GetUnusedRandomPort(); + deploymentParameters.WebConfigBasedEnvironmentVariables["ASPNETCORE_PORT"] = port.ToString(); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var responseText = await deploymentResult.HttpClient.GetStringAsync("/ServerAddresses"); + + Assert.Equal(port, new Uri(responseText).Port); + } + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task EnvVarInWebConfig_Empty(TestVariant variant) + { + // Must publish to set env vars in web.config + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + deploymentParameters.WebConfigBasedEnvironmentVariables["ASPNETCORE_PORT"] = string.Empty; + + var deploymentResult = await DeployAsync(deploymentParameters); + + var responseText = await deploymentResult.HttpClient.GetStringAsync("/ServerAddresses"); + + // If env var is empty, ANCM should assign a random port (same as no env var) + Assert.InRange(new Uri(responseText).Port, _minPort, _maxPort); + } + + [ConditionalTheory] + [MemberData(nameof(InvalidTestVariants))] + public async Task EnvVarInWebConfig_Invalid(TestVariant variant, string port) + { + // Must publish to set env vars in web.config + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant, publish: true); + deploymentParameters.WebConfigBasedEnvironmentVariables["ASPNETCORE_PORT"] = port; + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("/ServerAddresses"); + + Assert.Equal(HttpStatusCode.BadGateway, response.StatusCode); + } + + private static int GetUnusedRandomPort() + { + // Large number of retries to prevent test failures due to port collisions, but not infinite + // to prevent infinite loop in case Bind() fails repeatedly for some other reason. + const int retries = 100; + + List exceptions = null; + + for (var i = 0; i < retries; i++) + { + var port = _random.Next(_minPort, _maxPort); + + using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + { + try + { + socket.Bind(new IPEndPoint(IPAddress.Loopback, port)); + return port; + } + catch (Exception e) + { + // Bind failed, most likely because port is in use. Save exception and retry. + if (exceptions == null) + { + exceptions = new List(retries); + } + exceptions.Add(e); + } + } + } + + throw new AggregateException($"Unable to find unused random port after {retries} retries.", exceptions); + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/OutOfProcess/GlobalVersionTests.cs b/src/IISIntegration/test/Common.FunctionalTests/OutOfProcess/GlobalVersionTests.cs new file mode 100644 index 0000000000..f9e6836b6f --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/OutOfProcess/GlobalVersionTests.cs @@ -0,0 +1,252 @@ +// 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 System.Xml.Linq; +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; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class GlobalVersionTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public GlobalVersionTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + private const string _handlerVersion20 = "2.0.0"; + private const string _helloWorldRequest = "HelloWorld"; + private const string _helloWorldResponse = "Hello World"; + + [ConditionalFact] + public async Task GlobalVersion_DefaultWorks() + { + var deploymentParameters = GetGlobalVersionBaseDeploymentParameters(); + + deploymentParameters.HandlerSettings["handlerVersion"] = _handlerVersion20; + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync(_helloWorldRequest); + var responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(_helloWorldResponse, responseText); + } + + [ConditionalFact] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + [RequiresNewShim] + public async Task GlobalVersion_EnvironmentVariableWorks() + { + var temporaryFile = Path.GetTempFileName(); + try + { + var deploymentParameters = GetGlobalVersionBaseDeploymentParameters(); + CopyShimToOutput(deploymentParameters); + deploymentParameters.PublishApplicationBeforeDeployment = true; + deploymentParameters.EnvironmentVariables["ASPNETCORE_MODULE_OUTOFPROCESS_HANDLER"] = temporaryFile; + + var deploymentResult = await DeployAsync(deploymentParameters); + var requestHandlerPath = Path.Combine(GetANCMRequestHandlerPath(deploymentResult, _handlerVersion20), "aspnetcorev2_outofprocess.dll"); + + File.Delete(temporaryFile); + File.Move(requestHandlerPath, temporaryFile); + + var response = await deploymentResult.HttpClient.GetAsync(_helloWorldRequest); + var responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(_helloWorldResponse, responseText); + StopServer(); + } + finally + { + File.Delete(temporaryFile); + } + } + + [ConditionalTheory] + [InlineData("2.1.0")] + [InlineData("2.1.0-preview")] + public async Task GlobalVersion_NewVersionNumber_Fails(string version) + { + var deploymentParameters = GetGlobalVersionBaseDeploymentParameters(); + deploymentParameters.HandlerSettings["handlerVersion"] = version; + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync(_helloWorldRequest); + Assert.False(response.IsSuccessStatusCode); + var responseString = await response.Content.ReadAsStringAsync(); + Assert.Contains("HTTP Error 500.0 - ANCM Out-Of-Process Handler Load Failure", responseString); + } + + [ConditionalTheory] + [InlineData("2.1.0")] + [InlineData("2.1.0-preview")] + public async Task GlobalVersion_NewVersionNumber(string version) + { + var deploymentParameters = GetGlobalVersionBaseDeploymentParameters(); + CopyShimToOutput(deploymentParameters); + deploymentParameters.HandlerSettings["handlerVersion"] = version; + + var deploymentResult = await DeployAsync(deploymentParameters); + + var originalANCMPath = GetANCMRequestHandlerPath(deploymentResult, _handlerVersion20); + var newANCMPath = GetANCMRequestHandlerPath(deploymentResult, version); + Directory.Move(originalANCMPath, newANCMPath); + + var response = await deploymentResult.HttpClient.GetAsync(_helloWorldRequest); + var responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(_helloWorldResponse, responseText); + AssertLoadedVersion(version); + } + + [ConditionalTheory] + [InlineData("2.1.0")] + [InlineData("2.1.0-preview")] + public async Task GlobalVersion_MultipleRequestHandlers_PicksHighestOne(string version) + { + var deploymentParameters = GetGlobalVersionBaseDeploymentParameters(); + CopyShimToOutput(deploymentParameters); + var deploymentResult = await DeployAsync(deploymentParameters); + + var originalANCMPath = GetANCMRequestHandlerPath(deploymentResult, _handlerVersion20); + + var newANCMPath = GetANCMRequestHandlerPath(deploymentResult, version); + + CopyDirectory(originalANCMPath, newANCMPath); + + deploymentResult.HttpClient.DefaultRequestHeaders.Add("ANCMRHPath", newANCMPath); + var response = await deploymentResult.HttpClient.GetAsync("CheckRequestHandlerVersion"); + var responseText = await response.Content.ReadAsStringAsync(); + + Assert.Equal(_helloWorldResponse, responseText); + AssertLoadedVersion(version); + } + + [ConditionalTheory] + [InlineData("2.1.0")] + [InlineData("2.1.0-preview")] + public async Task GlobalVersion_MultipleRequestHandlers_UpgradeWorks(string version) + { + var deploymentParameters = GetGlobalVersionBaseDeploymentParameters(); + CopyShimToOutput(deploymentParameters); + var deploymentResult = await DeployAsync(deploymentParameters); + + var originalANCMPath = GetANCMRequestHandlerPath(deploymentResult, _handlerVersion20); + + deploymentResult.HttpClient.DefaultRequestHeaders.Add("ANCMRHPath", originalANCMPath); + var response = await deploymentResult.HttpClient.GetAsync("CheckRequestHandlerVersion"); + var responseText = await response.Content.ReadAsStringAsync(); + + Assert.Equal(_helloWorldResponse, responseText); + + StopServer(); + + deploymentResult = await DeployAsync(deploymentParameters); + + originalANCMPath = GetANCMRequestHandlerPath(deploymentResult, _handlerVersion20); + + var newANCMPath = GetANCMRequestHandlerPath(deploymentResult, version); + + CopyDirectory(originalANCMPath, newANCMPath); + + deploymentResult.HttpClient.DefaultRequestHeaders.Add("ANCMRHPath", newANCMPath); + response = await deploymentResult.HttpClient.GetAsync("CheckRequestHandlerVersion"); + responseText = await response.Content.ReadAsStringAsync(); + + Assert.Equal(_helloWorldResponse, responseText); + AssertLoadedVersion(version); + } + + [ConditionalFact] + public async Task DoesNotCrashWhenNoVersionsAvailable() + { + var deploymentParameters = GetGlobalVersionBaseDeploymentParameters(); + CopyShimToOutput(deploymentParameters); + var deploymentResult = await DeployAsync(deploymentParameters); + + var originalANCMPath = GetANCMRequestHandlerPath(deploymentResult, _handlerVersion20); + Directory.Delete(originalANCMPath, true); + var response = await deploymentResult.HttpClient.GetAsync("HelloWorld"); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + } + + private IISDeploymentParameters GetGlobalVersionBaseDeploymentParameters() + { + return _fixture.GetBaseDeploymentParameters(HostingModel.OutOfProcess, publish: true); + } + + private void CopyDirectory(string from, string to) + { + var toInfo = new DirectoryInfo(to); + toInfo.Create(); + + foreach (var file in new DirectoryInfo(from).GetFiles()) + { + file.CopyTo(Path.Combine(toInfo.FullName, file.Name)); + } + } + + private string GetANCMRequestHandlerPath(IISDeploymentResult deploymentResult, string version) + { + return Path.Combine(deploymentResult.ContentRoot, + deploymentResult.DeploymentParameters.RuntimeArchitecture.ToString(), + version); + } + + private void AssertLoadedVersion(string version) + { + StopServer(); + Assert.Contains(TestSink.Writes, context => context.Message.Contains(version + @"\aspnetcorev2_outofprocess.dll")); + } + + private static void CopyShimToOutput(IISDeploymentParameters parameters) + { + parameters.AddServerConfigAction( + (config, contentRoot) => { + var moduleNodes = config.DescendantNodesAndSelf() + .OfType() + .Where(element => + element.Name == "add" && + element.Attribute("name")?.Value.StartsWith("AspNetCoreModule") == true && + element.Attribute("image") != null); + + var sourceDirectory = new DirectoryInfo(Path.GetDirectoryName(moduleNodes.First().Attribute("image").Value)); + var destinationDirectory = new DirectoryInfo(Path.Combine(contentRoot, sourceDirectory.Name)); + destinationDirectory.Create(); + foreach (var element in moduleNodes) + { + var imageAttribute = element.Attribute("image"); + imageAttribute.Value = imageAttribute.Value.Replace(sourceDirectory.FullName, destinationDirectory.FullName); + } + CopyFiles(sourceDirectory, destinationDirectory); + }); + } + + private static void CopyFiles(DirectoryInfo source, DirectoryInfo target) + { + foreach (DirectoryInfo directoryInfo in source.GetDirectories()) + { + CopyFiles(directoryInfo, target.CreateSubdirectory(directoryInfo.Name)); + } + + foreach (FileInfo fileInfo in source.GetFiles()) + { + var destFileName = Path.Combine(target.FullName, fileInfo.Name); + fileInfo.CopyTo(destFileName); + } + } + + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/OutOfProcess/HelloWorldTest.cs b/src/IISIntegration/test/Common.FunctionalTests/OutOfProcess/HelloWorldTest.cs new file mode 100644 index 0000000000..f6e36613d7 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/OutOfProcess/HelloWorldTest.cs @@ -0,0 +1,81 @@ +// 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.Diagnostics; +using System.Linq; +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; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class HelloWorldTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public HelloWorldTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + public static TestMatrix TestVariants + => TestMatrix.ForServers(DeployerSelector.ServerType) + .WithTfms(Tfm.NetCoreApp22, Tfm.Net461) + .WithAllApplicationTypes() + .WithAllAncmVersions(); + + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task HelloWorld(TestVariant variant) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant); + deploymentParameters.ServerConfigActionList.Add( + (element, _) => { + element + .RequiredElement("system.webServer") + .RequiredElement("security") + .RequiredElement("authentication") + .Element("windowsAuthentication") + ?.SetAttributeValue("enabled", "false"); + }); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("/HelloWorld"); + var responseText = await response.Content.ReadAsStringAsync(); + + Assert.Equal("Hello World", responseText); + + response = await deploymentResult.HttpClient.GetAsync("/Path/%3F%3F?query"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal("/??", responseText); + + response = await deploymentResult.HttpClient.GetAsync("/Query/%3FPath?query?"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal("?query?", responseText); + + response = await deploymentResult.HttpClient.GetAsync("/BodyLimit"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal("null", responseText); + + response = await deploymentResult.HttpClient.GetAsync("/Auth"); + responseText = await response.Content.ReadAsStringAsync(); + + Assert.Equal("null", responseText); + + Assert.Equal( + $"ContentRootPath {deploymentResult.ContentRoot}" + Environment.NewLine + + $"WebRootPath {deploymentResult.ContentRoot}\\wwwroot" + Environment.NewLine + + $"CurrentDirectory {deploymentResult.ContentRoot}", + await deploymentResult.HttpClient.GetStringAsync("/HostingEnvironment")); + + var expectedDll = variant.AncmVersion == AncmVersion.AspNetCoreModule ? "aspnetcore.dll" : "aspnetcorev2.dll"; + Assert.Contains(deploymentResult.HostProcess.Modules.OfType(), m=> m.FileName.Contains(expectedDll)); + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/PublishedSitesFixture.cs b/src/IISIntegration/test/Common.FunctionalTests/PublishedSitesFixture.cs new file mode 100644 index 0000000000..282ee26109 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/PublishedSitesFixture.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + /// + /// This type just maps collection names to available fixtures + /// + [CollectionDefinition(Name)] + public class PublishedSitesCollection : ICollectionFixture, ICollectionFixture + { + public const string Name = nameof(PublishedSitesCollection); + } + + public class PublishedSitesFixture : IDisposable + { + public CachingApplicationPublisher InProcessTestSite { get; } = new CachingApplicationPublisher(Helpers.GetInProcessTestSitesPath()); + public CachingApplicationPublisher OutOfProcessTestSite { get; } = new CachingApplicationPublisher(Helpers.GetOutOfProcessTestSitesPath()); + + public void Dispose() + { + InProcessTestSite.Dispose(); + OutOfProcessTestSite.Dispose(); + } + + public IISDeploymentParameters GetBaseDeploymentParameters(HostingModel hostingModel = HostingModel.InProcess, bool publish = false) + { + var publisher = hostingModel == HostingModel.InProcess ? InProcessTestSite : OutOfProcessTestSite; + return GetBaseDeploymentParameters(publisher, hostingModel, publish); + } + public IISDeploymentParameters GetBaseDeploymentParameters(TestVariant variant, bool publish = false) + { + var publisher = variant.HostingModel == HostingModel.InProcess ? InProcessTestSite : OutOfProcessTestSite; + return GetBaseDeploymentParameters(publisher, new DeploymentParameters(variant), publish); + } + + public IISDeploymentParameters GetBaseDeploymentParameters(ApplicationPublisher publisher, HostingModel hostingModel = HostingModel.InProcess, bool publish = false) + { + return GetBaseDeploymentParameters( + publisher, + new DeploymentParameters(publisher.ApplicationPath, DeployerSelector.ServerType, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64) + { + HostingModel = hostingModel, + TargetFramework = "netcoreapp2.2", + AncmVersion = AncmVersion.AspNetCoreModuleV2 + }, + publish); + } + + public IISDeploymentParameters GetBaseDeploymentParameters(ApplicationPublisher publisher, DeploymentParameters baseParameters, bool publish = false) + { + return new IISDeploymentParameters(baseParameters) + { + ApplicationPublisher = publisher, + ApplicationPath = publisher.ApplicationPath, + PublishApplicationBeforeDeployment = publish + }; + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/RequiresNewHandler.cs b/src/IISIntegration/test/Common.FunctionalTests/RequiresNewHandler.cs new file mode 100644 index 0000000000..cbe43ec0c7 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/RequiresNewHandler.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Testing.xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] + public sealed class RequiresNewHandlerAttribute : Attribute, ITestCondition + { + public bool IsMet => !DeployerSelector.IsForwardsCompatibilityTest; + + public string SkipReason => "Test verifies new behavior in the aspnetcorev2_inprocess.dll that isn't supported in earlier versions."; + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/RequiresNewShim.cs b/src/IISIntegration/test/Common.FunctionalTests/RequiresNewShim.cs new file mode 100644 index 0000000000..b0bc50a83b --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/RequiresNewShim.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Testing.xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] + public sealed class RequiresNewShimAttribute : Attribute, ITestCondition + { + public bool IsMet => !DeployerSelector.IsBackwardsCompatiblityTest; + + public string SkipReason => "Test verifies new behavior in the aspnetcorev2.dll that isn't supported in earlier versions."; + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/ServerAbortTests.cs b/src/IISIntegration/test/Common.FunctionalTests/ServerAbortTests.cs new file mode 100644 index 0000000000..8ebd70db12 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/ServerAbortTests.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + public abstract class ServerAbortTests: FixtureLoggedTest + { + private readonly IISTestSiteFixture _fixture; + + [Collection(IISTestSiteCollection.Name)] + public class InProc: ServerAbortTests + { + public InProc(IISTestSiteFixture fixture) : base(fixture) { } + } + + [Collection(OutOfProcessTestSiteCollection.Name)] + public class OutOfProcess: ServerAbortTests + { + public OutOfProcess(OutOfProcessTestSiteFixture fixture) : base(fixture) { } + } + + [Collection(OutOfProcessV1TestSiteCollection.Name)] + public class OutOfProcessV1: ServerAbortTests + { + public OutOfProcessV1(OutOfProcessV1TestSiteFixture fixture) : base(fixture) { } + } + + protected ServerAbortTests(IISTestSiteFixture fixture) : base(fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task ClosesConnectionOnServerAbort() + { + try + { + var response = await _fixture.Client.GetAsync("/Abort").DefaultTimeout(); + + // 502 is expected for outofproc but not for inproc + if (_fixture.DeploymentResult.DeploymentParameters.HostingModel == HostingModel.OutOfProcess) + { + Assert.Equal(HttpStatusCode.BadGateway, response.StatusCode); + // 0x80072f78 ERROR_HTTP_INVALID_SERVER_RESPONSE The server returned an invalid or unrecognized response + Assert.Contains("0x80072f78", await response.Content.ReadAsStringAsync()); + } + else + { + Assert.True(false, "Should not reach here"); + } + } + catch (HttpRequestException) + { + // Connection reset is expected both for outofproc and inproc + } + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/SkipIfNotAdminAttribute.cs b/src/IISIntegration/test/Common.FunctionalTests/SkipIfNotAdminAttribute.cs new file mode 100644 index 0000000000..d2acb70415 --- /dev/null +++ b/src/IISIntegration/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/src/IISIntegration/test/Common.FunctionalTests/SkipVSTSAttribute.cs b/src/IISIntegration/test/Common.FunctionalTests/SkipVSTSAttribute.cs new file mode 100644 index 0000000000..8e88fc8c26 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/SkipVSTSAttribute.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] + public sealed class SkipInVSTSAttribute : Attribute, ITestCondition + { + public bool IsMet => string.IsNullOrEmpty(Environment.GetEnvironmentVariable("SYSTEM_TASKDEFINITIONSURI")); + + public string SkipReason => "Running in VSTS"; + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Utilities/AppVerifier.cs b/src/IISIntegration/test/Common.FunctionalTests/Utilities/AppVerifier.cs new file mode 100644 index 0000000000..7984193e29 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Utilities/AppVerifier.cs @@ -0,0 +1,83 @@ +// 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.Diagnostics; +using System.Linq; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + public class AppVerifier + { + private static readonly TimeSpan AppVerifierCommandTimeout = TimeSpan.FromSeconds(5); + + public static IDisposable Disable(ServerType serverType, int code) + { + // Set in SetupTestEnvironment.ps1 + var enabledCodes = (Environment.GetEnvironmentVariable("APPVERIFIER_ENABLED_CODES") ?? "").Split(' '); + string processName; + switch (serverType) + { + case ServerType.IISExpress: + processName = "iisexpress.exe"; + break; + case ServerType.IIS: + processName = "w3wp.exe"; + break; + default: + throw new ArgumentOutOfRangeException(nameof(serverType), serverType, null); + } + + if (!enabledCodes.Contains(code.ToString())) + { + return null; + } + + RunProcessAndWaitForExit("appverif.exe", $"-configure {code} -for {processName} -with ErrorReport=0", AppVerifierCommandTimeout); + return new AppVerifierToken(processName, code.ToString()); + } + + private static void RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout) + { + var startInfo = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + }; + + var process = Process.Start(startInfo); + + if (!process.WaitForExit((int)timeout.TotalMilliseconds)) + { + process.Kill(); + } + + if (process.ExitCode != 0) + { + throw new InvalidOperationException($"Exit code {process.ExitCode} when running {fileName} {arguments}. Stdout: {process.StandardOutput.ReadToEnd()} Stderr: {process.StandardError.ReadToEnd()}"); + } + } + + public class AppVerifierToken : IDisposable + { + private readonly string _processName; + + private readonly string _codes; + + public AppVerifierToken(string processName, string codes) + { + _processName = processName; + _codes = codes; + } + + public void Dispose() + { + // + RunProcessAndWaitForExit("appverif.exe", $"-configure {_codes} -for {_processName} -with ErrorReport={Environment.GetEnvironmentVariable("APPVERIFIER_LEVEL")}", AppVerifierCommandTimeout); + } + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Utilities/EventLogHelpers.cs b/src/IISIntegration/test/Common.FunctionalTests/Utilities/EventLogHelpers.cs new file mode 100644 index 0000000000..78c77fd2ca --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Utilities/EventLogHelpers.cs @@ -0,0 +1,210 @@ +// 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.Diagnostics; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + public class EventLogHelpers + { + public static void VerifyEventLogEvent(IISDeploymentResult deploymentResult, string expectedRegexMatchString) + { + Assert.True(deploymentResult.HostProcess.HasExited); + + var entries = GetEntries(deploymentResult); + AssertSingleEntry(expectedRegexMatchString, entries); + } + + public static void VerifyEventLogEvent(IISDeploymentResult deploymentResult, string expectedRegexMatchString, ILogger logger) + { + Assert.True(deploymentResult.HostProcess.HasExited); + + var entries = GetEntries(deploymentResult); + try + { + AssertSingleEntry(expectedRegexMatchString, entries); + } + catch (Exception ex) + { + foreach (var entry in entries) + { + logger.LogInformation(entry.Message); + } + throw ex; + } + } + + public static void VerifyEventLogEvents(IISDeploymentResult deploymentResult, params string[] expectedRegexMatchString) + { + Assert.True(deploymentResult.HostProcess.HasExited); + + var entries = GetEntries(deploymentResult).ToList(); + foreach (var regexString in expectedRegexMatchString) + { + var matchedEntries = AssertSingleEntry(regexString, entries); + + foreach (var matchedEntry in matchedEntries) + { + entries.Remove(matchedEntry); + } + } + + Assert.True(0 == entries.Count, $"Some entries were not matched by any regex {FormatEntries(entries)}"); + } + + private static EventLogEntry[] AssertSingleEntry(string regexString, IEnumerable entries) + { + var expectedRegex = new Regex(regexString, RegexOptions.Singleline); + var matchedEntries = entries.Where(entry => expectedRegex.IsMatch(entry.Message)).ToArray(); + Assert.True(matchedEntries.Length > 0, $"No entries matched by '{regexString}'"); + Assert.True(matchedEntries.Length < 2, $"Multiple entries matched by '{regexString}': {FormatEntries(matchedEntries)}"); + return matchedEntries; + } + + private static string FormatEntries(IEnumerable entries) + { + return string.Join(",", entries.Select(e => e.Message)); + } + + private static IEnumerable GetEntries(IISDeploymentResult deploymentResult) + { + var eventLog = new EventLog("Application"); + + // Eventlog is already sorted based on time of event in ascending time. + // Check results in reverse order. + var processIdString = $"Process Id: {deploymentResult.HostProcess.Id}."; + + // Event log messages round down to the nearest second, so subtract a second + var processStartTime = deploymentResult.HostProcess.StartTime.AddSeconds(-1); + for (var i = eventLog.Entries.Count - 1; i >= 0; i--) + { + var eventLogEntry = eventLog.Entries[i]; + if (eventLogEntry.TimeGenerated < processStartTime) + { + // If event logs is older than the process start time, we didn't find a match. + break; + } + + if (eventLogEntry.ReplacementStrings == null || + eventLogEntry.ReplacementStrings.Length < 3) + { + continue; + } + + // ReplacementStings == EventData collection in EventLog + // This is unaffected if event providers are not registered correctly + if (eventLogEntry.Source == AncmVersionToMatch(deploymentResult) && + processIdString == eventLogEntry.ReplacementStrings[1]) + { + yield return eventLogEntry; + } + } + } + + private static string AncmVersionToMatch(IISDeploymentResult deploymentResult) + { + return "IIS " + + (deploymentResult.DeploymentParameters.ServerType == ServerType.IISExpress ? "Express " : "") + + "AspNetCore Module" + + (deploymentResult.DeploymentParameters.AncmVersion == AncmVersion.AspNetCoreModuleV2 ? " V2" : ""); + } + + public static string Started(IISDeploymentResult deploymentResult) + { + if (deploymentResult.DeploymentParameters.HostingModel == HostingModel.InProcess) + { + return InProcessStarted(deploymentResult); + } + else + { + return OutOfProcessStarted(deploymentResult); + } + } + + public static string InProcessStarted(IISDeploymentResult deploymentResult) + { + return $"Application '{EscapedContentRoot(deploymentResult)}' started the coreclr in-process successfully"; + } + + public static string OutOfProcessStarted(IISDeploymentResult deploymentResult) + { + return $"Application '/LM/W3SVC/1/ROOT' started process '\\d+' successfully and process '\\d+' is listening on port '\\d+'."; + } + + public static string InProcessFailedToStart(IISDeploymentResult deploymentResult, string reason) + { + return $"Application '/LM/W3SVC/1/ROOT' with physical root '{EscapedContentRoot(deploymentResult)}' failed to load clr and managed application. {reason}"; + } + + public static string InProcessFailedToStop(IISDeploymentResult deploymentResult, string reason) + { + return "Failed to gracefully shutdown application 'MACHINE/WEBROOT/APPHOST/.*?'."; + } + + public static string InProcessThreadException(IISDeploymentResult deploymentResult, string reason) + { + return $"Application '/LM/W3SVC/1/ROOT' with physical root '{EscapedContentRoot(deploymentResult)}' hit unexpected managed exception{reason}"; + } + + public static string InProcessThreadExit(IISDeploymentResult deploymentResult, string code) + { + return $"Application '/LM/W3SVC/1/ROOT' with physical root '{EscapedContentRoot(deploymentResult)}' hit unexpected managed background thread exit, exit code = '{code}'."; + } + public static string InProcessThreadExitStdOut(IISDeploymentResult deploymentResult, string code, string output) + { + return $"Application '/LM/W3SVC/1/ROOT' with physical root '{EscapedContentRoot(deploymentResult)}' hit unexpected managed background thread exit, exit code = '{code}'. Last 4KB characters of captured stdout and stderr logs:\r\n{output}"; + } + + public static string FailedToStartApplication(IISDeploymentResult deploymentResult, string code) + { + return $"Failed to start application '/LM/W3SVC/1/ROOT', ErrorCode '{code}'."; + } + + public static string ConfigurationLoadError(IISDeploymentResult deploymentResult, string reason) + { + return $"Could not load configuration. Exception message: {reason}"; + } + + public static string OutOfProcessFailedToStart(IISDeploymentResult deploymentResult) + { + return $"Application '/LM/W3SVC/1/ROOT' with physical root '{EscapedContentRoot(deploymentResult)}' failed to start process with " + + $"commandline '(.*)' with multiple retries. " + + $"The last try of listening port is '(.*)'. See previous warnings for details."; + } + + public static string InProcessHostfxrInvalid(IISDeploymentResult deploymentResult) + { + return $"Hostfxr version used does not support 'hostfxr_get_native_search_directories', update the version of hostfxr to a higher version. Path to hostfxr: '(.*)'."; + } + + public static string InProcessFailedToFindNativeDependencies(IISDeploymentResult deploymentResult) + { + return "Invoking hostfxr to find the inprocess request handler failed without finding any native dependencies. " + + "This most likely means the app is misconfigured, please check the versions of Microsoft.NetCore.App and Microsoft.AspNetCore.App that " + + "are targeted by the application and are installed on the machine."; + } + + public static string InProcessFailedToFindRequestHandler(IISDeploymentResult deploymentResult) + { + return "Could not find the assembly '(.*)' referenced for the in-process application. Please confirm the Microsoft.AspNetCore.Server.IIS package is referenced in your application."; + } + + private static string EscapedContentRoot(IISDeploymentResult deploymentResult) + { + var contentRoot = deploymentResult.ContentRoot; + if (!contentRoot.EndsWith('\\')) + { + contentRoot += '\\'; + } + return Regex.Escape(contentRoot); + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Utilities/FunctionalTestsBase.cs b/src/IISIntegration/test/Common.FunctionalTests/Utilities/FunctionalTestsBase.cs new file mode 100644 index 0000000000..1a1a4ce490 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Utilities/FunctionalTestsBase.cs @@ -0,0 +1,59 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.Extensions.Logging.Testing; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + public class FunctionalTestsBase : LoggedTest + { + private const string DebugEnvironmentVariable = "ASPNETCORE_MODULE_DEBUG"; + + public FunctionalTestsBase(ITestOutputHelper output = null) : base(output) + { + } + + protected IISDeployerBase _deployer; + + protected ApplicationDeployer CreateDeployer(IISDeploymentParameters parameters) + { + if (parameters.ServerType == ServerType.IISExpress && + !parameters.EnvironmentVariables.ContainsKey(DebugEnvironmentVariable)) + { + parameters.EnvironmentVariables[DebugEnvironmentVariable] = "console"; + } + + return IISApplicationDeployerFactory.Create(parameters, LoggerFactory); + } + + protected virtual async Task DeployAsync(IISDeploymentParameters parameters) + { + _deployer = (IISDeployerBase)CreateDeployer(parameters); + return (IISDeploymentResult)await _deployer.DeployAsync(); + } + + protected virtual async Task StartAsync(IISDeploymentParameters parameters) + { + var result = await DeployAsync(parameters); + await result.AssertStarts(); + return result; + } + + public override void Dispose() + { + StopServer(false); + } + + public void StopServer(bool gracefulShutdown = true) + { + _deployer?.Dispose(gracefulShutdown); + _deployer = null; + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Utilities/Helpers.cs b/src/IISIntegration/test/Common.FunctionalTests/Utilities/Helpers.cs new file mode 100644 index 0000000000..1e31126eab --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Utilities/Helpers.cs @@ -0,0 +1,241 @@ +// 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.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + public static class Helpers + { + private static readonly TimeSpan RetryRequestDelay = TimeSpan.FromMilliseconds(100); + private static readonly int RetryRequestCount = 10; + + public static string GetTestWebSitePath(string name) + { + return Path.Combine(TestPathUtilities.GetSolutionRootDirectory("IISIntegration"),"test", "WebSites", name); + } + + public static string GetInProcessTestSitesPath() + { + return DeployerSelector.IsForwardsCompatibilityTest ? GetTestWebSitePath("InProcessForwardsCompatWebSite") : GetTestWebSitePath("InProcessWebSite"); + } + + public static string GetOutOfProcessTestSitesPath() => GetTestWebSitePath("OutOfProcessWebSite"); + + public static async Task AssertStarts(this IISDeploymentResult deploymentResult, string path = "/HelloWorld") + { + var response = await deploymentResult.HttpClient.GetAsync(path); + + var responseText = await response.Content.ReadAsStringAsync(); + + Assert.Equal("Hello World", responseText); + } + + public static async Task StressLoad(HttpClient httpClient, string path, Action action) + { + async Task RunRequests() + { + var connection = new HttpClient() { BaseAddress = httpClient.BaseAddress }; + + for (int j = 0; j < 10; j++) + { + var response = await connection.GetAsync(path); + action(response); + } + } + + List tasks = new List(); + for (int i = 0; i < 10; i++) + { + tasks.Add(Task.Run(RunRequests)); + } + + await Task.WhenAll(tasks); + } + + public static string GetFrebFolder(string folder, IISDeploymentResult result) + { + if (result.DeploymentParameters.ServerType == ServerType.IISExpress) + { + return Path.Combine(folder, result.DeploymentParameters.SiteName); + } + else + { + return Path.Combine(folder, "W3SVC1"); + } + } + + public static void CopyFiles(DirectoryInfo source, DirectoryInfo target, ILogger logger) + { + foreach (DirectoryInfo directoryInfo in source.GetDirectories()) + { + if (directoryInfo.FullName != target.FullName) + { + CopyFiles(directoryInfo, target.CreateSubdirectory(directoryInfo.Name), logger); + } + } + logger?.LogDebug($"Processing {target.FullName}"); + foreach (FileInfo fileInfo in source.GetFiles()) + { + logger?.LogDebug($" Copying {fileInfo.Name}"); + var destFileName = Path.Combine(target.FullName, fileInfo.Name); + fileInfo.CopyTo(destFileName); + } + } + + public static void ModifyWebConfig(this DeploymentResult deploymentResult, Action action) + { + var webConfigPath = Path.Combine(deploymentResult.ContentRoot, "web.config"); + var document = XDocument.Load(webConfigPath); + action(document.Root); + document.Save(webConfigPath); + } + + public static Task RetryRequestAsync(this HttpClient client, string uri, Func predicate) + { + return RetryRequestAsync(client, uri, message => Task.FromResult(predicate(message))); + } + + public static async Task RetryRequestAsync(this HttpClient client, string uri, Func> predicate) + { + HttpResponseMessage response = await client.GetAsync(uri); + + for (var i = 0; i < RetryRequestCount && !await predicate(response); i++) + { + // Keep retrying until app_offline is present. + response = await client.GetAsync(uri); + await Task.Delay(RetryRequestDelay); + } + + if (!await predicate(response)) + { + throw new InvalidOperationException($"Didn't get response that satisfies predicate after {RetryRequestCount} retries"); + } + + return response; + } + + public static async Task Retry(Func func, int attempts, int msDelay) + { + var exceptions = new List(); + + for (var attempt = 0; attempt < attempts; attempt++) + { + try + { + await func(); + return; + } + catch (Exception e) + { + exceptions.Add(e); + } + await Task.Delay(msDelay); + } + + throw new AggregateException(exceptions); + } + + public static void AssertWorkerProcessStop(this IISDeploymentResult deploymentResult, int? timeout = null) + { + var hostProcess = deploymentResult.HostProcess; + Assert.True(hostProcess.WaitForExit(timeout ?? (int)TimeoutExtensions.DefaultTimeoutValue.TotalMilliseconds)); + + if (deploymentResult.DeploymentParameters.ServerType == ServerType.IISExpress) + { + Assert.Equal(0, hostProcess.ExitCode); + } + } + + + public static async Task AssertRecycledAsync(this IISDeploymentResult deploymentResult, Func verificationAction = null) + { + if (deploymentResult.DeploymentParameters.HostingModel != HostingModel.InProcess) + { + throw new NotSupportedException(); + } + + deploymentResult.AssertWorkerProcessStop(); + if (deploymentResult.DeploymentParameters.ServerType == ServerType.IIS) + { + verificationAction = verificationAction ?? (() => deploymentResult.AssertStarts()); + await verificationAction(); + } + } + + public static IEnumerable ToTheoryData(this Dictionary dictionary) + { + return dictionary.Keys.Select(k => new[] { k }); + } + + public static string GetExpectedLogName(IISDeploymentResult deploymentResult, string logFolderPath) + { + var startTime = deploymentResult.HostProcess.StartTime.ToUniversalTime(); + + if (deploymentResult.DeploymentParameters.HostingModel == HostingModel.InProcess) + { + return Path.Combine(logFolderPath, $"std_{startTime.Year}{startTime.Month:D2}" + + $"{startTime.Day:D2}{startTime.Hour:D2}" + + $"{startTime.Minute:D2}{startTime.Second:D2}_" + + $"{deploymentResult.HostProcess.Id}.log"); + } + else + { + return Directory.GetFiles(logFolderPath).Single(); + } + } + + public static void ModifyFrameworkVersionInRuntimeConfig(IISDeploymentResult deploymentResult) + { + var path = Path.Combine(deploymentResult.ContentRoot, "InProcessWebSite.runtimeconfig.json"); + dynamic depsFileContent = JsonConvert.DeserializeObject(File.ReadAllText(path)); + depsFileContent["runtimeOptions"]["framework"]["version"] = "2.9.9"; + var output = JsonConvert.SerializeObject(depsFileContent); + File.WriteAllText(path, output); + } + + public static void AllowNoLogs(this IISDeploymentResult deploymentResult) + { + File.AppendAllText( + Path.Combine(deploymentResult.DeploymentParameters.PublishedApplicationRootPath, "aspnetcore-debug.log"), + "Running test allowed log file to be empty." + Environment.NewLine); + } + + public static string ReadAllTextFromFile(string filename, ILogger logger) + { + try + { + return File.ReadAllText(filename); + } + catch (Exception ex) + { + // check if there is a dotnet.exe, iisexpress.exe, or w3wp.exe processes still running. + var hostingProcesses = Process.GetProcessesByName("dotnet") + .Concat(Process.GetProcessesByName("iisexpress")) + .Concat(Process.GetProcessesByName("w3wp")); + + logger.LogError($"Could not read file content. Exception message {ex.Message}"); + logger.LogError("Current hosting exes running:"); + + foreach (var hostingProcess in hostingProcesses) + { + logger.LogError($"{hostingProcess.ProcessName} pid: {hostingProcess.Id} hasExited: {hostingProcess.HasExited.ToString()}"); + } + throw ex; + } + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISCapability.cs b/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISCapability.cs new file mode 100644 index 0000000000..0a080bb702 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISCapability.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Flags] + public enum IISCapability + { + None = 0, + Websockets = 1, + WindowsAuthentication = 2, + PoolEnvironmentVariables = 4, + ShutdownToken = 8, + DynamicCompression = 16, + ApplicationInitialization = 32, + TracingModule = 64, + FailedRequestTracingModule = 128, + BasicAuthentication = 256 + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISCompressionSiteCollection.cs b/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISCompressionSiteCollection.cs new file mode 100644 index 0000000000..2c424943f3 --- /dev/null +++ b/src/IISIntegration/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/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISCompressionSiteFixture.cs b/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISCompressionSiteFixture.cs new file mode 100644 index 0000000000..3aff68d11b --- /dev/null +++ b/src/IISIntegration/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/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISFunctionalTestBase.cs b/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISFunctionalTestBase.cs new file mode 100644 index 0000000000..a20a5e2e0e --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISFunctionalTestBase.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities +{ + public class IISFunctionalTestBase : FunctionalTestsBase + { + public IISFunctionalTestBase(ITestOutputHelper output = null) : base(output) + { + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISTestSiteCollection.cs b/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISTestSiteCollection.cs new file mode 100644 index 0000000000..562d63adbe --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISTestSiteCollection.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + /// + /// This type just maps collection names to available fixtures + /// + [CollectionDefinition(Name)] + public class IISTestSiteCollection : ICollectionFixture + { + public const string Name = nameof(IISTestSiteCollection); + } + + [CollectionDefinition(Name)] + public class OutOfProcessTestSiteCollection : ICollectionFixture + { + public const string Name = nameof(OutOfProcessTestSiteCollection); + } + + [CollectionDefinition(Name)] + public class OutOfProcessV1TestSiteCollection : ICollectionFixture + { + public const string Name = nameof(OutOfProcessV1TestSiteCollection); + } + +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISTestSiteFixture.cs b/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISTestSiteFixture.cs new file mode 100644 index 0000000000..e8cfd8f641 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Utilities/IISTestSiteFixture.cs @@ -0,0 +1,191 @@ +// 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 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 OutOfProcessTestSiteFixture : IISTestSiteFixture + { + public OutOfProcessTestSiteFixture() : base(Configure) + { + } + + private static void Configure(IISDeploymentParameters deploymentParameters) + { + deploymentParameters.ApplicationPath = Helpers.GetOutOfProcessTestSitesPath(); + deploymentParameters.HostingModel = HostingModel.OutOfProcess; + } + } + + public class OutOfProcessV1TestSiteFixture : IISTestSiteFixture + { + public OutOfProcessV1TestSiteFixture() : base(Configure) + { + } + + private static void Configure(IISDeploymentParameters deploymentParameters) + { + deploymentParameters.ApplicationPath = Helpers.GetOutOfProcessTestSitesPath(); + deploymentParameters.HostingModel = HostingModel.OutOfProcess; + deploymentParameters.AncmVersion = AncmVersion.AspNetCoreModule; + } + } + + public class IISTestSiteFixture : IDisposable + { + private ApplicationDeployer _deployer; + private ILoggerFactory _loggerFactory; + private ForwardingProvider _forwardingProvider; + private IISDeploymentResult _deploymentResult; + private readonly Action _configure; + + public IISTestSiteFixture() : this(_ => { }) + { + } + + 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() + { + return new TestConnection(Client.BaseAddress.Port); + } + + public void Dispose() + { + _deployer?.Dispose(); + } + + public void Attach(LoggedTest test) + { + if (_forwardingProvider.LoggerFactory != null) + { + throw new InvalidOperationException("Test instance is already attached to this fixture"); + } + + _forwardingProvider.LoggerFactory = test.LoggerFactory; + } + + public void Detach(LoggedTest test) + { + if (_forwardingProvider.LoggerFactory != test.LoggerFactory) + { + throw new InvalidOperationException("Different test is attached to this fixture"); + } + + _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(); + + private ILoggerFactory _loggerFactory; + + public ILoggerFactory LoggerFactory + { + get => _loggerFactory; + set + { + + lock (_loggers) + { + _loggerFactory = value; + foreach (var logger in _loggers) + { + logger.Logger = _loggerFactory?.CreateLogger("FIXTURE:" + logger.Name); + } + } + } + } + + public void Dispose() + { + lock (_loggers) + { + _loggers.Clear(); + } + } + + public ILogger CreateLogger(string categoryName) + { + lock (_loggers) + { + var logger = new ForwardingLogger(categoryName); + _loggers.Add(logger); + return logger; + } + } + } + + internal class ForwardingLogger : ILogger + { + public ForwardingLogger(string name) + { + Name = name; + } + + public ILogger Logger { get; set; } + public string Name { get; set; } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + Logger?.Log(logLevel, eventId, state, exception, formatter); + } + + public bool IsEnabled(LogLevel logLevel) => Logger?.IsEnabled(logLevel) == true; + + public IDisposable BeginScope(TState state) => Logger?.BeginScope(state); + } + } + +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Utilities/LogFileTestBase.cs b/src/IISIntegration/test/Common.FunctionalTests/Utilities/LogFileTestBase.cs new file mode 100644 index 0000000000..17885f3547 --- /dev/null +++ b/src/IISIntegration/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 Helpers.ReadAllTextFromFile(Helpers.GetExpectedLogName(deploymentResult, _logFolderPath), Logger); + } + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Utilities/RequiresEnvironmentVariableAttribute.cs b/src/IISIntegration/test/Common.FunctionalTests/Utilities/RequiresEnvironmentVariableAttribute.cs new file mode 100644 index 0000000000..d2749db547 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Utilities/RequiresEnvironmentVariableAttribute.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] + public sealed class RequiresEnvironmentVariableAttribute : Attribute, ITestCondition + { + private readonly string _name; + + public RequiresEnvironmentVariableAttribute(string name) + { + _name = name; + } + + public bool IsMet => !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(_name)); + + public string SkipReason => $"Environment variable {_name} is required to run this test."; + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/Utilities/SkipIfDebugAttribute.cs b/src/IISIntegration/test/Common.FunctionalTests/Utilities/SkipIfDebugAttribute.cs new file mode 100644 index 0000000000..6cdd725392 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/Utilities/SkipIfDebugAttribute.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] + public sealed class SkipIfDebugAttribute : Attribute, ITestCondition + { + public bool IsMet => + #if DEBUG + false; + #else + true; + #endif + + public string SkipReason => "Test cannot be run in Debug mode."; + } +} diff --git a/src/IISIntegration/test/Common.FunctionalTests/WindowsAuthTests.cs b/src/IISIntegration/test/Common.FunctionalTests/WindowsAuthTests.cs new file mode 100644 index 0000000000..8431b15801 --- /dev/null +++ b/src/IISIntegration/test/Common.FunctionalTests/WindowsAuthTests.cs @@ -0,0 +1,52 @@ +// 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.Http; +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; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class WindowsAuthTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public WindowsAuthTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + public static TestMatrix TestVariants + => TestMatrix.ForServers(DeployerSelector.ServerType) + .WithTfms(Tfm.NetCoreApp22, Tfm.Net461) + .WithApplicationTypes(ApplicationType.Portable) + .WithAllAncmVersions() + .WithAllHostingModels(); + + [ConditionalTheory] + [RequiresIIS(IISCapability.WindowsAuthentication)] + [MemberData(nameof(TestVariants))] + public async Task WindowsAuthTest(TestVariant variant) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant); + deploymentParameters.SetAnonymousAuth(enabled: false); + deploymentParameters.SetWindowsAuth(); + + // The default in hosting sets windows auth to true. + var deploymentResult = await DeployAsync(deploymentParameters); + + var client = deploymentResult.CreateClient(new HttpClientHandler { UseDefaultCredentials = true }); + var response = await client.GetAsync("/Auth"); + var responseText = await response.Content.ReadAsStringAsync(); + + Assert.StartsWith("Windows:", responseText); + Assert.Contains(Environment.UserName, responseText); + } + } +} diff --git a/src/IISIntegration/test/Common.Tests/Common.Tests.csproj b/src/IISIntegration/test/Common.Tests/Common.Tests.csproj new file mode 100644 index 0000000000..ede80732ee --- /dev/null +++ b/src/IISIntegration/test/Common.Tests/Common.Tests.csproj @@ -0,0 +1,22 @@ + + + + netcoreapp2.2 + false + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/test/Common.Tests/Utilities/DisposableList.cs b/src/IISIntegration/test/Common.Tests/Utilities/DisposableList.cs new file mode 100644 index 0000000000..78f76e41c2 --- /dev/null +++ b/src/IISIntegration/test/Common.Tests/Utilities/DisposableList.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.Collections.Generic; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + public class DisposableList : List, IDisposable where T : IDisposable + { + public DisposableList() : base() { } + + public DisposableList(IEnumerable collection) : base(collection) { } + + public DisposableList(int capacity) : base(capacity) { } + + public void Dispose() + { + foreach (var item in this) + { + item?.Dispose(); + } + } + } +} diff --git a/src/IISIntegration/test/Common.Tests/Utilities/TestConnections.cs b/src/IISIntegration/test/Common.Tests/Utilities/TestConnections.cs new file mode 100644 index 0000000000..3b7a870cf3 --- /dev/null +++ b/src/IISIntegration/test/Common.Tests/Utilities/TestConnections.cs @@ -0,0 +1,252 @@ +// 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.Diagnostics; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + /// + /// Summary description for TestConnection + /// + public class TestConnection : IDisposable + { + private static readonly TimeSpan Timeout = TimeSpan.FromMinutes(1); + + private readonly bool _ownsSocket; + private readonly Socket _socket; + private readonly NetworkStream _stream; + + public TestConnection(int port) + : this(port, AddressFamily.InterNetwork) + { + } + + public TestConnection(int port, AddressFamily addressFamily) + : this(CreateConnectedLoopbackSocket(port, addressFamily), ownsSocket: true) + { + } + + public TestConnection(Socket socket) + : this(socket, ownsSocket: false) + { + } + + private TestConnection(Socket socket, bool ownsSocket) + { + _ownsSocket = ownsSocket; + _socket = socket; + _stream = new NetworkStream(_socket, ownsSocket: false); + } + + public Socket Socket => _socket; + public Stream Stream => _stream; + + public void Dispose() + { + _stream.Dispose(); + + if (_ownsSocket) + { + _socket.Dispose(); + } + } + + public async Task Send(params string[] lines) + { + var bytes = Encoding.ASCII.GetBytes(string.Join("\r\n", lines)); + + for (var index = 0; index < bytes.Length; index++) + { + await _stream.WriteAsync(bytes, index, 1).ConfigureAwait(false); + await _stream.FlushAsync().ConfigureAwait(false); + // Re-add delay to help find socket input consumption bugs more consistently + //await Task.Delay(TimeSpan.FromMilliseconds(5)); + } + } + + 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 = 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 < length) + { + 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}{length} bytes of data{Environment.NewLine}{Environment.NewLine}" + + $"Actual:{Environment.NewLine}{Encoding.ASCII.GetString(actual, 0, offset)}{Environment.NewLine}", + ex); + } + + return actual.AsMemory(0, offset); + } + + public async Task ReceiveStartsWith(string prefix, int maxLineLength = 1024) + { + 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 = _stream.ReadAsync(actual, offset, 1); + if (!Debugger.IsAttached) + { + Assert.True(task.Wait(4000), "timeout"); + } + var count = await task.ConfigureAwait(false); + if (count == 0) + { + break; + } + + Assert.True(count == 1); + offset++; + + if (actual[offset - 1] == '\n') + { + break; + } + } + + var actualLine = Encoding.ASCII.GetString(actual, 0, offset); + Assert.StartsWith(prefix, actualLine); + } + + public async Task ReceiveHeaders(params string[] lines) + { + List headers = new List(); + string line; + do + { + line = await ReadLineAsync(); + headers.Add(line); + } while (line != ""); + + foreach (var s in lines) + { + Assert.Contains(s, headers); + } + + return headers.ToArray(); + } + + public Task WaitForConnectionClose() + { + var tcs = new TaskCompletionSource(); + var eventArgs = new SocketAsyncEventArgs(); + eventArgs.SetBuffer(new byte[128], 0, 128); + eventArgs.Completed += ReceiveAsyncCompleted; + eventArgs.UserToken = tcs; + + if (!_socket.ReceiveAsync(eventArgs)) + { + ReceiveAsyncCompleted(this, eventArgs); + } + + return tcs.Task; + } + + private void ReceiveAsyncCompleted(object sender, SocketAsyncEventArgs e) + { + var tcs = (TaskCompletionSource)e.UserToken; + if (e.BytesTransferred == 0) + { + tcs.SetResult(null); + } + else + { + tcs.SetException(new IOException( + $"Expected connection close, received data instead: \"{Encoding.ASCII.GetString(e.Buffer, 0, e.BytesTransferred)}\"")); + } + } + + public static Socket CreateConnectedLoopbackSocket(int port, AddressFamily addressFamily) + { + if (addressFamily != AddressFamily.InterNetwork && addressFamily != AddressFamily.InterNetworkV6) + { + throw new ArgumentException($"TestConnection does not support address family of type {addressFamily}", nameof(addressFamily)); + } + + var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp); + var address = addressFamily == AddressFamily.InterNetworkV6 + ? IPAddress.IPv6Loopback + : IPAddress.Loopback; + socket.Connect(new IPEndPoint(address, port)); + return socket; + } + } +} diff --git a/src/IISIntegration/test/Common.Tests/Utilities/TimeoutExtensions.cs b/src/IISIntegration/test/Common.Tests/Utilities/TimeoutExtensions.cs new file mode 100644 index 0000000000..ce7175dff9 --- /dev/null +++ b/src/IISIntegration/test/Common.Tests/Utilities/TimeoutExtensions.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + + public static class TimeoutExtensions + { + public static TimeSpan DefaultTimeoutValue = TimeSpan.FromSeconds(300); + + public static Task DefaultTimeout(this Task task, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = -1) + { + return task.TimeoutAfter(DefaultTimeoutValue, filePath, lineNumber); + } + + public static Task DefaultTimeout(this Task task, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = -1) + { + return task.TimeoutAfter(DefaultTimeoutValue, filePath, lineNumber); + } + } +} diff --git a/src/IISIntegration/test/CommonLibTests/CommonLibTests.vcxproj b/src/IISIntegration/test/CommonLibTests/CommonLibTests.vcxproj new file mode 100644 index 0000000000..87dbd16675 --- /dev/null +++ b/src/IISIntegration/test/CommonLibTests/CommonLibTests.vcxproj @@ -0,0 +1,196 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + {1eac8125-1765-4e2d-8cbe-56dc98a1c8c1} + Win32Proj + 10.0.15063.0 + Application + v141 + Unicode + + + + + + + + + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + + + + + + + + + + + + + + + + + + {ec82302f-d2f0-4727-99d1-eabc0dd9dc3b} + + + {55494e58-e061-4c4c-a0a8-837008e72f85} + + + {09d9d1d6-2951-4e14-bc35-76a23cf9391a} + + + {1533e271-f61b-441b-8b74-59fb61df0552} + + + {cac1267b-8778-4257-aac6-caf481723b01} + + + {d57ea297-6dc2-4bc0-8c91-334863327863} + + + + + + + NotUsing + Disabled + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + $(IntDir)$(TargetName).pch + stdafx.h + true + EnableFastChecks + MultiThreadedDebug + Level3 + $(MSBuildThisFileDirectory)include;%(AdditionalIncludeDirectories);..\..\src\AspNetCoreModuleV2\RequestHandlerLib;..\..\src\AspNetCoreModuleV2\IISLib;..\..\src\AspNetCoreModuleV2\CommonLib;..\gtest\googletest\googletest\include;..\gtest\googletest\googlemock\include;...\..\src\AspNetCoreModuleV2\AspNetCore\Inc;..\..\src\AspNetCoreModuleV2\InProcessRequestHandler\ + /D "_SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING" + stdcpp17 + + + true + Console + ..\..\src\AspNetCoreModuleV2\InProcessRequestHandler\$(Configuration)\; + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;inprocessapplication.obj;inprocesshandler.obj;ahadmin.lib;Rpcrt4.lib;inprocessapplicationbase.obj;stdafx.obj;version.lib;inprocessoptions.obj;%(AdditionalDependencies) + UseLinkTimeCodeGeneration + + + + + + + + + NotUsing + Disabled + X64;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + $(IntDir)$(TargetName).pch + stdafx.h + true + EnableFastChecks + MultiThreadedDebug + Level3 + $(MSBuildThisFileDirectory)include;%(AdditionalIncludeDirectories);..\..\src\AspNetCoreModuleV2\RequestHandlerLib;..\..\src\AspNetCoreModuleV2\IISLib;..\..\src\AspNetCoreModuleV2\CommonLib;..\gtest\googletest\googletest\include;..\gtest\googletest\googlemock\include;...\..\src\AspNetCoreModuleV2\AspNetCore\Inc;..\..\src\AspNetCoreModuleV2\InProcessRequestHandler\ + /D "_SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING" + stdcpp17 + + + true + Console + ..\..\src\AspNetCoreModuleV2\InProcessRequestHandler\x64\$(Configuration)\; + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;inprocessapplication.obj;inprocesshandler.obj;ahadmin.lib;Rpcrt4.lib;inprocessapplicationbase.obj;stdafx.obj;version.lib;inprocessoptions.obj;%(AdditionalDependencies) + UseLinkTimeCodeGeneration + + + + + + + + + NotUsing + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + $(IntDir)$(TargetName).pch + stdafx.h + MultiThreaded + Level3 + ProgramDatabase + $(MSBuildThisFileDirectory)include;%(AdditionalIncludeDirectories);..\..\src\AspNetCoreModuleV2\RequestHandlerLib;..\..\src\AspNetCoreModuleV2\IISLib;..\..\src\AspNetCoreModuleV2\CommonLib;..\gtest\googletest\googletest\include;..\gtest\googletest\googlemock\include;...\..\src\AspNetCoreModuleV2\AspNetCore\Inc;..\..\src\AspNetCoreModuleV2\InProcessRequestHandler\ + /D "_SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING" + stdcpp17 + + + true + Console + /NODEFAULTLIB:libucrt.lib /DEFAULTLIB:ucrt.lib %(AdditionalOptions) + true + true + ..\..\src\AspNetCoreModuleV2\InProcessRequestHandler\$(Configuration)\; + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;inprocessapplication.obj;inprocesshandler.obj;ahadmin.lib;Rpcrt4.lib;inprocessapplicationbase.obj;stdafx.obj;version.lib;inprocessoptions.obj;%(AdditionalDependencies) + UseLinkTimeCodeGeneration + + + + + + + + + NotUsing + X64;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + $(IntDir)$(TargetName).pch + stdafx.h + MultiThreaded + Level3 + ProgramDatabase + $(MSBuildThisFileDirectory)include;%(AdditionalIncludeDirectories);..\..\src\AspNetCoreModuleV2\RequestHandlerLib;..\..\src\AspNetCoreModuleV2\IISLib;..\..\src\AspNetCoreModuleV2\CommonLib;..\gtest\googletest\googletest\include;..\gtest\googletest\googlemock\include;...\..\src\AspNetCoreModuleV2\AspNetCore\Inc;..\..\src\AspNetCoreModuleV2\InProcessRequestHandler\ + /D "_SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING" + stdcpp17 + + + true + Console + /NODEFAULTLIB:libucrt.lib /DEFAULTLIB:ucrt.lib %(AdditionalOptions) + true + true + ..\..\src\AspNetCoreModuleV2\InProcessRequestHandler\x64\$(Configuration)\; + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;inprocessapplication.obj;inprocesshandler.obj;ahadmin.lib;Rpcrt4.lib;inprocessapplicationbase.obj;stdafx.obj;version.lib;inprocessoptions.obj;%(AdditionalDependencies) + UseLinkTimeCodeGeneration + + + + + + + + \ No newline at end of file diff --git a/src/IISIntegration/test/CommonLibTests/ConfigUtilityTests.cpp b/src/IISIntegration/test/CommonLibTests/ConfigUtilityTests.cpp new file mode 100644 index 0000000000..9b5bf6e9e6 --- /dev/null +++ b/src/IISIntegration/test/CommonLibTests/ConfigUtilityTests.cpp @@ -0,0 +1,123 @@ +// 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. + +#include "stdafx.h" +#include "gmock/gmock.h" +using ::testing::_; +using ::testing::NiceMock; + +namespace ConfigUtilityTests +{ + using ::testing::Test; + + class ConfigUtilityTest : public Test + { + protected: + void TestHandlerVersion(std::wstring key, std::wstring value, std::wstring expected, HRESULT(*func)(IAppHostElement*, STRU&)) + { + IAppHostElement* retElement = NULL; + + STRU handlerVersion; + + // NiceMock removes warnings about "uninteresting calls", + auto element = std::make_unique>(); + auto innerElement = std::make_unique>(); + auto collection = std::make_unique>(); + auto nameElement = std::make_unique>(); + auto mockProperty = std::make_unique>(); + + ON_CALL(*element, GetElementByName(_, _)) + .WillByDefault(DoAll(testing::SetArgPointee<1>(innerElement.get()), testing::Return(S_OK))); + ON_CALL(*innerElement, get_Collection(_)) + .WillByDefault(testing::DoAll(testing::SetArgPointee<0>(collection.get()), testing::Return(S_OK))); + ON_CALL(*collection, get_Count(_)) + .WillByDefault(DoAll(testing::SetArgPointee<0>(1), testing::Return(S_OK))); + ON_CALL(*collection, get_Item(_, _)) + .WillByDefault(DoAll(testing::SetArgPointee<1>(nameElement.get()), testing::Return(S_OK))); + ON_CALL(*nameElement, GetPropertyByName(_, _)) + .WillByDefault(DoAll(testing::SetArgPointee<1>(mockProperty.get()), testing::Return(S_OK))); + EXPECT_CALL(*mockProperty, get_StringValue(_)) + .WillOnce(DoAll(testing::SetArgPointee<0>(SysAllocString(key.c_str())), testing::Return(S_OK))) + .WillOnce(DoAll(testing::SetArgPointee<0>(SysAllocString(value.c_str())), testing::Return(S_OK))); + + HRESULT hr = func(element.get(), handlerVersion); + + EXPECT_EQ(hr, S_OK); + EXPECT_STREQ(handlerVersion.QueryStr(), expected.c_str()); + } + }; + + TEST_F(ConfigUtilityTest, CheckHandlerVersionKeysAndValues) + { + auto func = ConfigUtility::FindHandlerVersion; + TestHandlerVersion(L"handlerVersion", L"value", L"value", func); + TestHandlerVersion(L"handlerversion", L"value", L"value", func); + TestHandlerVersion(L"HandlerversioN", L"value", L"value", func); + TestHandlerVersion(L"randomvalue", L"value", L"", func); + TestHandlerVersion(L"", L"value", L"", func); + TestHandlerVersion(L"", L"", L"", func); + } + + TEST_F(ConfigUtilityTest, CheckDebugLogFile) + { + auto func = ConfigUtility::FindDebugFile; + + TestHandlerVersion(L"debugFile", L"value", L"value", func); + TestHandlerVersion(L"debugFILE", L"value", L"value", func); + } + + TEST_F(ConfigUtilityTest, CheckDebugLevel) + { + auto func = ConfigUtility::FindDebugLevel; + + TestHandlerVersion(L"debugLevel", L"value", L"value", func); + TestHandlerVersion(L"debugLEVEL", L"value", L"value", func); + } + + TEST(ConfigUtilityTestSingle, MultipleElements) + { + IAppHostElement* retElement = NULL; + STRU handlerVersion; + + auto element = std::make_unique>(); + auto innerElement = std::make_unique>(); + auto collection = std::make_unique>(); + auto nameElement = std::make_unique>(); + auto mockProperty = std::make_unique>(); + + ON_CALL(*element, GetElementByName(_, _)) + .WillByDefault(DoAll(testing::SetArgPointee<1>(innerElement.get()), testing::Return(S_OK))); + ON_CALL(*innerElement, get_Collection(_)) + .WillByDefault(testing::DoAll(testing::SetArgPointee<0>(collection.get()), testing::Return(S_OK))); + ON_CALL(*collection, get_Count(_)) + .WillByDefault(DoAll(testing::SetArgPointee<0>(2), testing::Return(S_OK))); + ON_CALL(*collection, get_Item(_, _)) + .WillByDefault(DoAll(testing::SetArgPointee<1>(nameElement.get()), testing::Return(S_OK))); + ON_CALL(*nameElement, GetPropertyByName(_, _)) + .WillByDefault(DoAll(testing::SetArgPointee<1>(mockProperty.get()), testing::Return(S_OK))); + EXPECT_CALL(*mockProperty, get_StringValue(_)) + .WillOnce(DoAll(testing::SetArgPointee<0>(SysAllocString(L"key")), testing::Return(S_OK))) + .WillOnce(DoAll(testing::SetArgPointee<0>(SysAllocString(L"value")), testing::Return(S_OK))) + .WillOnce(DoAll(testing::SetArgPointee<0>(SysAllocString(L"handlerVersion")), testing::Return(S_OK))) + .WillOnce(DoAll(testing::SetArgPointee<0>(SysAllocString(L"value2")), testing::Return(S_OK))); + + HRESULT hr = ConfigUtility::FindHandlerVersion(element.get(), handlerVersion); + + EXPECT_EQ(hr, S_OK); + EXPECT_STREQ(handlerVersion.QueryStr(), L"value2"); + } + + TEST(ConfigUtilityTestSingle, IgnoresFailedGetElement) + { + STRU handlerVersion; + + auto element = std::make_unique>(); + ON_CALL(*element, GetElementByName(_, _)) + .WillByDefault(DoAll(testing::SetArgPointee<1>(nullptr), testing::Return(HRESULT_FROM_WIN32( ERROR_INVALID_INDEX )))); + + HRESULT hr = ConfigUtility::FindHandlerVersion(element.get(), handlerVersion); + + EXPECT_EQ(hr, S_OK); + EXPECT_STREQ(handlerVersion.QueryStr(), L""); + } +} diff --git a/src/IISIntegration/test/CommonLibTests/FileOutputManagerTests.cpp b/src/IISIntegration/test/CommonLibTests/FileOutputManagerTests.cpp new file mode 100644 index 0000000000..66a9ff7f0b --- /dev/null +++ b/src/IISIntegration/test/CommonLibTests/FileOutputManagerTests.cpp @@ -0,0 +1,152 @@ +// 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. + +#include "stdafx.h" +#include "gtest/internal/gtest-port.h" +#include "FileOutputManager.h" + +class FileManagerWrapper +{ +public: + FileOutputManager* manager; + FileManagerWrapper(FileOutputManager* m) + : manager(m) + { + manager->Start(); + } + + ~FileManagerWrapper() + { + delete manager; + } +}; + +namespace FileOutManagerStartupTests +{ + using ::testing::Test; + class FileOutputManagerTest : public Test + { + protected: + void + Test(std::wstring fileNamePrefix, FILE* out) + { + PCWSTR expected = L"test"; + + auto tempDirectory = TempDirectory(); + FileOutputManager* pManager = new FileOutputManager(fileNamePrefix, tempDirectory.path()); + + { + FileManagerWrapper wrapper(pManager); + + wprintf(expected, out); + } + + for (auto & p : std::filesystem::directory_iterator(tempDirectory.path())) + { + std::wstring filename(p.path().filename()); + ASSERT_EQ(filename.substr(0, fileNamePrefix.size()), fileNamePrefix); + + std::wstring content = Helpers::ReadFileContent(std::wstring(p.path())); + } + } + }; + + TEST_F(FileOutputManagerTest, WriteToFileCheckContentsWritten) + { + Test(L"", stdout); + Test(L"log", stdout); + } + + TEST_F(FileOutputManagerTest, WriteToFileCheckContentsWrittenErr) + { + Test(L"", stderr); + Test(L"log", stderr); + } +} + +namespace FileOutManagerOutputTests +{ + TEST(FileOutManagerOutputTest, StdOut) + { + PCWSTR expected = L"test"; + + auto tempDirectory = TempDirectory(); + + FileOutputManager* pManager = new FileOutputManager(L"", tempDirectory.path()); + { + FileManagerWrapper wrapper(pManager); + + fwprintf(stdout, expected); + pManager->Stop(); + + auto output = pManager->GetStdOutContent(); + ASSERT_FALSE(output.empty()); + + ASSERT_STREQ(output.c_str(), expected); + } + } + + TEST(FileOutManagerOutputTest, StdErr) + { + PCWSTR expected = L"test"; + + auto tempDirectory = TempDirectory(); + + FileOutputManager* pManager = new FileOutputManager(L"", tempDirectory.path().c_str()); + { + FileManagerWrapper wrapper(pManager); + + fwprintf(stderr, expected); + pManager->Stop(); + + auto output = pManager->GetStdOutContent(); + ASSERT_FALSE(output.empty()); + + ASSERT_STREQ(output.c_str(), expected); + } + } + + TEST(FileOutManagerOutputTest, CapAt30KB) + { + PCWSTR expected = L"hello world"; + + auto tempDirectory = TempDirectory(); + + FileOutputManager* pManager = new FileOutputManager(L"", tempDirectory.path()); + { + FileManagerWrapper wrapper(pManager); + + for (int i = 0; i < 3000; i++) + { + wprintf(expected); + } + pManager->Stop(); + auto output = pManager->GetStdOutContent(); + ASSERT_FALSE(output.empty()); + + ASSERT_EQ(output.size(), 30000); + } + } + + TEST(FileOutManagerOutputTest, StartStopRestoresCorrectly) + { + PCWSTR expected = L"test"; + + auto tempDirectory = TempDirectory(); + + for (int i = 0; i < 10; i++) + { + FileOutputManager* pManager = new FileOutputManager(L"", tempDirectory.path()); + { + FileManagerWrapper wrapper(pManager); + + wprintf(expected); + pManager->Stop(); + auto output = pManager->GetStdOutContent(); + ASSERT_FALSE(output.empty()); + + ASSERT_STREQ(output.c_str(), expected); + } + } + } +} diff --git a/src/IISIntegration/test/CommonLibTests/GlobalVersionTests.cpp b/src/IISIntegration/test/CommonLibTests/GlobalVersionTests.cpp new file mode 100644 index 0000000000..f38c9361d2 --- /dev/null +++ b/src/IISIntegration/test/CommonLibTests/GlobalVersionTests.cpp @@ -0,0 +1,153 @@ +// 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. + +#include "stdafx.h" +#include "gtest/internal/gtest-port.h" + +namespace GlobalVersionTests +{ + using ::testing::Test; + namespace fs = std::filesystem; + + class GlobalVersionTest : public Test + { + protected: + void + RemoveFileNamePath(PCWSTR dllPath, PCWSTR expected) + { + std::wstring res = GlobalVersionUtility::RemoveFileNameFromFolderPath(dllPath); + EXPECT_STREQ(res.c_str(), expected); + } + }; + + TEST_F(GlobalVersionTest, RemovesPathCorrectly) + { + RemoveFileNamePath(L"test\\log.txt", L"test"); + RemoveFileNamePath(L"test\\log", L"test"); + RemoveFileNamePath(L"C:\\Program Files\\IIS\\aspnetcorev2.dll", L"C:\\Program Files\\IIS"); + RemoveFileNamePath(L"test\\log.txt", L"test"); + } + + TEST(GetRequestHandlerVersions, GetFolders) + { + auto tempPath = TempDirectory(); + EXPECT_TRUE(fs::create_directories(tempPath.path() / L"2.0.0")); + + auto res = GlobalVersionUtility::GetRequestHandlerVersions(tempPath.path().c_str()); + EXPECT_EQ(res.size(), 1); + EXPECT_EQ(res.at(0), fx_ver_t(2, 0, 0, std::wstring())); + } + + TEST(GetRequestHandlerVersions, GetFolderPreview) + { + auto tempPath = TempDirectory(); + EXPECT_TRUE(fs::create_directories(tempPath.path() / L"2.0.0-preview")); + + auto res = GlobalVersionUtility::GetRequestHandlerVersions(tempPath.path().c_str()); + EXPECT_EQ(res.size(), 1); + EXPECT_EQ(res.at(0), fx_ver_t(2, 0, 0, std::wstring(L"-preview"))); + } + + TEST(GetRequestHandlerVersions, GetFolderManyVersions) + { + auto tempPath = TempDirectory(); + EXPECT_TRUE(fs::create_directories(tempPath.path() / + L"2.0.0")); + EXPECT_TRUE(fs::create_directories(tempPath.path() / + L"1.9.0")); + EXPECT_TRUE(fs::create_directories(tempPath.path() / + L"2.1.0")); + + auto res = GlobalVersionUtility::GetRequestHandlerVersions(tempPath.path().c_str()); + EXPECT_EQ(res.size(), 3); + EXPECT_TRUE(std::find(res.begin(), res.end(), fx_ver_t(1, 9, 0, std::wstring())) != std::end(res)); + EXPECT_TRUE(std::find(res.begin(), res.end(), fx_ver_t(2, 0, 0, std::wstring())) != std::end(res)); + EXPECT_TRUE(std::find(res.begin(), res.end(), fx_ver_t(2, 1, 0, std::wstring())) != std::end(res)); + } + + TEST(FindHighestGlobalVersion, HighestVersionWithSingleFolder) + { + auto tempPath = TempDirectory(); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.0.0")); + + auto res = GlobalVersionUtility::FindHighestGlobalVersion(tempPath.path().c_str()); + + EXPECT_STREQ(res.c_str(), L"2.0.0"); + } + + TEST(FindHighestGlobalVersion, HighestVersionWithMultipleVersions) + { + auto tempPath = TempDirectory(); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.0.0")); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.1.0")); + + auto res = GlobalVersionUtility::FindHighestGlobalVersion(tempPath.path().c_str()); + + EXPECT_STREQ(res.c_str(), L"2.1.0"); + } + + TEST(FindHighestGlobalVersion, HighestVersionWithMultipleVersionsPreview) + { + auto tempPath = TempDirectory(); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.0.0")); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.1.0")); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.2.0-preview")); + + auto res = GlobalVersionUtility::FindHighestGlobalVersion(tempPath.path().c_str()); + + EXPECT_STREQ(res.c_str(), L"2.2.0-preview"); + } + + TEST(FindHighestGlobalVersion, HighestVersionWithMultipleVersionNoPreview) + { + auto tempPath = TempDirectory(); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.0.0")); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.1.0-preview")); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.1.0")); + + auto res = GlobalVersionUtility::FindHighestGlobalVersion(tempPath.path().c_str()); + + EXPECT_STREQ(res.c_str(), L"2.1.0"); + } + + TEST(GetGlobalRequestHandlerPath, FindHighestVersionNoHandlerName) + { + auto tempPath = TempDirectory(); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.0.0")); + auto result = GlobalVersionUtility::GetGlobalRequestHandlerPath(tempPath.path().c_str(), L"", L"aspnetcorev2_outofprocess.dll"); + + EXPECT_STREQ(result.c_str(), (tempPath.path() / L"2.0.0\\aspnetcorev2_outofprocess.dll").c_str()); + } + + TEST(GetGlobalRequestHandlerPath, FindHighestVersionPreviewWins) + { + auto tempPath = TempDirectory(); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.0.0")); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.1.0-preview")); + + auto result = GlobalVersionUtility::GetGlobalRequestHandlerPath(tempPath.path().c_str(), L"", L"aspnetcorev2_outofprocess.dll"); + + EXPECT_STREQ(result.c_str(), (tempPath.path() / L"2.1.0-preview\\aspnetcorev2_outofprocess.dll").c_str()); + } + + TEST(GetGlobalRequestHandlerPath, FindHighestVersionSpecificVersion) + { + auto tempPath = TempDirectory(); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.0.0")); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.1.0-preview")); + + auto result = GlobalVersionUtility::GetGlobalRequestHandlerPath(tempPath.path().c_str(), L"2.0.0", L"aspnetcorev2_outofprocess.dll"); + + EXPECT_STREQ(result.c_str(), (tempPath.path() / L"2.0.0\\aspnetcorev2_outofprocess.dll").c_str()); + } + + TEST(GetGlobalRequestHandlerPath, FindHighestVersionSpecificPreview) + { + auto tempPath = TempDirectory(); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.0.0")); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.1.0-preview")); + EXPECT_TRUE(fs::create_directories(tempPath.path() / "2.2.0")); + + + auto result = GlobalVersionUtility::GetGlobalRequestHandlerPath(tempPath.path().c_str(), L"2.1.0-preview", L"aspnetcorev2_outofprocess.dll"); + + EXPECT_STREQ(result.c_str(), (tempPath.path() / L"2.1.0-preview\\aspnetcorev2_outofprocess.dll").c_str()); + } +} diff --git a/src/IISIntegration/test/CommonLibTests/Helpers.cpp b/src/IISIntegration/test/CommonLibTests/Helpers.cpp new file mode 100644 index 0000000000..ccca6cad5b --- /dev/null +++ b/src/IISIntegration/test/CommonLibTests/Helpers.cpp @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#include "stdafx.h" + +std::wstring +Helpers::ReadFileContent(std::wstring file) +{ + std::wcout << file << std::endl; + + std::fstream t(file); + std::stringstream buffer; + buffer << t.rdbuf(); + + int nChars = MultiByteToWideChar(CP_ACP, 0, buffer.str().c_str(), -1, NULL, 0); + + std::wstring retVal(nChars, '\0'); + + MultiByteToWideChar(CP_UTF8, 0, buffer.str().c_str(), -1, retVal.data(), nChars); + + return retVal; +} + +TempDirectory::TempDirectory() +{ + UUID uuid; + UuidCreate(&uuid); + RPC_CSTR szUuid = NULL; + if (UuidToStringA(&uuid, &szUuid) == RPC_S_OK) + { + m_path = std::filesystem::temp_directory_path() / reinterpret_cast(szUuid); + RpcStringFreeA(&szUuid); + return; + } + throw std::exception("Cannot create temp directory"); +} + +TempDirectory::~TempDirectory() +{ + std::filesystem::remove_all(m_path); +} diff --git a/src/IISIntegration/test/CommonLibTests/Helpers.h b/src/IISIntegration/test/CommonLibTests/Helpers.h new file mode 100644 index 0000000000..67256966bb --- /dev/null +++ b/src/IISIntegration/test/CommonLibTests/Helpers.h @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once +class Helpers +{ +public: + static + std::wstring + ReadFileContent(std::wstring file); +}; + +class TempDirectory +{ +public: + + TempDirectory(); + + ~TempDirectory(); + + std::filesystem::path path() const + { + return m_path; + } + +private: + std::filesystem::path m_path; +}; diff --git a/src/IISIntegration/test/CommonLibTests/NativeTests.targets b/src/IISIntegration/test/CommonLibTests/NativeTests.targets new file mode 100644 index 0000000000..f3d2caf930 --- /dev/null +++ b/src/IISIntegration/test/CommonLibTests/NativeTests.targets @@ -0,0 +1,8 @@ + + + $(VCIDEInstallDir)..\CommonExtensions\Microsoft\TestWindow\vstest.console.exe + + + + + \ No newline at end of file diff --git a/src/IISIntegration/test/CommonLibTests/PipeOutputManagerTests.cpp b/src/IISIntegration/test/CommonLibTests/PipeOutputManagerTests.cpp new file mode 100644 index 0000000000..385d6df9e0 --- /dev/null +++ b/src/IISIntegration/test/CommonLibTests/PipeOutputManagerTests.cpp @@ -0,0 +1,162 @@ +// 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. + +#include "stdafx.h" +#include "gtest/internal/gtest-port.h" +#include "PipeOutputManager.h" + +class FileManagerWrapper +{ +public: + PipeOutputManager * manager; + FileManagerWrapper(PipeOutputManager* m) + : manager(m) + { + manager->Start(); + } + + ~FileManagerWrapper() + { + delete manager; + } +}; + +namespace PipeOutputManagerTests +{ + TEST(PipeManagerOutputTest, StdOut) + { + PCWSTR expected = L"test"; + + PipeOutputManager* pManager = new PipeOutputManager(true); + + pManager->Start(); + fwprintf(stdout, expected); + pManager->Stop(); + + auto output = pManager->GetStdOutContent(); + ASSERT_STREQ(output.c_str(), expected); + delete pManager; + } + + TEST(PipeManagerOutputTest, StdOutMultiToWide) + { + PipeOutputManager* pManager = new PipeOutputManager(true); + + pManager->Start(); + fprintf(stdout, "test"); + pManager->Stop(); + + auto output = pManager->GetStdOutContent(); + ASSERT_STREQ(output.c_str(), L"test"); + delete pManager; + } + + TEST(PipeManagerOutputTest, StdErr) + { + PCWSTR expected = L"test"; + + PipeOutputManager* pManager = new PipeOutputManager(); + + pManager->Start(); + fwprintf(stderr, expected); + pManager->Stop(); + + auto output = pManager->GetStdOutContent(); + ASSERT_STREQ(output.c_str(), expected); + delete pManager; + } + + TEST(PipeManagerOutputTest, CheckMaxPipeSize) + { + std::wstring test; + for (int i = 0; i < 3000; i++) + { + test.append(L"hello world"); + } + + PipeOutputManager* pManager = new PipeOutputManager(); + + pManager->Start(); + wprintf(test.c_str()); + pManager->Stop(); + + auto output = pManager->GetStdOutContent(); + ASSERT_EQ(output.size(), (DWORD)30000); + delete pManager; + } + + TEST(PipeManagerOutputTest, SetInvalidHandlesForErrAndOut) + { + auto m_fdPreviousStdOut = _dup(_fileno(stdout)); + auto m_fdPreviousStdErr = _dup(_fileno(stderr)); + + SetStdHandle(STD_ERROR_HANDLE, INVALID_HANDLE_VALUE); + SetStdHandle(STD_OUTPUT_HANDLE, INVALID_HANDLE_VALUE); + + PCWSTR expected = L"test"; + + PipeOutputManager* pManager = new PipeOutputManager(); + pManager->Start(); + + _dup2(m_fdPreviousStdOut, _fileno(stdout)); + _dup2(m_fdPreviousStdErr, _fileno(stderr)); + + // Test will fail if we didn't redirect stdout back to a file descriptor. + // This is because gtest relies on console output to know if a test succeeded or failed. + // If the output still points to a file/pipe, the test (and all other tests after it) will fail. + delete pManager; + } + + TEST(PipeManagerOutputTest, CreateDeleteMultipleTimesStdOutWorks) + { + for (int i = 0; i < 10; i++) + { + auto stdoutBefore = _fileno(stdout); + auto stderrBefore = _fileno(stderr); + PCWSTR expected = L"test"; + + PipeOutputManager* pManager = new PipeOutputManager(); + + pManager->Start(); + fwprintf(stdout, expected); + + pManager->Stop(); + + auto output = pManager->GetStdOutContent(); + ASSERT_STREQ(output.c_str(), expected); + ASSERT_EQ(stdoutBefore, _fileno(stdout)); + ASSERT_EQ(stderrBefore, _fileno(stderr)); + delete pManager; + } + // When this returns, we get an AV from gtest. + } + + TEST(PipeManagerOutputTest, CreateDeleteKeepOriginalStdErr) + { + for (int i = 0; i < 10; i++) + { + auto stdoutBefore = _fileno(stdout); + auto stderrBefore = _fileno(stderr); + auto stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); + auto stderrHandle = GetStdHandle(STD_ERROR_HANDLE); + PCWSTR expected = L"test"; + + PipeOutputManager* pManager = new PipeOutputManager(); + + pManager->Start(); + fwprintf(stderr, expected); + pManager->Stop(); + + auto output = pManager->GetStdOutContent(); + ASSERT_STREQ(output.c_str(), expected); + ASSERT_EQ(stdoutBefore, _fileno(stdout)); + + ASSERT_EQ(stderrBefore, _fileno(stderr)); + + delete pManager; + } + + wprintf(L"Hello!"); + } +} + diff --git a/src/IISIntegration/test/CommonLibTests/exception_handler_tests.cpp b/src/IISIntegration/test/CommonLibTests/exception_handler_tests.cpp new file mode 100644 index 0000000000..d0a9e97fdb --- /dev/null +++ b/src/IISIntegration/test/CommonLibTests/exception_handler_tests.cpp @@ -0,0 +1,49 @@ +// 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. + +#include "stdafx.h" + +TEST(CaughtExceptionHResult, ReturnsOutOfMemoryForBadAlloc) +{ + HRESULT hr; + try + { + throw std::bad_alloc(); + } + catch(...) + { + hr = CaughtExceptionHResult(); + } + + EXPECT_EQ(E_OUTOFMEMORY, hr); +} + +TEST(CaughtExceptionHResult, ReturnsValueForSystemError) +{ + HRESULT hr; + try + { + throw std::system_error(E_INVALIDARG, std::system_category()); + } + catch(...) + { + hr = CaughtExceptionHResult(); + } + + EXPECT_EQ(E_INVALIDARG, hr); +} + +TEST(CaughtExceptionHResult, ReturnsUhandledExceptionForOtherExceptions) +{ + HRESULT hr; + try + { + throw E_INVALIDARG; + } + catch(...) + { + hr = CaughtExceptionHResult(); + } + + EXPECT_EQ(HRESULT_FROM_WIN32(ERROR_UNHANDLED_EXCEPTION), hr); +} diff --git a/src/IISIntegration/test/CommonLibTests/fakeclasses.h b/src/IISIntegration/test/CommonLibTests/fakeclasses.h new file mode 100644 index 0000000000..a6f4a3e111 --- /dev/null +++ b/src/IISIntegration/test/CommonLibTests/fakeclasses.h @@ -0,0 +1,190 @@ +// 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. + +#pragma once + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "InProcessOptions.h" + +class MockProperty : public IAppHostProperty +{ +public: + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, QueryInterface, HRESULT(REFIID riid, void ** ppvObject)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, AddRef, ULONG()); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, Release, ULONG()); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_Name, HRESULT(BSTR* pbstrValue)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_Value, HRESULT(VARIANT * pVariant)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, put_Value, HRESULT(VARIANT value)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, Clear, HRESULT()); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_StringValue, HRESULT(BSTR* pbstrValue)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_Exception, HRESULT(IAppHostPropertyException ** ppException)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, GetMetadata, HRESULT(BSTR bstrMetadataType, VARIANT * pValue)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, SetMetadata, HRESULT(BSTR bstrMetadataType, VARIANT value)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_Schema, HRESULT(IAppHostPropertySchema ** ppSchema)); +}; + +class MockCollection : public IAppHostElementCollection +{ +public: + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, QueryInterface, HRESULT(REFIID riid, void ** ppvObject)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, AddRef, ULONG()); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, Release, ULONG()); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, Clear, HRESULT()); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_Schema, HRESULT(IAppHostCollectionSchema** pSchema)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_Count, HRESULT(DWORD * dwordElem)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, get_Item, HRESULT(VARIANT cIndex, IAppHostElement ** ppElement)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, AddElement, HRESULT(IAppHostElement * pElement, INT cPosition)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, DeleteElement, HRESULT(VARIANT cIndex)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, CreateNewElement, HRESULT(BSTR bstrElementName, IAppHostElement** ppElement)); +}; + +class MockElement : public IAppHostElement +{ +public: + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, QueryInterface, HRESULT(REFIID riid, void ** ppvObject)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, AddRef, ULONG()); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, Release, ULONG()); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_Name, HRESULT(BSTR * pbstrName)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_Collection, HRESULT(IAppHostElementCollection ** ppCollection)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_Properties, HRESULT(IAppHostPropertyCollection ** ppProperties)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_ChildElements, HRESULT(IAppHostChildElementCollection ** ppElements)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, GetMetadata, HRESULT(BSTR bstrMetadataType, VARIANT * pValue)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, SetMetadata, HRESULT(BSTR bstrMetadataType, VARIANT value)); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_Schema, HRESULT(IAppHostElementSchema** pSchema)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, GetElementByName, HRESULT(BSTR bstrSubName, IAppHostElement ** ppElement)); + MOCK_METHOD2_WITH_CALLTYPE(__stdcall, GetPropertyByName, HRESULT(BSTR bstrSubName, IAppHostProperty ** ppProperty)); + MOCK_METHOD0_WITH_CALLTYPE(__stdcall, Clear, HRESULT()); + MOCK_METHOD1_WITH_CALLTYPE(__stdcall, get_Methods, HRESULT(IAppHostMethodCollection ** ppMethods)); +}; + +class MockHttpServer : public IHttpServer +{ + // Inherited via IHttpServer + virtual BOOL IsCommandLineLaunch(VOID) const override + { + return 0; + } + virtual PCWSTR GetAppPoolName(VOID) const override + { + return PCWSTR(); + } + virtual HRESULT AssociateWithThreadPool(HANDLE hHandle, LPOVERLAPPED_COMPLETION_ROUTINE completionRoutine) override + { + return E_NOTIMPL; + } + virtual VOID IncrementThreadCount(VOID) override + { + return VOID(); + } + virtual VOID DecrementThreadCount(VOID) override + { + return VOID(); + } + virtual VOID ReportUnhealthy(PCWSTR pszReasonString, HRESULT hrReason) override + { + return VOID(); + } + virtual VOID RecycleProcess(PCWSTR pszReason) override + { + return VOID(); + } + virtual IAppHostAdminManager * GetAdminManager(VOID) const override + { + return nullptr; + } + virtual HRESULT GetFileInfo(PCWSTR pszPhysicalPath, HANDLE hUserToken, PSID pSid, PCWSTR pszChangeNotificationPath, HANDLE hChangeNotificationToken, BOOL fCache, IHttpFileInfo ** ppFileInfo, IHttpTraceContext * pHttpTraceContext = NULL) override + { + return E_NOTIMPL; + } + virtual HRESULT FlushKernelCache(PCWSTR pszUrl) override + { + return E_NOTIMPL; + } + virtual HRESULT DoCacheOperation(CACHE_OPERATION cacheOperation, IHttpCacheKey * pCacheKey, IHttpCacheSpecificData ** ppCacheSpecificData, IHttpTraceContext * pHttpTraceContext = NULL) override + { + return E_NOTIMPL; + } + virtual GLOBAL_NOTIFICATION_STATUS NotifyCustomNotification(ICustomNotificationProvider * pCustomOutput) override + { + return GLOBAL_NOTIFICATION_STATUS(); + } + virtual IHttpPerfCounterInfo * GetPerfCounterInfo(VOID) override + { + return nullptr; + } + virtual VOID RecycleApplication(PCWSTR pszAppConfigPath) override + { + return VOID(); + } + virtual VOID NotifyConfigurationChange(PCWSTR pszPath) override + { + return VOID(); + } + virtual VOID NotifyFileChange(PCWSTR pszFileName) override + { + return VOID(); + } + virtual IDispensedHttpModuleContextContainer * DispenseContainer(VOID) override + { + return nullptr; + } + virtual HRESULT AddFragmentToCache(HTTP_DATA_CHUNK * pDataChunk, PCWSTR pszFragmentName) override + { + return E_NOTIMPL; + } + virtual HRESULT ReadFragmentFromCache(PCWSTR pszFragmentName, BYTE * pvBuffer, DWORD cbSize, DWORD * pcbCopied) override + { + return E_NOTIMPL; + } + virtual HRESULT RemoveFragmentFromCache(PCWSTR pszFragmentName) override + { + return E_NOTIMPL; + } + virtual HRESULT GetWorkerProcessSettings(IWpfSettings ** ppWorkerProcessSettings) override + { + return E_NOTIMPL; + } + virtual HRESULT GetProtocolManagerCustomInterface(PCWSTR pProtocolManagerDll, PCWSTR pProtocolManagerDllInitFunction, DWORD dwCustomInterfaceId, PVOID * ppCustomInterface) override + { + return E_NOTIMPL; + } + virtual BOOL SatisfiesPrecondition(PCWSTR pszPrecondition, BOOL * pfUnknownPrecondition = NULL) const override + { + return 0; + } + virtual IHttpTraceContext * GetTraceContext(VOID) const override + { + return nullptr; + } + virtual HRESULT RegisterFileChangeMonitor(PCWSTR pszPath, HANDLE hToken, IHttpFileMonitor ** ppFileMonitor) override + { + return E_NOTIMPL; + } + virtual HRESULT GetExtendedInterface(HTTP_SERVER_INTERFACE_VERSION version, PVOID * ppInterface) override + { + return E_NOTIMPL; + } +}; + + +class MockHttpApplication: public IHttpApplication +{ +public: + MOCK_CONST_METHOD0(GetApplicationPhysicalPath, PCWSTR ()); + MOCK_CONST_METHOD0(GetApplicationId, PCWSTR ()); + MOCK_CONST_METHOD0(GetAppConfigPath, PCWSTR ()); + MOCK_METHOD0(GetModuleContextContainer, IHttpModuleContextContainer* ()); +}; + +class MockInProcessOptions : public InProcessOptions +{ +public: + static + MockInProcessOptions* + CreateConfig() + { + return new MockInProcessOptions; + } +}; + diff --git a/src/IISIntegration/test/CommonLibTests/hostfxr_utility_tests.cpp b/src/IISIntegration/test/CommonLibTests/hostfxr_utility_tests.cpp new file mode 100644 index 0000000000..01c9541429 --- /dev/null +++ b/src/IISIntegration/test/CommonLibTests/hostfxr_utility_tests.cpp @@ -0,0 +1,122 @@ +// 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. + +#include "stdafx.h" +#include +#include +#include +#include "hostfxr_utility.h" +#include "Environment.h" + +TEST(ParseHostFxrArguments, BasicHostFxrArguments) +{ + std::vector bstrArray; + + HOSTFXR_UTILITY::AppendArguments( + L"exec \"test.dll\"", // args + L"invalid", // physical path to application + bstrArray); // args array. + + EXPECT_EQ(2, bstrArray.size()); + ASSERT_STREQ(L"exec", bstrArray[0].c_str()); + ASSERT_STREQ(L"test.dll", bstrArray[1].c_str()); +} + +TEST(ParseHostFxrArguments, NoExecProvided) +{ + std::vector bstrArray; + + HOSTFXR_UTILITY::AppendArguments( + L"test.dll", // args + L"ignored", // physical path to application + bstrArray); // args array. + + EXPECT_EQ(1, bstrArray.size()); + ASSERT_STREQ(L"test.dll", bstrArray[0].c_str()); +} + +TEST(ParseHostFxrArguments, ConvertDllToAbsolutePath) +{ + std::vector bstrArray; + // we need to use existing dll so let's use ntdll that we know exists everywhere + auto system32 = Environment::ExpandEnvironmentVariables(L"%WINDIR%\\System32"); + HOSTFXR_UTILITY::AppendArguments( + L"exec \"ntdll.dll\"", // args + system32, // physical path to application + bstrArray, // args array. + true); // expandDllPaths + + EXPECT_EQ(2, bstrArray.size()); + ASSERT_STREQ(L"exec", bstrArray[0].c_str()); + ASSERT_STREQ((system32 + L"\\ntdll.dll").c_str(), bstrArray[1].c_str()); +} + +TEST(ParseHostFxrArguments, ProvideNoArgs_InvalidArgs) +{ + std::vector bstrArray; + std::filesystem::path struHostFxrDllLocation; + std::filesystem::path struExeLocation; + + EXPECT_THROW(HOSTFXR_UTILITY::GetHostFxrParameters( + L"dotnet", // processPath + L"some\\path", // application physical path, ignored. + L"", //arguments + struHostFxrDllLocation, + struExeLocation, + bstrArray), // args array. + InvalidOperationException); +} + +TEST(GetAbsolutePathToDotnetFromProgramFiles, BackupWorks) +{ + STRU struAbsolutePathToDotnet; + BOOL fDotnetInProgramFiles; + BOOL is64Bit; + BOOL fIsWow64 = FALSE; + SYSTEM_INFO systemInfo; + IsWow64Process(GetCurrentProcess(), &fIsWow64); + if (fIsWow64) + { + is64Bit = FALSE; + } + else + { + GetNativeSystemInfo(&systemInfo); + is64Bit = systemInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64; + } + + if (is64Bit) + { + fDotnetInProgramFiles = std::filesystem::is_regular_file(L"C:/Program Files/dotnet/dotnet.exe"); + } + else + { + fDotnetInProgramFiles = std::filesystem::is_regular_file(L"C:/Program Files (x86)/dotnet/dotnet.exe"); + } + + auto dotnetPath = HOSTFXR_UTILITY::GetAbsolutePathToDotnetFromProgramFiles(); + if (fDotnetInProgramFiles) + { + EXPECT_TRUE(dotnetPath.has_value()); + } + else + { + EXPECT_FALSE(dotnetPath.has_value()); + } +} + +TEST(GetHostFxrArguments, InvalidParams) +{ + std::vector bstrArray; + std::filesystem::path struHostFxrDllLocation; + std::filesystem::path struExeLocation; + + EXPECT_THROW(HOSTFXR_UTILITY::GetHostFxrParameters( + L"bogus", // processPath + L"", // application physical path, ignored. + L"ignored", //arguments + struHostFxrDllLocation, + struExeLocation, + bstrArray), // args array. + InvalidOperationException); +} diff --git a/src/IISIntegration/test/CommonLibTests/inprocess_application_tests.cpp b/src/IISIntegration/test/CommonLibTests/inprocess_application_tests.cpp new file mode 100644 index 0000000000..d2ec985723 --- /dev/null +++ b/src/IISIntegration/test/CommonLibTests/inprocess_application_tests.cpp @@ -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. + +#include "stdafx.h" + +#include +#include "inprocessapplication.h" +#include "fakeclasses.h" + +using ::testing::_; +using ::testing::NiceMock; + +// Externals defined in inprocess +BOOL g_fProcessDetach; +HANDLE g_hEventLog; + +namespace InprocessTests +{ + TEST(InProcessTest, NoNullRefForExePath) + { + MockHttpServer server; + NiceMock application; + + ON_CALL(application, GetApplicationPhysicalPath()) + .WillByDefault(testing::Return(L"Some path")); + + ON_CALL(application, GetAppConfigPath()) + .WillByDefault(testing::Return(L"")); + + ON_CALL(application, GetApplicationId()) + .WillByDefault(testing::Return(L"")); + + auto requestHandlerConfig = std::unique_ptr(MockInProcessOptions::CreateConfig()); + + std::wstring exePath(L"hello"); + + std::array parameters{ + {"InProcessExeLocation", exePath.data()} + }; + + IN_PROCESS_APPLICATION *app = new IN_PROCESS_APPLICATION(server, application, std::move(requestHandlerConfig), parameters.data(), 1); + + ASSERT_STREQ(app->QueryExeLocation().c_str(), L"hello"); + } + + TEST(InProcessTest, GeneratesVirtualPath) + { + MockHttpServer server; + NiceMock application; + + ON_CALL(application, GetApplicationPhysicalPath()) + .WillByDefault(testing::Return(L"Some path")); + + ON_CALL(application, GetAppConfigPath()) + .WillByDefault(testing::Return(L"SECTION1/SECTION2/SECTION3/SECTION4/SECTION5")); + + ON_CALL(application, GetApplicationId()) + .WillByDefault(testing::Return(L"")); + + auto requestHandlerConfig = std::unique_ptr(MockInProcessOptions::CreateConfig()); + IN_PROCESS_APPLICATION *app = new IN_PROCESS_APPLICATION(server, application, std::move(requestHandlerConfig), nullptr, 0); + + ASSERT_STREQ(app->QueryApplicationVirtualPath().c_str(), L"/SECTION5"); + } + + TEST(InProcessTest, GeneratesVirtualPathForDefaultApp) + { + MockHttpServer server; + NiceMock application; + + ON_CALL(application, GetApplicationPhysicalPath()) + .WillByDefault(testing::Return(L"Some path")); + + ON_CALL(application, GetAppConfigPath()) + .WillByDefault(testing::Return(L"SECTION1/SECTION2/SECTION3/SECTION4")); + + ON_CALL(application, GetApplicationId()) + .WillByDefault(testing::Return(L"")); + + auto requestHandlerConfig = std::unique_ptr(MockInProcessOptions::CreateConfig()); + IN_PROCESS_APPLICATION *app = new IN_PROCESS_APPLICATION(server, application, std::move(requestHandlerConfig), nullptr, 0); + + ASSERT_STREQ(app->QueryApplicationVirtualPath().c_str(), L"/"); + } +} diff --git a/src/IISIntegration/test/CommonLibTests/main.cpp b/src/IISIntegration/test/CommonLibTests/main.cpp new file mode 100644 index 0000000000..1ad0a10ccd --- /dev/null +++ b/src/IISIntegration/test/CommonLibTests/main.cpp @@ -0,0 +1,12 @@ +// 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. + +#include "stdafx.h" + +DECLARE_DEBUG_PRINT_OBJECT2("tests", ASPNETCORE_DEBUG_FLAG_INFO | ASPNETCORE_DEBUG_FLAG_CONSOLE); + +int wmain(int argc, wchar_t* argv[]) +{ + ::testing::InitGoogleTest(&argc, argv); + RUN_ALL_TESTS(); +} diff --git a/src/IISIntegration/test/CommonLibTests/stdafx.h b/src/IISIntegration/test/CommonLibTests/stdafx.h new file mode 100644 index 0000000000..4b9ac7cd27 --- /dev/null +++ b/src/IISIntegration/test/CommonLibTests/stdafx.h @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +#define WIN32_LEAN_AND_MEAN + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include "stringa.h" +#include "stringu.h" +#include "dbgutil.h" +#include "ahutil.h" +#include "multisz.h" +#include "multisza.h" +#include "base64.h" +#include +#include +#include +#include +#include + +#include "stringu.h" +#include "stringa.h" +#include "multisz.h" +#include "dbgutil.h" +#include "hashfn.h" + +#include "requesthandler_config.h" +#include "hostfxr_utility.h" +#include "config_utility.h" +#include "environmentvariablehash.h" +#include "iapplication.h" +#include "debugutil.h" +#include "requesthandler.h" +#include "resources.h" +#include "aspnetcore_msg.h" +#include "Helpers.h" +#include "GlobalVersionUtility.h" + +#undef assert // Macro redefinition in IISLib. +#include "gtest/gtest.h" +#include "fakeclasses.h" + diff --git a/src/IISIntegration/test/CommonLibTests/utility_tests.cpp b/src/IISIntegration/test/CommonLibTests/utility_tests.cpp new file mode 100644 index 0000000000..ee69d79054 --- /dev/null +++ b/src/IISIntegration/test/CommonLibTests/utility_tests.cpp @@ -0,0 +1,75 @@ +// 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. + +#include "stdafx.h" +#include "Environment.h" +#include "StringHelpers.h" + +TEST(PassUnexpandedEnvString, ExpandsResult) +{ + HRESULT hr = S_OK; + PCWSTR unexpandedString = L"ANCM_TEST_ENV_VAR"; + PCWSTR unexpandedStringValue = L"foobar"; + STRU struExpandedString; + SetEnvironmentVariable(L"ANCM_TEST_ENV_VAR", unexpandedStringValue); + + hr = struExpandedString.CopyAndExpandEnvironmentStrings(L"%ANCM_TEST_ENV_VAR%"); + EXPECT_EQ(hr, S_OK); + EXPECT_STREQ(L"foobar", struExpandedString.QueryStr()); +} + +TEST(PassUnexpandedEnvString, LongStringExpandsResults) +{ + HRESULT hr = S_OK; + PCWSTR unexpandedString = L"ANCM_TEST_ENV_VAR_LONG"; + STRU struStringValue; + STACK_STRU(struExpandedString, MAX_PATH); + + struStringValue.Append(L"TestValueThatIsLongerThan256CharactersLongToTriggerResize"); + struStringValue.Append(L"TestValueThatIsLongerThan256CharactersLongToTriggerResize"); + struStringValue.Append(L"TestValueThatIsLongerThan256CharactersLongToTriggerResize"); + struStringValue.Append(L"TestValueThatIsLongerThan256CharactersLongToTriggerResize"); + struStringValue.Append(L"TestValueThatIsLongerThan256CharactersLongToTriggerResize"); + struStringValue.Append(L"TestValueThatIsLongerThan256CharactersLongToTriggerResize"); + + SetEnvironmentVariable(unexpandedString, struStringValue.QueryStr()); + + hr = struExpandedString.CopyAndExpandEnvironmentStrings(L"%ANCM_TEST_ENV_VAR_LONG%"); + EXPECT_EQ(hr, S_OK); + EXPECT_EQ(struStringValue.QueryCCH(), struExpandedString.QueryCCH()); + // The values are exactly the same, however EXPECT_EQ is returning false. + //EXPECT_EQ(struStringValue.QueryStr(), struExpandedString.QueryStr()); + EXPECT_STREQ(struStringValue.QueryStr(), struExpandedString.QueryStr()); +} + + +TEST(GetEnvironmentVariableValue, ReturnsCorrectLenght) +{ + SetEnvironmentVariable(L"RANDOM_ENV_VAR_1", L"test"); + + auto result = Environment::GetEnvironmentVariableValue(L"RANDOM_ENV_VAR_1"); + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value().length(), 4); + EXPECT_STREQ(result.value().c_str(), L"test"); +} + + +TEST(GetEnvironmentVariableValue, ReturnsNulloptWhenNotFound) +{ + auto result = Environment::GetEnvironmentVariableValue(L"RANDOM_ENV_VAR_2"); + EXPECT_FALSE(result.has_value()); +} + +TEST(CheckStringHelpers, FormatWithoutContentDoesNotIncreaseSizeString) +{ + std::string testString = "test"; + auto result = format(testString); + EXPECT_EQ(testString.size(), result.size()); +} + +TEST(CheckStringHelpers, FormatWithoutContentDoesNotIncreaseSizeWstring) +{ + std::wstring testString = L"test"; + auto result = format(testString); + EXPECT_EQ(testString.size(), result.size()); +} diff --git a/src/IISIntegration/test/Directory.Build.props b/src/IISIntegration/test/Directory.Build.props new file mode 100644 index 0000000000..edfd666254 --- /dev/null +++ b/src/IISIntegration/test/Directory.Build.props @@ -0,0 +1,16 @@ + + + + + + netcoreapp2.2 + $(DeveloperBuildTestTfms) + $(StandardTestTfms) + $(StandardTestTfms);net461 + + + + + + diff --git a/src/IISIntegration/test/IIS.BackwardsCompatibility.FunctionalTests/BackwardsCompatibilityTests.cs b/src/IISIntegration/test/IIS.BackwardsCompatibility.FunctionalTests/BackwardsCompatibilityTests.cs new file mode 100644 index 0000000000..2e4a7b1c40 --- /dev/null +++ b/src/IISIntegration/test/IIS.BackwardsCompatibility.FunctionalTests/BackwardsCompatibilityTests.cs @@ -0,0 +1,39 @@ +// 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.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; +using Xunit.Sdk; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class BackwardsCompatibilityTests : FixtureLoggedTest + { + private readonly IISTestSiteFixture _fixture; + + public BackwardsCompatibilityTests(IISTestSiteFixture fixture) : base(fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task CheckBackwardsCompatibilityIsUsed() + { + var response = await _fixture.Client.GetAsync("/HelloWorld"); + var handles = _fixture.DeploymentResult.HostProcess.Modules; + + foreach (ProcessModule handle in handles) + { + if (handle.ModuleName == "aspnetcorev2.dll") + { + Assert.Equal("12.2.18287.0", handle.FileVersionInfo.FileVersion); + return; + } + } + throw new XunitException($"Could not find aspnetcorev2.dll loaded in process {_fixture.DeploymentResult.HostProcess.ProcessName}"); + } + } +} diff --git a/src/IISIntegration/test/IIS.BackwardsCompatibility.FunctionalTests/DeployerSelector.cs b/src/IISIntegration/test/IIS.BackwardsCompatibility.FunctionalTests/DeployerSelector.cs new file mode 100644 index 0000000000..5c6f3739a4 --- /dev/null +++ b/src/IISIntegration/test/IIS.BackwardsCompatibility.FunctionalTests/DeployerSelector.cs @@ -0,0 +1,14 @@ +// 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.Server.IntegrationTesting; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + public static class DeployerSelector + { + public static ServerType ServerType => ServerType.IIS; + public static bool IsBackwardsCompatiblityTest => true; + public static bool IsForwardsCompatibilityTest => false; + } +} diff --git a/src/IISIntegration/test/IIS.BackwardsCompatibility.FunctionalTests/IIS.BackwardsCompatibility.FunctionalTests.csproj b/src/IISIntegration/test/IIS.BackwardsCompatibility.FunctionalTests/IIS.BackwardsCompatibility.FunctionalTests.csproj new file mode 100644 index 0000000000..c819a03ab1 --- /dev/null +++ b/src/IISIntegration/test/IIS.BackwardsCompatibility.FunctionalTests/IIS.BackwardsCompatibility.FunctionalTests.csproj @@ -0,0 +1,46 @@ + + + + netcoreapp2.2 + IISBackwardsCompatibility.FunctionalTests + True + + + + + + + + + + + False + + + False + + + False + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/test/IIS.ForwardsCompatibility.FunctionalTests/DeployerSelector.cs b/src/IISIntegration/test/IIS.ForwardsCompatibility.FunctionalTests/DeployerSelector.cs new file mode 100644 index 0000000000..bd7aabbe0f --- /dev/null +++ b/src/IISIntegration/test/IIS.ForwardsCompatibility.FunctionalTests/DeployerSelector.cs @@ -0,0 +1,14 @@ +// 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.Server.IntegrationTesting; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + public static class DeployerSelector + { + public static ServerType ServerType => ServerType.IIS; + public static bool IsBackwardsCompatiblityTest => false; + public static bool IsForwardsCompatibilityTest => true; + } +} diff --git a/src/IISIntegration/test/IIS.ForwardsCompatibility.FunctionalTests/ForwardsCompatibilityTests.cs b/src/IISIntegration/test/IIS.ForwardsCompatibility.FunctionalTests/ForwardsCompatibilityTests.cs new file mode 100644 index 0000000000..5f4ebb5608 --- /dev/null +++ b/src/IISIntegration/test/IIS.ForwardsCompatibility.FunctionalTests/ForwardsCompatibilityTests.cs @@ -0,0 +1,38 @@ +// 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.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; +using Xunit.Sdk; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + public class ForwardsCompatibilityTests : FixtureLoggedTest + { + private readonly IISTestSiteFixture _fixture; + + public ForwardsCompatibilityTests(IISTestSiteFixture fixture) : base(fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task CheckForwardsCompatibilityIsUsed() + { + var response = await _fixture.Client.GetAsync("/HelloWorld"); + var handles = _fixture.DeploymentResult.HostProcess.Modules; + foreach (ProcessModule handle in handles) + { + if (handle.ModuleName == "aspnetcorev2_inprocess.dll") + { + Assert.Equal("12.2.18287.0", handle.FileVersionInfo.FileVersion); + return; + } + } + throw new XunitException($"Could not find aspnetcorev2_inprocess.dll loaded in process {_fixture.DeploymentResult.HostProcess.ProcessName}"); + } + } +} diff --git a/src/IISIntegration/test/IIS.ForwardsCompatibility.FunctionalTests/IIS.ForwardsCompatibility.FunctionalTests.csproj b/src/IISIntegration/test/IIS.ForwardsCompatibility.FunctionalTests/IIS.ForwardsCompatibility.FunctionalTests.csproj new file mode 100644 index 0000000000..929f6ec6b0 --- /dev/null +++ b/src/IISIntegration/test/IIS.ForwardsCompatibility.FunctionalTests/IIS.ForwardsCompatibility.FunctionalTests.csproj @@ -0,0 +1,45 @@ + + + + netcoreapp2.2 + IISForwardsCompatibility.FunctionalTests + True + + + + + + + + + + False + + + False + + + False + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/test/IIS.FunctionalTests/DeployerSelector.cs b/src/IISIntegration/test/IIS.FunctionalTests/DeployerSelector.cs new file mode 100644 index 0000000000..f2e1be321e --- /dev/null +++ b/src/IISIntegration/test/IIS.FunctionalTests/DeployerSelector.cs @@ -0,0 +1,14 @@ +// 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.Server.IntegrationTesting; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + public static class DeployerSelector + { + public static ServerType ServerType => ServerType.IIS; + public static bool IsBackwardsCompatiblityTest => false; + public static bool IsForwardsCompatibilityTest => false; + } +} diff --git a/src/IISIntegration/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj b/src/IISIntegration/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj new file mode 100644 index 0000000000..62dec62e60 --- /dev/null +++ b/src/IISIntegration/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj @@ -0,0 +1,46 @@ + + + + netcoreapp2.2 + IIS.FunctionalTests + True + + + + + + + + + + + False + + + False + + + False + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/test/IIS.Shared.FunctionalTests/Inprocess/StdOutRedirectionTests.cs b/src/IISIntegration/test/IIS.Shared.FunctionalTests/Inprocess/StdOutRedirectionTests.cs new file mode 100644 index 0000000000..35f10b13ab --- /dev/null +++ b/src/IISIntegration/test/IIS.Shared.FunctionalTests/Inprocess/StdOutRedirectionTests.cs @@ -0,0 +1,150 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.AspNetCore.Testing.xunit; +using Newtonsoft.Json; +using Xunit; + +namespace IIS.FunctionalTests.Inprocess +{ + [Collection(PublishedSitesCollection.Name)] + public class StdOutRedirectionTests : LogFileTestBase + { + private readonly PublishedSitesFixture _fixture; + + public StdOutRedirectionTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + [SkipIfDebug] + public async Task FrameworkNotFoundExceptionLogged_Pipe() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + + var deploymentResult = await DeployAsync(deploymentParameters); + + Helpers.ModifyFrameworkVersionInRuntimeConfig(deploymentResult); + + var response = await deploymentResult.HttpClient.GetAsync("/HelloWorld"); + Assert.False(response.IsSuccessStatusCode); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvent(deploymentResult, + "The specified framework 'Microsoft.NETCore.App', version '2.9.9' was not found."); + } + + [ConditionalFact] + [SkipIfDebug] + public async Task FrameworkNotFoundExceptionLogged_File() + { + var deploymentParameters = + _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + + deploymentParameters.EnableLogging(_logFolderPath); + + var deploymentResult = await DeployAsync(deploymentParameters); + + Helpers.ModifyFrameworkVersionInRuntimeConfig(deploymentResult); + + var response = await deploymentResult.HttpClient.GetAsync("/HelloWorld"); + Assert.False(response.IsSuccessStatusCode); + + StopServer(); + + var contents = Helpers.ReadAllTextFromFile(Helpers.GetExpectedLogName(deploymentResult, _logFolderPath), Logger); + var expectedString = "The specified framework 'Microsoft.NETCore.App', version '2.9.9' was not found."; + EventLogHelpers.VerifyEventLogEvent(deploymentResult, expectedString); + Assert.Contains(expectedString, contents); + } + + [ConditionalFact] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + [SkipIfDebug] + public async Task EnableCoreHostTraceLogging_TwoLogFilesCreated() + { + var deploymentParameters = + _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} CheckLargeStdOutWrites"); + + deploymentParameters.EnvironmentVariables["COREHOST_TRACE"] = "1"; + + deploymentParameters.EnableLogging(_logFolderPath); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("/HelloWorld"); + Assert.False(response.IsSuccessStatusCode); + + StopServer(); + + var fileInDirectory = Directory.GetFiles(_logFolderPath).Single(); + var contents = Helpers.ReadAllTextFromFile(fileInDirectory, Logger); + EventLogHelpers.VerifyEventLogEvent(deploymentResult, "Invoked hostfxr"); + Assert.Contains("Invoked hostfxr", contents); + } + + [ConditionalTheory] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + [SkipIfDebug] + [InlineData("CheckLargeStdErrWrites")] + [InlineData("CheckLargeStdOutWrites")] + [InlineData("CheckOversizedStdErrWrites")] + [InlineData("CheckOversizedStdOutWrites")] + public async Task EnableCoreHostTraceLogging_PipeCaptureNativeLogs(string path) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.EnvironmentVariables["COREHOST_TRACE"] = "1"; + deploymentParameters.TransformArguments((a, _) => $"{a} {path}"); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("/HelloWorld"); + + Assert.False(response.IsSuccessStatusCode); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvent(deploymentResult, "Invoked hostfxr"); + } + + [ConditionalTheory] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + [SkipIfDebug] + [InlineData("CheckLargeStdErrWrites")] + [InlineData("CheckLargeStdOutWrites")] + [InlineData("CheckOversizedStdErrWrites")] + [InlineData("CheckOversizedStdOutWrites")] + public async Task EnableCoreHostTraceLogging_FileCaptureNativeLogs(string path) + { + var deploymentParameters = + _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.EnvironmentVariables["COREHOST_TRACE"] = "1"; + deploymentParameters.TransformArguments((a, _) => $"{a} {path}"); + + deploymentParameters.EnableLogging(_logFolderPath); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("/HelloWorld"); + Assert.False(response.IsSuccessStatusCode); + + StopServer(); + + var fileInDirectory = Directory.GetFiles(_logFolderPath).First(); + var contents = Helpers.ReadAllTextFromFile(fileInDirectory, Logger); + + EventLogHelpers.VerifyEventLogEvent(deploymentResult, "Invoked hostfxr"); + Assert.Contains("Invoked hostfxr", contents); + } + } +} diff --git a/src/IISIntegration/test/IIS.Shared.FunctionalTests/MofFileTests.cs b/src/IISIntegration/test/IIS.Shared.FunctionalTests/MofFileTests.cs new file mode 100644 index 0000000000..a054225ac5 --- /dev/null +++ b/src/IISIntegration/test/IIS.Shared.FunctionalTests/MofFileTests.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics; +using System.IO; +using Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests; +using Microsoft.AspNetCore.Testing; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace IIS.FunctionalTests +{ + public class MofFileTests + { + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)] + [RequiresIIS(IISCapability.TracingModule)] + public void CheckMofFile() + { + var path = Path.Combine(TestPathUtilities.GetSolutionRootDirectory("IISIntegration"), "src", "aspnetcoremodulev2", "aspnetcore", "ancm.mof"); + var process = Process.Start("mofcomp.exe", path); + process.WaitForExit(); + Assert.Equal(0, process.ExitCode); + } + } +} diff --git a/src/IISIntegration/test/IIS.Shared.FunctionalTests/Properties/AssemblyInfo.cs b/src/IISIntegration/test/IIS.Shared.FunctionalTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..b26f48a815 --- /dev/null +++ b/src/IISIntegration/test/IIS.Shared.FunctionalTests/Properties/AssemblyInfo.cs @@ -0,0 +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 Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] +[assembly: RequiresIIS] +[assembly: ShortClassName] diff --git a/src/IISIntegration/test/IIS.Shared.FunctionalTests/RequiresIISAttribute.cs b/src/IISIntegration/test/IIS.Shared.FunctionalTests/RequiresIISAttribute.cs new file mode 100644 index 0000000000..cfbbf70486 --- /dev/null +++ b/src/IISIntegration/test/IIS.Shared.FunctionalTests/RequiresIISAttribute.cs @@ -0,0 +1,150 @@ +// 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.Runtime.InteropServices; +using System.Security.Principal; +using System.Xml.Linq; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Win32; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] + public sealed class RequiresIISAttribute : Attribute, ITestCondition + { + private static readonly (IISCapability Capability, string DllName)[] Modules = + { + (IISCapability.Websockets, "iiswsock.dll"), + (IISCapability.WindowsAuthentication, "authsspi.dll"), + (IISCapability.DynamicCompression, "compdyn.dll"), + (IISCapability.ApplicationInitialization, "warmup.dll"), + (IISCapability.TracingModule, "iisetw.dll"), + (IISCapability.FailedRequestTracingModule, "iisfreb.dll"), + (IISCapability.BasicAuthentication, "authbas.dll"), + }; + + private static readonly bool _isMetStatic; + private static readonly string _skipReasonStatic; + private static readonly bool _poolEnvironmentVariablesAvailable; + private static readonly IISCapability _modulesAvailable; + + static RequiresIISAttribute() + { + if (Environment.GetEnvironmentVariable("ASPNETCORE_TEST_SKIP_IIS") == "true") + { + _skipReasonStatic = "Test skipped using ASPNETCORE_TEST_SKIP_IIS environment variable"; + return; + } + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _skipReasonStatic = "IIS tests can only be run on Windows"; + return; + } + + var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + if (!principal.IsInRole(WindowsBuiltInRole.Administrator)) + { + _skipReasonStatic += "The current console is not running as admin."; + return; + } + + if (!File.Exists(Path.Combine(Environment.SystemDirectory, "inetsrv", "w3wp.exe"))) + { + _skipReasonStatic += "The machine does not have IIS installed."; + return; + } + + var ancmConfigPath = Path.Combine(Environment.SystemDirectory, "inetsrv", "config", "schema", "aspnetcore_schema_v2.xml"); + + if (!File.Exists(ancmConfigPath)) + { + _skipReasonStatic = "IIS Schema is not installed."; + return; + } + + XDocument ancmConfig; + + try + { + ancmConfig = XDocument.Load(ancmConfigPath); + } + catch + { + _skipReasonStatic = "Could not read ANCM schema configuration"; + return; + } + + _isMetStatic = ancmConfig + .Root + .Descendants("attribute") + .Any(n => "hostingModel".Equals(n.Attribute("name")?.Value, StringComparison.Ordinal)); + + _skipReasonStatic = _isMetStatic ? null : "IIS schema needs to be upgraded to support ANCM."; + + foreach (var module in Modules) + { + if (File.Exists(Path.Combine(Environment.SystemDirectory, "inetsrv", module.DllName))) + { + _modulesAvailable |= module.Capability; + } + } + + var iisRegistryKey = Registry.LocalMachine.OpenSubKey(@"Software\Microsoft\InetStp", writable: false); + if (iisRegistryKey == null) + { + _poolEnvironmentVariablesAvailable = false; + } + else + { + var majorVersion = (int)iisRegistryKey.GetValue("MajorVersion", -1); + var minorVersion = (int)iisRegistryKey.GetValue("MinorVersion", -1); + var version = new Version(majorVersion, minorVersion); + _poolEnvironmentVariablesAvailable = version >= new Version(10, 0); + } + } + + public RequiresIISAttribute() + : this(IISCapability.None) { } + + public RequiresIISAttribute(IISCapability capabilities) + { + IsMet = _isMetStatic; + SkipReason = _skipReasonStatic; + if (capabilities.HasFlag(IISCapability.PoolEnvironmentVariables)) + { + IsMet &= _poolEnvironmentVariablesAvailable; + if (!_poolEnvironmentVariablesAvailable) + { + 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"; + } + + foreach (var module in Modules) + { + if (capabilities.HasFlag(module.Capability)) + { + var available = _modulesAvailable.HasFlag(module.Capability); + IsMet &= available; + if (!available) + { + SkipReason += $"The machine does have {module.Capability} available."; + } + } + } + } + + public bool IsMet { get; } + public string SkipReason { get; } + } +} diff --git a/src/IISIntegration/test/IIS.Shared.FunctionalTests/ServicesTests.cs b/src/IISIntegration/test/IIS.Shared.FunctionalTests/ServicesTests.cs new file mode 100644 index 0000000000..875b5b13be --- /dev/null +++ b/src/IISIntegration/test/IIS.Shared.FunctionalTests/ServicesTests.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.IO; +using System.ServiceProcess; +using System.Threading; +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.IIS; +using Microsoft.AspNetCore.Testing; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace IIS.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class ApplicationInitializationTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public ApplicationInitializationTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalTheory] + [RequiresIIS(IISCapability.ApplicationInitialization)] + [InlineData(HostingModel.InProcess)] + [InlineData(HostingModel.OutOfProcess)] + public async Task ApplicationPreloadStartsApp(HostingModel hostingModel) + { + // This test often hits a memory leak in warmup.dll module, it has been reported to IIS team + using (AppVerifier.Disable(DeployerSelector.ServerType, 0x900)) + { + var baseDeploymentParameters = _fixture.GetBaseDeploymentParameters(hostingModel, publish: true); + baseDeploymentParameters.TransformArguments( + (args, contentRoot) => $"{args} CreateFile \"{Path.Combine(contentRoot, "Started.txt")}\""); + EnablePreload(baseDeploymentParameters); + + var result = await DeployAsync(baseDeploymentParameters); + + await Helpers.Retry(async () => await File.ReadAllTextAsync(Path.Combine(result.ContentRoot, "Started.txt")), 10, 200); + StopServer(); + EventLogHelpers.VerifyEventLogEvent(result, EventLogHelpers.Started(result)); + } + } + + [ConditionalTheory] + [RequiresIIS(IISCapability.ApplicationInitialization)] + [InlineData(HostingModel.InProcess)] + [InlineData(HostingModel.OutOfProcess)] + public async Task ApplicationInitializationPageIsRequested(HostingModel hostingModel) + { + // This test often hits a memory leak in warmup.dll module, it has been reported to IIS team + using (AppVerifier.Disable(DeployerSelector.ServerType, 0x900)) + { + var baseDeploymentParameters = _fixture.GetBaseDeploymentParameters(hostingModel, publish: true); + EnablePreload(baseDeploymentParameters); + + baseDeploymentParameters.ServerConfigActionList.Add( + (config, _) => { + config + .RequiredElement("system.webServer") + .GetOrAdd("applicationInitialization") + .GetOrAdd("add", "initializationPage", "/CreateFile"); + }); + + var result = await DeployAsync(baseDeploymentParameters); + + await Helpers.Retry(async () => await File.ReadAllTextAsync(Path.Combine(result.ContentRoot, "Started.txt")), 10, 200); + StopServer(); + EventLogHelpers.VerifyEventLogEvent(result, EventLogHelpers.Started(result)); + } + } + + private static void EnablePreload(IISDeploymentParameters baseDeploymentParameters) + { + baseDeploymentParameters.EnsureSection("applicationInitialization", "system.webServer"); + baseDeploymentParameters.ServerConfigActionList.Add( + (config, _) => { + + config + .RequiredElement("system.applicationHost") + .RequiredElement("sites") + .RequiredElement("site") + .RequiredElement("application") + .SetAttributeValue("preloadEnabled", true); + }); + + baseDeploymentParameters.EnableModule("ApplicationInitializationModule", "%IIS_BIN%\\warmup.dll"); + } + } +} diff --git a/src/IISIntegration/test/IIS.Tests/AppHostConfig/HostableWebCore.config b/src/IISIntegration/test/IIS.Tests/AppHostConfig/HostableWebCore.config new file mode 100644 index 0000000000..5e994d2855 --- /dev/null +++ b/src/IISIntegration/test/IIS.Tests/AppHostConfig/HostableWebCore.config @@ -0,0 +1,198 @@ + + + + + + + + +
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+ + +
+
+
+
+ +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/test/IIS.Tests/ClientDisconnectTests.cs b/src/IISIntegration/test/IIS.Tests/ClientDisconnectTests.cs new file mode 100644 index 0000000000..dbe562aa7c --- /dev/null +++ b/src/IISIntegration/test/IIS.Tests/ClientDisconnectTests.cs @@ -0,0 +1,302 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [SkipIfHostableWebCoreNotAvailable] + [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, "https://github.com/aspnet/IISIntegration/issues/866")] + public class ClientDisconnectTests : StrictTestServerTests + { + [ConditionalFact] + public async Task WritesSucceedAfterClientDisconnect() + { + var requestStartedCompletionSource = CreateTaskCompletionSource(); + var clientDisconnectedCompletionSource = CreateTaskCompletionSource(); + var requestCompletedCompletionSource = CreateTaskCompletionSource(); + + var data = new byte[1024]; + using (var testServer = await TestServer.Create( + async ctx => + { + requestStartedCompletionSource.SetResult(true); + await clientDisconnectedCompletionSource.Task; + for (var i = 0; i < 1000; i++) + { + await ctx.Response.Body.WriteAsync(data); + } + + requestCompletedCompletionSource.SetResult(true); + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + await requestStartedCompletionSource.Task.DefaultTimeout(); + } + clientDisconnectedCompletionSource.SetResult(true); + + await requestCompletedCompletionSource.Task.DefaultTimeout(); + } + + AssertConnectionDisconnectLog(); + } + + [ConditionalFact] + public async Task WritesCancelledWhenUsingAbortedToken() + { + var requestStartedCompletionSource = CreateTaskCompletionSource(); + var requestCompletedCompletionSource = CreateTaskCompletionSource(); + + Exception exception = null; + + var data = new byte[1]; + using (var testServer = await TestServer.Create(async ctx => + { + requestStartedCompletionSource.SetResult(true); + try + { + while (true) + { + await ctx.Response.Body.WriteAsync(data, ctx.RequestAborted); + } + } + catch (Exception e) + { + exception = e; + } + + requestCompletedCompletionSource.SetResult(true); + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + + await requestStartedCompletionSource.Task.DefaultTimeout(); + } + + await requestCompletedCompletionSource.Task.DefaultTimeout(); + + Assert.IsType(exception); + } + + AssertConnectionDisconnectLog(); + } + + [ConditionalFact] + public async Task ReadThrowsAfterClientDisconnect() + { + var requestStartedCompletionSource = CreateTaskCompletionSource(); + var requestCompletedCompletionSource = CreateTaskCompletionSource(); + + Exception exception = null; + + var data = new byte[1024]; + using (var testServer = await TestServer.Create(async ctx => + { + requestStartedCompletionSource.SetResult(true); + try + { + await ctx.Request.Body.ReadAsync(data); + } + catch (Exception e) + { + exception = e; + } + + requestCompletedCompletionSource.SetResult(true); + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + await requestStartedCompletionSource.Task.DefaultTimeout(); + } + + await requestCompletedCompletionSource.Task.DefaultTimeout(); + } + + Assert.IsType(exception); + Assert.Equal("The client has disconnected", exception.Message); + + AssertConnectionDisconnectLog(); + } + + [ConditionalFact] + public async Task WriterThrowsCancelledException() + { + var requestStartedCompletionSource = CreateTaskCompletionSource(); + var requestCompletedCompletionSource = CreateTaskCompletionSource(); + + Exception exception = null; + var cancellationTokenSource = new CancellationTokenSource(); + + var data = new byte[1]; + using (var testServer = await TestServer.Create(async ctx => + { + requestStartedCompletionSource.SetResult(true); + try + { + while (true) + { + await ctx.Response.Body.WriteAsync(data, cancellationTokenSource.Token); + } + } + catch (Exception e) + { + exception = e; + } + + requestCompletedCompletionSource.SetResult(true); + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + + await requestStartedCompletionSource.Task.DefaultTimeout(); + cancellationTokenSource.Cancel(); + await requestCompletedCompletionSource.Task.DefaultTimeout(); + } + + Assert.IsType(exception); + } + } + + [ConditionalFact] + public async Task ReaderThrowsCancelledException() + { + var requestStartedCompletionSource = CreateTaskCompletionSource(); + var requestCompletedCompletionSource = CreateTaskCompletionSource(); + + Exception exception = null; + var cancellationTokenSource = new CancellationTokenSource(); + + var data = new byte[1024]; + using (var testServer = await TestServer.Create(async ctx => + { + requestStartedCompletionSource.SetResult(true); + try + { + await ctx.Request.Body.ReadAsync(data, cancellationTokenSource.Token); + } + catch (Exception e) + { + exception = e; + } + + requestCompletedCompletionSource.SetResult(true); + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + await requestStartedCompletionSource.Task.DefaultTimeout(); + cancellationTokenSource.Cancel(); + await requestCompletedCompletionSource.Task.DefaultTimeout(); + } + Assert.IsType(exception); + } + } + + [ConditionalFact] + public async Task ReaderThrowsResetExceptionOnInvalidBody() + { + var requestStartedCompletionSource = CreateTaskCompletionSource(); + var requestCompletedCompletionSource = CreateTaskCompletionSource(); + + Exception exception = null; + + var data = new byte[1024]; + using (var testServer = await TestServer.Create(async ctx => + { + requestStartedCompletionSource.SetResult(true); + try + { + await ctx.Request.Body.ReadAsync(data); + } + catch (Exception e) + { + exception = e; + } + + requestCompletedCompletionSource.SetResult(true); + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Transfer-Encoding: chunked", + "Host: localhost", + "Connection: close", + "", + ""); + + await requestStartedCompletionSource.Task; + await connection.Send( + "ZZZZZZZZZZZZZ"); + + await connection.Receive( + "HTTP/1.1 400 Bad Request", + "" + ); + + } + await requestCompletedCompletionSource.Task.DefaultTimeout(); + } + + Assert.IsType(exception); + Assert.Equal("The client has disconnected", exception.Message); + AssertConnectionDisconnectLog(); + } + + [ConditionalFact] + public async Task RequestAbortedIsTrippedWithoutIO() + { + var requestStarted = CreateTaskCompletionSource(); + var requestAborted = CreateTaskCompletionSource(); + + using (var testServer = await TestServer.Create( + async ctx => { + ctx.RequestAborted.Register(() => requestAborted.SetResult(true)); + requestStarted.SetResult(true); + await requestAborted.Task; + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + await requestStarted.Task; + } + await requestAborted.Task; + } + + AssertConnectionDisconnectLog(); + } + + private void AssertConnectionDisconnectLog() + { + Assert.Single(TestSink.Writes, w => w.EventId.Name == "ConnectionDisconnect"); + } + + private static async Task SendContentLength1Post(TestConnection connection) + { + await connection.Send( + "POST / HTTP/1.1", + "Content-Length: 1", + "Host: localhost", + "Connection: close", + "", + ""); + } + } +} diff --git a/src/IISIntegration/test/IIS.Tests/ConnectionIdFeatureTests.cs b/src/IISIntegration/test/IIS.Tests/ConnectionIdFeatureTests.cs new file mode 100644 index 0000000000..37e69b3a32 --- /dev/null +++ b/src/IISIntegration/test/IIS.Tests/ConnectionIdFeatureTests.cs @@ -0,0 +1,54 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [SkipIfHostableWebCoreNotAvailable] + [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, "https://github.com/aspnet/IISIntegration/issues/866")] + public class HttpBodyControlFeatureTests : StrictTestServerTests + { + [ConditionalFact] + public async Task ThrowsOnSyncReadOrWrite() + { + Exception writeException = null; + Exception readException = null; + using (var testServer = await TestServer.Create( + ctx => { + var bodyControl = ctx.Features.Get(); + bodyControl.AllowSynchronousIO = false; + + try + { + ctx.Response.Body.Write(new byte[10]); + } + catch (Exception ex) + { + writeException = ex; + } + + try + { + ctx.Request.Body.Read(new byte[10]); + } + catch (Exception ex) + { + readException = ex; + } + + return Task.CompletedTask; + }, LoggerFactory)) + { + await testServer.HttpClient.GetStringAsync("/"); + } + + Assert.IsType(readException); + Assert.IsType(writeException); + } + } +} diff --git a/src/IISIntegration/test/IIS.Tests/HttpBodyControlFeatureTests.cs b/src/IISIntegration/test/IIS.Tests/HttpBodyControlFeatureTests.cs new file mode 100644 index 0000000000..3d91c445ca --- /dev/null +++ b/src/IISIntegration/test/IIS.Tests/HttpBodyControlFeatureTests.cs @@ -0,0 +1,31 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [SkipIfHostableWebCoreNotAvailable] + [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, "https://github.com/aspnet/IISIntegration/issues/866")] + public class ConnectionIdFeatureTests : StrictTestServerTests + { + [ConditionalFact] + public async Task ProvidesConnectionId() + { + string connectionId = null; + using (var testServer = await TestServer.Create(ctx => { + var connectionIdFeature = ctx.Features.Get(); + connectionId = connectionIdFeature.ConnectionId; + return Task.CompletedTask; + }, LoggerFactory)) + { + await testServer.HttpClient.GetStringAsync("/"); + } + + Assert.NotNull(connectionId); + } + } +} diff --git a/src/IISIntegration/test/IIS.Tests/IIS.Tests.csproj b/src/IISIntegration/test/IIS.Tests/IIS.Tests.csproj new file mode 100644 index 0000000000..3fdb2a5363 --- /dev/null +++ b/src/IISIntegration/test/IIS.Tests/IIS.Tests.csproj @@ -0,0 +1,32 @@ + + + + netcoreapp2.2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/test/IIS.Tests/ResponseAbortTests.cs b/src/IISIntegration/test/IIS.Tests/ResponseAbortTests.cs new file mode 100644 index 0000000000..c7ec472586 --- /dev/null +++ b/src/IISIntegration/test/IIS.Tests/ResponseAbortTests.cs @@ -0,0 +1,151 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [SkipIfHostableWebCoreNotAvailable] + [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, "https://github.com/aspnet/IISIntegration/issues/866")] + public class ResponseAbortTests : StrictTestServerTests + { + [ConditionalFact] + public async Task ClosesWithoutSendingAnything() + { + using (var testServer = await TestServer.Create( + ctx => { + ctx.Abort(); + return Task.CompletedTask; + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + await connection.WaitForConnectionClose(); + } + } + } + + [ConditionalFact] + public async Task ClosesAfterDataSent() + { + var bodyReceived = CreateTaskCompletionSource(); + using (var testServer = await TestServer.Create( + async ctx => { + await ctx.Response.WriteAsync("Abort"); + await ctx.Response.Body.FlushAsync(); + await bodyReceived.Task.DefaultTimeout(); + ctx.Abort(); + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + await connection.Receive( + "HTTP/1.1 200 OK", + ""); + await connection.ReceiveHeaders( + "Transfer-Encoding: chunked"); + + await connection.ReceiveChunk("Abort"); + bodyReceived.SetResult(true); + await connection.WaitForConnectionClose(); + } + } + } + + [ConditionalFact] + public async Task ReadsThrowAfterAbort() + { + Exception exception = null; + + using (var testServer = await TestServer.Create( + async ctx => { + ctx.Abort(); + try + { + var a = new byte[10]; + await ctx.Request.Body.ReadAsync(a); + } + catch (Exception e) + { + exception = e; + } + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + await connection.WaitForConnectionClose(); + } + } + + Assert.IsType(exception); + } + + [ConditionalFact] + public async Task WritesNoopAfterAbort() + { + Exception exception = null; + + using (var testServer = await TestServer.Create( + async ctx => { + ctx.Abort(); + try + { + await ctx.Response.Body.WriteAsync(new byte[10]); + } + catch (Exception e) + { + exception = e; + } + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + await connection.WaitForConnectionClose(); + } + } + + Assert.Null(exception); + } + + [ConditionalFact] + public async Task RequestAbortedIsTrippedAfterAbort() + { + bool tokenAborted = false; + using (var testServer = await TestServer.Create( + ctx => { + ctx.Abort(); + tokenAborted = ctx.RequestAborted.IsCancellationRequested; + return Task.CompletedTask; + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + await connection.WaitForConnectionClose(); + } + } + + Assert.True(tokenAborted); + } + + private static async Task SendContentLength1Post(TestConnection connection) + { + await connection.Send( + "POST / HTTP/1.1", + "Content-Length: 1", + "Host: localhost", + "", + ""); + } + } +} diff --git a/src/IISIntegration/test/IIS.Tests/StrictTestServerTests.cs b/src/IISIntegration/test/IIS.Tests/StrictTestServerTests.cs new file mode 100644 index 0000000000..c8f62beedb --- /dev/null +++ b/src/IISIntegration/test/IIS.Tests/StrictTestServerTests.cs @@ -0,0 +1,24 @@ +// 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.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + public class StrictTestServerTests: LoggedTest + { + public override void Dispose() + { + base.Dispose(); + Assert.DoesNotContain(TestSink.Writes, w => w.LogLevel > LogLevel.Information); + } + + protected static TaskCompletionSource CreateTaskCompletionSource() + { + return new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + } + } +} diff --git a/src/IISIntegration/test/IIS.Tests/TestServerTest.cs b/src/IISIntegration/test/IIS.Tests/TestServerTest.cs new file mode 100644 index 0000000000..40b538ea52 --- /dev/null +++ b/src/IISIntegration/test/IIS.Tests/TestServerTest.cs @@ -0,0 +1,35 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [SkipIfHostableWebCoreNotAvailable] + [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, "https://github.com/aspnet/IISIntegration/issues/866")] + public class TestServerTest : StrictTestServerTests + { + [ConditionalFact] + public async Task SingleProcessTestServer_HelloWorld() + { + var helloWorld = "Hello World"; + var expectedPath = "/Path"; + + string path = null; + using (var testServer = await TestServer.Create(ctx => + { + path = ctx.Request.Path.ToString(); + return ctx.Response.WriteAsync(helloWorld); + }, LoggerFactory)) + { + var result = await testServer.HttpClient.GetAsync(expectedPath); + Assert.Equal(helloWorld, await result.Content.ReadAsStringAsync()); + Assert.Equal(expectedPath, path); + } + } + } +} diff --git a/src/IISIntegration/test/IIS.Tests/Utilities/SkipIfHostableWebCoreNotAvailibleAttribute.cs b/src/IISIntegration/test/IIS.Tests/Utilities/SkipIfHostableWebCoreNotAvailibleAttribute.cs new file mode 100644 index 0000000000..b63743f106 --- /dev/null +++ b/src/IISIntegration/test/IIS.Tests/Utilities/SkipIfHostableWebCoreNotAvailibleAttribute.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using Microsoft.AspNetCore.Testing.xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] + public sealed class SkipIfHostableWebCoreNotAvailableAttribute : Attribute, ITestCondition + { + public bool IsMet { get; } = File.Exists(TestServer.HostableWebCoreLocation); + + public string SkipReason { get; } = $"Hostable Web Core not available, {TestServer.HostableWebCoreLocation} not found."; + } +} diff --git a/src/IISIntegration/test/IIS.Tests/Utilities/TestServer.cs b/src/IISIntegration/test/IIS.Tests/Utilities/TestServer.cs new file mode 100644 index 0000000000..351b826316 --- /dev/null +++ b/src/IISIntegration/test/IIS.Tests/Utilities/TestServer.cs @@ -0,0 +1,211 @@ +// 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.IO; +using System.Net.Http; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.Loader; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using System.Xml.XPath; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Server.IntegrationTesting.Common; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + public class TestServer: IDisposable, IStartup + { + private const string InProcessHandlerDll = "aspnetcorev2_inprocess.dll"; + private const string AspNetCoreModuleDll = "aspnetcorev2.dll"; + private const string HWebCoreDll = "hwebcore.dll"; + + internal static string HostableWebCoreLocation => Environment.ExpandEnvironmentVariables($@"%windir%\system32\inetsrv\{HWebCoreDll}"); + internal static string BasePath => Path.GetDirectoryName(new Uri(typeof(TestServer).Assembly.CodeBase).AbsolutePath); + + internal static string AspNetCoreModuleLocation => Path.Combine(BasePath, AspNetCoreModuleDll); + + private static readonly SemaphoreSlim WebCoreLock = new SemaphoreSlim(1, 1); + + private static readonly int PortRetryCount = 10; + + private readonly TaskCompletionSource _startedTaskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + private readonly Action _appBuilder; + private readonly ILoggerFactory _loggerFactory; + private readonly hostfxr_main_fn _hostfxrMainFn; + + private Uri BaseUri => new Uri("http://localhost:" + _currentPort); + public HttpClient HttpClient { get; private set; } + public TestConnection CreateConnection() => new TestConnection(_currentPort); + + private IWebHost _host; + + private string _appHostConfigPath; + private int _currentPort; + + private TestServer(Action appBuilder, ILoggerFactory loggerFactory) + { + _hostfxrMainFn = Main; + _appBuilder = appBuilder; + _loggerFactory = loggerFactory; + } + + public static async Task Create(Action appBuilder, ILoggerFactory loggerFactory) + { + await WebCoreLock.WaitAsync(); + var server = new TestServer(appBuilder, loggerFactory); + server.Start(); + (await server.HttpClient.GetAsync("/start")).EnsureSuccessStatusCode(); + await server._startedTaskCompletionSource.Task; + return server; + } + + public static Task Create(RequestDelegate app, ILoggerFactory loggerFactory) + { + return Create(builder => builder.Run(app), loggerFactory); + } + + private void Start() + { + LoadLibrary(HostableWebCoreLocation); + _appHostConfigPath = Path.GetTempFileName(); + + set_main_handler(_hostfxrMainFn); + + Retry(() => + { + _currentPort = TestPortHelper.GetNextPort(); + + InitializeConfig(_currentPort); + + var startResult = WebCoreActivate(_appHostConfigPath, null, "Instance"); + if (startResult != 0) + { + throw new InvalidOperationException($"Error while running WebCoreActivate: {startResult} on port {_currentPort}"); + } + }, PortRetryCount); + + HttpClient = new HttpClient(new LoggingHandler(new SocketsHttpHandler(), _loggerFactory.CreateLogger())) + { + BaseAddress = BaseUri + }; + } + + private void InitializeConfig(int port) + { + var webHostConfig = XDocument.Load(Path.GetFullPath("HostableWebCore.config")); + webHostConfig.XPathSelectElement("/configuration/system.webServer/globalModules/add[@name='AspNetCoreModuleV2']") + .SetAttributeValue("image", AspNetCoreModuleLocation); + + var siteElement = webHostConfig.Root + .RequiredElement("system.applicationHost") + .RequiredElement("sites") + .RequiredElement("site"); + + siteElement + .RequiredElement("bindings") + .RequiredElement("binding") + .SetAttributeValue("bindingInformation", $":{port}:localhost"); + + webHostConfig.Save(_appHostConfigPath); + } + + private int Main(IntPtr argc, IntPtr argv) + { + _host = new WebHostBuilder() + .UseIIS() + .ConfigureServices(services => { + services.AddSingleton(this); + services.AddSingleton(_loggerFactory); + }) + .UseSetting(WebHostDefaults.ApplicationKey, typeof(TestServer).GetTypeInfo().Assembly.FullName) + .Build(); + + var doneEvent = new ManualResetEventSlim(); + var lifetime = _host.Services.GetService(); + + lifetime.ApplicationStopping.Register(() => doneEvent.Set()); + _host.Start(); + _startedTaskCompletionSource.SetResult(null); + doneEvent.Wait(); + _host.Dispose(); + return 0; + } + + public void Dispose() + { + HttpClient.Dispose(); + + // WebCoreShutdown occasionally AVs + // This causes the dotnet test process to crash + // To avoid this, we have to wait to shutdown + // and pass in true to immediately shutdown the hostable web core + // Both of these seem to be required. + Thread.Sleep(100); + WebCoreShutdown(immediate: true); + WebCoreLock.Release(); + } + + public IServiceProvider ConfigureServices(IServiceCollection services) + { + return services.BuildServiceProvider(); + } + + public void Configure(IApplicationBuilder app) + { + app.Map("/start", builder => builder.Run(context => context.Response.WriteAsync("Done"))); + _appBuilder(app); + } + + private delegate int hostfxr_main_fn(IntPtr argc, IntPtr argv); + + [DllImport(HWebCoreDll)] + private static extern int WebCoreActivate( + [In, MarshalAs(UnmanagedType.LPWStr)] + string appHostConfigPath, + [In, MarshalAs(UnmanagedType.LPWStr)] + string rootWebConfigPath, + [In, MarshalAs(UnmanagedType.LPWStr)] + string instanceName); + + [DllImport(HWebCoreDll)] + private static extern int WebCoreShutdown(bool immediate); + + [DllImport(InProcessHandlerDll)] + private static extern int set_main_handler(hostfxr_main_fn main); + + [DllImport("kernel32", SetLastError=true, CharSet = CharSet.Ansi)] + private static extern IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpFileName); + + private void Retry(Action func, int attempts) + { + var exceptions = new List(); + + for (var attempt = 0; attempt < attempts; attempt++) + { + try + { + func(); + return; + } + catch (Exception e) + { + exceptions.Add(e); + } + } + + throw new AggregateException(exceptions); + } + } +} diff --git a/src/IISIntegration/test/IISExpress.FunctionalTests/DeployerSelector.cs b/src/IISIntegration/test/IISExpress.FunctionalTests/DeployerSelector.cs new file mode 100644 index 0000000000..ba6a1ec6e2 --- /dev/null +++ b/src/IISIntegration/test/IISExpress.FunctionalTests/DeployerSelector.cs @@ -0,0 +1,14 @@ +// 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.Server.IntegrationTesting; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + public static class DeployerSelector + { + public static ServerType ServerType => ServerType.IISExpress; + public static bool IsBackwardsCompatiblityTest => false; + public static bool IsForwardsCompatibilityTest => false; + } +} diff --git a/src/IISIntegration/test/IISExpress.FunctionalTests/HttpsTests.cs b/src/IISIntegration/test/IISExpress.FunctionalTests/HttpsTests.cs new file mode 100644 index 0000000000..ac58f73c0e --- /dev/null +++ b/src/IISIntegration/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/src/IISIntegration/test/IISExpress.FunctionalTests/IISExpress.FunctionalTests.csproj b/src/IISIntegration/test/IISExpress.FunctionalTests/IISExpress.FunctionalTests.csproj new file mode 100644 index 0000000000..988c2d5943 --- /dev/null +++ b/src/IISIntegration/test/IISExpress.FunctionalTests/IISExpress.FunctionalTests.csproj @@ -0,0 +1,38 @@ + + + + netcoreapp2.2 + True + + + + + + + + + + + False + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/test/IISExpress.FunctionalTests/InProcess/AuthenticationTests.cs b/src/IISIntegration/test/IISExpress.FunctionalTests/InProcess/AuthenticationTests.cs new file mode 100644 index 0000000000..47fb3a6553 --- /dev/null +++ b/src/IISIntegration/test/IISExpress.FunctionalTests/InProcess/AuthenticationTests.cs @@ -0,0 +1,70 @@ +// 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; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class AuthenticationTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public AuthenticationTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + [RequiresIIS(IISCapability.WindowsAuthentication)] + public async Task Authentication_InProcess() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(publish: true); + deploymentParameters.SetWindowsAuth(); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("/AuthenticationAnonymous"); + + var responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Anonymous?True", responseText); + + response = await deploymentResult.HttpClient.GetAsync("/AuthenticationRestricted"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Contains("NTLM", response.Headers.WwwAuthenticate.ToString()); + Assert.Contains("Negotiate", response.Headers.WwwAuthenticate.ToString()); + + response = await deploymentResult.HttpClient.GetAsync("/AuthenticationRestrictedNTLM"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Contains("NTLM", response.Headers.WwwAuthenticate.ToString()); + // Note we can't restrict a challenge to a specific auth type, the native auth modules always add themselves. + Assert.Contains("Negotiate", response.Headers.WwwAuthenticate.ToString()); + + response = await deploymentResult.HttpClient.GetAsync("/AuthenticationForbidden"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + + var httpClientHandler = new HttpClientHandler() { UseDefaultCredentials = true }; + var httpClient = deploymentResult.CreateHttpClient(httpClientHandler); + + response = await httpClient.GetAsync("/AuthenticationAnonymous"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Anonymous?True", responseText); + + response = await httpClient.GetAsync("/AuthenticationRestricted"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotEmpty(responseText); + } + } +} diff --git a/src/IISIntegration/test/IISExpress.FunctionalTests/InProcess/ShutdownTests.cs b/src/IISIntegration/test/IISExpress.FunctionalTests/InProcess/ShutdownTests.cs new file mode 100644 index 0000000000..caaf728c64 --- /dev/null +++ b/src/IISIntegration/test/IISExpress.FunctionalTests/InProcess/ShutdownTests.cs @@ -0,0 +1,90 @@ +// 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.Net.Http; +using System.Net.Sockets; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class ShutdownTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public ShutdownTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task ServerShutsDownWhenMainExits() + { + var parameters = _fixture.GetBaseDeploymentParameters(publish: true); + var deploymentResult = await DeployAsync(parameters); + try + { + await deploymentResult.HttpClient.GetAsync("/Shutdown"); + } + catch (HttpRequestException ex) when (ex.InnerException is IOException) + { + // Server might close a connection before request completes + } + + deploymentResult.AssertWorkerProcessStop(); + } + + + [ConditionalFact] + public async Task ServerShutsDownWhenMainExitsStress() + { + var parameters = _fixture.GetBaseDeploymentParameters(publish: true); + var deploymentResult = await StartAsync(parameters); + + var load = Helpers.StressLoad(deploymentResult.HttpClient, "/HelloWorld", response => { + var statusCode = (int)response.StatusCode; + Assert.True(statusCode == 200 || statusCode == 503, "Status code was " + statusCode); + }); + + try + { + await deploymentResult.HttpClient.GetAsync("/Shutdown"); + await load; + } + catch (HttpRequestException ex) when (ex.InnerException is IOException | ex.InnerException is SocketException) + { + // Server might close a connection before request completes + } + + deploymentResult.AssertWorkerProcessStop(); + } + + [ConditionalFact] + public async Task GracefulShutdown_DoesNotCrashProcess() + { + var parameters = _fixture.GetBaseDeploymentParameters(publish: true); + var result = await DeployAsync(parameters); + + var response = await result.HttpClient.GetAsync("/HelloWorld"); + StopServer(gracefulShutdown: true); + Assert.True(result.HostProcess.ExitCode == 0); + } + + [ConditionalFact] + public async Task ForcefulShutdown_DoesCrashProcess() + { + var parameters = _fixture.GetBaseDeploymentParameters(publish: true); + var result = await DeployAsync(parameters); + + var response = await result.HttpClient.GetAsync("/HelloWorld"); + StopServer(gracefulShutdown: false); + Assert.True(result.HostProcess.ExitCode == 1); + } + } +} diff --git a/src/IISIntegration/test/IISExpress.FunctionalTests/InProcess/WebSocketTests.cs b/src/IISIntegration/test/IISExpress.FunctionalTests/InProcess/WebSocketTests.cs new file mode 100644 index 0000000000..826001c96d --- /dev/null +++ b/src/IISIntegration/test/IISExpress.FunctionalTests/InProcess/WebSocketTests.cs @@ -0,0 +1,79 @@ +// 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.Linq; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(IISTestSiteCollection.Name)] + [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, SkipReason = "No supported on this platform")] + public class WebSocketsTests + { + private readonly string _webSocketUri; + + public WebSocketsTests(IISTestSiteFixture fixture) + { + _webSocketUri = fixture.DeploymentResult.ApplicationBaseUri.Replace("http:", "ws:"); + } + + [ConditionalFact] + public async Task OnStartedCalledForWebSocket() + { + var cws = new ClientWebSocket(); + await cws.ConnectAsync(new Uri(_webSocketUri + "WebSocketLifetimeEvents"), default); + + await ReceiveMessage(cws, "OnStarting"); + await ReceiveMessage(cws, "Upgraded"); + } + + [ConditionalFact] + public async Task WebReadBeforeUpgrade() + { + var cws = new ClientWebSocket(); + await cws.ConnectAsync(new Uri(_webSocketUri + "WebReadBeforeUpgrade"), default); + + await ReceiveMessage(cws, "Yay"); + } + + [ConditionalFact] + public async Task CanSendAndReceieveData() + { + var cws = new ClientWebSocket(); + await cws.ConnectAsync(new Uri(_webSocketUri + "WebSocketEcho"), default); + + for (int i = 0; i < 1000; i++) + { + var mesage = i.ToString(); + await SendMessage(cws, mesage); + await ReceiveMessage(cws, mesage); + } + } + + private async Task SendMessage(ClientWebSocket webSocket, string message) + { + await webSocket.SendAsync(new ArraySegment(Encoding.ASCII.GetBytes(message)), WebSocketMessageType.Text, true, default); + } + + private async Task ReceiveMessage(ClientWebSocket webSocket, string expectedMessage) + { + var received = new byte[expectedMessage.Length]; + + var offset = 0; + WebSocketReceiveResult result; + do + { + result = await webSocket.ReceiveAsync(new ArraySegment(received, offset, received.Length - offset), default); + offset += result.Count; + } while (!result.EndOfMessage); + + Assert.Equal(expectedMessage, Encoding.ASCII.GetString(received)); + } + } +} diff --git a/src/IISIntegration/test/IISExpress.FunctionalTests/OutOfProcess/MultipleAppTests.cs b/src/IISIntegration/test/IISExpress.FunctionalTests/OutOfProcess/MultipleAppTests.cs new file mode 100644 index 0000000000..349f72d7bf --- /dev/null +++ b/src/IISIntegration/test/IISExpress.FunctionalTests/OutOfProcess/MultipleAppTests.cs @@ -0,0 +1,81 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class MultipleAppTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public MultipleAppTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalTheory] + [InlineData(AncmVersion.AspNetCoreModule)] + [InlineData(AncmVersion.AspNetCoreModuleV2)] + public Task Startup(AncmVersion ancmVersion) + { + // ANCM v1 currently does *not* retry if an app fails to start the first time due to a port collision. + // So, this test is expected to fail on v1 approximately 1 in 1,000 times (probably of at least one collision + // when 10 sites choose a random port from the range 1025-48000). Adding one retry should reduce the failure + // rate from 1 in 1,000 to 1 in 1,000,000. The previous product code (with "srand(GetTickCount())") should still + // fail the test reliably. + // https://github.com/aspnet/IISIntegration/issues/1350 + // + // ANCM v2 does retry on port collisions, so no retries should be required. + var attempts = (ancmVersion == AncmVersion.AspNetCoreModule) ? 2 : 1; + + return Helpers.Retry(async () => + { + const int numApps = 10; + + using (var deployers = new DisposableList()) + { + var deploymentResults = new List(); + + // Deploy all apps + for (var i = 0; i < numApps; i++) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(hostingModel: IntegrationTesting.HostingModel.OutOfProcess); + deploymentParameters.AncmVersion = ancmVersion; + + var deployer = CreateDeployer(deploymentParameters); + deployers.Add(deployer); + deploymentResults.Add(await deployer.DeployAsync()); + } + + // Start all requests as quickly as possible, so apps are started as quickly as possible, + // to test possible race conditions when multiple apps start at the same time. + var requestTasks = new List>(); + foreach (var deploymentResult in deploymentResults) + { + requestTasks.Add(deploymentResult.HttpClient.GetAsync("/HelloWorld")); + } + + // Verify all apps started and return expected response + foreach (var requestTask in requestTasks) + { + var response = await requestTask; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal("Hello World", responseText); + } + } + }, + attempts: attempts, msDelay: 0); + } + } +} diff --git a/src/IISIntegration/test/IISExpress.FunctionalTests/OutOfProcess/NtlmAuthentationTest.cs b/src/IISIntegration/test/IISExpress.FunctionalTests/OutOfProcess/NtlmAuthentationTest.cs new file mode 100644 index 0000000000..3ff46bf304 --- /dev/null +++ b/src/IISIntegration/test/IISExpress.FunctionalTests/OutOfProcess/NtlmAuthentationTest.cs @@ -0,0 +1,87 @@ +// 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; +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.IIS; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class NtlmAuthenticationTests : IISFunctionalTestBase + { + // Test only runs on IISExpress today as our CI machines do not have + // Windows auth installed globally. + // TODO either enable windows auth on our CI or use containers to test this + // behavior + + private readonly PublishedSitesFixture _fixture; + + public NtlmAuthenticationTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + public static TestMatrix TestVariants + => TestMatrix.ForServers(DeployerSelector.ServerType) + .WithTfms(Tfm.NetCoreApp22, Tfm.Net461) + .WithAllAncmVersions(); + + [ConditionalTheory] + [RequiresIIS(IISCapability.WindowsAuthentication)] + [MemberData(nameof(TestVariants))] + public async Task NtlmAuthentication(TestVariant variant) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant); + deploymentParameters.ApplicationBaseUriHint = $"https://localhost:0/"; + + deploymentParameters.SetWindowsAuth(enabled: true); + + var result = await DeployAsync(deploymentParameters); + var response = await result.HttpClient.GetAsync("/HelloWorld"); + + var responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Hello World", responseText); + + var httpClient = result.HttpClient; + response = await httpClient.GetAsync("/Anonymous"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Anonymous?True", responseText); + + response = await httpClient.GetAsync("/Restricted"); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Contains("NTLM", response.Headers.WwwAuthenticate.ToString()); + Assert.Contains("Negotiate", response.Headers.WwwAuthenticate.ToString()); + + response = await httpClient.GetAsync("/RestrictedNTLM"); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Contains("NTLM", response.Headers.WwwAuthenticate.ToString()); + // Note we can't restrict a challenge to a specific auth type, the native auth modules always add themselves. + Assert.Contains("Negotiate", response.Headers.WwwAuthenticate.ToString()); + + response = await httpClient.GetAsync("/Forbidden"); + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + + var httpClientHandler = new HttpClientHandler() { UseDefaultCredentials = true }; + httpClient = result.CreateClient(httpClientHandler); + + response = await httpClient.GetAsync("/Anonymous"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Anonymous?True", responseText); + + response = await httpClient.GetAsync("/Restricted"); + responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotEmpty(responseText); + } + } +} diff --git a/src/IISIntegration/test/IISExpress.FunctionalTests/Properties/AssemblyInfo.cs b/src/IISIntegration/test/IISExpress.FunctionalTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..b26f48a815 --- /dev/null +++ b/src/IISIntegration/test/IISExpress.FunctionalTests/Properties/AssemblyInfo.cs @@ -0,0 +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 Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] +[assembly: RequiresIIS] +[assembly: ShortClassName] diff --git a/src/IISIntegration/test/IISExpress.FunctionalTests/RequiresIISAttribute.cs b/src/IISIntegration/test/IISExpress.FunctionalTests/RequiresIISAttribute.cs new file mode 100644 index 0000000000..2e0a68b9f7 --- /dev/null +++ b/src/IISIntegration/test/IISExpress.FunctionalTests/RequiresIISAttribute.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] + public sealed class RequiresIISAttribute : Attribute, ITestCondition + { + public bool IsMet => IISExpressAncmSchema.SupportsInProcessHosting; + + public string SkipReason => IISExpressAncmSchema.SkipReason; + + public RequiresIISAttribute() { } + + public RequiresIISAttribute(IISCapability capabilities) + { + // IISCapabilities aren't pertinent to IISExpress + } + } +} diff --git a/src/IISIntegration/test/IISExpress.FunctionalTests/UpgradeFeatureDetectionTests.cs b/src/IISIntegration/test/IISExpress.FunctionalTests/UpgradeFeatureDetectionTests.cs new file mode 100644 index 0000000000..62d07c5fd9 --- /dev/null +++ b/src/IISIntegration/test/IISExpress.FunctionalTests/UpgradeFeatureDetectionTests.cs @@ -0,0 +1,83 @@ +// 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.Linq; +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; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class UpgradeFeatureDetectionTests : IISFunctionalTestBase + { + private readonly string _isWebsocketsSupported = Environment.OSVersion.Version >= new Version(6, 2) ? "Enabled" : "Disabled"; + private readonly PublishedSitesFixture _fixture; + + public UpgradeFeatureDetectionTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public Task UpgradeFeatureDetectionDisabled_InProcess() + { + // fails due to not modifying the apphost.config file. + return UpgradeFeatureDetectionDeployer( + disableWebSocket: true, + Helpers.GetInProcessTestSitesPath(), + "Disabled", HostingModel.InProcess); + } + + [ConditionalFact] + public Task UpgradeFeatureDetectionEnabled_InProcess() + { + return UpgradeFeatureDetectionDeployer( + disableWebSocket: false, + Helpers.GetInProcessTestSitesPath(), + _isWebsocketsSupported, HostingModel.InProcess); + } + + [ConditionalFact] + public Task UpgradeFeatureDetectionDisabled_OutOfProcess() + { + return UpgradeFeatureDetectionDeployer( + disableWebSocket: true, + Helpers.GetOutOfProcessTestSitesPath(), + "Disabled", HostingModel.OutOfProcess); + } + + [ConditionalFact] + public Task UpgradeFeatureDetectionEnabled_OutOfProcess() + { + return UpgradeFeatureDetectionDeployer( + disableWebSocket: false, + Helpers.GetOutOfProcessTestSitesPath(), + _isWebsocketsSupported, HostingModel.OutOfProcess); + } + + private async Task UpgradeFeatureDetectionDeployer(bool disableWebSocket, string sitePath, string expected, HostingModel hostingModel) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(hostingModel, publish: true); + + if (disableWebSocket) + { + // For IIS, we need to modify the apphost.config file + deploymentParameters.AddServerConfigAction( + element => element.Descendants("webSocket") + .Single() + .SetAttributeValue("enabled", "false")); + } + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("UpgradeFeatureDetection"); + var responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal(expected, responseText); + } + } +} diff --git a/src/IISIntegration/test/Microsoft.AspNetCore.Server.IISIntegration.Tests/IISExtensionTests.cs b/src/IISIntegration/test/Microsoft.AspNetCore.Server.IISIntegration.Tests/IISExtensionTests.cs new file mode 100644 index 0000000000..772fbde2c3 --- /dev/null +++ b/src/IISIntegration/test/Microsoft.AspNetCore.Server.IISIntegration.Tests/IISExtensionTests.cs @@ -0,0 +1,33 @@ +// 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.Linq; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration +{ + public class IISExtensionTests + { + [Fact] + public void CallingUseIISIntegrationMultipleTimesWorks() + { + + var builder = new WebHostBuilder() + .UseSetting("TOKEN", "TestToken") + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", "/") + .UseIISIntegration() + .UseIISIntegration() + .Configure(app => { }); + var server = new TestServer(builder); + + var filters = server.Host.Services.GetServices() + .OfType(); + + Assert.Single(filters); + } + } +} diff --git a/src/IISIntegration/test/Microsoft.AspNetCore.Server.IISIntegration.Tests/IISMiddlewareTests.cs b/src/IISIntegration/test/Microsoft.AspNetCore.Server.IISIntegration.Tests/IISMiddlewareTests.cs new file mode 100644 index 0000000000..0898b7ae21 --- /dev/null +++ b/src/IISIntegration/test/Microsoft.AspNetCore.Server.IISIntegration.Tests/IISMiddlewareTests.cs @@ -0,0 +1,420 @@ +// 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.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http.Features.Authentication; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration +{ + public class IISMiddlewareTests + { + [Fact] + public async Task MiddlewareSkippedIfTokenIsMissing() + { + var assertsExecuted = false; + + var builder = new WebHostBuilder() + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", "/") + .UseIISIntegration() + .Configure(app => + { + app.Run(context => + { + var auth = context.Features.Get(); + Assert.Null(auth); + assertsExecuted = true; + return Task.FromResult(0); + }); + }); + var server = new TestServer(builder); + + var req = new HttpRequestMessage(HttpMethod.Get, ""); + req.Headers.TryAddWithoutValidation("MS-ASPNETCORE-TOKEN", "TestToken"); + var response = await server.CreateClient().SendAsync(req); + Assert.True(assertsExecuted); + response.EnsureSuccessStatusCode(); + } + + [Fact] + public async Task MiddlewareRejectsRequestIfTokenHeaderIsMissing() + { + var assertsExecuted = false; + + var builder = new WebHostBuilder() + .UseSetting("TOKEN", "TestToken") + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", "/") + .UseIISIntegration() + .Configure(app => + { + app.Run(context => + { + var auth = context.Features.Get(); + Assert.Null(auth); + assertsExecuted = true; + return Task.FromResult(0); + }); + }); + var server = new TestServer(builder); + + var req = new HttpRequestMessage(HttpMethod.Get, ""); + var response = await server.CreateClient().SendAsync(req); + Assert.False(assertsExecuted); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Theory] + [InlineData("/", "/iisintegration", "shutdown")] + [InlineData("/", "/iisintegration", "Shutdown")] + [InlineData("/pathBase", "/pathBase/iisintegration", "shutdown")] + [InlineData("/pathBase", "/pathBase/iisintegration", "Shutdown")] + public async Task MiddlewareShutsdownGivenANCMShutdown(string pathBase, string requestPath, string shutdownEvent) + { + var requestExecuted = new ManualResetEvent(false); + var applicationStoppingFired = new ManualResetEvent(false); + var builder = new WebHostBuilder() + .UseSetting("TOKEN", "TestToken") + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", pathBase) + .UseIISIntegration() + .Configure(app => + { + var appLifetime = app.ApplicationServices.GetRequiredService(); + appLifetime.ApplicationStopping.Register(() => applicationStoppingFired.Set()); + + app.Run(context => + { + requestExecuted.Set(); + return Task.FromResult(0); + }); + }); + var server = new TestServer(builder); + + var request = new HttpRequestMessage(HttpMethod.Post, requestPath); + request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-TOKEN", "TestToken"); + request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-EVENT", shutdownEvent); + var response = await server.CreateClient().SendAsync(request); + + Assert.True(applicationStoppingFired.WaitOne(TimeSpan.FromSeconds(5))); + Assert.False(requestExecuted.WaitOne(0)); + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + } + + public static TheoryData InvalidShutdownMethods + { + get + { + return new TheoryData + { + HttpMethod.Put, + HttpMethod.Trace, + HttpMethod.Head, + HttpMethod.Get, + HttpMethod.Delete, + HttpMethod.Options + }; + } + } + + [Theory] + [MemberData(nameof(InvalidShutdownMethods))] + public async Task MiddlewareIgnoresShutdownGivenWrongMethod(HttpMethod method) + { + var requestExecuted = new ManualResetEvent(false); + var applicationStoppingFired = new ManualResetEvent(false); + var builder = new WebHostBuilder() + .UseSetting("TOKEN", "TestToken") + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", "/") + .UseIISIntegration() + .Configure(app => + { + var appLifetime = app.ApplicationServices.GetRequiredService(); + appLifetime.ApplicationStopping.Register(() => applicationStoppingFired.Set()); + + app.Run(context => + { + requestExecuted.Set(); + return Task.FromResult(0); + }); + }); + var server = new TestServer(builder); + + var request = new HttpRequestMessage(method, "/iisintegration"); + request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-TOKEN", "TestToken"); + request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-EVENT", "shutdown"); + var response = await server.CreateClient().SendAsync(request); + + Assert.False(applicationStoppingFired.WaitOne(TimeSpan.FromSeconds(1))); + Assert.True(requestExecuted.WaitOne(0)); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Theory] + [InlineData("/")] + [InlineData("/path")] + [InlineData("/path/iisintegration")] + public async Task MiddlewareIgnoresShutdownGivenWrongPath(string path) + { + var requestExecuted = new ManualResetEvent(false); + var applicationStoppingFired = new ManualResetEvent(false); + var builder = new WebHostBuilder() + .UseSetting("TOKEN", "TestToken") + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", "/") + .UseIISIntegration() + .Configure(app => + { + var appLifetime = app.ApplicationServices.GetRequiredService(); + appLifetime.ApplicationStopping.Register(() => applicationStoppingFired.Set()); + + app.Run(context => + { + requestExecuted.Set(); + return Task.FromResult(0); + }); + }); + var server = new TestServer(builder); + + var request = new HttpRequestMessage(HttpMethod.Post, path); + request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-TOKEN", "TestToken"); + request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-EVENT", "shutdown"); + var response = await server.CreateClient().SendAsync(request); + + Assert.False(applicationStoppingFired.WaitOne(TimeSpan.FromSeconds(1))); + Assert.True(requestExecuted.WaitOne(0)); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Theory] + [InlineData("event")] + [InlineData("")] + [InlineData(null)] + public async Task MiddlewareIgnoresShutdownGivenWrongEvent(string shutdownEvent) + { + var requestExecuted = new ManualResetEvent(false); + var applicationStoppingFired = new ManualResetEvent(false); + var builder = new WebHostBuilder() + .UseSetting("TOKEN", "TestToken") + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", "/") + .UseIISIntegration() + .Configure(app => + { + var appLifetime = app.ApplicationServices.GetRequiredService(); + appLifetime.ApplicationStopping.Register(() => applicationStoppingFired.Set()); + + app.Run(context => + { + requestExecuted.Set(); + return Task.FromResult(0); + }); + }); + var server = new TestServer(builder); + + var request = new HttpRequestMessage(HttpMethod.Post, "/iisintegration"); + request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-TOKEN", "TestToken"); + request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-EVENT", shutdownEvent); + var response = await server.CreateClient().SendAsync(request); + + Assert.False(applicationStoppingFired.WaitOne(TimeSpan.FromSeconds(1))); + Assert.True(requestExecuted.WaitOne(0)); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public void UrlDelayRegisteredAndPreferHostingUrlsSet() + { + var builder = new WebHostBuilder() + .UseSetting("TOKEN", "TestToken") + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", "/") + .UseIISIntegration() + .Configure(app => + { + app.Run(context => Task.FromResult(0)); + }); + + Assert.Null(builder.GetSetting(WebHostDefaults.ServerUrlsKey)); + Assert.Null(builder.GetSetting(WebHostDefaults.PreferHostingUrlsKey)); + + // Adds a server and calls Build() + var server = new TestServer(builder); + + Assert.Equal("http://127.0.0.1:12345", builder.GetSetting(WebHostDefaults.ServerUrlsKey)); + Assert.Equal("true", builder.GetSetting(WebHostDefaults.PreferHostingUrlsKey)); + } + + [Fact] + public void PathBaseHiddenFromServer() + { + var builder = new WebHostBuilder() + .UseSetting("TOKEN", "TestToken") + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", "/pathBase") + .UseIISIntegration() + .Configure(app => + { + app.Run(context => Task.FromResult(0)); + }); + new TestServer(builder); + + Assert.Equal("http://127.0.0.1:12345", builder.GetSetting(WebHostDefaults.ServerUrlsKey)); + } + + [Fact] + public async Task AddsUsePathBaseMiddlewareWhenPathBaseSpecified() + { + var requestPathBase = string.Empty; + var requestPath = string.Empty; + var builder = new WebHostBuilder() + .UseSetting("TOKEN", "TestToken") + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", "/pathbase") + .UseIISIntegration() + .Configure(app => + { + app.Run(context => + { + requestPathBase = context.Request.PathBase.Value; + requestPath = context.Request.Path.Value; + return Task.FromResult(0); + }); + }); + var server = new TestServer(builder); + + var request = new HttpRequestMessage(HttpMethod.Get, "/PathBase/Path"); + request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-TOKEN", "TestToken"); + var response = await server.CreateClient().SendAsync(request); + + Assert.Equal("/PathBase", requestPathBase); + Assert.Equal("/Path", requestPath); + } + + [Fact] + public async Task AddsAuthenticationHandlerByDefault() + { + var assertsExecuted = false; + + var builder = new WebHostBuilder() + .UseSetting("TOKEN", "TestToken") + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", "/") + .UseIISIntegration() + .Configure(app => + { + app.Run(async context => + { + var auth = context.RequestServices.GetRequiredService(); + var windows = await auth.GetSchemeAsync(IISDefaults.AuthenticationScheme); + Assert.NotNull(windows); + Assert.Null(windows.DisplayName); + Assert.Equal("Microsoft.AspNetCore.Server.IISIntegration.AuthenticationHandler", windows.HandlerType.FullName); + assertsExecuted = true; + }); + }); + var server = new TestServer(builder); + + var req = new HttpRequestMessage(HttpMethod.Get, ""); + req.Headers.TryAddWithoutValidation("MS-ASPNETCORE-TOKEN", "TestToken"); + await server.CreateClient().SendAsync(req); + + Assert.True(assertsExecuted); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task OnlyAddAuthenticationHandlerIfForwardWindowsAuthentication(bool forward) + { + var assertsExecuted = false; + + var builder = new WebHostBuilder() + .UseSetting("TOKEN", "TestToken") + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", "/") + .UseIISIntegration() + .ConfigureServices(services => + { + services.Configure(options => + { + options.ForwardWindowsAuthentication = forward; + }); + }) + .Configure(app => + { + app.Run(async context => + { + var auth = context.RequestServices.GetService(); + Assert.NotNull(auth); + var windowsAuth = await auth.GetSchemeAsync(IISDefaults.AuthenticationScheme); + if (forward) + { + Assert.NotNull(windowsAuth); + Assert.Null(windowsAuth.DisplayName); + Assert.Equal("AuthenticationHandler", windowsAuth.HandlerType.Name); + } + else + { + Assert.Null(windowsAuth); + } + assertsExecuted = true; + }); + }); + var server = new TestServer(builder); + + var req = new HttpRequestMessage(HttpMethod.Get, ""); + req.Headers.TryAddWithoutValidation("MS-ASPNETCORE-TOKEN", "TestToken"); + await server.CreateClient().SendAsync(req); + + Assert.True(assertsExecuted); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task DoesNotBlowUpWithoutAuth(bool forward) + { + var assertsExecuted = false; + + var builder = new WebHostBuilder() + .UseSetting("TOKEN", "TestToken") + .UseSetting("PORT", "12345") + .UseSetting("APPL_PATH", "/") + .UseIISIntegration() + .ConfigureServices(services => + { + services.Configure(options => + { + options.ForwardWindowsAuthentication = forward; + }); + }) + .Configure(app => + { + app.Run(context => + { + assertsExecuted = true; + return Task.FromResult(0); + }); + }); + var server = new TestServer(builder); + + var req = new HttpRequestMessage(HttpMethod.Get, ""); + req.Headers.TryAddWithoutValidation("MS-ASPNETCORE-TOKEN", "TestToken"); + await server.CreateClient().SendAsync(req); + + Assert.True(assertsExecuted); + } + } +} diff --git a/src/IISIntegration/test/Microsoft.AspNetCore.Server.IISIntegration.Tests/Microsoft.AspNetCore.Server.IISIntegration.Tests.csproj b/src/IISIntegration/test/Microsoft.AspNetCore.Server.IISIntegration.Tests/Microsoft.AspNetCore.Server.IISIntegration.Tests.csproj new file mode 100644 index 0000000000..ea19c2a1fc --- /dev/null +++ b/src/IISIntegration/test/Microsoft.AspNetCore.Server.IISIntegration.Tests/Microsoft.AspNetCore.Server.IISIntegration.Tests.csproj @@ -0,0 +1,18 @@ + + + + $(StandardTestTfms) + + + + + + + + + + + + + + diff --git a/src/IISIntegration/test/TestTasks/InjectRequestHandler.cs b/src/IISIntegration/test/TestTasks/InjectRequestHandler.cs new file mode 100644 index 0000000000..4da8129d71 --- /dev/null +++ b/src/IISIntegration/test/TestTasks/InjectRequestHandler.cs @@ -0,0 +1,62 @@ +// 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.Diagnostics; +using System.IO; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace TestTasks +{ + public class InjectRequestHandler + { + private static void Main(string[] args) + { + string rid = args[0]; + string libraryLocation = args[1]; + string depsFile = args[2]; + + JToken deps; + using (var file = File.OpenText(depsFile)) + using (JsonTextReader reader = new JsonTextReader(file)) + { + deps = JObject.ReadFrom(reader); + } + + var libraryName = "ANCMRH/1.0"; + var libraries = (JObject)deps["libraries"]; + var targetName = (JValue)deps["runtimeTarget"]["name"]; + + var target = (JObject)deps["targets"][targetName.Value]; + var targetLibrary = target.Properties().FirstOrDefault(p => p.Name == libraryName); + targetLibrary?.Remove(); + targetLibrary = + new JProperty(libraryName, new JObject( + new JProperty("runtimeTargets", new JObject( + new JProperty(libraryLocation.Replace('\\', '/'), new JObject( + new JProperty("rid", rid), + new JProperty("assetType", "native") + )))))); + target.AddFirst(targetLibrary); + + var library = libraries.Properties().FirstOrDefault(p => p.Name == libraryName); + library?.Remove(); + library = + new JProperty(libraryName, new JObject( + new JProperty("type", "package"), + new JProperty("serviceable", true), + new JProperty("sha512", ""), + new JProperty("path", libraryName), + new JProperty("hashPath", ""))); + libraries.AddFirst(library); + + using (var file = File.CreateText(depsFile)) + using (var writer = new JsonTextWriter(file) { Formatting = Formatting.Indented }) + { + deps.WriteTo(writer); + } + } + } +} diff --git a/src/IISIntegration/test/TestTasks/TestTasks.csproj b/src/IISIntegration/test/TestTasks/TestTasks.csproj new file mode 100644 index 0000000000..aa4c144936 --- /dev/null +++ b/src/IISIntegration/test/TestTasks/TestTasks.csproj @@ -0,0 +1,12 @@ + + + + Exe + $(StandardTestTfms) + + + + + + + diff --git a/src/IISIntegration/test/WebSites/Directory.Build.props b/src/IISIntegration/test/WebSites/Directory.Build.props new file mode 100644 index 0000000000..9b29d34f16 --- /dev/null +++ b/src/IISIntegration/test/WebSites/Directory.Build.props @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/IISIntegration/test/WebSites/InProcessForwardsCompatWebSite/InProcessWebSite.csproj b/src/IISIntegration/test/WebSites/InProcessForwardsCompatWebSite/InProcessWebSite.csproj new file mode 100644 index 0000000000..4d0952108a --- /dev/null +++ b/src/IISIntegration/test/WebSites/InProcessForwardsCompatWebSite/InProcessWebSite.csproj @@ -0,0 +1,32 @@ + + + + + + netcoreapp2.2 + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/test/WebSites/InProcessForwardsCompatWebSite/Properties/launchSettings.json b/src/IISIntegration/test/WebSites/InProcessForwardsCompatWebSite/Properties/launchSettings.json new file mode 100644 index 0000000000..246b7a0b47 --- /dev/null +++ b/src/IISIntegration/test/WebSites/InProcessForwardsCompatWebSite/Properties/launchSettings.json @@ -0,0 +1,42 @@ +{ + "iisSettings": { + "windowsAuthentication": true, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5762/", + "sslPort": 0 + } + }, + "profiles": { + "ANCM IIS Express": { + "commandName": "Executable", + "executablePath": "$(IISExpressPath)", + "commandLineArgs": "$(IISExpressArguments)", + "environmentVariables": { + "IIS_SITE_PATH": "$(MSBuildThisFileDirectory)", + "ANCM_PATH": "$(AspNetCoreModuleV1ShimDll)", + "ANCMV2_PATH": "$(AspNetCoreModuleV2ShimDll)", + "ANCM_OUTOFPROCESS_HANDLER": "$(AspNetCoreModuleV2OutOfProcessHandlerDll)", + "LAUNCHER_ARGS": "$(TargetPath)", + "ASPNETCORE_ENVIRONMENT": "Development", + "LAUNCHER_PATH": "$(DotNetPath)", + "ASPNETCORE_MODULE_DEBUG": "console" + } + }, + "ANCM IIS": { + "commandName": "Executable", + "executablePath": "$(IISPath)", + "commandLineArgs": "$(IISArguments)", + "environmentVariables": { + "IIS_SITE_PATH": "$(MSBuildThisFileDirectory)", + "ANCM_PATH": "$(AspNetCoreModuleV1ShimDll)", + "ANCMV2_PATH": "$(AspNetCoreModuleV2ShimDll)", + "ASPNETCORE_MODULE_OUTOFPROCESS_HANDLER": "$(AspNetCoreModuleV2OutOfProcessHandlerDll)", + "LAUNCHER_ARGS": "$(TargetPath)", + "ASPNETCORE_ENVIRONMENT": "Development", + "LAUNCHER_PATH": "$(DotNetPath)", + "ASPNETCORE_MODULE_DEBUG": "console" + } + } + } +} diff --git a/src/IISIntegration/test/WebSites/InProcessWebSite/DummyServer.cs b/src/IISIntegration/test/WebSites/InProcessWebSite/DummyServer.cs new file mode 100644 index 0000000000..d122f05776 --- /dev/null +++ b/src/IISIntegration/test/WebSites/InProcessWebSite/DummyServer.cs @@ -0,0 +1,30 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http.Features; + +namespace TestSite +{ + public class DummyServer : IServer + { + public void Dispose() + { + } + + public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) + { + return Task.Delay(TimeSpan.MaxValue); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.Delay(TimeSpan.MaxValue); + } + + public IFeatureCollection Features { get; } + } +} diff --git a/src/IISIntegration/test/WebSites/InProcessWebSite/InProcessWebSite.csproj b/src/IISIntegration/test/WebSites/InProcessWebSite/InProcessWebSite.csproj new file mode 100644 index 0000000000..d007d2daa1 --- /dev/null +++ b/src/IISIntegration/test/WebSites/InProcessWebSite/InProcessWebSite.csproj @@ -0,0 +1,33 @@ + + + + + + netcoreapp2.2 + true + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/test/WebSites/InProcessWebSite/Program.cs b/src/IISIntegration/test/WebSites/InProcessWebSite/Program.cs new file mode 100644 index 0000000000..c40448ee91 --- /dev/null +++ b/src/IISIntegration/test/WebSites/InProcessWebSite/Program.cs @@ -0,0 +1,114 @@ +// 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.Text; +using System.Threading; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace TestSite +{ + public static class Program + { + public static int Main(string[] args) + { + var mode = args.FirstOrDefault(); + switch (mode) + { + case "CreateFile": + File.WriteAllText(args[1], ""); + return StartServer(); + case "CheckLargeStdOutWrites": + Console.WriteLine(new string('a', 30000)); + break; + case "CheckLargeStdErrWrites": + Console.Error.WriteLine(new string('a', 30000)); + Console.Error.Flush(); + break; + case "ConsoleWrite": + Console.WriteLine($"Random number: {args[1]}"); + break; + case "ConsoleErrorWrite": + Console.Error.WriteLine($"Random number: {args[1]}"); + Console.Error.Flush(); + break; + case "CheckOversizedStdErrWrites": + Console.WriteLine(new string('a', 31000)); + break; + case "CheckOversizedStdOutWrites": + Console.Error.WriteLine(new string('a', 31000)); + Console.Error.Flush(); + break; + case "Hang": + Thread.Sleep(Timeout.Infinite); + break; + case "Throw": + throw new InvalidOperationException("Program.Main exception"); + case "EarlyReturn": + return 12; + case "HangOnStop": + { + var host = new WebHostBuilder() + .UseIIS() + .UseStartup() + .Build(); + host.Run(); + + Thread.Sleep(Timeout.Infinite); + } + break; + case "CheckConsoleFunctions": + // Call a bunch of console functions and make sure none return invalid handle. + Console.OutputEncoding = Encoding.UTF8; + Console.Title = "Test"; + Console.WriteLine($"Is Console redirection: {Console.IsOutputRedirected}"); + Console.BackgroundColor = ConsoleColor.Blue; + Console.WriteLine("彡⾔"); + break; + case "OverriddenServer": + { + var host = new WebHostBuilder() + .UseIIS() + .ConfigureServices(services => services.AddSingleton()) + .Configure(builder => builder.Run(async context => { await context.Response.WriteAsync("I shouldn't work"); })) + .Build(); + host.Run(); + } + break; + case "ConsoleErrorWriteStartServer": + Console.Error.WriteLine("TEST MESSAGE"); + return StartServer(); + case "ConsoleWriteStartServer": + Console.WriteLine("TEST MESSAGE"); + return StartServer(); + default: + return StartServer(); + + } + return 12; + } + + private static int StartServer() + { + var host = new WebHostBuilder() + .ConfigureLogging((_, factory) => + { + factory.AddConsole(); + factory.AddFilter("Console", level => level >= LogLevel.Information); + }) + .UseIIS() + .UseStartup() + .Build(); + + host.Run(); + return 0; + } + } +} diff --git a/src/IISIntegration/test/WebSites/InProcessWebSite/Properties/launchSettings.json b/src/IISIntegration/test/WebSites/InProcessWebSite/Properties/launchSettings.json new file mode 100644 index 0000000000..246b7a0b47 --- /dev/null +++ b/src/IISIntegration/test/WebSites/InProcessWebSite/Properties/launchSettings.json @@ -0,0 +1,42 @@ +{ + "iisSettings": { + "windowsAuthentication": true, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5762/", + "sslPort": 0 + } + }, + "profiles": { + "ANCM IIS Express": { + "commandName": "Executable", + "executablePath": "$(IISExpressPath)", + "commandLineArgs": "$(IISExpressArguments)", + "environmentVariables": { + "IIS_SITE_PATH": "$(MSBuildThisFileDirectory)", + "ANCM_PATH": "$(AspNetCoreModuleV1ShimDll)", + "ANCMV2_PATH": "$(AspNetCoreModuleV2ShimDll)", + "ANCM_OUTOFPROCESS_HANDLER": "$(AspNetCoreModuleV2OutOfProcessHandlerDll)", + "LAUNCHER_ARGS": "$(TargetPath)", + "ASPNETCORE_ENVIRONMENT": "Development", + "LAUNCHER_PATH": "$(DotNetPath)", + "ASPNETCORE_MODULE_DEBUG": "console" + } + }, + "ANCM IIS": { + "commandName": "Executable", + "executablePath": "$(IISPath)", + "commandLineArgs": "$(IISArguments)", + "environmentVariables": { + "IIS_SITE_PATH": "$(MSBuildThisFileDirectory)", + "ANCM_PATH": "$(AspNetCoreModuleV1ShimDll)", + "ANCMV2_PATH": "$(AspNetCoreModuleV2ShimDll)", + "ASPNETCORE_MODULE_OUTOFPROCESS_HANDLER": "$(AspNetCoreModuleV2OutOfProcessHandlerDll)", + "LAUNCHER_ARGS": "$(TargetPath)", + "ASPNETCORE_ENVIRONMENT": "Development", + "LAUNCHER_PATH": "$(DotNetPath)", + "ASPNETCORE_MODULE_DEBUG": "console" + } + } + } +} diff --git a/src/IISIntegration/test/WebSites/InProcessWebSite/Startup.WebSockets.cs b/src/IISIntegration/test/WebSites/InProcessWebSite/Startup.WebSockets.cs new file mode 100644 index 0000000000..d658ebd03f --- /dev/null +++ b/src/IISIntegration/test/WebSites/InProcessWebSite/Startup.WebSockets.cs @@ -0,0 +1,147 @@ +// 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.IO; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace TestSite +{ + public partial class Startup + { + + private void WebsocketRequest(IApplicationBuilder app) + { + app.Run(async context => + { + await context.Response.WriteAsync("test"); + }); + } + + private void WebReadBeforeUpgrade(IApplicationBuilder app) + { + app.Run(async context => { + + var singleByteArray = new byte[1]; + Assert.Equal(0, await context.Request.Body.ReadAsync(singleByteArray, 0, 1)); + + var ws = await Upgrade(context); + await SendMessages(ws, "Yay"); + }); + } + + private void WebSocketEcho(IApplicationBuilder app) + { + app.Run(async context => + { + var ws = await Upgrade(context); + + var appLifetime = app.ApplicationServices.GetRequiredService(); + + await Echo(ws, appLifetime.ApplicationStopping); + }); + } + + private void WebSocketLifetimeEvents(IApplicationBuilder app) + { + app.Run(async context => { + + var messages = new List(); + + context.Response.OnStarting(() => { + context.Response.Headers["custom-header"] = "value"; + messages.Add("OnStarting"); + return Task.CompletedTask; + }); + + var ws = await Upgrade(context); + messages.Add("Upgraded"); + + await SendMessages(ws, messages.ToArray()); + }); + } + + private static async Task SendMessages(WebSocket webSocket, params string[] messages) + { + foreach (var message in messages) + { + await webSocket.SendAsync(new ArraySegment(Encoding.ASCII.GetBytes(message)), WebSocketMessageType.Text, true, CancellationToken.None); + } + } + + private static async Task Upgrade(HttpContext context) + { + var upgradeFeature = context.Features.Get(); + + // Generate WebSocket response headers + string key = context.Request.Headers[Constants.Headers.SecWebSocketKey].ToString(); + var responseHeaders = HandshakeHelpers.GenerateResponseHeaders(key); + foreach (var headerPair in responseHeaders) + { + context.Response.Headers[headerPair.Key] = headerPair.Value; + } + + // Upgrade the connection + Stream opaqueTransport = await upgradeFeature.UpgradeAsync(); + + // Get the WebSocket object + var ws = WebSocketProtocol.CreateFromStream(opaqueTransport, isServer: true, subProtocol: null, keepAliveInterval: TimeSpan.FromMinutes(2)); + return ws; + } + + private async Task Echo(WebSocket webSocket, CancellationToken token) + { + var buffer = new byte[1024 * 4]; + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), token); + bool closeFromServer = false; + string closeFromServerCmd = "CloseFromServer"; + int closeFromServerLength = closeFromServerCmd.Length; + + while (!result.CloseStatus.HasValue && !token.IsCancellationRequested && !closeFromServer) + { + if (result.Count == closeFromServerLength && + Encoding.ASCII.GetString(buffer).Substring(0, result.Count) == closeFromServerCmd) + { + // The client sent "CloseFromServer" text message to request the server to close (a test scenario). + closeFromServer = true; + } + else + { + await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, token); + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), token); + } + } + + if (result.CloseStatus.HasValue) + { + // Client-initiated close handshake + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + } + else + { + // Server-initiated close handshake due to either of the two conditions: + // (1) The applicaton host is performing a graceful shutdown. + // (2) The client sent "CloseFromServer" text message to request the server to close (a test scenario). + await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, closeFromServerCmd, CancellationToken.None); + + // The server has sent the Close frame. + // Stop sending but keep receiving until we get the Close frame from the client. + while (!result.CloseStatus.HasValue) + { + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + } + } + } + } +} diff --git a/src/IISIntegration/test/WebSites/InProcessWebSite/Startup.cs b/src/IISIntegration/test/WebSites/InProcessWebSite/Startup.cs new file mode 100644 index 0000000000..eb84272b76 --- /dev/null +++ b/src/IISIntegration/test/WebSites/InProcessWebSite/Startup.cs @@ -0,0 +1,699 @@ +// 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.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.IISIntegration.FunctionalTests; +using Microsoft.AspNetCore.Server.IIS; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace TestSite +{ + public partial class Startup + { + public void Configure(IApplicationBuilder app) + { + TestStartup.Register(app, this); + } + + private async Task ServerVariable(HttpContext ctx) + { + var varName = ctx.Request.Query["q"]; + 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) + { + await ctx.Response.WriteAsync("Anonymous?" + !ctx.User.Identity.IsAuthenticated); + } + + private async Task AuthenticationRestricted(HttpContext ctx) + { + if (ctx.User.Identity.IsAuthenticated) + { + await ctx.Response.WriteAsync(ctx.User.Identity.AuthenticationType); + } + else + { + await ctx.ChallengeAsync(IISServerDefaults.AuthenticationScheme); + } + } + + private async Task AuthenticationForbidden(HttpContext ctx) + { + await ctx.ForbidAsync(IISServerDefaults.AuthenticationScheme); + } + + private async Task AuthenticationRestrictedNTLM(HttpContext ctx) + { + if (string.Equals("NTLM", ctx.User.Identity.AuthenticationType, StringComparison.Ordinal)) + { + await ctx.Response.WriteAsync("NTLM"); + } + else + { + await ctx.ChallengeAsync(IISServerDefaults.AuthenticationScheme); + } + } + + private async Task FeatureCollectionSetRequestFeatures(HttpContext ctx) + { + try + { + Assert.Equal("GET", ctx.Request.Method); + ctx.Request.Method = "test"; + Assert.Equal("test", ctx.Request.Method); + + Assert.Equal("http", ctx.Request.Scheme); + ctx.Request.Scheme = "test"; + Assert.Equal("test", ctx.Request.Scheme); + + Assert.Equal("/FeatureCollectionSetRequestFeatures", ctx.Request.PathBase); + ctx.Request.PathBase = "/base"; + Assert.Equal("/base", ctx.Request.PathBase); + + Assert.Equal("/path", ctx.Request.Path); + ctx.Request.Path = "/path"; + Assert.Equal("/path", ctx.Request.Path); + + Assert.Equal("?query", ctx.Request.QueryString.Value); + ctx.Request.QueryString = QueryString.Empty; + Assert.Equal("", ctx.Request.QueryString.Value); + + Assert.Equal("HTTP/1.1", ctx.Request.Protocol); + ctx.Request.Protocol = "HTTP/1.0"; + Assert.Equal("HTTP/1.0", ctx.Request.Protocol); + + Assert.NotNull(ctx.Request.Headers); + var headers = new HeaderDictionary(); + ctx.Features.Get().Headers = headers; + Assert.Same(headers, ctx.Features.Get().Headers); + + Assert.NotNull(ctx.Request.Body); + var body = new MemoryStream(); + ctx.Request.Body = body; + Assert.Same(body, ctx.Request.Body); + + //Assert.NotNull(ctx.Features.Get().TraceIdentifier); + //Assert.NotEqual(CancellationToken.None, ctx.RequestAborted); + //var token = new CancellationTokenSource().Token; + //ctx.RequestAborted = token; + //Assert.Equal(token, ctx.RequestAborted); + + await ctx.Response.WriteAsync("Success"); + return; + } + catch (Exception exception) + { + ctx.Response.StatusCode = 500; + await ctx.Response.WriteAsync(exception.ToString()); + } + await ctx.Response.WriteAsync("_Failure"); + } + + private async Task FeatureCollectionSetResponseFeatures(HttpContext ctx) + { + try + { + Assert.Equal(200, ctx.Response.StatusCode); + ctx.Response.StatusCode = 404; + Assert.Equal(404, ctx.Response.StatusCode); + ctx.Response.StatusCode = 200; + + Assert.Null(ctx.Features.Get().ReasonPhrase); + ctx.Features.Get().ReasonPhrase = "Set Response"; + Assert.Equal("Set Response", ctx.Features.Get().ReasonPhrase); + + Assert.NotNull(ctx.Response.Headers); + var headers = new HeaderDictionary(); + ctx.Features.Get().Headers = headers; + Assert.Same(headers, ctx.Features.Get().Headers); + + var originalBody = ctx.Response.Body; + Assert.NotNull(originalBody); + var body = new MemoryStream(); + ctx.Response.Body = body; + Assert.Same(body, ctx.Response.Body); + ctx.Response.Body = originalBody; + + await ctx.Response.WriteAsync("Success"); + return; + } + catch (Exception exception) + { + ctx.Response.StatusCode = 500; + await ctx.Response.WriteAsync(exception.ToString()); + } + await ctx.Response.WriteAsync("_Failure"); + } + + private async Task FeatureCollectionSetConnectionFeatures(HttpContext ctx) + { + try + { + Assert.True(IPAddress.IsLoopback(ctx.Connection.LocalIpAddress)); + ctx.Connection.LocalIpAddress = IPAddress.IPv6Any; + Assert.Equal(IPAddress.IPv6Any, ctx.Connection.LocalIpAddress); + + Assert.True(IPAddress.IsLoopback(ctx.Connection.RemoteIpAddress)); + ctx.Connection.RemoteIpAddress = IPAddress.IPv6Any; + Assert.Equal(IPAddress.IPv6Any, ctx.Connection.RemoteIpAddress); + await ctx.Response.WriteAsync("Success"); + return; + } + catch (Exception exception) + { + ctx.Response.StatusCode = 500; + await ctx.Response.WriteAsync(exception.ToString()); + } + await ctx.Response.WriteAsync("_Failure"); + } + + private void Throw(HttpContext ctx) + { + throw new Exception(); + } + + private async Task SetCustomErorCode(HttpContext ctx) + { + var feature = ctx.Features.Get(); + feature.ReasonPhrase = ctx.Request.Query["reason"]; + feature.StatusCode = int.Parse(ctx.Request.Query["code"]); + if (ctx.Request.Query["writeBody"] == "True") + { + await ctx.Response.WriteAsync(ctx.Request.Query["body"]); + } + } + + private async Task HelloWorld(HttpContext ctx) + { + if (ctx.Request.Path.Value.StartsWith("/Path")) + { + await ctx.Response.WriteAsync(ctx.Request.Path.Value); + return; + } + if (ctx.Request.Path.Value.StartsWith("/Query")) + { + await ctx.Response.WriteAsync(ctx.Request.QueryString.Value); + return; + } + + await ctx.Response.WriteAsync("Hello World"); + } + + private async Task LargeResponseBody(HttpContext ctx) + { + if (int.TryParse(ctx.Request.Query["length"], out var length)) + { + await ctx.Response.WriteAsync(new string('a', length)); + } + } + + private async Task ResponseHeaders(HttpContext ctx) + { + ctx.Response.Headers["UnknownHeader"] = "test123=foo"; + ctx.Response.ContentType = "text/plain"; + ctx.Response.Headers["MultiHeader"] = new StringValues(new string[] { "1", "2" }); + await ctx.Response.WriteAsync("Request Complete"); + } + + private async Task ResponseInvalidOrdering(HttpContext ctx) + { + if (ctx.Request.Path.Equals("/SetStatusCodeAfterWrite")) + { + await ctx.Response.WriteAsync("Started_"); + try + { + ctx.Response.StatusCode = 200; + } + catch (InvalidOperationException) + { + await ctx.Response.WriteAsync("SetStatusCodeAfterWriteThrew_"); + } + await ctx.Response.WriteAsync("Finished"); + return; + } + else if (ctx.Request.Path.Equals("/SetHeaderAfterWrite")) + { + await ctx.Response.WriteAsync("Started_"); + try + { + ctx.Response.Headers["This will fail"] = "some value"; + } + catch (InvalidOperationException) + { + await ctx.Response.WriteAsync("SetHeaderAfterWriteThrew_"); + } + await ctx.Response.WriteAsync("Finished"); + return; + } + } + + private async Task CheckEnvironmentVariable(HttpContext ctx) + { + var variable = Environment.GetEnvironmentVariable("ASPNETCORE_INPROCESS_TESTING_VALUE"); + await ctx.Response.WriteAsync(variable); + } + + private async Task CheckEnvironmentLongValueVariable(HttpContext ctx) + { + var variable = Environment.GetEnvironmentVariable("ASPNETCORE_INPROCESS_TESTING_LONG_VALUE"); + await ctx.Response.WriteAsync(variable); + } + + private async Task CheckAppendedEnvironmentVariable(HttpContext ctx) + { + var variable = Environment.GetEnvironmentVariable("ProgramFiles"); + await ctx.Response.WriteAsync(variable); + } + + private async Task CheckRemoveAuthEnvironmentVariable(HttpContext ctx) + { + var variable = Environment.GetEnvironmentVariable("ASPNETCORE_IIS_HTTPAUTH"); + await ctx.Response.WriteAsync(variable); + } + private async Task ReadAndWriteSynchronously(HttpContext ctx) + { + var t2 = Task.Run(() => WriteManyTimesToResponseBody(ctx)); + var t1 = Task.Run(() => ReadRequestBody(ctx)); + await Task.WhenAll(t1, t2); + } + + private async Task ReadRequestBody(HttpContext ctx) + { + var readBuffer = new byte[1]; + var result = await ctx.Request.Body.ReadAsync(readBuffer, 0, 1); + while (result != 0) + { + result = await ctx.Request.Body.ReadAsync(readBuffer, 0, 1); + } + } + + private async Task WriteManyTimesToResponseBody(HttpContext ctx) + { + for (var i = 0; i < 10000; i++) + { + await ctx.Response.WriteAsync("hello world"); + } + } + + private async Task ReadAndWriteEcho(HttpContext ctx) + { + var readBuffer = new byte[4096]; + var result = await ctx.Request.Body.ReadAsync(readBuffer, 0, readBuffer.Length); + while (result != 0) + { + await ctx.Response.WriteAsync(Encoding.UTF8.GetString(readBuffer, 0, result)); + result = await ctx.Request.Body.ReadAsync(readBuffer, 0, readBuffer.Length); + } + } + private async Task ReadAndFlushEcho(HttpContext ctx) + { + var readBuffer = new byte[4096]; + var result = await ctx.Request.Body.ReadAsync(readBuffer, 0, readBuffer.Length); + while (result != 0) + { + await ctx.Response.WriteAsync(Encoding.UTF8.GetString(readBuffer, 0, result)); + await ctx.Response.Body.FlushAsync(); + result = await ctx.Request.Body.ReadAsync(readBuffer, 0, readBuffer.Length); + } + } + + 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(); + + 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); + await ctx.Response.Body.FlushAsync(); + } + } + + 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]; + var count = 0; + do + { + count += await ctx.Request.Body.ReadAsync(data, count, data.Length - count); + } while (count != data.Length); + await ctx.Response.Body.WriteAsync(data, 0, data.Length); + } + + private async Task SetHeaderFromBody(HttpContext ctx) + { + using (var reader = new StreamReader(ctx.Request.Body)) + { + var value = await reader.ReadToEndAsync(); + ctx.Response.Headers["BodyAsString"] = value; + await ctx.Response.WriteAsync(value); + } + } + + private async Task ReadAndWriteEchoTwice(HttpContext ctx) + { + var readBuffer = new byte[4096]; + var result = await ctx.Request.Body.ReadAsync(readBuffer, 0, readBuffer.Length); + while (result != 0) + { + await ctx.Response.WriteAsync(Encoding.UTF8.GetString(readBuffer, 0, result)); + await ctx.Response.Body.FlushAsync(); + await ctx.Response.WriteAsync(Encoding.UTF8.GetString(readBuffer, 0, result)); + await ctx.Response.Body.FlushAsync(); + result = await ctx.Request.Body.ReadAsync(readBuffer, 0, readBuffer.Length); + } + } + + private async Task ReadAndWriteSlowConnection(HttpContext ctx) + { + var t2 = Task.Run(() => WriteResponseBodyAFewTimes(ctx)); + var t1 = Task.Run(() => ReadRequestBody(ctx)); + await Task.WhenAll(t1, t2); + } + + private async Task WriteResponseBodyAFewTimes(HttpContext ctx) + { + for (var i = 0; i < 100; i++) + { + await ctx.Response.WriteAsync("hello world"); + } + } + + private async Task ReadAndWriteCopyToAsync(HttpContext ctx) + { + await ctx.Request.Body.CopyToAsync(ctx.Response.Body); + } + + private async Task UpgradeFeatureDetection(HttpContext ctx) + { + if (ctx.Features.Get() != null) + { + await ctx.Response.WriteAsync("Enabled"); + } + else + { + await ctx.Response.WriteAsync("Disabled"); + } + } + + private async Task TestReadOffsetWorks(HttpContext ctx) + { + var buffer = new byte[11]; + ctx.Request.Body.Read(buffer, 0, 6); + ctx.Request.Body.Read(buffer, 6, 5); + + await ctx.Response.WriteAsync(Encoding.UTF8.GetString(buffer)); + } + + private async Task TestInvalidReadOperations(HttpContext ctx) + { + var success = false; + if (ctx.Request.Path.StartsWithSegments("/NullBuffer")) + { + try + { + await ctx.Request.Body.ReadAsync(null, 0, 0); + } + catch (Exception) + { + success = true; + } + } + else if (ctx.Request.Path.StartsWithSegments("/InvalidOffsetSmall")) + { + try + { + await ctx.Request.Body.ReadAsync(new byte[1], -1, 0); + } + catch (ArgumentOutOfRangeException) + { + success = true; + } + } + else if (ctx.Request.Path.StartsWithSegments("/InvalidOffsetLarge")) + { + try + { + await ctx.Request.Body.ReadAsync(new byte[1], 2, 0); + } + catch (ArgumentOutOfRangeException) + { + success = true; + } + } + else if (ctx.Request.Path.StartsWithSegments("/InvalidCountSmall")) + { + try + { + await ctx.Request.Body.ReadAsync(new byte[1], 0, -1); + } + catch (ArgumentOutOfRangeException) + { + success = true; + } + } + else if (ctx.Request.Path.StartsWithSegments("/InvalidCountLarge")) + { + try + { + await ctx.Request.Body.ReadAsync(new byte[1], 0, -1); + } + catch (ArgumentOutOfRangeException) + { + success = true; + } + } + else if (ctx.Request.Path.StartsWithSegments("/InvalidCountWithOffset")) + { + try + { + await ctx.Request.Body.ReadAsync(new byte[3], 1, 3); + } + catch (ArgumentOutOfRangeException) + { + success = true; + } + } + + + await ctx.Response.WriteAsync(success ? "Success" : "Failure"); + } + + private async Task TestValidReadOperations(HttpContext ctx) + { + var count = -1; + + if (ctx.Request.Path.StartsWithSegments("/NullBuffer")) + { + count = await ctx.Request.Body.ReadAsync(null, 0, 0); + } + else if (ctx.Request.Path.StartsWithSegments("/NullBufferPost")) + { + count = await ctx.Request.Body.ReadAsync(null, 0, 0); + } + else if (ctx.Request.Path.StartsWithSegments("/InvalidCountZeroRead")) + { + count = await ctx.Request.Body.ReadAsync(new byte[1], 0, 0); + } + else if (ctx.Request.Path.StartsWithSegments("/InvalidCountZeroReadPost")) + { + count = await ctx.Request.Body.ReadAsync(new byte[1], 0, 0); + } + + await ctx.Response.WriteAsync(count == 0 ? "Success" : "Failure"); + } + + private async Task TestInvalidWriteOperations(HttpContext ctx) + { + var success = false; + + if (ctx.Request.Path.StartsWithSegments("/InvalidOffsetSmall")) + { + try + { + await ctx.Response.Body.WriteAsync(new byte[1], -1, 0); + } + catch (ArgumentOutOfRangeException) + { + success = true; + } + } + else if (ctx.Request.Path.StartsWithSegments("/InvalidOffsetLarge")) + { + try + { + await ctx.Response.Body.WriteAsync(new byte[1], 2, 0); + } + catch (ArgumentOutOfRangeException) + { + success = true; + } + } + else if (ctx.Request.Path.StartsWithSegments("/InvalidCountSmall")) + { + try + { + await ctx.Response.Body.WriteAsync(new byte[1], 0, -1); + } + catch (ArgumentOutOfRangeException) + { + success = true; + } + } + else if (ctx.Request.Path.StartsWithSegments("/InvalidCountLarge")) + { + try + { + await ctx.Response.Body.WriteAsync(new byte[1], 0, -1); + } + catch (ArgumentOutOfRangeException) + { + success = true; + } + } + else if (ctx.Request.Path.StartsWithSegments("/InvalidCountWithOffset")) + { + try + { + await ctx.Response.Body.WriteAsync(new byte[3], 1, 3); + } + catch (ArgumentOutOfRangeException) + { + success = true; + } + } + + await ctx.Response.WriteAsync(success ? "Success" : "Failure"); + } + + private async Task TestValidWriteOperations(HttpContext ctx) + { + if (ctx.Request.Path.StartsWithSegments("/NullBuffer")) + { + await ctx.Response.Body.WriteAsync(null, 0, 0); + } + else if (ctx.Request.Path.StartsWithSegments("/NullBufferPost")) + { + await ctx.Response.Body.WriteAsync(null, 0, 0); + } + + await ctx.Response.WriteAsync("Success"); + } + + private async Task LargeResponseFile(HttpContext ctx) + { + var tempFile = Path.GetTempFileName(); + var fileContent = new string('a', 200000); + var fileStream = File.OpenWrite(tempFile); + + for (var i = 0; i < 1000; i++) + { + await fileStream.WriteAsync(Encoding.UTF8.GetBytes(fileContent), 0, fileContent.Length); + } + fileStream.Close(); + + await ctx.Response.SendFileAsync(tempFile, 0, null); + + // Try to delete the file from the temp directory. If it fails, don't report an error + // to the application. File should eventually be cleaned up from the temp directory + // by OS. + try + { + File.Delete(tempFile); + } + catch (Exception) + { + } + } + + private async Task BasePath(HttpContext ctx) + { + await ctx.Response.WriteAsync(AppDomain.CurrentDomain.BaseDirectory); + } + + private async Task Shutdown(HttpContext ctx) + { + await ctx.Response.WriteAsync("Shutting down"); + ctx.RequestServices.GetService().StopApplication(); + } + + private async Task GetServerVariableStress(HttpContext ctx) + { + // This test simulates the scenario where native Flush call is being + // 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(serverVariableFeature["REMOTE_PORT"]); + await ctx.Response.Body.FlushAsync(); + } + await ctx.Response.WriteAsync("Response End"); + } + + private async Task CommandLineArgs(HttpContext ctx) + { + 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/src/IISIntegration/test/WebSites/InProcessWebSite/web.config b/src/IISIntegration/test/WebSites/InProcessWebSite/web.config new file mode 100644 index 0000000000..2a9bd223c3 --- /dev/null +++ b/src/IISIntegration/test/WebSites/InProcessWebSite/web.config @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/test/WebSites/InProcessWebSite/wwwroot/static.txt b/src/IISIntegration/test/WebSites/InProcessWebSite/wwwroot/static.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/IISIntegration/test/WebSites/OutOfProcessWebSite/OutOfProcessWebSite.csproj b/src/IISIntegration/test/WebSites/OutOfProcessWebSite/OutOfProcessWebSite.csproj new file mode 100644 index 0000000000..14beb7394e --- /dev/null +++ b/src/IISIntegration/test/WebSites/OutOfProcessWebSite/OutOfProcessWebSite.csproj @@ -0,0 +1,31 @@ + + + + + + $(StandardTestTfms) + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/test/WebSites/OutOfProcessWebSite/Program.cs b/src/IISIntegration/test/WebSites/OutOfProcessWebSite/Program.cs new file mode 100644 index 0000000000..90895237d2 --- /dev/null +++ b/src/IISIntegration/test/WebSites/OutOfProcessWebSite/Program.cs @@ -0,0 +1,45 @@ +// 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.IO; +using System.Linq; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Logging; + +namespace TestSite +{ + public static class Program + { + public static int Main(string[] args) + { + var mode = args.FirstOrDefault(); + switch (mode) + { + case "CreateFile": + File.WriteAllText(args[1], ""); + return StartServer(); + } + + return StartServer(); + } + + private static int StartServer() + { + var host = new WebHostBuilder() + .ConfigureLogging( + (_, factory) => { + factory.AddConsole(); + factory.AddFilter("Console", level => level >= LogLevel.Information); + }) + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .UseKestrel() + .Build(); + + host.Run(); + return 0; + } + } +} + diff --git a/src/IISIntegration/test/WebSites/OutOfProcessWebSite/Properties/launchSettings.json b/src/IISIntegration/test/WebSites/OutOfProcessWebSite/Properties/launchSettings.json new file mode 100644 index 0000000000..246b7a0b47 --- /dev/null +++ b/src/IISIntegration/test/WebSites/OutOfProcessWebSite/Properties/launchSettings.json @@ -0,0 +1,42 @@ +{ + "iisSettings": { + "windowsAuthentication": true, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5762/", + "sslPort": 0 + } + }, + "profiles": { + "ANCM IIS Express": { + "commandName": "Executable", + "executablePath": "$(IISExpressPath)", + "commandLineArgs": "$(IISExpressArguments)", + "environmentVariables": { + "IIS_SITE_PATH": "$(MSBuildThisFileDirectory)", + "ANCM_PATH": "$(AspNetCoreModuleV1ShimDll)", + "ANCMV2_PATH": "$(AspNetCoreModuleV2ShimDll)", + "ANCM_OUTOFPROCESS_HANDLER": "$(AspNetCoreModuleV2OutOfProcessHandlerDll)", + "LAUNCHER_ARGS": "$(TargetPath)", + "ASPNETCORE_ENVIRONMENT": "Development", + "LAUNCHER_PATH": "$(DotNetPath)", + "ASPNETCORE_MODULE_DEBUG": "console" + } + }, + "ANCM IIS": { + "commandName": "Executable", + "executablePath": "$(IISPath)", + "commandLineArgs": "$(IISArguments)", + "environmentVariables": { + "IIS_SITE_PATH": "$(MSBuildThisFileDirectory)", + "ANCM_PATH": "$(AspNetCoreModuleV1ShimDll)", + "ANCMV2_PATH": "$(AspNetCoreModuleV2ShimDll)", + "ASPNETCORE_MODULE_OUTOFPROCESS_HANDLER": "$(AspNetCoreModuleV2OutOfProcessHandlerDll)", + "LAUNCHER_ARGS": "$(TargetPath)", + "ASPNETCORE_ENVIRONMENT": "Development", + "LAUNCHER_PATH": "$(DotNetPath)", + "ASPNETCORE_MODULE_DEBUG": "console" + } + } + } +} diff --git a/src/IISIntegration/test/WebSites/OutOfProcessWebSite/Startup.cs b/src/IISIntegration/test/WebSites/OutOfProcessWebSite/Startup.cs new file mode 100644 index 0000000000..de54a85a8a --- /dev/null +++ b/src/IISIntegration/test/WebSites/OutOfProcessWebSite/Startup.cs @@ -0,0 +1,105 @@ +// 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.Diagnostics; +using System.IO; +using System.Linq; +using System.Security.Principal; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.IISIntegration.FunctionalTests; +using Microsoft.AspNetCore.Server.IISIntegration; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace TestSite +{ + public partial class Startup + { + private IServerAddressesFeature _serverAddresses; + + public void Configure(IApplicationBuilder app) + { + TestStartup.Register(app, this); + + _serverAddresses = app.ServerFeatures.Get(); + } + + public Task Path(HttpContext ctx) => ctx.Response.WriteAsync(ctx.Request.Path.Value); + + public Task Query(HttpContext ctx) => ctx.Response.WriteAsync(ctx.Request.QueryString.Value); + + public Task BodyLimit(HttpContext ctx) => ctx.Response.WriteAsync(ctx.Features.Get()?.MaxRequestBodySize?.ToString() ?? "null"); + + + public Task HelloWorld(HttpContext ctx) => ctx.Response.WriteAsync("Hello World"); + + public Task HttpsHelloWorld(HttpContext ctx) => + ctx.Response.WriteAsync("Scheme:" + ctx.Request.Scheme + "; Original:" + ctx.Request.Headers["x-original-proto"]); + + public Task Anonymous(HttpContext context) => context.Response.WriteAsync("Anonymous?" + !context.User.Identity.IsAuthenticated); + + public Task Restricted(HttpContext context) + { + if (context.User.Identity.IsAuthenticated) + { + Assert.IsType(context.User); + return context.Response.WriteAsync(context.User.Identity.AuthenticationType); + } + else + { + return context.ChallengeAsync(IISDefaults.AuthenticationScheme); + } + } + + public Task Forbidden(HttpContext context) => context.ForbidAsync(IISDefaults.AuthenticationScheme); + + public Task RestrictedNTLM(HttpContext context) + { + if (string.Equals("NTLM", context.User.Identity.AuthenticationType, StringComparison.Ordinal)) + { + return context.Response.WriteAsync("NTLM"); + } + else + { + return context.ChallengeAsync(IISDefaults.AuthenticationScheme); + } + } + + public Task UpgradeFeatureDetection(HttpContext context) => + context.Response.WriteAsync(context.Features.Get() != null? "Enabled": "Disabled"); + + public Task CheckRequestHandlerVersion(HttpContext context) + { + // We need to check if the aspnetcorev2_outofprocess dll is loaded by iisexpress.exe + // As they aren't in the same process, we will try to delete the file and expect a file + // in use error + try + { + File.Delete(context.Request.Headers["ANCMRHPath"]); + } + catch(UnauthorizedAccessException) + { + // TODO calling delete on the file will succeed when running with IIS + return context.Response.WriteAsync("Hello World"); + } + + return context.Response.WriteAsync(context.Request.Headers["ANCMRHPath"]); + } + + private async Task ProcessId(HttpContext context) + { + await context.Response.WriteAsync(Process.GetCurrentProcess().Id.ToString()); + } + + private async Task ServerAddresses(HttpContext context) + { + await context.Response.WriteAsync(string.Join(",", _serverAddresses.Addresses)); + } + } +} diff --git a/src/IISIntegration/test/WebSites/OutOfProcessWebSite/wwwroot/static.txt b/src/IISIntegration/test/WebSites/OutOfProcessWebSite/wwwroot/static.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/IISIntegration/test/WebSites/StressTestWebSite/Program.cs b/src/IISIntegration/test/WebSites/StressTestWebSite/Program.cs new file mode 100644 index 0000000000..e8e5392c2c --- /dev/null +++ b/src/IISIntegration/test/WebSites/StressTestWebSite/Program.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Logging; + +namespace ANCMStressTestApp +{ + public class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .ConfigureLogging((_, factory) => + { + factory.AddConsole(); + }) + .UseKestrel() + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/src/IISIntegration/test/WebSites/StressTestWebSite/Properties/launchSettings.json b/src/IISIntegration/test/WebSites/StressTestWebSite/Properties/launchSettings.json new file mode 100644 index 0000000000..246b7a0b47 --- /dev/null +++ b/src/IISIntegration/test/WebSites/StressTestWebSite/Properties/launchSettings.json @@ -0,0 +1,42 @@ +{ + "iisSettings": { + "windowsAuthentication": true, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5762/", + "sslPort": 0 + } + }, + "profiles": { + "ANCM IIS Express": { + "commandName": "Executable", + "executablePath": "$(IISExpressPath)", + "commandLineArgs": "$(IISExpressArguments)", + "environmentVariables": { + "IIS_SITE_PATH": "$(MSBuildThisFileDirectory)", + "ANCM_PATH": "$(AspNetCoreModuleV1ShimDll)", + "ANCMV2_PATH": "$(AspNetCoreModuleV2ShimDll)", + "ANCM_OUTOFPROCESS_HANDLER": "$(AspNetCoreModuleV2OutOfProcessHandlerDll)", + "LAUNCHER_ARGS": "$(TargetPath)", + "ASPNETCORE_ENVIRONMENT": "Development", + "LAUNCHER_PATH": "$(DotNetPath)", + "ASPNETCORE_MODULE_DEBUG": "console" + } + }, + "ANCM IIS": { + "commandName": "Executable", + "executablePath": "$(IISPath)", + "commandLineArgs": "$(IISArguments)", + "environmentVariables": { + "IIS_SITE_PATH": "$(MSBuildThisFileDirectory)", + "ANCM_PATH": "$(AspNetCoreModuleV1ShimDll)", + "ANCMV2_PATH": "$(AspNetCoreModuleV2ShimDll)", + "ASPNETCORE_MODULE_OUTOFPROCESS_HANDLER": "$(AspNetCoreModuleV2OutOfProcessHandlerDll)", + "LAUNCHER_ARGS": "$(TargetPath)", + "ASPNETCORE_ENVIRONMENT": "Development", + "LAUNCHER_PATH": "$(DotNetPath)", + "ASPNETCORE_MODULE_DEBUG": "console" + } + } + } +} diff --git a/src/IISIntegration/test/WebSites/StressTestWebSite/Startup.cs b/src/IISIntegration/test/WebSites/StressTestWebSite/Startup.cs new file mode 100644 index 0000000000..68f6eb1f77 --- /dev/null +++ b/src/IISIntegration/test/WebSites/StressTestWebSite/Startup.cs @@ -0,0 +1,232 @@ +// 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.Threading.Tasks; +using System.Threading; +using System.Text; +using System.Net.WebSockets; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Net.Http.Headers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; + +namespace ANCMStressTestApp +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + } + + public void Configure(IApplicationBuilder app) + { + app.Map("/HelloWorld", HelloWorld); + app.Map("/ConnectionClose", ConnectionClose); + app.Map("/EchoPostData", EchoPostData); + app.Map("/LargeResponseBody", LargeResponseBody); + app.Map("/ResponseHeaders", ResponseHeaders); + app.Map("/EnvironmentVariables", EnvironmentVariables); + app.Map("/RequestInformation", RequestInformation); + app.Map("/WebSocket", WebSocket); + + app.Run(async context => + { + await context.Response.WriteAsync("Default Page"); + }); + } + + private void HelloWorld(IApplicationBuilder app) + { + app.Run(async context => + { + await context.Response.WriteAsync("Hello World"); + }); + } + + private void ConnectionClose(IApplicationBuilder app) + { + app.Run(async context => + { + context.Response.Headers[HeaderNames.Connection] = "close"; + await context.Response.WriteAsync("Connnection Close"); + await context.Response.Body.FlushAsync(); + }); + } + + private void EchoPostData(IApplicationBuilder app) + { + app.Run(async context => + { + string responseBody = string.Empty; + + if (string.Equals(context.Request.Method, "POST", StringComparison.OrdinalIgnoreCase)) + { + using (StreamReader reader = new StreamReader(context.Request.Body, Encoding.UTF8)) + { + responseBody = await reader.ReadToEndAsync(); + } + } + else + { + responseBody = "NoAction"; + } + + await context.Response.WriteAsync(responseBody); + }); + } + + private void LargeResponseBody(IApplicationBuilder app) + { + app.Run(async context => + { + if (int.TryParse(context.Request.Query["length"], out var length)) + { + await context.Response.WriteAsync(new string('a', length)); + } + }); + } + + private void ResponseHeaders(IApplicationBuilder app) + { + app.Run(async context => + { + context.Response.Headers["UnknownHeader"] = "test123=foo"; + context.Response.ContentType = "text/plain"; + context.Response.Headers["MultiHeader"] = new StringValues(new string[] { "1", "2" }); + await context.Response.WriteAsync("Request Complete"); + }); + } + + private void EnvironmentVariables(IApplicationBuilder app) + { + app.Run(async context => + { + context.Response.ContentType = "text/plain"; + await context.Response.WriteAsync("Environment Variables:" + Environment.NewLine); + var vars = Environment.GetEnvironmentVariables(); + foreach (var key in vars.Keys.Cast().OrderBy(key => key, StringComparer.OrdinalIgnoreCase)) + { + var value = vars[key]; + await context.Response.WriteAsync(key + ": " + value + Environment.NewLine); + } + await context.Response.WriteAsync(Environment.NewLine); + }); + } + + private void RequestInformation(IApplicationBuilder app) + { + app.Run(async context => + { + context.Response.ContentType = "text/plain"; + + await context.Response.WriteAsync("Address:" + Environment.NewLine); + await context.Response.WriteAsync("Scheme: " + context.Request.Scheme + Environment.NewLine); + await context.Response.WriteAsync("Host: " + context.Request.Headers["Host"] + Environment.NewLine); + await context.Response.WriteAsync("PathBase: " + context.Request.PathBase.Value + Environment.NewLine); + await context.Response.WriteAsync("Path: " + context.Request.Path.Value + Environment.NewLine); + await context.Response.WriteAsync("Query: " + context.Request.QueryString.Value + Environment.NewLine); + await context.Response.WriteAsync(Environment.NewLine); + + await context.Response.WriteAsync("Connection:" + Environment.NewLine); + await context.Response.WriteAsync("RemoteIp: " + context.Connection.RemoteIpAddress + Environment.NewLine); + await context.Response.WriteAsync("RemotePort: " + context.Connection.RemotePort + Environment.NewLine); + await context.Response.WriteAsync("LocalIp: " + context.Connection.LocalIpAddress + Environment.NewLine); + await context.Response.WriteAsync("LocalPort: " + context.Connection.LocalPort + Environment.NewLine); + await context.Response.WriteAsync(Environment.NewLine); + + await context.Response.WriteAsync("Headers:" + Environment.NewLine); + foreach (var header in context.Request.Headers) + { + await context.Response.WriteAsync(header.Key + ": " + header.Value + Environment.NewLine); + } + await context.Response.WriteAsync(Environment.NewLine); + }); + } + + private void WebSocket(IApplicationBuilder app) + { + app.Run(async context => + { + var upgradeFeature = context.Features.Get(); + + // Generate WebSocket response headers + string key = context.Request.Headers[Constants.Headers.SecWebSocketKey].ToString(); + var responseHeaders = HandshakeHelpers.GenerateResponseHeaders(key); + foreach (var headerPair in responseHeaders) + { + context.Response.Headers[headerPair.Key] = headerPair.Value; + } + + // Upgrade the connection + Stream opaqueTransport = await upgradeFeature.UpgradeAsync(); + + // Get the WebSocket object + var ws = WebSocketProtocol.CreateFromStream(opaqueTransport, isServer: true, subProtocol: null, keepAliveInterval: TimeSpan.FromMinutes(2)); + + var appLifetime = app.ApplicationServices.GetRequiredService(); + + await Echo(ws, appLifetime.ApplicationStopping); + }); + } + + private async Task Echo(WebSocket webSocket, CancellationToken token) + { + try + { + var buffer = new byte[1024 * 4]; + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), token); + bool closeFromServer = false; + string closeFromServerCmd = "CloseFromServer"; + int closeFromServerLength = closeFromServerCmd.Length; + + while (!result.CloseStatus.HasValue && !token.IsCancellationRequested && !closeFromServer) + { + if (result.Count == closeFromServerLength && + Encoding.ASCII.GetString(buffer).Substring(0, result.Count) == closeFromServerCmd) + { + // The client sent "CloseFromServer" text message to request the server to close (a test scenario). + closeFromServer = true; + } + else + { + await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, token); + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), token); + } + } + + if (result.CloseStatus.HasValue) + { + // Client-initiated close handshake + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + } + else + { + // Server-initiated close handshake due to either of the two conditions: + // (1) The applicaton host is performing a graceful shutdown. + // (2) The client sent "CloseFromServer" text message to request the server to close (a test scenario). + await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, closeFromServerCmd, CancellationToken.None); + + // The server has sent the Close frame. + // Stop sending but keep receiving until we get the Close frame from the client. + while (!result.CloseStatus.HasValue) + { + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + } + } + } + catch (Exception e) + { + Console.WriteLine("{0} Exception caught!", e); + } + } + } +} diff --git a/src/IISIntegration/test/WebSites/StressTestWebSite/StressTestWebSite.csproj b/src/IISIntegration/test/WebSites/StressTestWebSite/StressTestWebSite.csproj new file mode 100644 index 0000000000..25ae032221 --- /dev/null +++ b/src/IISIntegration/test/WebSites/StressTestWebSite/StressTestWebSite.csproj @@ -0,0 +1,25 @@ + + + + + + $(StandardTestTfms) + true + + + + + + + + + + + + + + + + + + diff --git a/src/IISIntegration/test/WebSites/shared/SharedStartup/Startup.shared.cs b/src/IISIntegration/test/WebSites/shared/SharedStartup/Startup.shared.cs new file mode 100644 index 0000000000..b5d7f0305f --- /dev/null +++ b/src/IISIntegration/test/WebSites/shared/SharedStartup/Startup.shared.cs @@ -0,0 +1,117 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace TestSite +{ + public partial class Startup + { + public void ConfigureServices(IServiceCollection serviceCollection) + { + serviceCollection.AddResponseCompression(); + } + + private async Task HostingEnvironment(HttpContext ctx) + { + var hostingEnv = ctx.RequestServices.GetService(); + + await ctx.Response.WriteAsync("ContentRootPath " + hostingEnv.ContentRootPath + Environment.NewLine); + await ctx.Response.WriteAsync("WebRootPath " + hostingEnv.WebRootPath + Environment.NewLine); + await ctx.Response.WriteAsync("CurrentDirectory " + Environment.CurrentDirectory); + } + + private async Task ConsoleWrite(HttpContext ctx) + { + Console.WriteLine("TEST MESSAGE"); + + await ctx.Response.WriteAsync("Hello World"); + } + + private async Task ConsoleErrorWrite(HttpContext ctx) + { + Console.Error.WriteLine("TEST MESSAGE"); + + 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"); + if (ctx.User.Identity.Name != null) + { + await ctx.Response.WriteAsync(":" + ctx.User.Identity.Name); + } + } + + public async Task GetClientCert(HttpContext context) + { + var clientCert = context.Connection.ClientCertificate; + await context.Response.WriteAsync(clientCert != null ? $"Enabled;{clientCert.GetCertHashString()}" : "Disabled"); + } + + private static int _waitingRequestCount; + + public Task WaitForAbort(HttpContext context) + { + Interlocked.Increment(ref _waitingRequestCount); + try + { + context.RequestAborted.WaitHandle.WaitOne(); + return Task.CompletedTask; + } + finally + { + Interlocked.Decrement(ref _waitingRequestCount); + } + } + + public Task Abort(HttpContext context) + { + context.Abort(); + return Task.CompletedTask; + } + + public async Task WaitingRequestCount(HttpContext context) + { + await context.Response.WriteAsync(_waitingRequestCount.ToString()); + } + + public Task CreateFile(HttpContext context) + { + var hostingEnv = context.RequestServices.GetService(); + File.WriteAllText(System.IO.Path.Combine(hostingEnv.ContentRootPath, "Started.txt"), ""); + return Task.CompletedTask; + } + + public Task OverrideServer(HttpContext context) + { + context.Response.Headers["Server"] = "MyServer/7.8"; + return Task.CompletedTask; + } + + public void CompressedData(IApplicationBuilder builder) + { + builder.UseResponseCompression(); + // write random bytes to check that compressed data is passed through + builder.Run( + async context => + { + context.Response.ContentType = "text/html"; + await context.Response.Body.WriteAsync(new byte[100], 0, 100); + }); + } + } +} diff --git a/src/IISIntegration/test/WebSites/shared/WebSockets/Constants.cs b/src/IISIntegration/test/WebSites/shared/WebSockets/Constants.cs new file mode 100644 index 0000000000..95b53003cc --- /dev/null +++ b/src/IISIntegration/test/WebSites/shared/WebSockets/Constants.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests +{ + public static class Constants + { + public static class Headers + { + public const string Upgrade = "Upgrade"; + public const string UpgradeWebSocket = "websocket"; + public const string Connection = "Connection"; + public const string SecWebSocketKey = "Sec-WebSocket-Key"; + public const string SecWebSocketAccept = "Sec-WebSocket-Accept"; + } + } +} diff --git a/src/IISIntegration/test/WebSites/shared/WebSockets/HandshakeHelpers.cs b/src/IISIntegration/test/WebSites/shared/WebSockets/HandshakeHelpers.cs new file mode 100644 index 0000000000..b079ea3f85 --- /dev/null +++ b/src/IISIntegration/test/WebSites/shared/WebSockets/HandshakeHelpers.cs @@ -0,0 +1,41 @@ +// 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.Security.Cryptography; +using System.Text; + +namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests +{ + internal static class HandshakeHelpers + { + public static IEnumerable> GenerateResponseHeaders(string key) + { + yield return new KeyValuePair(Constants.Headers.Connection, Constants.Headers.Upgrade); + yield return new KeyValuePair(Constants.Headers.Upgrade, Constants.Headers.UpgradeWebSocket); + yield return new KeyValuePair(Constants.Headers.SecWebSocketAccept, CreateResponseKey(key)); + } + + public static string CreateResponseKey(string requestKey) + { + // "The value of this header field is constructed by concatenating /key/, defined above in step 4 + // in Section 4.2.2, with the string "258EAFA5- E914-47DA-95CA-C5AB0DC85B11", taking the SHA-1 hash of + // this concatenated value to obtain a 20-byte value and base64-encoding" + // https://tools.ietf.org/html/rfc6455#section-4.2.2 + + if (requestKey == null) + { + throw new ArgumentNullException(nameof(requestKey)); + } + + using (var algorithm = SHA1.Create()) + { + string merged = requestKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + byte[] mergedBytes = Encoding.UTF8.GetBytes(merged); + byte[] hashedBytes = algorithm.ComputeHash(mergedBytes); + return Convert.ToBase64String(hashedBytes); + } + } + } +} diff --git a/src/IISIntegration/test/WebSites/shared/WebSockets/TestStartup.cs b/src/IISIntegration/test/WebSites/shared/WebSockets/TestStartup.cs new file mode 100644 index 0000000000..b1604e367a --- /dev/null +++ b/src/IISIntegration/test/WebSites/shared/WebSockets/TestStartup.cs @@ -0,0 +1,38 @@ +using System; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.IISIntegration.FunctionalTests +{ + public static class TestStartup + { + public static void Register(IApplicationBuilder app, object startup) + { + var type = startup.GetType(); + foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + { + var parameters = method.GetParameters(); + if (method.Name != "Configure" && + parameters.Length == 1) + { + Action appfunc = null; + if (parameters[0].ParameterType == typeof(IApplicationBuilder)) + { + appfunc = innerAppBuilder => method.Invoke(startup, new[] { innerAppBuilder }); + } + else if (parameters[0].ParameterType == typeof(HttpContext)) + { + appfunc = innerAppBuilder => innerAppBuilder.Run(ctx => (Task)method.Invoke(startup, new[] { ctx })); + } + + if (appfunc != null) + { + app.Map("/" + method.Name, appfunc); + } + } + } + } + } +} diff --git a/src/IISIntegration/test/gtest/gtest.vcxproj b/src/IISIntegration/test/gtest/gtest.vcxproj new file mode 100644 index 0000000000..924f26337d --- /dev/null +++ b/src/IISIntegration/test/gtest/gtest.vcxproj @@ -0,0 +1,180 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + + + + + 15.0 + {CAC1267B-8778-4257-AAC6-CAF481723B01} + Win32Proj + gtest + 10.0.15063.0 + + + + StaticLibrary + true + v141 + Unicode + gtestd + + + StaticLibrary + false + v141 + true + Unicode + gtest + + + StaticLibrary + true + v141 + Unicode + gtestd + + + StaticLibrary + false + v141 + true + Unicode + gtest + + + + + + + + + + + + + + + + + + + + + true + $(VC_IncludePath);$(WindowsSDK_IncludePath); + $(VC_SourcePath); + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + true + $(VC_IncludePath);$(WindowsSDK_IncludePath); + $(VC_SourcePath); + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + false + $(VC_IncludePath);$(WindowsSDK_IncludePath); + $(VC_SourcePath); + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + false + $(VC_IncludePath);$(WindowsSDK_IncludePath); + $(VC_SourcePath); + $(MSBuildProjectDirectory)\bin\$(Configuration)\$(Platform)\ + + + + NotUsing + Level3 + Disabled + true + WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + true + googletest\googletest\include;googletest\googletest;googletest\googlemock;googletest\googlemock\include;%(AdditionalIncludeDirectories) + MultiThreadedDebug + + + Windows + true + + + + + NotUsing + Level3 + Disabled + true + _DEBUG;_LIB;%(PreprocessorDefinitions) + true + googletest\googletest\include;googletest\googletest;googletest\googlemock;googletest\googlemock\include;%(AdditionalIncludeDirectories) + MultiThreadedDebug + + + Windows + true + + + + + NotUsing + Level3 + MaxSpeed + true + true + true + WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + true + googletest\googletest\include;googletest\googletest;googletest\googlemock;googletest\googlemock\include;%(AdditionalIncludeDirectories) + MultiThreaded + + + Windows + true + true + true + + + + + NotUsing + Level3 + MaxSpeed + true + true + true + NDEBUG;_LIB;%(PreprocessorDefinitions) + true + googletest\googletest\include;googletest\googletest;googletest\googlemock;googletest\googlemock\include;%(AdditionalIncludeDirectories) + MultiThreaded + + + Windows + true + true + true + + + + + + \ No newline at end of file diff --git a/src/IISIntegration/tools/GenerateNativeAssets.ps1 b/src/IISIntegration/tools/GenerateNativeAssets.ps1 new file mode 100644 index 0000000000..f4b98959ed --- /dev/null +++ b/src/IISIntegration/tools/GenerateNativeAssets.ps1 @@ -0,0 +1,150 @@ +$targetFile = Join-Path (Split-Path -parent $PSCommandPath) "..\build\assets.props"; + +$platforms = @( + @{ + Platform = "x64"; + VCPlatform = "x64"; + }, + @{ + Platform = "x86"; + VCPlatform = "Win32"; + } +); +$srcDir = "`$(MSBuildThisFileDirectory)..\src"; +$projects = @( + @{ + ProjectDirectory = "$srcDir\AspNetCoreModuleV1\AspNetCore"; + ProjectName = "AspNetCore.vcxproj"; + NativeAsset = "aspnetcore"; + BaseOutputPath = "AspNetCoreModuleV1" + PropetyName = "AspNetCoreModuleV1Shim" + }, + @{ + ProjectDirectory = "$srcDir\AspNetCoreModuleV2\AspNetCore"; + ProjectName = "`AspNetCore.vcxproj"; + NativeAsset = "aspnetcorev2"; + BaseOutputPath = "AspNetCoreModuleV2" + PropetyName = "AspNetCoreModuleV2Shim" + }, + @{ + ProjectDirectory = "$srcDir\AspNetCoreModuleV2\InProcessRequestHandler"; + ProjectName = "InProcessRequestHandler.vcxproj"; + NativeAsset = "aspnetcorev2_inprocess"; + BaseOutputPath = "AspNetCoreModuleV2"; + PropetyName = "AspNetCoreModuleV2InProcessHandler" + }, + @{ + ProjectDirectory = "$srcDir\AspNetCoreModuleV2\OutOfProcessRequestHandler"; + ProjectName = "OutOfProcessRequestHandler.vcxproj"; + NativeAsset = "aspnetcorev2_outofprocess"; + BaseOutputPath = "AspNetCoreModuleV2"; + PackageSubPath = "`$(AspNetCoreModuleOutOfProcessVersion)\"; + PropetyName = "AspNetCoreModuleV2OutOfProcessHandler" + } +); +$currentPlatform = @{ + Platform = "`$(NativePlatform)"; + VCPlatform = "`$(NativeVCPlatform)"; +}; +$components = @(); +$shimComponents = @(); +$inProcessComponents = @(); +$runShimComponents = @(); +$runInProcessComponents = @(); +$properties = @(); + +function CopyProperties($from, $to) +{ + foreach ($key in $from.Keys) + { + $to.Add($key, $from.$key); + } +} + +function Write-Group($group, $name) +{ + return $( + foreach ($item in $group){ + " <$name$(foreach ($pair in $item.GetEnumerator()) { + "`n $($pair.Key)=`"`"$($pair.Value)`"`""}) + />`n"}); +} +function Write-Properties($group) +{ + return $( + foreach ($item in $group){ + "`n <$($item.Name)>$($item.Value)"}); +} + +function New-Component($project, $platform) +{ + $component = [ordered]@{}; + CopyProperties -from $platform -to $component; + CopyProperties -from $project -to $component; + CopyProperties -from @{ + Include = "$($project.ProjectDirectory)\$($project.ProjectName)"; + DllLocation = "$($project.ProjectDirectory)\bin\`$(Configuration)\$($platform.VCPlatform)\$($project.NativeAsset).dll"; + PdbLocation = "$($project.ProjectDirectory)\bin\`$(Configuration)\$($platform.VCPlatform)\$($project.NativeAsset).pdb"; + } -to $component; + + return $component; +} + +foreach ($project in $projects) +{ + foreach ($platform in $platforms) + { + $component = New-Component $project $platform; + $components += $component; + + if ($project.ProjectName.Contains("InProcess")) + { + $inProcessComponents += $component; + } + else + { + $shimComponents += $component; + } + } + + $properties += @{ + Name = "$($project.PropetyName)Dll"; + Value = "$($project.ProjectDirectory)\bin\`$(Configuration)\`$(NativeVCPlatform)\$($project.NativeAsset).dll"; + }; + + $runComponent = New-Component $project $currentPlatform; + + if ($project.ProjectName.Contains("InProcess")) + { + $runInProcessComponents += $runComponent; + } + else + { + $runShimComponents += $runComponent; + } +} + +$content = @" + + + + true + x64 + `$(Platform) + Win32 + `$(NativePlatform) + + +$(Write-Group $components "Components" ) +$(Write-Group $shimComponents "ShimComponents") +$(Write-Group $inProcessComponents "InProcessComponents") +$(Write-Group $runShimComponents "RunShimComponents") +$(Write-Group $runInProcessComponents "RunInProcessComponents") + + +$(Write-Properties $properties) + + +"@; + +[IO.File]::WriteAllLines($targetFile, $content) diff --git a/src/IISIntegration/tools/SetupTestEnvironment.ps1 b/src/IISIntegration/tools/SetupTestEnvironment.ps1 new file mode 100644 index 0000000000..3adaf79045 --- /dev/null +++ b/src/IISIntegration/tools/SetupTestEnvironment.ps1 @@ -0,0 +1,135 @@ +param($Mode) + +$DumpFolder = "$env:ASPNETCORE_TEST_LOG_DIR\dumps" +if (!($DumpFolder)) +{ + $DumpFolder = "$PSScriptRoot\..\artifacts\dumps" +} +if (!(Test-Path $DumpFolder)) +{ + New-Item $DumpFolder -ItemType Directory; +} +$DumpFolder = Resolve-Path $DumpFolder + +$LogsFolder = "$PSScriptRoot\..\artifacts\logs" +if (!(Test-Path $LogsFolder)) +{ + New-Item $LogsFolder -ItemType Directory; +} +$LogsFolder = Resolve-Path $LogsFolder + +$werHive = "HKLM:\SOFTWARE\Microsoft\Windows\Windows Error Reporting"; +$ldHive = "$werHive\LocalDumps"; + + +function Setup-appverif($application) +{ + appverif.exe -enable Exceptions Handles Heaps Leak Locks Memory Threadpool TLS SRWLock -for $application + $level = 0x1E1; + $codes = @( + # Exceptions + 0x650, + # Handles + 0x300, 0x301, 0x302, 0x303, 0x304, # 0x305, + # Heaps + 0x001, 0x002, 0x003, 0x004, 0x005, 0x006, 0x007, 0x008, 0x009, 0x00A, 0x00B, 0x00C, 0x00D, 0x00E, 0x00F, 0x010, 0x011, 0x012, 0x013, 0x014, + # Leak + 0x900, 0x901, 0x902, 0x903, 0x904, 0x905, 0x906, + # Locks + 0x200, 0x201, 0x202, 0x203, 0x204, 0x205, 0x206, 0x207, 0x208, 0x209, 0x210, 0x211, 0x212, 0x213, 0x214, 0x215, + # Memory + 0x600, 0x601, 0x602, 0x603, 0x604, 0x605, 0x606, 0x607, 0x608, 0x609, 0x60A, 0x60B, 0x60C, 0x60D, 0x60E, 0x60F, 0x610, 0x612, 0x613, 0x614, 0x615, 0x616, 0x617, 0x618, 0x619, 0x61A, 0x61B, 0x61C, 0x61D, 0x61E, + # SRWLock + 0x250, 0x251, 0x252, 0x253, 0x254, 0x255, 0x256, 0x257, + # TSL + 0x350, 0x351, 0x352, + # ThreadPool + 0x700, 0x701, 0x702, 0x703, 0x704, 0x705, 0x706, 0x707, 0x708, 0x709, 0x70A, 0x70B, 0x70C, 0x70D + ); + + setx APPVERIFIER_ENABLED_CODES "$codes"; + setx APPVERIFIER_LEVEL $level; + appverif.exe -configure $codes -for $application -with ErrorReport=$level + + # 0x305, - disabled because coreclr.dll!SetThreadName(void *) ofthen passes invalid handle (0xffffff) + appverif.exe -configure 0x305 -for $application -with ErrorReport=0 +} + +function Shutdown-appverif($application) +{ + setx APPVERIFIER_ENABLED_CODES "NONE"; + setx APPVERIFIER_LEVEL "NONE"; + + appverif.exe -disable * -for $application +} + +function Setup-Dumps() +{ + if (!(Test-Path $ldHive )) + { + New-Item -Path $werHive -Name LocalDumps + } + + Move-Item $env:windir\System32\vsjitdebugger.exe $env:windir\System32\_vsjitdebugger.exe; + + New-ItemProperty $werHive -Name "DontShowUI" -Value 1 -PropertyType "DWORD" -Force; + + New-ItemProperty $ldHive -Name "DumpFolder" -Value $DumpFolder -PropertyType "ExpandString" -Force; + New-ItemProperty $ldHive -Name "DumpCount" -Value 15 -PropertyType "DWORD" -Force; + New-ItemProperty $ldHive -Name "DumpType" -Value 2 -PropertyType "DWORD" -Force; + + Restart-Service WerSvc +} + +function Shutdown-Dumps() +{ + Move-Item $env:windir\System32\_vsjitdebugger.exe $env:windir\System32\vsjitdebugger.exe; + + Remove-Item $ldHive -Recurse -Force + + New-ItemProperty $werHive -Name "DontShowUI" -Value 0 -PropertyType "DWORD" -Force; + + $cdb = "c:\Program Files (x86)\Windows Kits\10\Debuggers\x64\cdb.exe" + if (!(Test-Path $cdb)) + { + $downloadedFile = [System.IO.Path]::GetTempFileName(); + $downloadedFile = "$downloadedFile.exe"; + Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?linkid=870807" -OutFile $downloadedFile; + & $downloadedFile /features OptionId.WindowsDesktopDebuggers /norestart /q; + } + + foreach ($dump in (Get-ChildItem -Path $DumpFolder -Filter "*.dmp")) + { + if (Test-Path $cdb) + { + & $cdb -z $dump.FullName -y "https://msdl.microsoft.com/download/symbols" -c ".loadby sos coreclr;!sym noisy;.reload /f;.dumpcab -a $($dump.FullName).cab;q;" + Remove-Item $dump.FullName + } + } +} + +if ($Mode -eq "Setup") +{ + Setup-appverif w3wp.exe + Setup-appverif iisexpress.exe + + Setup-Dumps; +} + +if ($Mode -eq "SetupDumps") +{ + Shutdown-appverif w3wp.exe + Shutdown-appverif iisexpress.exe + + Setup-Dumps; +} + +if ($Mode -eq "Shutdown") +{ + Shutdown-appverif w3wp.exe + Shutdown-appverif iisexpress.exe + + Shutdown-Dumps; +} + +Exit 0; \ No newline at end of file diff --git a/src/IISIntegration/tools/UpdateIISExpressCertificate.ps1 b/src/IISIntegration/tools/UpdateIISExpressCertificate.ps1 new file mode 100644 index 0000000000..9034cf8f75 --- /dev/null +++ b/src/IISIntegration/tools/UpdateIISExpressCertificate.ps1 @@ -0,0 +1,20 @@ +$cert = New-SelfSignedCertificate -DnsName "localhost", "localhost" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(5) +$thumb = $cert.GetCertHashString() + +$Store = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList 'root', 'LocalMachine' +$Store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) +$Store.Add($cert) +$Store.Close() + +$tempFile = [System.IO.Path]::GetTempFileName(); +$content = ""; + +for ($i=44300; $i -le 44399; $i++) { + $content += "http delete sslcert ipport=0.0.0.0:$i`n"; + $content += "http add sslcert ipport=0.0.0.0:$i certhash=$thumb appid=`{214124cd-d05b-4309-9af9-9caa44b2b74a`}`n"; +} + +[IO.File]::WriteAllLines($tempFile, $content) + +netsh -f $tempFile +Remove-Item $tempFile; \ No newline at end of file diff --git a/src/IISIntegration/tools/certificate.ps1 b/src/IISIntegration/tools/certificate.ps1 new file mode 100644 index 0000000000..52c8817796 --- /dev/null +++ b/src/IISIntegration/tools/certificate.ps1 @@ -0,0 +1,499 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. + +<############################################################################## + Example + + ############################################################################### + # Create a new root certificate on "Cert:\LocalMachine\My" and export it to "Cert:\LocalMachine\Root" + # FYI, you can do the same thing with one of the following commands: + # %sdxroot\tools\amd64\MakeCert.exe -r -pe -n "CN=ANCMTest_Root" -b 12/22/2013 -e 12/23/2020 -ss root -sr localmachine -len 2048 -a sha256 + # $thumbPrint = (New-SelfSignedCertificate -DnsName "ANCMTest_Root", "ANCMTest_Roo3" -CertStoreLocation "cert:\LocalMachine\My").Thumbprint + ############################################################################### + $rootSubject = "ANCMTest_Root" + $thumbPrint = .\certificate.ps1 -Command Create-SelfSignedCertificate -Subject $rootSubject + .\certificate.ps1 -Command Export-CertificateTo -TargetThumbPrint $thumbPrint -TargetSSLStore "Cert:\LocalMachine\My" -ExportToSSLStore "Cert:\LocalMachine\Root" + .\certificate.ps1 -Command Get-CertificateThumbPrint -Subject $rootSubject -TargetSSLStore "Cert:\LocalMachine\Root" + + ############################################################################### + # Create a new certificate setting issuer with the root certicate's subject name on "Cert:\LocalMachine\My" and export it to "Cert:\LocalMachine\Root" + # FYI, you can do the same thing with one of the following commands: + # %sdxroot\tools\amd64\MakeCert.exe -pe -n "CN=ANCMTestWebServer" -b 12/22/2013 -e 12/23/2020 -eku 1.3.6.1.5.5.7.3.1 -is root -ir localmachine -in $rootSubject -len 2048 -ss my -sr localmachine -a sha256 + # %sdxroot\tools\amd64\MakeCert.exe -pe -n "CN=ANCMTest_Client" -eku 1.3.6.1.5.5.7.3.2 -is root -ir localmachine -in ANCMTest_Root -ss my -sr currentuser -len 2048 -a sha256 + ############################################################################### + $childSubject = "ANCMTest_Client" + $thumbPrint2 = .\certificate.ps1 -Command Create-SelfSignedCertificate -Subject $childSubject -IssuerName $rootSubject + ("Result: $thumbPrint2") + .\certificate.ps1 -Command Export-CertificateTo -TargetThumbPrint $thumbPrint2 -TargetSSLStore "Cert:\LocalMachine\My" -ExportToSSLStore "Cert:\CurrentUser\My" + + .\certificate.ps1 -Command Export-CertificateTo -TargetThumbPrint $thumbPrint2 -TargetSSLStore "Cert:\LocalMachine\My" -ExportToSSLStore C:\gitroot\AspNetCoreModule\tools\test.pfx -PfxPassword test + + + # Clean up + .\certificate.ps1 -Command Delete-Certificate -TargetThumbPrint $thumbPrint2 -TargetSSLStore "Cert:\LocalMachine\My" + .\certificate.ps1 -Command Delete-Certificate -TargetThumbPrint $thumbPrint2 -TargetSSLStore "Cert:\CurrentUser\Root" + .\certificate.ps1 -Command Delete-Certificate -TargetThumbPrint $thumbPrint -TargetSSLStore "Cert:\LocalMachine\My" + .\certificate.ps1 -Command Delete-Certificate -TargetThumbPrint $thumbPrint -TargetSSLStore "Cert:\LocalMachine\Root" + +###############################################################################> + + +Param( + [parameter(Mandatory=$true , Position=0)] + [ValidateSet("Create-SelfSignedCertificate", + "Delete-Certificate", + "Export-CertificateTo", + "Get-CertificateThumbPrint", + "Get-CertificatePublicKey")] + [string] + $Command, + + [parameter()] + [string] + $Subject, + + [parameter()] + [string] + $IssuerName, + + [Parameter()] + [string] + $FriendlyName = "", + + [Parameter()] + [string[]] + $AlternativeNames = "", + + [Parameter()] + [string] + $TargetSSLStore = "", + + [Parameter()] + [string] + $ExportToSSLStore = "", + + [Parameter()] + [string] + $PfxPassword = "", + + [Parameter()] + [string] + $TargetThumbPrint = "" +) + +function Create-SelfSignedCertificate($_subject, $_friendlyName, $_alternativeNames, $_issuerName) { + + if (-not $_subject) + { + return ("Error!!! _subject is required") + } + + # + # $_issuerName should be set with the value subject and its certificate path will be root path + if (-not $_issuerName) + { + $_issuerName = $_subject + } + + # + # Create $subjectDn and $issuerDn + $subjectDn = new-object -com "X509Enrollment.CX500DistinguishedName" + $subjectDn.Encode( "CN=" + $_subject, $subjectDn.X500NameFlags.X500NameFlags.XCN_CERT_NAME_STR_NONE) + $issuerDn = new-object -com "X509Enrollment.CX500DistinguishedName" + $issuerDn.Encode("CN=" + $_issuerName, $subjectDn.X500NameFlags.X500NameFlags.XCN_CERT_NAME_STR_NONE) + + # + # Create a new Private Key + $key = new-object -com "X509Enrollment.CX509PrivateKey" + $key.ProviderName = "Microsoft Enhanced RSA and AES Cryptographic Provider" + # XCN_AT_SIGNATURE, The key can be used for signing + $key.KeySpec = 2 + $key.Length = 2048 + # MachineContext 0: Current User, 1: Local Machine + $key.MachineContext = 1 + $key.Create() + + # + # Create a cert object with the newly created private key + $cert = new-object -com "X509Enrollment.CX509CertificateRequestCertificate" + $cert.InitializeFromPrivateKey(2, $key, "") + $cert.Subject = $subjectDn + $cert.Issuer = $issuerDn + $cert.NotBefore = (get-date).AddMinutes(-10) + $cert.NotAfter = $cert.NotBefore.AddYears(2) + + #Use Sha256 + $hashAlgorithm = New-Object -ComObject X509Enrollment.CObjectId + $hashAlgorithm.InitializeFromAlgorithmName(1,0,0,"SHA256") + $cert.HashAlgorithm = $hashAlgorithm + + # + # Key usage should be set for non-root certificate + if ($_issuerName -ne $_subject) + { + # + # Extended key usage + $clientAuthOid = New-Object -ComObject "X509Enrollment.CObjectId" + $clientAuthOid.InitializeFromValue("1.3.6.1.5.5.7.3.2") + $serverAuthOid = new-object -com "X509Enrollment.CObjectId" + $serverAuthOid.InitializeFromValue("1.3.6.1.5.5.7.3.1") + $ekuOids = new-object -com "X509Enrollment.CObjectIds.1" + $ekuOids.add($clientAuthOid) + $ekuOids.add($serverAuthOid) + $ekuExt = new-object -com "X509Enrollment.CX509ExtensionEnhancedKeyUsage" + $ekuExt.InitializeEncode($ekuOids) + $cert.X509Extensions.Add($ekuext) + + # + #Set Key usage + $keyUsage = New-Object -com "X509Enrollment.cx509extensionkeyusage" + # XCN_CERT_KEY_ENCIPHERMENT_KEY_USAGE + $flags = 0x20 + # XCN_CERT_DIGITAL_SIGNATURE_KEY_USAGE + $flags = $flags -bor 0x80 + $keyUsage.InitializeEncode($flags) + $cert.X509Extensions.Add($keyUsage) + } + + # + # Subject alternative names + if ($_alternativeNames -ne $null) { + $names = new-object -com "X509Enrollment.CAlternativeNames" + $altNames = new-object -com "X509Enrollment.CX509ExtensionAlternativeNames" + foreach ($n in $_alternativeNames) { + $name = new-object -com "X509Enrollment.CAlternativeName" + # Dns Alternative Name + $name.InitializeFromString(3, $n) + $names.Add($name) + } + $altNames.InitializeEncode($names) + $cert.X509Extensions.Add($altNames) + } + + $cert.Encode() + + #$locator = $(New-Object "System.Guid").ToString() + $locator = [guid]::NewGuid().ToString() + $enrollment = new-object -com "X509Enrollment.CX509Enrollment" + $enrollment.CertificateFriendlyName = $locator + $enrollment.InitializeFromRequest($cert) + $certdata = $enrollment.CreateRequest(0) + $enrollment.InstallResponse(2, $certdata, 0, "") + + # Wait for certificate to be populated + $end = $(Get-Date).AddSeconds(1) + do { + $Certificates = Get-ChildItem Cert:\LocalMachine\My + foreach ($item in $Certificates) + { + if ($item.FriendlyName -eq $locator) + { + $CACertificate = $item + } + } + } while ($CACertificate -eq $null -and $(Get-Date) -lt $end) + + $thumbPrint = "" + if ($CACertificate -and $CACertificate.Thumbprint) + { + $thumbPrint = $CACertificate.Thumbprint.Trim() + } + return $thumbPrint +} + +function Delete-Certificate($_targetThumbPrint, $_targetSSLStore = $TargetSSLStore) { + + if (-not $_targetThumbPrint) + { + return ("Error!!! _targetThumbPrint is required") + } + + if (Test-Path "$_targetSSLStore\$_targetThumbPrint") + { + Remove-Item "$_targetSSLStore\$_targetThumbPrint" -Force -Confirm:$false + } + + if (Test-Path "$_targetSSLStore\$_targetThumbPrint") + { + return ("Error!!! Failed to delete a certificate of $_targetThumbPrint") + } +} + +function Export-CertificateTo($_targetThumbPrint, $_exportToSSLStore, $_password) +{ + if (-not $_targetThumbPrint) + { + return ("Error!!! _targetThumbPrint is required") + } + + if (-not (Test-Path "$TargetSSLStore\$_targetThumbPrint")) + { + return ("Error!!! Export failed. Can't find target certificate: $TargetSSLStore\$_targetThumbPrint") + } + + $cert = Get-Item "$TargetSSLStore\$_targetThumbPrint" + $tempExportFile = "$env:temp\_tempCertificate.cer" + if (Test-Path $tempExportFile) + { + Remove-Item $tempExportFile -Force -Confirm:$false + } + + $isThisWin7 = $false + $exportToSSLStoreName = $null + $exportToSSLStoreLocation = $null + $targetSSLStoreName = $null + $targetSSLStoreLocation = $null + + if ((Get-Command Export-Certificate 2> out-null) -eq $null) + { + $isThisWin7 = $true + } + + # if _exportToSSLStore points to a .pfx file + if ($exportToSSLStore.ToLower().EndsWith(".pfx")) + { + if (-not $_password) + { + return ("Error!!! _password is required") + } + + if ($isThisWin7) + { + if ($TargetSSLStore.ToLower().Contains("my")) + { + $targetSSLStoreName = "My" + } + elseif ($_exportToSSLStore.ToLower().Contains("root")) + { + $targetSSLStoreName = "Root" + } + else + { + throw ("Unsupported store name " + $TargetSSLStore) + } + if ($TargetSSLStore.ToLower().Contains("localmachine")) + { + $targetSSLStoreLocation = "LocalMachine" + } + else + { + throw ("Unsupported store location name " + $TargetSSLStore) + } + + &certutil.exe @('-exportpfx', '-p', $_password, $targetSSLStoreName, $_targetThumbPrint, $_exportToSSLStore) | out-null + + if ( Test-Path $_exportToSSLStore ) + { + # Succeeded to export to .pfx file + return + } + else + { + return ("Error!!! Can't export $TargetSSLStore\$_targetThumbPrint to $tempExportFile") + } + } + else + { + $securedPassword = ConvertTo-SecureString -String $_password -Force –AsPlainText + $exportedPfxFile = Export-PfxCertificate -FilePath $_exportToSSLStore -Cert $TargetSSLStore\$_targetThumbPrint -Password $securedPassword + if ( ($exportedPfxFile -ne $null) -and (Test-Path $exportedPfxFile.FullName) ) + { + # Succeeded to export to .pfx file + return + } + else + { + return ("Error!!! Can't export $TargetSSLStore\$_targetThumbPrint to $tempExportFile") + } + } + } + + if ($isThisWin7) + { + # Initialize variables for Win7 + if ($_exportToSSLStore.ToLower().Contains("my")) + { + $exportToSSLStoreName = [System.Security.Cryptography.X509Certificates.StoreName]::My + } + elseif ($_exportToSSLStore.ToLower().Contains("root")) + { + $exportToSSLStoreName = [System.Security.Cryptography.X509Certificates.StoreName]::Root + } + else + { + throw ("Unsupported store name " + $_exportToSSLStore) + } + if ($_exportToSSLStore.ToLower().Contains("localmachine")) + { + $exportToSSLStoreLocation = [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine + } + elseif ($_exportToSSLStore.ToLower().Contains("currentuser")) + { + $exportToSSLStoreLocation = [System.Security.Cryptography.X509Certificates.StoreLocation]::CurrentUser + } + else + { + throw ("Unsupported store location name " + $_exportToSSLStore) + } + + # Export-Certificate is not available. + $isThisWin7 = $true + $certificate = Get-Item "$TargetSSLStore\$_targetThumbPrint" + $base64certificate = @" +-----BEGIN CERTIFICATE----- +$([Convert]::ToBase64String($certificate.Export('Cert'), [System.Base64FormattingOptions]::InsertLineBreaks))) +-----END CERTIFICATE----- +"@ + Set-Content -Path $tempExportFile -Value $base64certificate | Out-Null + } + else + { + Export-Certificate -Cert $cert -FilePath $tempExportFile | Out-Null + if (-not (Test-Path $tempExportFile)) + { + return ("Error!!! Can't export $TargetSSLStore\$_targetThumbPrint to $tempExportFile") + } + } + + if ($isThisWin7) + { + [Reflection.Assembly]::Load("System.Security, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=b03f5f7f11d50a3a") | Out-Null + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($tempExportFile) + $store = New-Object System.Security.Cryptography.X509Certificates.X509Store($exportToSSLStoreName,$exportToSSLStoreLocation) + $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) | Out-Null + $store.Add($cert) | Out-Null + } + else + { + # clean up destination SSL store + Delete-Certificate $_targetThumbPrint $_exportToSSLStore + if (Test-Path "$_exportToSSLStore\$_targetThumbPrint") + { + return ("Error!!! Can't delete already existing one $_exportToSSLStore\$_targetThumbPrint") + } + Import-Certificate -CertStoreLocation $_exportToSSLStore -FilePath $tempExportFile | Out-Null + } + + Sleep 3 + if (-not (Test-Path "$_exportToSSLStore\$_targetThumbPrint")) + { + return ("Error!!! Can't copy $TargetSSLStore\$_targetThumbPrint to $_exportToSSLStore") + } +} + +function Get-CertificateThumbPrint($_subject, $_issuerName, $_targetSSLStore) +{ + if (-not $_subject) + { + return ("Error!!! _subject is required") + } + if (-not $_targetSSLStore) + { + return ("Error!!! _targetSSLStore is required") + } + + if (-not (Test-Path "$_targetSSLStore")) + { + return ("Error!!! Can't find target store") + } + + $targetCertificate = $null + + $Certificates = Get-ChildItem $_targetSSLStore + foreach ($item in $Certificates) + { + $findItem = $false + # check subject name first + if ($item.Subject.ToLower() -eq "CN=$_subject".ToLower()) + { + $findItem = $true + } + + # check issuerName as well + if ($_issuerName -and $item.Issuer.ToLower() -ne "CN=$_issuerName".ToLower()) + { + $findItem = $false + } + + if ($findItem) + { + $targetCertificate = $item + break + } + } + $result = "" + if ($targetCertificate) + { + $result = $targetCertificate.Thumbprint + } + else + { + ("Error!!! Can't find target certificate") + } + return $result +} + +function Get-CertificatePublicKey($_targetThumbPrint) +{ + if (-not $_targetThumbPrint) + { + return ("Error!!! _targetThumbPrint is required") + } + + if (-not (Test-Path "$TargetSSLStore\$_targetThumbPrint")) + { + return ("Error!!! Can't find target certificate") + } + + $cert = Get-Item "$TargetSSLStore\$_targetThumbPrint" + $byteArray = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert) + $publicKey = [System.Convert]::ToBase64String($byteArray).Trim() + + return $publicKey +} + +# Error handling and initializing default values +if (-not $TargetSSLStore) +{ + $TargetSSLStore = "Cert:\LocalMachine\My" +} +else +{ + if ($Command -eq "Create-SelfSignedCertificate") + { + return ("Error!!! Create-SelfSignedCertificate should use default value for -TargetSSLStore if -Issuer is not provided") + } +} + +if (-not $ExportToSSLStore) +{ + $ExportToSSLStore = "Cert:\LocalMachine\Root" +} + +switch ($Command) +{ + "Create-SelfSignedCertificate" + { + return Create-SelfSignedCertificate $Subject $FriendlyName $AlternativeNames $IssuerName + } + "Delete-Certificate" + { + return Delete-Certificate $TargetThumbPrint + } + "Export-CertificateTo" + { + return Export-CertificateTo $TargetThumbPrint $ExportToSSLStore $PfxPassword + } + "Get-CertificateThumbPrint" + { + return Get-CertificateThumbPrint $Subject $IssuerName $TargetSSLStore + } + "Get-CertificatePublicKey" + { + return Get-CertificatePublicKey $TargetThumbPrint + } + default + { + throw "Unknown command" + } +} diff --git a/src/IISIntegration/tools/httpsys.ps1 b/src/IISIntegration/tools/httpsys.ps1 new file mode 100644 index 0000000000..af2254e96f --- /dev/null +++ b/src/IISIntegration/tools/httpsys.ps1 @@ -0,0 +1,394 @@ +# Copyright (c) .NET Foundation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. + +############################################################################## +# Example +# $result = .\httpsys.ps1 -Command Get-SslBinding -IpAddress "0x00" -Port 46300 +# .\httpsys.ps1 -Command Add-SslBinding -IpAddress "0x00" -Port 46300 –Thumbprint $result.CertificateHash +# .\httpsys.ps1 -Command Delete-SslBinding -IpAddress "0x00" -Port 46300 +############################################################################## + +Param ( + [parameter(Mandatory=$true , Position=0)] + [ValidateSet("Add-SslBinding", + "Delete-SslBinding", + "Get-SslBinding")] + [string] + $Command, + + [parameter()] + [string] + $IpAddress, + + [parameter()] + [string] + $Port, + + [parameter()] + [string] + $Thumbprint, + + [parameter()] + [string] + $TargetSSLStore, + + [parameter()] + [string] + $AppId, + + [parameter()] + [System.Net.IPEndPoint] + $IpEndPoint + ) + + +# adjust parameter variables +if (-not $IpEndPoint) +{ + if ($IpAddress -and $Port) + { + $IpEndPoint = New-Object "System.Net.IPEndPoint" -ArgumentList $IpAddress,$Port + } +} + +if (-not $TargetSSLStore) +{ + $TargetSSLStore = "Cert:\LocalMachine\My" +} + +$StoreName = ($TargetSSLStore.Split("\") | Select-Object -Last 1).Trim() + +$Certificate = Get-Item "$TargetSSLStore\$Thumbprint" + +if (-not $AppId) +{ + # Assign a random GUID for $AppId + $AppId = [guid]::NewGuid() +} + +$cs = ' +namespace Microsoft.IIS.Administration.Setup { +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Runtime.InteropServices; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.ComponentModel; + + public class Http { + public const int HTTP_INITIALIZE_CONFIG = 2; + public const int HTTP_SERVICE_CONFIG_SSLCERT_INFO = 1; + + [DllImport("httpapi.dll", CharSet = CharSet.Auto, PreserveSig = true)] + public static extern uint HttpDeleteServiceConfiguration(IntPtr ServiceHandle, int ConfigId, ref HTTP_SERVICE_CONFIG_SSL_SET pConfigInformation, int ConfigInformationLength, IntPtr pOverlapped); + + [DllImport("httpapi.dll", CharSet = CharSet.Auto, PreserveSig = true)] + public static extern uint HttpInitialize(HTTPAPI_VERSION version, uint flags, IntPtr pReserved); + + [DllImport("httpapi.dll", EntryPoint = "HttpQueryServiceConfiguration", + CharSet = CharSet.Unicode, ExactSpelling = true, + CallingConvention = CallingConvention.StdCall)] + public static extern uint HttpQueryServiceConfiguration( + IntPtr serviceHandle, + HTTP_SERVICE_CONFIG_ID configID, + ref HTTP_SERVICE_CONFIG_SSL_QUERY pInputConfigInfo, + UInt32 InputConfigInfoLength, + IntPtr pOutputConfigInfo, + UInt32 OutputConfigInfoLength, + [In, Out] ref UInt32 pReturnLength, + IntPtr pOverlapped + ); + + [DllImport("httpapi.dll", CharSet = CharSet.Auto, PreserveSig = true)] + public static extern uint HttpSetServiceConfiguration(IntPtr ServiceHandle, int ConfigId, ref HTTP_SERVICE_CONFIG_SSL_SET pConfigInformation, int ConfigInformationLength, IntPtr pOverlapped); + + [DllImport("httpapi.dll", CharSet = CharSet.Auto, PreserveSig = true)] + public static extern uint HttpTerminate(uint flags, IntPtr pReserved); + + public static HTTP_SERVICE_CONFIG_SSL_SET MarshalConfigSslSet(IntPtr ptr) { + return (HTTP_SERVICE_CONFIG_SSL_SET)Marshal.PtrToStructure(ptr, typeof(HTTP_SERVICE_CONFIG_SSL_SET)); + } + } + + public enum HTTP_SERVICE_CONFIG_ID { + HttpServiceConfigIPListenList, + HttpServiceConfigSSLCertInfo, + HttpServiceConfigUrlAclInfo, + HttpServiceConfigMax + } + + public enum HTTP_SERVICE_CONFIG_QUERY_TYPE { + HttpServiceConfigQueryExact, + HttpServiceConfigQueryNext, + HttpServiceConfigQueryMax + } + + [StructLayout(LayoutKind.Sequential)] + public struct HTTPAPI_VERSION { + public ushort HttpApiMajorVersion; + public ushort HttpApiMinorVersion; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public struct HTTP_SERVICE_CONFIG_SSL_KEY { + public IntPtr pIpPort; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public struct HTTP_SERVICE_CONFIG_SSL_QUERY { + public HTTP_SERVICE_CONFIG_QUERY_TYPE QueryDesc; + public IntPtr KeyDesc; + public Int32 dwToken; + } + + [StructLayout(LayoutKind.Sequential)] + public struct HTTP_SERVICE_CONFIG_SSL_SET { + public IntPtr KeyDesc; + public uint SslHashLength; + public IntPtr pSslHash; + public Guid AppId; + [MarshalAs(UnmanagedType.LPWStr)] + public string pSslCertStoreName; + public int DefaultCertCheckMode; + public int DefaultRevocationFreshnessTime; + public int DefaultRecovationUrlRetrievalTimeout; + [MarshalAs(UnmanagedType.LPWStr)] + public string pDefaultSslCtlIdentifier; + [MarshalAs(UnmanagedType.LPWStr)] + public string pDefaultSslCtlStoreName; + public int DefaultFlags; + } +} +' + +$SUCCESS = 0 +function InitializeInterop() { + try { + [Microsoft.IIS.Administration.Setup.Http] | Out-Null + } + catch { + Add-Type $cs + } +} + +function GetIpEndpointBytes($_ipEndpoint) { + $socketAddress = $_ipEndpoint.Serialize() + $ipBytes = [System.Array]::CreateInstance([System.Byte], $socketAddress.Size) + for ($i = 0; $i -lt $socketAddress.Size; $i++) { + $ipBytes[$i] = $socketAddress[$i] + } + return $ipBytes +} + +function GetBindingInfo($sslConfig) { + $hash = [System.Array]::CreateInstance([System.Byte], [int]($sslConfig.SslHashLength)) + [System.Runtime.InteropServices.Marshal]::Copy($sslConfig.pSslHash, $hash, 0, $sslConfig.SslHashLength) + + $socketAddressLength = 16 + $sa = [System.Array]::CreateInstance([System.Byte], $socketAddressLength) + [System.Runtime.InteropServices.Marshal]::Copy($sslConfig.KeyDesc, $sa, 0, $socketAddressLength) + $socketAddress = New-Object "System.Net.SocketAddress" -ArgumentList ([System.Net.Sockets.AddressFamily]::InterNetwork, $socketAddressLength) + for ($i = 0; $i -lt $sa.Length; $i++) { + $socketAddress[$i] = $sa[$i] + } + + $ep = New-Object "System.Net.IPEndPoint" -ArgumentList ([ipaddress]::Any, 0) + $endpoint = [System.Net.IPEndPoint]$ep.Create($socketAddress) + + $ret = @{} + $ret.CertificateHash = [System.BitConverter]::ToString($hash).Replace("-", "") + $ret.AppId = $sslConfig.AppId + $ret.IpEndpoint = $endpoint + return $ret +} + +function InitializeHttpSys() { + $v = New-Object "Microsoft.IIS.Administration.Setup.HTTPAPI_VERSION" + $V.HttpApiMajorVersion = 1 + $v.HttpApiMinorVersion = 0 + + $result = [Microsoft.IIS.Administration.Setup.Http]::HttpInitialize($v, [Microsoft.IIS.Administration.Setup.Http]::HTTP_INITIALIZE_CONFIG, [System.IntPtr]::Zero) + + if ($result -ne $SUCCESS) { + Write-Warning "Error initializing Http API" + throw [System.ComponentModel.Win32Exception] $([System.int32]$result) + } + + return $result +} + +function TerminateHttpSys() { + return [Microsoft.IIS.Administration.Setup.Http]::HttpTerminate([Microsoft.IIS.Administration.Setup.Http]::HTTP_INITIALIZE_CONFIG, [System.IntPtr]::Zero) +} + +function Add-SslBinding($_ipEndpoint, $_certificate, $_appId) { + if ($_ipEndpoint -eq $null) { + throw "Ip Endpoint required." + } + + if ($_certificate -eq $null) { + throw "Certificate required." + } + + if ($appId -eq $null) { + throw "App id required." + } + + <# FYI, [System.Guid]::Parse() is not supported in lower version of powershell + if (-not($_appId -is [System.Guid])) { + $_appId = [System.Guid]::Parse($_appId) + } + #> + + setSslConfiguration $_ipEndpoint $_certificate $_appId +} + +function Delete-SslBinding($_ipEndpoint) { + + if ($_ipEndpoint -eq $null) { + throw "Ip Endpoint required." + } + + setSslConfiguration $_ipEndpoint $null $([System.Guid]::Empty) +} + +function Get-SslBinding($_ipEndpoint) { + + if ($_ipEndpoint -eq $null) { + throw "Ip Endpoint required." + } + + $bufferSize = 4096 + try { + InitializeHttpSys| Out-Null + + $ipBytes = [System.Byte[]]$(GetIpEndpointBytes($_ipEndpoint)) + $hIp = [System.Runtime.InteropServices.GCHandle]::Alloc($ipBytes, [System.Runtime.InteropServices.GCHandleType]::Pinned) + $pIp = $hIp.AddrOfPinnedObject() + + $queryParam = New-Object "Microsoft.IIS.Administration.Setup.HTTP_SERVICE_CONFIG_SSL_QUERY" + $queryParam.QueryDesc = [Microsoft.IIS.Administration.Setup.HTTP_SERVICE_CONFIG_QUERY_TYPE]::HttpServiceConfigQueryExact + $queryParam.dwToken = 0 + $queryParam.KeyDesc = $pIp + + $returnLen = 0 + $pReturnSet = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($bufferSize) + + $result = [Microsoft.IIS.Administration.Setup.Http]::HttpQueryServiceConfiguration( + [System.IntPtr]::Zero, + [Microsoft.IIS.Administration.Setup.HTTP_SERVICE_CONFIG_ID]::HttpServiceConfigSSLCertInfo, + [ref] $queryParam, + [uint32]([System.Runtime.InteropServices.Marshal]::SizeOf($queryParam)), + $pReturnSet, + $bufferSize, + [ref] $returnLen, + [System.IntPtr]::Zero) + + if ($result -eq 2) { + # File not found + return $null + } + if ($result -ne $SUCCESS) { + Write-Warning "Error reading Ssl Cert Configuration" + throw [System.ComponentModel.Win32Exception] $([System.int32]$result) + } + $sslConfig = [Microsoft.IIS.Administration.Setup.Http]::MarshalConfigSslSet($pReturnSet) + return GetBindingInfo $sslConfig + } + finally { + if ($hIp -ne $null) { + $hIp.Free() + $hIp = $null + } + if ($pReturnSet -ne [System.IntPtr]::Zero) { + [System.Runtime.InteropServices.Marshal]::FreeHGlobal($pReturnSet) + $pReturnSet = [System.IntPtr]::Zero + } + TerminateHttpSys | Out-Null + } +} + +function setSslConfiguration($_ipEndpoint, $_certificate, $_appId) { + + try { + InitializeHttpSys| Out-Null + + $sslSet = New-Object "Microsoft.IIS.Administration.Setup.HTTP_SERVICE_CONFIG_SSL_SET" + $sslSetSize = [System.Runtime.InteropServices.Marshal]::SizeOf($sslSet) + + $ipBytes = [System.Byte[]]$(GetIpEndpointBytes($_ipEndpoint)) + $hIp = [System.Runtime.InteropServices.GCHandle]::Alloc($ipBytes, [System.Runtime.InteropServices.GCHandleType]::Pinned) + $pIp = $hIp.AddrOfPinnedObject() + + $sslSet.KeyDesc = $pIp # IntPtr + $sslSet.SslHashLength = 0 + $sslSet.pSslHash = [System.IntPtr]::Zero + $sslSet.pSslCertStoreName = [System.IntPtr]::Zero + $sslSet.AppId = $_appId + + if ($_certificate -ne $null) { + # Create binding + + $certBytes = $_certificate.GetCertHash() + $hCertBytes = [System.Runtime.InteropServices.GCHandle]::Alloc($certBytes, [System.Runtime.InteropServices.GCHandleType]::Pinned) + $pCertBytes = $hCertBytes.AddrOfPinnedObject() + + $sslSet.SslHashLength = 20 + $sslSet.pSslHash = $pCertBytes + $sslSet.pSslCertStoreName = $StoreName + + $result = [Microsoft.IIS.Administration.Setup.Http]::HttpSetServiceConfiguration([System.IntPtr]::Zero, + [Microsoft.IIS.Administration.Setup.Http]::HTTP_SERVICE_CONFIG_SSLCERT_INFO, + [ref]$sslSet, + $sslSetSize, + [System.IntPtr]::Zero) + } + else { + #Delete binding + $result = [Microsoft.IIS.Administration.Setup.Http]::HttpDeleteServiceConfiguration([System.IntPtr]::Zero, + [Microsoft.IIS.Administration.Setup.Http]::HTTP_SERVICE_CONFIG_SSLCERT_INFO, + [ref]$sslSet, + $sslSetSize, + [System.IntPtr]::Zero) + } + + if ($result -ne $SUCCESS) { + Write-Warning "Error setting Ssl Cert Configuration" + throw [System.ComponentModel.Win32Exception] $([System.int32]$result) + } + } + finally { + if ($hIp -ne $null) { + $hIp.Free() + $hIp = $null + } + if ($hCertBytes -ne $null) { + $hCertBytes.Free() + $hCertBytes = $null + } + TerminateHttpSys| Out-Null + } +} + +InitializeInterop +switch ($Command) +{ + "Add-SslBinding" + { + return Add-SslBinding $IpEndPoint $Certificate $AppId + } + "Delete-SslBinding" + { + return Delete-SslBinding $IpEndpoint + } + "Get-SslBinding" + { + return Get-SslBinding $IpEndpoint + } + default + { + throw "Unknown command" + } +} \ No newline at end of file diff --git a/src/IISIntegration/tools/installancm.ps1 b/src/IISIntegration/tools/installancm.ps1 new file mode 100644 index 0000000000..b54a830d0e --- /dev/null +++ b/src/IISIntegration/tools/installancm.ps1 @@ -0,0 +1,471 @@ +<# +.SYNOPSIS + Installs asnetcore to IISExpress and IIS directory +.DESCRIPTION + Installs asnetcore to IISExpress and IIS directory +.PARAMETER Rollback + Default: $false + Rollback the updated files with the original files +.PARAMETER ForceToBackup + Default: $false + Force to do the initial backup again (this parameter is meaningful only when you want to replace the existing backup file) +.PARAMETER Extract + Default: $false + Search ANCM nugetfile and extract the file to the path of the ExtractFilesTo parameter value +.PARAMETER PackagePath + Default: $PSScriptRoot\..\..\artifacts + Root path where ANCM nuget package is placed +.PARAMETER ExtractFilesTo + Default: $PSScriptRoot\..\..\artifacts" + Output path where aspentcore.dll file is extracted + +Example: + .\installancm.ps1 "C:\Users\jhkim\AppData\Local\Temp\ihvufnf1.atw\ancm\Debug" + +#> +[cmdletbinding()] +param( + [Parameter(Mandatory=$false, Position = 0)] + [string] $ExtractFilesTo="$PSScriptRoot\..\artifacts\build\AspNetCore\bin\Debug", + [Parameter(Mandatory=$false, Position = 1)] + [string] $PackagePath="$PSScriptRoot\..\artifacts\build", + [Parameter(Mandatory=$false)] + [switch] $Rollback=$false, + [Parameter(Mandatory=$false)] + [switch] $ForceToBackup=$false, + [Parameter(Mandatory=$false)] + [switch] $Extract=$false +) + +function Get-ANCMNugetFilePath() { + + $NugetFilePath = Get-ChildItem $PackagePath -Recurse -Filter Microsoft.AspNetCore.AspNetCoreModule*.nupkg | Select-Object -Last 1 + return ($NugetFilePath.FullName) +} + +function Check-TargetFiles() { + $functionName = "Check-TargetFiles" + $LogHeader = "[$ScriptFileName::$functionName]" + $result = $true + + if (-not $isIISExpressInstalled -and -not $isIISInstalled) + { + Say ("$LogHeader Both IIS and IISExpress does not have aspnetcore.dll file") + $result = $false + } + + if ($isIISExpressInstalled) + { + if (-not (Test-Path $aspnetCorex64To)) + { + Say ("$LogHeader Error!!! Failed to find the file $aspnetCorex64To") + $result = $false + } + if (-not (Test-Path $aspnetCoreSchemax64To)) + { + Say ("$LogHeader Error!!! Failed to find the file $aspnetCoreSchemax64To") + $result = $false + } + if ($is64BitMachine) + { + if (-not (Test-Path $aspnetCoreWin32To)) + { + Say ("$LogHeader Error!!! Failed to find the file $aspnetCoreWin32To") + $result = $false + } + if (-not (Test-Path $aspnetCoreSchemaWin32To)) + { + Say ("$LogHeader Error!!! Failed to find the file $aspnetCoreSchemaWin32To") + $result = $false + } + } + } + + if ($isIISInstalled) + { + if (-not (Test-Path $aspnetCorex64IISTo)) + { + Say ("$LogHeader Error!!! Failed to find the file $aspnetCorex64IISTo") + $result = $false + } + if (-not (Test-Path $aspnetCoreSchemax64IISTo)) + { + Say ("$LogHeader Error!!! Failed to find the file $aspnetCoreSchemax64IISTo") + $result = $false + } + if ($is64BitMachine) + { + if (-not (Test-Path $aspnetCoreWin32IISTo)) + { + Say ("$LogHeader Error!!! Failed to find the file $aspnetCoreWin32IISTo") + $result = $false + } + } + } + + return $result +} + +function Check-ExtractedFiles() { + $functionName = "Check-ExtractedFiles" + $LogHeader = "[$ScriptFileName::$functionName]" + $result = $true + + if (-not (Test-Path $aspnetCorex64From)) + { + Say ("$LogHeader Error!!! Failed to find the file $aspnetCorex64From") + $result = $false + } + if (-not (Test-Path $aspnetCoreWin32From)) + { + Say ("$LogHeader Error!!! Failed to find the file $aspnetCoreWin32From") + $result = $false + } + if (-not (Test-Path $aspnetCoreSchemax64From)) + { + Say ("$LogHeader Error!!! Failed to find the file $aspnetCoreSchemax64From") + $result = $false + } + if (-not (Test-Path $aspnetCoreSchemaWin32From)) + { + Say ("$LogHeader Error!!! Failed to find the file $aspnetCoreSchemaWin32From") + $result = $false + } + return $result +} + +function Extract-ANCMFromNugetPackage() { + $result = $true + + $functionName = "Extract-ANCMFromNugetPackage" + $LogHeader = "[$ScriptFileName::$functionName]" + + $backupAncmNugetFilePath = Join-Path $TempExtractFilesTo (get-item $ancmNugetFilePath).Name + if (Test-Path $backupAncmNugetFilePath) + { + Say ("$LogHeader Found backup file at $backupAncmNugetFilePath") + if ((get-item $ancmNugetFilePath).LastWriteTime -eq (get-item $backupAncmNugetFilePath).LastWriteTime) + { + if (Check-ExtractedFiles) + { + Say ("$LogHeader Skip to extract ANCM files because $ancmNugetFilePath is matched to the backup file $backupAncmNugetFilePath.") + return $result + } + } + } + + Add-Type -Assembly System.IO.Compression.FileSystem + if (Test-Path $TempExtractFilesTo) + { + remove-item $TempExtractFilesTo -Force -Recurse -Confirm:$false | out-null + } + if (Test-Path $TempExtractFilesTo) + { + Say ("$LogHeader Error!!! Failed to delete $TempExtractFilesTo") + $result = $false + return $result + } + else + { + new-item -Type directory $TempExtractFilesTo | out-null + } + if (-not (Test-Path $TempExtractFilesTo)) + { + Say ("$LogHeader Error!!! Failed to create $TempExtractFilesTo") + $result = $false + return $result + } + + # + Say ("$LogHeader Extract the ancm nuget file $ancmNugetFilePath to $TempExtractFilesTo ...") + [System.IO.Compression.ZipFile]::ExtractToDirectory($ancmNugetFilePath, $TempExtractFilesTo) + + Say ("$LogHeader Create the backup file of the nuget file to $backupAncmNugetFilePath") + copy-item $ancmNugetFilePath $backupAncmNugetFilePath + + return $result +} + +function Update-ANCM() { + + $functionName = "Update-ANCM -Rollback:$" + $Rollback.ToString() + $LogHeader = "[$ScriptFileName::$functionName]" + + if ($isIISExpressInstalled) + { + if ($is64BitMachine) + { + Say ("$LogHeader Start updating ANCM files for IISExpress for amd64 machine...") + Update-File $aspnetCorex64From $aspnetCorex64To + Update-File $aspnetCoreWin32From $aspnetCoreWin32To + Update-File $aspnetCoreSchemax64From $aspnetCoreSchemax64To + Update-File $aspnetCoreSchemaWin32From $aspnetCoreSchemaWin32To + } + else + { + Say ("$LogHeader Start updating ANCM files for IISExpress for x86 machine...") + Update-File $aspnetCoreWin32From $aspnetCorex64To + Update-File $aspnetCoreSchemaWin32From $aspnetCoreSchemax64To + } + } + else + { + Say ("$LogHeader Can't find aspnetcore.dll for IISExpress. Skipping updating ANCM files for IISExpress") + } + + if ($isIISInstalled) + { + if ($is64BitMachine) + { + Say ("$LogHeader Start updating ANCM files for IIS for amd64 machine...") + Update-File $aspnetCorex64From $aspnetCorex64IISTo + Update-File $aspnetCoreWin32From $aspnetCoreWin32IISTo + Update-File $aspnetCoreSchemax64From $aspnetCoreSchemax64IISTo + } + else + { + Say ("$LogHeader Start updating ANCM files for IIS for x86 machine...") + Update-File $aspnetCoreWin32IISFrom $aspnetCorex64IISTo + Update-File $aspnetCoreSchemaWin32From $aspnetCoreSchemax64IISTo + } + } + else + { + Say ("$LogHeader Can't find aspnetcore.dll for IIS. Skipping updating ANCM files for IIS server") + } +} + +function Update-File([string]$SourceFilePath, [string]$DestinationFilePath) { + + $Source = $SourceFilePath + $Destination = $DestinationFilePath + + $BackupFilePath = $Destination + ".ancm_backup" + if ($Rollback) + { + $Source = $BackupFilePath + } + + $functionName = "Update-File -Rollback:$" + $Rollback.ToString() + $LogHeader = "[$ScriptFileName::$functionName]" + + if ($ForceToBackup) + { + if (Test-Path $BackupFilePath) + { + $backupFileRemoved = $false + if ( ((get-item $DestinationFilePath).CreationTime -gt (get-item $BackupFilePath).CreationTime) -and ((get-item $DestinationFilePath).CreationTime -gt (get-item $SourceFilePath).CreationTime) ) + { + $backupFileRemoved = $true + Say (' Delete the existing "$BackupFilePath" because "$DestinationFilePath" is newer than both "$BackupFilePath" and "$SourceFilePath"') + Remove-Item $BackupFilePath -Force -Confirm:$false + } + else + { + Say-Verbose (' Skipping to delete the existing backupfile because "$DestinationFilePath" is not newer than $BackupFilePath"') + } + } + if ($backupFileRemoved -and (Test-Path $BackupFilePath)) + { + throw ("$LogHeader Can't delete $BackupFilePath") + } + } + + # Do the initial back up before updating file + if (-Not (Test-Path $BackupFilePath)) + { + Say (" Create a backup $BackupFilePath") + Copy-Item $Destination $BackupFilePath -Force + + $fileMatched = $null -eq (Compare-Object -ReferenceObject $(Get-Content $Destination) -DifferenceObject $(Get-Content $BackupFilePath)) + if (-not $fileMatched) + { + throw ("$LogHeader File not matched!!! $Destination $BackupFilePath") + } + } + if (-Not (Test-Path $BackupFilePath)) + { + throw ("$LogHeader Can't backup $Source to $BackupFilePath") + } + + # Copy file from Source to Destination if those files are different each other + if (-Not (Test-Path $Destination)) + { + throw ("$LogHeader Can't find $Destination") + } + $fileMatched = $null -eq (Compare-Object -ReferenceObject $(Get-Content $Source) -DifferenceObject $(Get-Content $Destination)) + if (-not $fileMatched) + { + Say (" Copying $Source to $Desting...") + Copy-Item $Source $Destination -Force + + # check file is correctly copied + $fileMatched = $null -eq (Compare-Object -ReferenceObject $(Get-Content $Source) -DifferenceObject $(Get-Content $Destination)) + if (-not $fileMatched) + { + throw ("$LogHeader File not matched!!! $Source $Destination") + } + else + { + Say-Verbose ("$LogHeader File matched!!! $Source to $Destination") + } + } + else + { + Say (" Skipping $Destination that is already identical to $Source ") + } +} + +function Say($str) { + Write-Host $str +} + +function Say-Verbose($str) { + Write-Verbose $str +} + +####################################################### +# Start execution point +####################################################### + +$EXIT_FAIL = 1 +$EXIT_SUCCESS = 0 + +$ScriptFileName = "installancm.ps1" +$LogHeader = "[$ScriptFileName]" + +if ($Extract -and (-Not $Rollback)) +{ + if (-not (Test-Path $PackagePath)) + { + Say ("$LogHeader Error!!! Failed to find the directory $PackagePath") + exit $EXIT_FAIL + } + + $ancmNugetFilePath = Get-ANCMNugetFilePath + if (-not (Test-Path $ancmNugetFilePath)) + { + Say ("$LogHeader Error!!! Failed to find AspNetCoreModule nupkg file under $PackagePath nor its child directories") + exit $EXIT_FAIL + } +} + +if (-Not $Rollback) +{ + if (-not (Test-Path $ExtractFilesTo)) + { + Say ("$LogHeader Error!!! Failed to find the directory $ExtractFilesTo") + exit $EXIT_FAIL + } +} + +$TempExtractFilesTo = $ExtractFilesTo + "\.ancm" +$ExtractFilesRootPath = "" +if ($Extract) +{ + $ExtractFilesRootPath = $TempExtractFilesTo + "\ancm\Debug" +} +else +{ + $ExtractFilesRootPath = $ExtractFilesTo +} + +# Try with solution output path +$aspnetCorex64From = $ExtractFilesRootPath + "\x64\aspnetcore.dll" +$aspnetCoreWin32From = $ExtractFilesRootPath + "\Win32\aspnetcore.dll" +$aspnetCoreSchemax64From = $ExtractFilesRootPath + "\x64\aspnetcore_schema.xml" +$aspnetCoreSchemaWin32From = $ExtractFilesRootPath + "\Win32\aspnetcore_schema.xml" + +$aspnetCorex64To = "$env:ProgramFiles\IIS Express\aspnetcore.dll" +$aspnetCoreWin32To = "${env:ProgramFiles(x86)}\IIS Express\aspnetcore.dll" +$aspnetCoreSchemax64To = "$env:ProgramFiles\IIS Express\config\schema\aspnetcore_schema.xml" +$aspnetCoreSchemaWin32To = "${env:ProgramFiles(x86)}\IIS Express\config\schema\aspnetcore_schema.xml" + +$aspnetCorex64IISTo = "$env:windir\system32\inetsrv\aspnetcore.dll" +$aspnetCoreWin32IISTo = "$env:windir\syswow64\inetsrv\aspnetcore.dll" +$aspnetCoreSchemax64IISTo = "$env:windir\system32\inetsrv\config\schema\aspnetcore_schema.xml" + +# if this is not solution output path, use nuget package directory structure +if (-not (Test-Path $aspnetCorex64From)) +{ + $aspnetCorex64From = $ExtractFilesRootPath + "\runtimes\win7-x64\native\aspnetcore.dll" + $aspnetCoreWin32From = $ExtractFilesRootPath + "\runtimes\win7-x86\native\aspnetcore.dll" + $aspnetCoreSchemax64From = $ExtractFilesRootPath + "\aspnetcore_schema.xml" + $aspnetCoreSchemaWin32From = $ExtractFilesRootPath + "\aspnetcore_schema.xml" + + $aspnetCorex64To = "$env:ProgramFiles\IIS Express\aspnetcore.dll" + $aspnetCoreWin32To = "${env:ProgramFiles(x86)}\IIS Express\aspnetcore.dll" + $aspnetCoreSchemax64To = "$env:ProgramFiles\IIS Express\config\schema\aspnetcore_schema.xml" + $aspnetCoreSchemaWin32To = "${env:ProgramFiles(x86)}\IIS Express\config\schema\aspnetcore_schema.xml" + + $aspnetCorex64IISTo = "$env:windir\system32\inetsrv\aspnetcore.dll" + $aspnetCoreWin32IISTo = "$env:windir\syswow64\inetsrv\aspnetcore.dll" + $aspnetCoreSchemax64IISTo = "$env:windir\system32\inetsrv\config\schema\aspnetcore_schema.xml" +} + +$is64BitMachine = $env:PROCESSOR_ARCHITECTURE.ToLower() -eq "amd64" +$isIISExpressInstalled = Test-Path $aspnetCorex64To +$isIISInstalled = Test-Path $aspnetCorex64IISTo + +# Check expected files are available on IIS/IISExpress directory +if (-not (Check-TargetFiles)) +{ + Say ("$LogHeader Error!!! Failed to update ANCM files because AspnetCore.dll is not installed on IIS/IISExpress directory.") + exit $EXIT_FAIL +} + +if ($Extract) +{ + # Extrack nuget package when $DoExtract is true + if (-not (Extract-ANCMFromNugetPackage)) + { + Say ("$LogHeader Error!!! Failed to extract ANCM file") + exit $EXIT_FAIL + } +} + +# clean up IIS and IISExpress worker processes and IIS services +Say ("$LogHeader Stopping w3wp.exe process") +Stop-Process -Name w3wp -ErrorAction Ignore -Force -Confirm:$false + +Say ("$LogHeader Stopping iisexpress.exe process") +Stop-Process -Name iisexpress -ErrorAction Ignore -Force -Confirm:$false + +$w3svcGotStopped = $false +$w3svcWindowsServce = Get-Service W3SVC -ErrorAction Ignore +if ($w3svcWindowsServce -and $w3svcWindowsServce.Status -eq "Running") +{ + Say ("$LogHeader Stopping w3svc service") + $w3svcGotStopped = $true + Stop-Service W3SVC -Force -ErrorAction Ignore + Say ("$LogHeader Stopping w3logsvc service") + Stop-Service W3LOGSVC -Force -ErrorAction Ignore +} + +if ($Rollback) +{ + Say ("$LogHeader Rolling back ANCM files...") +} +else +{ + Say ("Updating ANCM files...") +} +Update-ANCM + +# Recover w3svc service +if ($w3svcGotStopped) +{ + Say ("$LogHeader Starting w3svc service") + Start-Service W3SVC -ErrorAction Ignore + $w3svcServiceStopped = $false + + $w3svcWindowsServce = Get-Service W3SVC -ErrorAction Ignore + if ($w3svcWindowsServce.Status -ne "Running") + { + Say ("$LogHeader Error!!! Failed to start w3svc service.") + exit $EXIT_FAIL + } +} + +Say ("$LogHeader Finished!!!") +exit $EXIT_SUCCESS diff --git a/src/IISIntegration/tools/update_schema.ps1 b/src/IISIntegration/tools/update_schema.ps1 new file mode 100644 index 0000000000..2a45a3152c --- /dev/null +++ b/src/IISIntegration/tools/update_schema.ps1 @@ -0,0 +1,69 @@ +<# +.DESCRIPTION +Updates aspnetcore_schema.xml to the latest version. +Updates aspnetcore_schema.xml to the latest version. +Requires admin privileges. +#> +[cmdletbinding(SupportsShouldProcess = $true)] +param() + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 1 + +$ancmSchemaFiles = @( + "aspnetcore_schema.xml", + "aspnetcore_schema_v2.xml" +) + +$ancmSchemaFileLocation = Resolve-Path "$PSScriptRoot\..\src\AspNetCoreModuleV2\AspNetCore\aspnetcore_schema_v2.xml"; + +[bool]$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator") + +if (-not $isAdmin -and -not $WhatIfPreference) { + if ($PSCmdlet.ShouldContinue("Continue as an admin?", "This script needs admin privileges to update IIS Express and IIS.")) { + $thisFile = Join-Path $PSScriptRoot $MyInvocation.MyCommand.Name + + Start-Process ` + -Verb runas ` + -FilePath "powershell.exe" ` + -ArgumentList $thisFile ` + -Wait ` + | Out-Null + + if (-not $?) { + throw 'Update failed' + } + exit + } + else { + throw 'Requires admin privileges' + } +} + +for ($i=0; $i -lt $ancmSchemaFiles.Length; $i++) +{ + $schemaFile = $ancmSchemaFiles[$i] + $schemaSource = $ancmSchemaFileLocation + + $destinations = @( + "${env:ProgramFiles(x86)}\IIS Express\config\schema\", + "${env:ProgramFiles}\IIS Express\config\schema\", + "${env:windir}\system32\inetsrv\config\schema\" + ) + + foreach ($destPath in $destinations) { + $dest = "$destPath\${schemaFile}"; + + if (!(Test-Path $destPath)) + { + Write-Host "$destPath doesn't exist" + continue; + } + + if ($PSCmdlet.ShouldProcess($dest, "Replace file")) { + Write-Host "Updated $dest" + Move-Item $dest "${dest}.bak" -ErrorAction Ignore + Copy-Item $schemaSource $dest + } + } +} diff --git a/src/IISIntegration/version.props b/src/IISIntegration/version.props new file mode 100644 index 0000000000..6fb4839c0d --- /dev/null +++ b/src/IISIntegration/version.props @@ -0,0 +1,21 @@ + + + 2 + 2 + 1 + 0 + $(DotNetMajorVersion).$(DotNetMinorVersion).$(DotNetPatchVersion) + 12 + 2 + $(DotNetPatchVersion) + rtm + ancm-oob + $(VersionPrefix) + $(VersionPrefix)-$(VersionSuffix)-final + t000 + a- + $(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-')) + $(VersionSuffix)-$(BuildNumber) + 2.0.0 + +