diff --git a/.gitmodules b/.gitmodules
index 1f31381c06..8476ec5888 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -62,22 +62,10 @@
path = modules/Security
url = https://github.com/aspnet/Security.git
branch = release/2.2
-[submodule "modules/ServerTests"]
- path = modules/ServerTests
- url = https://github.com/aspnet/ServerTests.git
- branch = release/2.2
-[submodule "modules/Session"]
- path = modules/Session
- url = https://github.com/aspnet/Session.git
- branch = release/2.2
[submodule "modules/SignalR"]
path = modules/SignalR
url = https://github.com/aspnet/SignalR.git
branch = release/2.2
-[submodule "modules/StaticFiles"]
- path = modules/StaticFiles
- url = https://github.com/aspnet/StaticFiles.git
- branch = release/2.2
[submodule "src/IISIntegration/test/gtest/googletest"]
path = src/IISIntegration/test/gtest/googletest
url = https://github.com/google/googletest
diff --git a/build/buildorder.props b/build/buildorder.props
index 4caa334235..e46fa3e2d0 100644
--- a/build/buildorder.props
+++ b/build/buildorder.props
@@ -13,12 +13,12 @@
-
-
-
-
+
+
+
+
diff --git a/build/submodules.props b/build/submodules.props
index feca4cd58d..da1a794e15 100644
--- a/build/submodules.props
+++ b/build/submodules.props
@@ -43,7 +43,7 @@
-
+
@@ -65,8 +65,8 @@
-
+
-
+
diff --git a/modules/ServerTests b/modules/ServerTests
deleted file mode 160000
index 04c6c6bab8..0000000000
--- a/modules/ServerTests
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 04c6c6bab868ca11fc6521d5621b986f5b7352ee
diff --git a/modules/Session b/modules/Session
deleted file mode 160000
index 774079d60d..0000000000
--- a/modules/Session
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 774079d60d29762ef7c8bba3f0fa06e73cb323f2
diff --git a/modules/StaticFiles b/modules/StaticFiles
deleted file mode 160000
index a5fcd9dd87..0000000000
--- a/modules/StaticFiles
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit a5fcd9dd8724eb71f0642340f68aeb85b0473374
diff --git a/src/ServerTests/.gitignore b/src/ServerTests/.gitignore
new file mode 100644
index 0000000000..a7760db659
--- /dev/null
+++ b/src/ServerTests/.gitignore
@@ -0,0 +1,31 @@
+[Oo]bj/
+[Bb]in/
+TestResults/
+.nuget/
+*.sln.ide/
+_ReSharper.*/
+packages/
+artifacts/
+PublishProfiles/
+*.user
+*.suo
+*.cache
+*.docstates
+_ReSharper.*
+nuget.exe
+*net45.csproj
+*net451.csproj
+*k10.csproj
+*.psess
+*.vsp
+*.pidb
+*.userprefs
+*DS_Store
+*.ncrunchsolution
+*.*sdf
+*.ipch
+project.lock.json
+/.vs/
+.testPublish/
+.build/
+global.json
diff --git a/src/ServerTests/Directory.Build.props b/src/ServerTests/Directory.Build.props
new file mode 100644
index 0000000000..fc93574bae
--- /dev/null
+++ b/src/ServerTests/Directory.Build.props
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+ Microsoft ASP.NET Core
+ https://github.com/aspnet/servertests
+ git
+ $(MSBuildThisFileDirectory)
+ true
+
+
+
+
+
+
+
diff --git a/src/ServerTests/Directory.Build.targets b/src/ServerTests/Directory.Build.targets
new file mode 100644
index 0000000000..53b3f6e1da
--- /dev/null
+++ b/src/ServerTests/Directory.Build.targets
@@ -0,0 +1,7 @@
+
+
+ $(MicrosoftNETCoreApp20PackageVersion)
+ $(MicrosoftNETCoreApp21PackageVersion)
+ $(NETStandardLibrary20PackageVersion)
+
+
diff --git a/src/ServerTests/NuGetPackageVerifier.json b/src/ServerTests/NuGetPackageVerifier.json
new file mode 100644
index 0000000000..22ef3c09c0
--- /dev/null
+++ b/src/ServerTests/NuGetPackageVerifier.json
@@ -0,0 +1,7 @@
+{
+ "Default": {
+ "rules": [
+ "DefaultCompositeRule"
+ ]
+ }
+}
diff --git a/src/ServerTests/README.md b/src/ServerTests/README.md
new file mode 100644
index 0000000000..355a989132
--- /dev/null
+++ b/src/ServerTests/README.md
@@ -0,0 +1,9 @@
+Server Tests
+============
+
+[](https://travis-ci.org/aspnet/ServerTests/branches)
+[](https://ci.appveyor.com/project/aspnetci/ServerTests/branch/dev)
+
+This repo hosts [HttpSysServer](https://github.com/aspnet/HttpSysServer) and [Kestrel](https://github.com/aspnet/KestrelHttpServer) tests.
+
+This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [Home](https://github.com/aspnet/home) repo.
diff --git a/src/ServerTests/ServerTests.sln b/src/ServerTests/ServerTests.sln
new file mode 100644
index 0000000000..bd58846053
--- /dev/null
+++ b/src/ServerTests/ServerTests.sln
@@ -0,0 +1,50 @@
+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}") = "Solution Items", "Solution Items", "{49AB8AAA-8160-48DF-A18B-78F51E54E02A}"
+ ProjectSection(SolutionItems) = preProject
+ Directory.Build.props = Directory.Build.props
+ Directory.Build.targets = Directory.Build.targets
+ NuGet.config = NuGet.config
+ build\repo.props = build\repo.props
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{FA91F388-F4AF-4850-9D68-D4D128E6B1A6}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServerComparison.FunctionalTests", "test\ServerComparison.FunctionalTests\ServerComparison.FunctionalTests.csproj", "{A319ACCE-060B-4385-9534-9F2202F6180E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServerComparison.TestSites", "test\ServerComparison.TestSites\ServerComparison.TestSites.csproj", "{030225D8-4EE8-47E5-B692-2A96B3B51A38}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{55694E45-5EDE-46F8-80AA-797DE5F8C5C3}"
+ ProjectSection(SolutionItems) = preProject
+ build\dependencies.props = build\dependencies.props
+ build\repo.props = build\repo.props
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {A319ACCE-060B-4385-9534-9F2202F6180E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A319ACCE-060B-4385-9534-9F2202F6180E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A319ACCE-060B-4385-9534-9F2202F6180E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A319ACCE-060B-4385-9534-9F2202F6180E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {030225D8-4EE8-47E5-B692-2A96B3B51A38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {030225D8-4EE8-47E5-B692-2A96B3B51A38}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {030225D8-4EE8-47E5-B692-2A96B3B51A38}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {030225D8-4EE8-47E5-B692-2A96B3B51A38}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {A319ACCE-060B-4385-9534-9F2202F6180E} = {FA91F388-F4AF-4850-9D68-D4D128E6B1A6}
+ {030225D8-4EE8-47E5-B692-2A96B3B51A38} = {FA91F388-F4AF-4850-9D68-D4D128E6B1A6}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {8A313020-8407-494F-81D7-7631580C5FCC}
+ EndGlobalSection
+EndGlobal
diff --git a/src/ServerTests/build/dependencies.props b/src/ServerTests/build/dependencies.props
new file mode 100644
index 0000000000..72cad24822
--- /dev/null
+++ b/src/ServerTests/build/dependencies.props
@@ -0,0 +1,39 @@
+
+
+ $(MSBuildAllProjects);$(MSBuildThisFileFullPath)
+
+
+
+
+ 2.1.3-rtm-15802
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 0.5.1
+ 2.1.2
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.0.0
+ 2.1.2
+ 2.1.1
+ 15.6.1
+ 2.0.3
+ 1.4.0
+ 3.2.0
+ 2.3.1
+ 2.4.0-beta.1.build3945
+
+
+
+
+
+
+
+
diff --git a/src/ServerTests/build/repo.props b/src/ServerTests/build/repo.props
new file mode 100644
index 0000000000..c8bd413e1e
--- /dev/null
+++ b/src/ServerTests/build/repo.props
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+ Internal.AspNetCore.Universe.Lineup
+ 2.1.0-rc1-*
+ https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json
+
+
+
+
+
+
+
diff --git a/src/ServerTests/build/sources.props b/src/ServerTests/build/sources.props
new file mode 100644
index 0000000000..9215df9751
--- /dev/null
+++ b/src/ServerTests/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/ServerTests/install-nginx.sh b/src/ServerTests/install-nginx.sh
new file mode 100644
index 0000000000..9564813c2d
--- /dev/null
+++ b/src/ServerTests/install-nginx.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+curl -sSL http://nginx.org/download/nginx-1.8.0.tar.gz | tar zxfv - -C /tmp && cd /tmp/nginx-1.8.0/
+./configure --prefix=$HOME/nginxinstall --with-http_ssl_module
+make
+make install
diff --git a/src/ServerTests/test/Directory.Build.props b/src/ServerTests/test/Directory.Build.props
new file mode 100644
index 0000000000..620b803d21
--- /dev/null
+++ b/src/ServerTests/test/Directory.Build.props
@@ -0,0 +1,9 @@
+
+
+
+
+ netcoreapp2.1;netcoreapp2.0
+ $(DeveloperBuildTestTfms)
+ $(StandardTestTfms);net461
+
+
diff --git a/src/ServerTests/test/ServerComparison.FunctionalTests/HelloWorldTest.cs b/src/ServerTests/test/ServerComparison.FunctionalTests/HelloWorldTest.cs
new file mode 100644
index 0000000000..62e1fcfbd8
--- /dev/null
+++ b/src/ServerTests/test/ServerComparison.FunctionalTests/HelloWorldTest.cs
@@ -0,0 +1,118 @@
+// Copyright (c) .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;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Server.IntegrationTesting;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Testing;
+using Xunit;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace ServerComparison.FunctionalTests
+{
+ public class HelloWorldTests : LoggedTest
+ {
+ public HelloWorldTests(ITestOutputHelper output) : base(output)
+ {
+ }
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone)]
+ public Task HelloWorld_WebListener(RuntimeFlavor runtimeFlavor, ApplicationType applicationType)
+ {
+ return HelloWorld(ServerType.WebListener, runtimeFlavor, RuntimeArchitecture.x64, applicationType);
+ }
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V1")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable, HostingModel.OutOfProcess, "/p:ANCMVersion=V1")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V2")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable, HostingModel.OutOfProcess, "/p:ANCMVersion=V2")]
+ [InlineData(RuntimeFlavor.Clr, ApplicationType.Portable, HostingModel.OutOfProcess, "/p:ANCMVersion=V1", Skip = "Websdk issue with full framework publish. See https://github.com/aspnet/websdk/pull/322")]
+ public Task HelloWorld_IISExpress(RuntimeFlavor runtimeFlavor, ApplicationType applicationType, HostingModel hostingModel, string additionalPublishParameters)
+ {
+ return HelloWorld(ServerType.IISExpress, runtimeFlavor, RuntimeArchitecture.x64, applicationType, hostingModel: hostingModel, additionalPublishParameters: additionalPublishParameters);
+ }
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
+ [InlineData(RuntimeFlavor.Clr, ApplicationType.Portable)]
+ public Task HelloWorld_Kestrel_Clr(RuntimeFlavor runtimeFlavor, ApplicationType applicationType)
+ {
+ return HelloWorld(ServerType.Kestrel, runtimeFlavor, RuntimeArchitecture.x64, applicationType);
+ }
+
+ [Theory]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone)]
+ public Task HelloWorld_Kestrel(RuntimeFlavor runtimeFlavor, ApplicationType applicationType)
+ {
+ return HelloWorld(ServerType.Kestrel, runtimeFlavor, RuntimeArchitecture.x64, applicationType);
+ }
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Windows)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone)]
+ public Task HelloWorld_Nginx(RuntimeFlavor runtimeFlavor, ApplicationType applicationType)
+ {
+ return HelloWorld(ServerType.Nginx, runtimeFlavor, RuntimeArchitecture.x64, applicationType);
+ }
+
+
+ private async Task HelloWorld(ServerType serverType,
+ RuntimeFlavor runtimeFlavor,
+ RuntimeArchitecture architecture,
+ ApplicationType applicationType,
+ [CallerMemberName] string testName = null,
+ HostingModel hostingModel = HostingModel.OutOfProcess,
+ string additionalPublishParameters = "")
+ {
+ testName = $"{testName}_{serverType}_{runtimeFlavor}_{architecture}_{applicationType}";
+ using (StartLog(out var loggerFactory, testName))
+ {
+ var logger = loggerFactory.CreateLogger("HelloWorld");
+
+ var deploymentParameters = new DeploymentParameters(Helpers.GetApplicationPath(applicationType), serverType, runtimeFlavor, architecture)
+ {
+ EnvironmentName = "HelloWorld", // Will pick the Start class named 'StartupHelloWorld',
+ ServerConfigTemplateContent = Helpers.GetConfigContent(serverType, "Http.config", "nginx.conf"),
+ SiteName = "HttpTestSite", // This is configured in the Http.config
+ TargetFramework = Helpers.GetTargetFramework(runtimeFlavor),
+ ApplicationType = applicationType,
+ HostingModel = hostingModel,
+ AdditionalPublishParameters = additionalPublishParameters
+ };
+
+ using (var deployer = ApplicationDeployerFactory.Create(deploymentParameters, loggerFactory))
+ {
+ var deploymentResult = await deployer.DeployAsync();
+
+ // Request to base address and check if various parts of the body are rendered & measure the cold startup time.
+ var response = await RetryHelper.RetryRequest(() =>
+ {
+ return deploymentResult.HttpClient.GetAsync(string.Empty);
+ }, logger, deploymentResult.HostShutdownToken);
+
+ var responseText = await response.Content.ReadAsStringAsync();
+ try
+ {
+ Assert.Equal("Hello World", responseText);
+ }
+ catch (XunitException)
+ {
+ logger.LogWarning(response.ToString());
+ logger.LogWarning(responseText);
+ throw;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/ServerTests/test/ServerComparison.FunctionalTests/Helpers.cs b/src/ServerTests/test/ServerComparison.FunctionalTests/Helpers.cs
new file mode 100644
index 0000000000..0c4c4ab89d
--- /dev/null
+++ b/src/ServerTests/test/ServerComparison.FunctionalTests/Helpers.cs
@@ -0,0 +1,69 @@
+// Copyright (c) .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.IntegrationTesting;
+
+namespace ServerComparison.FunctionalTests
+{
+ public class Helpers
+ {
+ public static string GetApplicationPath(ApplicationType applicationType)
+ {
+ var applicationBasePath = AppContext.BaseDirectory;
+
+ var directoryInfo = new DirectoryInfo(applicationBasePath);
+ do
+ {
+ var solutionFileInfo = new FileInfo(Path.Combine(directoryInfo.FullName, "ServerTests.sln"));
+ if (solutionFileInfo.Exists)
+ {
+ return Path.GetFullPath(Path.Combine(directoryInfo.FullName, "test", "ServerComparison.TestSites"));
+ }
+
+ directoryInfo = directoryInfo.Parent;
+ }
+ while (directoryInfo.Parent != null);
+
+ throw new Exception($"Solution root could not be found using {applicationBasePath}");
+ }
+
+ public static string GetConfigContent(ServerType serverType, string iisConfig, string nginxConfig)
+ {
+ var applicationBasePath = AppContext.BaseDirectory;
+
+ string content = null;
+ if (serverType == ServerType.IISExpress)
+ {
+ content = File.ReadAllText(Path.Combine(applicationBasePath, iisConfig));
+ }
+ else if (serverType == ServerType.Nginx)
+ {
+ content = File.ReadAllText(Path.Combine(applicationBasePath, nginxConfig));
+ }
+
+ return content;
+ }
+
+ public static string GetTargetFramework(RuntimeFlavor runtimeFlavor)
+ {
+ if (runtimeFlavor == RuntimeFlavor.Clr)
+ {
+ return "net461";
+ }
+ else if (runtimeFlavor == RuntimeFlavor.CoreClr)
+ {
+#if NETCOREAPP2_0
+ return "netcoreapp2.0";
+#elif NETCOREAPP2_1 || NET461
+ return "netcoreapp2.1";
+#else
+#error Target frameworks need to be updated.
+#endif
+ }
+
+ throw new ArgumentException($"Unknown RuntimeFlavor '{runtimeFlavor}'");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ServerTests/test/ServerComparison.FunctionalTests/Http.config b/src/ServerTests/test/ServerComparison.FunctionalTests/Http.config
new file mode 100644
index 0000000000..8e8b3f2f9d
--- /dev/null
+++ b/src/ServerTests/test/ServerComparison.FunctionalTests/Http.config
@@ -0,0 +1,1029 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/ServerTests/test/ServerComparison.FunctionalTests/NoCompression.conf b/src/ServerTests/test/ServerComparison.FunctionalTests/NoCompression.conf
new file mode 100644
index 0000000000..1fa5e2f1e7
--- /dev/null
+++ b/src/ServerTests/test/ServerComparison.FunctionalTests/NoCompression.conf
@@ -0,0 +1,36 @@
+error_log [errorlog];
+user [user];
+worker_processes 4;
+pid [pidFile];
+
+events {
+ worker_connections 768;
+}
+
+http {
+ sendfile on;
+ tcp_nopush on;
+ tcp_nodelay on;
+ keepalive_timeout 10;
+ types_hash_max_size 2048;
+
+ default_type application/octet-stream;
+
+ ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+ ssl_prefer_server_ciphers on;
+
+ access_log [accesslog];
+
+ gzip off;
+
+ server {
+ listen [listenPort];
+ location / {
+ proxy_pass [redirectUri];
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Host $host;
+ proxy_cache_bypass $http_upgrade;
+ }
+ }
+}
diff --git a/src/ServerTests/test/ServerComparison.FunctionalTests/NoCompression.config b/src/ServerTests/test/ServerComparison.FunctionalTests/NoCompression.config
new file mode 100644
index 0000000000..c9b9f970be
--- /dev/null
+++ b/src/ServerTests/test/ServerComparison.FunctionalTests/NoCompression.config
@@ -0,0 +1,1021 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/ServerTests/test/ServerComparison.FunctionalTests/NtlmAuthentication.config b/src/ServerTests/test/ServerComparison.FunctionalTests/NtlmAuthentication.config
new file mode 100644
index 0000000000..c35bcf3443
--- /dev/null
+++ b/src/ServerTests/test/ServerComparison.FunctionalTests/NtlmAuthentication.config
@@ -0,0 +1,1041 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/ServerTests/test/ServerComparison.FunctionalTests/NtlmAuthenticationTest.cs b/src/ServerTests/test/ServerComparison.FunctionalTests/NtlmAuthenticationTest.cs
new file mode 100644
index 0000000000..0aff3407db
--- /dev/null
+++ b/src/ServerTests/test/ServerComparison.FunctionalTests/NtlmAuthenticationTest.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.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Server.IntegrationTesting;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Testing;
+using Xunit;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace ServerComparison.FunctionalTests
+{
+ public class NtlmAuthenticationTests : LoggedTest
+ {
+ public NtlmAuthenticationTests(ITestOutputHelper output) : base(output)
+ {
+ }
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
+ [InlineData(ServerType.IISExpress, RuntimeFlavor.Clr, "net461", RuntimeArchitecture.x64, ApplicationType.Portable, HostingModel.OutOfProcess, "V2", Skip = "Websdk issue with full framework publish. See https://github.com/aspnet/websdk/pull/322")]
+ [InlineData(ServerType.IISExpress, RuntimeFlavor.CoreClr, "netcoreapp2.1", RuntimeArchitecture.x64, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V2")]
+ [InlineData(ServerType.IISExpress, RuntimeFlavor.CoreClr, "netcoreapp2.1", RuntimeArchitecture.x64, ApplicationType.Portable, HostingModel.OutOfProcess, "/p:ANCMVersion=V2")]
+ [InlineData(ServerType.IISExpress, RuntimeFlavor.CoreClr, "netcoreapp2.1", RuntimeArchitecture.x64, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V1")]
+ [InlineData(ServerType.IISExpress, RuntimeFlavor.CoreClr, "netcoreapp2.1", RuntimeArchitecture.x64, ApplicationType.Portable, HostingModel.OutOfProcess, "/p:ANCMVersion=V1")]
+ [InlineData(ServerType.IISExpress, RuntimeFlavor.CoreClr, "netcoreapp2.0", RuntimeArchitecture.x64, ApplicationType.Portable, HostingModel.OutOfProcess, "/p:ANCMVersion=V2")]
+ [InlineData(ServerType.IISExpress, RuntimeFlavor.CoreClr, "netcoreapp2.0", RuntimeArchitecture.x64, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V2")]
+ [InlineData(ServerType.IISExpress, RuntimeFlavor.CoreClr, "netcoreapp2.0", RuntimeArchitecture.x64, ApplicationType.Portable, HostingModel.OutOfProcess, "/p:ANCMVersion=V1")]
+ [InlineData(ServerType.IISExpress, RuntimeFlavor.CoreClr, "netcoreapp2.0", RuntimeArchitecture.x64, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V1")]
+ [InlineData(ServerType.WebListener, RuntimeFlavor.CoreClr, "netcoreapp2.0", RuntimeArchitecture.x64, ApplicationType.Portable)]
+ [InlineData(ServerType.WebListener, RuntimeFlavor.CoreClr, "netcoreapp2.1", RuntimeArchitecture.x64, ApplicationType.Portable)]
+ [InlineData(ServerType.WebListener, RuntimeFlavor.CoreClr, "netcoreapp2.0", RuntimeArchitecture.x64, ApplicationType.Standalone)]
+ [InlineData(ServerType.WebListener, RuntimeFlavor.CoreClr, "netcoreapp2.1", RuntimeArchitecture.x64, ApplicationType.Standalone)]
+ public async Task NtlmAuthentication(ServerType serverType,
+ RuntimeFlavor runtimeFlavor,
+ string targetFramework,
+ RuntimeArchitecture architecture,
+ ApplicationType applicationType,
+ HostingModel hostingModel = HostingModel.OutOfProcess,
+ string additionalPublishParameters = "")
+ {
+ var testName = $"NtlmAuthentication_{serverType}_{runtimeFlavor}_{architecture}_{applicationType}";
+ using (StartLog(out var loggerFactory, testName))
+ {
+ var logger = loggerFactory.CreateLogger("NtlmAuthenticationTest");
+
+ var deploymentParameters = new DeploymentParameters(Helpers.GetApplicationPath(applicationType), serverType, runtimeFlavor, architecture)
+ {
+ EnvironmentName = "NtlmAuthentication", // Will pick the Start class named 'StartupNtlmAuthentication'
+ ServerConfigTemplateContent = Helpers.GetConfigContent(serverType, "NtlmAuthentication.config", nginxConfig: null),
+ SiteName = "NtlmAuthenticationTestSite", // This is configured in the NtlmAuthentication.config
+ TargetFramework = targetFramework,
+ ApplicationType = applicationType,
+ HostingModel = hostingModel,
+ AdditionalPublishParameters = additionalPublishParameters
+ };
+
+ using (var deployer = ApplicationDeployerFactory.Create(deploymentParameters, loggerFactory))
+ {
+ var deploymentResult = await deployer.DeployAsync();
+ var httpClient = deploymentResult.HttpClient;
+
+ // Request to base address and check if various parts of the body are rendered & measure the cold startup time.
+ var response = await RetryHelper.RetryRequest(() =>
+ {
+ return httpClient.GetAsync(string.Empty);
+ }, logger, deploymentResult.HostShutdownToken);
+
+ var responseText = await response.Content.ReadAsStringAsync();
+ try
+ {
+ Assert.Equal("Hello World", responseText);
+
+ logger.LogInformation("Testing /Anonymous");
+ response = await httpClient.GetAsync("/Anonymous");
+ responseText = await response.Content.ReadAsStringAsync();
+ Assert.Equal("Anonymous?True", responseText);
+
+ logger.LogInformation("Testing /Restricted");
+ response = await httpClient.GetAsync("/Restricted");
+ 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());
+
+ logger.LogInformation("Testing /Forbidden");
+ response = await httpClient.GetAsync("/Forbidden");
+ Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
+
+ logger.LogInformation("Enabling Default Credentials");
+
+ // Change the http client to one that uses default credentials
+ httpClient = deploymentResult.CreateHttpClient(new HttpClientHandler() { UseDefaultCredentials = true });
+
+ logger.LogInformation("Testing /Restricted");
+ response = await httpClient.GetAsync("/Restricted");
+ responseText = await response.Content.ReadAsStringAsync();
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("Authenticated", responseText);
+
+ logger.LogInformation("Testing /Forbidden");
+ response = await httpClient.GetAsync("/Forbidden");
+ Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
+ }
+ catch (XunitException)
+ {
+ logger.LogWarning(response.ToString());
+ logger.LogWarning(responseText);
+ throw;
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ServerTests/test/ServerComparison.FunctionalTests/Properties/AssemblyInfo.cs b/src/ServerTests/test/ServerComparison.FunctionalTests/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..b1fa884228
--- /dev/null
+++ b/src/ServerTests/test/ServerComparison.FunctionalTests/Properties/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using Xunit;
+
+[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)]
\ No newline at end of file
diff --git a/src/ServerTests/test/ServerComparison.FunctionalTests/ResponseCompressionTests.cs b/src/ServerTests/test/ServerComparison.FunctionalTests/ResponseCompressionTests.cs
new file mode 100644
index 0000000000..301a0b74e6
--- /dev/null
+++ b/src/ServerTests/test/ServerComparison.FunctionalTests/ResponseCompressionTests.cs
@@ -0,0 +1,331 @@
+// Copyright (c) .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.IO.Compression;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Server.IntegrationTesting;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Testing;
+using Microsoft.Net.Http.Headers;
+using Xunit;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace ServerComparison.FunctionalTests
+{
+ public class ResponseCompressionTests : LoggedTest
+ {
+ // NGinx's default min size is 20 bytes
+ private static readonly string HelloWorldBody = "Hello World;" + new string('a', 20);
+
+ public ResponseCompressionTests(ITestOutputHelper output) : base(output)
+ {
+ }
+
+ // IIS Express
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable, HostingModel.OutOfProcess, "/p:ANCMVersion=V1")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable, HostingModel.OutOfProcess, "/p:ANCMVersion=V2")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V1")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V2")]
+ [InlineData(RuntimeFlavor.Clr, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V1", Skip = "Websdk issue with full framework publish. See https://github.com/aspnet/websdk/pull/322")]
+ [InlineData(RuntimeFlavor.Clr, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V2", Skip = "Websdk issue with full framework publish. See https://github.com/aspnet/websdk/pull/322")]
+ public Task ResponseCompression_IISExpress_NoCompression(RuntimeFlavor runtimeFlavor, ApplicationType applicationType, HostingModel hostingModel, string additionalPublishParameters)
+ {
+ return ResponseCompression(ServerType.IISExpress,
+ runtimeFlavor,
+ RuntimeArchitecture.x64,
+ CheckNoCompressionAsync,
+ applicationType,
+ hostCompression: false,
+ hostingModel: hostingModel,
+ additionalPublishParameters: additionalPublishParameters);
+ }
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable, HostingModel.OutOfProcess, "/p:ANCMVersion=V1")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable, HostingModel.OutOfProcess, "/p:ANCMVersion=V2")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V1")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V2")]
+ [InlineData(RuntimeFlavor.Clr, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V1", Skip = "Websdk issue with full framework publish. See https://github.com/aspnet/websdk/pull/322")]
+ [InlineData(RuntimeFlavor.Clr, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V2", Skip = "Websdk issue with full framework publish. See https://github.com/aspnet/websdk/pull/322")]
+ public Task ResponseCompression_IISExpress_HostCompression(RuntimeFlavor runtimeFlavor, ApplicationType applicationType, HostingModel hostingModel, string additionalPublishParameters)
+ {
+ return ResponseCompression(ServerType.IISExpress,
+ runtimeFlavor,
+ RuntimeArchitecture.x64,
+ CheckHostCompressionAsync,
+ applicationType,
+ hostCompression: true,
+ hostingModel: hostingModel,
+ additionalPublishParameters: additionalPublishParameters);
+ }
+
+ [ConditionalTheory(Skip = "Websdk issue with full framework publish. See https://github.com/aspnet/websdk/pull/322")]
+ [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable, HostingModel.OutOfProcess, "/p:ANCMVersion=V1")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable, HostingModel.OutOfProcess, "/p:ANCMVersion=V2")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V1")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V2")]
+ [InlineData(RuntimeFlavor.Clr, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V1", Skip = "Websdk issue with full framework publish. See https://github.com/aspnet/websdk/pull/322")]
+ [InlineData(RuntimeFlavor.Clr, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V2", Skip = "Websdk issue with full framework publish. See https://github.com/aspnet/websdk/pull/322")]
+ public Task ResponseCompression_IISExpress_AppCompression(RuntimeFlavor runtimeFlavor, ApplicationType applicationType, HostingModel hostingModel, string additionalPublishParameters)
+ {
+ return ResponseCompression(ServerType.IISExpress,
+ runtimeFlavor,
+ RuntimeArchitecture.x64,
+ CheckAppCompressionAsync,
+ applicationType,
+ hostCompression: true,
+ hostingModel: hostingModel,
+ additionalPublishParameters: additionalPublishParameters);
+ }
+
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable, HostingModel.OutOfProcess, "/p:ANCMVersion=V1")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable, HostingModel.OutOfProcess, "/p:ANCMVersion=V2")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V1")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V2")]
+ [InlineData(RuntimeFlavor.Clr, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V1", Skip = "Websdk issue with full framework publish. See https://github.com/aspnet/websdk/pull/322")]
+ [InlineData(RuntimeFlavor.Clr, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V2", Skip = "Websdk issue with full framework publish. See https://github.com/aspnet/websdk/pull/322")]
+ public Task ResponseCompression_IISExpress_AppAndHostCompression(RuntimeFlavor runtimeFlavor, ApplicationType applicationType, HostingModel hostingModel, string additionalPublishParameters)
+ {
+ return ResponseCompression(ServerType.IISExpress,
+ runtimeFlavor,
+ RuntimeArchitecture.x64,
+ CheckAppCompressionAsync,
+ applicationType,
+ hostCompression: true,
+ hostingModel: hostingModel,
+ additionalPublishParameters: additionalPublishParameters);
+ }
+
+ // WebListener
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
+ [InlineData(RuntimeFlavor.Clr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone)]
+ public Task ResponseCompression_WebListener_NoCompression(RuntimeFlavor runtimeFlavor, ApplicationType applicationType)
+ {
+ return ResponseCompression(ServerType.WebListener, runtimeFlavor, RuntimeArchitecture.x64, CheckNoCompressionAsync, applicationType, hostCompression: false);
+ }
+
+ // WebListener doesn't support HostCompression
+ // "The archive entry was compressed using an unsupported compression method."
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
+ [InlineData(RuntimeFlavor.Clr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone)]
+ public Task ResponseCompression_WebListener_AppCompression(RuntimeFlavor runtimeFlavor, ApplicationType applicationType)
+ {
+ return ResponseCompression(ServerType.WebListener, runtimeFlavor, RuntimeArchitecture.x64, CheckAppCompressionAsync, applicationType, hostCompression: false);
+ }
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
+ [InlineData(RuntimeFlavor.Clr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone)]
+ public Task ResponseCompression_WebListener_AppAndHostCompression(RuntimeFlavor runtimeFlavor, ApplicationType applicationType)
+ {
+ return ResponseCompression(ServerType.WebListener, runtimeFlavor, RuntimeArchitecture.x64, CheckAppCompressionAsync, applicationType, hostCompression: true);
+ }
+
+ // Kestrel
+ [Theory]
+ [InlineData(ServerType.Kestrel, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, ApplicationType.Portable)]
+ [InlineData(ServerType.Kestrel, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, ApplicationType.Standalone)]
+ public Task ResponseCompression_Kestrel_NoCompression(ServerType serverType, RuntimeFlavor runtimeFlavor, RuntimeArchitecture architecture, ApplicationType applicationType)
+ {
+ return ResponseCompression(serverType, runtimeFlavor, architecture, CheckNoCompressionAsync, applicationType, hostCompression: false);
+ }
+
+ [Theory]
+ [InlineData(ServerType.Kestrel, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, ApplicationType.Portable)]
+ [InlineData(ServerType.Kestrel, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, ApplicationType.Standalone)]
+ public Task ResponseCompression_Kestrel_AppCompression(ServerType serverType, RuntimeFlavor runtimeFlavor, RuntimeArchitecture architecture, ApplicationType applicationType)
+ {
+ return ResponseCompression(serverType, runtimeFlavor, architecture, CheckAppCompressionAsync, applicationType, hostCompression: false);
+ }
+
+ // Nginx
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Windows)]
+ [InlineData(ServerType.Nginx, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, ApplicationType.Portable)]
+ [InlineData(ServerType.Nginx, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, ApplicationType.Standalone)]
+ public Task ResponseCompression_Nginx_NoCompression(ServerType serverType, RuntimeFlavor runtimeFlavor, RuntimeArchitecture architecture, ApplicationType applicationType)
+ {
+ return ResponseCompression(serverType, runtimeFlavor, architecture, CheckNoCompressionAsync, applicationType, hostCompression: false);
+ }
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Windows)]
+ [InlineData(ServerType.Nginx, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, ApplicationType.Portable)]
+ [InlineData(ServerType.Nginx, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, ApplicationType.Standalone)]
+ public Task ResponseCompression_Nginx_HostCompression(ServerType serverType, RuntimeFlavor runtimeFlavor, RuntimeArchitecture architecture, ApplicationType applicationType)
+ {
+ return ResponseCompression(serverType, runtimeFlavor, architecture, CheckHostCompressionAsync, applicationType, hostCompression: true);
+ }
+
+ [ConditionalTheory(Skip = "No pass-through compression https://github.com/aspnet/BasicMiddleware/issues/123")]
+ [OSSkipCondition(OperatingSystems.Windows)]
+ [InlineData(ServerType.Nginx, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, ApplicationType.Portable)]
+ [InlineData(ServerType.Nginx, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, ApplicationType.Standalone)]
+ public Task ResponseCompression_Nginx_AppCompression(ServerType serverType, RuntimeFlavor runtimeFlavor, RuntimeArchitecture architecture, ApplicationType applicationType)
+ {
+ return ResponseCompression(serverType, runtimeFlavor, architecture, CheckHostCompressionAsync, applicationType, hostCompression: false);
+ }
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Windows)]
+ [InlineData(ServerType.Nginx, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, ApplicationType.Portable)]
+ [InlineData(ServerType.Nginx, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, ApplicationType.Standalone)]
+ public Task ResponseCompression_Nginx_AppAndHostCompression(ServerType serverType, RuntimeFlavor runtimeFlavor, RuntimeArchitecture architecture, ApplicationType applicationType)
+ {
+ return ResponseCompression(serverType, runtimeFlavor, architecture, CheckAppCompressionAsync, applicationType, hostCompression: true);
+ }
+
+ private async Task ResponseCompression(ServerType serverType,
+ RuntimeFlavor runtimeFlavor,
+ RuntimeArchitecture architecture,
+ Func scenario,
+ ApplicationType applicationType,
+ bool hostCompression,
+ [CallerMemberName] string testName = null,
+ HostingModel hostingModel = HostingModel.OutOfProcess,
+ string additionalPublishParameters = "")
+ {
+ testName = $"{testName}_{serverType}_{runtimeFlavor}_{architecture}_{applicationType}";
+ using (StartLog(out var loggerFactory, testName))
+ {
+ var logger = loggerFactory.CreateLogger("ResponseCompression");
+
+ var deploymentParameters = new DeploymentParameters(Helpers.GetApplicationPath(applicationType), serverType, runtimeFlavor, architecture)
+ {
+ EnvironmentName = "ResponseCompression",
+ ServerConfigTemplateContent = Helpers.GetConfigContent(serverType,
+ hostCompression ? "http.config" : "NoCompression.config",
+ hostCompression ? "nginx.conf" : "NoCompression.conf"),
+ SiteName = "HttpTestSite", // This is configured in the Http.config
+ TargetFramework = Helpers.GetTargetFramework(runtimeFlavor),
+ ApplicationType = applicationType,
+ HostingModel = hostingModel,
+ AdditionalPublishParameters = additionalPublishParameters
+ };
+
+ using (var deployer = ApplicationDeployerFactory.Create(deploymentParameters, loggerFactory))
+ {
+ var deploymentResult = await deployer.DeployAsync();
+ var httpClientHandler = new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.None };
+ Assert.True(httpClientHandler.SupportsAutomaticDecompression);
+ var httpClient = deploymentResult.CreateHttpClient(httpClientHandler);
+
+ // Request to base address and check if various parts of the body are rendered & measure the cold startup time.
+ var response = await RetryHelper.RetryRequest(() =>
+ {
+ return httpClient.GetAsync(string.Empty);
+ }, logger, deploymentResult.HostShutdownToken);
+
+ var responseText = await response.Content.ReadAsStringAsync();
+ try
+ {
+ Assert.Equal("Running", responseText);
+ }
+ catch (XunitException)
+ {
+ logger.LogWarning(response.ToString());
+ logger.LogWarning(responseText);
+ throw;
+ }
+
+ await scenario(httpClient, logger);
+ }
+ }
+ }
+
+ private static async Task CheckNoCompressionAsync(HttpClient client, ILogger logger)
+ {
+ logger.LogInformation("Testing /NoAppCompression");
+ var request = new HttpRequestMessage(HttpMethod.Get, "NoAppCompression");
+ request.Headers.AcceptEncoding.ParseAdd("gzip,deflate");
+ var response = await client.SendAsync(request);
+ var responseText = await response.Content.ReadAsStringAsync();
+ try
+ {
+ Assert.Equal(HelloWorldBody, responseText);
+ Assert.Equal(HelloWorldBody.Length.ToString(), GetContentLength(response));
+ Assert.Equal(0, response.Content.Headers.ContentEncoding.Count);
+ }
+ catch (XunitException)
+ {
+ logger.LogWarning(response.ToString());
+ logger.LogWarning(responseText);
+ throw;
+ }
+ }
+
+ private static Task CheckHostCompressionAsync(HttpClient client, ILogger logger)
+ {
+ return CheckCompressionAsync(client, "NoAppCompression", logger);
+ }
+
+ private static Task CheckAppCompressionAsync(HttpClient client, ILogger logger)
+ {
+ return CheckCompressionAsync(client, "AppCompression", logger);
+ }
+
+ private static async Task CheckCompressionAsync(HttpClient client, string url, ILogger logger)
+ {
+ // Manage the compression manually because HttpClient removes the Content-Encoding header when decompressing.
+ logger.LogInformation($"Testing /{url}");
+ var request = new HttpRequestMessage(HttpMethod.Get, url);
+ request.Headers.AcceptEncoding.ParseAdd("gzip,deflate");
+ var response = await client.SendAsync(request);
+ var responseText = await response.Content.ReadAsStringAsync();
+ try
+ {
+ responseText = await ReadCompressedAsStringAsync(response.Content);
+ Assert.Equal(HelloWorldBody, responseText);
+ Assert.Equal(1, response.Content.Headers.ContentEncoding.Count);
+ Assert.Equal("gzip", response.Content.Headers.ContentEncoding.First());
+ }
+ catch (XunitException)
+ {
+ logger.LogWarning(response.ToString());
+ logger.LogWarning(responseText);
+ throw;
+ }
+ }
+
+ private static string GetContentLength(HttpResponseMessage response)
+ {
+ // Don't use response.Content.Headers.ContentLength, it will dynamically calculate the value if it can.
+ return response.Content.Headers.TryGetValues(HeaderNames.ContentLength, out var values) ? values.FirstOrDefault() : null;
+ }
+
+ private static async Task ReadCompressedAsStringAsync(HttpContent content)
+ {
+ using (var stream = await content.ReadAsStreamAsync())
+ using (var compressStream = new GZipStream(stream, CompressionMode.Decompress))
+ using (var reader = new StreamReader(compressStream))
+ {
+ return await reader.ReadToEndAsync();
+ }
+ }
+ }
+}
diff --git a/src/ServerTests/test/ServerComparison.FunctionalTests/ResponseTests.cs b/src/ServerTests/test/ServerComparison.FunctionalTests/ResponseTests.cs
new file mode 100644
index 0000000000..62d7fec6e3
--- /dev/null
+++ b/src/ServerTests/test/ServerComparison.FunctionalTests/ResponseTests.cs
@@ -0,0 +1,406 @@
+// Copyright (c) .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.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Server.IntegrationTesting;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Testing;
+using Microsoft.Net.Http.Headers;
+using Xunit;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace ServerComparison.FunctionalTests
+{
+ public class ResponseTests : LoggedTest
+ {
+ public ResponseTests(ITestOutputHelper output) : base(output)
+ {
+ }
+
+ // IIS Express
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
+ [InlineData(RuntimeFlavor.Clr, ApplicationType.Portable, HostingModel.OutOfProcess, "", Skip = "Websdk issue with full framework publish. See https://github.com/aspnet/websdk/pull/322")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable, HostingModel.OutOfProcess, "/p:ANCMVersion=V1")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V1")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable, HostingModel.OutOfProcess, "/p:ANCMVersion=V2")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V2")]
+ public Task ResponseFormats_IISExpress_ContentLength(RuntimeFlavor runtimeFlavor, ApplicationType applicationType, HostingModel hostingModel, string additionalPublishParameters)
+ {
+ return ResponseFormats(ServerType.IISExpress, runtimeFlavor, RuntimeArchitecture.x64, CheckContentLengthAsync, applicationType, hostingModel: hostingModel, additionalPublishParameters: additionalPublishParameters);
+ }
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
+ [InlineData(RuntimeFlavor.Clr, ApplicationType.Portable, HostingModel.OutOfProcess, "", Skip = "Websdk issue with full framework publish. See https://github.com/aspnet/websdk/pull/322")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable, HostingModel.OutOfProcess, "/p:ANCMVersion=V1")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V1")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable, HostingModel.OutOfProcess, "/p:ANCMVersion=V2")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V2")]
+ public Task ResponseFormats_IISExpress_Chunked(RuntimeFlavor runtimeFlavor, ApplicationType applicationType, HostingModel hostingModel, string additionalPublishParameters)
+ {
+ return ResponseFormats(ServerType.IISExpress, runtimeFlavor, RuntimeArchitecture.x64, CheckChunkedAsync, applicationType, hostingModel: hostingModel, additionalPublishParameters: additionalPublishParameters);
+ }
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
+ [InlineData(RuntimeFlavor.Clr, ApplicationType.Portable, HostingModel.OutOfProcess, "", Skip = "Websdk issue with full framework publish. See https://github.com/aspnet/websdk/pull/322")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable, HostingModel.OutOfProcess, "/p:ANCMVersion=V1")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V1")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable, HostingModel.OutOfProcess, "/p:ANCMVersion=V2")]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone, HostingModel.OutOfProcess, "/p:ANCMVersion=V2")]
+ public Task ResponseFormats_IIS_ManuallyChunk(RuntimeFlavor runtimeFlavor, ApplicationType applicationType, HostingModel hostingModel, string additionalPublishParameters)
+ {
+ return ResponseFormats(ServerType.IISExpress, runtimeFlavor, RuntimeArchitecture.x64, CheckManuallyChunkedAsync, applicationType, hostingModel: hostingModel, additionalPublishParameters: additionalPublishParameters);
+ }
+
+ // Weblistener
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
+ [InlineData(RuntimeFlavor.Clr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone)]
+ public Task ResponseFormats_WebListener_ContentLength(RuntimeFlavor runtimeFlavor, ApplicationType applicationType)
+ {
+ return ResponseFormats(ServerType.WebListener, runtimeFlavor, RuntimeArchitecture.x64, CheckContentLengthAsync, applicationType);
+ }
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
+ [InlineData(RuntimeFlavor.Clr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone)]
+ public Task ResponseFormats_WebListener_Chunked(RuntimeFlavor runtimeFlavor, ApplicationType applicationType)
+ {
+ return ResponseFormats(ServerType.WebListener, runtimeFlavor, RuntimeArchitecture.x64, CheckChunkedAsync, applicationType);
+ }
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
+ [InlineData(RuntimeFlavor.Clr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone)]
+ // IIS will remove the "Connection: close" header https://github.com/aspnet/IISIntegration/issues/7
+ public Task ResponseFormats_WebListener_Http10ConnectionClose(RuntimeFlavor runtimeFlavor, ApplicationType applicationType)
+ {
+ return ResponseFormats(ServerType.WebListener, runtimeFlavor, RuntimeArchitecture.x64, CheckHttp10ConnectionCloseAsync, applicationType);
+ }
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
+ [InlineData(RuntimeFlavor.Clr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone)] // https://github.com/aspnet/WebListener/issues/259
+ // IIS will remove the "Connection: close" header https://github.com/aspnet/IISIntegration/issues/7
+ public Task ResponseFormats_WebListener_Http11ConnectionClose(RuntimeFlavor runtimeFlavor, ApplicationType applicationType)
+ {
+ return ResponseFormats(ServerType.WebListener, runtimeFlavor, RuntimeArchitecture.x64, CheckHttp11ConnectionCloseAsync, applicationType);
+ }
+
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
+ [InlineData(RuntimeFlavor.Clr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone)]
+ public Task ResponseFormats_WebListener_ManuallyChunk(RuntimeFlavor runtimeFlavor, ApplicationType applicationType)
+ {
+ return ResponseFormats(ServerType.WebListener, runtimeFlavor, RuntimeArchitecture.x64, CheckManuallyChunkedAsync, applicationType);
+ }
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
+ [InlineData(RuntimeFlavor.Clr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone)]
+ public Task ResponseFormats_WebListener_ManuallyChunkAndClose(RuntimeFlavor runtimeFlavor, ApplicationType applicationType)
+ {
+ return ResponseFormats(ServerType.WebListener, runtimeFlavor, RuntimeArchitecture.x64, CheckManuallyChunkedAndCloseAsync, applicationType);
+ }
+
+ // Kestrel
+ [Theory]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone)]
+ public Task ResponseFormats_Kestrel_ContentLength(RuntimeFlavor runtimeFlavor, ApplicationType applicationType)
+ {
+ return ResponseFormats(ServerType.Kestrel, runtimeFlavor, RuntimeArchitecture.x64, CheckContentLengthAsync, applicationType);
+ }
+
+ [Theory]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone)]
+ public Task ResponseFormats_Kestrel_Http10ConnectionClose(RuntimeFlavor runtimeFlavor, ApplicationType applicationType)
+ {
+ return ResponseFormats(ServerType.Kestrel, runtimeFlavor, RuntimeArchitecture.x64, CheckHttp10ConnectionCloseAsync, applicationType);
+ }
+
+ [Theory]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone)]
+ public Task ResponseFormats_Kestrel_Http11ConnectionClose(RuntimeFlavor runtimeFlavor, ApplicationType applicationType)
+ {
+ return ResponseFormats(ServerType.Kestrel, runtimeFlavor, RuntimeArchitecture.x64, CheckHttp11ConnectionCloseAsync, applicationType);
+ }
+
+ [Theory]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone)]
+ public Task ResponseFormats_Kestrel_Chunked(RuntimeFlavor runtimeFlavor, ApplicationType applicationType)
+ {
+ return ResponseFormats(ServerType.Kestrel, runtimeFlavor, RuntimeArchitecture.x64, CheckChunkedAsync, applicationType);
+ }
+
+ [Theory]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone)]
+ public Task ResponseFormats_Kestrel_ManuallyChunk(RuntimeFlavor runtimeFlavor, ApplicationType applicationType)
+ {
+ return ResponseFormats(ServerType.Kestrel, runtimeFlavor, RuntimeArchitecture.x64, CheckManuallyChunkedAsync, applicationType);
+ }
+
+ [Theory]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone)]
+ public Task ResponseFormats_Kestrel_ManuallyChunkAndClose(RuntimeFlavor runtimeFlavor, ApplicationType applicationType)
+ {
+ return ResponseFormats(ServerType.Kestrel, runtimeFlavor, RuntimeArchitecture.x64, CheckManuallyChunkedAndCloseAsync, applicationType);
+ }
+
+ // Nginx
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Windows)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone)]
+ public Task ResponseFormats_Nginx_ContentLength( RuntimeFlavor runtimeFlavor, ApplicationType applicationType)
+ {
+ return ResponseFormats(ServerType.Nginx, runtimeFlavor, RuntimeArchitecture.x64, CheckContentLengthAsync, applicationType);
+ }
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Windows)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone)]
+ public Task ResponseFormats_Nginx_Chunked(RuntimeFlavor runtimeFlavor, ApplicationType applicationType)
+ {
+ return ResponseFormats(ServerType.Nginx, runtimeFlavor, RuntimeArchitecture.x64, CheckChunkedAsync, applicationType);
+ }
+
+
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Windows)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Portable)]
+ [InlineData(RuntimeFlavor.CoreClr, ApplicationType.Standalone)]
+ public Task ResponseFormats_Nginx_ManuallyChunk(RuntimeFlavor runtimeFlavor, ApplicationType applicationType)
+ {
+ return ResponseFormats(ServerType.Nginx, runtimeFlavor, RuntimeArchitecture.x64, CheckManuallyChunkedAsync, applicationType);
+ }
+
+ private async Task ResponseFormats(ServerType serverType,
+ RuntimeFlavor runtimeFlavor,
+ RuntimeArchitecture architecture,
+ Func scenario,
+ ApplicationType applicationType,
+ [CallerMemberName] string testName = null,
+ HostingModel hostingModel = HostingModel.OutOfProcess,
+ string additionalPublishParameters = "")
+ {
+ testName = $"{testName}_{serverType}_{runtimeFlavor}_{architecture}_{applicationType}";
+ using (StartLog(out var loggerFactory, testName))
+ {
+ var logger = loggerFactory.CreateLogger("ResponseFormats");
+
+ var deploymentParameters = new DeploymentParameters(Helpers.GetApplicationPath(applicationType), serverType, runtimeFlavor, architecture)
+ {
+ EnvironmentName = "Responses",
+ ServerConfigTemplateContent = Helpers.GetConfigContent(serverType, "Http.config", "nginx.conf"),
+ SiteName = "HttpTestSite", // This is configured in the Http.config
+ TargetFramework = Helpers.GetTargetFramework(runtimeFlavor),
+ ApplicationType = applicationType,
+ HostingModel = hostingModel,
+ AdditionalPublishParameters = additionalPublishParameters
+ };
+
+ using (var deployer = ApplicationDeployerFactory.Create(deploymentParameters, loggerFactory))
+ {
+ var deploymentResult = await deployer.DeployAsync();
+
+ // Request to base address and check if various parts of the body are rendered & measure the cold startup time.
+ var response = await RetryHelper.RetryRequest(() =>
+ {
+ return deploymentResult.HttpClient.GetAsync(string.Empty);
+ }, logger, deploymentResult.HostShutdownToken);
+
+ var responseText = await response.Content.ReadAsStringAsync();
+ try
+ {
+ Assert.Equal("Running", responseText);
+ }
+ catch (XunitException)
+ {
+ logger.LogWarning(response.ToString());
+ logger.LogWarning(responseText);
+ throw;
+ }
+
+ await scenario(deploymentResult.HttpClient, logger);
+ }
+ }
+ }
+
+ private static async Task CheckContentLengthAsync(HttpClient client, ILogger logger)
+ {
+ logger.LogInformation("Testing ContentLength");
+ var requestMessage = new HttpRequestMessage(HttpMethod.Get, "contentlength")
+ {
+ Version = new Version(1, 1)
+ };
+
+ var response = await client.SendAsync(requestMessage);
+ var responseText = await response.Content.ReadAsStringAsync();
+ try
+ {
+ Assert.Equal("Content Length", responseText);
+ Assert.Null(response.Headers.TransferEncodingChunked);
+ Assert.Null(response.Headers.ConnectionClose);
+ Assert.Equal("14", GetContentLength(response));
+ }
+ catch (XunitException)
+ {
+ logger.LogWarning(response.ToString());
+ logger.LogWarning(responseText);
+ throw;
+ }
+ }
+
+ private static async Task CheckHttp11ConnectionCloseAsync(HttpClient client, ILogger logger)
+ {
+ logger.LogInformation("Testing Http11ConnectionClose");
+ var response = await client.GetAsync("connectionclose");
+ var responseText = await response.Content.ReadAsStringAsync();
+ try
+ {
+ Assert.Equal("Connnection Close", responseText);
+ Assert.True(response.Headers.ConnectionClose, "/connectionclose, closed?");
+ Assert.True(response.Headers.TransferEncodingChunked);
+ Assert.Null(GetContentLength(response));
+ }
+ catch (XunitException)
+ {
+ logger.LogWarning(response.ToString());
+ logger.LogWarning(responseText);
+ throw;
+ }
+ }
+
+ private static async Task CheckHttp10ConnectionCloseAsync(HttpClient client, ILogger logger)
+ {
+ logger.LogInformation("Testing Http10ConnectionClose");
+ var requestMessage = new HttpRequestMessage(HttpMethod.Get, "connectionclose")
+ {
+ Version = new Version(1, 0)
+ };
+
+ var response = await client.SendAsync(requestMessage);
+ var responseText = await response.Content.ReadAsStringAsync();
+ try
+ {
+ Assert.Equal("Connnection Close", responseText);
+ Assert.True(response.Headers.ConnectionClose, "/connectionclose, closed?");
+ Assert.Null(response.Headers.TransferEncodingChunked);
+ Assert.Null(GetContentLength(response));
+ }
+ catch (XunitException)
+ {
+ logger.LogWarning(response.ToString());
+ logger.LogWarning(responseText);
+ throw;
+ }
+ }
+
+ private static async Task CheckChunkedAsync(HttpClient client, ILogger logger)
+ {
+ logger.LogInformation("Testing Chunked");
+ var requestMessage = new HttpRequestMessage(HttpMethod.Get, "chunked")
+ {
+ Version = new Version(1, 1)
+ };
+
+ var response = await client.SendAsync(requestMessage);
+ var responseText = await response.Content.ReadAsStringAsync();
+ try
+ {
+ Assert.Equal("Chunked", responseText);
+ Assert.True(response.Headers.TransferEncodingChunked, "/chunked, chunked?");
+ Assert.Null(response.Headers.ConnectionClose);
+ Assert.Null(GetContentLength(response));
+ }
+ catch (XunitException)
+ {
+ logger.LogWarning(response.ToString());
+ logger.LogWarning(responseText);
+ throw;
+ }
+ }
+
+ private static async Task CheckManuallyChunkedAsync(HttpClient client, ILogger logger)
+ {
+ logger.LogInformation("Testing ManuallyChunked");
+ var requestMessage = new HttpRequestMessage(HttpMethod.Get, "manuallychunked")
+ {
+ Version = new Version(1, 1)
+ };
+
+ var response = await client.SendAsync(requestMessage);
+ var responseText = await response.Content.ReadAsStringAsync();
+ try
+ {
+ Assert.Equal("Manually Chunked", responseText);
+ Assert.True(response.Headers.TransferEncodingChunked, "/manuallychunked, chunked?");
+ Assert.Null(response.Headers.ConnectionClose);
+ Assert.Null(GetContentLength(response));
+ }
+ catch (XunitException)
+ {
+ logger.LogWarning(response.ToString());
+ logger.LogWarning(responseText);
+ throw;
+ }
+ }
+
+ private static async Task CheckManuallyChunkedAndCloseAsync(HttpClient client, ILogger logger)
+ {
+ logger.LogInformation("Testing ManuallyChunkedAndClose");
+ var response = await client.GetAsync("manuallychunkedandclose");
+ var responseText = await response.Content.ReadAsStringAsync();
+ try
+ {
+ Assert.Equal("Manually Chunked and Close", responseText);
+ Assert.True(response.Headers.TransferEncodingChunked, "/manuallychunkedandclose, chunked?");
+ Assert.True(response.Headers.ConnectionClose, "/manuallychunkedandclose, closed?");
+ Assert.Null(GetContentLength(response));
+ }
+ catch (XunitException)
+ {
+ logger.LogWarning(response.ToString());
+ logger.LogWarning(responseText);
+ throw;
+ }
+ }
+
+ private static string GetContentLength(HttpResponseMessage response)
+ {
+ // Don't use response.Content.Headers.ContentLength, it will dynamically calculate the value if it can.
+ IEnumerable values;
+ return response.Content.Headers.TryGetValues(HeaderNames.ContentLength, out values) ? values.FirstOrDefault() : null;
+ }
+ }
+}
diff --git a/src/ServerTests/test/ServerComparison.FunctionalTests/ServerComparison.FunctionalTests.csproj b/src/ServerTests/test/ServerComparison.FunctionalTests/ServerComparison.FunctionalTests.csproj
new file mode 100644
index 0000000000..ba0fea38e1
--- /dev/null
+++ b/src/ServerTests/test/ServerComparison.FunctionalTests/ServerComparison.FunctionalTests.csproj
@@ -0,0 +1,24 @@
+
+
+
+
+ netcoreapp2.1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ServerTests/test/ServerComparison.FunctionalTests/nginx.conf b/src/ServerTests/test/ServerComparison.FunctionalTests/nginx.conf
new file mode 100644
index 0000000000..f9b0bc85c9
--- /dev/null
+++ b/src/ServerTests/test/ServerComparison.FunctionalTests/nginx.conf
@@ -0,0 +1,37 @@
+error_log [errorlog];
+worker_processes 4;
+pid [pidFile];
+
+events {
+ worker_connections 768;
+}
+
+http {
+ sendfile on;
+ tcp_nopush on;
+ tcp_nodelay on;
+ keepalive_timeout 10;
+ types_hash_max_size 2048;
+
+ default_type application/octet-stream;
+
+ ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+ ssl_prefer_server_ciphers on;
+
+ access_log [accesslog];
+
+ gzip on;
+ gzip_types text/plain;
+ gzip_disable "msie6";
+
+ server {
+ listen [listenPort];
+ location / {
+ proxy_pass [redirectUri];
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Host $host;
+ proxy_cache_bypass $http_upgrade;
+ }
+ }
+}
diff --git a/src/ServerTests/test/ServerComparison.TestSites/Program.cs b/src/ServerTests/test/ServerComparison.TestSites/Program.cs
new file mode 100644
index 0000000000..f6ae1163c1
--- /dev/null
+++ b/src/ServerTests/test/ServerComparison.TestSites/Program.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.Hosting;
+using Microsoft.AspNetCore.Server.HttpSys;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace ServerComparison.TestSites
+{
+ public static class Program
+ {
+ public static void Main(string[] args)
+ {
+ var config = new ConfigurationBuilder()
+ .AddCommandLine(args)
+ .Build();
+
+ var builder = new WebHostBuilder()
+ .UseConfiguration(config)
+ .ConfigureLogging((_, factory) =>
+ {
+ factory.AddConsole();
+ factory.AddFilter("Console", level => level >= LogLevel.Warning);
+ })
+ .UseStartup("ServerComparison.TestSites");
+
+ // Switch between Kestrel, IIS, and HttpSys for different tests. Default to Kestrel for normal app execution.
+ if (string.Equals(builder.GetSetting("server"), "Microsoft.AspNetCore.Server.HttpSys", StringComparison.Ordinal))
+ {
+ if (string.Equals(builder.GetSetting("environment") ??
+ Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"),
+ "NtlmAuthentication", System.StringComparison.Ordinal))
+ {
+ // Set up NTLM authentication for HttpSys as follows.
+ // For IIS and IISExpress use inetmgr to setup NTLM authentication on the application or
+ // modify the applicationHost.config to enable NTLM.
+ builder.UseHttpSys(options =>
+ {
+ options.Authentication.AllowAnonymous = true;
+ options.Authentication.Schemes =
+ AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM;
+ });
+ }
+ else
+ {
+ builder.UseHttpSys();
+ }
+ }
+ else
+ {
+ // Check that we are not using IIS inproc before we add Kestrel.
+ builder.UseKestrel();
+ }
+
+ builder.UseIISIntegration();
+
+ var host = builder.Build();
+
+ host.Run();
+ }
+ }
+}
+
diff --git a/src/ServerTests/test/ServerComparison.TestSites/Properties/launchSettings.json b/src/ServerTests/test/ServerComparison.TestSites/Properties/launchSettings.json
new file mode 100644
index 0000000000..7e255d2086
--- /dev/null
+++ b/src/ServerTests/test/ServerComparison.TestSites/Properties/launchSettings.json
@@ -0,0 +1,25 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:39982/",
+ "sslPort": 0
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNET_ENVIRONMENT": "HelloWorld"
+ }
+ },
+ "web": {
+ "commandName": "web",
+ "environmentVariables": {
+ "ASPNET_ENVIRONMENT": "HelloWorld"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ServerTests/test/ServerComparison.TestSites/ServerComparison.TestSites.csproj b/src/ServerTests/test/ServerComparison.TestSites/ServerComparison.TestSites.csproj
new file mode 100644
index 0000000000..f3aa4898e5
--- /dev/null
+++ b/src/ServerTests/test/ServerComparison.TestSites/ServerComparison.TestSites.csproj
@@ -0,0 +1,28 @@
+
+
+
+ $(StandardTestTfms)
+ win7-x86;win7-x64;linux-x64;osx-x64
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ServerTests/test/ServerComparison.TestSites/StartupHelloWorld.cs b/src/ServerTests/test/ServerComparison.TestSites/StartupHelloWorld.cs
new file mode 100644
index 0000000000..f92b8d45c7
--- /dev/null
+++ b/src/ServerTests/test/ServerComparison.TestSites/StartupHelloWorld.cs
@@ -0,0 +1,20 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+
+namespace ServerComparison.TestSites
+{
+ public class StartupHelloWorld
+ {
+ public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
+ {
+ app.Run(ctx =>
+ {
+ return ctx.Response.WriteAsync("Hello World");
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ServerTests/test/ServerComparison.TestSites/StartupNtlmAuthentication.cs b/src/ServerTests/test/ServerComparison.TestSites/StartupNtlmAuthentication.cs
new file mode 100644
index 0000000000..c4e837db58
--- /dev/null
+++ b/src/ServerTests/test/ServerComparison.TestSites/StartupNtlmAuthentication.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 Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+
+namespace ServerComparison.TestSites
+{
+ public class StartupNtlmAuthentication
+ {
+ public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
+ {
+ app.Use(async (context, next) =>
+ {
+ try
+ {
+ await next();
+ }
+ catch (Exception ex)
+ {
+ if (context.Response.HasStarted)
+ {
+ throw;
+ }
+ context.Response.Clear();
+ context.Response.StatusCode = 500;
+ await context.Response.WriteAsync(ex.ToString());
+ }
+ });
+
+ app.Use((context, next) =>
+ {
+ if (context.Request.Path.Equals("/Anonymous"))
+ {
+ return context.Response.WriteAsync("Anonymous?" + !context.User.Identity.IsAuthenticated);
+ }
+
+ if (context.Request.Path.Equals("/Restricted"))
+ {
+ if (context.User.Identity.IsAuthenticated)
+ {
+ return context.Response.WriteAsync("Authenticated");
+ }
+ else
+ {
+ return context.ChallengeAsync("Windows");
+ }
+ }
+
+ if (context.Request.Path.Equals("/Forbidden"))
+ {
+ return context.ForbidAsync("Windows");
+ }
+
+ return context.Response.WriteAsync("Hello World");
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ServerTests/test/ServerComparison.TestSites/StartupResponseCompression.cs b/src/ServerTests/test/ServerComparison.TestSites/StartupResponseCompression.cs
new file mode 100644
index 0000000000..4f5398fe4c
--- /dev/null
+++ b/src/ServerTests/test/ServerComparison.TestSites/StartupResponseCompression.cs
@@ -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.
+
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace ServerComparison.TestSites
+{
+ public class StartupResponseCompression
+ {
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddResponseCompression();
+ }
+
+ public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
+ {
+ // NGinx's default min size is 20 bytes
+ var helloWorldBody = "Hello World;" + new string('a', 20);
+
+ app.Map("/NoAppCompression", subApp =>
+ {
+ subApp.Run(context =>
+ {
+ context.Response.ContentType = "text/plain";
+ context.Response.ContentLength = helloWorldBody.Length;
+ return context.Response.WriteAsync(helloWorldBody);
+ });
+ });
+
+ app.Map("/AppCompression", subApp =>
+ {
+ subApp.UseResponseCompression();
+ subApp.Run(context =>
+ {
+ context.Response.ContentType = "text/plain";
+ context.Response.ContentLength = helloWorldBody.Length;
+ return context.Response.WriteAsync(helloWorldBody);
+ });
+ });
+ /* If we implement DisableResponseBuffering on IISMiddleware
+ app.Map("/NoBuffer", subApp =>
+ {
+ subApp.UseResponseCompression();
+ subApp.Run(context =>
+ {
+ context.Features.Get().DisableResponseBuffering();
+ context.Response.ContentType = "text/plain";
+ context.Response.ContentLength = helloWorldBody.Length;
+ return context.Response.WriteAsync(helloWorldBody);
+ });
+ });
+ */
+ app.Run(context =>
+ {
+ context.Response.ContentType = "text/plain";
+ string body;
+ if (context.Request.Path.Value == "/")
+ {
+ body = "Running";
+ }
+ else
+ {
+ body = "Not Implemented: " + context.Request.Path;
+ }
+
+ context.Response.ContentLength = body.Length;
+ return context.Response.WriteAsync(body);
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ServerTests/test/ServerComparison.TestSites/StartupResponses.cs b/src/ServerTests/test/ServerComparison.TestSites/StartupResponses.cs
new file mode 100644
index 0000000000..3fc22ee091
--- /dev/null
+++ b/src/ServerTests/test/ServerComparison.TestSites/StartupResponses.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 Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+
+namespace ServerComparison.TestSites
+{
+ public class StartupResponses
+ {
+ public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
+ {
+ app.Map("/contentlength", subApp =>
+ {
+ subApp.Run(context =>
+ {
+ context.Response.ContentLength = 14;
+ return context.Response.WriteAsync("Content Length");
+ });
+ });
+
+ app.Map("/connectionclose", subApp =>
+ {
+ subApp.Run(async context =>
+ {
+ context.Response.Headers[HeaderNames.Connection] = "close";
+ await context.Response.WriteAsync("Connnection Close");
+ await context.Response.Body.FlushAsync(); // Bypass IIS write-behind buffering
+ });
+ });
+
+ app.Map("/chunked", subApp =>
+ {
+ subApp.Run(async context =>
+ {
+ await context.Response.WriteAsync("Chunked");
+ await context.Response.Body.FlushAsync(); // Bypass IIS write-behind buffering
+ });
+ });
+
+ app.Map("/manuallychunked", subApp =>
+ {
+ subApp.Run(context =>
+ {
+ context.Response.Headers[HeaderNames.TransferEncoding] = "chunked";
+ return context.Response.WriteAsync("10\r\nManually Chunked\r\n0\r\n\r\n");
+ });
+ });
+
+ app.Map("/manuallychunkedandclose", subApp =>
+ {
+ subApp.Run(context =>
+ {
+ context.Response.Headers[HeaderNames.Connection] = "close";
+ context.Response.Headers[HeaderNames.TransferEncoding] = "chunked";
+ return context.Response.WriteAsync("1A\r\nManually Chunked and Close\r\n0\r\n\r\n");
+ });
+ });
+
+ app.Run(context =>
+ {
+ return context.Response.WriteAsync("Running");
+ });
+ }
+ }
+}
diff --git a/src/ServerTests/test/ServerComparison.TestSites/web.config b/src/ServerTests/test/ServerComparison.TestSites/web.config
new file mode 100644
index 0000000000..3379e820ea
--- /dev/null
+++ b/src/ServerTests/test/ServerComparison.TestSites/web.config
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/ServerTests/version.props b/src/ServerTests/version.props
new file mode 100644
index 0000000000..669c874829
--- /dev/null
+++ b/src/ServerTests/version.props
@@ -0,0 +1,12 @@
+
+
+ 2.1.1
+ rtm
+ $(VersionPrefix)
+ $(VersionPrefix)-$(VersionSuffix)-final
+ t000
+ a-
+ $(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-'))
+ $(VersionSuffix)-$(BuildNumber)
+
+
diff --git a/src/Session/.gitignore b/src/Session/.gitignore
new file mode 100644
index 0000000000..f332e76e0f
--- /dev/null
+++ b/src/Session/.gitignore
@@ -0,0 +1,33 @@
+[Oo]bj/
+[Bb]in/
+TestResults/
+.nuget/
+*.sln.ide/
+_ReSharper.*/
+packages/
+artifacts/
+PublishProfiles/
+*.user
+*.suo
+*.cache
+*.docstates
+_ReSharper.*
+nuget.exe
+*net45.csproj
+*net451.csproj
+*k10.csproj
+*.psess
+*.vsp
+*.pidb
+*.userprefs
+*DS_Store
+*.ncrunchsolution
+*.*sdf
+*.ipch
+.vs/
+.vscode/
+project.lock.json
+.build/
+.testPublish/
+launchSettings.json
+global.json
diff --git a/src/Session/Directory.Build.props b/src/Session/Directory.Build.props
new file mode 100644
index 0000000000..c2790fd6a6
--- /dev/null
+++ b/src/Session/Directory.Build.props
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+ Microsoft ASP.NET Core
+ https://github.com/aspnet/Session
+ git
+ $(MSBuildThisFileDirectory)
+ $(MSBuildThisFileDirectory)build\Key.snk
+ true
+ true
+ true
+
+
+
diff --git a/src/Session/Directory.Build.targets b/src/Session/Directory.Build.targets
new file mode 100644
index 0000000000..53b3f6e1da
--- /dev/null
+++ b/src/Session/Directory.Build.targets
@@ -0,0 +1,7 @@
+
+
+ $(MicrosoftNETCoreApp20PackageVersion)
+ $(MicrosoftNETCoreApp21PackageVersion)
+ $(NETStandardLibrary20PackageVersion)
+
+
diff --git a/src/Session/NuGetPackageVerifier.json b/src/Session/NuGetPackageVerifier.json
new file mode 100644
index 0000000000..b153ab1515
--- /dev/null
+++ b/src/Session/NuGetPackageVerifier.json
@@ -0,0 +1,7 @@
+{
+ "Default": {
+ "rules": [
+ "DefaultCompositeRule"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/src/Session/README.md b/src/Session/README.md
new file mode 100644
index 0000000000..976f9f21cf
--- /dev/null
+++ b/src/Session/README.md
@@ -0,0 +1,12 @@
+Session
+================
+
+AppVeyor: [](https://ci.appveyor.com/project/aspnetci/Session/branch/dev)
+
+Travis: [](https://travis-ci.org/aspnet/Session)
+
+Contains libraries for session state middleware for ASP.NET Core.
+
+For ASP.NET 4.x session state, please go to https://github.com/aspnet/AspNetSessionState.
+
+This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [Home](https://github.com/aspnet/home) repo.
diff --git a/src/Session/Session.sln b/src/Session/Session.sln
new file mode 100644
index 0000000000..d8d95dc295
--- /dev/null
+++ b/src/Session/Session.sln
@@ -0,0 +1,74 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.26621.2
+MinimumVisualStudioVersion = 15.0.26730.03
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{E9D63F97-6078-42AD-BFD3-F956BF921BB5}"
+ ProjectSection(SolutionItems) = preProject
+ test\Directory.Build.props = test\Directory.Build.props
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A189F10C-3A9C-4F81-83D0-32E5FE50DAD8}"
+ ProjectSection(SolutionItems) = preProject
+ src\Directory.Build.props = src\Directory.Build.props
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Session", "src\Microsoft.AspNetCore.Session\Microsoft.AspNetCore.Session.csproj", "{71802736-F640-4733-9671-02D267EDD76A}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Session.Tests", "test\Microsoft.AspNetCore.Session.Tests\Microsoft.AspNetCore.Session.Tests.csproj", "{8C131A0A-BC1A-4CF3-8B77-8813FBFE5639}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{94E80ED2-9F27-40AC-A9EF-C707BDFAA3BE}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SessionSample", "samples\SessionSample\SessionSample.csproj", "{FE0B9969-3BDE-4A7D-BE1B-47EAE8DBF365}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{3B45F658-5BF1-4E07-BE9C-6F5110AC2277}"
+ ProjectSection(SolutionItems) = preProject
+ .appveyor.yml = .appveyor.yml
+ .gitattributes = .gitattributes
+ .gitignore = .gitignore
+ .travis.yml = .travis.yml
+ Directory.Build.props = Directory.Build.props
+ Directory.Build.targets = Directory.Build.targets
+ NuGet.config = NuGet.config
+ NuGetPackageVerifier.json = NuGetPackageVerifier.json
+ README.md = README.md
+ version.xml = version.xml
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{4F21221F-2813-41B7-AAFC-E03FD52971CC}"
+ ProjectSection(SolutionItems) = preProject
+ build\common.props = build\common.props
+ build\dependencies.props = build\dependencies.props
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {71802736-F640-4733-9671-02D267EDD76A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {71802736-F640-4733-9671-02D267EDD76A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {71802736-F640-4733-9671-02D267EDD76A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {71802736-F640-4733-9671-02D267EDD76A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8C131A0A-BC1A-4CF3-8B77-8813FBFE5639}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8C131A0A-BC1A-4CF3-8B77-8813FBFE5639}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8C131A0A-BC1A-4CF3-8B77-8813FBFE5639}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8C131A0A-BC1A-4CF3-8B77-8813FBFE5639}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FE0B9969-3BDE-4A7D-BE1B-47EAE8DBF365}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FE0B9969-3BDE-4A7D-BE1B-47EAE8DBF365}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FE0B9969-3BDE-4A7D-BE1B-47EAE8DBF365}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FE0B9969-3BDE-4A7D-BE1B-47EAE8DBF365}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {71802736-F640-4733-9671-02D267EDD76A} = {A189F10C-3A9C-4F81-83D0-32E5FE50DAD8}
+ {8C131A0A-BC1A-4CF3-8B77-8813FBFE5639} = {E9D63F97-6078-42AD-BFD3-F956BF921BB5}
+ {FE0B9969-3BDE-4A7D-BE1B-47EAE8DBF365} = {94E80ED2-9F27-40AC-A9EF-C707BDFAA3BE}
+ {4F21221F-2813-41B7-AAFC-E03FD52971CC} = {3B45F658-5BF1-4E07-BE9C-6F5110AC2277}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {6AE224B9-B604-4E47-9617-9D114DAE9BE5}
+ EndGlobalSection
+EndGlobal
diff --git a/src/Session/build/Key.snk b/src/Session/build/Key.snk
new file mode 100644
index 0000000000..e10e4889c1
Binary files /dev/null and b/src/Session/build/Key.snk differ
diff --git a/src/Session/build/dependencies.props b/src/Session/build/dependencies.props
new file mode 100644
index 0000000000..6e86d21f90
--- /dev/null
+++ b/src/Session/build/dependencies.props
@@ -0,0 +1,36 @@
+
+
+ $(MSBuildAllProjects);$(MSBuildThisFileFullPath)
+
+
+
+
+ 2.1.3-rtm-15802
+ 2.0.0
+ 2.1.2
+ 15.6.1
+ 2.0.3
+ 2.3.1
+ 2.4.0-beta.1.build3945
+
+
+
+
+
+
+
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.1.2
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.1.1
+
+
\ No newline at end of file
diff --git a/src/Session/build/repo.props b/src/Session/build/repo.props
new file mode 100644
index 0000000000..dab1601c88
--- /dev/null
+++ b/src/Session/build/repo.props
@@ -0,0 +1,15 @@
+
+
+
+
+
+ Internal.AspNetCore.Universe.Lineup
+ 2.1.0-rc1-*
+ https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json
+
+
+
+
+
+
+
diff --git a/src/Session/build/sources.props b/src/Session/build/sources.props
new file mode 100644
index 0000000000..9215df9751
--- /dev/null
+++ b/src/Session/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/Session/samples/SessionSample/Properties/launchSettings.json b/src/Session/samples/SessionSample/Properties/launchSettings.json
new file mode 100644
index 0000000000..6bab3d3602
--- /dev/null
+++ b/src/Session/samples/SessionSample/Properties/launchSettings.json
@@ -0,0 +1,27 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:2481/",
+ "sslPort": 0
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "SessionSample": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "launchUrl": "http://localhost:5000",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Session/samples/SessionSample/SessionSample.csproj b/src/Session/samples/SessionSample/SessionSample.csproj
new file mode 100644
index 0000000000..67abefae09
--- /dev/null
+++ b/src/Session/samples/SessionSample/SessionSample.csproj
@@ -0,0 +1,20 @@
+
+
+
+ netcoreapp2.1;net461
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Session/samples/SessionSample/Startup.cs b/src/Session/samples/SessionSample/Startup.cs
new file mode 100644
index 0000000000..41fcb566c5
--- /dev/null
+++ b/src/Session/samples/SessionSample/Startup.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 Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace SessionSample
+{
+ public class Startup
+ {
+ public void ConfigureServices(IServiceCollection services)
+ {
+ // Adds a default in-memory implementation of IDistributedCache
+ services.AddDistributedMemoryCache();
+
+ // Uncomment the following line to use the Microsoft SQL Server implementation of IDistributedCache.
+ // Note that this would require setting up the session state database.
+ //services.AddSqlServerCache(o =>
+ //{
+ // o.ConnectionString = "Server=.;Database=ASPNET5SessionState;Trusted_Connection=True;";
+ // o.SchemaName = "dbo";
+ // o.TableName = "Sessions";
+ //});
+
+ // Uncomment the following line to use the Redis implementation of IDistributedCache.
+ // This will override any previously registered IDistributedCache service.
+ //services.AddDistributedRedisCache(o =>
+ //{
+ // o.Configuration = "localhost";
+ // o.InstanceName = "SampleInstance";
+ //});
+
+ services.AddSession(o =>
+ {
+ o.IdleTimeout = TimeSpan.FromSeconds(10);
+ });
+ }
+
+ public void Configure(IApplicationBuilder app)
+ {
+ app.UseSession();
+
+ app.Map("/session", subApp =>
+ {
+ subApp.Run(async context =>
+ {
+ int visits = 0;
+ visits = context.Session.GetInt32("visits") ?? 0;
+ context.Session.SetInt32("visits", ++visits);
+ await context.Response.WriteAsync("Counting: You have visited our page this many times: " + visits);
+ });
+ });
+
+ app.Run(async context =>
+ {
+ int visits = 0;
+ visits = context.Session.GetInt32("visits") ?? 0;
+ await context.Response.WriteAsync("");
+ if (visits == 0)
+ {
+ await context.Response.WriteAsync("Your session has not been established.
");
+ await context.Response.WriteAsync(DateTime.Now + "
");
+ await context.Response.WriteAsync("Establish session.
");
+ }
+ else
+ {
+ context.Session.SetInt32("visits", ++visits);
+ await context.Response.WriteAsync("Your session was located, you've visited the site this many times: " + visits);
+ }
+ await context.Response.WriteAsync("");
+ });
+ }
+
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .ConfigureLogging(factory => factory.AddConsole())
+ .UseKestrel()
+ .UseIISIntegration()
+ .UseStartup()
+ .Build();
+
+ host.Run();
+ }
+ }
+}
diff --git a/src/Session/src/Directory.Build.props b/src/Session/src/Directory.Build.props
new file mode 100644
index 0000000000..1e0980f663
--- /dev/null
+++ b/src/Session/src/Directory.Build.props
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/CookieProtection.cs b/src/Session/src/Microsoft.AspNetCore.Session/CookieProtection.cs
new file mode 100644
index 0000000000..64a3a3fbbf
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/CookieProtection.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.Text;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Session
+{
+ internal static class CookieProtection
+ {
+ internal static string Protect(IDataProtector protector, string data)
+ {
+ if (protector == null)
+ {
+ throw new ArgumentNullException(nameof(protector));
+ }
+ if (string.IsNullOrEmpty(data))
+ {
+ return data;
+ }
+
+ var userData = Encoding.UTF8.GetBytes(data);
+
+ var protectedData = protector.Protect(userData);
+ return Convert.ToBase64String(protectedData).TrimEnd('=');
+ }
+
+ internal static string Unprotect(IDataProtector protector, string protectedText, ILogger logger)
+ {
+ try
+ {
+ if (string.IsNullOrEmpty(protectedText))
+ {
+ return string.Empty;
+ }
+
+ var protectedData = Convert.FromBase64String(Pad(protectedText));
+ if (protectedData == null)
+ {
+ return string.Empty;
+ }
+
+ var userData = protector.Unprotect(protectedData);
+ if (userData == null)
+ {
+ return string.Empty;
+ }
+
+ return Encoding.UTF8.GetString(userData);
+ }
+ catch (Exception ex)
+ {
+ // Log the exception, but do not leak other information
+ logger.ErrorUnprotectingSessionCookie(ex);
+ return string.Empty;
+ }
+ }
+
+ private static string Pad(string text)
+ {
+ var padding = 3 - ((text.Length + 3) % 4);
+ if (padding == 0)
+ {
+ return text;
+ }
+ return text + new string('=', padding);
+ }
+ }
+}
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/DistributedSession.cs b/src/Session/src/Microsoft.AspNetCore.Session/DistributedSession.cs
new file mode 100644
index 0000000000..76ab3f3cf5
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/DistributedSession.cs
@@ -0,0 +1,425 @@
+// Copyright (c) .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.Security.Cryptography;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Caching.Distributed;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Session
+{
+ public class DistributedSession : ISession
+ {
+ private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create();
+ private const int IdByteCount = 16;
+
+ private const byte SerializationRevision = 2;
+ private const int KeyLengthLimit = ushort.MaxValue;
+
+ private readonly IDistributedCache _cache;
+ private readonly string _sessionKey;
+ private readonly TimeSpan _idleTimeout;
+ private readonly TimeSpan _ioTimeout;
+ private readonly Func _tryEstablishSession;
+ private readonly ILogger _logger;
+ private IDictionary _store;
+ private bool _isModified;
+ private bool _loaded;
+ private bool _isAvailable;
+ private bool _isNewSessionKey;
+ private string _sessionId;
+ private byte[] _sessionIdBytes;
+
+ public DistributedSession(
+ IDistributedCache cache,
+ string sessionKey,
+ TimeSpan idleTimeout,
+ TimeSpan ioTimeout,
+ Func tryEstablishSession,
+ ILoggerFactory loggerFactory,
+ bool isNewSessionKey)
+ {
+ if (cache == null)
+ {
+ throw new ArgumentNullException(nameof(cache));
+ }
+
+ if (string.IsNullOrEmpty(sessionKey))
+ {
+ throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(sessionKey));
+ }
+
+ if (tryEstablishSession == null)
+ {
+ throw new ArgumentNullException(nameof(tryEstablishSession));
+ }
+
+ if (loggerFactory == null)
+ {
+ throw new ArgumentNullException(nameof(loggerFactory));
+ }
+
+ _cache = cache;
+ _sessionKey = sessionKey;
+ _idleTimeout = idleTimeout;
+ _ioTimeout = ioTimeout;
+ _tryEstablishSession = tryEstablishSession;
+ _store = new Dictionary();
+ _logger = loggerFactory.CreateLogger();
+ _isNewSessionKey = isNewSessionKey;
+ }
+
+ public bool IsAvailable
+ {
+ get
+ {
+ Load();
+ return _isAvailable;
+ }
+ }
+
+ public string Id
+ {
+ get
+ {
+ Load();
+ if (_sessionId == null)
+ {
+ _sessionId = new Guid(IdBytes).ToString();
+ }
+ return _sessionId;
+ }
+ }
+
+ private byte[] IdBytes
+ {
+ get
+ {
+ if (IsAvailable && _sessionIdBytes == null)
+ {
+ _sessionIdBytes = new byte[IdByteCount];
+ CryptoRandom.GetBytes(_sessionIdBytes);
+ }
+ return _sessionIdBytes;
+ }
+ }
+
+ public IEnumerable Keys
+ {
+ get
+ {
+ Load();
+ return _store.Keys.Select(key => key.KeyString);
+ }
+ }
+
+ public bool TryGetValue(string key, out byte[] value)
+ {
+ Load();
+ return _store.TryGetValue(new EncodedKey(key), out value);
+ }
+
+ public void Set(string key, byte[] value)
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ if (IsAvailable)
+ {
+ var encodedKey = new EncodedKey(key);
+ if (encodedKey.KeyBytes.Length > KeyLengthLimit)
+ {
+ throw new ArgumentOutOfRangeException(nameof(key),
+ Resources.FormatException_KeyLengthIsExceeded(KeyLengthLimit));
+ }
+
+ if (!_tryEstablishSession())
+ {
+ throw new InvalidOperationException(Resources.Exception_InvalidSessionEstablishment);
+ }
+ _isModified = true;
+ byte[] copy = new byte[value.Length];
+ Buffer.BlockCopy(src: value, srcOffset: 0, dst: copy, dstOffset: 0, count: value.Length);
+ _store[encodedKey] = copy;
+ }
+ }
+
+ public void Remove(string key)
+ {
+ Load();
+ _isModified |= _store.Remove(new EncodedKey(key));
+ }
+
+ public void Clear()
+ {
+ Load();
+ _isModified |= _store.Count > 0;
+ _store.Clear();
+ }
+
+ private void Load()
+ {
+ if (!_loaded)
+ {
+ try
+ {
+ var data = _cache.Get(_sessionKey);
+ if (data != null)
+ {
+ Deserialize(new MemoryStream(data));
+ }
+ else if (!_isNewSessionKey)
+ {
+ _logger.AccessingExpiredSession(_sessionKey);
+ }
+ _isAvailable = true;
+ }
+ catch (Exception exception)
+ {
+ _logger.SessionCacheReadException(_sessionKey, exception);
+ _isAvailable = false;
+ _sessionId = string.Empty;
+ _sessionIdBytes = null;
+ _store = new NoOpSessionStore();
+ }
+ finally
+ {
+ _loaded = true;
+ }
+ }
+ }
+
+ // This will throw if called directly and a failure occurs. The user is expected to handle the failures.
+ public async Task LoadAsync(CancellationToken cancellationToken = default)
+ {
+ if (!_loaded)
+ {
+ using (var timeout = new CancellationTokenSource(_ioTimeout))
+ {
+ var cts = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, cancellationToken);
+ try
+ {
+ cts.Token.ThrowIfCancellationRequested();
+ var data = await _cache.GetAsync(_sessionKey, cts.Token);
+ if (data != null)
+ {
+ Deserialize(new MemoryStream(data));
+ }
+ else if (!_isNewSessionKey)
+ {
+ _logger.AccessingExpiredSession(_sessionKey);
+ }
+ }
+ catch (OperationCanceledException oex)
+ {
+ if (timeout.Token.IsCancellationRequested)
+ {
+ _logger.SessionLoadingTimeout();
+ throw new OperationCanceledException("Timed out loading the session.", oex, timeout.Token);
+ }
+ throw;
+ }
+ }
+ _isAvailable = true;
+ _loaded = true;
+ }
+ }
+
+ public async Task CommitAsync(CancellationToken cancellationToken = default)
+ {
+ using (var timeout = new CancellationTokenSource(_ioTimeout))
+ {
+ var cts = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, cancellationToken);
+ if (_isModified)
+ {
+ if (_logger.IsEnabled(LogLevel.Information))
+ {
+ // This operation is only so we can log if the session already existed.
+ // Log and ignore failures.
+ try
+ {
+ cts.Token.ThrowIfCancellationRequested();
+ var data = await _cache.GetAsync(_sessionKey, cts.Token);
+ if (data == null)
+ {
+ _logger.SessionStarted(_sessionKey, Id);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ catch (Exception exception)
+ {
+ _logger.SessionCacheReadException(_sessionKey, exception);
+ }
+ }
+
+ var stream = new MemoryStream();
+ Serialize(stream);
+
+ try
+ {
+ cts.Token.ThrowIfCancellationRequested();
+ await _cache.SetAsync(
+ _sessionKey,
+ stream.ToArray(),
+ new DistributedCacheEntryOptions().SetSlidingExpiration(_idleTimeout),
+ cts.Token);
+ _isModified = false;
+ _logger.SessionStored(_sessionKey, Id, _store.Count);
+ }
+ catch (OperationCanceledException oex)
+ {
+ if (timeout.Token.IsCancellationRequested)
+ {
+ _logger.SessionCommitTimeout();
+ throw new OperationCanceledException("Timed out committing the session.", oex, timeout.Token);
+ }
+ throw;
+ }
+ }
+ else
+ {
+ try
+ {
+ await _cache.RefreshAsync(_sessionKey, cts.Token);
+ }
+ catch (OperationCanceledException oex)
+ {
+ if (timeout.Token.IsCancellationRequested)
+ {
+ _logger.SessionRefreshTimeout();
+ throw new OperationCanceledException("Timed out refreshing the session.", oex, timeout.Token);
+ }
+ throw;
+ }
+ }
+ }
+ }
+
+ // Format:
+ // Serialization revision: 1 byte, range 0-255
+ // Entry count: 3 bytes, range 0-16,777,215
+ // SessionId: IdByteCount bytes (16)
+ // foreach entry:
+ // key name byte length: 2 bytes, range 0-65,535
+ // UTF-8 encoded key name byte[]
+ // data byte length: 4 bytes, range 0-2,147,483,647
+ // data byte[]
+ private void Serialize(Stream output)
+ {
+ output.WriteByte(SerializationRevision);
+ SerializeNumAs3Bytes(output, _store.Count);
+ output.Write(IdBytes, 0, IdByteCount);
+
+ foreach (var entry in _store)
+ {
+ var keyBytes = entry.Key.KeyBytes;
+ SerializeNumAs2Bytes(output, keyBytes.Length);
+ output.Write(keyBytes, 0, keyBytes.Length);
+ SerializeNumAs4Bytes(output, entry.Value.Length);
+ output.Write(entry.Value, 0, entry.Value.Length);
+ }
+ }
+
+ private void Deserialize(Stream content)
+ {
+ if (content == null || content.ReadByte() != SerializationRevision)
+ {
+ // Replace the un-readable format.
+ _isModified = true;
+ return;
+ }
+
+ int expectedEntries = DeserializeNumFrom3Bytes(content);
+ _sessionIdBytes = ReadBytes(content, IdByteCount);
+
+ for (int i = 0; i < expectedEntries; i++)
+ {
+ int keyLength = DeserializeNumFrom2Bytes(content);
+ var key = new EncodedKey(ReadBytes(content, keyLength));
+ int dataLength = DeserializeNumFrom4Bytes(content);
+ _store[key] = ReadBytes(content, dataLength);
+ }
+
+ if (_logger.IsEnabled(LogLevel.Debug))
+ {
+ _sessionId = new Guid(_sessionIdBytes).ToString();
+ _logger.SessionLoaded(_sessionKey, _sessionId, expectedEntries);
+ }
+ }
+
+ private void SerializeNumAs2Bytes(Stream output, int num)
+ {
+ if (num < 0 || ushort.MaxValue < num)
+ {
+ throw new ArgumentOutOfRangeException(nameof(num), Resources.Exception_InvalidToSerializeIn2Bytes);
+ }
+ output.WriteByte((byte)(num >> 8));
+ output.WriteByte((byte)(0xFF & num));
+ }
+
+ private int DeserializeNumFrom2Bytes(Stream content)
+ {
+ return content.ReadByte() << 8 | content.ReadByte();
+ }
+
+ private void SerializeNumAs3Bytes(Stream output, int num)
+ {
+ if (num < 0 || 0xFFFFFF < num)
+ {
+ throw new ArgumentOutOfRangeException(nameof(num), Resources.Exception_InvalidToSerializeIn3Bytes);
+ }
+ output.WriteByte((byte)(num >> 16));
+ output.WriteByte((byte)(0xFF & (num >> 8)));
+ output.WriteByte((byte)(0xFF & num));
+ }
+
+ private int DeserializeNumFrom3Bytes(Stream content)
+ {
+ return content.ReadByte() << 16 | content.ReadByte() << 8 | content.ReadByte();
+ }
+
+ private void SerializeNumAs4Bytes(Stream output, int num)
+ {
+ if (num < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(num), Resources.Exception_NumberShouldNotBeNegative);
+ }
+ output.WriteByte((byte)(num >> 24));
+ output.WriteByte((byte)(0xFF & (num >> 16)));
+ output.WriteByte((byte)(0xFF & (num >> 8)));
+ output.WriteByte((byte)(0xFF & num));
+ }
+
+ private int DeserializeNumFrom4Bytes(Stream content)
+ {
+ return content.ReadByte() << 24 | content.ReadByte() << 16 | content.ReadByte() << 8 | content.ReadByte();
+ }
+
+ private byte[] ReadBytes(Stream stream, int count)
+ {
+ var output = new byte[count];
+ int total = 0;
+ while (total < count)
+ {
+ var read = stream.Read(output, total, count - total);
+ if (read == 0)
+ {
+ throw new EndOfStreamException();
+ }
+ total += read;
+ }
+ return output;
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/DistributedSessionStore.cs b/src/Session/src/Microsoft.AspNetCore.Session/DistributedSessionStore.cs
new file mode 100644
index 0000000000..49050af588
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/DistributedSessionStore.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 Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Caching.Distributed;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Session
+{
+ public class DistributedSessionStore : ISessionStore
+ {
+ private readonly IDistributedCache _cache;
+ private readonly ILoggerFactory _loggerFactory;
+
+ public DistributedSessionStore(IDistributedCache cache, ILoggerFactory loggerFactory)
+ {
+ if (cache == null)
+ {
+ throw new ArgumentNullException(nameof(cache));
+ }
+
+ if (loggerFactory == null)
+ {
+ throw new ArgumentNullException(nameof(loggerFactory));
+ }
+
+ _cache = cache;
+ _loggerFactory = loggerFactory;
+ }
+
+ public ISession Create(string sessionKey, TimeSpan idleTimeout, TimeSpan ioTimeout, Func tryEstablishSession, bool isNewSessionKey)
+ {
+ if (string.IsNullOrEmpty(sessionKey))
+ {
+ throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(sessionKey));
+ }
+
+ if (tryEstablishSession == null)
+ {
+ throw new ArgumentNullException(nameof(tryEstablishSession));
+ }
+
+ return new DistributedSession(_cache, sessionKey, idleTimeout, ioTimeout, tryEstablishSession, _loggerFactory, isNewSessionKey);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/EncodedKey.cs b/src/Session/src/Microsoft.AspNetCore.Session/EncodedKey.cs
new file mode 100644
index 0000000000..ac169542f5
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/EncodedKey.cs
@@ -0,0 +1,80 @@
+// Copyright (c) .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.Text;
+
+namespace Microsoft.AspNetCore.Session
+{
+ // Keys are stored in their utf-8 encoded state.
+ // This saves us from de-serializing and re-serializing every key on every request.
+ internal class EncodedKey
+ {
+ private string _keyString;
+ private int? _hashCode;
+
+ internal EncodedKey(string key)
+ {
+ _keyString = key;
+ KeyBytes = Encoding.UTF8.GetBytes(key);
+ }
+
+ public EncodedKey(byte[] key)
+ {
+ KeyBytes = key;
+ }
+
+ internal string KeyString
+ {
+ get
+ {
+ if (_keyString == null)
+ {
+ _keyString = Encoding.UTF8.GetString(KeyBytes, 0, KeyBytes.Length);
+ }
+ return _keyString;
+ }
+ }
+
+ internal byte[] KeyBytes { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ var otherKey = obj as EncodedKey;
+ if (otherKey == null)
+ {
+ return false;
+ }
+ if (KeyBytes.Length != otherKey.KeyBytes.Length)
+ {
+ return false;
+ }
+ if (_hashCode.HasValue && otherKey._hashCode.HasValue
+ && _hashCode.Value != otherKey._hashCode.Value)
+ {
+ return false;
+ }
+ for (int i = 0; i < KeyBytes.Length; i++)
+ {
+ if (KeyBytes[i] != otherKey.KeyBytes[i])
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public override int GetHashCode()
+ {
+ if (!_hashCode.HasValue)
+ {
+ _hashCode = SipHash.GetHashCode(KeyBytes);
+ }
+ return _hashCode.Value;
+ }
+
+ public override string ToString()
+ {
+ return KeyString;
+ }
+ }
+}
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/ISessionStore.cs b/src/Session/src/Microsoft.AspNetCore.Session/ISessionStore.cs
new file mode 100644
index 0000000000..247ba2307f
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/ISessionStore.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 System;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Session
+{
+ public interface ISessionStore
+ {
+ ISession Create(string sessionKey, TimeSpan idleTimeout, TimeSpan ioTimeout, Func tryEstablishSession, bool isNewSessionKey);
+ }
+}
\ No newline at end of file
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/LoggingExtensions.cs b/src/Session/src/Microsoft.AspNetCore.Session/LoggingExtensions.cs
new file mode 100644
index 0000000000..2552ac20de
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/LoggingExtensions.cs
@@ -0,0 +1,135 @@
+// Copyright (c) .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.Extensions.Logging
+{
+ internal static class LoggingExtensions
+ {
+ private static Action _errorClosingTheSession;
+ private static Action _accessingExpiredSession;
+ private static Action _sessionStarted;
+ private static Action _sessionLoaded;
+ private static Action _sessionStored;
+ private static Action _sessionCacheReadException;
+ private static Action _errorUnprotectingCookie;
+ private static Action _sessionLoadingTimeout;
+ private static Action _sessionCommitTimeout;
+ private static Action _sessionCommitCanceled;
+ private static Action _sessionRefreshTimeout;
+ private static Action _sessionRefreshCanceled;
+
+ static LoggingExtensions()
+ {
+ _errorClosingTheSession = LoggerMessage.Define(
+ eventId: 1,
+ logLevel: LogLevel.Error,
+ formatString: "Error closing the session.");
+ _accessingExpiredSession = LoggerMessage.Define(
+ eventId: 2,
+ logLevel: LogLevel.Information,
+ formatString: "Accessing expired session, Key:{sessionKey}");
+ _sessionStarted = LoggerMessage.Define(
+ eventId: 3,
+ logLevel: LogLevel.Information,
+ formatString: "Session started; Key:{sessionKey}, Id:{sessionId}");
+ _sessionLoaded = LoggerMessage.Define(
+ eventId: 4,
+ logLevel: LogLevel.Debug,
+ formatString: "Session loaded; Key:{sessionKey}, Id:{sessionId}, Count:{count}");
+ _sessionStored = LoggerMessage.Define(
+ eventId: 5,
+ logLevel: LogLevel.Debug,
+ formatString: "Session stored; Key:{sessionKey}, Id:{sessionId}, Count:{count}");
+ _sessionCacheReadException = LoggerMessage.Define(
+ eventId: 6,
+ logLevel: LogLevel.Error,
+ formatString: "Session cache read exception, Key:{sessionKey}");
+ _errorUnprotectingCookie = LoggerMessage.Define(
+ eventId: 7,
+ logLevel: LogLevel.Warning,
+ formatString: "Error unprotecting the session cookie.");
+ _sessionLoadingTimeout = LoggerMessage.Define(
+ eventId: 8,
+ logLevel: LogLevel.Warning,
+ formatString: "Loading the session timed out.");
+ _sessionCommitTimeout = LoggerMessage.Define(
+ eventId: 9,
+ logLevel: LogLevel.Warning,
+ formatString: "Committing the session timed out.");
+ _sessionCommitCanceled = LoggerMessage.Define(
+ eventId: 10,
+ logLevel: LogLevel.Information,
+ formatString: "Committing the session was canceled.");
+ _sessionRefreshTimeout = LoggerMessage.Define(
+ eventId: 11,
+ logLevel: LogLevel.Warning,
+ formatString: "Refreshing the session timed out.");
+ _sessionRefreshCanceled = LoggerMessage.Define(
+ eventId: 12,
+ logLevel: LogLevel.Information,
+ formatString: "Refreshing the session was canceled.");
+ }
+
+ public static void ErrorClosingTheSession(this ILogger logger, Exception exception)
+ {
+ _errorClosingTheSession(logger, exception);
+ }
+
+ public static void AccessingExpiredSession(this ILogger logger, string sessionKey)
+ {
+ _accessingExpiredSession(logger, sessionKey, null);
+ }
+
+ public static void SessionStarted(this ILogger logger, string sessionKey, string sessionId)
+ {
+ _sessionStarted(logger, sessionKey, sessionId, null);
+ }
+
+ public static void SessionLoaded(this ILogger logger, string sessionKey, string sessionId, int count)
+ {
+ _sessionLoaded(logger, sessionKey, sessionId, count, null);
+ }
+
+ public static void SessionStored(this ILogger logger, string sessionKey, string sessionId, int count)
+ {
+ _sessionStored(logger, sessionKey, sessionId, count, null);
+ }
+
+ public static void SessionCacheReadException(this ILogger logger, string sessionKey, Exception exception)
+ {
+ _sessionCacheReadException(logger, sessionKey, exception);
+ }
+
+ public static void ErrorUnprotectingSessionCookie(this ILogger logger, Exception exception)
+ {
+ _errorUnprotectingCookie(logger, exception);
+ }
+
+ public static void SessionLoadingTimeout(this ILogger logger)
+ {
+ _sessionLoadingTimeout(logger, null);
+ }
+
+ public static void SessionCommitTimeout(this ILogger logger)
+ {
+ _sessionCommitTimeout(logger, null);
+ }
+
+ public static void SessionCommitCanceled(this ILogger logger)
+ {
+ _sessionCommitCanceled(logger, null);
+ }
+
+ public static void SessionRefreshTimeout(this ILogger logger)
+ {
+ _sessionRefreshTimeout(logger, null);
+ }
+
+ public static void SessionRefreshCanceled(this ILogger logger)
+ {
+ _sessionRefreshCanceled(logger, null);
+ }
+ }
+}
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/Microsoft.AspNetCore.Session.csproj b/src/Session/src/Microsoft.AspNetCore.Session/Microsoft.AspNetCore.Session.csproj
new file mode 100644
index 0000000000..3da00b9032
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/Microsoft.AspNetCore.Session.csproj
@@ -0,0 +1,20 @@
+
+
+
+ ASP.NET Core session state middleware.
+ netstandard2.0
+ $(NoWarn);CS1591
+ true
+ true
+ aspnetcore;session;sessionstate
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/NoOpSessionStore.cs b/src/Session/src/Microsoft.AspNetCore.Session/NoOpSessionStore.cs
new file mode 100644
index 0000000000..6a89ad3900
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/NoOpSessionStore.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;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.AspNetCore.Session
+{
+ internal class NoOpSessionStore : IDictionary
+ {
+ public byte[] this[EncodedKey key]
+ {
+ get
+ {
+ return null;
+ }
+
+ set
+ {
+
+ }
+ }
+
+ public int Count { get; } = 0;
+
+ public bool IsReadOnly { get; } = false;
+
+ public ICollection Keys { get; } = new EncodedKey[0];
+
+ public ICollection Values { get; } = new byte[0][];
+
+ public void Add(KeyValuePair item) { }
+
+ public void Add(EncodedKey key, byte[] value) { }
+
+ public void Clear() { }
+
+ public bool Contains(KeyValuePair item) => false;
+
+ public bool ContainsKey(EncodedKey key) => false;
+
+ public void CopyTo(KeyValuePair[] array, int arrayIndex) { }
+
+ public IEnumerator> GetEnumerator() => Enumerable.Empty>().GetEnumerator();
+
+ public bool Remove(KeyValuePair item) => false;
+
+ public bool Remove(EncodedKey key) => false;
+
+ public bool TryGetValue(EncodedKey key, out byte[] value)
+ {
+ value = null;
+ return false;
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ }
+}
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/Properties/Resources.Designer.cs b/src/Session/src/Microsoft.AspNetCore.Session/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..d071f6b698
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/Properties/Resources.Designer.cs
@@ -0,0 +1,126 @@
+//
+namespace Microsoft.AspNetCore.Session
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNetCore.Session.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ ///
+ /// The key cannot be longer than '{0}' when encoded with UTF-8.
+ ///
+ internal static string Exception_KeyLengthIsExceeded
+ {
+ get { return GetString("Exception_KeyLengthIsExceeded"); }
+ }
+
+ ///
+ /// The key cannot be longer than '{0}' when encoded with UTF-8.
+ ///
+ internal static string FormatException_KeyLengthIsExceeded(object p0)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Exception_KeyLengthIsExceeded"), p0);
+ }
+
+ ///
+ /// The session cannot be established after the response has started.
+ ///
+ internal static string Exception_InvalidSessionEstablishment
+ {
+ get { return GetString("Exception_InvalidSessionEstablishment"); }
+ }
+
+ ///
+ /// The session cannot be established after the response has started.
+ ///
+ internal static string FormatException_InvalidSessionEstablishment()
+ {
+ return GetString("Exception_InvalidSessionEstablishment");
+ }
+
+ ///
+ /// The value cannot be serialized in two bytes.
+ ///
+ internal static string Exception_InvalidToSerializeIn2Bytes
+ {
+ get { return GetString("Exception_InvalidToSerializeIn2Bytes"); }
+ }
+
+ ///
+ /// The value cannot be serialized in two bytes.
+ ///
+ internal static string FormatException_InvalidToSerializeIn2Bytes()
+ {
+ return GetString("Exception_InvalidToSerializeIn2Bytes");
+ }
+
+ ///
+ /// The value cannot be serialized in three bytes.
+ ///
+ internal static string Exception_InvalidToSerializeIn3Bytes
+ {
+ get { return GetString("Exception_InvalidToSerializeIn3Bytes"); }
+ }
+
+ ///
+ /// The value cannot be serialized in three bytes.
+ ///
+ internal static string FormatException_InvalidToSerializeIn3Bytes()
+ {
+ return GetString("Exception_InvalidToSerializeIn3Bytes");
+ }
+
+ ///
+ /// The value cannot be negative.
+ ///
+ internal static string Exception_NumberShouldNotBeNegative
+ {
+ get { return GetString("Exception_NumberShouldNotBeNegative"); }
+ }
+
+ ///
+ /// The value cannot be negative.
+ ///
+ internal static string FormatException_NumberShouldNotBeNegative()
+ {
+ return GetString("Exception_NumberShouldNotBeNegative");
+ }
+
+ ///
+ /// Argument cannot be null or empty string.
+ ///
+ internal static string ArgumentCannotBeNullOrEmpty
+ {
+ get { return GetString("ArgumentCannotBeNullOrEmpty"); }
+ }
+
+ ///
+ /// Argument cannot be null or empty string.
+ ///
+ internal static string FormatArgumentCannotBeNullOrEmpty()
+ {
+ return GetString("ArgumentCannotBeNullOrEmpty");
+ }
+
+ 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/Session/src/Microsoft.AspNetCore.Session/Resources.resx b/src/Session/src/Microsoft.AspNetCore.Session/Resources.resx
new file mode 100644
index 0000000000..5bb1c8e0bb
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/Resources.resx
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+ The key cannot be longer than '{0}' when encoded with UTF-8.
+
+
+ The session cannot be established after the response has started.
+
+
+ The value cannot be serialized in two bytes.
+
+
+ The value cannot be serialized in three bytes.
+
+
+ The value cannot be negative.
+
+
+ Argument cannot be null or empty string.
+
+
\ No newline at end of file
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/SessionDefaults.cs b/src/Session/src/Microsoft.AspNetCore.Session/SessionDefaults.cs
new file mode 100644
index 0000000000..e9f590a0c7
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/SessionDefaults.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.
+
+namespace Microsoft.AspNetCore.Session
+{
+ ///
+ /// Represents defaults for the Session.
+ ///
+ public static class SessionDefaults
+ {
+ ///
+ /// Represent the default cookie name, which is ".AspNetCore.Session".
+ ///
+ public static readonly string CookieName = ".AspNetCore.Session";
+
+ ///
+ /// Represents the default path used to create the cookie, which is "/".
+ ///
+ public static readonly string CookiePath = "/";
+ }
+}
\ No newline at end of file
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/SessionFeature.cs b/src/Session/src/Microsoft.AspNetCore.Session/SessionFeature.cs
new file mode 100644
index 0000000000..44a378e614
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/SessionFeature.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 Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+
+namespace Microsoft.AspNetCore.Session
+{
+ public class SessionFeature : ISessionFeature
+ {
+ public ISession Session { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/SessionMiddleware.cs b/src/Session/src/Microsoft.AspNetCore.Session/SessionMiddleware.cs
new file mode 100644
index 0000000000..bd74afa9cb
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/SessionMiddleware.cs
@@ -0,0 +1,174 @@
+// Copyright (c) .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.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Session
+{
+ ///
+ /// Enables the session state for the application.
+ ///
+ public class SessionMiddleware
+ {
+ private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create();
+ private const int SessionKeyLength = 36; // "382c74c3-721d-4f34-80e5-57657b6cbc27"
+ private static readonly Func ReturnTrue = () => true;
+ private readonly RequestDelegate _next;
+ private readonly SessionOptions _options;
+ private readonly ILogger _logger;
+ private readonly ISessionStore _sessionStore;
+ private readonly IDataProtector _dataProtector;
+
+ ///
+ /// Creates a new .
+ ///
+ /// The representing the next middleware in the pipeline.
+ /// The representing the factory that used to create logger instances.
+ /// The used to protect and verify the cookie.
+ /// The representing the session store.
+ /// The session configuration options.
+ public SessionMiddleware(
+ RequestDelegate next,
+ ILoggerFactory loggerFactory,
+ IDataProtectionProvider dataProtectionProvider,
+ ISessionStore sessionStore,
+ IOptions options)
+ {
+ if (next == null)
+ {
+ throw new ArgumentNullException(nameof(next));
+ }
+
+ if (loggerFactory == null)
+ {
+ throw new ArgumentNullException(nameof(loggerFactory));
+ }
+
+ if (dataProtectionProvider == null)
+ {
+ throw new ArgumentNullException(nameof(dataProtectionProvider));
+ }
+
+ if (sessionStore == null)
+ {
+ throw new ArgumentNullException(nameof(sessionStore));
+ }
+
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ _next = next;
+ _logger = loggerFactory.CreateLogger();
+ _dataProtector = dataProtectionProvider.CreateProtector(nameof(SessionMiddleware));
+ _options = options.Value;
+ _sessionStore = sessionStore;
+ }
+
+ ///
+ /// Invokes the logic of the middleware.
+ ///
+ /// The .
+ /// A that completes when the middleware has completed processing.
+ public async Task Invoke(HttpContext context)
+ {
+ var isNewSessionKey = false;
+ Func tryEstablishSession = ReturnTrue;
+ var cookieValue = context.Request.Cookies[_options.Cookie.Name];
+ var sessionKey = CookieProtection.Unprotect(_dataProtector, cookieValue, _logger);
+ if (string.IsNullOrWhiteSpace(sessionKey) || sessionKey.Length != SessionKeyLength)
+ {
+ // No valid cookie, new session.
+ var guidBytes = new byte[16];
+ CryptoRandom.GetBytes(guidBytes);
+ sessionKey = new Guid(guidBytes).ToString();
+ cookieValue = CookieProtection.Protect(_dataProtector, sessionKey);
+ var establisher = new SessionEstablisher(context, cookieValue, _options);
+ tryEstablishSession = establisher.TryEstablishSession;
+ isNewSessionKey = true;
+ }
+
+ var feature = new SessionFeature();
+ feature.Session = _sessionStore.Create(sessionKey, _options.IdleTimeout, _options.IOTimeout, tryEstablishSession, isNewSessionKey);
+ context.Features.Set(feature);
+
+ try
+ {
+ await _next(context);
+ }
+ finally
+ {
+ context.Features.Set(null);
+
+ if (feature.Session != null)
+ {
+ try
+ {
+ await feature.Session.CommitAsync(context.RequestAborted);
+ }
+ catch (OperationCanceledException)
+ {
+ _logger.SessionCommitCanceled();
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorClosingTheSession(ex);
+ }
+ }
+ }
+ }
+
+ private class SessionEstablisher
+ {
+ private readonly HttpContext _context;
+ private readonly string _cookieValue;
+ private readonly SessionOptions _options;
+ private bool _shouldEstablishSession;
+
+ public SessionEstablisher(HttpContext context, string cookieValue, SessionOptions options)
+ {
+ _context = context;
+ _cookieValue = cookieValue;
+ _options = options;
+ context.Response.OnStarting(OnStartingCallback, state: this);
+ }
+
+ private static Task OnStartingCallback(object state)
+ {
+ var establisher = (SessionEstablisher)state;
+ if (establisher._shouldEstablishSession)
+ {
+ establisher.SetCookie();
+ }
+ return Task.FromResult(0);
+ }
+
+ private void SetCookie()
+ {
+ var cookieOptions = _options.Cookie.Build(_context);
+
+ _context.Response.Cookies.Append(_options.Cookie.Name, _cookieValue, cookieOptions);
+
+ _context.Response.Headers["Cache-Control"] = "no-cache";
+ _context.Response.Headers["Pragma"] = "no-cache";
+ _context.Response.Headers["Expires"] = "-1";
+ }
+
+ // Returns true if the session has already been established, or if it still can be because the response has not been sent.
+ internal bool TryEstablishSession()
+ {
+ return (_shouldEstablishSession |= !_context.Response.HasStarted);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/SessionMiddlewareExtensions.cs b/src/Session/src/Microsoft.AspNetCore.Session/SessionMiddlewareExtensions.cs
new file mode 100644
index 0000000000..c273124379
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/SessionMiddlewareExtensions.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;
+using Microsoft.AspNetCore.Session;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ ///
+ /// Extension methods for adding the to an application.
+ ///
+ public static class SessionMiddlewareExtensions
+ {
+ ///
+ /// Adds the to automatically enable session state for the application.
+ ///
+ /// The .
+ /// The .
+ public static IApplicationBuilder UseSession(this IApplicationBuilder app)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+
+ return app.UseMiddleware();
+ }
+
+ ///
+ /// Adds the to automatically enable session state for the application.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ public static IApplicationBuilder UseSession(this IApplicationBuilder app, SessionOptions options)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ return app.UseMiddleware(Options.Create(options));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/SessionOptions.cs b/src/Session/src/Microsoft.AspNetCore.Session/SessionOptions.cs
new file mode 100644
index 0000000000..4d456b0f60
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/SessionOptions.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.Threading;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Session;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ ///
+ /// Represents the session state options for the application.
+ ///
+ public class SessionOptions
+ {
+ private CookieBuilder _cookieBuilder = new SessionCookieBuilder();
+
+ ///
+ /// Determines the settings used to create the cookie.
+ ///
+ /// defaults to .
+ /// defaults to .
+ /// defaults to .
+ /// defaults to true
+ /// defaults to false
+ ///
+ ///
+ public CookieBuilder Cookie
+ {
+ get => _cookieBuilder;
+ set => _cookieBuilder = value ?? throw new ArgumentNullException(nameof(value));
+ }
+
+ ///
+ /// The IdleTimeout indicates how long the session can be idle before its contents are abandoned. Each session access
+ /// resets the timeout. Note this only applies to the content of the session, not the cookie.
+ ///
+ public TimeSpan IdleTimeout { get; set; } = TimeSpan.FromMinutes(20);
+
+ ///
+ /// The maximim amount of time allowed to load a session from the store or to commit it back to the store.
+ /// Note this may only apply to asynchronous operations. This timeout can be disabled using .
+ ///
+ public TimeSpan IOTimeout { get; set; } = TimeSpan.FromMinutes(1);
+
+ #region Obsolete API
+ ///
+ ///
+ /// This property is obsolete and will be removed in a future version. The recommended alternative is on .
+ ///
+ ///
+ /// Determines the cookie name used to persist the session ID.
+ ///
+ ///
+ [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.Name) + ".")]
+ public string CookieName { get => Cookie.Name; set => Cookie.Name = value; }
+
+ ///
+ ///
+ /// This property is obsolete and will be removed in a future version. The recommended alternative is on .
+ ///
+ ///
+ /// Determines the domain used to create the cookie. Is not provided by default.
+ ///
+ ///
+ [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.Domain) + ".")]
+ public string CookieDomain { get => Cookie.Domain; set => Cookie.Domain = value; }
+
+ ///
+ ///
+ /// This property is obsolete and will be removed in a future version. The recommended alternative is on .
+ ///
+ ///
+ /// Determines the path used to create the cookie.
+ /// Defaults to .
+ ///
+ ///
+ [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.Path) + ".")]
+ public string CookiePath { get => Cookie.Path; set => Cookie.Path = value; }
+
+ ///
+ ///
+ /// This property is obsolete and will be removed in a future version. The recommended alternative is on .
+ ///
+ ///
+ /// Determines if the browser should allow the cookie to be accessed by client-side JavaScript. The
+ /// default is true, which means the cookie will only be passed to HTTP requests and is not made available
+ /// to script on the page.
+ ///
+ ///
+ [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.HttpOnly) + ".")]
+ public bool CookieHttpOnly { get => Cookie.HttpOnly; set => Cookie.HttpOnly = value; }
+
+ ///
+ ///
+ /// This property is obsolete and will be removed in a future version. The recommended alternative is on .
+ ///
+ ///
+ /// Determines if the cookie should only be transmitted on HTTPS requests.
+ ///
+ ///
+ [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.SecurePolicy) + ".")]
+ public CookieSecurePolicy CookieSecure { get => Cookie.SecurePolicy; set => Cookie.SecurePolicy = value; }
+ #endregion
+
+ private class SessionCookieBuilder : CookieBuilder
+ {
+ public SessionCookieBuilder()
+ {
+ Name = SessionDefaults.CookieName;
+ Path = SessionDefaults.CookiePath;
+ SecurePolicy = CookieSecurePolicy.None;
+ SameSite = SameSiteMode.Lax;
+ HttpOnly = true;
+ // Session is considered non-essential as it's designed for ephemeral data.
+ IsEssential = false;
+ }
+
+ public override TimeSpan? Expiration
+ {
+ get => null;
+ set => throw new InvalidOperationException(nameof(Expiration) + " cannot be set for the cookie defined by " + nameof(SessionOptions));
+ }
+ }
+ }
+}
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/SessionServiceCollectionExtensions.cs b/src/Session/src/Microsoft.AspNetCore.Session/SessionServiceCollectionExtensions.cs
new file mode 100644
index 0000000000..628390fbe3
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/SessionServiceCollectionExtensions.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 Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Session;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ ///
+ /// Extension methods for adding session services to the DI container.
+ ///
+ public static class SessionServiceCollectionExtensions
+ {
+ ///
+ /// Adds services required for application session state.
+ ///
+ /// The to add the services to.
+ /// The so that additional calls can be chained.
+ public static IServiceCollection AddSession(this IServiceCollection services)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ services.TryAddTransient();
+ services.AddDataProtection();
+ return services;
+ }
+
+ ///
+ /// Adds services required for application session state.
+ ///
+ /// The to add the services to.
+ /// The session options to configure the middleware with.
+ /// The so that additional calls can be chained.
+ public static IServiceCollection AddSession(this IServiceCollection services, Action configure)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ if (configure == null)
+ {
+ throw new ArgumentNullException(nameof(configure));
+ }
+
+ services.Configure(configure);
+ services.AddSession();
+
+ return services;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/SipHash.cs b/src/Session/src/Microsoft.AspNetCore.Session/SipHash.cs
new file mode 100644
index 0000000000..bad98fcab3
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/SipHash.cs
@@ -0,0 +1,202 @@
+// Copyright (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.Session
+{
+ // A byte[] equality comparer based on the SipHash-2-4 algorithm. Key differences:
+ // (a) we output 32-bit hashes instead of 64-bit hashes, and
+ // (b) we don't care about endianness since hashes are used only in hash tables
+ // and aren't returned to user code.
+ //
+ // Derived from the implementation in SignalR and modified to address byte[] instead of string. This derived version is not for cryptographic use, just hash codes.
+ // https://github.com/aspnet/SignalR-Server/blob/75f74169c81a51780f195d06b798302b2d76dbde/src/Microsoft.AspNetCore.SignalR.Server/Infrastructure/SipHashBasedStringEqualityComparer.cs
+ // Derivative work of https://github.com/tanglebones/ch-siphash.
+ internal static class SipHash
+ {
+ internal static int GetHashCode(byte[] bytes)
+ {
+ // Assume SipHash-2-4 is a strong PRF, therefore truncation to 32 bits is acceptable.
+ return (int)SipHash_2_4_UlongCast_ForcedInline(bytes);
+ }
+
+ private static ulong SipHash_2_4_UlongCast_ForcedInline(byte[] bytes)
+ {
+ unsafe
+ {
+ ulong v0 = 0x736f6d6570736575;
+ ulong v1 = 0x646f72616e646f6d;
+ ulong v2 = 0x6c7967656e657261;
+ ulong v3 = 0x7465646279746573;
+
+ uint inlen = (uint)bytes.Length;
+ fixed (byte* finb = bytes)
+ {
+ var b = ((ulong)inlen) << 56;
+
+ if (inlen > 0)
+ {
+ var inb = finb;
+ var left = inlen & 7;
+ var end = inb + inlen - left;
+ var linb = (ulong*)finb;
+ var lend = (ulong*)end;
+ for (; linb < lend; ++linb)
+ {
+ v3 ^= *linb;
+
+ v0 += v1;
+ v1 = (v1 << 13) | (v1 >> (64 - 13));
+ v1 ^= v0;
+ v0 = (v0 << 32) | (v0 >> (64 - 32));
+
+ v2 += v3;
+ v3 = (v3 << 16) | (v3 >> (64 - 16));
+ v3 ^= v2;
+
+ v0 += v3;
+ v3 = (v3 << 21) | (v3 >> (64 - 21));
+ v3 ^= v0;
+
+ v2 += v1;
+ v1 = (v1 << 17) | (v1 >> (64 - 17));
+ v1 ^= v2;
+ v2 = (v2 << 32) | (v2 >> (64 - 32));
+ v0 += v1;
+ v1 = (v1 << 13) | (v1 >> (64 - 13));
+ v1 ^= v0;
+ v0 = (v0 << 32) | (v0 >> (64 - 32));
+
+ v2 += v3;
+ v3 = (v3 << 16) | (v3 >> (64 - 16));
+ v3 ^= v2;
+
+ v0 += v3;
+ v3 = (v3 << 21) | (v3 >> (64 - 21));
+ v3 ^= v0;
+
+ v2 += v1;
+ v1 = (v1 << 17) | (v1 >> (64 - 17));
+ v1 ^= v2;
+ v2 = (v2 << 32) | (v2 >> (64 - 32));
+
+ v0 ^= *linb;
+ }
+ for (var i = 0; i < left; ++i)
+ {
+ b |= ((ulong)end[i]) << (8 * i);
+ }
+ }
+
+ v3 ^= b;
+ v0 += v1;
+ v1 = (v1 << 13) | (v1 >> (64 - 13));
+ v1 ^= v0;
+ v0 = (v0 << 32) | (v0 >> (64 - 32));
+
+ v2 += v3;
+ v3 = (v3 << 16) | (v3 >> (64 - 16));
+ v3 ^= v2;
+
+ v0 += v3;
+ v3 = (v3 << 21) | (v3 >> (64 - 21));
+ v3 ^= v0;
+
+ v2 += v1;
+ v1 = (v1 << 17) | (v1 >> (64 - 17));
+ v1 ^= v2;
+ v2 = (v2 << 32) | (v2 >> (64 - 32));
+ v0 += v1;
+ v1 = (v1 << 13) | (v1 >> (64 - 13));
+ v1 ^= v0;
+ v0 = (v0 << 32) | (v0 >> (64 - 32));
+
+ v2 += v3;
+ v3 = (v3 << 16) | (v3 >> (64 - 16));
+ v3 ^= v2;
+
+ v0 += v3;
+ v3 = (v3 << 21) | (v3 >> (64 - 21));
+ v3 ^= v0;
+
+ v2 += v1;
+ v1 = (v1 << 17) | (v1 >> (64 - 17));
+ v1 ^= v2;
+ v2 = (v2 << 32) | (v2 >> (64 - 32));
+ v0 ^= b;
+ v2 ^= 0xff;
+
+ v0 += v1;
+ v1 = (v1 << 13) | (v1 >> (64 - 13));
+ v1 ^= v0;
+ v0 = (v0 << 32) | (v0 >> (64 - 32));
+
+ v2 += v3;
+ v3 = (v3 << 16) | (v3 >> (64 - 16));
+ v3 ^= v2;
+
+ v0 += v3;
+ v3 = (v3 << 21) | (v3 >> (64 - 21));
+ v3 ^= v0;
+
+ v2 += v1;
+ v1 = (v1 << 17) | (v1 >> (64 - 17));
+ v1 ^= v2;
+ v2 = (v2 << 32) | (v2 >> (64 - 32));
+ v0 += v1;
+ v1 = (v1 << 13) | (v1 >> (64 - 13));
+ v1 ^= v0;
+ v0 = (v0 << 32) | (v0 >> (64 - 32));
+
+ v2 += v3;
+ v3 = (v3 << 16) | (v3 >> (64 - 16));
+ v3 ^= v2;
+
+ v0 += v3;
+ v3 = (v3 << 21) | (v3 >> (64 - 21));
+ v3 ^= v0;
+
+ v2 += v1;
+ v1 = (v1 << 17) | (v1 >> (64 - 17));
+ v1 ^= v2;
+ v2 = (v2 << 32) | (v2 >> (64 - 32));
+ v0 += v1;
+ v1 = (v1 << 13) | (v1 >> (64 - 13));
+ v1 ^= v0;
+ v0 = (v0 << 32) | (v0 >> (64 - 32));
+
+ v2 += v3;
+ v3 = (v3 << 16) | (v3 >> (64 - 16));
+ v3 ^= v2;
+
+ v0 += v3;
+ v3 = (v3 << 21) | (v3 >> (64 - 21));
+ v3 ^= v0;
+
+ v2 += v1;
+ v1 = (v1 << 17) | (v1 >> (64 - 17));
+ v1 ^= v2;
+ v2 = (v2 << 32) | (v2 >> (64 - 32));
+ v0 += v1;
+ v1 = (v1 << 13) | (v1 >> (64 - 13));
+ v1 ^= v0;
+ v0 = (v0 << 32) | (v0 >> (64 - 32));
+
+ v2 += v3;
+ v3 = (v3 << 16) | (v3 >> (64 - 16));
+ v3 ^= v2;
+
+ v0 += v3;
+ v3 = (v3 << 21) | (v3 >> (64 - 21));
+ v3 ^= v0;
+
+ v2 += v1;
+ v1 = (v1 << 17) | (v1 >> (64 - 17));
+ v1 ^= v2;
+ v2 = (v2 << 32) | (v2 >> (64 - 32));
+ }
+
+ return v0 ^ v1 ^ v2 ^ v3;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Session/src/Microsoft.AspNetCore.Session/baseline.netcore.json b/src/Session/src/Microsoft.AspNetCore.Session/baseline.netcore.json
new file mode 100644
index 0000000000..ff9ee6811f
--- /dev/null
+++ b/src/Session/src/Microsoft.AspNetCore.Session/baseline.netcore.json
@@ -0,0 +1,687 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.Session, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.Extensions.DependencyInjection.SessionServiceCollectionExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AddSession",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddSession",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ },
+ {
+ "Name": "configure",
+ "Type": "System.Action"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.SessionMiddlewareExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "UseSession",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseSession",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Builder.SessionOptions"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.SessionOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Cookie",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.CookieBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Cookie",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.CookieBuilder"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_IdleTimeout",
+ "Parameters": [],
+ "ReturnType": "System.TimeSpan",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_IdleTimeout",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.TimeSpan"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_IOTimeout",
+ "Parameters": [],
+ "ReturnType": "System.TimeSpan",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_IOTimeout",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.TimeSpan"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_CookieName",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_CookieName",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_CookieDomain",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_CookieDomain",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_CookiePath",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_CookiePath",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_CookieHttpOnly",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_CookieHttpOnly",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_CookieSecure",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.CookieSecurePolicy",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_CookieSecure",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.CookieSecurePolicy"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Session.DistributedSession",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Http.ISession"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_IsAvailable",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Http.ISession",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Id",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Http.ISession",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Keys",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IEnumerable",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Http.ISession",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "TryGetValue",
+ "Parameters": [
+ {
+ "Name": "key",
+ "Type": "System.String"
+ },
+ {
+ "Name": "value",
+ "Type": "System.Byte[]",
+ "Direction": "Out"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Http.ISession",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Set",
+ "Parameters": [
+ {
+ "Name": "key",
+ "Type": "System.String"
+ },
+ {
+ "Name": "value",
+ "Type": "System.Byte[]"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Http.ISession",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Remove",
+ "Parameters": [
+ {
+ "Name": "key",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Http.ISession",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Clear",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Http.ISession",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "LoadAsync",
+ "Parameters": [
+ {
+ "Name": "cancellationToken",
+ "Type": "System.Threading.CancellationToken",
+ "DefaultValue": "default(System.Threading.CancellationToken)"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Http.ISession",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CommitAsync",
+ "Parameters": [
+ {
+ "Name": "cancellationToken",
+ "Type": "System.Threading.CancellationToken",
+ "DefaultValue": "default(System.Threading.CancellationToken)"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Http.ISession",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "cache",
+ "Type": "Microsoft.Extensions.Caching.Distributed.IDistributedCache"
+ },
+ {
+ "Name": "sessionKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "idleTimeout",
+ "Type": "System.TimeSpan"
+ },
+ {
+ "Name": "ioTimeout",
+ "Type": "System.TimeSpan"
+ },
+ {
+ "Name": "tryEstablishSession",
+ "Type": "System.Func"
+ },
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ },
+ {
+ "Name": "isNewSessionKey",
+ "Type": "System.Boolean"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Session.DistributedSessionStore",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Session.ISessionStore"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Create",
+ "Parameters": [
+ {
+ "Name": "sessionKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "idleTimeout",
+ "Type": "System.TimeSpan"
+ },
+ {
+ "Name": "ioTimeout",
+ "Type": "System.TimeSpan"
+ },
+ {
+ "Name": "tryEstablishSession",
+ "Type": "System.Func"
+ },
+ {
+ "Name": "isNewSessionKey",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Http.ISession",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Session.ISessionStore",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "cache",
+ "Type": "Microsoft.Extensions.Caching.Distributed.IDistributedCache"
+ },
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Session.ISessionStore",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Create",
+ "Parameters": [
+ {
+ "Name": "sessionKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "idleTimeout",
+ "Type": "System.TimeSpan"
+ },
+ {
+ "Name": "ioTimeout",
+ "Type": "System.TimeSpan"
+ },
+ {
+ "Name": "tryEstablishSession",
+ "Type": "System.Func"
+ },
+ {
+ "Name": "isNewSessionKey",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Http.ISession",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Session.SessionDefaults",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Field",
+ "Name": "CookieName",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "CookiePath",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Session.SessionFeature",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Http.Features.ISessionFeature"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Session",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.ISession",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.ISessionFeature",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Session",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.ISession"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Http.Features.ISessionFeature",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Session.SessionMiddleware",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Invoke",
+ "Parameters": [
+ {
+ "Name": "context",
+ "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": "dataProtectionProvider",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider"
+ },
+ {
+ "Name": "sessionStore",
+ "Type": "Microsoft.AspNetCore.Session.ISessionStore"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptions"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/Session/test/Directory.Build.props b/src/Session/test/Directory.Build.props
new file mode 100644
index 0000000000..270e1fa209
--- /dev/null
+++ b/src/Session/test/Directory.Build.props
@@ -0,0 +1,14 @@
+
+
+
+
+ netcoreapp2.1
+ $(DeveloperBuildTestTfms)
+ netcoreapp2.1;netcoreapp2.0
+ $(StandardTestTfms);net461
+
+
+
+
+
+
diff --git a/src/Session/test/Microsoft.AspNetCore.Session.Tests/Microsoft.AspNetCore.Session.Tests.csproj b/src/Session/test/Microsoft.AspNetCore.Session.Tests/Microsoft.AspNetCore.Session.Tests.csproj
new file mode 100644
index 0000000000..9dc11b9d2f
--- /dev/null
+++ b/src/Session/test/Microsoft.AspNetCore.Session.Tests/Microsoft.AspNetCore.Session.Tests.csproj
@@ -0,0 +1,22 @@
+
+
+
+ $(StandardTestTfms)
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Session/test/Microsoft.AspNetCore.Session.Tests/SessionTests.cs b/src/Session/test/Microsoft.AspNetCore.Session.Tests/SessionTests.cs
new file mode 100644
index 0000000000..f98a018bc5
--- /dev/null
+++ b/src/Session/test/Microsoft.AspNetCore.Session.Tests/SessionTests.cs
@@ -0,0 +1,1016 @@
+// Copyright (c) .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.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.Caching.Distributed;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Internal;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Logging.Testing;
+using Microsoft.Extensions.Options;
+using Microsoft.Net.Http.Headers;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Session
+{
+ public class SessionTests
+ {
+ [Fact]
+ public async Task ReadingEmptySessionDoesNotCreateCookie()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+
+ app.Run(context =>
+ {
+ Assert.Null(context.Session.GetString("NotFound"));
+ return Task.FromResult(0);
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddDistributedMemoryCache();
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ Assert.False(response.Headers.TryGetValues("Set-Cookie", out var _));
+ }
+ }
+
+ [Fact]
+ public async Task SettingAValueCausesTheCookieToBeCreated()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(context =>
+ {
+ Assert.Null(context.Session.GetString("Key"));
+ context.Session.SetString("Key", "Value");
+ Assert.Equal("Value", context.Session.GetString("Key"));
+ return Task.FromResult(0);
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddDistributedMemoryCache();
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ Assert.True(response.Headers.TryGetValues("Set-Cookie", out var values));
+ Assert.Single(values);
+ Assert.True(!string.IsNullOrWhiteSpace(values.First()));
+ }
+ }
+
+ [Theory]
+ [InlineData(CookieSecurePolicy.Always, "http://example.com/testpath", true)]
+ [InlineData(CookieSecurePolicy.Always, "https://example.com/testpath", true)]
+ [InlineData(CookieSecurePolicy.None, "http://example.com/testpath", false)]
+ [InlineData(CookieSecurePolicy.None, "https://example.com/testpath", false)]
+ [InlineData(CookieSecurePolicy.SameAsRequest, "http://example.com/testpath", false)]
+ [InlineData(CookieSecurePolicy.SameAsRequest, "https://example.com/testpath", true)]
+ public async Task SecureSessionBasedOnHttpsAndSecurePolicy(
+ CookieSecurePolicy cookieSecurePolicy,
+ string requestUri,
+ bool shouldBeSecureOnly)
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession(new SessionOptions
+ {
+ Cookie =
+ {
+ Name = "TestCookie",
+ SecurePolicy = cookieSecurePolicy
+ }
+ });
+ app.Run(context =>
+ {
+ Assert.Null(context.Session.GetString("Key"));
+ context.Session.SetString("Key", "Value");
+ Assert.Equal("Value", context.Session.GetString("Key"));
+ return Task.FromResult(0);
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddDistributedMemoryCache();
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(requestUri);
+ response.EnsureSuccessStatusCode();
+ Assert.True(response.Headers.TryGetValues("Set-Cookie", out var values));
+ Assert.Single(values);
+ if (shouldBeSecureOnly)
+ {
+ Assert.Contains("; secure", values.First());
+ }
+ else
+ {
+ Assert.DoesNotContain("; secure", values.First());
+ }
+ }
+ }
+
+ [Fact]
+ public async Task SessionCanBeAccessedOnTheNextRequest()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(context =>
+ {
+ int? value = context.Session.GetInt32("Key");
+ if (context.Request.Path == new PathString("/first"))
+ {
+ Assert.False(value.HasValue);
+ value = 0;
+ }
+ Assert.True(value.HasValue);
+ context.Session.SetInt32("Key", value.Value + 1);
+ return context.Response.WriteAsync(value.Value.ToString());
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddDistributedMemoryCache();
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync("first");
+ response.EnsureSuccessStatusCode();
+ Assert.Equal("0", await response.Content.ReadAsStringAsync());
+
+ client = server.CreateClient();
+ var cookie = SetCookieHeaderValue.ParseList(response.Headers.GetValues("Set-Cookie").ToList()).First();
+ client.DefaultRequestHeaders.Add("Cookie", new CookieHeaderValue(cookie.Name, cookie.Value).ToString());
+ Assert.Equal("1", await client.GetStringAsync("/"));
+ Assert.Equal("2", await client.GetStringAsync("/"));
+ Assert.Equal("3", await client.GetStringAsync("/"));
+ }
+ }
+
+ [Fact]
+ public async Task RemovedItemCannotBeAccessedAgain()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(context =>
+ {
+ int? value = context.Session.GetInt32("Key");
+ if (context.Request.Path == new PathString("/first"))
+ {
+ Assert.False(value.HasValue);
+ value = 0;
+ context.Session.SetInt32("Key", 1);
+ }
+ else if (context.Request.Path == new PathString("/second"))
+ {
+ Assert.True(value.HasValue);
+ Assert.Equal(1, value);
+ context.Session.Remove("Key");
+ }
+ else if (context.Request.Path == new PathString("/third"))
+ {
+ Assert.False(value.HasValue);
+ value = 2;
+ }
+ return context.Response.WriteAsync(value.Value.ToString());
+ });
+ })
+ .ConfigureServices(
+ services =>
+ {
+ services.AddDistributedMemoryCache();
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync("first");
+ response.EnsureSuccessStatusCode();
+ Assert.Equal("0", await response.Content.ReadAsStringAsync());
+
+ client = server.CreateClient();
+ var cookie = SetCookieHeaderValue.ParseList(response.Headers.GetValues("Set-Cookie").ToList()).First();
+ client.DefaultRequestHeaders.Add("Cookie", new CookieHeaderValue(cookie.Name, cookie.Value).ToString());
+ Assert.Equal("1", await client.GetStringAsync("/second"));
+ Assert.Equal("2", await client.GetStringAsync("/third"));
+ }
+ }
+
+ [Fact]
+ public async Task ClearedItemsCannotBeAccessedAgain()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(context =>
+ {
+ int? value = context.Session.GetInt32("Key");
+ if (context.Request.Path == new PathString("/first"))
+ {
+ Assert.False(value.HasValue);
+ value = 0;
+ context.Session.SetInt32("Key", 1);
+ }
+ else if (context.Request.Path == new PathString("/second"))
+ {
+ Assert.True(value.HasValue);
+ Assert.Equal(1, value);
+ context.Session.Clear();
+ }
+ else if (context.Request.Path == new PathString("/third"))
+ {
+ Assert.False(value.HasValue);
+ value = 2;
+ }
+ return context.Response.WriteAsync(value.Value.ToString());
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddDistributedMemoryCache();
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync("first");
+ response.EnsureSuccessStatusCode();
+ Assert.Equal("0", await response.Content.ReadAsStringAsync());
+
+ client = server.CreateClient();
+ var cookie = SetCookieHeaderValue.ParseList(response.Headers.GetValues("Set-Cookie").ToList()).First();
+ client.DefaultRequestHeaders.Add("Cookie", new CookieHeaderValue(cookie.Name, cookie.Value).ToString());
+ Assert.Equal("1", await client.GetStringAsync("/second"));
+ Assert.Equal("2", await client.GetStringAsync("/third"));
+ }
+ }
+
+ [Fact]
+ public async Task SessionStart_LogsInformation()
+ {
+ var sink = new TestSink(
+ TestSink.EnableWithTypeName,
+ TestSink.EnableWithTypeName);
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(context =>
+ {
+ context.Session.SetString("Key", "Value");
+ return Task.FromResult(0);
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
+ services.AddDistributedMemoryCache();
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ }
+
+ var sessionLogMessages = sink.Writes.ToList();
+
+ Assert.Equal(2, sessionLogMessages.Count);
+ Assert.Contains("started", sessionLogMessages[0].State.ToString());
+ Assert.Equal(LogLevel.Information, sessionLogMessages[0].LogLevel);
+ Assert.Contains("stored", sessionLogMessages[1].State.ToString());
+ Assert.Equal(LogLevel.Debug, sessionLogMessages[1].LogLevel);
+ }
+
+ [Fact]
+ public async Task ExpiredSession_LogsInfo()
+ {
+ var sink = new TestSink(
+ TestSink.EnableWithTypeName,
+ TestSink.EnableWithTypeName);
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(context =>
+ {
+ int? value = context.Session.GetInt32("Key");
+ if (context.Request.Path == new PathString("/first"))
+ {
+ Assert.False(value.HasValue);
+ value = 1;
+ context.Session.SetInt32("Key", 1);
+ }
+ else if (context.Request.Path == new PathString("/second"))
+ {
+ Assert.False(value.HasValue);
+ value = 2;
+ }
+ return context.Response.WriteAsync(value.Value.ToString());
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
+ services.AddDistributedMemoryCache();
+ services.AddSession(o => o.IdleTimeout = TimeSpan.FromMilliseconds(30));
+ });
+
+ string result;
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync("first");
+ response.EnsureSuccessStatusCode();
+
+ client = server.CreateClient();
+ var cookie = SetCookieHeaderValue.ParseList(response.Headers.GetValues("Set-Cookie").ToList()).First();
+ client.DefaultRequestHeaders.Add("Cookie", new CookieHeaderValue(cookie.Name, cookie.Value).ToString());
+ Thread.Sleep(50);
+ result = await client.GetStringAsync("/second");
+ }
+
+ var sessionLogMessages = sink.Writes.ToList();
+
+ Assert.Equal("2", result);
+ Assert.Equal(3, sessionLogMessages.Count);
+ Assert.Contains("started", sessionLogMessages[0].State.ToString());
+ Assert.Contains("stored", sessionLogMessages[1].State.ToString());
+ Assert.Contains("expired", sessionLogMessages[2].State.ToString());
+ Assert.Equal(LogLevel.Information, sessionLogMessages[0].LogLevel);
+ Assert.Equal(LogLevel.Debug, sessionLogMessages[1].LogLevel);
+ Assert.Equal(LogLevel.Information, sessionLogMessages[2].LogLevel);
+ }
+
+ [Fact]
+ public async Task RefreshesSession_WhenSessionData_IsNotModified()
+ {
+ var clock = new TestClock();
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(context =>
+ {
+ string responseData = string.Empty;
+ if (context.Request.Path == new PathString("/AddDataToSession"))
+ {
+ context.Session.SetInt32("Key", 10);
+ responseData = "added data to session";
+ }
+ else if (context.Request.Path == new PathString("/AccessSessionData"))
+ {
+ var value = context.Session.GetInt32("Key");
+ responseData = (value == null) ? "No value found in session." : value.ToString();
+ }
+ else if (context.Request.Path == new PathString("/DoNotAccessSessionData"))
+ {
+ responseData = "did not access session data";
+ }
+
+ return context.Response.WriteAsync(responseData);
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton(typeof(ILoggerFactory), NullLoggerFactory.Instance);
+ services.AddDistributedMemoryCache();
+ services.AddSession(o => o.IdleTimeout = TimeSpan.FromMinutes(20));
+ services.Configure(o => o.Clock = clock);
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync("AddDataToSession");
+ response.EnsureSuccessStatusCode();
+
+ client = server.CreateClient();
+ var cookie = SetCookieHeaderValue.ParseList(response.Headers.GetValues("Set-Cookie").ToList()).First();
+ client.DefaultRequestHeaders.Add(
+ "Cookie", new CookieHeaderValue(cookie.Name, cookie.Value).ToString());
+
+ for (var i = 0; i < 5; i++)
+ {
+ clock.Add(TimeSpan.FromMinutes(10));
+ await client.GetStringAsync("/DoNotAccessSessionData");
+ }
+
+ var data = await client.GetStringAsync("/AccessSessionData");
+ Assert.Equal("10", data);
+ }
+ }
+
+ [Fact]
+ public async Task SessionFeature_IsUnregistered_WhenResponseGoingOut()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.Use(async (httpContext, next) =>
+ {
+ await next();
+
+ Assert.Null(httpContext.Features.Get());
+ });
+
+ app.UseSession();
+
+ app.Run(context =>
+ {
+ context.Session.SetString("key", "value");
+ return Task.FromResult(0);
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddDistributedMemoryCache();
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ }
+ }
+
+ [Fact]
+ public async Task SessionFeature_IsUnregistered_WhenResponseGoingOut_AndAnUnhandledExcetionIsThrown()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.Use(async (httpContext, next) =>
+ {
+ var exceptionThrown = false;
+ try
+ {
+ await next();
+ }
+ catch
+ {
+ exceptionThrown = true;
+ }
+
+ Assert.True(exceptionThrown);
+ Assert.Null(httpContext.Features.Get());
+ });
+
+ app.UseSession();
+
+ app.Run(context =>
+ {
+ throw new InvalidOperationException("An error occurred.");
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddDistributedMemoryCache();
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ }
+ }
+
+ [Fact]
+ public async Task SessionKeys_AreCaseSensitive()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(context =>
+ {
+ context.Session.SetString("KEY", "VALUE");
+ context.Session.SetString("key", "value");
+ Assert.Equal("VALUE", context.Session.GetString("KEY"));
+ Assert.Equal("value", context.Session.GetString("key"));
+ return Task.FromResult(0);
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddDistributedMemoryCache();
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ }
+ }
+
+ [Fact]
+ public async Task SessionLogsCacheReadException()
+ {
+ var sink = new TestSink(
+ TestSink.EnableWithTypeName,
+ TestSink.EnableWithTypeName);
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(context =>
+ {
+ Assert.False(context.Session.TryGetValue("key", out var value));
+ Assert.Null(value);
+ Assert.Equal(string.Empty, context.Session.Id);
+ Assert.False(context.Session.Keys.Any());
+ return Task.FromResult(0);
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
+ services.AddSingleton(new UnreliableCache(new MemoryCache(new MemoryCacheOptions()))
+ {
+ DisableGet = true
+ });
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ }
+
+ var message = Assert.Single(sink.Writes);
+ Assert.Contains("Session cache read exception", message.State.ToString());
+ Assert.Equal(LogLevel.Error, message.LogLevel);
+ }
+
+ [Fact]
+ public async Task SessionLogsCacheLoadAsyncException()
+ {
+ var sink = new TestSink(
+ TestSink.EnableWithTypeName,
+ TestSink.EnableWithTypeName);
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(async context =>
+ {
+ await Assert.ThrowsAsync(() => context.Session.LoadAsync());
+ Assert.False(context.Session.IsAvailable);
+ Assert.Equal(string.Empty, context.Session.Id);
+ Assert.False(context.Session.Keys.Any());
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
+ services.AddSingleton(new UnreliableCache(new MemoryCache(new MemoryCacheOptions()))
+ {
+ DisableGet = true
+ });
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ }
+
+ var message = Assert.Single(sink.Writes);
+ Assert.Contains("Session cache read exception", message.State.ToString());
+ Assert.Equal(LogLevel.Error, message.LogLevel);
+ }
+
+ [Fact]
+ public async Task SessionLogsCacheLoadAsyncTimeoutException()
+ {
+ var sink = new TestSink(
+ TestSink.EnableWithTypeName,
+ TestSink.EnableWithTypeName);
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession(new SessionOptions()
+ {
+ IOTimeout = TimeSpan.FromSeconds(0.5)
+ });
+ app.Run(async context =>
+ {
+ await Assert.ThrowsAsync(() => context.Session.LoadAsync());
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
+ services.AddSingleton(new UnreliableCache(new MemoryCache(new MemoryCacheOptions()))
+ {
+ DelayGetAsync = true
+ });
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ }
+
+ var message = Assert.Single(sink.Writes);
+ Assert.Contains("Loading the session timed out.", message.State.ToString());
+ Assert.Equal(LogLevel.Warning, message.LogLevel);
+ }
+
+ [Fact]
+ public async Task SessionLoadAsyncCanceledException()
+ {
+ var sink = new TestSink(
+ TestSink.EnableWithTypeName,
+ TestSink.EnableWithTypeName);
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(async context =>
+ {
+ var cts = new CancellationTokenSource();
+ var token = cts.Token;
+ cts.Cancel();
+ await Assert.ThrowsAsync(() => context.Session.LoadAsync(token));
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
+ services.AddSingleton(new UnreliableCache(new MemoryCache(new MemoryCacheOptions()))
+ {
+ DelayGetAsync = true
+ });
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ }
+
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public async Task SessionLogsCacheCommitException()
+ {
+ var sink = new TestSink(
+ writeContext =>
+ {
+ return writeContext.LoggerName.Equals(typeof(SessionMiddleware).FullName)
+ || writeContext.LoggerName.Equals(typeof(DistributedSession).FullName);
+ },
+ beginScopeContext =>
+ {
+ return beginScopeContext.LoggerName.Equals(typeof(SessionMiddleware).FullName)
+ || beginScopeContext.LoggerName.Equals(typeof(DistributedSession).FullName);
+ });
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(context =>
+ {
+ context.Session.SetInt32("key", 0);
+ return Task.FromResult(0);
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
+ services.AddSingleton(new UnreliableCache(new MemoryCache(new MemoryCacheOptions()))
+ {
+ DisableSetAsync = true
+ });
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ }
+
+ var sessionLogMessage = sink.Writes.Where(message => message.LoggerName.Equals(typeof(DistributedSession).FullName, StringComparison.Ordinal)).Single();
+
+ Assert.Contains("Session started", sessionLogMessage.State.ToString());
+ Assert.Equal(LogLevel.Information, sessionLogMessage.LogLevel);
+
+ var sessionMiddlewareLogMessage = sink.Writes.Where(message => message.LoggerName.Equals(typeof(SessionMiddleware).FullName, StringComparison.Ordinal)).Single();
+
+ Assert.Contains("Error closing the session.", sessionMiddlewareLogMessage.State.ToString());
+ Assert.Equal(LogLevel.Error, sessionMiddlewareLogMessage.LogLevel);
+ }
+
+ [Fact]
+ public async Task SessionLogsCacheCommitTimeoutException()
+ {
+ var sink = new TestSink(
+ writeContext =>
+ {
+ return writeContext.LoggerName.Equals(typeof(SessionMiddleware).FullName)
+ || writeContext.LoggerName.Equals(typeof(DistributedSession).FullName);
+ },
+ beginScopeContext =>
+ {
+ return beginScopeContext.LoggerName.Equals(typeof(SessionMiddleware).FullName)
+ || beginScopeContext.LoggerName.Equals(typeof(DistributedSession).FullName);
+ });
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession(new SessionOptions()
+ {
+ IOTimeout = TimeSpan.FromSeconds(0.5)
+ });
+ app.Run(context =>
+ {
+ context.Session.SetInt32("key", 0);
+ return Task.FromResult(0);
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
+ services.AddSingleton(new UnreliableCache(new MemoryCache(new MemoryCacheOptions()))
+ {
+ DelaySetAsync = true
+ });
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ }
+
+ var sessionLogMessages = sink.Writes.Where(message => message.LoggerName.Equals(typeof(DistributedSession).FullName, StringComparison.Ordinal)).ToList();
+
+ Assert.Contains("Session started", sessionLogMessages[0].State.ToString());
+ Assert.Equal(LogLevel.Information, sessionLogMessages[0].LogLevel);
+
+ Assert.Contains("Committing the session timed out.", sessionLogMessages[1].State.ToString());
+ Assert.Equal(LogLevel.Warning, sessionLogMessages[1].LogLevel);
+
+ var sessionMiddlewareLogs = sink.Writes.Where(message => message.LoggerName.Equals(typeof(SessionMiddleware).FullName, StringComparison.Ordinal)).ToList();
+
+ Assert.Contains("Committing the session was canceled.", sessionMiddlewareLogs[0].State.ToString());
+ Assert.Equal(LogLevel.Information, sessionMiddlewareLogs[0].LogLevel);
+ }
+
+ [Fact]
+ public async Task SessionLogsCacheCommitCanceledException()
+ {
+ var sink = new TestSink(
+ writeContext =>
+ {
+ return writeContext.LoggerName.Equals(typeof(SessionMiddleware).FullName)
+ || writeContext.LoggerName.Equals(typeof(DistributedSession).FullName);
+ },
+ beginScopeContext =>
+ {
+ return beginScopeContext.LoggerName.Equals(typeof(SessionMiddleware).FullName)
+ || beginScopeContext.LoggerName.Equals(typeof(DistributedSession).FullName);
+ });
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(async context =>
+ {
+ context.Session.SetInt32("key", 0);
+ var cts = new CancellationTokenSource();
+ var token = cts.Token;
+ cts.Cancel();
+ await Assert.ThrowsAsync(() => context.Session.CommitAsync(token));
+ context.RequestAborted = token;
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
+ services.AddSingleton(new UnreliableCache(new MemoryCache(new MemoryCacheOptions()))
+ {
+ DelaySetAsync = true
+ });
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ }
+
+ Assert.Empty(sink.Writes.Where(message => message.LoggerName.Equals(typeof(DistributedSession).FullName, StringComparison.Ordinal)));
+
+ var sessionMiddlewareLogs = sink.Writes.Where(message => message.LoggerName.Equals(typeof(SessionMiddleware).FullName, StringComparison.Ordinal)).ToList();
+
+ Assert.Contains("Committing the session was canceled.", sessionMiddlewareLogs[0].State.ToString());
+ Assert.Equal(LogLevel.Information, sessionMiddlewareLogs[0].LogLevel);
+ }
+
+ [Fact]
+ public async Task SessionLogsCacheRefreshException()
+ {
+ var sink = new TestSink(
+ TestSink.EnableWithTypeName,
+ TestSink.EnableWithTypeName);
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseSession();
+ app.Run(context =>
+ {
+ // The middleware calls context.Session.CommitAsync() once per request
+ return Task.FromResult(0);
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
+ services.AddSingleton(new UnreliableCache(new MemoryCache(new MemoryCacheOptions()))
+ {
+ DisableRefreshAsync = true
+ });
+ services.AddSession();
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var response = await client.GetAsync(string.Empty);
+ response.EnsureSuccessStatusCode();
+ }
+
+ var message = Assert.Single(sink.Writes);
+ Assert.Contains("Error closing the session.", message.State.ToString());
+ Assert.Equal(LogLevel.Error, message.LogLevel);
+ }
+
+ private class TestClock : ISystemClock
+ {
+ public TestClock()
+ {
+ UtcNow = new DateTimeOffset(2013, 1, 1, 1, 0, 0, TimeSpan.Zero);
+ }
+
+ public DateTimeOffset UtcNow { get; private set; }
+
+ public void Add(TimeSpan timespan)
+ {
+ UtcNow = UtcNow.Add(timespan);
+ }
+ }
+
+ private class UnreliableCache : IDistributedCache
+ {
+ private readonly MemoryDistributedCache _cache;
+
+ public bool DisableGet { get; set; }
+ public bool DisableSetAsync { get; set; }
+ public bool DisableRefreshAsync { get; set; }
+ public bool DelayGetAsync { get; set; }
+ public bool DelaySetAsync { get; set; }
+ public bool DelayRefreshAsync { get; set; }
+
+ public UnreliableCache(IMemoryCache memoryCache)
+ {
+ _cache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions()));
+ }
+
+ public byte[] Get(string key)
+ {
+ if (DisableGet)
+ {
+ throw new InvalidOperationException();
+ }
+ return _cache.Get(key);
+ }
+
+ public Task GetAsync(string key, CancellationToken token = default)
+ {
+ if (DisableGet)
+ {
+ throw new InvalidOperationException();
+ }
+ if (DelayGetAsync)
+ {
+ token.WaitHandle.WaitOne(TimeSpan.FromSeconds(10));
+ token.ThrowIfCancellationRequested();
+ }
+ return _cache.GetAsync(key, token);
+ }
+
+ public void Refresh(string key) => _cache.Refresh(key);
+
+ public Task RefreshAsync(string key, CancellationToken token = default)
+ {
+ if (DisableRefreshAsync)
+ {
+ throw new InvalidOperationException();
+ }
+ if (DelayRefreshAsync)
+ {
+ token.WaitHandle.WaitOne(TimeSpan.FromSeconds(10));
+ token.ThrowIfCancellationRequested();
+ }
+ return _cache.RefreshAsync(key);
+ }
+
+ public void Remove(string key) => _cache.Remove(key);
+
+ public Task RemoveAsync(string key, CancellationToken token = default) => _cache.RemoveAsync(key);
+
+ public void Set(string key, byte[] value, DistributedCacheEntryOptions options) => _cache.Set(key, value, options);
+
+ public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default)
+ {
+ if (DisableSetAsync)
+ {
+ throw new InvalidOperationException();
+ }
+ if (DelaySetAsync)
+ {
+ token.WaitHandle.WaitOne(TimeSpan.FromSeconds(10));
+ token.ThrowIfCancellationRequested();
+ }
+ return _cache.SetAsync(key, value, options);
+ }
+ }
+ }
+}
diff --git a/src/Session/version.props b/src/Session/version.props
new file mode 100644
index 0000000000..669c874829
--- /dev/null
+++ b/src/Session/version.props
@@ -0,0 +1,12 @@
+
+
+ 2.1.1
+ rtm
+ $(VersionPrefix)
+ $(VersionPrefix)-$(VersionSuffix)-final
+ t000
+ a-
+ $(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-'))
+ $(VersionSuffix)-$(BuildNumber)
+
+
diff --git a/src/StaticFiles/.gitignore b/src/StaticFiles/.gitignore
new file mode 100644
index 0000000000..bcc811de9a
--- /dev/null
+++ b/src/StaticFiles/.gitignore
@@ -0,0 +1,31 @@
+[Oo]bj/
+[Bb]in/
+TestResults/
+.nuget/
+_ReSharper.*/
+packages/
+artifacts/
+PublishProfiles/
+*.user
+*.suo
+*.cache
+*.docstates
+_ReSharper.*
+nuget.exe
+*net45.csproj
+*net451.csproj
+*k10.csproj
+*.psess
+*.vsp
+*.pidb
+*.userprefs
+*DS_Store
+*.ncrunchsolution
+*.*sdf
+*.ipch
+*.sln.ide
+project.lock.json
+.build/
+.testPublish/
+/.vs/
+global.json
diff --git a/src/StaticFiles/Directory.Build.props b/src/StaticFiles/Directory.Build.props
new file mode 100644
index 0000000000..2c2d9cc9d0
--- /dev/null
+++ b/src/StaticFiles/Directory.Build.props
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+ Microsoft ASP.NET Core
+ https://github.com/aspnet/StaticFiles
+ git
+ $(MSBuildThisFileDirectory)
+ $(MSBuildThisFileDirectory)build\Key.snk
+ true
+ true
+ true
+
+
+
diff --git a/src/StaticFiles/Directory.Build.targets b/src/StaticFiles/Directory.Build.targets
new file mode 100644
index 0000000000..53b3f6e1da
--- /dev/null
+++ b/src/StaticFiles/Directory.Build.targets
@@ -0,0 +1,7 @@
+
+
+ $(MicrosoftNETCoreApp20PackageVersion)
+ $(MicrosoftNETCoreApp21PackageVersion)
+ $(NETStandardLibrary20PackageVersion)
+
+
diff --git a/src/StaticFiles/NuGetPackageVerifier.json b/src/StaticFiles/NuGetPackageVerifier.json
new file mode 100644
index 0000000000..4328f86b21
--- /dev/null
+++ b/src/StaticFiles/NuGetPackageVerifier.json
@@ -0,0 +1,13 @@
+{
+ "adx-nonshipping": {
+ "rules": [],
+ "packages": {
+ "Microsoft.AspNetCore.RangeHelper.Sources": {}
+ }
+ },
+ "Default": {
+ "rules": [
+ "DefaultCompositeRule"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/src/StaticFiles/README.md b/src/StaticFiles/README.md
new file mode 100644
index 0000000000..dca5218cb5
--- /dev/null
+++ b/src/StaticFiles/README.md
@@ -0,0 +1,10 @@
+StaticFiles
+===========
+
+AppVeyor: [](https://ci.appveyor.com/project/aspnetci/StaticFiles/branch/dev)
+
+Travis: [](https://travis-ci.org/aspnet/StaticFiles)
+
+This repo contains middleware for handling requests for file system resources including files and directories.
+
+This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [Home](https://github.com/aspnet/home) repo.
diff --git a/src/StaticFiles/StaticFiles.sln b/src/StaticFiles/StaticFiles.sln
new file mode 100644
index 0000000000..0e5ab3b48f
--- /dev/null
+++ b/src/StaticFiles/StaticFiles.sln
@@ -0,0 +1,110 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.26228.9
+MinimumVisualStudioVersion = 15.0.26730.03
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{40EE0889-960E-41B4-A3D3-9CE963EB0797}"
+ ProjectSection(SolutionItems) = preProject
+ src\Directory.Build.props = src\Directory.Build.props
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{8B21A3A9-9CA6-4857-A6E0-1A3203404B60}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.StaticFiles", "src\Microsoft.AspNetCore.StaticFiles\Microsoft.AspNetCore.StaticFiles.csproj", "{8D7BC5A4-F19C-4184-8338-A6B42997218C}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StaticFileSample", "samples\StaticFileSample\StaticFileSample.csproj", "{092141D9-305A-4FC5-AE74-CB23982CA8D4}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{EF02AFE8-7C15-4DDB-8B2C-58A676112A98}"
+ ProjectSection(SolutionItems) = preProject
+ test\Directory.Build.props = test\Directory.Build.props
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.StaticFiles.Tests", "test\Microsoft.AspNetCore.StaticFiles.Tests\Microsoft.AspNetCore.StaticFiles.Tests.csproj", "{CC87FE7D-8F42-4BE9-A152-9625E837C1E5}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.StaticFiles.FunctionalTests", "test\Microsoft.AspNetCore.StaticFiles.FunctionalTests\Microsoft.AspNetCore.StaticFiles.FunctionalTests.csproj", "{FDF0539C-1F62-4B78-91B1-C687886931CA}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RangeHelper.Sources.Test", "test\Microsoft.AspNetCore.RangeHelper.Sources.Test\Microsoft.AspNetCore.RangeHelper.Sources.Test.csproj", "{D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{360DC2F8-EEB4-4C69-9784-C686EAD78279}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microsoft.AspNetCore.RangeHelper.Sources", "Microsoft.AspNetCore.RangeHelper.Sources", "{DB6A1D14-B8A2-488F-9C4B-422FD45C8853}"
+ ProjectSection(SolutionItems) = preProject
+ shared\Microsoft.AspNetCore.RangeHelper.Sources\RangeHelper.cs = shared\Microsoft.AspNetCore.RangeHelper.Sources\RangeHelper.cs
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|Mixed Platforms = Debug|Mixed Platforms
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|Mixed Platforms = Release|Mixed Platforms
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Release|x86.ActiveCfg = Release|Any CPU
+ {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Release|x86.ActiveCfg = Release|Any CPU
+ {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Release|x86.ActiveCfg = Release|Any CPU
+ {FDF0539C-1F62-4B78-91B1-C687886931CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FDF0539C-1F62-4B78-91B1-C687886931CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FDF0539C-1F62-4B78-91B1-C687886931CA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {FDF0539C-1F62-4B78-91B1-C687886931CA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {FDF0539C-1F62-4B78-91B1-C687886931CA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {FDF0539C-1F62-4B78-91B1-C687886931CA}.Debug|x86.Build.0 = Debug|Any CPU
+ {FDF0539C-1F62-4B78-91B1-C687886931CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FDF0539C-1F62-4B78-91B1-C687886931CA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FDF0539C-1F62-4B78-91B1-C687886931CA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {FDF0539C-1F62-4B78-91B1-C687886931CA}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {FDF0539C-1F62-4B78-91B1-C687886931CA}.Release|x86.ActiveCfg = Release|Any CPU
+ {FDF0539C-1F62-4B78-91B1-C687886931CA}.Release|x86.Build.0 = Release|Any CPU
+ {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Debug|x86.Build.0 = Debug|Any CPU
+ {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Release|x86.ActiveCfg = Release|Any CPU
+ {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {8D7BC5A4-F19C-4184-8338-A6B42997218C} = {40EE0889-960E-41B4-A3D3-9CE963EB0797}
+ {092141D9-305A-4FC5-AE74-CB23982CA8D4} = {8B21A3A9-9CA6-4857-A6E0-1A3203404B60}
+ {CC87FE7D-8F42-4BE9-A152-9625E837C1E5} = {EF02AFE8-7C15-4DDB-8B2C-58A676112A98}
+ {FDF0539C-1F62-4B78-91B1-C687886931CA} = {EF02AFE8-7C15-4DDB-8B2C-58A676112A98}
+ {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA} = {EF02AFE8-7C15-4DDB-8B2C-58A676112A98}
+ {DB6A1D14-B8A2-488F-9C4B-422FD45C8853} = {360DC2F8-EEB4-4C69-9784-C686EAD78279}
+ EndGlobalSection
+EndGlobal
diff --git a/src/StaticFiles/build/Key.snk b/src/StaticFiles/build/Key.snk
new file mode 100644
index 0000000000..e10e4889c1
Binary files /dev/null and b/src/StaticFiles/build/Key.snk differ
diff --git a/src/StaticFiles/build/dependencies.props b/src/StaticFiles/build/dependencies.props
new file mode 100644
index 0000000000..531f1f0941
--- /dev/null
+++ b/src/StaticFiles/build/dependencies.props
@@ -0,0 +1,39 @@
+
+
+ $(MSBuildAllProjects);$(MSBuildThisFileFullPath)
+
+
+
+
+ 2.1.3-rtm-15802
+ 2.0.0
+ 2.1.2
+ 15.6.1
+ 4.7.49
+ 2.0.3
+ 0.8.0
+ 2.3.1
+ 2.4.0-beta.1.build3945
+
+
+
+
+
+
+
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 0.5.1
+ 2.1.2
+ 2.1.1
+ 2.1.0
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.1.1
+ 2.1.1
+
+
\ No newline at end of file
diff --git a/src/StaticFiles/build/repo.props b/src/StaticFiles/build/repo.props
new file mode 100644
index 0000000000..dab1601c88
--- /dev/null
+++ b/src/StaticFiles/build/repo.props
@@ -0,0 +1,15 @@
+
+
+
+
+
+ Internal.AspNetCore.Universe.Lineup
+ 2.1.0-rc1-*
+ https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json
+
+
+
+
+
+
+
diff --git a/src/StaticFiles/build/sources.props b/src/StaticFiles/build/sources.props
new file mode 100644
index 0000000000..9215df9751
--- /dev/null
+++ b/src/StaticFiles/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/StaticFiles/samples/StaticFileSample/Properties/launchSettings.json b/src/StaticFiles/samples/StaticFileSample/Properties/launchSettings.json
new file mode 100644
index 0000000000..50cb7a7470
--- /dev/null
+++ b/src/StaticFiles/samples/StaticFileSample/Properties/launchSettings.json
@@ -0,0 +1,27 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:35192/",
+ "sslPort": 0
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "StaticFileSample": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "launchUrl": "http://localhost:5000/",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/StaticFiles/samples/StaticFileSample/Startup.cs b/src/StaticFiles/samples/StaticFileSample/Startup.cs
new file mode 100644
index 0000000000..e731022e1f
--- /dev/null
+++ b/src/StaticFiles/samples/StaticFileSample/Startup.cs
@@ -0,0 +1,44 @@
+using System;
+using System.IO;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace StaticFilesSample
+{
+ public class Startup
+ {
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddDirectoryBrowser();
+ }
+
+ public void Configure(IApplicationBuilder app, IHostingEnvironment host)
+ {
+ Console.WriteLine("webroot: " + host.WebRootPath);
+
+ app.UseFileServer(new FileServerOptions
+ {
+ EnableDirectoryBrowsing = true
+ });
+ }
+
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .ConfigureLogging(factory =>
+ {
+ factory.AddFilter("Console", level => level >= LogLevel.Debug);
+ factory.AddConsole();
+ })
+ .UseContentRoot(Directory.GetCurrentDirectory())
+ .UseKestrel()
+ .UseIISIntegration()
+ .UseStartup()
+ .Build();
+
+ host.Run();
+ }
+ }
+}
diff --git a/src/StaticFiles/samples/StaticFileSample/StaticFileSample.csproj b/src/StaticFiles/samples/StaticFileSample/StaticFileSample.csproj
new file mode 100644
index 0000000000..bf7b35d825
--- /dev/null
+++ b/src/StaticFiles/samples/StaticFileSample/StaticFileSample.csproj
@@ -0,0 +1,17 @@
+
+
+
+ netcoreapp2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/StaticFiles/samples/StaticFileSample/wwwroot/htmlpage.html b/src/StaticFiles/samples/StaticFileSample/wwwroot/htmlpage.html
new file mode 100644
index 0000000000..c2dacddcb9
--- /dev/null
+++ b/src/StaticFiles/samples/StaticFileSample/wwwroot/htmlpage.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+ A static HTML file.
+
+
\ No newline at end of file
diff --git a/src/StaticFiles/shared/Microsoft.AspNetCore.RangeHelper.Sources/RangeHelper.cs b/src/StaticFiles/shared/Microsoft.AspNetCore.RangeHelper.Sources/RangeHelper.cs
new file mode 100644
index 0000000000..bf9769a8b9
--- /dev/null
+++ b/src/StaticFiles/shared/Microsoft.AspNetCore.RangeHelper.Sources/RangeHelper.cs
@@ -0,0 +1,127 @@
+// Copyright (c) .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 Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Headers;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.Internal
+{
+ ///
+ /// Provides a parser for the Range Header in an .
+ ///
+ internal static class RangeHelper
+ {
+ ///
+ /// Returns the normalized form of the requested range if the Range Header in the is valid.
+ ///
+ /// The associated with the request.
+ /// The associated with the given .
+ /// The total length of the file representation requested.
+ /// The .
+ /// A boolean value which represents if the contain a single valid
+ /// range request. A which represents the normalized form of the
+ /// range parsed from the or null if it cannot be normalized.
+ /// If the Range header exists but cannot be parsed correctly, or if the provided length is 0, then the range request cannot be satisfied (status 416).
+ /// This results in (true,null) return values.
+ public static (bool isRangeRequest, RangeItemHeaderValue range) ParseRange(
+ HttpContext context,
+ RequestHeaders requestHeaders,
+ long length,
+ ILogger logger)
+ {
+ var rawRangeHeader = context.Request.Headers[HeaderNames.Range];
+ if (StringValues.IsNullOrEmpty(rawRangeHeader))
+ {
+ logger.LogTrace("Range header's value is empty.");
+ return (false, null);
+ }
+
+ // Perf: Check for a single entry before parsing it
+ if (rawRangeHeader.Count > 1 || rawRangeHeader[0].IndexOf(',') >= 0)
+ {
+ logger.LogDebug("Multiple ranges are not supported.");
+
+ // The spec allows for multiple ranges but we choose not to support them because the client may request
+ // very strange ranges (e.g. each byte separately, overlapping ranges, etc.) that could negatively
+ // impact the server. Ignore the header and serve the response normally.
+ return (false, null);
+ }
+
+ var rangeHeader = requestHeaders.Range;
+ if (rangeHeader == null)
+ {
+ logger.LogDebug("Range header's value is invalid.");
+ // Invalid
+ return (false, null);
+ }
+
+ // Already verified above
+ Debug.Assert(rangeHeader.Ranges.Count == 1);
+
+ var ranges = rangeHeader.Ranges;
+ if (ranges == null)
+ {
+ logger.LogDebug("Range header's value is invalid.");
+ return (false, null);
+ }
+
+ if (ranges.Count == 0)
+ {
+ return (true, null);
+ }
+
+ if (length == 0)
+ {
+ return (true, null);
+ }
+
+ // Normalize the ranges
+ var range = NormalizeRange(ranges.SingleOrDefault(), length);
+
+ // Return the single range
+ return (true, range);
+ }
+
+ // Internal for testing
+ internal static RangeItemHeaderValue NormalizeRange(RangeItemHeaderValue range, long length)
+ {
+ var start = range.From;
+ var end = range.To;
+
+ // X-[Y]
+ if (start.HasValue)
+ {
+ if (start.Value >= length)
+ {
+ // Not satisfiable, skip/discard.
+ return null;
+ }
+ if (!end.HasValue || end.Value >= length)
+ {
+ end = length - 1;
+ }
+ }
+ else
+ {
+ // suffix range "-X" e.g. the last X bytes, resolve
+ if (end.Value == 0)
+ {
+ // Not satisfiable, skip/discard.
+ return null;
+ }
+
+ var bytes = Math.Min(end.Value, length);
+ start = length - bytes;
+ end = start + bytes - 1;
+ }
+
+ return new RangeItemHeaderValue(start, end);
+ }
+ }
+}
diff --git a/src/StaticFiles/src/Directory.Build.props b/src/StaticFiles/src/Directory.Build.props
new file mode 100644
index 0000000000..1e0980f663
--- /dev/null
+++ b/src/StaticFiles/src/Directory.Build.props
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Constants.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Constants.cs
new file mode 100644
index 0000000000..b98937a747
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Constants.cs
@@ -0,0 +1,20 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.StaticFiles
+{
+ internal static class Constants
+ {
+ internal const string ServerCapabilitiesKey = "server.Capabilities";
+ internal const string SendFileVersionKey = "sendfile.Version";
+ internal const string SendFileVersion = "1.0";
+
+ internal const int Status200Ok = 200;
+ internal const int Status206PartialContent = 206;
+ internal const int Status304NotModified = 304;
+ internal const int Status412PreconditionFailed = 412;
+ internal const int Status416RangeNotSatisfiable = 416;
+ }
+}
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/CustomDictionary.xml b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/CustomDictionary.xml
new file mode 100644
index 0000000000..78a76142f7
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/CustomDictionary.xml
@@ -0,0 +1,10 @@
+
+
+
+
+ Owin
+
+
+
+
+
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/DefaultFilesExtensions.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/DefaultFilesExtensions.cs
new file mode 100644
index 0000000000..2e8bea6977
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/DefaultFilesExtensions.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 Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.StaticFiles;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ ///
+ /// Extension methods for the DefaultFilesMiddleware
+ ///
+ public static class DefaultFilesExtensions
+ {
+ ///
+ /// Enables default file mapping on the current path
+ ///
+ ///
+ ///
+ public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+
+ return app.UseMiddleware();
+ }
+
+ ///
+ /// Enables default file mapping for the given request path
+ ///
+ ///
+ /// The relative request path.
+ ///
+ public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app, string requestPath)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+
+ return app.UseDefaultFiles(new DefaultFilesOptions
+ {
+ RequestPath = new PathString(requestPath)
+ });
+ }
+
+ ///
+ /// Enables default file mapping with the given options
+ ///
+ ///
+ ///
+ ///
+ public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app, DefaultFilesOptions options)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ return app.UseMiddleware(Options.Create(options));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/DefaultFilesMiddleware.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/DefaultFilesMiddleware.cs
new file mode 100644
index 0000000000..a401759b3d
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/DefaultFilesMiddleware.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;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Options;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.StaticFiles
+{
+ ///
+ /// This examines a directory path and determines if there is a default file present.
+ /// If so the file name is appended to the path and execution continues.
+ /// Note we don't just serve the file because it may require interpretation.
+ ///
+ public class DefaultFilesMiddleware
+ {
+ private readonly DefaultFilesOptions _options;
+ private readonly PathString _matchUrl;
+ private readonly RequestDelegate _next;
+ private readonly IFileProvider _fileProvider;
+
+ ///
+ /// Creates a new instance of the DefaultFilesMiddleware.
+ ///
+ /// The next middleware in the pipeline.
+ /// The used by this middleware.
+ /// The configuration options for this middleware.
+ public DefaultFilesMiddleware(RequestDelegate next, IHostingEnvironment hostingEnv, IOptions options)
+ {
+ if (next == null)
+ {
+ throw new ArgumentNullException(nameof(next));
+ }
+
+ if (hostingEnv == null)
+ {
+ throw new ArgumentNullException(nameof(hostingEnv));
+ }
+
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ _next = next;
+ _options = options.Value;
+ _fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);
+ _matchUrl = _options.RequestPath;
+ }
+
+ ///
+ /// This examines the request to see if it matches a configured directory, and if there are any files with the
+ /// configured default names in that directory. If so this will append the corresponding file name to the request
+ /// path for a later middleware to handle.
+ ///
+ ///
+ ///
+ public Task Invoke(HttpContext context)
+ {
+ PathString subpath;
+ if (Helpers.IsGetOrHeadMethod(context.Request.Method)
+ && Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out subpath))
+ {
+ var dirContents = _fileProvider.GetDirectoryContents(subpath.Value);
+ if (dirContents.Exists)
+ {
+ // Check if any of our default files exist.
+ for (int matchIndex = 0; matchIndex < _options.DefaultFileNames.Count; matchIndex++)
+ {
+ string defaultFile = _options.DefaultFileNames[matchIndex];
+ var file = _fileProvider.GetFileInfo(subpath + defaultFile);
+ // TryMatchPath will make sure subpath always ends with a "/" by adding it if needed.
+ if (file.Exists)
+ {
+ // If the path matches a directory but does not end in a slash, redirect to add the slash.
+ // This prevents relative links from breaking.
+ if (!Helpers.PathEndsInSlash(context.Request.Path))
+ {
+ context.Response.StatusCode = 301;
+ context.Response.Headers[HeaderNames.Location] = context.Request.PathBase + context.Request.Path + "/" + context.Request.QueryString;
+ return Task.CompletedTask;
+ }
+
+ // Match found, re-write the url. A later middleware will actually serve the file.
+ context.Request.Path = new PathString(context.Request.Path.Value + defaultFile);
+ break;
+ }
+ }
+ }
+ }
+
+ return _next(context);
+ }
+ }
+}
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/DefaultFilesOptions.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/DefaultFilesOptions.cs
new file mode 100644
index 0000000000..72b577dfcc
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/DefaultFilesOptions.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.Collections.Generic;
+using Microsoft.AspNetCore.StaticFiles.Infrastructure;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ ///
+ /// Options for selecting default file names.
+ ///
+ public class DefaultFilesOptions : SharedOptionsBase
+ {
+ ///
+ /// Configuration for the DefaultFilesMiddleware.
+ ///
+ public DefaultFilesOptions()
+ : this(new SharedOptions())
+ {
+ }
+
+ ///
+ /// Configuration for the DefaultFilesMiddleware.
+ ///
+ ///
+ public DefaultFilesOptions(SharedOptions sharedOptions)
+ : base(sharedOptions)
+ {
+ // Prioritized list
+ DefaultFileNames = new List()
+ {
+ "default.htm",
+ "default.html",
+ "index.htm",
+ "index.html",
+ };
+ }
+
+ ///
+ /// An ordered list of file names to select by default. List length and ordering may affect performance.
+ ///
+ public IList DefaultFileNames { get; set; }
+ }
+}
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/DirectoryBrowserExtensions.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/DirectoryBrowserExtensions.cs
new file mode 100644
index 0000000000..dce00489f1
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/DirectoryBrowserExtensions.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 Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.StaticFiles;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ ///
+ /// Extension methods for the DirectoryBrowserMiddleware
+ ///
+ public static class DirectoryBrowserExtensions
+ {
+ ///
+ /// Enable directory browsing on the current path
+ ///
+ ///
+ ///
+ public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+
+ return app.UseMiddleware();
+ }
+
+ ///
+ /// Enables directory browsing for the given request path
+ ///
+ ///
+ /// The relative request path.
+ ///
+ public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app, string requestPath)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+
+ return app.UseDirectoryBrowser(new DirectoryBrowserOptions
+ {
+ RequestPath = new PathString(requestPath)
+ });
+ }
+
+ ///
+ /// Enable directory browsing with the given options
+ ///
+ ///
+ ///
+ ///
+ public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app, DirectoryBrowserOptions options)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ return app.UseMiddleware(Options.Create(options));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/DirectoryBrowserMiddleware.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/DirectoryBrowserMiddleware.cs
new file mode 100644
index 0000000000..71765d0459
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/DirectoryBrowserMiddleware.cs
@@ -0,0 +1,109 @@
+// Copyright (c) .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.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Options;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.StaticFiles
+{
+ ///
+ /// Enables directory browsing
+ ///
+ public class DirectoryBrowserMiddleware
+ {
+ private readonly DirectoryBrowserOptions _options;
+ private readonly PathString _matchUrl;
+ private readonly RequestDelegate _next;
+ private readonly IDirectoryFormatter _formatter;
+ private readonly IFileProvider _fileProvider;
+
+ ///
+ /// Creates a new instance of the SendFileMiddleware. Using instance.
+ ///
+ /// The next middleware in the pipeline.
+ /// The used by this middleware.
+ /// The configuration for this middleware.
+ public DirectoryBrowserMiddleware(RequestDelegate next, IHostingEnvironment hostingEnv, IOptions options)
+ : this(next, hostingEnv, HtmlEncoder.Default, options)
+ {
+ }
+
+ ///
+ /// Creates a new instance of the SendFileMiddleware.
+ ///
+ /// The next middleware in the pipeline.
+ /// The used by this middleware.
+ /// The used by the default .
+ /// The configuration for this middleware.
+ public DirectoryBrowserMiddleware(RequestDelegate next, IHostingEnvironment hostingEnv, HtmlEncoder encoder, IOptions options)
+ {
+ if (next == null)
+ {
+ throw new ArgumentNullException(nameof(next));
+ }
+
+ if (hostingEnv == null)
+ {
+ throw new ArgumentNullException(nameof(hostingEnv));
+ }
+
+ if (encoder == null)
+ {
+ throw new ArgumentNullException(nameof(encoder));
+ }
+
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ _next = next;
+ _options = options.Value;
+ _fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);
+ _formatter = options.Value.Formatter ?? new HtmlDirectoryFormatter(encoder);
+ _matchUrl = _options.RequestPath;
+ }
+
+ ///
+ /// Examines the request to see if it matches a configured directory. If so, a view of the directory contents is returned.
+ ///
+ ///
+ ///
+ public Task Invoke(HttpContext context)
+ {
+ // Check if the URL matches any expected paths
+ PathString subpath;
+ IDirectoryContents contents;
+ if (Helpers.IsGetOrHeadMethod(context.Request.Method)
+ && Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out subpath)
+ && TryGetDirectoryInfo(subpath, out contents))
+ {
+ // If the path matches a directory but does not end in a slash, redirect to add the slash.
+ // This prevents relative links from breaking.
+ if (!Helpers.PathEndsInSlash(context.Request.Path))
+ {
+ context.Response.StatusCode = 301;
+ context.Response.Headers[HeaderNames.Location] = context.Request.PathBase + context.Request.Path + "/" + context.Request.QueryString;
+ return Task.CompletedTask;
+ }
+
+ return _formatter.GenerateContentAsync(context, contents);
+ }
+
+ return _next(context);
+ }
+
+ private bool TryGetDirectoryInfo(PathString subpath, out IDirectoryContents contents)
+ {
+ contents = _fileProvider.GetDirectoryContents(subpath.Value);
+ return contents.Exists;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/DirectoryBrowserOptions.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/DirectoryBrowserOptions.cs
new file mode 100644
index 0000000000..611df33e54
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/DirectoryBrowserOptions.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 Microsoft.AspNetCore.StaticFiles;
+using Microsoft.AspNetCore.StaticFiles.Infrastructure;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ ///
+ /// Directory browsing options
+ ///
+ public class DirectoryBrowserOptions : SharedOptionsBase
+ {
+ ///
+ /// Enabled directory browsing for all request paths
+ ///
+ public DirectoryBrowserOptions()
+ : this(new SharedOptions())
+ {
+ }
+
+ ///
+ /// Enabled directory browsing all request paths
+ ///
+ ///
+ public DirectoryBrowserOptions(SharedOptions sharedOptions)
+ : base(sharedOptions)
+ {
+ }
+
+ ///
+ /// The component that generates the view.
+ ///
+ public IDirectoryFormatter Formatter { get; set; }
+ }
+}
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/DirectoryBrowserServiceExtensions.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/DirectoryBrowserServiceExtensions.cs
new file mode 100644
index 0000000000..36d164c443
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/DirectoryBrowserServiceExtensions.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;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ ///
+ /// Extension methods for adding directory browser services.
+ ///
+ public static class DirectoryBrowserServiceExtensions
+ {
+ ///
+ /// Adds directory browser middleware services.
+ ///
+ /// The to add services to.
+ /// The so that additional calls can be chained.
+ public static IServiceCollection AddDirectoryBrowser(this IServiceCollection services)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ services.AddWebEncoders();
+
+ return services;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/FileExtensionContentTypeProvider.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/FileExtensionContentTypeProvider.cs
new file mode 100644
index 0000000000..f2e8f1b788
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/FileExtensionContentTypeProvider.cs
@@ -0,0 +1,459 @@
+// Copyright (c) .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.StaticFiles
+{
+ ///
+ /// Provides a mapping between file extensions and MIME types.
+ ///
+ public class FileExtensionContentTypeProvider : IContentTypeProvider
+ {
+ #region Extension mapping table
+ ///
+ /// Creates a new provider with a set of default mappings.
+ ///
+ public FileExtensionContentTypeProvider()
+ : this(new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ { ".323", "text/h323" },
+ { ".3g2", "video/3gpp2" },
+ { ".3gp2", "video/3gpp2" },
+ { ".3gp", "video/3gpp" },
+ { ".3gpp", "video/3gpp" },
+ { ".aac", "audio/aac" },
+ { ".aaf", "application/octet-stream" },
+ { ".aca", "application/octet-stream" },
+ { ".accdb", "application/msaccess" },
+ { ".accde", "application/msaccess" },
+ { ".accdt", "application/msaccess" },
+ { ".acx", "application/internet-property-stream" },
+ { ".adt", "audio/vnd.dlna.adts" },
+ { ".adts", "audio/vnd.dlna.adts" },
+ { ".afm", "application/octet-stream" },
+ { ".ai", "application/postscript" },
+ { ".aif", "audio/x-aiff" },
+ { ".aifc", "audio/aiff" },
+ { ".aiff", "audio/aiff" },
+ { ".appcache", "text/cache-manifest" },
+ { ".application", "application/x-ms-application" },
+ { ".art", "image/x-jg" },
+ { ".asd", "application/octet-stream" },
+ { ".asf", "video/x-ms-asf" },
+ { ".asi", "application/octet-stream" },
+ { ".asm", "text/plain" },
+ { ".asr", "video/x-ms-asf" },
+ { ".asx", "video/x-ms-asf" },
+ { ".atom", "application/atom+xml" },
+ { ".au", "audio/basic" },
+ { ".avi", "video/x-msvideo" },
+ { ".axs", "application/olescript" },
+ { ".bas", "text/plain" },
+ { ".bcpio", "application/x-bcpio" },
+ { ".bin", "application/octet-stream" },
+ { ".bmp", "image/bmp" },
+ { ".c", "text/plain" },
+ { ".cab", "application/vnd.ms-cab-compressed" },
+ { ".calx", "application/vnd.ms-office.calx" },
+ { ".cat", "application/vnd.ms-pki.seccat" },
+ { ".cdf", "application/x-cdf" },
+ { ".chm", "application/octet-stream" },
+ { ".class", "application/x-java-applet" },
+ { ".clp", "application/x-msclip" },
+ { ".cmx", "image/x-cmx" },
+ { ".cnf", "text/plain" },
+ { ".cod", "image/cis-cod" },
+ { ".cpio", "application/x-cpio" },
+ { ".cpp", "text/plain" },
+ { ".crd", "application/x-mscardfile" },
+ { ".crl", "application/pkix-crl" },
+ { ".crt", "application/x-x509-ca-cert" },
+ { ".csh", "application/x-csh" },
+ { ".css", "text/css" },
+ { ".csv", "application/octet-stream" },
+ { ".cur", "application/octet-stream" },
+ { ".dcr", "application/x-director" },
+ { ".deploy", "application/octet-stream" },
+ { ".der", "application/x-x509-ca-cert" },
+ { ".dib", "image/bmp" },
+ { ".dir", "application/x-director" },
+ { ".disco", "text/xml" },
+ { ".dlm", "text/dlm" },
+ { ".doc", "application/msword" },
+ { ".docm", "application/vnd.ms-word.document.macroEnabled.12" },
+ { ".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" },
+ { ".dot", "application/msword" },
+ { ".dotm", "application/vnd.ms-word.template.macroEnabled.12" },
+ { ".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template" },
+ { ".dsp", "application/octet-stream" },
+ { ".dtd", "text/xml" },
+ { ".dvi", "application/x-dvi" },
+ { ".dvr-ms", "video/x-ms-dvr" },
+ { ".dwf", "drawing/x-dwf" },
+ { ".dwp", "application/octet-stream" },
+ { ".dxr", "application/x-director" },
+ { ".eml", "message/rfc822" },
+ { ".emz", "application/octet-stream" },
+ { ".eot", "application/vnd.ms-fontobject" },
+ { ".eps", "application/postscript" },
+ { ".etx", "text/x-setext" },
+ { ".evy", "application/envoy" },
+ { ".fdf", "application/vnd.fdf" },
+ { ".fif", "application/fractals" },
+ { ".fla", "application/octet-stream" },
+ { ".flr", "x-world/x-vrml" },
+ { ".flv", "video/x-flv" },
+ { ".gif", "image/gif" },
+ { ".gtar", "application/x-gtar" },
+ { ".gz", "application/x-gzip" },
+ { ".h", "text/plain" },
+ { ".hdf", "application/x-hdf" },
+ { ".hdml", "text/x-hdml" },
+ { ".hhc", "application/x-oleobject" },
+ { ".hhk", "application/octet-stream" },
+ { ".hhp", "application/octet-stream" },
+ { ".hlp", "application/winhlp" },
+ { ".hqx", "application/mac-binhex40" },
+ { ".hta", "application/hta" },
+ { ".htc", "text/x-component" },
+ { ".htm", "text/html" },
+ { ".html", "text/html" },
+ { ".htt", "text/webviewhtml" },
+ { ".hxt", "text/html" },
+ { ".ical", "text/calendar" },
+ { ".icalendar", "text/calendar" },
+ { ".ico", "image/x-icon" },
+ { ".ics", "text/calendar" },
+ { ".ief", "image/ief" },
+ { ".ifb", "text/calendar" },
+ { ".iii", "application/x-iphone" },
+ { ".inf", "application/octet-stream" },
+ { ".ins", "application/x-internet-signup" },
+ { ".isp", "application/x-internet-signup" },
+ { ".IVF", "video/x-ivf" },
+ { ".jar", "application/java-archive" },
+ { ".java", "application/octet-stream" },
+ { ".jck", "application/liquidmotion" },
+ { ".jcz", "application/liquidmotion" },
+ { ".jfif", "image/pjpeg" },
+ { ".jpb", "application/octet-stream" },
+ { ".jpe", "image/jpeg" },
+ { ".jpeg", "image/jpeg" },
+ { ".jpg", "image/jpeg" },
+ { ".js", "application/javascript" },
+ { ".json", "application/json" },
+ { ".jsx", "text/jscript" },
+ { ".latex", "application/x-latex" },
+ { ".lit", "application/x-ms-reader" },
+ { ".lpk", "application/octet-stream" },
+ { ".lsf", "video/x-la-asf" },
+ { ".lsx", "video/x-la-asf" },
+ { ".lzh", "application/octet-stream" },
+ { ".m13", "application/x-msmediaview" },
+ { ".m14", "application/x-msmediaview" },
+ { ".m1v", "video/mpeg" },
+ { ".m2ts", "video/vnd.dlna.mpeg-tts" },
+ { ".m3u", "audio/x-mpegurl" },
+ { ".m4a", "audio/mp4" },
+ { ".m4v", "video/mp4" },
+ { ".man", "application/x-troff-man" },
+ { ".manifest", "application/x-ms-manifest" },
+ { ".map", "text/plain" },
+ { ".markdown", "text/markdown" },
+ { ".md", "text/markdown" },
+ { ".mdb", "application/x-msaccess" },
+ { ".mdp", "application/octet-stream" },
+ { ".me", "application/x-troff-me" },
+ { ".mht", "message/rfc822" },
+ { ".mhtml", "message/rfc822" },
+ { ".mid", "audio/mid" },
+ { ".midi", "audio/mid" },
+ { ".mix", "application/octet-stream" },
+ { ".mmf", "application/x-smaf" },
+ { ".mno", "text/xml" },
+ { ".mny", "application/x-msmoney" },
+ { ".mov", "video/quicktime" },
+ { ".movie", "video/x-sgi-movie" },
+ { ".mp2", "video/mpeg" },
+ { ".mp3", "audio/mpeg" },
+ { ".mp4", "video/mp4" },
+ { ".mp4v", "video/mp4" },
+ { ".mpa", "video/mpeg" },
+ { ".mpe", "video/mpeg" },
+ { ".mpeg", "video/mpeg" },
+ { ".mpg", "video/mpeg" },
+ { ".mpp", "application/vnd.ms-project" },
+ { ".mpv2", "video/mpeg" },
+ { ".ms", "application/x-troff-ms" },
+ { ".msi", "application/octet-stream" },
+ { ".mso", "application/octet-stream" },
+ { ".mvb", "application/x-msmediaview" },
+ { ".mvc", "application/x-miva-compiled" },
+ { ".nc", "application/x-netcdf" },
+ { ".nsc", "video/x-ms-asf" },
+ { ".nws", "message/rfc822" },
+ { ".ocx", "application/octet-stream" },
+ { ".oda", "application/oda" },
+ { ".odc", "text/x-ms-odc" },
+ { ".ods", "application/oleobject" },
+ { ".oga", "audio/ogg" },
+ { ".ogg", "video/ogg" },
+ { ".ogv", "video/ogg" },
+ { ".ogx", "application/ogg" },
+ { ".one", "application/onenote" },
+ { ".onea", "application/onenote" },
+ { ".onetoc", "application/onenote" },
+ { ".onetoc2", "application/onenote" },
+ { ".onetmp", "application/onenote" },
+ { ".onepkg", "application/onenote" },
+ { ".osdx", "application/opensearchdescription+xml" },
+ { ".otf", "font/otf" },
+ { ".p10", "application/pkcs10" },
+ { ".p12", "application/x-pkcs12" },
+ { ".p7b", "application/x-pkcs7-certificates" },
+ { ".p7c", "application/pkcs7-mime" },
+ { ".p7m", "application/pkcs7-mime" },
+ { ".p7r", "application/x-pkcs7-certreqresp" },
+ { ".p7s", "application/pkcs7-signature" },
+ { ".pbm", "image/x-portable-bitmap" },
+ { ".pcx", "application/octet-stream" },
+ { ".pcz", "application/octet-stream" },
+ { ".pdf", "application/pdf" },
+ { ".pfb", "application/octet-stream" },
+ { ".pfm", "application/octet-stream" },
+ { ".pfx", "application/x-pkcs12" },
+ { ".pgm", "image/x-portable-graymap" },
+ { ".pko", "application/vnd.ms-pki.pko" },
+ { ".pma", "application/x-perfmon" },
+ { ".pmc", "application/x-perfmon" },
+ { ".pml", "application/x-perfmon" },
+ { ".pmr", "application/x-perfmon" },
+ { ".pmw", "application/x-perfmon" },
+ { ".png", "image/png" },
+ { ".pnm", "image/x-portable-anymap" },
+ { ".pnz", "image/png" },
+ { ".pot", "application/vnd.ms-powerpoint" },
+ { ".potm", "application/vnd.ms-powerpoint.template.macroEnabled.12" },
+ { ".potx", "application/vnd.openxmlformats-officedocument.presentationml.template" },
+ { ".ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12" },
+ { ".ppm", "image/x-portable-pixmap" },
+ { ".pps", "application/vnd.ms-powerpoint" },
+ { ".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12" },
+ { ".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow" },
+ { ".ppt", "application/vnd.ms-powerpoint" },
+ { ".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12" },
+ { ".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" },
+ { ".prf", "application/pics-rules" },
+ { ".prm", "application/octet-stream" },
+ { ".prx", "application/octet-stream" },
+ { ".ps", "application/postscript" },
+ { ".psd", "application/octet-stream" },
+ { ".psm", "application/octet-stream" },
+ { ".psp", "application/octet-stream" },
+ { ".pub", "application/x-mspublisher" },
+ { ".qt", "video/quicktime" },
+ { ".qtl", "application/x-quicktimeplayer" },
+ { ".qxd", "application/octet-stream" },
+ { ".ra", "audio/x-pn-realaudio" },
+ { ".ram", "audio/x-pn-realaudio" },
+ { ".rar", "application/octet-stream" },
+ { ".ras", "image/x-cmu-raster" },
+ { ".rf", "image/vnd.rn-realflash" },
+ { ".rgb", "image/x-rgb" },
+ { ".rm", "application/vnd.rn-realmedia" },
+ { ".rmi", "audio/mid" },
+ { ".roff", "application/x-troff" },
+ { ".rpm", "audio/x-pn-realaudio-plugin" },
+ { ".rtf", "application/rtf" },
+ { ".rtx", "text/richtext" },
+ { ".scd", "application/x-msschedule" },
+ { ".sct", "text/scriptlet" },
+ { ".sea", "application/octet-stream" },
+ { ".setpay", "application/set-payment-initiation" },
+ { ".setreg", "application/set-registration-initiation" },
+ { ".sgml", "text/sgml" },
+ { ".sh", "application/x-sh" },
+ { ".shar", "application/x-shar" },
+ { ".sit", "application/x-stuffit" },
+ { ".sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12" },
+ { ".sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide" },
+ { ".smd", "audio/x-smd" },
+ { ".smi", "application/octet-stream" },
+ { ".smx", "audio/x-smd" },
+ { ".smz", "audio/x-smd" },
+ { ".snd", "audio/basic" },
+ { ".snp", "application/octet-stream" },
+ { ".spc", "application/x-pkcs7-certificates" },
+ { ".spl", "application/futuresplash" },
+ { ".spx", "audio/ogg" },
+ { ".src", "application/x-wais-source" },
+ { ".ssm", "application/streamingmedia" },
+ { ".sst", "application/vnd.ms-pki.certstore" },
+ { ".stl", "application/vnd.ms-pki.stl" },
+ { ".sv4cpio", "application/x-sv4cpio" },
+ { ".sv4crc", "application/x-sv4crc" },
+ { ".svg", "image/svg+xml" },
+ { ".svgz", "image/svg+xml" },
+ { ".swf", "application/x-shockwave-flash" },
+ { ".t", "application/x-troff" },
+ { ".tar", "application/x-tar" },
+ { ".tcl", "application/x-tcl" },
+ { ".tex", "application/x-tex" },
+ { ".texi", "application/x-texinfo" },
+ { ".texinfo", "application/x-texinfo" },
+ { ".tgz", "application/x-compressed" },
+ { ".thmx", "application/vnd.ms-officetheme" },
+ { ".thn", "application/octet-stream" },
+ { ".tif", "image/tiff" },
+ { ".tiff", "image/tiff" },
+ { ".toc", "application/octet-stream" },
+ { ".tr", "application/x-troff" },
+ { ".trm", "application/x-msterminal" },
+ { ".ts", "video/vnd.dlna.mpeg-tts" },
+ { ".tsv", "text/tab-separated-values" },
+ { ".ttc", "application/x-font-ttf" },
+ { ".ttf", "application/x-font-ttf" },
+ { ".tts", "video/vnd.dlna.mpeg-tts" },
+ { ".txt", "text/plain" },
+ { ".u32", "application/octet-stream" },
+ { ".uls", "text/iuls" },
+ { ".ustar", "application/x-ustar" },
+ { ".vbs", "text/vbscript" },
+ { ".vcf", "text/x-vcard" },
+ { ".vcs", "text/plain" },
+ { ".vdx", "application/vnd.ms-visio.viewer" },
+ { ".vml", "text/xml" },
+ { ".vsd", "application/vnd.visio" },
+ { ".vss", "application/vnd.visio" },
+ { ".vst", "application/vnd.visio" },
+ { ".vsto", "application/x-ms-vsto" },
+ { ".vsw", "application/vnd.visio" },
+ { ".vsx", "application/vnd.visio" },
+ { ".vtx", "application/vnd.visio" },
+ { ".wav", "audio/wav" },
+ { ".wax", "audio/x-ms-wax" },
+ { ".wbmp", "image/vnd.wap.wbmp" },
+ { ".wcm", "application/vnd.ms-works" },
+ { ".wdb", "application/vnd.ms-works" },
+ { ".webm", "video/webm" },
+ { ".webp", "image/webp" },
+ { ".wks", "application/vnd.ms-works" },
+ { ".wm", "video/x-ms-wm" },
+ { ".wma", "audio/x-ms-wma" },
+ { ".wmd", "application/x-ms-wmd" },
+ { ".wmf", "application/x-msmetafile" },
+ { ".wml", "text/vnd.wap.wml" },
+ { ".wmlc", "application/vnd.wap.wmlc" },
+ { ".wmls", "text/vnd.wap.wmlscript" },
+ { ".wmlsc", "application/vnd.wap.wmlscriptc" },
+ { ".wmp", "video/x-ms-wmp" },
+ { ".wmv", "video/x-ms-wmv" },
+ { ".wmx", "video/x-ms-wmx" },
+ { ".wmz", "application/x-ms-wmz" },
+ { ".woff", "application/font-woff" }, // https://www.w3.org/TR/WOFF/#appendix-b
+ { ".woff2", "font/woff2" }, // https://www.w3.org/TR/WOFF2/#IMT
+ { ".wps", "application/vnd.ms-works" },
+ { ".wri", "application/x-mswrite" },
+ { ".wrl", "x-world/x-vrml" },
+ { ".wrz", "x-world/x-vrml" },
+ { ".wsdl", "text/xml" },
+ { ".wtv", "video/x-ms-wtv" },
+ { ".wvx", "video/x-ms-wvx" },
+ { ".x", "application/directx" },
+ { ".xaf", "x-world/x-vrml" },
+ { ".xaml", "application/xaml+xml" },
+ { ".xap", "application/x-silverlight-app" },
+ { ".xbap", "application/x-ms-xbap" },
+ { ".xbm", "image/x-xbitmap" },
+ { ".xdr", "text/plain" },
+ { ".xht", "application/xhtml+xml" },
+ { ".xhtml", "application/xhtml+xml" },
+ { ".xla", "application/vnd.ms-excel" },
+ { ".xlam", "application/vnd.ms-excel.addin.macroEnabled.12" },
+ { ".xlc", "application/vnd.ms-excel" },
+ { ".xlm", "application/vnd.ms-excel" },
+ { ".xls", "application/vnd.ms-excel" },
+ { ".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12" },
+ { ".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12" },
+ { ".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" },
+ { ".xlt", "application/vnd.ms-excel" },
+ { ".xltm", "application/vnd.ms-excel.template.macroEnabled.12" },
+ { ".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template" },
+ { ".xlw", "application/vnd.ms-excel" },
+ { ".xml", "text/xml" },
+ { ".xof", "x-world/x-vrml" },
+ { ".xpm", "image/x-xpixmap" },
+ { ".xps", "application/vnd.ms-xpsdocument" },
+ { ".xsd", "text/xml" },
+ { ".xsf", "text/xml" },
+ { ".xsl", "text/xml" },
+ { ".xslt", "text/xml" },
+ { ".xsn", "application/octet-stream" },
+ { ".xtp", "application/octet-stream" },
+ { ".xwd", "image/x-xwindowdump" },
+ { ".z", "application/x-compress" },
+ { ".zip", "application/x-zip-compressed" },
+ })
+ {
+ }
+ #endregion
+
+ ///
+ /// Creates a lookup engine using the provided mapping.
+ /// It is recommended that the IDictionary instance use StringComparer.OrdinalIgnoreCase.
+ ///
+ ///
+ public FileExtensionContentTypeProvider(IDictionary mapping)
+ {
+ if (mapping == null)
+ {
+ throw new ArgumentNullException(nameof(mapping));
+ }
+ Mappings = mapping;
+ }
+
+ ///
+ /// The cross reference table of file extensions and content-types.
+ ///
+ public IDictionary Mappings { get; private set; }
+
+ ///
+ /// Given a file path, determine the MIME type
+ ///
+ /// A file path
+ /// The resulting MIME type
+ /// True if MIME type could be determined
+ public bool TryGetContentType(string subpath, out string contentType)
+ {
+ string extension = GetExtension(subpath);
+ if (extension == null)
+ {
+ contentType = null;
+ return false;
+ }
+ return Mappings.TryGetValue(extension, out contentType);
+ }
+
+ private static string GetExtension(string path)
+ {
+ // Don't use Path.GetExtension as that may throw an exception if there are
+ // invalid characters in the path. Invalid characters should be handled
+ // by the FileProviders
+
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ return null;
+ }
+
+ int index = path.LastIndexOf('.');
+ if (index < 0)
+ {
+ return null;
+ }
+
+ return path.Substring(index);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/FileServerExtensions.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/FileServerExtensions.cs
new file mode 100644
index 0000000000..c9eb06e4c3
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/FileServerExtensions.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 Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.StaticFiles;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ ///
+ /// Extension methods that combine all of the static file middleware components:
+ /// Default files, directory browsing, send file, and static files
+ ///
+ public static class FileServerExtensions
+ {
+ ///
+ /// Enable all static file middleware (except directory browsing) for the current request path in the current directory.
+ ///
+ ///
+ ///
+ public static IApplicationBuilder UseFileServer(this IApplicationBuilder app)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+
+ return app.UseFileServer(new FileServerOptions());
+ }
+
+ ///
+ /// Enable all static file middleware on for the current request path in the current directory.
+ ///
+ ///
+ /// Should directory browsing be enabled?
+ ///
+ public static IApplicationBuilder UseFileServer(this IApplicationBuilder app, bool enableDirectoryBrowsing)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+
+ return app.UseFileServer(new FileServerOptions
+ {
+ EnableDirectoryBrowsing = enableDirectoryBrowsing
+ });
+ }
+
+ ///
+ /// Enables all static file middleware (except directory browsing) for the given request path from the directory of the same name
+ ///
+ ///
+ /// The relative request path.
+ ///
+ public static IApplicationBuilder UseFileServer(this IApplicationBuilder app, string requestPath)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+
+ if (requestPath == null)
+ {
+ throw new ArgumentNullException(nameof(requestPath));
+ }
+
+ return app.UseFileServer(new FileServerOptions
+ {
+ RequestPath = new PathString(requestPath)
+ });
+ }
+
+ ///
+ /// Enable all static file middleware with the given options
+ ///
+ ///
+ ///
+ ///
+ public static IApplicationBuilder UseFileServer(this IApplicationBuilder app, FileServerOptions options)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ if (options.EnableDefaultFiles)
+ {
+ app.UseDefaultFiles(options.DefaultFilesOptions);
+ }
+
+ if (options.EnableDirectoryBrowsing)
+ {
+ app.UseDirectoryBrowser(options.DirectoryBrowserOptions);
+ }
+
+ return app.UseStaticFiles(options.StaticFileOptions);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/FileServerOptions.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/FileServerOptions.cs
new file mode 100644
index 0000000000..f46e274cc1
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/FileServerOptions.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 Microsoft.AspNetCore.StaticFiles.Infrastructure;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ ///
+ /// Options for all of the static file middleware components
+ ///
+ public class FileServerOptions : SharedOptionsBase
+ {
+ ///
+ /// Creates a combined options class for all of the static file middleware components.
+ ///
+ public FileServerOptions()
+ : base(new SharedOptions())
+ {
+ StaticFileOptions = new StaticFileOptions(SharedOptions);
+ DirectoryBrowserOptions = new DirectoryBrowserOptions(SharedOptions);
+ DefaultFilesOptions = new DefaultFilesOptions(SharedOptions);
+ EnableDefaultFiles = true;
+ }
+
+ ///
+ /// Options for configuring the StaticFileMiddleware.
+ ///
+ public StaticFileOptions StaticFileOptions { get; private set; }
+
+ ///
+ /// Options for configuring the DirectoryBrowserMiddleware.
+ ///
+ public DirectoryBrowserOptions DirectoryBrowserOptions { get; private set; }
+
+ ///
+ /// Options for configuring the DefaultFilesMiddleware.
+ ///
+ public DefaultFilesOptions DefaultFilesOptions { get; private set; }
+
+ ///
+ /// Directory browsing is disabled by default.
+ ///
+ public bool EnableDirectoryBrowsing { get; set; }
+
+ ///
+ /// Default files are enabled by default.
+ ///
+ public bool EnableDefaultFiles { get; set; }
+ }
+}
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Helpers.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Helpers.cs
new file mode 100644
index 0000000000..733377cef3
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Helpers.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 Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.FileProviders;
+
+namespace Microsoft.AspNetCore.StaticFiles
+{
+ internal static class Helpers
+ {
+ internal static IFileProvider ResolveFileProvider(IHostingEnvironment hostingEnv)
+ {
+ if (hostingEnv.WebRootFileProvider == null) {
+ throw new InvalidOperationException("Missing FileProvider.");
+ }
+ return hostingEnv.WebRootFileProvider;
+ }
+
+ internal static bool IsGetOrHeadMethod(string method)
+ {
+ return HttpMethods.IsGet(method) || HttpMethods.IsHead(method);
+ }
+
+ internal static bool PathEndsInSlash(PathString path)
+ {
+ return path.Value.EndsWith("/", StringComparison.Ordinal);
+ }
+
+ internal static bool TryMatchPath(HttpContext context, PathString matchUrl, bool forDirectory, out PathString subpath)
+ {
+ var path = context.Request.Path;
+
+ if (forDirectory && !PathEndsInSlash(path))
+ {
+ path += new PathString("/");
+ }
+
+ if (path.StartsWithSegments(matchUrl, out subpath))
+ {
+ return true;
+ }
+ return false;
+ }
+ }
+}
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/HtmlDirectoryFormatter.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/HtmlDirectoryFormatter.cs
new file mode 100644
index 0000000000..22ddcb99d2
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/HtmlDirectoryFormatter.cs
@@ -0,0 +1,172 @@
+// Copyright (c) .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.Globalization;
+using System.Linq;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.FileProviders;
+
+namespace Microsoft.AspNetCore.StaticFiles
+{
+ ///
+ /// Generates an HTML view for a directory.
+ ///
+ public class HtmlDirectoryFormatter : IDirectoryFormatter
+ {
+ private const string TextHtmlUtf8 = "text/html; charset=utf-8";
+
+ private HtmlEncoder _htmlEncoder;
+
+ public HtmlDirectoryFormatter(HtmlEncoder encoder)
+ {
+ if (encoder == null)
+ {
+ throw new ArgumentNullException(nameof(encoder));
+ }
+ _htmlEncoder = encoder;
+ }
+
+ ///
+ /// Generates an HTML view for a directory.
+ ///
+ public virtual Task GenerateContentAsync(HttpContext context, IEnumerable contents)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+ if (contents == null)
+ {
+ throw new ArgumentNullException(nameof(contents));
+ }
+
+ context.Response.ContentType = TextHtmlUtf8;
+
+ if (HttpMethods.IsHead(context.Request.Method))
+ {
+ // HEAD, no response body
+ return Task.CompletedTask;
+ }
+
+ PathString requestPath = context.Request.PathBase + context.Request.Path;
+
+ var builder = new StringBuilder();
+
+ builder.AppendFormat(
+@"
+", CultureInfo.CurrentUICulture.TwoLetterISOLanguageName);
+
+ builder.AppendFormat(@"
+
+ {0} {1}", HtmlEncode(Resources.HtmlDir_IndexOf), HtmlEncode(requestPath.Value));
+
+ builder.Append(@"
+
+
+
+ ");
+ builder.AppendFormat(@"
+ {0} /", HtmlEncode(Resources.HtmlDir_IndexOf));
+
+ string cumulativePath = "/";
+ foreach (var segment in requestPath.Value.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries))
+ {
+ cumulativePath = cumulativePath + segment + "/";
+ builder.AppendFormat(@"{1}/",
+ HtmlEncode(cumulativePath), HtmlEncode(segment));
+ }
+
+ builder.AppendFormat(CultureInfo.CurrentUICulture,
+ @"
+
+
+ | {1} | {2} | {4} |
+
+ ",
+ HtmlEncode(Resources.HtmlDir_TableSummary),
+ HtmlEncode(Resources.HtmlDir_Name),
+ HtmlEncode(Resources.HtmlDir_Size),
+ HtmlEncode(Resources.HtmlDir_Modified),
+ HtmlEncode(Resources.HtmlDir_LastModified));
+
+ foreach (var subdir in contents.Where(info => info.IsDirectory))
+ {
+ builder.AppendFormat(@"
+
+ | {0}/ |
+ |
+ {1} |
+
",
+ HtmlEncode(subdir.Name),
+ HtmlEncode(subdir.LastModified.ToString(CultureInfo.CurrentCulture)));
+ }
+
+ foreach (var file in contents.Where(info => !info.IsDirectory))
+ {
+ builder.AppendFormat(@"
+
+ | {0} |
+ {1} |
+ {2} |
+
",
+ HtmlEncode(file.Name),
+ HtmlEncode(file.Length.ToString("n0", CultureInfo.CurrentCulture)),
+ HtmlEncode(file.LastModified.ToString(CultureInfo.CurrentCulture)));
+ }
+
+ builder.Append(@"
+
+
+
+
+");
+ string data = builder.ToString();
+ byte[] bytes = Encoding.UTF8.GetBytes(data);
+ context.Response.ContentLength = bytes.Length;
+ return context.Response.Body.WriteAsync(bytes, 0, bytes.Length);
+ }
+
+ private string HtmlEncode(string body)
+ {
+ return _htmlEncoder.Encode(body);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/IContentTypeProvider.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/IContentTypeProvider.cs
new file mode 100644
index 0000000000..ba5065df03
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/IContentTypeProvider.cs
@@ -0,0 +1,19 @@
+// Copyright (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.StaticFiles
+{
+ ///
+ /// Used to look up MIME types given a file path
+ ///
+ public interface IContentTypeProvider
+ {
+ ///
+ /// Given a file path, determine the MIME type
+ ///
+ /// A file path
+ /// The resulting MIME type
+ /// True if MIME type could be determined
+ bool TryGetContentType(string subpath, out string contentType);
+ }
+}
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/IDirectoryFormatter.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/IDirectoryFormatter.cs
new file mode 100644
index 0000000000..6f379dea98
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/IDirectoryFormatter.cs
@@ -0,0 +1,23 @@
+// Copyright (c) .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.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.FileProviders;
+
+namespace Microsoft.AspNetCore.StaticFiles
+{
+ ///
+ /// Generates the view for a directory
+ ///
+ public interface IDirectoryFormatter
+ {
+ ///
+ /// Generates the view for a directory.
+ /// Implementers should properly handle HEAD requests.
+ /// Implementers should set all necessary response headers (e.g. Content-Type, Content-Length, etc.).
+ ///
+ Task GenerateContentAsync(HttpContext context, IEnumerable contents);
+ }
+}
\ No newline at end of file
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Infrastructure/SharedOptions.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Infrastructure/SharedOptions.cs
new file mode 100644
index 0000000000..1c1cc80ad5
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Infrastructure/SharedOptions.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;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.FileProviders;
+
+namespace Microsoft.AspNetCore.StaticFiles.Infrastructure
+{
+ ///
+ /// Options common to several middleware components
+ ///
+ public class SharedOptions
+ {
+ private PathString _requestPath;
+
+ ///
+ /// Defaults to all request paths.
+ ///
+ public SharedOptions()
+ {
+ RequestPath = PathString.Empty;
+ }
+
+ ///
+ /// The request path that maps to static resources
+ ///
+ public PathString RequestPath
+ {
+ get { return _requestPath; }
+ set
+ {
+ if (value.HasValue && value.Value.EndsWith("/", StringComparison.Ordinal))
+ {
+ throw new ArgumentException("Request path must not end in a slash");
+ }
+ _requestPath = value;
+ }
+ }
+
+ ///
+ /// The file system used to locate resources
+ ///
+ public IFileProvider FileProvider { get; set; }
+ }
+}
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Infrastructure/SharedOptionsBase.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Infrastructure/SharedOptionsBase.cs
new file mode 100644
index 0000000000..16900ec6fb
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Infrastructure/SharedOptionsBase.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 Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.FileProviders;
+
+namespace Microsoft.AspNetCore.StaticFiles.Infrastructure
+{
+ ///
+ /// Options common to several middleware components
+ ///
+ public abstract class SharedOptionsBase
+ {
+ ///
+ /// Creates an new instance of the SharedOptionsBase.
+ ///
+ ///
+ protected SharedOptionsBase(SharedOptions sharedOptions)
+ {
+ if (sharedOptions == null)
+ {
+ throw new ArgumentNullException(nameof(sharedOptions));
+ }
+
+ SharedOptions = sharedOptions;
+ }
+
+ ///
+ /// Options common to several middleware components
+ ///
+ protected SharedOptions SharedOptions { get; private set; }
+
+ ///
+ /// The relative request path that maps to static resources.
+ ///
+ public PathString RequestPath
+ {
+ get { return SharedOptions.RequestPath; }
+ set { SharedOptions.RequestPath = value; }
+ }
+
+ ///
+ /// The file system used to locate resources
+ ///
+ public IFileProvider FileProvider
+ {
+ get { return SharedOptions.FileProvider; }
+ set { SharedOptions.FileProvider = value; }
+ }
+ }
+}
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/LoggerExtensions.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/LoggerExtensions.cs
new file mode 100644
index 0000000000..7fa6e35083
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/LoggerExtensions.cs
@@ -0,0 +1,159 @@
+// Copyright (c) .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.Extensions.Logging;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.StaticFiles
+{
+ ///
+ /// Defines *all* the logger messages produced by static files
+ ///
+ internal static class LoggerExtensions
+ {
+ private static Action _logMethodNotSupported;
+ private static Action _logFileServed;
+ private static Action _logPathMismatch;
+ private static Action _logFileTypeNotSupported;
+ private static Action _logFileNotFound;
+ private static Action _logPathNotModified;
+ private static Action _logPreconditionFailed;
+ private static Action _logHandled;
+ private static Action _logRangeNotSatisfiable;
+ private static Action _logSendingFileRange;
+ private static Action _logCopyingFileRange;
+ private static Action _logCopyingBytesToResponse;
+ private static Action _logWriteCancelled;
+
+ static LoggerExtensions()
+ {
+ _logMethodNotSupported = LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: 1,
+ formatString: "{Method} requests are not supported");
+ _logFileServed = LoggerMessage.Define(
+ logLevel: LogLevel.Information,
+ eventId: 2,
+ formatString: "Sending file. Request path: '{VirtualPath}'. Physical path: '{PhysicalPath}'");
+ _logPathMismatch = LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: 3,
+ formatString: "The request path {Path} does not match the path filter");
+ _logFileTypeNotSupported = LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: 4,
+ formatString: "The request path {Path} does not match a supported file type");
+ _logFileNotFound = LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: 5,
+ formatString: "The request path {Path} does not match an existing file");
+ _logPathNotModified = LoggerMessage.Define(
+ logLevel: LogLevel.Information,
+ eventId: 6,
+ formatString: "The file {Path} was not modified");
+ _logPreconditionFailed = LoggerMessage.Define(
+ logLevel: LogLevel.Information,
+ eventId: 7,
+ formatString: "Precondition for {Path} failed");
+ _logHandled = LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: 8,
+ formatString: "Handled. Status code: {StatusCode} File: {Path}");
+ _logRangeNotSatisfiable = LoggerMessage.Define(
+ logLevel: LogLevel.Warning,
+ eventId: 9,
+ formatString: "Range not satisfiable for {Path}");
+ _logSendingFileRange = LoggerMessage.Define(
+ logLevel: LogLevel.Information,
+ eventId: 10,
+ formatString: "Sending {Range} of file {Path}");
+ _logCopyingFileRange = LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: 11,
+ formatString: "Copying {Range} of file {Path} to the response body");
+ _logCopyingBytesToResponse = LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: 12,
+ formatString: "Copying bytes {Start}-{End} of file {Path} to response body");
+ _logWriteCancelled = LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: 14,
+ formatString: "The file transmission was cancelled");
+ }
+
+ public static void LogRequestMethodNotSupported(this ILogger logger, string method)
+ {
+ _logMethodNotSupported(logger, method, null);
+ }
+
+ public static void LogFileServed(this ILogger logger, string virtualPath, string physicalPath)
+ {
+ if (string.IsNullOrEmpty(physicalPath))
+ {
+ physicalPath = "N/A";
+ }
+ _logFileServed(logger, virtualPath, physicalPath, null);
+ }
+
+ public static void LogPathMismatch(this ILogger logger, string path)
+ {
+ _logPathMismatch(logger, path, null);
+ }
+
+ public static void LogFileTypeNotSupported(this ILogger logger, string path)
+ {
+ _logFileTypeNotSupported(logger, path, null);
+ }
+
+ public static void LogFileNotFound(this ILogger logger, string path)
+ {
+ _logFileNotFound(logger, path, null);
+ }
+
+ public static void LogPathNotModified(this ILogger logger, string path)
+ {
+ _logPathNotModified(logger, path, null);
+ }
+
+ public static void LogPreconditionFailed(this ILogger logger, string path)
+ {
+ _logPreconditionFailed(logger, path, null);
+ }
+
+ public static void LogHandled(this ILogger logger, int statusCode, string path)
+ {
+ _logHandled(logger, statusCode, path, null);
+ }
+
+ public static void LogRangeNotSatisfiable(this ILogger logger, string path)
+ {
+ _logRangeNotSatisfiable(logger, path, null);
+ }
+
+ public static void LogSendingFileRange(this ILogger logger, StringValues range, string path)
+ {
+ _logSendingFileRange(logger, range, path, null);
+ }
+
+ public static void LogCopyingFileRange(this ILogger logger, StringValues range, string path)
+ {
+ _logCopyingFileRange(logger, range, path, null);
+ }
+
+ public static void LogCopyingBytesToResponse(this ILogger logger, long start, long? end, string path)
+ {
+ _logCopyingBytesToResponse(
+ logger,
+ start,
+ end != null ? end.ToString() : "*",
+ path,
+ null);
+ }
+
+ public static void LogWriteCancelled(this ILogger logger, Exception ex)
+ {
+ _logWriteCancelled(logger, ex);
+ }
+ }
+}
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Microsoft.AspNetCore.StaticFiles.csproj b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Microsoft.AspNetCore.StaticFiles.csproj
new file mode 100644
index 0000000000..46d506ff58
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Microsoft.AspNetCore.StaticFiles.csproj
@@ -0,0 +1,23 @@
+
+
+
+ ASP.NET Core static files middleware. Includes middleware for serving static files, directory browsing, and default files.
+ netstandard2.0
+ $(NoWarn);CS1591
+ true
+ aspnetcore;staticfiles
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Properties/AssemblyInfo.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..3af9731ac7
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.StaticFiles.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Resources.Designer.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Resources.Designer.cs
new file mode 100644
index 0000000000..850389c5b1
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Resources.Designer.cs
@@ -0,0 +1,142 @@
+//
+namespace Microsoft.AspNetCore.StaticFiles
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNetCore.StaticFiles.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ ///
+ /// No formatter provided.
+ ///
+ internal static string Args_NoFormatter
+ {
+ get { return GetString("Args_NoFormatter"); }
+ }
+
+ ///
+ /// No formatter provided.
+ ///
+ internal static string FormatArgs_NoFormatter()
+ {
+ return GetString("Args_NoFormatter");
+ }
+
+ ///
+ /// Index of
+ ///
+ internal static string HtmlDir_IndexOf
+ {
+ get { return GetString("HtmlDir_IndexOf"); }
+ }
+
+ ///
+ /// Index of
+ ///
+ internal static string FormatHtmlDir_IndexOf()
+ {
+ return GetString("HtmlDir_IndexOf");
+ }
+
+ ///
+ /// Last Modified
+ ///
+ internal static string HtmlDir_LastModified
+ {
+ get { return GetString("HtmlDir_LastModified"); }
+ }
+
+ ///
+ /// Last Modified
+ ///
+ internal static string FormatHtmlDir_LastModified()
+ {
+ return GetString("HtmlDir_LastModified");
+ }
+
+ ///
+ /// Modified
+ ///
+ internal static string HtmlDir_Modified
+ {
+ get { return GetString("HtmlDir_Modified"); }
+ }
+
+ ///
+ /// Modified
+ ///
+ internal static string FormatHtmlDir_Modified()
+ {
+ return GetString("HtmlDir_Modified");
+ }
+
+ ///
+ /// Name
+ ///
+ internal static string HtmlDir_Name
+ {
+ get { return GetString("HtmlDir_Name"); }
+ }
+
+ ///
+ /// Name
+ ///
+ internal static string FormatHtmlDir_Name()
+ {
+ return GetString("HtmlDir_Name");
+ }
+
+ ///
+ /// Size
+ ///
+ internal static string HtmlDir_Size
+ {
+ get { return GetString("HtmlDir_Size"); }
+ }
+
+ ///
+ /// Size
+ ///
+ internal static string FormatHtmlDir_Size()
+ {
+ return GetString("HtmlDir_Size");
+ }
+
+ ///
+ /// The list of files in the given directory. Column headers are listed in the first row.
+ ///
+ internal static string HtmlDir_TableSummary
+ {
+ get { return GetString("HtmlDir_TableSummary"); }
+ }
+
+ ///
+ /// The list of files in the given directory. Column headers are listed in the first row.
+ ///
+ internal static string FormatHtmlDir_TableSummary()
+ {
+ return GetString("HtmlDir_TableSummary");
+ }
+
+ 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/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Resources.resx b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Resources.resx
new file mode 100644
index 0000000000..73d3ecda10
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Resources.resx
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+ No formatter provided.
+
+
+ Index of
+
+
+ Last Modified
+
+
+ Modified
+
+
+ Name
+
+
+ Size
+
+
+ The list of files in the given directory. Column headers are listed in the first row.
+
+
\ No newline at end of file
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/StaticFileContext.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/StaticFileContext.cs
new file mode 100644
index 0000000000..f5024dcb23
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/StaticFileContext.cs
@@ -0,0 +1,405 @@
+// Copyright (c) .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.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Http.Headers;
+using Microsoft.AspNetCore.Internal;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.StaticFiles
+{
+ internal struct StaticFileContext
+ {
+ private const int StreamCopyBufferSize = 64 * 1024;
+ private readonly HttpContext _context;
+ private readonly StaticFileOptions _options;
+ private readonly PathString _matchUrl;
+ private readonly HttpRequest _request;
+ private readonly HttpResponse _response;
+ private readonly ILogger _logger;
+ private readonly IFileProvider _fileProvider;
+ private readonly IContentTypeProvider _contentTypeProvider;
+ private string _method;
+ private bool _isGet;
+ private bool _isHead;
+ private PathString _subPath;
+ private string _contentType;
+ private IFileInfo _fileInfo;
+ private long _length;
+ private DateTimeOffset _lastModified;
+ private EntityTagHeaderValue _etag;
+
+ private RequestHeaders _requestHeaders;
+ private ResponseHeaders _responseHeaders;
+
+ private PreconditionState _ifMatchState;
+ private PreconditionState _ifNoneMatchState;
+ private PreconditionState _ifModifiedSinceState;
+ private PreconditionState _ifUnmodifiedSinceState;
+
+ private RangeItemHeaderValue _range;
+ private bool _isRangeRequest;
+
+ public StaticFileContext(HttpContext context, StaticFileOptions options, PathString matchUrl, ILogger logger, IFileProvider fileProvider, IContentTypeProvider contentTypeProvider)
+ {
+ _context = context;
+ _options = options;
+ _matchUrl = matchUrl;
+ _request = context.Request;
+ _response = context.Response;
+ _logger = logger;
+ _requestHeaders = _request.GetTypedHeaders();
+ _responseHeaders = _response.GetTypedHeaders();
+ _fileProvider = fileProvider;
+ _contentTypeProvider = contentTypeProvider;
+
+ _method = null;
+ _isGet = false;
+ _isHead = false;
+ _subPath = PathString.Empty;
+ _contentType = null;
+ _fileInfo = null;
+ _length = 0;
+ _lastModified = new DateTimeOffset();
+ _etag = null;
+ _ifMatchState = PreconditionState.Unspecified;
+ _ifNoneMatchState = PreconditionState.Unspecified;
+ _ifModifiedSinceState = PreconditionState.Unspecified;
+ _ifUnmodifiedSinceState = PreconditionState.Unspecified;
+ _range = null;
+ _isRangeRequest = false;
+ }
+
+ internal enum PreconditionState
+ {
+ Unspecified,
+ NotModified,
+ ShouldProcess,
+ PreconditionFailed
+ }
+
+ public bool IsHeadMethod
+ {
+ get { return _isHead; }
+ }
+
+ public bool IsRangeRequest
+ {
+ get { return _isRangeRequest; }
+ }
+
+ public string SubPath
+ {
+ get { return _subPath.Value; }
+ }
+
+ public string PhysicalPath
+ {
+ get { return _fileInfo?.PhysicalPath; }
+ }
+
+ public bool ValidateMethod()
+ {
+ _method = _request.Method;
+ _isGet = HttpMethods.IsGet(_method);
+ _isHead = HttpMethods.IsHead(_method);
+ return _isGet || _isHead;
+ }
+
+ // Check if the URL matches any expected paths
+ public bool ValidatePath()
+ {
+ return Helpers.TryMatchPath(_context, _matchUrl, forDirectory: false, subpath: out _subPath);
+ }
+
+ public bool LookupContentType()
+ {
+ if (_contentTypeProvider.TryGetContentType(_subPath.Value, out _contentType))
+ {
+ return true;
+ }
+
+ if (_options.ServeUnknownFileTypes)
+ {
+ _contentType = _options.DefaultContentType;
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool LookupFileInfo()
+ {
+ _fileInfo = _fileProvider.GetFileInfo(_subPath.Value);
+ if (_fileInfo.Exists)
+ {
+ _length = _fileInfo.Length;
+
+ DateTimeOffset last = _fileInfo.LastModified;
+ // Truncate to the second.
+ _lastModified = new DateTimeOffset(last.Year, last.Month, last.Day, last.Hour, last.Minute, last.Second, last.Offset).ToUniversalTime();
+
+ long etagHash = _lastModified.ToFileTime() ^ _length;
+ _etag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
+ }
+ return _fileInfo.Exists;
+ }
+
+ public void ComprehendRequestHeaders()
+ {
+ ComputeIfMatch();
+
+ ComputeIfModifiedSince();
+
+ ComputeRange();
+
+ ComputeIfRange();
+ }
+
+ private void ComputeIfMatch()
+ {
+ // 14.24 If-Match
+ var ifMatch = _requestHeaders.IfMatch;
+ if (ifMatch != null && ifMatch.Any())
+ {
+ _ifMatchState = PreconditionState.PreconditionFailed;
+ foreach (var etag in ifMatch)
+ {
+ if (etag.Equals(EntityTagHeaderValue.Any) || etag.Compare(_etag, useStrongComparison: true))
+ {
+ _ifMatchState = PreconditionState.ShouldProcess;
+ break;
+ }
+ }
+ }
+
+ // 14.26 If-None-Match
+ var ifNoneMatch = _requestHeaders.IfNoneMatch;
+ if (ifNoneMatch != null && ifNoneMatch.Any())
+ {
+ _ifNoneMatchState = PreconditionState.ShouldProcess;
+ foreach (var etag in ifNoneMatch)
+ {
+ if (etag.Equals(EntityTagHeaderValue.Any) || etag.Compare(_etag, useStrongComparison: true))
+ {
+ _ifNoneMatchState = PreconditionState.NotModified;
+ break;
+ }
+ }
+ }
+ }
+
+ private void ComputeIfModifiedSince()
+ {
+ var now = DateTimeOffset.UtcNow;
+
+ // 14.25 If-Modified-Since
+ var ifModifiedSince = _requestHeaders.IfModifiedSince;
+ if (ifModifiedSince.HasValue && ifModifiedSince <= now)
+ {
+ bool modified = ifModifiedSince < _lastModified;
+ _ifModifiedSinceState = modified ? PreconditionState.ShouldProcess : PreconditionState.NotModified;
+ }
+
+ // 14.28 If-Unmodified-Since
+ var ifUnmodifiedSince = _requestHeaders.IfUnmodifiedSince;
+ if (ifUnmodifiedSince.HasValue && ifUnmodifiedSince <= now)
+ {
+ bool unmodified = ifUnmodifiedSince >= _lastModified;
+ _ifUnmodifiedSinceState = unmodified ? PreconditionState.ShouldProcess : PreconditionState.PreconditionFailed;
+ }
+ }
+
+ private void ComputeIfRange()
+ {
+ // 14.27 If-Range
+ var ifRangeHeader = _requestHeaders.IfRange;
+ if (ifRangeHeader != null)
+ {
+ // If the validator given in the If-Range header field matches the
+ // current validator for the selected representation of the target
+ // resource, then the server SHOULD process the Range header field as
+ // requested. If the validator does not match, the server MUST ignore
+ // the Range header field.
+ if (ifRangeHeader.LastModified.HasValue)
+ {
+ if (_lastModified !=null && _lastModified > ifRangeHeader.LastModified)
+ {
+ _isRangeRequest = false;
+ }
+ }
+ else if (_etag != null && ifRangeHeader.EntityTag != null && !ifRangeHeader.EntityTag.Compare(_etag, useStrongComparison: true))
+ {
+ _isRangeRequest = false;
+ }
+ }
+ }
+
+ private void ComputeRange()
+ {
+ // 14.35 Range
+ // http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-24
+
+ // A server MUST ignore a Range header field received with a request method other
+ // than GET.
+ if (!_isGet)
+ {
+ return;
+ }
+
+ (_isRangeRequest, _range) = RangeHelper.ParseRange(_context, _requestHeaders, _length, _logger);
+ }
+
+ public void ApplyResponseHeaders(int statusCode)
+ {
+ _response.StatusCode = statusCode;
+ if (statusCode < 400)
+ {
+ // these headers are returned for 200, 206, and 304
+ // they are not returned for 412 and 416
+ if (!string.IsNullOrEmpty(_contentType))
+ {
+ _response.ContentType = _contentType;
+ }
+ _responseHeaders.LastModified = _lastModified;
+ _responseHeaders.ETag = _etag;
+ _responseHeaders.Headers[HeaderNames.AcceptRanges] = "bytes";
+ }
+ if (statusCode == Constants.Status200Ok)
+ {
+ // this header is only returned here for 200
+ // it already set to the returned range for 206
+ // it is not returned for 304, 412, and 416
+ _response.ContentLength = _length;
+ }
+ _options.OnPrepareResponse(new StaticFileResponseContext()
+ {
+ Context = _context,
+ File = _fileInfo,
+ });
+ }
+
+ public PreconditionState GetPreconditionState()
+ {
+ return GetMaxPreconditionState(_ifMatchState, _ifNoneMatchState,
+ _ifModifiedSinceState, _ifUnmodifiedSinceState);
+ }
+
+ private static PreconditionState GetMaxPreconditionState(params PreconditionState[] states)
+ {
+ PreconditionState max = PreconditionState.Unspecified;
+ for (int i = 0; i < states.Length; i++)
+ {
+ if (states[i] > max)
+ {
+ max = states[i];
+ }
+ }
+ return max;
+ }
+
+ public Task SendStatusAsync(int statusCode)
+ {
+ ApplyResponseHeaders(statusCode);
+
+ _logger.LogHandled(statusCode, SubPath);
+ return Task.CompletedTask;
+ }
+
+ public async Task SendAsync()
+ {
+ ApplyResponseHeaders(Constants.Status200Ok);
+ string physicalPath = _fileInfo.PhysicalPath;
+ var sendFile = _context.Features.Get();
+ if (sendFile != null && !string.IsNullOrEmpty(physicalPath))
+ {
+ // We don't need to directly cancel this, if the client disconnects it will fail silently.
+ await sendFile.SendFileAsync(physicalPath, 0, _length, CancellationToken.None);
+ return;
+ }
+
+ try
+ {
+ using (var readStream = _fileInfo.CreateReadStream())
+ {
+ // Larger StreamCopyBufferSize is required because in case of FileStream readStream isn't going to be buffering
+ await StreamCopyOperation.CopyToAsync(readStream, _response.Body, _length, StreamCopyBufferSize, _context.RequestAborted);
+ }
+ }
+ catch (OperationCanceledException ex)
+ {
+ _logger.LogWriteCancelled(ex);
+ // Don't throw this exception, it's most likely caused by the client disconnecting.
+ // However, if it was cancelled for any other reason we need to prevent empty responses.
+ _context.Abort();
+ }
+ }
+
+ // When there is only a single range the bytes are sent directly in the body.
+ internal async Task SendRangeAsync()
+ {
+ if (_range == null)
+ {
+ // 14.16 Content-Range - A server sending a response with status code 416 (Requested range not satisfiable)
+ // SHOULD include a Content-Range field with a byte-range-resp-spec of "*". The instance-length specifies
+ // the current length of the selected resource. e.g. */length
+ _responseHeaders.ContentRange = new ContentRangeHeaderValue(_length);
+ ApplyResponseHeaders(Constants.Status416RangeNotSatisfiable);
+
+ _logger.LogRangeNotSatisfiable(SubPath);
+ return;
+ }
+
+ long start, length;
+ _responseHeaders.ContentRange = ComputeContentRange(_range, out start, out length);
+ _response.ContentLength = length;
+ ApplyResponseHeaders(Constants.Status206PartialContent);
+
+ string physicalPath = _fileInfo.PhysicalPath;
+ var sendFile = _context.Features.Get();
+ if (sendFile != null && !string.IsNullOrEmpty(physicalPath))
+ {
+ _logger.LogSendingFileRange(_response.Headers[HeaderNames.ContentRange], physicalPath);
+ // We don't need to directly cancel this, if the client disconnects it will fail silently.
+ await sendFile.SendFileAsync(physicalPath, start, length, CancellationToken.None);
+ return;
+ }
+
+ try
+ {
+ using (var readStream = _fileInfo.CreateReadStream())
+ {
+ readStream.Seek(start, SeekOrigin.Begin); // TODO: What if !CanSeek?
+ _logger.LogCopyingFileRange(_response.Headers[HeaderNames.ContentRange], SubPath);
+ await StreamCopyOperation.CopyToAsync(readStream, _response.Body, length, _context.RequestAborted);
+ }
+ }
+ catch (OperationCanceledException ex)
+ {
+ _logger.LogWriteCancelled(ex);
+ // Don't throw this exception, it's most likely caused by the client disconnecting.
+ // However, if it was cancelled for any other reason we need to prevent empty responses.
+ _context.Abort();
+ }
+ }
+
+ // Note: This assumes ranges have been normalized to absolute byte offsets.
+ private ContentRangeHeaderValue ComputeContentRange(RangeItemHeaderValue range, out long start, out long length)
+ {
+ start = range.From.Value;
+ long end = range.To.Value;
+ length = end - start + 1;
+ return new ContentRangeHeaderValue(start, end, _length);
+ }
+ }
+}
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/StaticFileExtensions.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/StaticFileExtensions.cs
new file mode 100644
index 0000000000..1f9270a432
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/StaticFileExtensions.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 Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.StaticFiles;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ ///
+ /// Extension methods for the StaticFileMiddleware
+ ///
+ public static class StaticFileExtensions
+ {
+ ///
+ /// Enables static file serving for the current request path
+ ///
+ ///
+ ///
+ public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+
+ return app.UseMiddleware();
+ }
+
+ ///
+ /// Enables static file serving for the given request path
+ ///
+ ///
+ /// The relative request path.
+ ///
+ public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app, string requestPath)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+
+ return app.UseStaticFiles(new StaticFileOptions
+ {
+ RequestPath = new PathString(requestPath)
+ });
+ }
+
+ ///
+ /// Enables static file serving with the given options
+ ///
+ ///
+ ///
+ ///
+ public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app, StaticFileOptions options)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ return app.UseMiddleware(Options.Create(options));
+ }
+ }
+}
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/StaticFileMiddleware.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/StaticFileMiddleware.cs
new file mode 100644
index 0000000000..46594fc35d
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/StaticFileMiddleware.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.Diagnostics;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.StaticFiles
+{
+ ///
+ /// Enables serving static files for a given request path
+ ///
+ public class StaticFileMiddleware
+ {
+ private readonly StaticFileOptions _options;
+ private readonly PathString _matchUrl;
+ private readonly RequestDelegate _next;
+ private readonly ILogger _logger;
+ private readonly IFileProvider _fileProvider;
+ private readonly IContentTypeProvider _contentTypeProvider;
+
+ ///
+ /// Creates a new instance of the StaticFileMiddleware.
+ ///
+ /// The next middleware in the pipeline.
+ /// The used by this middleware.
+ /// The configuration options.
+ /// An instance used to create loggers.
+ public StaticFileMiddleware(RequestDelegate next, IHostingEnvironment hostingEnv, IOptions options, ILoggerFactory loggerFactory)
+ {
+ if (next == null)
+ {
+ throw new ArgumentNullException(nameof(next));
+ }
+
+ if (hostingEnv == null)
+ {
+ throw new ArgumentNullException(nameof(hostingEnv));
+ }
+
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ if (loggerFactory == null)
+ {
+ throw new ArgumentNullException(nameof(loggerFactory));
+ }
+
+ _next = next;
+ _options = options.Value;
+ _contentTypeProvider = options.Value.ContentTypeProvider ?? new FileExtensionContentTypeProvider();
+ _fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);
+ _matchUrl = _options.RequestPath;
+ _logger = loggerFactory.CreateLogger();
+ }
+
+ ///
+ /// Processes a request to determine if it matches a known file, and if so, serves it.
+ ///
+ ///
+ ///
+ public async Task Invoke(HttpContext context)
+ {
+ var fileContext = new StaticFileContext(context, _options, _matchUrl, _logger, _fileProvider, _contentTypeProvider);
+
+ if (!fileContext.ValidateMethod())
+ {
+ _logger.LogRequestMethodNotSupported(context.Request.Method);
+ }
+ else if (!fileContext.ValidatePath())
+ {
+ _logger.LogPathMismatch(fileContext.SubPath);
+ }
+ else if (!fileContext.LookupContentType())
+ {
+ _logger.LogFileTypeNotSupported(fileContext.SubPath);
+ }
+ else if (!fileContext.LookupFileInfo())
+ {
+ _logger.LogFileNotFound(fileContext.SubPath);
+ }
+ else
+ {
+ // If we get here, we can try to serve the file
+ fileContext.ComprehendRequestHeaders();
+ switch (fileContext.GetPreconditionState())
+ {
+ case StaticFileContext.PreconditionState.Unspecified:
+ case StaticFileContext.PreconditionState.ShouldProcess:
+ if (fileContext.IsHeadMethod)
+ {
+ await fileContext.SendStatusAsync(Constants.Status200Ok);
+ return;
+ }
+
+ try
+ {
+ if (fileContext.IsRangeRequest)
+ {
+ await fileContext.SendRangeAsync();
+ return;
+ }
+
+ await fileContext.SendAsync();
+ _logger.LogFileServed(fileContext.SubPath, fileContext.PhysicalPath);
+ return;
+ }
+ catch (FileNotFoundException)
+ {
+ context.Response.Clear();
+ }
+ break;
+ case StaticFileContext.PreconditionState.NotModified:
+ _logger.LogPathNotModified(fileContext.SubPath);
+ await fileContext.SendStatusAsync(Constants.Status304NotModified);
+ return;
+
+ case StaticFileContext.PreconditionState.PreconditionFailed:
+ _logger.LogPreconditionFailed(fileContext.SubPath);
+ await fileContext.SendStatusAsync(Constants.Status412PreconditionFailed);
+ return;
+
+ default:
+ var exception = new NotImplementedException(fileContext.GetPreconditionState().ToString());
+ Debug.Fail(exception.ToString());
+ throw exception;
+ }
+ }
+
+ await _next(context);
+ }
+ }
+}
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/StaticFileOptions.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/StaticFileOptions.cs
new file mode 100644
index 0000000000..01cef16b68
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/StaticFileOptions.cs
@@ -0,0 +1,55 @@
+// Copyright (c) .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.StaticFiles;
+using Microsoft.AspNetCore.StaticFiles.Infrastructure;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ ///
+ /// Options for serving static files
+ ///
+ public class StaticFileOptions : SharedOptionsBase
+ {
+ ///
+ /// Defaults to all request paths
+ ///
+ public StaticFileOptions() : this(new SharedOptions())
+ {
+ }
+
+ ///
+ /// Defaults to all request paths
+ ///
+ ///
+ public StaticFileOptions(SharedOptions sharedOptions) : base(sharedOptions)
+ {
+ OnPrepareResponse = _ => { };
+ }
+
+ ///
+ /// Used to map files to content-types.
+ ///
+ public IContentTypeProvider ContentTypeProvider { get; set; }
+
+ ///
+ /// The default content type for a request if the ContentTypeProvider cannot determine one.
+ /// None is provided by default, so the client must determine the format themselves.
+ /// http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7
+ ///
+ public string DefaultContentType { get; set; }
+
+ ///
+ /// If the file is not a recognized content-type should it be served?
+ /// Default: false.
+ ///
+ public bool ServeUnknownFileTypes { get; set; }
+
+ ///
+ /// Called after the status code and headers have been set, but before the body has been written.
+ /// This can be used to add or change the response headers.
+ ///
+ public Action OnPrepareResponse { get; set; }
+ }
+}
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/StaticFileResponseContext.cs b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/StaticFileResponseContext.cs
new file mode 100644
index 0000000000..72b25c8259
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/StaticFileResponseContext.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 Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.FileProviders;
+
+namespace Microsoft.AspNetCore.StaticFiles
+{
+ ///
+ /// Contains information about the request and the file that will be served in response.
+ ///
+ public class StaticFileResponseContext
+ {
+ ///
+ /// The request and response information.
+ ///
+ public HttpContext Context { get; internal set; }
+
+ ///
+ /// The file to be served.
+ ///
+ public IFileInfo File { get; internal set; }
+ }
+}
diff --git a/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/baseline.netcore.json b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/baseline.netcore.json
new file mode 100644
index 0000000000..489e7f3a81
--- /dev/null
+++ b/src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/baseline.netcore.json
@@ -0,0 +1,1077 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.StaticFiles, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.Extensions.DependencyInjection.DirectoryBrowserServiceExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AddDirectoryBrowser",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.DefaultFilesExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "UseDefaultFiles",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseDefaultFiles",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "requestPath",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseDefaultFiles",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Builder.DefaultFilesOptions"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.DefaultFilesOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.StaticFiles.Infrastructure.SharedOptionsBase",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_DefaultFileNames",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IList",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_DefaultFileNames",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Collections.Generic.IList"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "sharedOptions",
+ "Type": "Microsoft.AspNetCore.StaticFiles.Infrastructure.SharedOptions"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.DirectoryBrowserExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "UseDirectoryBrowser",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseDirectoryBrowser",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "requestPath",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseDirectoryBrowser",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Builder.DirectoryBrowserOptions"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.DirectoryBrowserOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.StaticFiles.Infrastructure.SharedOptionsBase",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Formatter",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.StaticFiles.IDirectoryFormatter",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Formatter",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.StaticFiles.IDirectoryFormatter"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "sharedOptions",
+ "Type": "Microsoft.AspNetCore.StaticFiles.Infrastructure.SharedOptions"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.FileServerExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "UseFileServer",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseFileServer",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "enableDirectoryBrowsing",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseFileServer",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "requestPath",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseFileServer",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Builder.FileServerOptions"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.FileServerOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.StaticFiles.Infrastructure.SharedOptionsBase",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_StaticFileOptions",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Builder.StaticFileOptions",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_DirectoryBrowserOptions",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Builder.DirectoryBrowserOptions",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_DefaultFilesOptions",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Builder.DefaultFilesOptions",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_EnableDirectoryBrowsing",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_EnableDirectoryBrowsing",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_EnableDefaultFiles",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_EnableDefaultFiles",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.StaticFileExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "UseStaticFiles",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseStaticFiles",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "requestPath",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseStaticFiles",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Builder.StaticFileOptions"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.StaticFileOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.StaticFiles.Infrastructure.SharedOptionsBase",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ContentTypeProvider",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.StaticFiles.IContentTypeProvider",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ContentTypeProvider",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.StaticFiles.IContentTypeProvider"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_DefaultContentType",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_DefaultContentType",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ServeUnknownFileTypes",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ServeUnknownFileTypes",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnPrepareResponse",
+ "Parameters": [],
+ "ReturnType": "System.Action",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnPrepareResponse",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Action"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "sharedOptions",
+ "Type": "Microsoft.AspNetCore.StaticFiles.Infrastructure.SharedOptions"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.StaticFiles.DefaultFilesMiddleware",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Invoke",
+ "Parameters": [
+ {
+ "Name": "context",
+ "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": "hostingEnv",
+ "Type": "Microsoft.AspNetCore.Hosting.IHostingEnvironment"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptions"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.StaticFiles.DirectoryBrowserMiddleware",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Invoke",
+ "Parameters": [
+ {
+ "Name": "context",
+ "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": "hostingEnv",
+ "Type": "Microsoft.AspNetCore.Hosting.IHostingEnvironment"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptions"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "next",
+ "Type": "Microsoft.AspNetCore.Http.RequestDelegate"
+ },
+ {
+ "Name": "hostingEnv",
+ "Type": "Microsoft.AspNetCore.Hosting.IHostingEnvironment"
+ },
+ {
+ "Name": "encoder",
+ "Type": "System.Text.Encodings.Web.HtmlEncoder"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptions"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.StaticFiles.FileExtensionContentTypeProvider",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.StaticFiles.IContentTypeProvider"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Mappings",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IDictionary",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "TryGetContentType",
+ "Parameters": [
+ {
+ "Name": "subpath",
+ "Type": "System.String"
+ },
+ {
+ "Name": "contentType",
+ "Type": "System.String",
+ "Direction": "Out"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.StaticFiles.IContentTypeProvider",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "mapping",
+ "Type": "System.Collections.Generic.IDictionary"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.StaticFiles.HtmlDirectoryFormatter",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.StaticFiles.IDirectoryFormatter"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "GenerateContentAsync",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "contents",
+ "Type": "System.Collections.Generic.IEnumerable"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.StaticFiles.IDirectoryFormatter",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "encoder",
+ "Type": "System.Text.Encodings.Web.HtmlEncoder"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.StaticFiles.IContentTypeProvider",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "TryGetContentType",
+ "Parameters": [
+ {
+ "Name": "subpath",
+ "Type": "System.String"
+ },
+ {
+ "Name": "contentType",
+ "Type": "System.String",
+ "Direction": "Out"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.StaticFiles.IDirectoryFormatter",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "GenerateContentAsync",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "contents",
+ "Type": "System.Collections.Generic.IEnumerable"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Invoke",
+ "Parameters": [
+ {
+ "Name": "context",
+ "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": "hostingEnv",
+ "Type": "Microsoft.AspNetCore.Hosting.IHostingEnvironment"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptions"
+ },
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.StaticFiles.StaticFileResponseContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Context",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.HttpContext",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_File",
+ "Parameters": [],
+ "ReturnType": "Microsoft.Extensions.FileProviders.IFileInfo",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.StaticFiles.Infrastructure.SharedOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_RequestPath",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.PathString",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_RequestPath",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.PathString"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_FileProvider",
+ "Parameters": [],
+ "ReturnType": "Microsoft.Extensions.FileProviders.IFileProvider",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_FileProvider",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.Extensions.FileProviders.IFileProvider"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.StaticFiles.Infrastructure.SharedOptionsBase",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_SharedOptions",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.StaticFiles.Infrastructure.SharedOptions",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_RequestPath",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.PathString",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_RequestPath",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.PathString"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_FileProvider",
+ "Parameters": [],
+ "ReturnType": "Microsoft.Extensions.FileProviders.IFileProvider",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_FileProvider",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.Extensions.FileProviders.IFileProvider"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "sharedOptions",
+ "Type": "Microsoft.AspNetCore.StaticFiles.Infrastructure.SharedOptions"
+ }
+ ],
+ "Visibility": "Protected",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/StaticFiles/test/Directory.Build.props b/src/StaticFiles/test/Directory.Build.props
new file mode 100644
index 0000000000..c6e5ecaf30
--- /dev/null
+++ b/src/StaticFiles/test/Directory.Build.props
@@ -0,0 +1,19 @@
+
+
+
+
+ netcoreapp2.1
+ $(DeveloperBuildTestTfms)
+ netcoreapp2.1;netcoreapp2.0
+ $(StandardTestTfms);net461
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/StaticFiles/test/Microsoft.AspNetCore.RangeHelper.Sources.Test/Microsoft.AspNetCore.RangeHelper.Sources.Test.csproj b/src/StaticFiles/test/Microsoft.AspNetCore.RangeHelper.Sources.Test/Microsoft.AspNetCore.RangeHelper.Sources.Test.csproj
new file mode 100644
index 0000000000..e75e27a9bb
--- /dev/null
+++ b/src/StaticFiles/test/Microsoft.AspNetCore.RangeHelper.Sources.Test/Microsoft.AspNetCore.RangeHelper.Sources.Test.csproj
@@ -0,0 +1,17 @@
+
+
+
+ $(StandardTestTfms)
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/StaticFiles/test/Microsoft.AspNetCore.RangeHelper.Sources.Test/RangeHelperTests.cs b/src/StaticFiles/test/Microsoft.AspNetCore.RangeHelper.Sources.Test/RangeHelperTests.cs
new file mode 100644
index 0000000000..a6e06810a5
--- /dev/null
+++ b/src/StaticFiles/test/Microsoft.AspNetCore.RangeHelper.Sources.Test/RangeHelperTests.cs
@@ -0,0 +1,110 @@
+// Copyright (c) .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.Extensions.Logging.Abstractions;
+using Microsoft.Net.Http.Headers;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Internal
+{
+ public class RangeHelperTests
+ {
+ [Theory]
+ [InlineData(1, 2)]
+ [InlineData(2, 3)]
+ public void NormalizeRange_ReturnsNullWhenRangeStartEqualsOrGreaterThanLength(long start, long end)
+ {
+ // Arrange & Act
+ var normalizedRange = RangeHelper.NormalizeRange(new RangeItemHeaderValue(start, end), 1);
+
+ // Assert
+ Assert.Null(normalizedRange);
+ }
+
+ [Fact]
+ public void NormalizeRange_ReturnsNullWhenRangeEndEqualsZero()
+ {
+ // Arrange & Act
+ var normalizedRange = RangeHelper.NormalizeRange(new RangeItemHeaderValue(null, 0), 1);
+
+ // Assert
+ Assert.Null(normalizedRange);
+ }
+
+ [Theory]
+ [InlineData(0, null, 0, 2)]
+ [InlineData(0, 0, 0, 0)]
+ public void NormalizeRange_ReturnsNormalizedRange(long? start, long? end, long? normalizedStart, long? normalizedEnd)
+ {
+ // Arrange & Act
+ var normalizedRange = RangeHelper.NormalizeRange(new RangeItemHeaderValue(start, end), 3);
+
+ // Assert
+ Assert.Equal(normalizedStart, normalizedRange.From);
+ Assert.Equal(normalizedEnd, normalizedRange.To);
+ }
+
+ [Fact]
+ public void NormalizeRange_ReturnsRangeWithNoChange()
+ {
+ // Arrange & Act
+ var normalizedRange = RangeHelper.NormalizeRange(new RangeItemHeaderValue(1, 3), 4);
+
+ // Assert
+ Assert.Equal(1, normalizedRange.From);
+ Assert.Equal(3, normalizedRange.To);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ public void ParseRange_ReturnsNullWhenRangeHeaderNotProvided(string range)
+ {
+ // Arrange
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.Headers[HeaderNames.Range] = range;
+
+ // Act
+ var (isRangeRequest, parsedRangeResult) = RangeHelper.ParseRange(httpContext, httpContext.Request.GetTypedHeaders(), 10, NullLogger.Instance);
+
+ // Assert
+ Assert.False(isRangeRequest);
+ Assert.Null(parsedRangeResult);
+ }
+
+ [Theory]
+ [InlineData("1-2, 3-4")]
+ [InlineData("1-2, ")]
+ public void ParseRange_ReturnsNullWhenMultipleRangesProvidedInRangeHeader(string range)
+ {
+ // Arrange
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.Headers[HeaderNames.Range] = range;
+
+ // Act
+ var (isRangeRequest, parsedRangeResult) = RangeHelper.ParseRange(httpContext, httpContext.Request.GetTypedHeaders(), 10, NullLogger.Instance);
+
+ // Assert
+ Assert.False(isRangeRequest);
+ Assert.Null(parsedRangeResult);
+ }
+
+ [Fact]
+ public void ParseRange_ReturnsSingleRangeWhenInputValid()
+ {
+ // Arrange
+ var httpContext = new DefaultHttpContext();
+ var range = new RangeHeaderValue(1, 2);
+ httpContext.Request.Headers[HeaderNames.Range] = range.ToString();
+
+ // Act
+ var (isRangeRequest, parsedRange) = RangeHelper.ParseRange(httpContext, httpContext.Request.GetTypedHeaders(), 4, NullLogger.Instance);
+
+ // Assert
+ Assert.True(isRangeRequest);
+ Assert.Equal(1, parsedRange.From);
+ Assert.Equal(2, parsedRange.To);
+ }
+ }
+}
diff --git a/src/StaticFiles/test/Microsoft.AspNetCore.StaticFiles.FunctionalTests/Microsoft.AspNetCore.StaticFiles.FunctionalTests.csproj b/src/StaticFiles/test/Microsoft.AspNetCore.StaticFiles.FunctionalTests/Microsoft.AspNetCore.StaticFiles.FunctionalTests.csproj
new file mode 100644
index 0000000000..69f71eecca
--- /dev/null
+++ b/src/StaticFiles/test/Microsoft.AspNetCore.StaticFiles.FunctionalTests/Microsoft.AspNetCore.StaticFiles.FunctionalTests.csproj
@@ -0,0 +1,35 @@
+
+
+
+ $(StandardTestTfms)
+
+
+ true
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+ PreserveNewest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/StaticFiles/test/Microsoft.AspNetCore.StaticFiles.FunctionalTests/StaticFileMiddlewareTests.cs b/src/StaticFiles/test/Microsoft.AspNetCore.StaticFiles.FunctionalTests/StaticFileMiddlewareTests.cs
new file mode 100644
index 0000000000..b2ba4a4f47
--- /dev/null
+++ b/src/StaticFiles/test/Microsoft.AspNetCore.StaticFiles.FunctionalTests/StaticFileMiddlewareTests.cs
@@ -0,0 +1,247 @@
+// Copyright (c) .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.Net.Http;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Server.IntegrationTesting;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.AspNetCore.StaticFiles
+{
+ public class StaticFileMiddlewareTests
+ {
+ [Fact]
+ public async Task ReturnsNotFoundWithoutWwwroot()
+ {
+ var baseAddress = "http://localhost:12345";
+ var builder = new WebHostBuilder()
+ .UseKestrel()
+ .Configure(app => app.UseStaticFiles());
+
+ using (var server = builder.Start(baseAddress))
+ {
+ using (var client = new HttpClient() { BaseAddress = new Uri(baseAddress) })
+ {
+ var response = await client.GetAsync("TestDocument.txt");
+
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task FoundFile_LastModifiedTrimsSeconds()
+ {
+ var baseAddress = "http://localhost:12345";
+ var builder = new WebHostBuilder()
+ .UseKestrel()
+ .UseWebRoot(AppContext.BaseDirectory)
+ .Configure(app => app.UseStaticFiles());
+
+ using (var server = builder.Start(baseAddress))
+ {
+ using (var client = new HttpClient() { BaseAddress = new Uri(baseAddress) })
+ {
+ var last = File.GetLastWriteTimeUtc(Path.Combine(AppContext.BaseDirectory, "TestDocument.txt"));
+ var response = await client.GetAsync("TestDocument.txt");
+
+ var trimed = new DateTimeOffset(last.Year, last.Month, last.Day, last.Hour, last.Minute, last.Second, TimeSpan.Zero).ToUniversalTime();
+
+ Assert.Equal(response.Content.Headers.LastModified.Value, trimed);
+ }
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(ExistingFiles))]
+ public async Task FoundFile_Served_All(string baseUrl, string baseDir, string requestUrl)
+ {
+ await FoundFile_Served(baseUrl, baseDir, requestUrl);
+ }
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Linux)]
+ [OSSkipCondition(OperatingSystems.MacOSX)]
+ [InlineData("", @".", "/testDocument.Txt")]
+ [InlineData("/somedir", @".", "/somedir/Testdocument.TXT")]
+ [InlineData("/SomeDir", @".", "/soMediR/testdocument.txT")]
+ [InlineData("/somedir", @"SubFolder", "/somedir/Ranges.tXt")]
+ public async Task FoundFile_Served_Windows(string baseUrl, string baseDir, string requestUrl)
+ {
+ await FoundFile_Served(baseUrl, baseDir, requestUrl);
+ }
+
+ private async Task FoundFile_Served(string baseUrl, string baseDir, string requestUrl)
+ {
+ var baseAddress = "http://localhost:12345";
+ var builder = new WebHostBuilder()
+ .UseKestrel()
+ .UseWebRoot(Path.Combine(AppContext.BaseDirectory, baseDir))
+ .Configure(app => app.UseStaticFiles(new StaticFileOptions()
+ {
+ RequestPath = new PathString(baseUrl),
+ }));
+
+ using (var server = builder.Start(baseAddress))
+ {
+ var hostingEnvironment = server.Services.GetService();
+
+ using (var client = new HttpClient() { BaseAddress = new Uri(baseAddress) })
+ {
+ var fileInfo = hostingEnvironment.WebRootFileProvider.GetFileInfo(Path.GetFileName(requestUrl));
+ var response = await client.GetAsync(requestUrl);
+ var responseContent = await response.Content.ReadAsByteArrayAsync();
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
+ Assert.True(response.Content.Headers.ContentLength == fileInfo.Length);
+ Assert.Equal(response.Content.Headers.ContentLength, responseContent.Length);
+
+ using (var stream = fileInfo.CreateReadStream())
+ {
+ var fileContents = new byte[stream.Length];
+ stream.Read(fileContents, 0, (int)stream.Length);
+ Assert.True(responseContent.SequenceEqual(fileContents));
+ }
+ }
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(ExistingFiles))]
+ public async Task HeadFile_HeadersButNotBodyServed(string baseUrl, string baseDir, string requestUrl)
+ {
+ var baseAddress = "http://localhost:12345";
+ var builder = new WebHostBuilder()
+ .UseKestrel()
+ .UseWebRoot(Path.Combine(AppContext.BaseDirectory, baseDir))
+ .Configure(app => app.UseStaticFiles(new StaticFileOptions()
+ {
+ RequestPath = new PathString(baseUrl),
+ }));
+
+ using (var server = builder.Start(baseAddress))
+ {
+ var hostingEnvironment = server.Services.GetService();
+
+ using (var client = new HttpClient() { BaseAddress = new Uri(baseAddress) })
+ {
+ var fileInfo = hostingEnvironment.WebRootFileProvider.GetFileInfo(Path.GetFileName(requestUrl));
+ var request = new HttpRequestMessage(HttpMethod.Head, requestUrl);
+ var response = await client.SendAsync(request);
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
+ Assert.True(response.Content.Headers.ContentLength == fileInfo.Length);
+ Assert.Empty((await response.Content.ReadAsByteArrayAsync()));
+ }
+ }
+ }
+
+ public static IEnumerable