Merge remote-tracking branch 'StaticFiles/rybrande/release21ToSrc' into rybrande/Mondo2.1

This commit is contained in:
Ryan Brandenburg 2018-11-21 10:05:32 -08:00
commit 93238389a7
73 changed files with 6533 additions and 0 deletions

31
src/StaticFiles/.gitignore vendored Normal file
View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,13 @@
{
"adx-nonshipping": {
"rules": [],
"packages": {
"Microsoft.AspNetCore.RangeHelper.Sources": {}
}
},
"Default": {
"rules": [
"DefaultCompositeRule"
]
}
}

10
src/StaticFiles/README.md Normal file
View File

@ -0,0 +1,10 @@
StaticFiles
===========
AppVeyor: [![AppVeyor](https://ci.appveyor.com/api/projects/status/ibwhfogib5key90k/branch/dev?svg=true)](https://ci.appveyor.com/project/aspnetci/StaticFiles/branch/dev)
Travis: [![Travis](https://travis-ci.org/aspnet/StaticFiles.svg?branch=dev)](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.

View File

@ -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.

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"
}
}
}
}

View File

@ -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();
}
}
}

View File

@ -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>

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
A static HTML file.
</body>
</html>

View File

@ -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);
}
}
}

View File

@ -0,0 +1,7 @@
<Project>
<Import Project="..\Directory.Build.props" />
<ItemGroup>
<PackageReference Include="Internal.AspNetCore.Sdk" PrivateAssets="All" Version="$(InternalAspNetCoreSdkPackageVersion)" />
</ItemGroup>
</Project>

View File

@ -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;
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8" ?>
<Dictionary>
<Words>
<Recognized>
<Word>Owin</Word>
</Recognized>
</Words>
</Dictionary>

View File

@ -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));
}
}
}

View File

@ -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);
}
}
}

View File

@ -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; }
}
}

View File

@ -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));
}
}
}

View File

@ -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;
}
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}
}

View File

@ -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);
}
}
}

View File

@ -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>

View File

@ -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")]

View 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;
}
}
}

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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));
}
}
}

View File

@ -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);
}
}
}

View File

@ -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; }
}
}

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -0,0 +1 @@
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ

View File

@ -0,0 +1 @@
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ

File diff suppressed because one or more lines are too long

View File

@ -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") }
};
}
}

View File

@ -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));
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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()));
}
}
}
}

View File

@ -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>

View File

@ -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());
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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"}
};
}
}

View File

@ -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);
}
}
}

View File

@ -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>

View File

@ -0,0 +1 @@
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ

View File

@ -0,0 +1 @@
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ

View File

@ -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>