Merge remote-tracking branch 'StaticFiles/rybrande/release21ToSrc' into rybrande/Mondo2.1
This commit is contained in:
commit
93238389a7
|
|
@ -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
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<Project>
|
||||
<Import
|
||||
Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), AspNetCoreSettings.props))\AspNetCoreSettings.props"
|
||||
Condition=" '$(CI)' != 'true' AND '$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), AspNetCoreSettings.props))' != '' " />
|
||||
|
||||
<Import Project="version.props" />
|
||||
<Import Project="build\dependencies.props" />
|
||||
<Import Project="build\sources.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<Product>Microsoft ASP.NET Core</Product>
|
||||
<RepositoryUrl>https://github.com/aspnet/StaticFiles</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryRoot>$(MSBuildThisFileDirectory)</RepositoryRoot>
|
||||
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)build\Key.snk</AssemblyOriginatorKeyFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<PublicSign Condition="'$(OS)' != 'Windows_NT'">true</PublicSign>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<Project>
|
||||
<PropertyGroup>
|
||||
<RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.0' ">$(MicrosoftNETCoreApp20PackageVersion)</RuntimeFrameworkVersion>
|
||||
<RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.1' ">$(MicrosoftNETCoreApp21PackageVersion)</RuntimeFrameworkVersion>
|
||||
<NETStandardImplicitPackageVersion Condition=" '$(TargetFramework)' == 'netstandard2.0' ">$(NETStandardLibrary20PackageVersion)</NETStandardImplicitPackageVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"adx-nonshipping": {
|
||||
"rules": [],
|
||||
"packages": {
|
||||
"Microsoft.AspNetCore.RangeHelper.Sources": {}
|
||||
}
|
||||
},
|
||||
"Default": {
|
||||
"rules": [
|
||||
"DefaultCompositeRule"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
Binary file not shown.
|
|
@ -0,0 +1,39 @@
|
|||
<Project>
|
||||
<PropertyGroup>
|
||||
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- These package versions may be overridden or updated by automation. -->
|
||||
<PropertyGroup Label="Package Versions: Auto">
|
||||
<InternalAspNetCoreSdkPackageVersion>2.1.3-rtm-15802</InternalAspNetCoreSdkPackageVersion>
|
||||
<MicrosoftNETCoreApp20PackageVersion>2.0.0</MicrosoftNETCoreApp20PackageVersion>
|
||||
<MicrosoftNETCoreApp21PackageVersion>2.1.2</MicrosoftNETCoreApp21PackageVersion>
|
||||
<MicrosoftNETTestSdkPackageVersion>15.6.1</MicrosoftNETTestSdkPackageVersion>
|
||||
<MoqPackageVersion>4.7.49</MoqPackageVersion>
|
||||
<NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
|
||||
<XunitAnalyzersPackageVersion>0.8.0</XunitAnalyzersPackageVersion>
|
||||
<XunitPackageVersion>2.3.1</XunitPackageVersion>
|
||||
<XunitRunnerVisualStudioPackageVersion>2.4.0-beta.1.build3945</XunitRunnerVisualStudioPackageVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- This may import a generated file which may override the variables above. -->
|
||||
<Import Project="$(DotNetPackageVersionPropsPath)" Condition=" '$(DotNetPackageVersionPropsPath)' != '' " />
|
||||
|
||||
<!-- These are package versions that should not be overridden or updated by automation. -->
|
||||
<PropertyGroup Label="Package Versions: Pinned">
|
||||
<MicrosoftAspNetCoreHostingAbstractionsPackageVersion>2.1.1</MicrosoftAspNetCoreHostingAbstractionsPackageVersion>
|
||||
<MicrosoftAspNetCoreHttpExtensionsPackageVersion>2.1.1</MicrosoftAspNetCoreHttpExtensionsPackageVersion>
|
||||
<MicrosoftAspNetCoreHttpPackageVersion>2.1.1</MicrosoftAspNetCoreHttpPackageVersion>
|
||||
<MicrosoftAspNetCoreServerHttpSysPackageVersion>2.1.1</MicrosoftAspNetCoreServerHttpSysPackageVersion>
|
||||
<MicrosoftAspNetCoreServerIISIntegrationPackageVersion>2.1.1</MicrosoftAspNetCoreServerIISIntegrationPackageVersion>
|
||||
<MicrosoftAspNetCoreServerIntegrationTestingPackageVersion>0.5.1</MicrosoftAspNetCoreServerIntegrationTestingPackageVersion>
|
||||
<MicrosoftAspNetCoreServerKestrelPackageVersion>2.1.2</MicrosoftAspNetCoreServerKestrelPackageVersion>
|
||||
<MicrosoftAspNetCoreTestHostPackageVersion>2.1.1</MicrosoftAspNetCoreTestHostPackageVersion>
|
||||
<MicrosoftAspNetCoreTestingPackageVersion>2.1.0</MicrosoftAspNetCoreTestingPackageVersion>
|
||||
<MicrosoftExtensionsFileProvidersAbstractionsPackageVersion>2.1.1</MicrosoftExtensionsFileProvidersAbstractionsPackageVersion>
|
||||
<MicrosoftExtensionsLoggingAbstractionsPackageVersion>2.1.1</MicrosoftExtensionsLoggingAbstractionsPackageVersion>
|
||||
<MicrosoftExtensionsLoggingConsolePackageVersion>2.1.1</MicrosoftExtensionsLoggingConsolePackageVersion>
|
||||
<MicrosoftExtensionsLoggingTestingPackageVersion>2.1.1</MicrosoftExtensionsLoggingTestingPackageVersion>
|
||||
<MicrosoftExtensionsWebEncodersPackageVersion>2.1.1</MicrosoftExtensionsWebEncodersPackageVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<Project>
|
||||
<Import Project="dependencies.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- These properties are use by the automation that updates dependencies.props -->
|
||||
<LineupPackageId>Internal.AspNetCore.Universe.Lineup</LineupPackageId>
|
||||
<LineupPackageVersion>2.1.0-rc1-*</LineupPackageVersion>
|
||||
<LineupPackageRestoreSource>https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json</LineupPackageRestoreSource>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<DotNetCoreRuntime Include="$(MicrosoftNETCoreApp20PackageVersion)" />
|
||||
<DotNetCoreRuntime Include="$(MicrosoftNETCoreApp21PackageVersion)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<Project>
|
||||
<Import Project="$(DotNetRestoreSourcePropsPath)" Condition="'$(DotNetRestoreSourcePropsPath)' != ''"/>
|
||||
|
||||
<PropertyGroup Label="RestoreSources">
|
||||
<RestoreSources>$(DotNetRestoreSources)</RestoreSources>
|
||||
<RestoreSources Condition="'$(DotNetBuildOffline)' != 'true' AND '$(AspNetUniverseBuildOffline)' != 'true' ">
|
||||
$(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>
|
||||
<RestoreSources Condition="'$(DotNetBuildOffline)' != 'true'">
|
||||
$(RestoreSources);
|
||||
https://api.nuget.org/v3/index.json;
|
||||
</RestoreSources>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Startup>()
|
||||
.Build();
|
||||
|
||||
host.Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.StaticFiles\Microsoft.AspNetCore.StaticFiles.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
A static HTML file.
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a parser for the Range Header in an <see cref="HttpContext.Request"/>.
|
||||
/// </summary>
|
||||
internal static class RangeHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the normalized form of the requested range if the Range Header in the <see cref="HttpContext.Request"/> is valid.
|
||||
/// </summary>
|
||||
/// <param name="context">The <see cref="HttpContext"/> associated with the request.</param>
|
||||
/// <param name="requestHeaders">The <see cref="RequestHeaders"/> associated with the given <paramref name="context"/>.</param>
|
||||
/// <param name="length">The total length of the file representation requested.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/>.</param>
|
||||
/// <returns>A boolean value which represents if the <paramref name="requestHeaders"/> contain a single valid
|
||||
/// range request. A <see cref="RangeItemHeaderValue"/> which represents the normalized form of the
|
||||
/// range parsed from the <paramref name="requestHeaders"/> or <c>null</c> if it cannot be normalized.</returns>
|
||||
/// <remark>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 (<c>true</c>,<c>null</c>) return values.</remark>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<Project>
|
||||
<Import Project="..\Directory.Build.props" />
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Internal.AspNetCore.Sdk" PrivateAssets="All" Version="$(InternalAspNetCoreSdkPackageVersion)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Dictionary>
|
||||
<Words>
|
||||
<Recognized>
|
||||
<Word>Owin</Word>
|
||||
</Recognized>
|
||||
</Words>
|
||||
</Dictionary>
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for the DefaultFilesMiddleware
|
||||
/// </summary>
|
||||
public static class DefaultFilesExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enables default file mapping on the current path
|
||||
/// </summary>
|
||||
/// <param name="app"></param>
|
||||
/// <returns></returns>
|
||||
public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app)
|
||||
{
|
||||
if (app == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(app));
|
||||
}
|
||||
|
||||
return app.UseMiddleware<DefaultFilesMiddleware>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables default file mapping for the given request path
|
||||
/// </summary>
|
||||
/// <param name="app"></param>
|
||||
/// <param name="requestPath">The relative request path.</param>
|
||||
/// <returns></returns>
|
||||
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)
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables default file mapping with the given options
|
||||
/// </summary>
|
||||
/// <param name="app"></param>
|
||||
/// <param name="options"></param>
|
||||
/// <returns></returns>
|
||||
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<DefaultFilesMiddleware>(Options.Create(options));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class DefaultFilesMiddleware
|
||||
{
|
||||
private readonly DefaultFilesOptions _options;
|
||||
private readonly PathString _matchUrl;
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly IFileProvider _fileProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the DefaultFilesMiddleware.
|
||||
/// </summary>
|
||||
/// <param name="next">The next middleware in the pipeline.</param>
|
||||
/// <param name="hostingEnv">The <see cref="IHostingEnvironment"/> used by this middleware.</param>
|
||||
/// <param name="options">The configuration options for this middleware.</param>
|
||||
public DefaultFilesMiddleware(RequestDelegate next, IHostingEnvironment hostingEnv, IOptions<DefaultFilesOptions> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for selecting default file names.
|
||||
/// </summary>
|
||||
public class DefaultFilesOptions : SharedOptionsBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration for the DefaultFilesMiddleware.
|
||||
/// </summary>
|
||||
public DefaultFilesOptions()
|
||||
: this(new SharedOptions())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the DefaultFilesMiddleware.
|
||||
/// </summary>
|
||||
/// <param name="sharedOptions"></param>
|
||||
public DefaultFilesOptions(SharedOptions sharedOptions)
|
||||
: base(sharedOptions)
|
||||
{
|
||||
// Prioritized list
|
||||
DefaultFileNames = new List<string>()
|
||||
{
|
||||
"default.htm",
|
||||
"default.html",
|
||||
"index.htm",
|
||||
"index.html",
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An ordered list of file names to select by default. List length and ordering may affect performance.
|
||||
/// </summary>
|
||||
public IList<string> DefaultFileNames { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for the DirectoryBrowserMiddleware
|
||||
/// </summary>
|
||||
public static class DirectoryBrowserExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enable directory browsing on the current path
|
||||
/// </summary>
|
||||
/// <param name="app"></param>
|
||||
/// <returns></returns>
|
||||
public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app)
|
||||
{
|
||||
if (app == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(app));
|
||||
}
|
||||
|
||||
return app.UseMiddleware<DirectoryBrowserMiddleware>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables directory browsing for the given request path
|
||||
/// </summary>
|
||||
/// <param name="app"></param>
|
||||
/// <param name="requestPath">The relative request path.</param>
|
||||
/// <returns></returns>
|
||||
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)
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enable directory browsing with the given options
|
||||
/// </summary>
|
||||
/// <param name="app"></param>
|
||||
/// <param name="options"></param>
|
||||
/// <returns></returns>
|
||||
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<DirectoryBrowserMiddleware>(Options.Create(options));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Enables directory browsing
|
||||
/// </summary>
|
||||
public class DirectoryBrowserMiddleware
|
||||
{
|
||||
private readonly DirectoryBrowserOptions _options;
|
||||
private readonly PathString _matchUrl;
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly IDirectoryFormatter _formatter;
|
||||
private readonly IFileProvider _fileProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the SendFileMiddleware. Using <see cref="HtmlEncoder.Default"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="next">The next middleware in the pipeline.</param>
|
||||
/// <param name="hostingEnv">The <see cref="IHostingEnvironment"/> used by this middleware.</param>
|
||||
/// <param name="options">The configuration for this middleware.</param>
|
||||
public DirectoryBrowserMiddleware(RequestDelegate next, IHostingEnvironment hostingEnv, IOptions<DirectoryBrowserOptions> options)
|
||||
: this(next, hostingEnv, HtmlEncoder.Default, options)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the SendFileMiddleware.
|
||||
/// </summary>
|
||||
/// <param name="next">The next middleware in the pipeline.</param>
|
||||
/// <param name="hostingEnv">The <see cref="IHostingEnvironment"/> used by this middleware.</param>
|
||||
/// <param name="encoder">The <see cref="HtmlEncoder"/> used by the default <see cref="HtmlDirectoryFormatter"/>.</param>
|
||||
/// <param name="options">The configuration for this middleware.</param>
|
||||
public DirectoryBrowserMiddleware(RequestDelegate next, IHostingEnvironment hostingEnv, HtmlEncoder encoder, IOptions<DirectoryBrowserOptions> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Examines the request to see if it matches a configured directory. If so, a view of the directory contents is returned.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Directory browsing options
|
||||
/// </summary>
|
||||
public class DirectoryBrowserOptions : SharedOptionsBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Enabled directory browsing for all request paths
|
||||
/// </summary>
|
||||
public DirectoryBrowserOptions()
|
||||
: this(new SharedOptions())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enabled directory browsing all request paths
|
||||
/// </summary>
|
||||
/// <param name="sharedOptions"></param>
|
||||
public DirectoryBrowserOptions(SharedOptions sharedOptions)
|
||||
: base(sharedOptions)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The component that generates the view.
|
||||
/// </summary>
|
||||
public IDirectoryFormatter Formatter { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for adding directory browser services.
|
||||
/// </summary>
|
||||
public static class DirectoryBrowserServiceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds directory browser middleware services.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
|
||||
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
|
||||
public static IServiceCollection AddDirectoryBrowser(this IServiceCollection services)
|
||||
{
|
||||
if (services == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
|
||||
services.AddWebEncoders();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a mapping between file extensions and MIME types.
|
||||
/// </summary>
|
||||
public class FileExtensionContentTypeProvider : IContentTypeProvider
|
||||
{
|
||||
#region Extension mapping table
|
||||
/// <summary>
|
||||
/// Creates a new provider with a set of default mappings.
|
||||
/// </summary>
|
||||
public FileExtensionContentTypeProvider()
|
||||
: this(new Dictionary<string, string>(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
|
||||
|
||||
/// <summary>
|
||||
/// Creates a lookup engine using the provided mapping.
|
||||
/// It is recommended that the IDictionary instance use StringComparer.OrdinalIgnoreCase.
|
||||
/// </summary>
|
||||
/// <param name="mapping"></param>
|
||||
public FileExtensionContentTypeProvider(IDictionary<string, string> mapping)
|
||||
{
|
||||
if (mapping == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(mapping));
|
||||
}
|
||||
Mappings = mapping;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The cross reference table of file extensions and content-types.
|
||||
/// </summary>
|
||||
public IDictionary<string, string> Mappings { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Given a file path, determine the MIME type
|
||||
/// </summary>
|
||||
/// <param name="subpath">A file path</param>
|
||||
/// <param name="contentType">The resulting MIME type</param>
|
||||
/// <returns>True if MIME type could be determined</returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods that combine all of the static file middleware components:
|
||||
/// Default files, directory browsing, send file, and static files
|
||||
/// </summary>
|
||||
public static class FileServerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enable all static file middleware (except directory browsing) for the current request path in the current directory.
|
||||
/// </summary>
|
||||
/// <param name="app"></param>
|
||||
/// <returns></returns>
|
||||
public static IApplicationBuilder UseFileServer(this IApplicationBuilder app)
|
||||
{
|
||||
if (app == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(app));
|
||||
}
|
||||
|
||||
return app.UseFileServer(new FileServerOptions());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enable all static file middleware on for the current request path in the current directory.
|
||||
/// </summary>
|
||||
/// <param name="app"></param>
|
||||
/// <param name="enableDirectoryBrowsing">Should directory browsing be enabled?</param>
|
||||
/// <returns></returns>
|
||||
public static IApplicationBuilder UseFileServer(this IApplicationBuilder app, bool enableDirectoryBrowsing)
|
||||
{
|
||||
if (app == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(app));
|
||||
}
|
||||
|
||||
return app.UseFileServer(new FileServerOptions
|
||||
{
|
||||
EnableDirectoryBrowsing = enableDirectoryBrowsing
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables all static file middleware (except directory browsing) for the given request path from the directory of the same name
|
||||
/// </summary>
|
||||
/// <param name="app"></param>
|
||||
/// <param name="requestPath">The relative request path.</param>
|
||||
/// <returns></returns>
|
||||
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)
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enable all static file middleware with the given options
|
||||
/// </summary>
|
||||
/// <param name="app"></param>
|
||||
/// <param name="options"></param>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for all of the static file middleware components
|
||||
/// </summary>
|
||||
public class FileServerOptions : SharedOptionsBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a combined options class for all of the static file middleware components.
|
||||
/// </summary>
|
||||
public FileServerOptions()
|
||||
: base(new SharedOptions())
|
||||
{
|
||||
StaticFileOptions = new StaticFileOptions(SharedOptions);
|
||||
DirectoryBrowserOptions = new DirectoryBrowserOptions(SharedOptions);
|
||||
DefaultFilesOptions = new DefaultFilesOptions(SharedOptions);
|
||||
EnableDefaultFiles = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for configuring the StaticFileMiddleware.
|
||||
/// </summary>
|
||||
public StaticFileOptions StaticFileOptions { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Options for configuring the DirectoryBrowserMiddleware.
|
||||
/// </summary>
|
||||
public DirectoryBrowserOptions DirectoryBrowserOptions { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Options for configuring the DefaultFilesMiddleware.
|
||||
/// </summary>
|
||||
public DefaultFilesOptions DefaultFilesOptions { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Directory browsing is disabled by default.
|
||||
/// </summary>
|
||||
public bool EnableDirectoryBrowsing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Default files are enabled by default.
|
||||
/// </summary>
|
||||
public bool EnableDefaultFiles { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates an HTML view for a directory.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates an HTML view for a directory.
|
||||
/// </summary>
|
||||
public virtual Task GenerateContentAsync(HttpContext context, IEnumerable<IFileInfo> 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(
|
||||
@"<!DOCTYPE html>
|
||||
<html lang=""{0}"">", CultureInfo.CurrentUICulture.TwoLetterISOLanguageName);
|
||||
|
||||
builder.AppendFormat(@"
|
||||
<head>
|
||||
<title>{0} {1}</title>", HtmlEncode(Resources.HtmlDir_IndexOf), HtmlEncode(requestPath.Value));
|
||||
|
||||
builder.Append(@"
|
||||
<style>
|
||||
body {
|
||||
font-family: ""Segoe UI"", ""Segoe WP"", ""Helvetica Neue"", 'RobotoRegular', sans-serif;
|
||||
font-size: 14px;}
|
||||
header h1 {
|
||||
font-family: ""Segoe UI Light"", ""Helvetica Neue"", 'RobotoLight', ""Segoe UI"", ""Segoe WP"", sans-serif;
|
||||
font-size: 28px;
|
||||
font-weight: 100;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 0px;}
|
||||
#index {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
margin: 0 0 20px; }
|
||||
#index th {
|
||||
vertical-align: bottom;
|
||||
padding: 10px 5px 5px 5px;
|
||||
font-weight: 400;
|
||||
color: #a0a0a0;
|
||||
text-align: center; }
|
||||
#index td { padding: 3px 10px; }
|
||||
#index th, #index td {
|
||||
border-right: 1px #ddd solid;
|
||||
border-bottom: 1px #ddd solid;
|
||||
border-left: 1px transparent solid;
|
||||
border-top: 1px transparent solid;
|
||||
box-sizing: border-box; }
|
||||
#index th:last-child, #index td:last-child {
|
||||
border-right: 1px transparent solid; }
|
||||
#index td.length, td.modified { text-align:right; }
|
||||
a { color:#1ba1e2;text-decoration:none; }
|
||||
a:hover { color:#13709e;text-decoration:underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<section id=""main"">");
|
||||
builder.AppendFormat(@"
|
||||
<header><h1>{0} <a href=""/"">/</a>", HtmlEncode(Resources.HtmlDir_IndexOf));
|
||||
|
||||
string cumulativePath = "/";
|
||||
foreach (var segment in requestPath.Value.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
cumulativePath = cumulativePath + segment + "/";
|
||||
builder.AppendFormat(@"<a href=""{0}"">{1}/</a>",
|
||||
HtmlEncode(cumulativePath), HtmlEncode(segment));
|
||||
}
|
||||
|
||||
builder.AppendFormat(CultureInfo.CurrentUICulture,
|
||||
@"</h1></header>
|
||||
<table id=""index"" summary=""{0}"">
|
||||
<thead>
|
||||
<tr><th abbr=""{1}"">{1}</th><th abbr=""{2}"">{2}</th><th abbr=""{3}"">{4}</th></tr>
|
||||
</thead>
|
||||
<tbody>",
|
||||
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(@"
|
||||
<tr class=""directory"">
|
||||
<td class=""name""><a href=""./{0}/"">{0}/</a></td>
|
||||
<td></td>
|
||||
<td class=""modified"">{1}</td>
|
||||
</tr>",
|
||||
HtmlEncode(subdir.Name),
|
||||
HtmlEncode(subdir.LastModified.ToString(CultureInfo.CurrentCulture)));
|
||||
}
|
||||
|
||||
foreach (var file in contents.Where(info => !info.IsDirectory))
|
||||
{
|
||||
builder.AppendFormat(@"
|
||||
<tr class=""file"">
|
||||
<td class=""name""><a href=""./{0}"">{0}</a></td>
|
||||
<td class=""length"">{1}</td>
|
||||
<td class=""modified"">{2}</td>
|
||||
</tr>",
|
||||
HtmlEncode(file.Name),
|
||||
HtmlEncode(file.Length.ToString("n0", CultureInfo.CurrentCulture)),
|
||||
HtmlEncode(file.LastModified.ToString(CultureInfo.CurrentCulture)));
|
||||
}
|
||||
|
||||
builder.Append(@"
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</body>
|
||||
</html>");
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to look up MIME types given a file path
|
||||
/// </summary>
|
||||
public interface IContentTypeProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Given a file path, determine the MIME type
|
||||
/// </summary>
|
||||
/// <param name="subpath">A file path</param>
|
||||
/// <param name="contentType">The resulting MIME type</param>
|
||||
/// <returns>True if MIME type could be determined</returns>
|
||||
bool TryGetContentType(string subpath, out string contentType);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates the view for a directory
|
||||
/// </summary>
|
||||
public interface IDirectoryFormatter
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.).
|
||||
/// </summary>
|
||||
Task GenerateContentAsync(HttpContext context, IEnumerable<IFileInfo> contents);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Options common to several middleware components
|
||||
/// </summary>
|
||||
public class SharedOptions
|
||||
{
|
||||
private PathString _requestPath;
|
||||
|
||||
/// <summary>
|
||||
/// Defaults to all request paths.
|
||||
/// </summary>
|
||||
public SharedOptions()
|
||||
{
|
||||
RequestPath = PathString.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The request path that maps to static resources
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The file system used to locate resources
|
||||
/// </summary>
|
||||
public IFileProvider FileProvider { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Options common to several middleware components
|
||||
/// </summary>
|
||||
public abstract class SharedOptionsBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an new instance of the SharedOptionsBase.
|
||||
/// </summary>
|
||||
/// <param name="sharedOptions"></param>
|
||||
protected SharedOptionsBase(SharedOptions sharedOptions)
|
||||
{
|
||||
if (sharedOptions == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(sharedOptions));
|
||||
}
|
||||
|
||||
SharedOptions = sharedOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options common to several middleware components
|
||||
/// </summary>
|
||||
protected SharedOptions SharedOptions { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The relative request path that maps to static resources.
|
||||
/// </summary>
|
||||
public PathString RequestPath
|
||||
{
|
||||
get { return SharedOptions.RequestPath; }
|
||||
set { SharedOptions.RequestPath = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The file system used to locate resources
|
||||
/// </summary>
|
||||
public IFileProvider FileProvider
|
||||
{
|
||||
get { return SharedOptions.FileProvider; }
|
||||
set { SharedOptions.FileProvider = value; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines *all* the logger messages produced by static files
|
||||
/// </summary>
|
||||
internal static class LoggerExtensions
|
||||
{
|
||||
private static Action<ILogger, string, Exception> _logMethodNotSupported;
|
||||
private static Action<ILogger, string, string, Exception> _logFileServed;
|
||||
private static Action<ILogger, string, Exception> _logPathMismatch;
|
||||
private static Action<ILogger, string, Exception> _logFileTypeNotSupported;
|
||||
private static Action<ILogger, string, Exception> _logFileNotFound;
|
||||
private static Action<ILogger, string, Exception> _logPathNotModified;
|
||||
private static Action<ILogger, string, Exception> _logPreconditionFailed;
|
||||
private static Action<ILogger, int, string, Exception> _logHandled;
|
||||
private static Action<ILogger, string, Exception> _logRangeNotSatisfiable;
|
||||
private static Action<ILogger, StringValues, string, Exception> _logSendingFileRange;
|
||||
private static Action<ILogger, StringValues, string, Exception> _logCopyingFileRange;
|
||||
private static Action<ILogger, long, string, string, Exception> _logCopyingBytesToResponse;
|
||||
private static Action<ILogger, Exception> _logWriteCancelled;
|
||||
|
||||
static LoggerExtensions()
|
||||
{
|
||||
_logMethodNotSupported = LoggerMessage.Define<string>(
|
||||
logLevel: LogLevel.Debug,
|
||||
eventId: 1,
|
||||
formatString: "{Method} requests are not supported");
|
||||
_logFileServed = LoggerMessage.Define<string, string>(
|
||||
logLevel: LogLevel.Information,
|
||||
eventId: 2,
|
||||
formatString: "Sending file. Request path: '{VirtualPath}'. Physical path: '{PhysicalPath}'");
|
||||
_logPathMismatch = LoggerMessage.Define<string>(
|
||||
logLevel: LogLevel.Debug,
|
||||
eventId: 3,
|
||||
formatString: "The request path {Path} does not match the path filter");
|
||||
_logFileTypeNotSupported = LoggerMessage.Define<string>(
|
||||
logLevel: LogLevel.Debug,
|
||||
eventId: 4,
|
||||
formatString: "The request path {Path} does not match a supported file type");
|
||||
_logFileNotFound = LoggerMessage.Define<string>(
|
||||
logLevel: LogLevel.Debug,
|
||||
eventId: 5,
|
||||
formatString: "The request path {Path} does not match an existing file");
|
||||
_logPathNotModified = LoggerMessage.Define<string>(
|
||||
logLevel: LogLevel.Information,
|
||||
eventId: 6,
|
||||
formatString: "The file {Path} was not modified");
|
||||
_logPreconditionFailed = LoggerMessage.Define<string>(
|
||||
logLevel: LogLevel.Information,
|
||||
eventId: 7,
|
||||
formatString: "Precondition for {Path} failed");
|
||||
_logHandled = LoggerMessage.Define<int, string>(
|
||||
logLevel: LogLevel.Debug,
|
||||
eventId: 8,
|
||||
formatString: "Handled. Status code: {StatusCode} File: {Path}");
|
||||
_logRangeNotSatisfiable = LoggerMessage.Define<string>(
|
||||
logLevel: LogLevel.Warning,
|
||||
eventId: 9,
|
||||
formatString: "Range not satisfiable for {Path}");
|
||||
_logSendingFileRange = LoggerMessage.Define<StringValues, string>(
|
||||
logLevel: LogLevel.Information,
|
||||
eventId: 10,
|
||||
formatString: "Sending {Range} of file {Path}");
|
||||
_logCopyingFileRange = LoggerMessage.Define<StringValues, string>(
|
||||
logLevel: LogLevel.Debug,
|
||||
eventId: 11,
|
||||
formatString: "Copying {Range} of file {Path} to the response body");
|
||||
_logCopyingBytesToResponse = LoggerMessage.Define<long, string, string>(
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>ASP.NET Core static files middleware. Includes middleware for serving static files, directory browsing, and default files.</Description>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageTags>aspnetcore;staticfiles</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\shared\Microsoft.AspNetCore.RangeHelper.Sources\**\*.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="$(MicrosoftAspNetCoreHostingAbstractionsPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="$(MicrosoftAspNetCoreHttpExtensionsPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Abstractions" Version="$(MicrosoftExtensionsFileProvidersAbstractionsPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsLoggingAbstractionsPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.WebEncoders" Version="$(MicrosoftExtensionsWebEncodersPackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -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")]
|
||||
142
src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Resources.Designer.cs
generated
Normal file
142
src/StaticFiles/src/Microsoft.AspNetCore.StaticFiles/Resources.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
// <auto-generated />
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// No formatter provided.
|
||||
/// </summary>
|
||||
internal static string Args_NoFormatter
|
||||
{
|
||||
get { return GetString("Args_NoFormatter"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// No formatter provided.
|
||||
/// </summary>
|
||||
internal static string FormatArgs_NoFormatter()
|
||||
{
|
||||
return GetString("Args_NoFormatter");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Index of
|
||||
/// </summary>
|
||||
internal static string HtmlDir_IndexOf
|
||||
{
|
||||
get { return GetString("HtmlDir_IndexOf"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Index of
|
||||
/// </summary>
|
||||
internal static string FormatHtmlDir_IndexOf()
|
||||
{
|
||||
return GetString("HtmlDir_IndexOf");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Last Modified
|
||||
/// </summary>
|
||||
internal static string HtmlDir_LastModified
|
||||
{
|
||||
get { return GetString("HtmlDir_LastModified"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Last Modified
|
||||
/// </summary>
|
||||
internal static string FormatHtmlDir_LastModified()
|
||||
{
|
||||
return GetString("HtmlDir_LastModified");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Modified
|
||||
/// </summary>
|
||||
internal static string HtmlDir_Modified
|
||||
{
|
||||
get { return GetString("HtmlDir_Modified"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Modified
|
||||
/// </summary>
|
||||
internal static string FormatHtmlDir_Modified()
|
||||
{
|
||||
return GetString("HtmlDir_Modified");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Name
|
||||
/// </summary>
|
||||
internal static string HtmlDir_Name
|
||||
{
|
||||
get { return GetString("HtmlDir_Name"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Name
|
||||
/// </summary>
|
||||
internal static string FormatHtmlDir_Name()
|
||||
{
|
||||
return GetString("HtmlDir_Name");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Size
|
||||
/// </summary>
|
||||
internal static string HtmlDir_Size
|
||||
{
|
||||
get { return GetString("HtmlDir_Size"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Size
|
||||
/// </summary>
|
||||
internal static string FormatHtmlDir_Size()
|
||||
{
|
||||
return GetString("HtmlDir_Size");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The list of files in the given directory. Column headers are listed in the first row.
|
||||
/// </summary>
|
||||
internal static string HtmlDir_TableSummary
|
||||
{
|
||||
get { return GetString("HtmlDir_TableSummary"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The list of files in the given directory. Column headers are listed in the first row.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="Args_NoFormatter" xml:space="preserve">
|
||||
<value>No formatter provided.</value>
|
||||
</data>
|
||||
<data name="HtmlDir_IndexOf" xml:space="preserve">
|
||||
<value>Index of</value>
|
||||
</data>
|
||||
<data name="HtmlDir_LastModified" xml:space="preserve">
|
||||
<value>Last Modified</value>
|
||||
</data>
|
||||
<data name="HtmlDir_Modified" xml:space="preserve">
|
||||
<value>Modified</value>
|
||||
</data>
|
||||
<data name="HtmlDir_Name" xml:space="preserve">
|
||||
<value>Name</value>
|
||||
</data>
|
||||
<data name="HtmlDir_Size" xml:space="preserve">
|
||||
<value>Size</value>
|
||||
</data>
|
||||
<data name="HtmlDir_TableSummary" xml:space="preserve">
|
||||
<value>The list of files in the given directory. Column headers are listed in the first row.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -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<IHttpSendFileFeature>();
|
||||
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<IHttpSendFileFeature>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for the StaticFileMiddleware
|
||||
/// </summary>
|
||||
public static class StaticFileExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enables static file serving for the current request path
|
||||
/// </summary>
|
||||
/// <param name="app"></param>
|
||||
/// <returns></returns>
|
||||
public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app)
|
||||
{
|
||||
if (app == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(app));
|
||||
}
|
||||
|
||||
return app.UseMiddleware<StaticFileMiddleware>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables static file serving for the given request path
|
||||
/// </summary>
|
||||
/// <param name="app"></param>
|
||||
/// <param name="requestPath">The relative request path.</param>
|
||||
/// <returns></returns>
|
||||
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)
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables static file serving with the given options
|
||||
/// </summary>
|
||||
/// <param name="app"></param>
|
||||
/// <param name="options"></param>
|
||||
/// <returns></returns>
|
||||
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<StaticFileMiddleware>(Options.Create(options));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Enables serving static files for a given request path
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the StaticFileMiddleware.
|
||||
/// </summary>
|
||||
/// <param name="next">The next middleware in the pipeline.</param>
|
||||
/// <param name="hostingEnv">The <see cref="IHostingEnvironment"/> used by this middleware.</param>
|
||||
/// <param name="options">The configuration options.</param>
|
||||
/// <param name="loggerFactory">An <see cref="ILoggerFactory"/> instance used to create loggers.</param>
|
||||
public StaticFileMiddleware(RequestDelegate next, IHostingEnvironment hostingEnv, IOptions<StaticFileOptions> 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<StaticFileMiddleware>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes a request to determine if it matches a known file, and if so, serves it.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for serving static files
|
||||
/// </summary>
|
||||
public class StaticFileOptions : SharedOptionsBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Defaults to all request paths
|
||||
/// </summary>
|
||||
public StaticFileOptions() : this(new SharedOptions())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defaults to all request paths
|
||||
/// </summary>
|
||||
/// <param name="sharedOptions"></param>
|
||||
public StaticFileOptions(SharedOptions sharedOptions) : base(sharedOptions)
|
||||
{
|
||||
OnPrepareResponse = _ => { };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to map files to content-types.
|
||||
/// </summary>
|
||||
public IContentTypeProvider ContentTypeProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
public string DefaultContentType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the file is not a recognized content-type should it be served?
|
||||
/// Default: false.
|
||||
/// </summary>
|
||||
public bool ServeUnknownFileTypes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public Action<StaticFileResponseContext> OnPrepareResponse { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains information about the request and the file that will be served in response.
|
||||
/// </summary>
|
||||
public class StaticFileResponseContext
|
||||
{
|
||||
/// <summary>
|
||||
/// The request and response information.
|
||||
/// </summary>
|
||||
public HttpContext Context { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The file to be served.
|
||||
/// </summary>
|
||||
public IFileInfo File { get; internal set; }
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,19 @@
|
|||
<Project>
|
||||
<Import Project="..\Directory.Build.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<DeveloperBuildTestTfms>netcoreapp2.1</DeveloperBuildTestTfms>
|
||||
<StandardTestTfms>$(DeveloperBuildTestTfms)</StandardTestTfms>
|
||||
<StandardTestTfms Condition=" '$(DeveloperBuild)' != 'true' ">netcoreapp2.1;netcoreapp2.0</StandardTestTfms>
|
||||
<StandardTestTfms Condition=" '$(DeveloperBuild)' != 'true' AND '$(OS)' == 'Windows_NT' ">$(StandardTestTfms);net461</StandardTestTfms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Internal.AspNetCore.Sdk" PrivateAssets="All" Version="$(InternalAspNetCoreSdkPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPackageVersion)" />
|
||||
<PackageReference Include="xunit" Version="$(XunitPackageVersion)" />
|
||||
<PackageReference Include="xunit.analyzers" Version="$(XunitAnalyzersPackageVersion)" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitRunnerVisualStudioPackageVersion)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\shared\Microsoft.AspNetCore.RangeHelper.Sources\**\*.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="$(MicrosoftAspNetCoreHttpPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsLoggingAbstractionsPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="$(MicrosoftAspNetCoreHttpExtensionsPackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
||||
|
||||
<!--
|
||||
Workaround for "Use executable flags in Microsoft.NET.Test.Sdk" (https://github.com/Microsoft/vstest/issues/792).
|
||||
Remove when fixed.
|
||||
-->
|
||||
<HasRuntimeOutput>true</HasRuntimeOutput>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\shared\*.cs" />
|
||||
<Content Include="TestDocument1MB.txt">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="SubFolder\**\*;TestDocument.txt">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.StaticFiles\Microsoft.AspNetCore.StaticFiles.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.HttpSys" Version="$(MicrosoftAspNetCoreServerHttpSysPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.IntegrationTesting" Version="$(MicrosoftAspNetCoreServerIntegrationTestingPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Testing" Version="$(MicrosoftExtensionsLoggingTestingPackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -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<IHostingEnvironment>();
|
||||
|
||||
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<IHostingEnvironment>();
|
||||
|
||||
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<object[]> ExistingFiles => new[]
|
||||
{
|
||||
new[] {"", @".", "/TestDocument.txt"},
|
||||
new[] {"/somedir", @".", "/somedir/TestDocument.txt"},
|
||||
new[] {"/SomeDir", @".", "/soMediR/TestDocument.txt"},
|
||||
new[] {"", @"SubFolder", "/ranges.txt"},
|
||||
new[] {"/somedir", @"SubFolder", "/somedir/ranges.txt"},
|
||||
new[] {"", @"SubFolder", "/Empty.txt"}
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void ClientDisconnect_Kestrel_NoWriteExceptionThrown()
|
||||
{
|
||||
ClientDisconnect_NoWriteExceptionThrown(ServerType.Kestrel);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
[OSSkipCondition(OperatingSystems.Linux)]
|
||||
[OSSkipCondition(OperatingSystems.MacOSX)]
|
||||
public void ClientDisconnect_WebListener_NoWriteExceptionThrown()
|
||||
{
|
||||
ClientDisconnect_NoWriteExceptionThrown(ServerType.WebListener);
|
||||
}
|
||||
|
||||
private void ClientDisconnect_NoWriteExceptionThrown(ServerType serverType)
|
||||
{
|
||||
var interval = TimeSpan.FromSeconds(15);
|
||||
var baseAddress = "http://localhost:12345";
|
||||
var requestReceived = new ManualResetEvent(false);
|
||||
var requestCacelled = new ManualResetEvent(false);
|
||||
var responseComplete = new ManualResetEvent(false);
|
||||
Exception exception = null;
|
||||
var builder = new WebHostBuilder()
|
||||
.UseWebRoot(Path.Combine(AppContext.BaseDirectory))
|
||||
.Configure(app =>
|
||||
{
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
requestReceived.Set();
|
||||
Assert.True(requestCacelled.WaitOne(interval), "not cancelled");
|
||||
Assert.True(context.RequestAborted.WaitHandle.WaitOne(interval), "not aborted");
|
||||
await next();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exception = ex;
|
||||
}
|
||||
responseComplete.Set();
|
||||
});
|
||||
app.UseStaticFiles();
|
||||
});
|
||||
|
||||
if (serverType == ServerType.WebListener)
|
||||
{
|
||||
builder.UseHttpSys();
|
||||
}
|
||||
else if (serverType == ServerType.Kestrel)
|
||||
{
|
||||
builder.UseKestrel();
|
||||
}
|
||||
|
||||
using (var server = builder.Start(baseAddress))
|
||||
{
|
||||
// We don't use HttpClient here because it's disconnect behavior varies across platforms.
|
||||
var socket = SendSocketRequestAsync(baseAddress, "/TestDocument1MB.txt");
|
||||
Assert.True(requestReceived.WaitOne(interval), "not received");
|
||||
|
||||
socket.LingerState = new LingerOption(true, 0);
|
||||
socket.Dispose();
|
||||
requestCacelled.Set();
|
||||
|
||||
Assert.True(responseComplete.WaitOne(interval), "not completed");
|
||||
Assert.Null(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private Socket SendSocketRequestAsync(string address, string path, string method = "GET")
|
||||
{
|
||||
var uri = new Uri(address);
|
||||
var builder = new StringBuilder();
|
||||
builder.Append($"{method} {path} HTTP/1.1\r\n");
|
||||
builder.Append($"HOST: {uri.Authority}\r\n\r\n");
|
||||
|
||||
byte[] request = Encoding.ASCII.GetBytes(builder.ToString());
|
||||
|
||||
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
|
||||
socket.Connect(IPAddress.Loopback, uri.Port);
|
||||
socket.Send(request);
|
||||
return socket;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
A
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
Hello World
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1 @@
|
|||
<xml/>
|
||||
|
|
@ -0,0 +1 @@
|
|||
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
|
|
@ -0,0 +1 @@
|
|||
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,430 @@
|
|||
// Copyright (c) .NET 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.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.StaticFiles
|
||||
{
|
||||
public class CacheHeaderTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ServerShouldReturnETag()
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
|
||||
HttpResponseMessage response = await server.CreateClient().GetAsync("http://localhost/SubFolder/extra.xml");
|
||||
Assert.NotNull(response.Headers.ETag);
|
||||
Assert.NotNull(response.Headers.ETag.Tag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SameETagShouldBeReturnedAgain()
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
|
||||
HttpResponseMessage response1 = await server.CreateClient().GetAsync("http://localhost/SubFolder/extra.xml");
|
||||
HttpResponseMessage response2 = await server.CreateClient().GetAsync("http://localhost/SubFolder/extra.xml");
|
||||
Assert.Equal(response2.Headers.ETag, response1.Headers.ETag);
|
||||
}
|
||||
|
||||
// 14.24 If-Match
|
||||
// If none of the entity tags match, or if "*" is given and no current
|
||||
// entity exists, the server MUST NOT perform the requested method, and
|
||||
// MUST return a 412 (Precondition Failed) response. This behavior is
|
||||
// most useful when the client wants to prevent an updating method, such
|
||||
// as PUT, from modifying a resource that has changed since the client
|
||||
// last retrieved it.
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SupportedMethods))]
|
||||
public async Task IfMatchShouldReturn412WhenNotListed(HttpMethod method)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
var req = new HttpRequestMessage(method, "http://localhost/SubFolder/extra.xml");
|
||||
req.Headers.Add("If-Match", "\"fake\"");
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.PreconditionFailed, resp.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SupportedMethods))]
|
||||
public async Task IfMatchShouldBeServedWhenListed(HttpMethod method)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
HttpResponseMessage original = await server.CreateClient().GetAsync("http://localhost/SubFolder/extra.xml");
|
||||
|
||||
var req = new HttpRequestMessage(method, "http://localhost/SubFolder/extra.xml");
|
||||
req.Headers.Add("If-Match", original.Headers.ETag.ToString());
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SupportedMethods))]
|
||||
public async Task IfMatchShouldBeServedForAstrisk(HttpMethod method)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
var req = new HttpRequestMessage(method, "http://localhost/SubFolder/extra.xml");
|
||||
req.Headers.Add("If-Match", "*");
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(UnsupportedMethods))]
|
||||
public async Task IfMatchShouldBeIgnoredForUnsupportedMethods(HttpMethod method)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
var req = new HttpRequestMessage(method, "http://localhost/SubFolder/extra.xml");
|
||||
req.Headers.Add("If-Match", "*");
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
|
||||
}
|
||||
|
||||
// 14.26 If-None-Match
|
||||
// If any of the entity tags match the entity tag of the entity that
|
||||
// would have been returned in the response to a similar GET request
|
||||
// (without the If-None-Match header) on that resource, or if "*" is
|
||||
// given and any current entity exists for that resource, then the
|
||||
// server MUST NOT perform the requested method, unless required to do
|
||||
// so because the resource's modification date fails to match that
|
||||
// supplied in an If-Modified-Since header field in the request.
|
||||
// Instead, if the request method was GET or HEAD, the server SHOULD
|
||||
// respond with a 304 (Not Modified) response, including the cache-
|
||||
// related header fields (particularly ETag) of one of the entities that
|
||||
// matched. For all other request methods, the server MUST respond with
|
||||
// a status of 412 (Precondition Failed).
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SupportedMethods))]
|
||||
public async Task IfNoneMatchShouldReturn304ForMatching(HttpMethod method)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
HttpResponseMessage resp1 = await server.CreateClient().GetAsync("http://localhost/SubFolder/extra.xml");
|
||||
|
||||
var req2 = new HttpRequestMessage(method, "http://localhost/SubFolder/extra.xml");
|
||||
req2.Headers.Add("If-None-Match", resp1.Headers.ETag.ToString());
|
||||
HttpResponseMessage resp2 = await server.CreateClient().SendAsync(req2);
|
||||
Assert.Equal(HttpStatusCode.NotModified, resp2.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SupportedMethods))]
|
||||
public async Task IfNoneMatchAllShouldReturn304ForMatching(HttpMethod method)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
HttpResponseMessage resp1 = await server.CreateClient().GetAsync("http://localhost/SubFolder/extra.xml");
|
||||
|
||||
var req2 = new HttpRequestMessage(method, "http://localhost/SubFolder/extra.xml");
|
||||
req2.Headers.Add("If-None-Match", "*");
|
||||
HttpResponseMessage resp2 = await server.CreateClient().SendAsync(req2);
|
||||
Assert.Equal(HttpStatusCode.NotModified, resp2.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(UnsupportedMethods))]
|
||||
public async Task IfNoneMatchShouldBeIgnoredForNonTwoHundredAnd304Responses(HttpMethod method)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
HttpResponseMessage resp1 = await server.CreateClient().GetAsync("http://localhost/SubFolder/extra.xml");
|
||||
|
||||
var req2 = new HttpRequestMessage(method, "http://localhost/SubFolder/extra.xml");
|
||||
req2.Headers.Add("If-None-Match", resp1.Headers.ETag.ToString());
|
||||
HttpResponseMessage resp2 = await server.CreateClient().SendAsync(req2);
|
||||
Assert.Equal(HttpStatusCode.NotFound, resp2.StatusCode);
|
||||
}
|
||||
|
||||
// 14.26 If-None-Match
|
||||
// If none of the entity tags match, then the server MAY perform the
|
||||
// requested method as if the If-None-Match header field did not exist,
|
||||
// but MUST also ignore any If-Modified-Since header field(s) in the
|
||||
// request. That is, if no entity tags match, then the server MUST NOT
|
||||
// return a 304 (Not Modified) response.
|
||||
|
||||
// A server MUST use the strong comparison function (see section 13.3.3)
|
||||
// to compare the entity tags in If-Match.
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SupportedMethods))]
|
||||
public async Task ServerShouldReturnLastModified(HttpMethod method)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
|
||||
HttpResponseMessage response = await server.CreateClient().SendAsync(
|
||||
new HttpRequestMessage(method, "http://localhost/SubFolder/extra.xml"));
|
||||
|
||||
Assert.NotNull(response.Content.Headers.LastModified);
|
||||
// Verify that DateTimeOffset is UTC
|
||||
Assert.Equal(response.Content.Headers.LastModified.Value.Offset, TimeSpan.Zero);
|
||||
}
|
||||
|
||||
// 13.3.4
|
||||
// An HTTP/1.1 origin server, upon receiving a conditional request that
|
||||
// includes both a Last-Modified date (e.g., in an If-Modified-Since or
|
||||
// If-Unmodified-Since header field) and one or more entity tags (e.g.,
|
||||
// in an If-Match, If-None-Match, or If-Range header field) as cache
|
||||
// validators, MUST NOT return a response status of 304 (Not Modified)
|
||||
// unless doing so is consistent with all of the conditional header
|
||||
// fields in the request.
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SupportedMethods))]
|
||||
public async Task MatchingBothConditionsReturnsNotModified(HttpMethod method)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
HttpResponseMessage resp1 = await server
|
||||
.CreateRequest("/SubFolder/extra.xml")
|
||||
.SendAsync(method.Method);
|
||||
|
||||
HttpResponseMessage resp2 = await server
|
||||
.CreateRequest("/SubFolder/extra.xml")
|
||||
.AddHeader("If-None-Match", resp1.Headers.ETag.ToString())
|
||||
.And(req => req.Headers.IfModifiedSince = resp1.Content.Headers.LastModified)
|
||||
.SendAsync(method.Method);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotModified, resp2.StatusCode);
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SupportedMethods))]
|
||||
public async Task MatchingAtLeastOneETagReturnsNotModified(HttpMethod method)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
HttpResponseMessage resp1 = await server
|
||||
.CreateRequest("/SubFolder/extra.xml")
|
||||
.SendAsync(method.Method);
|
||||
var etag = resp1.Headers.ETag.ToString();
|
||||
|
||||
HttpResponseMessage resp2 = await server
|
||||
.CreateRequest("/SubFolder/extra.xml")
|
||||
.AddHeader("If-Match", etag + ", " + etag)
|
||||
.SendAsync(method.Method);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, resp2.StatusCode);
|
||||
|
||||
HttpResponseMessage resp3 = await server
|
||||
.CreateRequest("/SubFolder/extra.xml")
|
||||
.AddHeader("If-Match", etag+ ", \"badetag\"")
|
||||
.SendAsync(method.Method);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, resp3.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SupportedMethods))]
|
||||
public async Task MissingEitherOrBothConditionsReturnsNormally(HttpMethod method)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
HttpResponseMessage resp1 = await server
|
||||
.CreateRequest("/SubFolder/extra.xml")
|
||||
.SendAsync(method.Method);
|
||||
|
||||
DateTimeOffset lastModified = resp1.Content.Headers.LastModified.Value;
|
||||
DateTimeOffset pastDate = lastModified.AddHours(-1);
|
||||
DateTimeOffset furtureDate = lastModified.AddHours(1);
|
||||
|
||||
HttpResponseMessage resp2 = await server
|
||||
.CreateRequest("/SubFolder/extra.xml")
|
||||
.AddHeader("If-None-Match", "\"fake\"")
|
||||
.And(req => req.Headers.IfModifiedSince = lastModified)
|
||||
.SendAsync(method.Method);
|
||||
|
||||
HttpResponseMessage resp3 = await server
|
||||
.CreateRequest("/SubFolder/extra.xml")
|
||||
.AddHeader("If-None-Match", resp1.Headers.ETag.ToString())
|
||||
.And(req => req.Headers.IfModifiedSince = pastDate)
|
||||
.SendAsync(method.Method);
|
||||
|
||||
HttpResponseMessage resp4 = await server
|
||||
.CreateRequest("/SubFolder/extra.xml")
|
||||
.AddHeader("If-None-Match", "\"fake\"")
|
||||
.And(req => req.Headers.IfModifiedSince = furtureDate)
|
||||
.SendAsync(method.Method);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, resp2.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, resp3.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, resp4.StatusCode);
|
||||
}
|
||||
|
||||
// 14.25 If-Modified-Since
|
||||
// The If-Modified-Since request-header field is used with a method to
|
||||
// make it conditional: if the requested variant has not been modified
|
||||
// since the time specified in this field, an entity will not be
|
||||
// returned from the server; instead, a 304 (not modified) response will
|
||||
// be returned without any message-body.
|
||||
|
||||
// a) If the request would normally result in anything other than a
|
||||
// 200 (OK) status, or if the passed If-Modified-Since date is
|
||||
// invalid, the response is exactly the same as for a normal GET.
|
||||
// A date which is later than the server's current time is
|
||||
// invalid.
|
||||
[Theory]
|
||||
[MemberData(nameof(SupportedMethods))]
|
||||
public async Task InvalidIfModifiedSinceDateFormatGivesNormalGet(HttpMethod method)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
|
||||
HttpResponseMessage res = await server
|
||||
.CreateRequest("/SubFolder/extra.xml")
|
||||
.AddHeader("If-Modified-Since", "bad-date")
|
||||
.SendAsync(method.Method);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, res.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SupportedMethods))]
|
||||
public async Task FutureIfModifiedSinceDateFormatGivesNormalGet(HttpMethod method)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
|
||||
HttpResponseMessage res = await server
|
||||
.CreateRequest("/SubFolder/extra.xml")
|
||||
.And(req => req.Headers.IfModifiedSince = DateTimeOffset.Now.AddYears(1))
|
||||
.SendAsync(method.Method);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, res.StatusCode);
|
||||
}
|
||||
|
||||
// b) If the variant has been modified since the If-Modified-Since
|
||||
// date, the response is exactly the same as for a normal GET.
|
||||
|
||||
// c) If the variant has not been modified since a valid If-
|
||||
// Modified-Since date, the server SHOULD return a 304 (Not
|
||||
// Modified) response.
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SupportedMethods))]
|
||||
public async Task IfModifiedSinceDateGreaterThanLastModifiedShouldReturn304(HttpMethod method)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
|
||||
HttpResponseMessage res1 = await server
|
||||
.CreateRequest("/SubFolder/extra.xml")
|
||||
.SendAsync(method.Method);
|
||||
|
||||
HttpResponseMessage res2 = await server
|
||||
.CreateRequest("/SubFolder/extra.xml")
|
||||
.And(req => req.Headers.IfModifiedSince = DateTimeOffset.Now)
|
||||
.SendAsync(method.Method);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotModified, res2.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SupportedMethods))]
|
||||
public async Task SuppportsIfModifiedDateFormats(HttpMethod method)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
HttpResponseMessage res1 = await server
|
||||
.CreateRequest("/SubFolder/extra.xml")
|
||||
.SendAsync(method.Method);
|
||||
|
||||
var formats = new[]
|
||||
{
|
||||
"ddd, dd MMM yyyy HH:mm:ss 'GMT'",
|
||||
"dddd, dd-MMM-yy HH:mm:ss 'GMT'",
|
||||
"ddd MMM d HH:mm:ss yyyy"
|
||||
};
|
||||
|
||||
foreach (var format in formats)
|
||||
{
|
||||
HttpResponseMessage res2 = await server
|
||||
.CreateRequest("/SubFolder/extra.xml")
|
||||
.AddHeader("If-Modified-Since", DateTimeOffset.UtcNow.ToString(format))
|
||||
.SendAsync(method.Method);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotModified, res2.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SupportedMethods))]
|
||||
public async Task IfModifiedSinceDateLessThanLastModifiedShouldReturn200(HttpMethod method)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
|
||||
HttpResponseMessage res1 = await server
|
||||
.CreateRequest("/SubFolder/extra.xml")
|
||||
.SendAsync(method.Method);
|
||||
|
||||
HttpResponseMessage res2 = await server
|
||||
.CreateRequest("/SubFolder/extra.xml")
|
||||
.And(req => req.Headers.IfModifiedSince = DateTimeOffset.MinValue)
|
||||
.SendAsync(method.Method);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, res2.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SupportedMethods))]
|
||||
public async Task InvalidIfUnmodifiedSinceDateFormatGivesNormalGet(HttpMethod method)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
|
||||
HttpResponseMessage res = await server
|
||||
.CreateRequest("/SubFolder/extra.xml")
|
||||
.AddHeader("If-Unmodified-Since", "bad-date")
|
||||
.SendAsync(method.Method);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, res.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SupportedMethods))]
|
||||
public async Task FutureIfUnmodifiedSinceDateFormatGivesNormalGet(HttpMethod method)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
|
||||
HttpResponseMessage res = await server
|
||||
.CreateRequest("/SubFolder/extra.xml")
|
||||
.And(req => req.Headers.IfUnmodifiedSince = DateTimeOffset.Now.AddYears(1))
|
||||
.SendAsync(method.Method);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, res.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SupportedMethods))]
|
||||
public async Task IfUnmodifiedSinceDateLessThanLastModifiedShouldReturn412(HttpMethod method)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
|
||||
HttpResponseMessage res1 = await server
|
||||
.CreateRequest("/SubFolder/extra.xml")
|
||||
.SendAsync(method.Method);
|
||||
|
||||
HttpResponseMessage res2 = await server
|
||||
.CreateRequest("/SubFolder/extra.xml")
|
||||
.And(req => req.Headers.IfUnmodifiedSince = DateTimeOffset.MinValue)
|
||||
.SendAsync(method.Method);
|
||||
|
||||
Assert.Equal(HttpStatusCode.PreconditionFailed, res2.StatusCode);
|
||||
}
|
||||
|
||||
|
||||
public static IEnumerable<object[]> SupportedMethods => new[]
|
||||
{
|
||||
new [] { HttpMethod.Get },
|
||||
new [] { HttpMethod.Head }
|
||||
};
|
||||
|
||||
public static IEnumerable<object[]> UnsupportedMethods => new[]
|
||||
{
|
||||
new [] { HttpMethod.Post },
|
||||
new [] { HttpMethod.Put },
|
||||
new [] { HttpMethod.Options },
|
||||
new [] { HttpMethod.Trace },
|
||||
new [] { new HttpMethod("VERB") }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.StaticFiles
|
||||
{
|
||||
public class DefaultContentTypeProviderTests
|
||||
{
|
||||
[Fact]
|
||||
public void UnknownExtensionsReturnFalse()
|
||||
{
|
||||
var provider = new FileExtensionContentTypeProvider();
|
||||
string contentType;
|
||||
Assert.False(provider.TryGetContentType("unknown.ext", out contentType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KnownExtensionsReturnTrye()
|
||||
{
|
||||
var provider = new FileExtensionContentTypeProvider();
|
||||
string contentType;
|
||||
Assert.True(provider.TryGetContentType("known.txt", out contentType));
|
||||
Assert.Equal("text/plain", contentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoubleDottedExtensionsAreNotSupported()
|
||||
{
|
||||
var provider = new FileExtensionContentTypeProvider();
|
||||
string contentType;
|
||||
Assert.False(provider.TryGetContentType("known.exe.config", out contentType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DashedExtensionsShouldBeMatched()
|
||||
{
|
||||
var provider = new FileExtensionContentTypeProvider();
|
||||
string contentType;
|
||||
Assert.True(provider.TryGetContentType("known.dvr-ms", out contentType));
|
||||
Assert.Equal("video/x-ms-dvr", contentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BothSlashFormatsAreUnderstood()
|
||||
{
|
||||
var provider = new FileExtensionContentTypeProvider();
|
||||
string contentType;
|
||||
Assert.True(provider.TryGetContentType(@"/first/example.txt", out contentType));
|
||||
Assert.Equal("text/plain", contentType);
|
||||
Assert.True(provider.TryGetContentType(@"\second\example.txt", out contentType));
|
||||
Assert.Equal("text/plain", contentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DotsInDirectoryAreIgnored()
|
||||
{
|
||||
var provider = new FileExtensionContentTypeProvider();
|
||||
string contentType;
|
||||
Assert.True(provider.TryGetContentType(@"/first.css/example.txt", out contentType));
|
||||
Assert.Equal("text/plain", contentType);
|
||||
Assert.True(provider.TryGetContentType(@"\second.css\example.txt", out contentType));
|
||||
Assert.Equal("text/plain", contentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidCharactersAreIgnored()
|
||||
{
|
||||
var provider = new FileExtensionContentTypeProvider();
|
||||
string contentType;
|
||||
Assert.True(provider.TryGetContentType($"{new string(System.IO.Path.GetInvalidPathChars())}.txt", out contentType));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.AspNetCore.Testing.xunit;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.StaticFiles
|
||||
{
|
||||
public class DefaultFilesMiddlewareTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task NullArguments()
|
||||
{
|
||||
// No exception, default provided
|
||||
StaticFilesTestServer.Create(app => app.UseDefaultFiles(new DefaultFilesOptions { FileProvider = null }));
|
||||
|
||||
// PathString(null) is OK.
|
||||
var server = StaticFilesTestServer.Create(app => app.UseDefaultFiles((string)null));
|
||||
var response = await server.CreateClient().GetAsync("/");
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("", @".", "/missing.dir")]
|
||||
[InlineData("", @".", "/missing.dir/")]
|
||||
[InlineData("/subdir", @".", "/subdir/missing.dir")]
|
||||
[InlineData("/subdir", @".", "/subdir/missing.dir/")]
|
||||
[InlineData("", @"./", "/missing.dir")]
|
||||
public async Task NoMatch_PassesThrough_All(string baseUrl, string baseDir, string requestUrl)
|
||||
{
|
||||
await NoMatch_PassesThrough(baseUrl, baseDir, requestUrl);
|
||||
}
|
||||
|
||||
[ConditionalTheory]
|
||||
[OSSkipCondition(OperatingSystems.Linux)]
|
||||
[OSSkipCondition(OperatingSystems.MacOSX)]
|
||||
[InlineData("", @".\", "/missing.dir")]
|
||||
[InlineData("", @".\", "/Missing.dir")]
|
||||
public async Task NoMatch_PassesThrough_Windows(string baseUrl, string baseDir, string requestUrl)
|
||||
{
|
||||
await NoMatch_PassesThrough(baseUrl, baseDir, requestUrl);
|
||||
}
|
||||
|
||||
private async Task NoMatch_PassesThrough(string baseUrl, string baseDir, string requestUrl)
|
||||
{
|
||||
using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir)))
|
||||
{
|
||||
var server = StaticFilesTestServer.Create(app =>
|
||||
{
|
||||
app.UseDefaultFiles(new DefaultFilesOptions
|
||||
{
|
||||
RequestPath = new PathString(baseUrl),
|
||||
FileProvider = fileProvider
|
||||
});
|
||||
app.Run(context => context.Response.WriteAsync(context.Request.Path.Value));
|
||||
});
|
||||
|
||||
var response = await server.CreateClient().GetAsync(requestUrl);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(requestUrl, await response.Content.ReadAsStringAsync()); // Should not be modified
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("", @".", "/SubFolder/")]
|
||||
[InlineData("", @"./", "/SubFolder/")]
|
||||
[InlineData("", @"./SubFolder", "/")]
|
||||
public async Task FoundDirectoryWithDefaultFile_PathModified_All(string baseUrl, string baseDir, string requestUrl)
|
||||
{
|
||||
await FoundDirectoryWithDefaultFile_PathModified(baseUrl, baseDir, requestUrl);
|
||||
}
|
||||
|
||||
[ConditionalTheory]
|
||||
[OSSkipCondition(OperatingSystems.Linux)]
|
||||
[OSSkipCondition(OperatingSystems.MacOSX)]
|
||||
[InlineData("", @".\", "/SubFolder/")]
|
||||
[InlineData("", @".\subFolder", "/")]
|
||||
public async Task FoundDirectoryWithDefaultFile_PathModified_Windows(string baseUrl, string baseDir, string requestUrl)
|
||||
{
|
||||
await FoundDirectoryWithDefaultFile_PathModified(baseUrl, baseDir, requestUrl);
|
||||
}
|
||||
|
||||
private async Task FoundDirectoryWithDefaultFile_PathModified(string baseUrl, string baseDir, string requestUrl)
|
||||
{
|
||||
using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir)))
|
||||
{
|
||||
var server = StaticFilesTestServer.Create(app =>
|
||||
{
|
||||
app.UseDefaultFiles(new DefaultFilesOptions
|
||||
{
|
||||
RequestPath = new PathString(baseUrl),
|
||||
FileProvider = fileProvider
|
||||
});
|
||||
app.Run(context => context.Response.WriteAsync(context.Request.Path.Value));
|
||||
});
|
||||
|
||||
var response = await server.CreateClient().GetAsync(requestUrl);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(requestUrl + "default.html", await response.Content.ReadAsStringAsync()); // Should be modified
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("", @".", "/SubFolder", "")]
|
||||
[InlineData("", @"./", "/SubFolder", "")]
|
||||
[InlineData("", @"./", "/SubFolder", "?a=b")]
|
||||
public async Task NearMatch_RedirectAddSlash_All(string baseUrl, string baseDir, string requestUrl, string queryString)
|
||||
{
|
||||
await NearMatch_RedirectAddSlash(baseUrl, baseDir, requestUrl, queryString);
|
||||
}
|
||||
|
||||
[ConditionalTheory]
|
||||
[OSSkipCondition(OperatingSystems.Linux)]
|
||||
[OSSkipCondition(OperatingSystems.MacOSX)]
|
||||
[InlineData("", @".\", "/SubFolder", "")]
|
||||
[InlineData("", @".\", "/SubFolder", "?a=b")]
|
||||
public async Task NearMatch_RedirectAddSlash_Windows(string baseUrl, string baseDir, string requestUrl, string queryString)
|
||||
{
|
||||
await NearMatch_RedirectAddSlash(baseUrl, baseDir, requestUrl, queryString);
|
||||
}
|
||||
|
||||
private async Task NearMatch_RedirectAddSlash(string baseUrl, string baseDir, string requestUrl, string queryString)
|
||||
{
|
||||
using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir)))
|
||||
{
|
||||
var server = StaticFilesTestServer.Create(app => app.UseDefaultFiles(new DefaultFilesOptions
|
||||
{
|
||||
RequestPath = new PathString(baseUrl),
|
||||
FileProvider = fileProvider
|
||||
}));
|
||||
var response = await server.CreateRequest(requestUrl + queryString).GetAsync();
|
||||
|
||||
Assert.Equal(HttpStatusCode.Moved, response.StatusCode);
|
||||
Assert.Equal(requestUrl + "/" + queryString, response.Headers.GetValues("Location").FirstOrDefault());
|
||||
Assert.Empty((await response.Content.ReadAsByteArrayAsync()));
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/SubFolder", @"./", "/SubFolder/")]
|
||||
[InlineData("/SubFolder", @".", "/somedir/")]
|
||||
[InlineData("", @"./SubFolder", "/")]
|
||||
[InlineData("", @"./SubFolder/", "/")]
|
||||
public async Task PostDirectory_PassesThrough_All(string baseUrl, string baseDir, string requestUrl)
|
||||
{
|
||||
await PostDirectory_PassesThrough(baseUrl, baseDir, requestUrl);
|
||||
}
|
||||
|
||||
[ConditionalTheory]
|
||||
[OSSkipCondition(OperatingSystems.Linux)]
|
||||
[OSSkipCondition(OperatingSystems.MacOSX)]
|
||||
[InlineData("/SubFolder", @".\", "/SubFolder/")]
|
||||
[InlineData("", @".\SubFolder", "/")]
|
||||
[InlineData("", @".\SubFolder\", "/")]
|
||||
public async Task PostDirectory_PassesThrough_Windows(string baseUrl, string baseDir, string requestUrl)
|
||||
{
|
||||
await PostDirectory_PassesThrough(baseUrl, baseDir, requestUrl);
|
||||
}
|
||||
|
||||
private async Task PostDirectory_PassesThrough(string baseUrl, string baseDir, string requestUrl)
|
||||
{
|
||||
using (var fileProvder = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir)))
|
||||
{
|
||||
var server = StaticFilesTestServer.Create(app => app.UseDefaultFiles(new DefaultFilesOptions
|
||||
{
|
||||
RequestPath = new PathString(baseUrl),
|
||||
FileProvider = fileProvder
|
||||
}));
|
||||
var response = await server.CreateRequest(requestUrl).GetAsync();
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); // Passed through
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.AspNetCore.Testing.xunit;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.StaticFiles
|
||||
{
|
||||
public class DirectoryBrowserMiddlewareTests
|
||||
{
|
||||
[Fact]
|
||||
public void WorksWithoutEncoderRegistered()
|
||||
{
|
||||
// No exception, uses HtmlEncoder.Default
|
||||
StaticFilesTestServer.Create(
|
||||
app => app.UseDirectoryBrowser());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NullArguments()
|
||||
{
|
||||
// No exception, default provided
|
||||
StaticFilesTestServer.Create(
|
||||
app => app.UseDirectoryBrowser(new DirectoryBrowserOptions { Formatter = null }),
|
||||
services => services.AddDirectoryBrowser());
|
||||
|
||||
// No exception, default provided
|
||||
StaticFilesTestServer.Create(
|
||||
app => app.UseDirectoryBrowser(new DirectoryBrowserOptions { FileProvider = null }),
|
||||
services => services.AddDirectoryBrowser());
|
||||
|
||||
// PathString(null) is OK.
|
||||
var server = StaticFilesTestServer.Create(
|
||||
app => app.UseDirectoryBrowser((string)null),
|
||||
services => services.AddDirectoryBrowser());
|
||||
|
||||
var response = await server.CreateClient().GetAsync("/");
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("", @".", "/missing.dir")]
|
||||
[InlineData("", @".", "/missing.dir/")]
|
||||
[InlineData("/subdir", @".", "/subdir/missing.dir")]
|
||||
[InlineData("/subdir", @".", "/subdir/missing.dir/")]
|
||||
[InlineData("", @"./", "/missing.dir")]
|
||||
public async Task NoMatch_PassesThrough_All(string baseUrl, string baseDir, string requestUrl)
|
||||
{
|
||||
await NoMatch_PassesThrough(baseUrl, baseDir, requestUrl);
|
||||
}
|
||||
|
||||
[ConditionalTheory]
|
||||
[OSSkipCondition(OperatingSystems.Linux)]
|
||||
[OSSkipCondition(OperatingSystems.MacOSX)]
|
||||
[InlineData("", @".\", "/missing.dir")]
|
||||
[InlineData("", @".\", "/Missing.dir")]
|
||||
public async Task NoMatch_PassesThrough_Windows(string baseUrl, string baseDir, string requestUrl)
|
||||
{
|
||||
await NoMatch_PassesThrough(baseUrl, baseDir, requestUrl);
|
||||
}
|
||||
|
||||
private async Task NoMatch_PassesThrough(string baseUrl, string baseDir, string requestUrl)
|
||||
{
|
||||
using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir)))
|
||||
{
|
||||
var server = StaticFilesTestServer.Create(
|
||||
app => app.UseDirectoryBrowser(new DirectoryBrowserOptions
|
||||
{
|
||||
RequestPath = new PathString(baseUrl),
|
||||
FileProvider = fileProvider
|
||||
}),
|
||||
services => services.AddDirectoryBrowser());
|
||||
var response = await server.CreateRequest(requestUrl).GetAsync();
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("", @".", "/")]
|
||||
[InlineData("", @".", "/SubFolder/")]
|
||||
[InlineData("/somedir", @".", "/somedir/")]
|
||||
[InlineData("/somedir", @"./", "/somedir/")]
|
||||
[InlineData("/somedir", @".", "/somedir/SubFolder/")]
|
||||
public async Task FoundDirectory_Served_All(string baseUrl, string baseDir, string requestUrl)
|
||||
{
|
||||
await FoundDirectory_Served(baseUrl, baseDir, requestUrl);
|
||||
}
|
||||
|
||||
[ConditionalTheory]
|
||||
[OSSkipCondition(OperatingSystems.Linux)]
|
||||
[OSSkipCondition(OperatingSystems.MacOSX)]
|
||||
[InlineData("/somedir", @".\", "/somedir/")]
|
||||
[InlineData("/somedir", @".", "/somedir/subFolder/")]
|
||||
public async Task FoundDirectory_Served_Windows(string baseUrl, string baseDir, string requestUrl)
|
||||
{
|
||||
await FoundDirectory_Served(baseUrl, baseDir, requestUrl);
|
||||
}
|
||||
|
||||
private async Task FoundDirectory_Served(string baseUrl, string baseDir, string requestUrl)
|
||||
{
|
||||
using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir)))
|
||||
{
|
||||
var server = StaticFilesTestServer.Create(
|
||||
app => app.UseDirectoryBrowser(new DirectoryBrowserOptions
|
||||
{
|
||||
RequestPath = new PathString(baseUrl),
|
||||
FileProvider = fileProvider
|
||||
}),
|
||||
services => services.AddDirectoryBrowser());
|
||||
var response = await server.CreateRequest(requestUrl).GetAsync();
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType.ToString());
|
||||
Assert.True(response.Content.Headers.ContentLength > 0);
|
||||
Assert.Equal(response.Content.Headers.ContentLength, (await response.Content.ReadAsByteArrayAsync()).Length);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("", @".", "/SubFolder", "")]
|
||||
[InlineData("/somedir", @".", "/somedir", "")]
|
||||
[InlineData("/somedir", @".", "/somedir/SubFolder", "")]
|
||||
[InlineData("", @".", "/SubFolder", "?a=b")]
|
||||
[InlineData("/somedir", @".", "/somedir", "?a=b")]
|
||||
[InlineData("/somedir", @".", "/somedir/SubFolder", "?a=b")]
|
||||
public async Task NearMatch_RedirectAddSlash_All(string baseUrl, string baseDir, string requestUrl, string queryString)
|
||||
{
|
||||
await NearMatch_RedirectAddSlash(baseUrl, baseDir, requestUrl, queryString);
|
||||
}
|
||||
|
||||
[ConditionalTheory]
|
||||
[OSSkipCondition(OperatingSystems.Linux)]
|
||||
[OSSkipCondition(OperatingSystems.MacOSX)]
|
||||
[InlineData("/somedir", @".", "/somedir/subFolder", "")]
|
||||
[InlineData("/somedir", @".", "/somedir/subFolder", "?a=b")]
|
||||
public async Task NearMatch_RedirectAddSlash_Windows(string baseUrl, string baseDir, string requestUrl, string queryString)
|
||||
{
|
||||
await NearMatch_RedirectAddSlash(baseUrl, baseDir, requestUrl, queryString);
|
||||
}
|
||||
|
||||
private async Task NearMatch_RedirectAddSlash(string baseUrl, string baseDir, string requestUrl, string queryString)
|
||||
{
|
||||
using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir)))
|
||||
{
|
||||
var server = StaticFilesTestServer.Create(
|
||||
app => app.UseDirectoryBrowser(new DirectoryBrowserOptions
|
||||
{
|
||||
RequestPath = new PathString(baseUrl),
|
||||
FileProvider = fileProvider
|
||||
}),
|
||||
services => services.AddDirectoryBrowser());
|
||||
|
||||
var response = await server.CreateRequest(requestUrl + queryString).GetAsync();
|
||||
|
||||
Assert.Equal(HttpStatusCode.Moved, response.StatusCode);
|
||||
Assert.Equal(requestUrl + "/" + queryString, response.Headers.GetValues("Location").FirstOrDefault());
|
||||
Assert.Empty((await response.Content.ReadAsByteArrayAsync()));
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("", @".", "/")]
|
||||
[InlineData("", @".", "/SubFolder/")]
|
||||
[InlineData("/somedir", @".", "/somedir/")]
|
||||
[InlineData("/somedir", @".", "/somedir/SubFolder/")]
|
||||
public async Task PostDirectory_PassesThrough_All(string baseUrl, string baseDir, string requestUrl)
|
||||
{
|
||||
await PostDirectory_PassesThrough(baseUrl, baseDir, requestUrl);
|
||||
}
|
||||
|
||||
[ConditionalTheory]
|
||||
[OSSkipCondition(OperatingSystems.Linux)]
|
||||
[OSSkipCondition(OperatingSystems.MacOSX)]
|
||||
[InlineData("/somedir", @".", "/somedir/subFolder/")]
|
||||
public async Task PostDirectory_PassesThrough_Windows(string baseUrl, string baseDir, string requestUrl)
|
||||
{
|
||||
await PostDirectory_PassesThrough(baseUrl, baseDir, requestUrl);
|
||||
}
|
||||
|
||||
private async Task PostDirectory_PassesThrough(string baseUrl, string baseDir, string requestUrl)
|
||||
{
|
||||
using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir)))
|
||||
{
|
||||
var server = StaticFilesTestServer.Create(
|
||||
app => app.UseDirectoryBrowser(new DirectoryBrowserOptions
|
||||
{
|
||||
RequestPath = new PathString(baseUrl),
|
||||
FileProvider = fileProvider
|
||||
}),
|
||||
services => services.AddDirectoryBrowser());
|
||||
|
||||
var response = await server.CreateRequest(requestUrl).PostAsync();
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("", @".", "/")]
|
||||
[InlineData("", @".", "/SubFolder/")]
|
||||
[InlineData("/somedir", @".", "/somedir/")]
|
||||
[InlineData("/somedir", @".", "/somedir/SubFolder/")]
|
||||
public async Task HeadDirectory_HeadersButNotBodyServed_All(string baseUrl, string baseDir, string requestUrl)
|
||||
{
|
||||
await HeadDirectory_HeadersButNotBodyServed(baseUrl, baseDir, requestUrl);
|
||||
}
|
||||
|
||||
[ConditionalTheory]
|
||||
[OSSkipCondition(OperatingSystems.Linux)]
|
||||
[OSSkipCondition(OperatingSystems.MacOSX)]
|
||||
[InlineData("/somedir", @".", "/somedir/subFolder/")]
|
||||
public async Task HeadDirectory_HeadersButNotBodyServed_Windows(string baseUrl, string baseDir, string requestUrl)
|
||||
{
|
||||
await HeadDirectory_HeadersButNotBodyServed(baseUrl, baseDir, requestUrl);
|
||||
}
|
||||
|
||||
private async Task HeadDirectory_HeadersButNotBodyServed(string baseUrl, string baseDir, string requestUrl)
|
||||
{
|
||||
using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir)))
|
||||
{
|
||||
var server = StaticFilesTestServer.Create(
|
||||
app => app.UseDirectoryBrowser(new DirectoryBrowserOptions
|
||||
{
|
||||
RequestPath = new PathString(baseUrl),
|
||||
FileProvider = fileProvider
|
||||
}),
|
||||
services => services.AddDirectoryBrowser());
|
||||
|
||||
var response = await server.CreateRequest(requestUrl).SendAsync("HEAD");
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType.ToString());
|
||||
Assert.True(response.Content.Headers.ContentLength == 0);
|
||||
Assert.Empty((await response.Content.ReadAsByteArrayAsync()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\shared\*.cs" />
|
||||
<Content Include="SubFolder\**\*;TestDocument.txt">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.StaticFiles\Microsoft.AspNetCore.StaticFiles.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="$(MicrosoftAspNetCoreTestHostPackageVersion)" />
|
||||
<PackageReference Include="Moq" Version="$(MoqPackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,431 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.StaticFiles
|
||||
{
|
||||
public class RangeHeaderTests
|
||||
{
|
||||
// 14.27 If-Range
|
||||
// If the entity tag given in the If-Range header matches the current entity tag for the entity, then the server SHOULD
|
||||
// provide the specified sub-range of the entity using a 206 (Partial content) response.
|
||||
[Fact]
|
||||
public async Task IfRangeWithCurrentEtagShouldServePartialContent()
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
HttpResponseMessage original = await server.CreateClient().GetAsync("http://localhost/SubFolder/ranges.txt");
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/ranges.txt");
|
||||
req.Headers.Add("If-Range", original.Headers.ETag.ToString());
|
||||
req.Headers.Add("Range", "bytes=0-10");
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.PartialContent, resp.StatusCode);
|
||||
Assert.Equal("bytes 0-10/62", resp.Content.Headers.ContentRange.ToString());
|
||||
Assert.Equal(11, resp.Content.Headers.ContentLength);
|
||||
Assert.Equal("0123456789a", await resp.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
// 14.27 If-Range
|
||||
// If the entity tag given in the If-Range header matches the current entity tag for the entity, then the server SHOULD
|
||||
// provide the specified sub-range of the entity using a 206 (Partial content) response.
|
||||
// HEAD requests should ignore the Range header
|
||||
[Fact]
|
||||
public async Task HEADIfRangeWithCurrentEtagShouldReturn200Ok()
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
HttpResponseMessage original = await server.CreateClient().GetAsync("http://localhost/SubFolder/ranges.txt");
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Head, "http://localhost/SubFolder/ranges.txt");
|
||||
req.Headers.Add("If-Range", original.Headers.ETag.ToString());
|
||||
req.Headers.Add("Range", "bytes=0-10");
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
|
||||
Assert.Equal(original.Headers.ETag, resp.Headers.ETag);
|
||||
Assert.Null(resp.Content.Headers.ContentRange);
|
||||
Assert.Equal(62, resp.Content.Headers.ContentLength);
|
||||
Assert.Equal(string.Empty, await resp.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
// 14.27 If-Range
|
||||
// If the client has no entity tag for an entity, but does have a Last- Modified date, it MAY use that date in an If-Range header.
|
||||
[Fact]
|
||||
public async Task IfRangeWithCurrentDateShouldServePartialContent()
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
HttpResponseMessage original = await server.CreateClient().GetAsync("http://localhost/SubFolder/ranges.txt");
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/ranges.txt");
|
||||
req.Headers.Add("If-Range", original.Content.Headers.LastModified.Value.ToString("r"));
|
||||
req.Headers.Add("Range", "bytes=0-10");
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.PartialContent, resp.StatusCode);
|
||||
Assert.Equal("bytes 0-10/62", resp.Content.Headers.ContentRange.ToString());
|
||||
Assert.Equal(11, resp.Content.Headers.ContentLength);
|
||||
Assert.Equal("0123456789a", await resp.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IfModifiedSinceWithPastDateShouldServePartialContent()
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
HttpResponseMessage original = await server.CreateClient().GetAsync("http://localhost/SubFolder/ranges.txt");
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/ranges.txt");
|
||||
req.Headers.Add("If-Modified-Since", original.Content.Headers.LastModified.Value.AddHours(-1).ToString("r"));
|
||||
req.Headers.Add("Range", "bytes=0-10");
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.PartialContent, resp.StatusCode);
|
||||
Assert.Equal("bytes 0-10/62", resp.Content.Headers.ContentRange.ToString());
|
||||
Assert.Equal(11, resp.Content.Headers.ContentLength);
|
||||
Assert.Equal("0123456789a", await resp.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IfModifiedSinceWithCurrentDateShouldReturn304()
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
HttpResponseMessage original = await server.CreateClient().GetAsync("http://localhost/SubFolder/ranges.txt");
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/ranges.txt");
|
||||
req.Headers.Add("If-Modified-Since", original.Content.Headers.LastModified.Value.ToString("r"));
|
||||
req.Headers.Add("Range", "bytes=0-10");
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.NotModified, resp.StatusCode);
|
||||
}
|
||||
|
||||
// 14.27 If-Range
|
||||
// If the client has no entity tag for an entity, but does have a Last- Modified date, it MAY use that date in an If-Range header.
|
||||
// HEAD requests should ignore the Range header
|
||||
[Fact]
|
||||
public async Task HEADIfRangeWithCurrentDateShouldReturn200Ok()
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
HttpResponseMessage original = await server.CreateClient().GetAsync("http://localhost/SubFolder/ranges.txt");
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Head, "http://localhost/SubFolder/ranges.txt");
|
||||
req.Headers.Add("If-Range", original.Content.Headers.LastModified.Value.ToString("r"));
|
||||
req.Headers.Add("Range", "bytes=0-10");
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
|
||||
Assert.Equal(original.Content.Headers.LastModified, resp.Content.Headers.LastModified);
|
||||
Assert.Null(resp.Content.Headers.ContentRange);
|
||||
Assert.Equal(62, resp.Content.Headers.ContentLength);
|
||||
Assert.Equal(string.Empty, await resp.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
// 14.27 If-Range
|
||||
// If the entity tag does not match, then the server SHOULD return the entire entity using a 200 (OK) response.
|
||||
[Fact]
|
||||
public async Task IfRangeWithOldEtagShouldServeFullContent()
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/ranges.txt");
|
||||
req.Headers.Add("If-Range", "\"OldEtag\"");
|
||||
req.Headers.Add("Range", "bytes=0-10");
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
|
||||
Assert.Null(resp.Content.Headers.ContentRange);
|
||||
Assert.Equal(62, resp.Content.Headers.ContentLength);
|
||||
Assert.Equal("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", await resp.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
// 14.27 If-Range
|
||||
// If the entity tag does not match, then the server SHOULD return the entire entity using a 200 (OK) response.
|
||||
[Fact]
|
||||
public async Task HEADIfRangeWithOldEtagShouldServeFullContent()
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
var req = new HttpRequestMessage(HttpMethod.Head, "http://localhost/SubFolder/ranges.txt");
|
||||
req.Headers.Add("If-Range", "\"OldEtag\"");
|
||||
req.Headers.Add("Range", "bytes=0-10");
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
|
||||
Assert.Null(resp.Content.Headers.ContentRange);
|
||||
Assert.Equal(62, resp.Content.Headers.ContentLength);
|
||||
Assert.Equal(string.Empty, await resp.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
// 14.27 If-Range
|
||||
// If the entity tag/date does not match, then the server SHOULD return the entire entity using a 200 (OK) response.
|
||||
[Fact]
|
||||
public async Task IfRangeWithOldDateShouldServeFullContent()
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
HttpResponseMessage original = await server.CreateClient().GetAsync("http://localhost/SubFolder/ranges.txt");
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/ranges.txt");
|
||||
req.Headers.Add("If-Range", original.Content.Headers.LastModified.Value.Subtract(TimeSpan.FromDays(1)).ToString("r"));
|
||||
req.Headers.Add("Range", "bytes=0-10");
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
|
||||
Assert.Null(resp.Content.Headers.ContentRange);
|
||||
Assert.Equal(62, resp.Content.Headers.ContentLength);
|
||||
Assert.Equal("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", await resp.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
// 14.27 If-Range
|
||||
// If the entity tag/date does not match, then the server SHOULD return the entire entity using a 200 (OK) response.
|
||||
[Fact]
|
||||
public async Task HEADIfRangeWithOldDateShouldServeFullContent()
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
HttpResponseMessage original = await server.CreateClient().GetAsync("http://localhost/SubFolder/ranges.txt");
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Head, "http://localhost/SubFolder/ranges.txt");
|
||||
req.Headers.Add("If-Range", original.Content.Headers.LastModified.Value.Subtract(TimeSpan.FromDays(1)).ToString("r"));
|
||||
req.Headers.Add("Range", "bytes=0-10");
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
|
||||
Assert.Null(resp.Content.Headers.ContentRange);
|
||||
Assert.Equal(62, resp.Content.Headers.ContentLength);
|
||||
Assert.Equal(string.Empty, await resp.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
// 14.27 If-Range
|
||||
// The If-Range header SHOULD only be used together with a Range header, and MUST be ignored if the request
|
||||
// does not include a Range header, or if the server does not support the sub-range operation.
|
||||
[Fact]
|
||||
public async Task IfRangeWithoutRangeShouldServeFullContent()
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
HttpResponseMessage original = await server.CreateClient().GetAsync("http://localhost/SubFolder/ranges.txt");
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/ranges.txt");
|
||||
req.Headers.Add("If-Range", original.Headers.ETag.ToString());
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
|
||||
Assert.Null(resp.Content.Headers.ContentRange);
|
||||
Assert.Equal(62, resp.Content.Headers.ContentLength);
|
||||
Assert.Equal("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", await resp.Content.ReadAsStringAsync());
|
||||
|
||||
req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/ranges.txt");
|
||||
req.Headers.Add("If-Range", original.Content.Headers.LastModified.Value.ToString("r"));
|
||||
resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
|
||||
Assert.Null(resp.Content.Headers.ContentRange);
|
||||
Assert.Equal(62, resp.Content.Headers.ContentLength);
|
||||
Assert.Equal("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", await resp.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
// 14.27 If-Range
|
||||
// The If-Range header SHOULD only be used together with a Range header, and MUST be ignored if the request
|
||||
// does not include a Range header, or if the server does not support the sub-range operation.
|
||||
[Fact]
|
||||
public async Task HEADIfRangeWithoutRangeShouldServeFullContent()
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
HttpResponseMessage original = await server.CreateClient().GetAsync("http://localhost/SubFolder/ranges.txt");
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Head, "http://localhost/SubFolder/ranges.txt");
|
||||
req.Headers.Add("If-Range", original.Headers.ETag.ToString());
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
|
||||
Assert.Null(resp.Content.Headers.ContentRange);
|
||||
Assert.Equal(62, resp.Content.Headers.ContentLength);
|
||||
Assert.Equal(string.Empty, await resp.Content.ReadAsStringAsync());
|
||||
|
||||
req = new HttpRequestMessage(HttpMethod.Head, "http://localhost/SubFolder/ranges.txt");
|
||||
req.Headers.Add("If-Range", original.Content.Headers.LastModified.Value.ToString("r"));
|
||||
resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
|
||||
Assert.Null(resp.Content.Headers.ContentRange);
|
||||
Assert.Equal(62, resp.Content.Headers.ContentLength);
|
||||
Assert.Equal(string.Empty, await resp.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
// 14.35 Range
|
||||
[Theory]
|
||||
[InlineData("0-0", "0-0", 1, "0")]
|
||||
[InlineData("0- 9", "0-9", 10, "0123456789")]
|
||||
[InlineData("0 -9", "0-9", 10, "0123456789")]
|
||||
[InlineData("0 - 9", "0-9", 10, "0123456789")]
|
||||
[InlineData("10-35", "10-35", 26, "abcdefghijklmnopqrstuvwxyz")]
|
||||
[InlineData("36-61", "36-61", 26, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")]
|
||||
[InlineData("36-", "36-61", 26, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")] // Last 26
|
||||
[InlineData("-26", "36-61", 26, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")] // Last 26
|
||||
[InlineData("0-", "0-61", 62, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")]
|
||||
[InlineData("-1001", "0-61", 62, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")]
|
||||
public async Task SingleValidRangeShouldServePartialContent(string range, string expectedRange, int length, string expectedData)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/ranges.txt");
|
||||
req.Headers.Add("Range", "bytes=" + range);
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.PartialContent, resp.StatusCode);
|
||||
Assert.NotNull(resp.Content.Headers.ContentRange);
|
||||
Assert.Equal("bytes " + expectedRange + "/62", resp.Content.Headers.ContentRange.ToString());
|
||||
Assert.Equal(length, resp.Content.Headers.ContentLength);
|
||||
Assert.Equal(expectedData, await resp.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("0-0", "0-0", 1, "A")]
|
||||
[InlineData("0-", "0-0", 1, "A")]
|
||||
[InlineData("-1", "0-0", 1, "A")]
|
||||
[InlineData("-2", "0-0", 1, "A")]
|
||||
[InlineData("0-1", "0-0", 1, "A")]
|
||||
[InlineData("0-2", "0-0", 1, "A")]
|
||||
public async Task SingleValidRangeShouldServePartialContentSingleByteFile(string range, string expectedRange, int length, string expectedData)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/SingleByte.txt");
|
||||
req.Headers.Add("Range", "bytes=" + range);
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.PartialContent, resp.StatusCode);
|
||||
Assert.NotNull(resp.Content.Headers.ContentRange);
|
||||
Assert.Equal("bytes " + expectedRange + "/1", resp.Content.Headers.ContentRange.ToString());
|
||||
Assert.Equal(length, resp.Content.Headers.ContentLength);
|
||||
Assert.Equal(expectedData, await resp.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("0-0")]
|
||||
[InlineData("0-")]
|
||||
[InlineData("-1")]
|
||||
[InlineData("-2")]
|
||||
[InlineData("0-1")]
|
||||
[InlineData("0-2")]
|
||||
public async Task SingleValidRangeShouldServeRequestedRangeNotSatisfiableEmptyFile(string range)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/Empty.txt");
|
||||
req.Headers.Add("Range", "bytes=" + range);
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, resp.StatusCode);
|
||||
}
|
||||
|
||||
// 14.35 Range
|
||||
// HEAD ignores range headers
|
||||
[Theory]
|
||||
[InlineData("10-35")]
|
||||
public async Task HEADSingleValidRangeShouldReturnOk(string range)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
var req = new HttpRequestMessage(HttpMethod.Head, "http://localhost/SubFolder/ranges.txt");
|
||||
req.Headers.Add("Range", "bytes=" + range);
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
|
||||
Assert.Null(resp.Content.Headers.ContentRange);
|
||||
Assert.Equal(62, resp.Content.Headers.ContentLength);
|
||||
Assert.Equal(string.Empty, await resp.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
// 14.35 Range
|
||||
[Theory]
|
||||
[InlineData("100-")] // Out of range
|
||||
[InlineData("1000-1001")] // Out of range
|
||||
[InlineData("-0")] // Suffix range must be non-zero
|
||||
public async Task SingleNotSatisfiableRange(string range)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/ranges.txt");
|
||||
req.Headers.TryAddWithoutValidation("Range", "bytes=" + range);
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, resp.StatusCode);
|
||||
Assert.Equal("bytes */62", resp.Content.Headers.ContentRange.ToString());
|
||||
}
|
||||
|
||||
// 14.35 Range
|
||||
// HEAD ignores range headers
|
||||
[Theory]
|
||||
[InlineData("1000-1001")] // Out of range
|
||||
public async Task HEADSingleNotSatisfiableRangeReturnsOk(string range)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
var req = new HttpRequestMessage(HttpMethod.Head, "http://localhost/SubFolder/ranges.txt");
|
||||
req.Headers.TryAddWithoutValidation("Range", "bytes=" + range);
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
|
||||
Assert.Null(resp.Content.Headers.ContentRange);
|
||||
}
|
||||
|
||||
// 14.35 Range
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("0")]
|
||||
[InlineData("1-0")]
|
||||
[InlineData("-")]
|
||||
[InlineData("a-")]
|
||||
[InlineData("-b")]
|
||||
[InlineData("a-b")]
|
||||
public async Task SingleInvalidRangeIgnored(string range)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/ranges.txt");
|
||||
req.Headers.TryAddWithoutValidation("Range", "bytes=" + range);
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
|
||||
Assert.Null(resp.Content.Headers.ContentRange);
|
||||
Assert.Equal(62, resp.Content.Headers.ContentLength);
|
||||
Assert.Equal("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", await resp.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
// 14.35 Range
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("0")]
|
||||
[InlineData("1-0")]
|
||||
[InlineData("-")]
|
||||
[InlineData("a-")]
|
||||
[InlineData("-b")]
|
||||
[InlineData("a-b")]
|
||||
public async Task HEADSingleInvalidRangeIgnored(string range)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
var req = new HttpRequestMessage(HttpMethod.Head, "http://localhost/SubFolder/ranges.txt");
|
||||
req.Headers.TryAddWithoutValidation("Range", "bytes=" + range);
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
|
||||
Assert.Null(resp.Content.Headers.ContentRange);
|
||||
Assert.Equal(62, resp.Content.Headers.ContentLength);
|
||||
Assert.Equal(string.Empty, await resp.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
// 14.35 Range
|
||||
[Theory]
|
||||
[InlineData("0-0,2-2")]
|
||||
[InlineData("0-0,60-")]
|
||||
[InlineData("0-0,-2")]
|
||||
[InlineData("2-2,0-0")]
|
||||
[InlineData("0-0,2-2,4-4,6-6,8-8")]
|
||||
[InlineData("0-0,6-6,8-8,2-2,4-4")]
|
||||
public async Task MultipleValidRangesShouldServeFullContent(string ranges)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/ranges.txt");
|
||||
req.Headers.Add("Range", "bytes=" + ranges);
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
|
||||
Assert.Equal("text/plain", resp.Content.Headers.ContentType.ToString());
|
||||
Assert.Null(resp.Content.Headers.ContentRange);
|
||||
Assert.Equal(62, resp.Content.Headers.ContentLength);
|
||||
Assert.Equal("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", await resp.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
// 14.35 Range
|
||||
[Theory]
|
||||
[InlineData("0-0,2-2")]
|
||||
[InlineData("0-0,60-")]
|
||||
[InlineData("0-0,-2")]
|
||||
[InlineData("2-2,0-0")] // SHOULD send in the requested order.
|
||||
public async Task HEADMultipleValidRangesShouldServeFullContent(string range)
|
||||
{
|
||||
TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer());
|
||||
var req = new HttpRequestMessage(HttpMethod.Head, "http://localhost/SubFolder/ranges.txt");
|
||||
req.Headers.Add("Range", "bytes=" + range);
|
||||
HttpResponseMessage resp = await server.CreateClient().SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
|
||||
Assert.Equal("text/plain", resp.Content.Headers.ContentType.ToString());
|
||||
Assert.Equal(string.Empty, await resp.Content.ReadAsStringAsync());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
// Copyright (c) .NET 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 Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.StaticFiles
|
||||
{
|
||||
public class StaticFileContextTest
|
||||
{
|
||||
[Fact]
|
||||
public void LookupFileInfo_ReturnsFalse_IfFileDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
var options = new StaticFileOptions();
|
||||
var context = new StaticFileContext(new DefaultHttpContext(), options, PathString.Empty, NullLogger.Instance, new TestFileProvider(), new FileExtensionContentTypeProvider());
|
||||
|
||||
// Act
|
||||
var validateResult = context.ValidatePath();
|
||||
var lookupResult = context.LookupFileInfo();
|
||||
|
||||
// Assert
|
||||
Assert.True(validateResult);
|
||||
Assert.False(lookupResult);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LookupFileInfo_ReturnsTrue_IfFileExists()
|
||||
{
|
||||
// Arrange
|
||||
var options = new StaticFileOptions();
|
||||
var fileProvider = new TestFileProvider();
|
||||
fileProvider.AddFile("/foo.txt", new TestFileInfo
|
||||
{
|
||||
LastModified = new DateTimeOffset(2014, 1, 2, 3, 4, 5, TimeSpan.Zero)
|
||||
});
|
||||
var pathString = new PathString("/test");
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.Path = new PathString("/test/foo.txt");
|
||||
var context = new StaticFileContext(httpContext, options, pathString, NullLogger.Instance, fileProvider, new FileExtensionContentTypeProvider());
|
||||
|
||||
// Act
|
||||
context.ValidatePath();
|
||||
var result = context.LookupFileInfo();
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
private sealed class TestFileProvider : IFileProvider
|
||||
{
|
||||
private readonly Dictionary<string, IFileInfo> _files = new Dictionary<string, IFileInfo>(StringComparer.Ordinal);
|
||||
|
||||
public void AddFile(string path, IFileInfo fileInfo)
|
||||
{
|
||||
_files[path] = fileInfo;
|
||||
}
|
||||
|
||||
public IDirectoryContents GetDirectoryContents(string subpath)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IFileInfo GetFileInfo(string subpath)
|
||||
{
|
||||
IFileInfo result;
|
||||
if (_files.TryGetValue(subpath, out result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return new NotFoundFileInfo();
|
||||
}
|
||||
|
||||
public IChangeToken Watch(string filter)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
private class NotFoundFileInfo : IFileInfo
|
||||
{
|
||||
public bool Exists
|
||||
{
|
||||
get
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public DateTimeOffset LastModified
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public long Length
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public string PhysicalPath
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public Stream CreateReadStream()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestFileInfo : IFileInfo
|
||||
{
|
||||
public bool Exists
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
public bool IsDirectory
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public DateTimeOffset LastModified { get; set; }
|
||||
|
||||
public long Length { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public string PhysicalPath { get; set; }
|
||||
|
||||
public Stream CreateReadStream()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
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.AspNetCore.Testing.xunit;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.StaticFiles
|
||||
{
|
||||
public class StaticFileMiddlewareTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ReturnsNotFoundWithoutWwwroot()
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app => app.UseStaticFiles());
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var response = await server.CreateClient().GetAsync("/ranges.txt");
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
Assert.Null(response.Headers.ETag);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
[OSSkipCondition(OperatingSystems.Windows, SkipReason = "Symlinks not supported on Windows")]
|
||||
public async Task ReturnsNotFoundForBrokenSymlink()
|
||||
{
|
||||
var badLink = Path.Combine(AppContext.BaseDirectory, Path.GetRandomFileName() + ".txt");
|
||||
|
||||
Process.Start("ln", $"-s \"/tmp/{Path.GetRandomFileName()}\" \"{badLink}\"").WaitForExit();
|
||||
Assert.True(File.Exists(badLink), "Should have created a symlink");
|
||||
|
||||
try
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app => app.UseStaticFiles(new StaticFileOptions { ServeUnknownFileTypes = true }))
|
||||
.UseWebRoot(AppContext.BaseDirectory);
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var response = await server.CreateClient().GetAsync(Path.GetFileName(badLink));
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
Assert.Null(response.Headers.ETag);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(badLink);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReturnsNotFoundIfSendFileThrows()
|
||||
{
|
||||
var mockSendFile = new Mock<IHttpSendFileFeature>();
|
||||
mockSendFile.Setup(m => m.SendFileAsync(It.IsAny<string>(), It.IsAny<long>(), It.IsAny<long?>(), It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new FileNotFoundException());
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.Use(async (ctx, next) =>
|
||||
{
|
||||
ctx.Features.Set(mockSendFile.Object);
|
||||
await next();
|
||||
});
|
||||
app.UseStaticFiles(new StaticFileOptions { ServeUnknownFileTypes = true });
|
||||
})
|
||||
.UseWebRoot(AppContext.BaseDirectory);
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var response = await server.CreateClient().GetAsync("TestDocument.txt");
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
Assert.Null(response.Headers.ETag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FoundFile_LastModifiedTrimsSeconds()
|
||||
{
|
||||
using (var fileProvider = new PhysicalFileProvider(AppContext.BaseDirectory))
|
||||
{
|
||||
var server = StaticFilesTestServer.Create(app => app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
FileProvider = fileProvider
|
||||
}));
|
||||
var fileInfo = fileProvider.GetFileInfo("TestDocument.txt");
|
||||
var response = await server.CreateRequest("TestDocument.txt").GetAsync();
|
||||
|
||||
var last = fileInfo.LastModified;
|
||||
var trimed = new DateTimeOffset(last.Year, last.Month, last.Day, last.Hour, last.Minute, last.Second, last.Offset).ToUniversalTime();
|
||||
|
||||
Assert.Equal(response.Content.Headers.LastModified.Value, trimed);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NullArguments()
|
||||
{
|
||||
// No exception, default provided
|
||||
StaticFilesTestServer.Create(app => app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = null }));
|
||||
|
||||
// No exception, default provided
|
||||
StaticFilesTestServer.Create(app => app.UseStaticFiles(new StaticFileOptions { FileProvider = null }));
|
||||
|
||||
// PathString(null) is OK.
|
||||
var server = StaticFilesTestServer.Create(app => app.UseStaticFiles((string)null));
|
||||
var response = await server.CreateClient().GetAsync("/");
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir)))
|
||||
{
|
||||
var server = StaticFilesTestServer.Create(app => app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
RequestPath = new PathString(baseUrl),
|
||||
FileProvider = fileProvider
|
||||
}));
|
||||
var fileInfo = fileProvider.GetFileInfo(Path.GetFileName(requestUrl));
|
||||
var response = await server.CreateRequest(requestUrl).GetAsync();
|
||||
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);
|
||||
Assert.NotNull(response.Headers.ETag);
|
||||
|
||||
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)
|
||||
{
|
||||
using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir)))
|
||||
{
|
||||
var server = StaticFilesTestServer.Create(app => app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
RequestPath = new PathString(baseUrl),
|
||||
FileProvider = fileProvider
|
||||
}));
|
||||
var fileInfo = fileProvider.GetFileInfo(Path.GetFileName(requestUrl));
|
||||
var response = await server.CreateRequest(requestUrl).SendAsync("HEAD");
|
||||
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(MissingFiles))]
|
||||
public async Task Get_NoMatch_PassesThrough(string baseUrl, string baseDir, string requestUrl) =>
|
||||
await PassesThrough("GET", baseUrl, baseDir, requestUrl);
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(MissingFiles))]
|
||||
public async Task Head_NoMatch_PassesThrough(string baseUrl, string baseDir, string requestUrl) =>
|
||||
await PassesThrough("HEAD", baseUrl, baseDir, requestUrl);
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(MissingFiles))]
|
||||
public async Task Unknown_NoMatch_PassesThrough(string baseUrl, string baseDir, string requestUrl) =>
|
||||
await PassesThrough("VERB", baseUrl, baseDir, requestUrl);
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ExistingFiles))]
|
||||
public async Task Options_Match_PassesThrough(string baseUrl, string baseDir, string requestUrl) =>
|
||||
await PassesThrough("OPTIONS", baseUrl, baseDir, requestUrl);
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ExistingFiles))]
|
||||
public async Task Trace_Match_PassesThrough(string baseUrl, string baseDir, string requestUrl) =>
|
||||
await PassesThrough("TRACE", baseUrl, baseDir, requestUrl);
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ExistingFiles))]
|
||||
public async Task Post_Match_PassesThrough(string baseUrl, string baseDir, string requestUrl) =>
|
||||
await PassesThrough("POST", baseUrl, baseDir, requestUrl);
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ExistingFiles))]
|
||||
public async Task Put_Match_PassesThrough(string baseUrl, string baseDir, string requestUrl) =>
|
||||
await PassesThrough("PUT", baseUrl, baseDir, requestUrl);
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ExistingFiles))]
|
||||
public async Task Unknown_Match_PassesThrough(string baseUrl, string baseDir, string requestUrl) =>
|
||||
await PassesThrough("VERB", baseUrl, baseDir, requestUrl);
|
||||
|
||||
private async Task PassesThrough(string method, string baseUrl, string baseDir, string requestUrl)
|
||||
{
|
||||
using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir)))
|
||||
{
|
||||
var server = StaticFilesTestServer.Create(app => app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
RequestPath = new PathString(baseUrl),
|
||||
FileProvider = fileProvider
|
||||
}));
|
||||
var response = await server.CreateRequest(requestUrl).SendAsync(method);
|
||||
Assert.Null(response.Content.Headers.LastModified);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> MissingFiles => new[]
|
||||
{
|
||||
new[] {"", @".", "/missing.file"},
|
||||
new[] {"/subdir", @".", "/subdir/missing.file"},
|
||||
new[] {"/missing.file", @"./", "/missing.file"},
|
||||
new[] {"", @"./", "/xunit.xml"}
|
||||
};
|
||||
|
||||
public static IEnumerable<object[]> ExistingFiles => new[]
|
||||
{
|
||||
new[] {"", @".", "/TestDocument.txt"},
|
||||
new[] {"/somedir", @".", "/somedir/TestDocument.txt"},
|
||||
new[] {"/SomeDir", @".", "/soMediR/TestDocument.txt"},
|
||||
new[] {"", @"SubFolder", "/ranges.txt"},
|
||||
new[] {"/somedir", @"SubFolder", "/somedir/ranges.txt"},
|
||||
new[] {"", @"SubFolder", "/Empty.txt"}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.StaticFiles
|
||||
{
|
||||
public static class StaticFilesTestServer
|
||||
{
|
||||
public static TestServer Create(Action<IApplicationBuilder> configureApp, Action<IServiceCollection> configureServices = null)
|
||||
{
|
||||
Action<IServiceCollection> defaultConfigureServices = services => { };
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new []
|
||||
{
|
||||
new KeyValuePair<string, string>("webroot", ".")
|
||||
})
|
||||
.Build();
|
||||
var builder = new WebHostBuilder()
|
||||
.UseConfiguration(configuration)
|
||||
.Configure(configureApp)
|
||||
.ConfigureServices(configureServices ?? defaultConfigureServices);
|
||||
return new TestServer(builder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
A
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
Hello World
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1 @@
|
|||
<xml/>
|
||||
|
|
@ -0,0 +1 @@
|
|||
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
|
|
@ -0,0 +1 @@
|
|||
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<Project>
|
||||
<PropertyGroup>
|
||||
<VersionPrefix>2.1.1</VersionPrefix>
|
||||
<VersionSuffix>rtm</VersionSuffix>
|
||||
<PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' == 'rtm' ">$(VersionPrefix)</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' != 'rtm' ">$(VersionPrefix)-$(VersionSuffix)-final</PackageVersion>
|
||||
<BuildNumber Condition="'$(BuildNumber)' == ''">t000</BuildNumber>
|
||||
<FeatureBranchVersionPrefix Condition="'$(FeatureBranchVersionPrefix)' == ''">a-</FeatureBranchVersionPrefix>
|
||||
<VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(FeatureBranchVersionSuffix)' != ''">$(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-'))</VersionSuffix>
|
||||
<VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(BuildNumber)' != ''">$(VersionSuffix)-$(BuildNumber)</VersionSuffix>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
Loading…
Reference in New Issue