├── test ├── Microsoft.AspNetCore.StaticFiles.Tests │ ├── SubFolder │ │ ├── Empty.txt │ │ ├── SingleByte.txt │ │ ├── extra.xml │ │ ├── ranges.txt │ │ ├── default.html │ │ └── 你好 │ │ │ ├── default.html │ │ │ └── 世界 │ │ │ └── default.html │ ├── TestDocument.txt │ ├── Microsoft.AspNetCore.StaticFiles.Tests.csproj │ ├── StaticFilesTestServer.cs │ ├── DefaultContentTypeProviderTests.cs │ ├── StaticFileContextTest.cs │ ├── DefaultFilesMiddlewareTests.cs │ ├── DirectoryBrowserMiddlewareTests.cs │ └── StaticFileMiddlewareTests.cs ├── Microsoft.AspNetCore.StaticFiles.FunctionalTests │ ├── SubFolder │ │ ├── Empty.txt │ │ ├── SingleByte.txt │ │ ├── extra.xml │ │ ├── ranges.txt │ │ └── default.html │ ├── TestDocument.txt │ ├── Microsoft.AspNetCore.StaticFiles.FunctionalTests.csproj │ └── StaticFileMiddlewareTests.cs ├── Microsoft.AspNetCore.RangeHelper.Sources.Test │ ├── Microsoft.AspNetCore.RangeHelper.Sources.Test.csproj │ └── RangeHelperTests.cs └── Directory.Build.props ├── korebuild-lock.txt ├── korebuild.json ├── CONTRIBUTING.md ├── version.props ├── .github └── ISSUE_TEMPLATE.md ├── samples └── StaticFileSample │ ├── wwwroot │ └── htmlpage.html │ ├── Properties │ └── launchSettings.json │ ├── StaticFileSample.csproj │ └── Startup.cs ├── NuGet.config ├── src ├── Microsoft.AspNetCore.StaticFiles │ ├── CustomDictionary.xml │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── IContentTypeProvider.cs │ ├── Constants.cs │ ├── IDirectoryFormatter.cs │ ├── DirectoryBrowserServiceExtensions.cs │ ├── DirectoryBrowserOptions.cs │ ├── Microsoft.AspNetCore.StaticFiles.csproj │ ├── Infrastructure │ │ ├── SharedOptions.cs │ │ └── SharedOptionsBase.cs │ ├── DefaultFilesOptions.cs │ ├── StaticFileResponseContext.cs │ ├── Helpers.cs │ ├── FileServerOptions.cs │ ├── StaticFileOptions.cs │ ├── StaticFileExtensions.cs │ ├── DefaultFilesExtensions.cs │ ├── DirectoryBrowserExtensions.cs │ ├── FileServerExtensions.cs │ ├── Resources.Designer.cs │ ├── DefaultFilesMiddleware.cs │ ├── DirectoryBrowserMiddleware.cs │ ├── StaticFileMiddleware.cs │ ├── HtmlDirectoryFormatter.cs │ ├── Resources.resx │ ├── LoggerExtensions.cs │ └── StaticFileContext.cs └── Directory.Build.props ├── run.cmd ├── NuGetPackageVerifier.json ├── .vsts-pipelines └── builds │ ├── ci-internal.yml │ └── ci-public.yml ├── .appveyor.yml ├── .gitignore ├── README.md ├── .travis.yml ├── Directory.Build.targets ├── Directory.Build.props ├── .gitattributes ├── shared └── Microsoft.AspNetCore.RangeHelper.Sources │ └── RangeHelper.cs ├── run.ps1 ├── StaticFiles.sln ├── run.sh └── LICENSE.txt /test/Microsoft.AspNetCore.StaticFiles.Tests/SubFolder/Empty.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.StaticFiles.FunctionalTests/SubFolder/Empty.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.StaticFiles.Tests/SubFolder/SingleByte.txt: -------------------------------------------------------------------------------- 1 | A -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.StaticFiles.Tests/SubFolder/extra.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.StaticFiles.FunctionalTests/SubFolder/SingleByte.txt: -------------------------------------------------------------------------------- 1 | A -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.StaticFiles.FunctionalTests/SubFolder/extra.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /korebuild-lock.txt: -------------------------------------------------------------------------------- 1 | version:3.0.0-alpha1-20181011.3 2 | commithash:e7569d931e994629267ab2646e9926140962b4ac 3 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.StaticFiles.Tests/TestDocument.txt: -------------------------------------------------------------------------------- 1 | 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.StaticFiles.Tests/SubFolder/ranges.txt: -------------------------------------------------------------------------------- 1 | 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.StaticFiles.FunctionalTests/TestDocument.txt: -------------------------------------------------------------------------------- 1 | 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.StaticFiles.FunctionalTests/SubFolder/ranges.txt: -------------------------------------------------------------------------------- 1 | 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ -------------------------------------------------------------------------------- /korebuild.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/master/tools/korebuild.schema.json", 3 | "channel": "master" 4 | } 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ====== 3 | 4 | Information on contributing to this repo is in the [Contributing Guide](https://github.com/aspnet/Home/blob/master/CONTRIBUTING.md) in the Home repo. 5 | -------------------------------------------------------------------------------- /version.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 3.0.0 4 | dev 5 | 6 | 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | THIS ISSUE TRACKER IS CLOSED - please log new issues here: https://github.com/aspnet/Home/issues 2 | 3 | For information about this change, see https://github.com/aspnet/Announcements/issues/283 4 | -------------------------------------------------------------------------------- /samples/StaticFileSample/wwwroot/htmlpage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A static HTML file. 10 | 11 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/CustomDictionary.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Owin 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /run.cmd: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0run.ps1' %*; exit $LASTEXITCODE" 3 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NuGetPackageVerifier.json: -------------------------------------------------------------------------------- 1 | { 2 | "adx-nonshipping": { 3 | "rules": [], 4 | "packages": { 5 | "Microsoft.AspNetCore.RangeHelper.Sources": {} 6 | } 7 | }, 8 | "Default": { 9 | "rules": [ 10 | "DefaultCompositeRule" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.StaticFiles.Tests/SubFolder/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Hello World 10 | 11 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.StaticFiles.Tests/SubFolder/你好/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Hello World 10 | 11 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.StaticFiles.Tests/SubFolder/你好/世界/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Hello World 10 | 11 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.StaticFiles.FunctionalTests/SubFolder/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Hello World 10 | 11 | -------------------------------------------------------------------------------- /.vsts-pipelines/builds/ci-internal.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | - release/* 4 | 5 | resources: 6 | repositories: 7 | - repository: buildtools 8 | type: git 9 | name: aspnet-BuildTools 10 | ref: refs/heads/master 11 | 12 | phases: 13 | - template: .vsts-pipelines/templates/project-ci.yml@buildtools 14 | -------------------------------------------------------------------------------- /.appveyor.yml: -------------------------------------------------------------------------------- 1 | init: 2 | - git config --global core.autocrlf true 3 | branches: 4 | only: 5 | - master 6 | - /^release\/.*$/ 7 | - /^(.*\/)?ci-.*$/ 8 | build_script: 9 | - ps: .\run.ps1 default-build 10 | clone_depth: 1 11 | environment: 12 | global: 13 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 14 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 15 | test: 'off' 16 | deploy: 'off' 17 | os: Visual Studio 2017 18 | -------------------------------------------------------------------------------- /.vsts-pipelines/builds/ci-public.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | - release/* 4 | 5 | # See https://github.com/aspnet/BuildTools 6 | resources: 7 | repositories: 8 | - repository: buildtools 9 | type: github 10 | endpoint: DotNet-Bot GitHub Connection 11 | name: aspnet/BuildTools 12 | ref: refs/heads/master 13 | 14 | phases: 15 | - template: .vsts-pipelines/templates/project-ci.yml@buildtools 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | [Oo]bj/ 2 | [Bb]in/ 3 | TestResults/ 4 | .nuget/ 5 | _ReSharper.*/ 6 | packages/ 7 | artifacts/ 8 | PublishProfiles/ 9 | *.user 10 | *.suo 11 | *.cache 12 | *.docstates 13 | _ReSharper.* 14 | nuget.exe 15 | *net45.csproj 16 | *net451.csproj 17 | *k10.csproj 18 | *.psess 19 | *.vsp 20 | *.pidb 21 | *.userprefs 22 | *DS_Store 23 | *.ncrunchsolution 24 | *.*sdf 25 | *.ipch 26 | *.sln.ide 27 | project.lock.json 28 | .build/ 29 | .testPublish/ 30 | /.vs/ 31 | global.json 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | StaticFiles [Archived] 2 | ====================== 3 | 4 | **This GitHub project has been archived.** Ongoing development on this project can be found in . 5 | 6 | This repo contains middleware for handling requests for file system resources including files and directories. 7 | 8 | This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [AspNetCore](https://github.com/aspnet/AspNetCore) repo. 9 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Runtime.CompilerServices; 5 | 6 | [assembly: InternalsVisibleTo("Microsoft.AspNetCore.StaticFiles.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | sudo: false 3 | dist: trusty 4 | env: 5 | global: 6 | - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 7 | - DOTNET_CLI_TELEMETRY_OPTOUT: 1 8 | mono: none 9 | os: 10 | - linux 11 | - osx 12 | osx_image: xcode8.2 13 | addons: 14 | apt: 15 | packages: 16 | - libunwind8 17 | branches: 18 | only: 19 | - master 20 | - /^release\/.*$/ 21 | - /^(.*\/)?ci-.*$/ 22 | before_install: 23 | - if test "$TRAVIS_OS_NAME" == "osx"; then brew update; brew install openssl; ln -s 24 | /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/; ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib 25 | /usr/local/lib/; fi 26 | script: 27 | - ./build.sh 28 | -------------------------------------------------------------------------------- /samples/StaticFileSample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:35192/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "StaticFileSample": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "launchUrl": "http://localhost:5000/", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /samples/StaticFileSample/StaticFileSample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.RangeHelper.Sources.Test/Microsoft.AspNetCore.RangeHelper.Sources.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(StandardTestTfms) 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/IContentTypeProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.AspNetCore.StaticFiles 5 | { 6 | /// 7 | /// Used to look up MIME types given a file path 8 | /// 9 | public interface IContentTypeProvider 10 | { 11 | /// 12 | /// Given a file path, determine the MIME type 13 | /// 14 | /// A file path 15 | /// The resulting MIME type 16 | /// True if MIME type could be determined 17 | bool TryGetContentType(string subpath, out string contentType); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(MicrosoftNETCoreApp20PackageVersion) 4 | $(MicrosoftNETCoreApp21PackageVersion) 5 | $(MicrosoftNETCoreApp22PackageVersion) 6 | $(NETStandardLibrary20PackageVersion) 7 | 8 | 99.9 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/Constants.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Threading.Tasks; 5 | 6 | namespace Microsoft.AspNetCore.StaticFiles 7 | { 8 | internal static class Constants 9 | { 10 | internal const string ServerCapabilitiesKey = "server.Capabilities"; 11 | internal const string SendFileVersionKey = "sendfile.Version"; 12 | internal const string SendFileVersion = "1.0"; 13 | 14 | internal const int Status200Ok = 200; 15 | internal const int Status206PartialContent = 206; 16 | internal const int Status304NotModified = 304; 17 | internal const int Status412PreconditionFailed = 412; 18 | internal const int Status416RangeNotSatisfiable = 416; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.StaticFiles.Tests/Microsoft.AspNetCore.StaticFiles.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(StandardTestTfms) 5 | 6 | 7 | 8 | 9 | 10 | PreserveNewest 11 | PreserveNewest 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/IDirectoryFormatter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.Extensions.FileProviders; 8 | 9 | namespace Microsoft.AspNetCore.StaticFiles 10 | { 11 | /// 12 | /// Generates the view for a directory 13 | /// 14 | public interface IDirectoryFormatter 15 | { 16 | /// 17 | /// Generates the view for a directory. 18 | /// Implementers should properly handle HEAD requests. 19 | /// Implementers should set all necessary response headers (e.g. Content-Type, Content-Length, etc.). 20 | /// 21 | Task GenerateContentAsync(HttpContext context, IEnumerable contents); 22 | } 23 | } -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Microsoft ASP.NET Core 12 | https://github.com/aspnet/StaticFiles 13 | git 14 | $(MSBuildThisFileDirectory) 15 | $(MSBuildThisFileDirectory)build\Key.snk 16 | true 17 | true 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.doc diff=astextplain 2 | *.DOC diff=astextplain 3 | *.docx diff=astextplain 4 | *.DOCX diff=astextplain 5 | *.dot diff=astextplain 6 | *.DOT diff=astextplain 7 | *.pdf diff=astextplain 8 | *.PDF diff=astextplain 9 | *.rtf diff=astextplain 10 | *.RTF diff=astextplain 11 | 12 | *.jpg binary 13 | *.png binary 14 | *.gif binary 15 | 16 | *.cs text=auto diff=csharp 17 | *.vb text=auto 18 | *.resx text=auto 19 | *.c text=auto 20 | *.cpp text=auto 21 | *.cxx text=auto 22 | *.h text=auto 23 | *.hxx text=auto 24 | *.py text=auto 25 | *.rb text=auto 26 | *.java text=auto 27 | *.html text=auto 28 | *.htm text=auto 29 | *.css text=auto 30 | *.scss text=auto 31 | *.sass text=auto 32 | *.less text=auto 33 | *.js text=auto 34 | *.lisp text=auto 35 | *.clj text=auto 36 | *.sql text=auto 37 | *.php text=auto 38 | *.lua text=auto 39 | *.m text=auto 40 | *.asm text=auto 41 | *.erl text=auto 42 | *.fs text=auto 43 | *.fsx text=auto 44 | *.hs text=auto 45 | 46 | *.csproj text=auto 47 | *.vbproj text=auto 48 | *.fsproj text=auto 49 | *.dbproj text=auto 50 | *.sln text=auto eol=crlf 51 | *.sh eol=lf 52 | -------------------------------------------------------------------------------- /test/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | netcoreapp2.2 6 | $(DeveloperBuildTestTfms) 7 | 8 | $(StandardTestTfms);net461 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/DirectoryBrowserServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace Microsoft.Extensions.DependencyInjection 7 | { 8 | /// 9 | /// Extension methods for adding directory browser services. 10 | /// 11 | public static class DirectoryBrowserServiceExtensions 12 | { 13 | /// 14 | /// Adds directory browser middleware services. 15 | /// 16 | /// The to add services to. 17 | /// The so that additional calls can be chained. 18 | public static IServiceCollection AddDirectoryBrowser(this IServiceCollection services) 19 | { 20 | if (services == null) 21 | { 22 | throw new ArgumentNullException(nameof(services)); 23 | } 24 | 25 | services.AddWebEncoders(); 26 | 27 | return services; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/DirectoryBrowserOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNetCore.StaticFiles; 5 | using Microsoft.AspNetCore.StaticFiles.Infrastructure; 6 | 7 | namespace Microsoft.AspNetCore.Builder 8 | { 9 | /// 10 | /// Directory browsing options 11 | /// 12 | public class DirectoryBrowserOptions : SharedOptionsBase 13 | { 14 | /// 15 | /// Enabled directory browsing for all request paths 16 | /// 17 | public DirectoryBrowserOptions() 18 | : this(new SharedOptions()) 19 | { 20 | } 21 | 22 | /// 23 | /// Enabled directory browsing all request paths 24 | /// 25 | /// 26 | public DirectoryBrowserOptions(SharedOptions sharedOptions) 27 | : base(sharedOptions) 28 | { 29 | } 30 | 31 | /// 32 | /// The component that generates the view. 33 | /// 34 | public IDirectoryFormatter Formatter { get; set; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /samples/StaticFileSample/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace StaticFilesSample 9 | { 10 | public class Startup 11 | { 12 | public void ConfigureServices(IServiceCollection services) 13 | { 14 | services.AddDirectoryBrowser(); 15 | } 16 | 17 | public void Configure(IApplicationBuilder app, IHostingEnvironment host) 18 | { 19 | Console.WriteLine("webroot: " + host.WebRootPath); 20 | 21 | app.UseFileServer(new FileServerOptions 22 | { 23 | EnableDirectoryBrowsing = true 24 | }); 25 | } 26 | 27 | public static void Main(string[] args) 28 | { 29 | var host = new WebHostBuilder() 30 | .ConfigureLogging(factory => 31 | { 32 | factory.AddFilter("Console", level => level >= LogLevel.Debug); 33 | factory.AddConsole(); 34 | }) 35 | .UseContentRoot(Directory.GetCurrentDirectory()) 36 | .UseKestrel() 37 | .UseIISIntegration() 38 | .UseStartup() 39 | .Build(); 40 | 41 | host.Run(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/Microsoft.AspNetCore.StaticFiles.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ASP.NET Core static files middleware. Includes middleware for serving static files, directory browsing, and default files. 5 | netstandard2.0 6 | $(NoWarn);CS1591 7 | true 8 | aspnetcore;staticfiles 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.StaticFiles.Tests/StaticFilesTestServer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Runtime.InteropServices; 7 | using Microsoft.AspNetCore.Builder; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.AspNetCore.TestHost; 10 | using Microsoft.Extensions.Configuration; 11 | using Microsoft.Extensions.DependencyInjection; 12 | 13 | namespace Microsoft.AspNetCore.StaticFiles 14 | { 15 | public static class StaticFilesTestServer 16 | { 17 | public static TestServer Create(Action configureApp, Action configureServices = null) 18 | { 19 | Action defaultConfigureServices = services => { }; 20 | var configuration = new ConfigurationBuilder() 21 | .AddInMemoryCollection(new [] 22 | { 23 | new KeyValuePair("webroot", ".") 24 | }) 25 | .Build(); 26 | var builder = new WebHostBuilder() 27 | .UseConfiguration(configuration) 28 | .Configure(configureApp) 29 | .ConfigureServices(configureServices ?? defaultConfigureServices); 30 | return new TestServer(builder); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/Infrastructure/SharedOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.FileProviders; 7 | 8 | namespace Microsoft.AspNetCore.StaticFiles.Infrastructure 9 | { 10 | /// 11 | /// Options common to several middleware components 12 | /// 13 | public class SharedOptions 14 | { 15 | private PathString _requestPath; 16 | 17 | /// 18 | /// Defaults to all request paths. 19 | /// 20 | public SharedOptions() 21 | { 22 | RequestPath = PathString.Empty; 23 | } 24 | 25 | /// 26 | /// The request path that maps to static resources 27 | /// 28 | public PathString RequestPath 29 | { 30 | get { return _requestPath; } 31 | set 32 | { 33 | if (value.HasValue && value.Value.EndsWith("/", StringComparison.Ordinal)) 34 | { 35 | throw new ArgumentException("Request path must not end in a slash"); 36 | } 37 | _requestPath = value; 38 | } 39 | } 40 | 41 | /// 42 | /// The file system used to locate resources 43 | /// 44 | public IFileProvider FileProvider { get; set; } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/DefaultFilesOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using Microsoft.AspNetCore.StaticFiles.Infrastructure; 6 | 7 | namespace Microsoft.AspNetCore.Builder 8 | { 9 | /// 10 | /// Options for selecting default file names. 11 | /// 12 | public class DefaultFilesOptions : SharedOptionsBase 13 | { 14 | /// 15 | /// Configuration for the DefaultFilesMiddleware. 16 | /// 17 | public DefaultFilesOptions() 18 | : this(new SharedOptions()) 19 | { 20 | } 21 | 22 | /// 23 | /// Configuration for the DefaultFilesMiddleware. 24 | /// 25 | /// 26 | public DefaultFilesOptions(SharedOptions sharedOptions) 27 | : base(sharedOptions) 28 | { 29 | // Prioritized list 30 | DefaultFileNames = new List 31 | { 32 | "default.htm", 33 | "default.html", 34 | "index.htm", 35 | "index.html", 36 | }; 37 | } 38 | 39 | /// 40 | /// An ordered list of file names to select by default. List length and ordering may affect performance. 41 | /// 42 | public IList DefaultFileNames { get; set; } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/StaticFileResponseContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.FileProviders; 7 | 8 | namespace Microsoft.AspNetCore.StaticFiles 9 | { 10 | /// 11 | /// Contains information about the request and the file that will be served in response. 12 | /// 13 | public class StaticFileResponseContext 14 | { 15 | [Obsolete("Use the constructor that passes in the HttpContext and IFileInfo parameters: StaticFileResponseContext(HttpContext context, IFileInfo file)", false)] 16 | public StaticFileResponseContext() 17 | { 18 | } 19 | 20 | public StaticFileResponseContext(HttpContext context, IFileInfo file) 21 | { 22 | if (file == null) 23 | { 24 | throw new ArgumentNullException(nameof(file)); 25 | } 26 | if (context == null) 27 | { 28 | throw new ArgumentNullException(nameof(context)); 29 | } 30 | Context = context; 31 | File = file; 32 | } 33 | 34 | /// 35 | /// The request and response information. 36 | /// 37 | public HttpContext Context { get; } 38 | 39 | /// 40 | /// The file to be served. 41 | /// 42 | public IFileInfo File { get; } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/Helpers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.Extensions.FileProviders; 8 | 9 | namespace Microsoft.AspNetCore.StaticFiles 10 | { 11 | internal static class Helpers 12 | { 13 | internal static IFileProvider ResolveFileProvider(IHostingEnvironment hostingEnv) 14 | { 15 | if (hostingEnv.WebRootFileProvider == null) { 16 | throw new InvalidOperationException("Missing FileProvider."); 17 | } 18 | return hostingEnv.WebRootFileProvider; 19 | } 20 | 21 | internal static bool IsGetOrHeadMethod(string method) 22 | { 23 | return HttpMethods.IsGet(method) || HttpMethods.IsHead(method); 24 | } 25 | 26 | internal static bool PathEndsInSlash(PathString path) 27 | { 28 | return path.Value.EndsWith("/", StringComparison.Ordinal); 29 | } 30 | 31 | internal static bool TryMatchPath(HttpContext context, PathString matchUrl, bool forDirectory, out PathString subpath) 32 | { 33 | var path = context.Request.Path; 34 | 35 | if (forDirectory && !PathEndsInSlash(path)) 36 | { 37 | path += new PathString("/"); 38 | } 39 | 40 | if (path.StartsWithSegments(matchUrl, out subpath)) 41 | { 42 | return true; 43 | } 44 | return false; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.StaticFiles.FunctionalTests/Microsoft.AspNetCore.StaticFiles.FunctionalTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(StandardTestTfms) 5 | 6 | 10 | true 11 | 12 | 13 | 14 | 15 | 16 | PreserveNewest 17 | 18 | 19 | PreserveNewest 20 | PreserveNewest 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/Infrastructure/SharedOptionsBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.FileProviders; 7 | 8 | namespace Microsoft.AspNetCore.StaticFiles.Infrastructure 9 | { 10 | /// 11 | /// Options common to several middleware components 12 | /// 13 | public abstract class SharedOptionsBase 14 | { 15 | /// 16 | /// Creates an new instance of the SharedOptionsBase. 17 | /// 18 | /// 19 | protected SharedOptionsBase(SharedOptions sharedOptions) 20 | { 21 | if (sharedOptions == null) 22 | { 23 | throw new ArgumentNullException(nameof(sharedOptions)); 24 | } 25 | 26 | SharedOptions = sharedOptions; 27 | } 28 | 29 | /// 30 | /// Options common to several middleware components 31 | /// 32 | protected SharedOptions SharedOptions { get; private set; } 33 | 34 | /// 35 | /// The relative request path that maps to static resources. 36 | /// 37 | public PathString RequestPath 38 | { 39 | get { return SharedOptions.RequestPath; } 40 | set { SharedOptions.RequestPath = value; } 41 | } 42 | 43 | /// 44 | /// The file system used to locate resources 45 | /// 46 | public IFileProvider FileProvider 47 | { 48 | get { return SharedOptions.FileProvider; } 49 | set { SharedOptions.FileProvider = value; } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/FileServerOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNetCore.StaticFiles.Infrastructure; 5 | 6 | namespace Microsoft.AspNetCore.Builder 7 | { 8 | /// 9 | /// Options for all of the static file middleware components 10 | /// 11 | public class FileServerOptions : SharedOptionsBase 12 | { 13 | /// 14 | /// Creates a combined options class for all of the static file middleware components. 15 | /// 16 | public FileServerOptions() 17 | : base(new SharedOptions()) 18 | { 19 | StaticFileOptions = new StaticFileOptions(SharedOptions); 20 | DirectoryBrowserOptions = new DirectoryBrowserOptions(SharedOptions); 21 | DefaultFilesOptions = new DefaultFilesOptions(SharedOptions); 22 | EnableDefaultFiles = true; 23 | } 24 | 25 | /// 26 | /// Options for configuring the StaticFileMiddleware. 27 | /// 28 | public StaticFileOptions StaticFileOptions { get; private set; } 29 | 30 | /// 31 | /// Options for configuring the DirectoryBrowserMiddleware. 32 | /// 33 | public DirectoryBrowserOptions DirectoryBrowserOptions { get; private set; } 34 | 35 | /// 36 | /// Options for configuring the DefaultFilesMiddleware. 37 | /// 38 | public DefaultFilesOptions DefaultFilesOptions { get; private set; } 39 | 40 | /// 41 | /// Directory browsing is disabled by default. 42 | /// 43 | public bool EnableDirectoryBrowsing { get; set; } 44 | 45 | /// 46 | /// Default files are enabled by default. 47 | /// 48 | public bool EnableDefaultFiles { get; set; } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/StaticFileOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.AspNetCore.StaticFiles; 6 | using Microsoft.AspNetCore.StaticFiles.Infrastructure; 7 | 8 | namespace Microsoft.AspNetCore.Builder 9 | { 10 | /// 11 | /// Options for serving static files 12 | /// 13 | public class StaticFileOptions : SharedOptionsBase 14 | { 15 | /// 16 | /// Defaults to all request paths 17 | /// 18 | public StaticFileOptions() : this(new SharedOptions()) 19 | { 20 | } 21 | 22 | /// 23 | /// Defaults to all request paths 24 | /// 25 | /// 26 | public StaticFileOptions(SharedOptions sharedOptions) : base(sharedOptions) 27 | { 28 | OnPrepareResponse = _ => { }; 29 | } 30 | 31 | /// 32 | /// Used to map files to content-types. 33 | /// 34 | public IContentTypeProvider ContentTypeProvider { get; set; } 35 | 36 | /// 37 | /// The default content type for a request if the ContentTypeProvider cannot determine one. 38 | /// None is provided by default, so the client must determine the format themselves. 39 | /// http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7 40 | /// 41 | public string DefaultContentType { get; set; } 42 | 43 | /// 44 | /// If the file is not a recognized content-type should it be served? 45 | /// Default: false. 46 | /// 47 | public bool ServeUnknownFileTypes { get; set; } 48 | 49 | /// 50 | /// Called after the status code and headers have been set, but before the body has been written. 51 | /// This can be used to add or change the response headers. 52 | /// 53 | public Action OnPrepareResponse { get; set; } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/StaticFileExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.StaticFiles; 7 | using Microsoft.Extensions.Options; 8 | 9 | namespace Microsoft.AspNetCore.Builder 10 | { 11 | /// 12 | /// Extension methods for the StaticFileMiddleware 13 | /// 14 | public static class StaticFileExtensions 15 | { 16 | /// 17 | /// Enables static file serving for the current request path 18 | /// 19 | /// 20 | /// 21 | public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app) 22 | { 23 | if (app == null) 24 | { 25 | throw new ArgumentNullException(nameof(app)); 26 | } 27 | 28 | return app.UseMiddleware(); 29 | } 30 | 31 | /// 32 | /// Enables static file serving for the given request path 33 | /// 34 | /// 35 | /// The relative request path. 36 | /// 37 | public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app, string requestPath) 38 | { 39 | if (app == null) 40 | { 41 | throw new ArgumentNullException(nameof(app)); 42 | } 43 | 44 | return app.UseStaticFiles(new StaticFileOptions 45 | { 46 | RequestPath = new PathString(requestPath) 47 | }); 48 | } 49 | 50 | /// 51 | /// Enables static file serving with the given options 52 | /// 53 | /// 54 | /// 55 | /// 56 | public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app, StaticFileOptions options) 57 | { 58 | if (app == null) 59 | { 60 | throw new ArgumentNullException(nameof(app)); 61 | } 62 | if (options == null) 63 | { 64 | throw new ArgumentNullException(nameof(options)); 65 | } 66 | 67 | return app.UseMiddleware(Options.Create(options)); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/DefaultFilesExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.StaticFiles; 7 | using Microsoft.Extensions.Options; 8 | 9 | namespace Microsoft.AspNetCore.Builder 10 | { 11 | /// 12 | /// Extension methods for the DefaultFilesMiddleware 13 | /// 14 | public static class DefaultFilesExtensions 15 | { 16 | /// 17 | /// Enables default file mapping on the current path 18 | /// 19 | /// 20 | /// 21 | public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app) 22 | { 23 | if (app == null) 24 | { 25 | throw new ArgumentNullException(nameof(app)); 26 | } 27 | 28 | return app.UseMiddleware(); 29 | } 30 | 31 | /// 32 | /// Enables default file mapping for the given request path 33 | /// 34 | /// 35 | /// The relative request path. 36 | /// 37 | public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app, string requestPath) 38 | { 39 | if (app == null) 40 | { 41 | throw new ArgumentNullException(nameof(app)); 42 | } 43 | 44 | return app.UseDefaultFiles(new DefaultFilesOptions 45 | { 46 | RequestPath = new PathString(requestPath) 47 | }); 48 | } 49 | 50 | /// 51 | /// Enables default file mapping with the given options 52 | /// 53 | /// 54 | /// 55 | /// 56 | public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app, DefaultFilesOptions options) 57 | { 58 | if (app == null) 59 | { 60 | throw new ArgumentNullException(nameof(app)); 61 | } 62 | if (options == null) 63 | { 64 | throw new ArgumentNullException(nameof(options)); 65 | } 66 | 67 | return app.UseMiddleware(Options.Create(options)); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/DirectoryBrowserExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.StaticFiles; 7 | using Microsoft.Extensions.Options; 8 | 9 | namespace Microsoft.AspNetCore.Builder 10 | { 11 | /// 12 | /// Extension methods for the DirectoryBrowserMiddleware 13 | /// 14 | public static class DirectoryBrowserExtensions 15 | { 16 | /// 17 | /// Enable directory browsing on the current path 18 | /// 19 | /// 20 | /// 21 | public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app) 22 | { 23 | if (app == null) 24 | { 25 | throw new ArgumentNullException(nameof(app)); 26 | } 27 | 28 | return app.UseMiddleware(); 29 | } 30 | 31 | /// 32 | /// Enables directory browsing for the given request path 33 | /// 34 | /// 35 | /// The relative request path. 36 | /// 37 | public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app, string requestPath) 38 | { 39 | if (app == null) 40 | { 41 | throw new ArgumentNullException(nameof(app)); 42 | } 43 | 44 | return app.UseDirectoryBrowser(new DirectoryBrowserOptions 45 | { 46 | RequestPath = new PathString(requestPath) 47 | }); 48 | } 49 | 50 | /// 51 | /// Enable directory browsing with the given options 52 | /// 53 | /// 54 | /// 55 | /// 56 | public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app, DirectoryBrowserOptions options) 57 | { 58 | if (app == null) 59 | { 60 | throw new ArgumentNullException(nameof(app)); 61 | } 62 | if (options == null) 63 | { 64 | throw new ArgumentNullException(nameof(options)); 65 | } 66 | 67 | return app.UseMiddleware(Options.Create(options)); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.StaticFiles.Tests/DefaultContentTypeProviderTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Xunit; 5 | 6 | namespace Microsoft.AspNetCore.StaticFiles 7 | { 8 | public class DefaultContentTypeProviderTests 9 | { 10 | [Fact] 11 | public void UnknownExtensionsReturnFalse() 12 | { 13 | var provider = new FileExtensionContentTypeProvider(); 14 | string contentType; 15 | Assert.False(provider.TryGetContentType("unknown.ext", out contentType)); 16 | } 17 | 18 | [Fact] 19 | public void KnownExtensionsReturnType() 20 | { 21 | var provider = new FileExtensionContentTypeProvider(); 22 | Assert.True(provider.TryGetContentType("known.txt", out var contentType)); 23 | Assert.Equal("text/plain", contentType); 24 | } 25 | 26 | [Fact] 27 | public void DoubleDottedExtensionsAreNotSupported() 28 | { 29 | var provider = new FileExtensionContentTypeProvider(); 30 | string contentType; 31 | Assert.False(provider.TryGetContentType("known.exe.config", out contentType)); 32 | } 33 | 34 | [Fact] 35 | public void DashedExtensionsShouldBeMatched() 36 | { 37 | var provider = new FileExtensionContentTypeProvider(); 38 | Assert.True(provider.TryGetContentType("known.dvr-ms", out var contentType)); 39 | Assert.Equal("video/x-ms-dvr", contentType); 40 | } 41 | 42 | [Fact] 43 | public void BothSlashFormatsAreUnderstood() 44 | { 45 | var provider = new FileExtensionContentTypeProvider(); 46 | Assert.True(provider.TryGetContentType(@"/first/example.txt", out var contentType)); 47 | Assert.Equal("text/plain", contentType); 48 | Assert.True(provider.TryGetContentType(@"\second\example.txt", out contentType)); 49 | Assert.Equal("text/plain", contentType); 50 | } 51 | 52 | [Fact] 53 | public void DotsInDirectoryAreIgnored() 54 | { 55 | var provider = new FileExtensionContentTypeProvider(); 56 | Assert.True(provider.TryGetContentType(@"/first.css/example.txt", out var contentType)); 57 | Assert.Equal("text/plain", contentType); 58 | Assert.True(provider.TryGetContentType(@"\second.css\example.txt", out contentType)); 59 | Assert.Equal("text/plain", contentType); 60 | } 61 | 62 | [Fact] 63 | public void InvalidCharactersAreIgnored() 64 | { 65 | var provider = new FileExtensionContentTypeProvider(); 66 | string contentType; 67 | Assert.True(provider.TryGetContentType($"{new string(System.IO.Path.GetInvalidPathChars())}.txt", out contentType)); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/FileServerExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.StaticFiles; 7 | 8 | namespace Microsoft.AspNetCore.Builder 9 | { 10 | /// 11 | /// Extension methods that combine all of the static file middleware components: 12 | /// Default files, directory browsing, send file, and static files 13 | /// 14 | public static class FileServerExtensions 15 | { 16 | /// 17 | /// Enable all static file middleware (except directory browsing) for the current request path in the current directory. 18 | /// 19 | /// 20 | /// 21 | public static IApplicationBuilder UseFileServer(this IApplicationBuilder app) 22 | { 23 | if (app == null) 24 | { 25 | throw new ArgumentNullException(nameof(app)); 26 | } 27 | 28 | return app.UseFileServer(new FileServerOptions()); 29 | } 30 | 31 | /// 32 | /// Enable all static file middleware on for the current request path in the current directory. 33 | /// 34 | /// 35 | /// Should directory browsing be enabled? 36 | /// 37 | public static IApplicationBuilder UseFileServer(this IApplicationBuilder app, bool enableDirectoryBrowsing) 38 | { 39 | if (app == null) 40 | { 41 | throw new ArgumentNullException(nameof(app)); 42 | } 43 | 44 | return app.UseFileServer(new FileServerOptions 45 | { 46 | EnableDirectoryBrowsing = enableDirectoryBrowsing 47 | }); 48 | } 49 | 50 | /// 51 | /// Enables all static file middleware (except directory browsing) for the given request path from the directory of the same name 52 | /// 53 | /// 54 | /// The relative request path. 55 | /// 56 | public static IApplicationBuilder UseFileServer(this IApplicationBuilder app, string requestPath) 57 | { 58 | if (app == null) 59 | { 60 | throw new ArgumentNullException(nameof(app)); 61 | } 62 | 63 | if (requestPath == null) 64 | { 65 | throw new ArgumentNullException(nameof(requestPath)); 66 | } 67 | 68 | return app.UseFileServer(new FileServerOptions 69 | { 70 | RequestPath = new PathString(requestPath) 71 | }); 72 | } 73 | 74 | /// 75 | /// Enable all static file middleware with the given options 76 | /// 77 | /// 78 | /// 79 | /// 80 | public static IApplicationBuilder UseFileServer(this IApplicationBuilder app, FileServerOptions options) 81 | { 82 | if (app == null) 83 | { 84 | throw new ArgumentNullException(nameof(app)); 85 | } 86 | if (options == null) 87 | { 88 | throw new ArgumentNullException(nameof(options)); 89 | } 90 | 91 | if (options.EnableDefaultFiles) 92 | { 93 | app.UseDefaultFiles(options.DefaultFilesOptions); 94 | } 95 | 96 | if (options.EnableDirectoryBrowsing) 97 | { 98 | app.UseDirectoryBrowser(options.DirectoryBrowserOptions); 99 | } 100 | 101 | return app.UseStaticFiles(options.StaticFileOptions); 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.RangeHelper.Sources.Test/RangeHelperTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.Extensions.Logging.Abstractions; 6 | using Microsoft.Net.Http.Headers; 7 | using Xunit; 8 | 9 | namespace Microsoft.AspNetCore.Internal 10 | { 11 | public class RangeHelperTests 12 | { 13 | [Theory] 14 | [InlineData(1, 2)] 15 | [InlineData(2, 3)] 16 | public void NormalizeRange_ReturnsNullWhenRangeStartEqualsOrGreaterThanLength(long start, long end) 17 | { 18 | // Arrange & Act 19 | var normalizedRange = RangeHelper.NormalizeRange(new RangeItemHeaderValue(start, end), 1); 20 | 21 | // Assert 22 | Assert.Null(normalizedRange); 23 | } 24 | 25 | [Fact] 26 | public void NormalizeRange_ReturnsNullWhenRangeEndEqualsZero() 27 | { 28 | // Arrange & Act 29 | var normalizedRange = RangeHelper.NormalizeRange(new RangeItemHeaderValue(null, 0), 1); 30 | 31 | // Assert 32 | Assert.Null(normalizedRange); 33 | } 34 | 35 | [Theory] 36 | [InlineData(0, null, 0, 2)] 37 | [InlineData(0, 0, 0, 0)] 38 | public void NormalizeRange_ReturnsNormalizedRange(long? start, long? end, long? normalizedStart, long? normalizedEnd) 39 | { 40 | // Arrange & Act 41 | var normalizedRange = RangeHelper.NormalizeRange(new RangeItemHeaderValue(start, end), 3); 42 | 43 | // Assert 44 | Assert.Equal(normalizedStart, normalizedRange.From); 45 | Assert.Equal(normalizedEnd, normalizedRange.To); 46 | } 47 | 48 | [Fact] 49 | public void NormalizeRange_ReturnsRangeWithNoChange() 50 | { 51 | // Arrange & Act 52 | var normalizedRange = RangeHelper.NormalizeRange(new RangeItemHeaderValue(1, 3), 4); 53 | 54 | // Assert 55 | Assert.Equal(1, normalizedRange.From); 56 | Assert.Equal(3, normalizedRange.To); 57 | } 58 | 59 | [Theory] 60 | [InlineData(null)] 61 | [InlineData("")] 62 | public void ParseRange_ReturnsNullWhenRangeHeaderNotProvided(string range) 63 | { 64 | // Arrange 65 | var httpContext = new DefaultHttpContext(); 66 | httpContext.Request.Headers[HeaderNames.Range] = range; 67 | 68 | // Act 69 | var (isRangeRequest, parsedRangeResult) = RangeHelper.ParseRange(httpContext, httpContext.Request.GetTypedHeaders(), 10, NullLogger.Instance); 70 | 71 | // Assert 72 | Assert.False(isRangeRequest); 73 | Assert.Null(parsedRangeResult); 74 | } 75 | 76 | [Theory] 77 | [InlineData("1-2, 3-4")] 78 | [InlineData("1-2, ")] 79 | public void ParseRange_ReturnsNullWhenMultipleRangesProvidedInRangeHeader(string range) 80 | { 81 | // Arrange 82 | var httpContext = new DefaultHttpContext(); 83 | httpContext.Request.Headers[HeaderNames.Range] = range; 84 | 85 | // Act 86 | var (isRangeRequest, parsedRangeResult) = RangeHelper.ParseRange(httpContext, httpContext.Request.GetTypedHeaders(), 10, NullLogger.Instance); 87 | 88 | // Assert 89 | Assert.False(isRangeRequest); 90 | Assert.Null(parsedRangeResult); 91 | } 92 | 93 | [Fact] 94 | public void ParseRange_ReturnsSingleRangeWhenInputValid() 95 | { 96 | // Arrange 97 | var httpContext = new DefaultHttpContext(); 98 | var range = new RangeHeaderValue(1, 2); 99 | httpContext.Request.Headers[HeaderNames.Range] = range.ToString(); 100 | 101 | // Act 102 | var (isRangeRequest, parsedRange) = RangeHelper.ParseRange(httpContext, httpContext.Request.GetTypedHeaders(), 4, NullLogger.Instance); 103 | 104 | // Assert 105 | Assert.True(isRangeRequest); 106 | Assert.Equal(1, parsedRange.From); 107 | Assert.Equal(2, parsedRange.To); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | namespace Microsoft.AspNetCore.StaticFiles 3 | { 4 | using System.Globalization; 5 | using System.Reflection; 6 | using System.Resources; 7 | 8 | internal static class Resources 9 | { 10 | private static readonly ResourceManager _resourceManager 11 | = new ResourceManager("Microsoft.AspNetCore.StaticFiles.Resources", typeof(Resources).GetTypeInfo().Assembly); 12 | 13 | /// 14 | /// No formatter provided. 15 | /// 16 | internal static string Args_NoFormatter 17 | { 18 | get { return GetString("Args_NoFormatter"); } 19 | } 20 | 21 | /// 22 | /// No formatter provided. 23 | /// 24 | internal static string FormatArgs_NoFormatter() 25 | { 26 | return GetString("Args_NoFormatter"); 27 | } 28 | 29 | /// 30 | /// Index of 31 | /// 32 | internal static string HtmlDir_IndexOf 33 | { 34 | get { return GetString("HtmlDir_IndexOf"); } 35 | } 36 | 37 | /// 38 | /// Index of 39 | /// 40 | internal static string FormatHtmlDir_IndexOf() 41 | { 42 | return GetString("HtmlDir_IndexOf"); 43 | } 44 | 45 | /// 46 | /// Last Modified 47 | /// 48 | internal static string HtmlDir_LastModified 49 | { 50 | get { return GetString("HtmlDir_LastModified"); } 51 | } 52 | 53 | /// 54 | /// Last Modified 55 | /// 56 | internal static string FormatHtmlDir_LastModified() 57 | { 58 | return GetString("HtmlDir_LastModified"); 59 | } 60 | 61 | /// 62 | /// Modified 63 | /// 64 | internal static string HtmlDir_Modified 65 | { 66 | get { return GetString("HtmlDir_Modified"); } 67 | } 68 | 69 | /// 70 | /// Modified 71 | /// 72 | internal static string FormatHtmlDir_Modified() 73 | { 74 | return GetString("HtmlDir_Modified"); 75 | } 76 | 77 | /// 78 | /// Name 79 | /// 80 | internal static string HtmlDir_Name 81 | { 82 | get { return GetString("HtmlDir_Name"); } 83 | } 84 | 85 | /// 86 | /// Name 87 | /// 88 | internal static string FormatHtmlDir_Name() 89 | { 90 | return GetString("HtmlDir_Name"); 91 | } 92 | 93 | /// 94 | /// Size 95 | /// 96 | internal static string HtmlDir_Size 97 | { 98 | get { return GetString("HtmlDir_Size"); } 99 | } 100 | 101 | /// 102 | /// Size 103 | /// 104 | internal static string FormatHtmlDir_Size() 105 | { 106 | return GetString("HtmlDir_Size"); 107 | } 108 | 109 | /// 110 | /// The list of files in the given directory. Column headers are listed in the first row. 111 | /// 112 | internal static string HtmlDir_TableSummary 113 | { 114 | get { return GetString("HtmlDir_TableSummary"); } 115 | } 116 | 117 | /// 118 | /// The list of files in the given directory. Column headers are listed in the first row. 119 | /// 120 | internal static string FormatHtmlDir_TableSummary() 121 | { 122 | return GetString("HtmlDir_TableSummary"); 123 | } 124 | 125 | private static string GetString(string name, params string[] formatterNames) 126 | { 127 | var value = _resourceManager.GetString(name); 128 | 129 | System.Diagnostics.Debug.Assert(value != null); 130 | 131 | if (formatterNames != null) 132 | { 133 | for (var i = 0; i < formatterNames.Length; i++) 134 | { 135 | value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); 136 | } 137 | } 138 | 139 | return value; 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/DefaultFilesMiddleware.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Builder; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.Extensions.FileProviders; 10 | using Microsoft.Extensions.Options; 11 | using Microsoft.Net.Http.Headers; 12 | 13 | namespace Microsoft.AspNetCore.StaticFiles 14 | { 15 | /// 16 | /// This examines a directory path and determines if there is a default file present. 17 | /// If so the file name is appended to the path and execution continues. 18 | /// Note we don't just serve the file because it may require interpretation. 19 | /// 20 | public class DefaultFilesMiddleware 21 | { 22 | private readonly DefaultFilesOptions _options; 23 | private readonly PathString _matchUrl; 24 | private readonly RequestDelegate _next; 25 | private readonly IFileProvider _fileProvider; 26 | 27 | /// 28 | /// Creates a new instance of the DefaultFilesMiddleware. 29 | /// 30 | /// The next middleware in the pipeline. 31 | /// The used by this middleware. 32 | /// The configuration options for this middleware. 33 | public DefaultFilesMiddleware(RequestDelegate next, IHostingEnvironment hostingEnv, IOptions options) 34 | { 35 | if (next == null) 36 | { 37 | throw new ArgumentNullException(nameof(next)); 38 | } 39 | 40 | if (hostingEnv == null) 41 | { 42 | throw new ArgumentNullException(nameof(hostingEnv)); 43 | } 44 | 45 | if (options == null) 46 | { 47 | throw new ArgumentNullException(nameof(options)); 48 | } 49 | 50 | _next = next; 51 | _options = options.Value; 52 | _fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv); 53 | _matchUrl = _options.RequestPath; 54 | } 55 | 56 | /// 57 | /// This examines the request to see if it matches a configured directory, and if there are any files with the 58 | /// configured default names in that directory. If so this will append the corresponding file name to the request 59 | /// path for a later middleware to handle. 60 | /// 61 | /// 62 | /// 63 | public Task Invoke(HttpContext context) 64 | { 65 | if (Helpers.IsGetOrHeadMethod(context.Request.Method) 66 | && Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out var subpath)) 67 | { 68 | var dirContents = _fileProvider.GetDirectoryContents(subpath.Value); 69 | if (dirContents.Exists) 70 | { 71 | // Check if any of our default files exist. 72 | for (int matchIndex = 0; matchIndex < _options.DefaultFileNames.Count; matchIndex++) 73 | { 74 | string defaultFile = _options.DefaultFileNames[matchIndex]; 75 | var file = _fileProvider.GetFileInfo(subpath.Value + defaultFile); 76 | // TryMatchPath will make sure subpath always ends with a "/" by adding it if needed. 77 | if (file.Exists) 78 | { 79 | // If the path matches a directory but does not end in a slash, redirect to add the slash. 80 | // This prevents relative links from breaking. 81 | if (!Helpers.PathEndsInSlash(context.Request.Path)) 82 | { 83 | context.Response.StatusCode = 301; 84 | context.Response.Headers[HeaderNames.Location] = context.Request.PathBase + context.Request.Path + "/" + context.Request.QueryString; 85 | return Task.CompletedTask; 86 | } 87 | 88 | // Match found, re-write the url. A later middleware will actually serve the file. 89 | context.Request.Path = new PathString(context.Request.Path.Value + defaultFile); 90 | break; 91 | } 92 | } 93 | } 94 | } 95 | 96 | return _next(context); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/DirectoryBrowserMiddleware.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Text.Encodings.Web; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Builder; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.AspNetCore.Http; 10 | using Microsoft.Extensions.FileProviders; 11 | using Microsoft.Extensions.Options; 12 | using Microsoft.Net.Http.Headers; 13 | 14 | namespace Microsoft.AspNetCore.StaticFiles 15 | { 16 | /// 17 | /// Enables directory browsing 18 | /// 19 | public class DirectoryBrowserMiddleware 20 | { 21 | private readonly DirectoryBrowserOptions _options; 22 | private readonly PathString _matchUrl; 23 | private readonly RequestDelegate _next; 24 | private readonly IDirectoryFormatter _formatter; 25 | private readonly IFileProvider _fileProvider; 26 | 27 | /// 28 | /// Creates a new instance of the SendFileMiddleware. Using instance. 29 | /// 30 | /// The next middleware in the pipeline. 31 | /// The used by this middleware. 32 | /// The configuration for this middleware. 33 | public DirectoryBrowserMiddleware(RequestDelegate next, IHostingEnvironment hostingEnv, IOptions options) 34 | : this(next, hostingEnv, HtmlEncoder.Default, options) 35 | { 36 | } 37 | 38 | /// 39 | /// Creates a new instance of the SendFileMiddleware. 40 | /// 41 | /// The next middleware in the pipeline. 42 | /// The used by this middleware. 43 | /// The used by the default . 44 | /// The configuration for this middleware. 45 | public DirectoryBrowserMiddleware(RequestDelegate next, IHostingEnvironment hostingEnv, HtmlEncoder encoder, IOptions options) 46 | { 47 | if (next == null) 48 | { 49 | throw new ArgumentNullException(nameof(next)); 50 | } 51 | 52 | if (hostingEnv == null) 53 | { 54 | throw new ArgumentNullException(nameof(hostingEnv)); 55 | } 56 | 57 | if (encoder == null) 58 | { 59 | throw new ArgumentNullException(nameof(encoder)); 60 | } 61 | 62 | if (options == null) 63 | { 64 | throw new ArgumentNullException(nameof(options)); 65 | } 66 | 67 | _next = next; 68 | _options = options.Value; 69 | _fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv); 70 | _formatter = options.Value.Formatter ?? new HtmlDirectoryFormatter(encoder); 71 | _matchUrl = _options.RequestPath; 72 | } 73 | 74 | /// 75 | /// Examines the request to see if it matches a configured directory. If so, a view of the directory contents is returned. 76 | /// 77 | /// 78 | /// 79 | public Task Invoke(HttpContext context) 80 | { 81 | // Check if the URL matches any expected paths 82 | if (Helpers.IsGetOrHeadMethod(context.Request.Method) 83 | && Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out var subpath) 84 | && TryGetDirectoryInfo(subpath, out var contents)) 85 | { 86 | // If the path matches a directory but does not end in a slash, redirect to add the slash. 87 | // This prevents relative links from breaking. 88 | if (!Helpers.PathEndsInSlash(context.Request.Path)) 89 | { 90 | context.Response.StatusCode = 301; 91 | context.Response.Headers[HeaderNames.Location] = context.Request.PathBase + context.Request.Path + "/" + context.Request.QueryString; 92 | return Task.CompletedTask; 93 | } 94 | 95 | return _formatter.GenerateContentAsync(context, contents); 96 | } 97 | 98 | return _next(context); 99 | } 100 | 101 | private bool TryGetDirectoryInfo(PathString subpath, out IDirectoryContents contents) 102 | { 103 | contents = _fileProvider.GetDirectoryContents(subpath.Value); 104 | return contents.Exists; 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /shared/Microsoft.AspNetCore.RangeHelper.Sources/RangeHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Diagnostics; 6 | using System.Linq; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.AspNetCore.Http.Headers; 9 | using Microsoft.Extensions.Logging; 10 | using Microsoft.Extensions.Primitives; 11 | using Microsoft.Net.Http.Headers; 12 | 13 | namespace Microsoft.AspNetCore.Internal 14 | { 15 | /// 16 | /// Provides a parser for the Range Header in an . 17 | /// 18 | internal static class RangeHelper 19 | { 20 | /// 21 | /// Returns the normalized form of the requested range if the Range Header in the is valid. 22 | /// 23 | /// The associated with the request. 24 | /// The associated with the given . 25 | /// The total length of the file representation requested. 26 | /// The . 27 | /// A boolean value which represents if the contain a single valid 28 | /// range request. A which represents the normalized form of the 29 | /// range parsed from the or null if it cannot be normalized. 30 | /// 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). 31 | /// This results in (true,null) return values. 32 | public static (bool isRangeRequest, RangeItemHeaderValue range) ParseRange( 33 | HttpContext context, 34 | RequestHeaders requestHeaders, 35 | long length, 36 | ILogger logger) 37 | { 38 | var rawRangeHeader = context.Request.Headers[HeaderNames.Range]; 39 | if (StringValues.IsNullOrEmpty(rawRangeHeader)) 40 | { 41 | logger.LogTrace("Range header's value is empty."); 42 | return (false, null); 43 | } 44 | 45 | // Perf: Check for a single entry before parsing it 46 | if (rawRangeHeader.Count > 1 || rawRangeHeader[0].IndexOf(',') >= 0) 47 | { 48 | logger.LogDebug("Multiple ranges are not supported."); 49 | 50 | // The spec allows for multiple ranges but we choose not to support them because the client may request 51 | // very strange ranges (e.g. each byte separately, overlapping ranges, etc.) that could negatively 52 | // impact the server. Ignore the header and serve the response normally. 53 | return (false, null); 54 | } 55 | 56 | var rangeHeader = requestHeaders.Range; 57 | if (rangeHeader == null) 58 | { 59 | logger.LogDebug("Range header's value is invalid."); 60 | // Invalid 61 | return (false, null); 62 | } 63 | 64 | // Already verified above 65 | Debug.Assert(rangeHeader.Ranges.Count == 1); 66 | 67 | var ranges = rangeHeader.Ranges; 68 | if (ranges == null) 69 | { 70 | logger.LogDebug("Range header's value is invalid."); 71 | return (false, null); 72 | } 73 | 74 | if (ranges.Count == 0) 75 | { 76 | return (true, null); 77 | } 78 | 79 | if (length == 0) 80 | { 81 | return (true, null); 82 | } 83 | 84 | // Normalize the ranges 85 | var range = NormalizeRange(ranges.SingleOrDefault(), length); 86 | 87 | // Return the single range 88 | return (true, range); 89 | } 90 | 91 | // Internal for testing 92 | internal static RangeItemHeaderValue NormalizeRange(RangeItemHeaderValue range, long length) 93 | { 94 | var start = range.From; 95 | var end = range.To; 96 | 97 | // X-[Y] 98 | if (start.HasValue) 99 | { 100 | if (start.Value >= length) 101 | { 102 | // Not satisfiable, skip/discard. 103 | return null; 104 | } 105 | if (!end.HasValue || end.Value >= length) 106 | { 107 | end = length - 1; 108 | } 109 | } 110 | else 111 | { 112 | // suffix range "-X" e.g. the last X bytes, resolve 113 | if (end.Value == 0) 114 | { 115 | // Not satisfiable, skip/discard. 116 | return null; 117 | } 118 | 119 | var bytes = Math.Min(end.Value, length); 120 | start = length - bytes; 121 | end = start + bytes - 1; 122 | } 123 | 124 | return new RangeItemHeaderValue(start, end); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.StaticFiles.Tests/StaticFileContextTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using Microsoft.AspNetCore.Builder; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.Extensions.FileProviders; 10 | using Microsoft.Extensions.Logging.Abstractions; 11 | using Microsoft.Extensions.Primitives; 12 | using Xunit; 13 | 14 | namespace Microsoft.AspNetCore.StaticFiles 15 | { 16 | public class StaticFileContextTest 17 | { 18 | [Fact] 19 | public void LookupFileInfo_ReturnsFalse_IfFileDoesNotExist() 20 | { 21 | // Arrange 22 | var options = new StaticFileOptions(); 23 | var context = new StaticFileContext(new DefaultHttpContext(), options, PathString.Empty, NullLogger.Instance, new TestFileProvider(), new FileExtensionContentTypeProvider()); 24 | 25 | // Act 26 | var validateResult = context.ValidatePath(); 27 | var lookupResult = context.LookupFileInfo(); 28 | 29 | // Assert 30 | Assert.True(validateResult); 31 | Assert.False(lookupResult); 32 | } 33 | 34 | [Fact] 35 | public void LookupFileInfo_ReturnsTrue_IfFileExists() 36 | { 37 | // Arrange 38 | var options = new StaticFileOptions(); 39 | var fileProvider = new TestFileProvider(); 40 | fileProvider.AddFile("/foo.txt", new TestFileInfo 41 | { 42 | LastModified = new DateTimeOffset(2014, 1, 2, 3, 4, 5, TimeSpan.Zero) 43 | }); 44 | var pathString = new PathString("/test"); 45 | var httpContext = new DefaultHttpContext(); 46 | httpContext.Request.Path = new PathString("/test/foo.txt"); 47 | var context = new StaticFileContext(httpContext, options, pathString, NullLogger.Instance, fileProvider, new FileExtensionContentTypeProvider()); 48 | 49 | // Act 50 | context.ValidatePath(); 51 | var result = context.LookupFileInfo(); 52 | 53 | // Assert 54 | Assert.True(result); 55 | } 56 | 57 | private sealed class TestFileProvider : IFileProvider 58 | { 59 | private readonly Dictionary _files = new Dictionary(StringComparer.Ordinal); 60 | 61 | public void AddFile(string path, IFileInfo fileInfo) 62 | { 63 | _files[path] = fileInfo; 64 | } 65 | 66 | public IDirectoryContents GetDirectoryContents(string subpath) 67 | { 68 | throw new NotImplementedException(); 69 | } 70 | 71 | public IFileInfo GetFileInfo(string subpath) 72 | { 73 | if (_files.TryGetValue(subpath, out var result)) 74 | { 75 | return result; 76 | } 77 | 78 | return new NotFoundFileInfo(); 79 | } 80 | 81 | public IChangeToken Watch(string filter) 82 | { 83 | throw new NotSupportedException(); 84 | } 85 | 86 | private class NotFoundFileInfo : IFileInfo 87 | { 88 | public bool Exists 89 | { 90 | get 91 | { 92 | return false; 93 | } 94 | } 95 | 96 | public bool IsDirectory 97 | { 98 | get 99 | { 100 | throw new NotImplementedException(); 101 | } 102 | } 103 | 104 | public DateTimeOffset LastModified 105 | { 106 | get 107 | { 108 | throw new NotImplementedException(); 109 | } 110 | } 111 | 112 | public long Length 113 | { 114 | get 115 | { 116 | throw new NotImplementedException(); 117 | } 118 | } 119 | 120 | public string Name 121 | { 122 | get 123 | { 124 | throw new NotImplementedException(); 125 | } 126 | } 127 | 128 | public string PhysicalPath 129 | { 130 | get 131 | { 132 | throw new NotImplementedException(); 133 | } 134 | } 135 | 136 | public Stream CreateReadStream() 137 | { 138 | throw new NotImplementedException(); 139 | } 140 | } 141 | } 142 | 143 | private sealed class TestFileInfo : IFileInfo 144 | { 145 | public bool Exists 146 | { 147 | get { return true; } 148 | } 149 | 150 | public bool IsDirectory 151 | { 152 | get { return false; } 153 | } 154 | 155 | public DateTimeOffset LastModified { get; set; } 156 | 157 | public long Length { get; set; } 158 | 159 | public string Name { get; set; } 160 | 161 | public string PhysicalPath { get; set; } 162 | 163 | public Stream CreateReadStream() 164 | { 165 | throw new NotImplementedException(); 166 | } 167 | } 168 | } 169 | } -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/StaticFileMiddleware.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Diagnostics; 6 | using System.IO; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Builder; 9 | using Microsoft.AspNetCore.Hosting; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.Extensions.FileProviders; 12 | using Microsoft.Extensions.Logging; 13 | using Microsoft.Extensions.Options; 14 | 15 | namespace Microsoft.AspNetCore.StaticFiles 16 | { 17 | /// 18 | /// Enables serving static files for a given request path 19 | /// 20 | public class StaticFileMiddleware 21 | { 22 | private readonly StaticFileOptions _options; 23 | private readonly PathString _matchUrl; 24 | private readonly RequestDelegate _next; 25 | private readonly ILogger _logger; 26 | private readonly IFileProvider _fileProvider; 27 | private readonly IContentTypeProvider _contentTypeProvider; 28 | 29 | /// 30 | /// Creates a new instance of the StaticFileMiddleware. 31 | /// 32 | /// The next middleware in the pipeline. 33 | /// The used by this middleware. 34 | /// The configuration options. 35 | /// An instance used to create loggers. 36 | public StaticFileMiddleware(RequestDelegate next, IHostingEnvironment hostingEnv, IOptions options, ILoggerFactory loggerFactory) 37 | { 38 | if (next == null) 39 | { 40 | throw new ArgumentNullException(nameof(next)); 41 | } 42 | 43 | if (hostingEnv == null) 44 | { 45 | throw new ArgumentNullException(nameof(hostingEnv)); 46 | } 47 | 48 | if (options == null) 49 | { 50 | throw new ArgumentNullException(nameof(options)); 51 | } 52 | 53 | if (loggerFactory == null) 54 | { 55 | throw new ArgumentNullException(nameof(loggerFactory)); 56 | } 57 | 58 | _next = next; 59 | _options = options.Value; 60 | _contentTypeProvider = options.Value.ContentTypeProvider ?? new FileExtensionContentTypeProvider(); 61 | _fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv); 62 | _matchUrl = _options.RequestPath; 63 | _logger = loggerFactory.CreateLogger(); 64 | } 65 | 66 | /// 67 | /// Processes a request to determine if it matches a known file, and if so, serves it. 68 | /// 69 | /// 70 | /// 71 | public async Task Invoke(HttpContext context) 72 | { 73 | var fileContext = new StaticFileContext(context, _options, _matchUrl, _logger, _fileProvider, _contentTypeProvider); 74 | 75 | if (!fileContext.ValidateMethod()) 76 | { 77 | _logger.LogRequestMethodNotSupported(context.Request.Method); 78 | } 79 | else if (!fileContext.ValidatePath()) 80 | { 81 | _logger.LogPathMismatch(fileContext.SubPath); 82 | } 83 | else if (!fileContext.LookupContentType()) 84 | { 85 | _logger.LogFileTypeNotSupported(fileContext.SubPath); 86 | } 87 | else if (!fileContext.LookupFileInfo()) 88 | { 89 | _logger.LogFileNotFound(fileContext.SubPath); 90 | } 91 | else 92 | { 93 | // If we get here, we can try to serve the file 94 | fileContext.ComprehendRequestHeaders(); 95 | switch (fileContext.GetPreconditionState()) 96 | { 97 | case StaticFileContext.PreconditionState.Unspecified: 98 | case StaticFileContext.PreconditionState.ShouldProcess: 99 | if (fileContext.IsHeadMethod) 100 | { 101 | await fileContext.SendStatusAsync(Constants.Status200Ok); 102 | return; 103 | } 104 | 105 | try 106 | { 107 | if (fileContext.IsRangeRequest) 108 | { 109 | await fileContext.SendRangeAsync(); 110 | return; 111 | } 112 | 113 | await fileContext.SendAsync(); 114 | _logger.LogFileServed(fileContext.SubPath, fileContext.PhysicalPath); 115 | return; 116 | } 117 | catch (FileNotFoundException) 118 | { 119 | context.Response.Clear(); 120 | } 121 | break; 122 | case StaticFileContext.PreconditionState.NotModified: 123 | _logger.LogPathNotModified(fileContext.SubPath); 124 | await fileContext.SendStatusAsync(Constants.Status304NotModified); 125 | return; 126 | 127 | case StaticFileContext.PreconditionState.PreconditionFailed: 128 | _logger.LogPreconditionFailed(fileContext.SubPath); 129 | await fileContext.SendStatusAsync(Constants.Status412PreconditionFailed); 130 | return; 131 | 132 | default: 133 | var exception = new NotImplementedException(fileContext.GetPreconditionState().ToString()); 134 | Debug.Fail(exception.ToString()); 135 | throw exception; 136 | } 137 | } 138 | 139 | await _next(context); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/HtmlDirectoryFormatter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Globalization; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Text.Encodings.Web; 10 | using System.Threading.Tasks; 11 | using Microsoft.AspNetCore.Http; 12 | using Microsoft.Extensions.DependencyInjection; 13 | using Microsoft.Extensions.FileProviders; 14 | 15 | namespace Microsoft.AspNetCore.StaticFiles 16 | { 17 | /// 18 | /// Generates an HTML view for a directory. 19 | /// 20 | public class HtmlDirectoryFormatter : IDirectoryFormatter 21 | { 22 | private const string TextHtmlUtf8 = "text/html; charset=utf-8"; 23 | 24 | private HtmlEncoder _htmlEncoder; 25 | 26 | public HtmlDirectoryFormatter(HtmlEncoder encoder) 27 | { 28 | if (encoder == null) 29 | { 30 | throw new ArgumentNullException(nameof(encoder)); 31 | } 32 | _htmlEncoder = encoder; 33 | } 34 | 35 | /// 36 | /// Generates an HTML view for a directory. 37 | /// 38 | public virtual Task GenerateContentAsync(HttpContext context, IEnumerable contents) 39 | { 40 | if (context == null) 41 | { 42 | throw new ArgumentNullException(nameof(context)); 43 | } 44 | if (contents == null) 45 | { 46 | throw new ArgumentNullException(nameof(contents)); 47 | } 48 | 49 | context.Response.ContentType = TextHtmlUtf8; 50 | 51 | if (HttpMethods.IsHead(context.Request.Method)) 52 | { 53 | // HEAD, no response body 54 | return Task.CompletedTask; 55 | } 56 | 57 | PathString requestPath = context.Request.PathBase + context.Request.Path; 58 | 59 | var builder = new StringBuilder(); 60 | 61 | builder.AppendFormat( 62 | @" 63 | ", CultureInfo.CurrentUICulture.TwoLetterISOLanguageName); 64 | 65 | builder.AppendFormat(@" 66 | 67 | {0} {1}", HtmlEncode(Resources.HtmlDir_IndexOf), HtmlEncode(requestPath.Value)); 68 | 69 | builder.Append(@" 70 | 103 | 104 | 105 |
"); 106 | builder.AppendFormat(@" 107 |

{0} /", HtmlEncode(Resources.HtmlDir_IndexOf)); 108 | 109 | string cumulativePath = "/"; 110 | foreach (var segment in requestPath.Value.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)) 111 | { 112 | cumulativePath = cumulativePath + segment + "/"; 113 | builder.AppendFormat(@"{1}/", 114 | HtmlEncode(cumulativePath), HtmlEncode(segment)); 115 | } 116 | 117 | builder.AppendFormat(CultureInfo.CurrentUICulture, 118 | @"

119 | 120 | 121 | 122 | 123 | ", 124 | HtmlEncode(Resources.HtmlDir_TableSummary), 125 | HtmlEncode(Resources.HtmlDir_Name), 126 | HtmlEncode(Resources.HtmlDir_Size), 127 | HtmlEncode(Resources.HtmlDir_Modified), 128 | HtmlEncode(Resources.HtmlDir_LastModified)); 129 | 130 | foreach (var subdir in contents.Where(info => info.IsDirectory)) 131 | { 132 | builder.AppendFormat(@" 133 | 134 | 135 | 136 | 137 | ", 138 | HtmlEncode(subdir.Name), 139 | HtmlEncode(subdir.LastModified.ToString(CultureInfo.CurrentCulture))); 140 | } 141 | 142 | foreach (var file in contents.Where(info => !info.IsDirectory)) 143 | { 144 | builder.AppendFormat(@" 145 | 146 | 147 | 148 | 149 | ", 150 | HtmlEncode(file.Name), 151 | HtmlEncode(file.Length.ToString("n0", CultureInfo.CurrentCulture)), 152 | HtmlEncode(file.LastModified.ToString(CultureInfo.CurrentCulture))); 153 | } 154 | 155 | builder.Append(@" 156 | 157 |
{1}{2}{4}
{0}/{1}
{0}{1}{2}
158 |
159 | 160 | "); 161 | string data = builder.ToString(); 162 | byte[] bytes = Encoding.UTF8.GetBytes(data); 163 | context.Response.ContentLength = bytes.Length; 164 | return context.Response.Body.WriteAsync(bytes, 0, bytes.Length); 165 | } 166 | 167 | private string HtmlEncode(string body) 168 | { 169 | return _htmlEncoder.Encode(body); 170 | } 171 | } 172 | } -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/Resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | No formatter provided. 122 | 123 | 124 | Index of 125 | 126 | 127 | Last Modified 128 | 129 | 130 | Modified 131 | 132 | 133 | Name 134 | 135 | 136 | Size 137 | 138 | 139 | The list of files in the given directory. Column headers are listed in the first row. 140 | 141 | -------------------------------------------------------------------------------- /run.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env powershell 2 | #requires -version 4 3 | 4 | <# 5 | .SYNOPSIS 6 | Executes KoreBuild commands. 7 | 8 | .DESCRIPTION 9 | Downloads korebuild if required. Then executes the KoreBuild command. To see available commands, execute with `-Command help`. 10 | 11 | .PARAMETER Command 12 | The KoreBuild command to run. 13 | 14 | .PARAMETER Path 15 | The folder to build. Defaults to the folder containing this script. 16 | 17 | .PARAMETER Channel 18 | The channel of KoreBuild to download. Overrides the value from the config file. 19 | 20 | .PARAMETER DotNetHome 21 | The directory where .NET Core tools will be stored. 22 | 23 | .PARAMETER ToolsSource 24 | The base url where build tools can be downloaded. Overrides the value from the config file. 25 | 26 | .PARAMETER Update 27 | Updates KoreBuild to the latest version even if a lock file is present. 28 | 29 | .PARAMETER Reinstall 30 | Re-installs KoreBuild 31 | 32 | .PARAMETER ConfigFile 33 | The path to the configuration file that stores values. Defaults to korebuild.json. 34 | 35 | .PARAMETER ToolsSourceSuffix 36 | The Suffix to append to the end of the ToolsSource. Useful for query strings in blob stores. 37 | 38 | .PARAMETER CI 39 | Sets up CI specific settings and variables. 40 | 41 | .PARAMETER Arguments 42 | Arguments to be passed to the command 43 | 44 | .NOTES 45 | This function will create a file $PSScriptRoot/korebuild-lock.txt. This lock file can be committed to source, but does not have to be. 46 | When the lockfile is not present, KoreBuild will create one using latest available version from $Channel. 47 | 48 | The $ConfigFile is expected to be an JSON file. It is optional, and the configuration values in it are optional as well. Any options set 49 | in the file are overridden by command line parameters. 50 | 51 | .EXAMPLE 52 | Example config file: 53 | ```json 54 | { 55 | "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/master/tools/korebuild.schema.json", 56 | "channel": "master", 57 | "toolsSource": "https://aspnetcore.blob.core.windows.net/buildtools" 58 | } 59 | ``` 60 | #> 61 | [CmdletBinding(PositionalBinding = $false)] 62 | param( 63 | [Parameter(Mandatory = $true, Position = 0)] 64 | [string]$Command, 65 | [string]$Path = $PSScriptRoot, 66 | [Alias('c')] 67 | [string]$Channel, 68 | [Alias('d')] 69 | [string]$DotNetHome, 70 | [Alias('s')] 71 | [string]$ToolsSource, 72 | [Alias('u')] 73 | [switch]$Update, 74 | [switch]$Reinstall, 75 | [string]$ToolsSourceSuffix, 76 | [string]$ConfigFile = $null, 77 | [switch]$CI, 78 | [Parameter(ValueFromRemainingArguments = $true)] 79 | [string[]]$Arguments 80 | ) 81 | 82 | Set-StrictMode -Version 2 83 | $ErrorActionPreference = 'Stop' 84 | 85 | # 86 | # Functions 87 | # 88 | 89 | function Get-KoreBuild { 90 | 91 | $lockFile = Join-Path $Path 'korebuild-lock.txt' 92 | 93 | if (!(Test-Path $lockFile) -or $Update) { 94 | Get-RemoteFile "$ToolsSource/korebuild/channels/$Channel/latest.txt" $lockFile $ToolsSourceSuffix 95 | } 96 | 97 | $version = Get-Content $lockFile | Where-Object { $_ -like 'version:*' } | Select-Object -first 1 98 | if (!$version) { 99 | Write-Error "Failed to parse version from $lockFile. Expected a line that begins with 'version:'" 100 | } 101 | $version = $version.TrimStart('version:').Trim() 102 | $korebuildPath = Join-Paths $DotNetHome ('buildtools', 'korebuild', $version) 103 | 104 | if ($Reinstall -and (Test-Path $korebuildPath)) { 105 | Remove-Item -Force -Recurse $korebuildPath 106 | } 107 | 108 | if (!(Test-Path $korebuildPath)) { 109 | Write-Host -ForegroundColor Magenta "Downloading KoreBuild $version" 110 | New-Item -ItemType Directory -Path $korebuildPath | Out-Null 111 | $remotePath = "$ToolsSource/korebuild/artifacts/$version/korebuild.$version.zip" 112 | 113 | try { 114 | $tmpfile = Join-Path ([IO.Path]::GetTempPath()) "KoreBuild-$([guid]::NewGuid()).zip" 115 | Get-RemoteFile $remotePath $tmpfile $ToolsSourceSuffix 116 | if (Get-Command -Name 'Microsoft.PowerShell.Archive\Expand-Archive' -ErrorAction Ignore) { 117 | # Use built-in commands where possible as they are cross-plat compatible 118 | Microsoft.PowerShell.Archive\Expand-Archive -Path $tmpfile -DestinationPath $korebuildPath 119 | } 120 | else { 121 | # Fallback to old approach for old installations of PowerShell 122 | Add-Type -AssemblyName System.IO.Compression.FileSystem 123 | [System.IO.Compression.ZipFile]::ExtractToDirectory($tmpfile, $korebuildPath) 124 | } 125 | } 126 | catch { 127 | Remove-Item -Recurse -Force $korebuildPath -ErrorAction Ignore 128 | throw 129 | } 130 | finally { 131 | Remove-Item $tmpfile -ErrorAction Ignore 132 | } 133 | } 134 | 135 | return $korebuildPath 136 | } 137 | 138 | function Join-Paths([string]$path, [string[]]$childPaths) { 139 | $childPaths | ForEach-Object { $path = Join-Path $path $_ } 140 | return $path 141 | } 142 | 143 | function Get-RemoteFile([string]$RemotePath, [string]$LocalPath, [string]$RemoteSuffix) { 144 | if ($RemotePath -notlike 'http*') { 145 | Copy-Item $RemotePath $LocalPath 146 | return 147 | } 148 | 149 | $retries = 10 150 | while ($retries -gt 0) { 151 | $retries -= 1 152 | try { 153 | Invoke-WebRequest -UseBasicParsing -Uri $($RemotePath + $RemoteSuffix) -OutFile $LocalPath 154 | return 155 | } 156 | catch { 157 | Write-Verbose "Request failed. $retries retries remaining" 158 | } 159 | } 160 | 161 | Write-Error "Download failed: '$RemotePath'." 162 | } 163 | 164 | # 165 | # Main 166 | # 167 | 168 | # Load configuration or set defaults 169 | 170 | $Path = Resolve-Path $Path 171 | if (!$ConfigFile) { $ConfigFile = Join-Path $Path 'korebuild.json' } 172 | 173 | if (Test-Path $ConfigFile) { 174 | try { 175 | $config = Get-Content -Raw -Encoding UTF8 -Path $ConfigFile | ConvertFrom-Json 176 | if ($config) { 177 | if (!($Channel) -and (Get-Member -Name 'channel' -InputObject $config)) { [string] $Channel = $config.channel } 178 | if (!($ToolsSource) -and (Get-Member -Name 'toolsSource' -InputObject $config)) { [string] $ToolsSource = $config.toolsSource} 179 | } 180 | } 181 | catch { 182 | Write-Host -ForegroundColor Red $Error[0] 183 | Write-Error "$ConfigFile contains invalid JSON." 184 | exit 1 185 | } 186 | } 187 | 188 | if (!$DotNetHome) { 189 | $DotNetHome = if ($env:DOTNET_HOME) { $env:DOTNET_HOME } ` 190 | elseif ($env:USERPROFILE) { Join-Path $env:USERPROFILE '.dotnet'} ` 191 | elseif ($env:HOME) {Join-Path $env:HOME '.dotnet'}` 192 | else { Join-Path $PSScriptRoot '.dotnet'} 193 | } 194 | 195 | if (!$Channel) { $Channel = 'master' } 196 | if (!$ToolsSource) { $ToolsSource = 'https://aspnetcore.blob.core.windows.net/buildtools' } 197 | 198 | # Execute 199 | 200 | $korebuildPath = Get-KoreBuild 201 | Import-Module -Force -Scope Local (Join-Path $korebuildPath 'KoreBuild.psd1') 202 | 203 | try { 204 | Set-KoreBuildSettings -ToolsSource $ToolsSource -DotNetHome $DotNetHome -RepoPath $Path -ConfigFile $ConfigFile -CI:$CI 205 | Invoke-KoreBuildCommand $Command @Arguments 206 | } 207 | finally { 208 | Remove-Module 'KoreBuild' -ErrorAction Ignore 209 | } 210 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/LoggerExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.Extensions.Primitives; 7 | 8 | namespace Microsoft.AspNetCore.StaticFiles 9 | { 10 | /// 11 | /// Defines *all* the logger messages produced by static files 12 | /// 13 | internal static class LoggerExtensions 14 | { 15 | private static Action _logMethodNotSupported; 16 | private static Action _logFileServed; 17 | private static Action _logPathMismatch; 18 | private static Action _logFileTypeNotSupported; 19 | private static Action _logFileNotFound; 20 | private static Action _logPathNotModified; 21 | private static Action _logPreconditionFailed; 22 | private static Action _logHandled; 23 | private static Action _logRangeNotSatisfiable; 24 | private static Action _logSendingFileRange; 25 | private static Action _logCopyingFileRange; 26 | private static Action _logCopyingBytesToResponse; 27 | private static Action _logWriteCancelled; 28 | 29 | static LoggerExtensions() 30 | { 31 | _logMethodNotSupported = LoggerMessage.Define( 32 | logLevel: LogLevel.Debug, 33 | eventId: 1, 34 | formatString: "{Method} requests are not supported"); 35 | _logFileServed = LoggerMessage.Define( 36 | logLevel: LogLevel.Information, 37 | eventId: 2, 38 | formatString: "Sending file. Request path: '{VirtualPath}'. Physical path: '{PhysicalPath}'"); 39 | _logPathMismatch = LoggerMessage.Define( 40 | logLevel: LogLevel.Debug, 41 | eventId: 3, 42 | formatString: "The request path {Path} does not match the path filter"); 43 | _logFileTypeNotSupported = LoggerMessage.Define( 44 | logLevel: LogLevel.Debug, 45 | eventId: 4, 46 | formatString: "The request path {Path} does not match a supported file type"); 47 | _logFileNotFound = LoggerMessage.Define( 48 | logLevel: LogLevel.Debug, 49 | eventId: 5, 50 | formatString: "The request path {Path} does not match an existing file"); 51 | _logPathNotModified = LoggerMessage.Define( 52 | logLevel: LogLevel.Information, 53 | eventId: 6, 54 | formatString: "The file {Path} was not modified"); 55 | _logPreconditionFailed = LoggerMessage.Define( 56 | logLevel: LogLevel.Information, 57 | eventId: 7, 58 | formatString: "Precondition for {Path} failed"); 59 | _logHandled = LoggerMessage.Define( 60 | logLevel: LogLevel.Debug, 61 | eventId: 8, 62 | formatString: "Handled. Status code: {StatusCode} File: {Path}"); 63 | _logRangeNotSatisfiable = LoggerMessage.Define( 64 | logLevel: LogLevel.Warning, 65 | eventId: 9, 66 | formatString: "Range not satisfiable for {Path}"); 67 | _logSendingFileRange = LoggerMessage.Define( 68 | logLevel: LogLevel.Information, 69 | eventId: 10, 70 | formatString: "Sending {Range} of file {Path}"); 71 | _logCopyingFileRange = LoggerMessage.Define( 72 | logLevel: LogLevel.Debug, 73 | eventId: 11, 74 | formatString: "Copying {Range} of file {Path} to the response body"); 75 | _logCopyingBytesToResponse = LoggerMessage.Define( 76 | logLevel: LogLevel.Debug, 77 | eventId: 12, 78 | formatString: "Copying bytes {Start}-{End} of file {Path} to response body"); 79 | _logWriteCancelled = LoggerMessage.Define( 80 | logLevel: LogLevel.Debug, 81 | eventId: 14, 82 | formatString: "The file transmission was cancelled"); 83 | } 84 | 85 | public static void LogRequestMethodNotSupported(this ILogger logger, string method) 86 | { 87 | _logMethodNotSupported(logger, method, null); 88 | } 89 | 90 | public static void LogFileServed(this ILogger logger, string virtualPath, string physicalPath) 91 | { 92 | if (string.IsNullOrEmpty(physicalPath)) 93 | { 94 | physicalPath = "N/A"; 95 | } 96 | _logFileServed(logger, virtualPath, physicalPath, null); 97 | } 98 | 99 | public static void LogPathMismatch(this ILogger logger, string path) 100 | { 101 | _logPathMismatch(logger, path, null); 102 | } 103 | 104 | public static void LogFileTypeNotSupported(this ILogger logger, string path) 105 | { 106 | _logFileTypeNotSupported(logger, path, null); 107 | } 108 | 109 | public static void LogFileNotFound(this ILogger logger, string path) 110 | { 111 | _logFileNotFound(logger, path, null); 112 | } 113 | 114 | public static void LogPathNotModified(this ILogger logger, string path) 115 | { 116 | _logPathNotModified(logger, path, null); 117 | } 118 | 119 | public static void LogPreconditionFailed(this ILogger logger, string path) 120 | { 121 | _logPreconditionFailed(logger, path, null); 122 | } 123 | 124 | public static void LogHandled(this ILogger logger, int statusCode, string path) 125 | { 126 | _logHandled(logger, statusCode, path, null); 127 | } 128 | 129 | public static void LogRangeNotSatisfiable(this ILogger logger, string path) 130 | { 131 | _logRangeNotSatisfiable(logger, path, null); 132 | } 133 | 134 | public static void LogSendingFileRange(this ILogger logger, StringValues range, string path) 135 | { 136 | _logSendingFileRange(logger, range, path, null); 137 | } 138 | 139 | public static void LogCopyingFileRange(this ILogger logger, StringValues range, string path) 140 | { 141 | _logCopyingFileRange(logger, range, path, null); 142 | } 143 | 144 | public static void LogCopyingBytesToResponse(this ILogger logger, long start, long? end, string path) 145 | { 146 | _logCopyingBytesToResponse( 147 | logger, 148 | start, 149 | end != null ? end.ToString() : "*", 150 | path, 151 | null); 152 | } 153 | 154 | public static void LogWriteCancelled(this ILogger logger, Exception ex) 155 | { 156 | _logWriteCancelled(logger, ex); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /StaticFiles.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 15 3 | VisualStudioVersion = 15.0.26228.9 4 | MinimumVisualStudioVersion = 15.0.26730.03 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{40EE0889-960E-41B4-A3D3-9CE963EB0797}" 6 | ProjectSection(SolutionItems) = preProject 7 | src\Directory.Build.props = src\Directory.Build.props 8 | EndProjectSection 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{8B21A3A9-9CA6-4857-A6E0-1A3203404B60}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.StaticFiles", "src\Microsoft.AspNetCore.StaticFiles\Microsoft.AspNetCore.StaticFiles.csproj", "{8D7BC5A4-F19C-4184-8338-A6B42997218C}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StaticFileSample", "samples\StaticFileSample\StaticFileSample.csproj", "{092141D9-305A-4FC5-AE74-CB23982CA8D4}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{EF02AFE8-7C15-4DDB-8B2C-58A676112A98}" 17 | ProjectSection(SolutionItems) = preProject 18 | test\Directory.Build.props = test\Directory.Build.props 19 | EndProjectSection 20 | EndProject 21 | 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}" 22 | EndProject 23 | 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}" 24 | EndProject 25 | 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}" 26 | EndProject 27 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{360DC2F8-EEB4-4C69-9784-C686EAD78279}" 28 | EndProject 29 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microsoft.AspNetCore.RangeHelper.Sources", "Microsoft.AspNetCore.RangeHelper.Sources", "{DB6A1D14-B8A2-488F-9C4B-422FD45C8853}" 30 | ProjectSection(SolutionItems) = preProject 31 | shared\Microsoft.AspNetCore.RangeHelper.Sources\RangeHelper.cs = shared\Microsoft.AspNetCore.RangeHelper.Sources\RangeHelper.cs 32 | EndProjectSection 33 | EndProject 34 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DE015849-E126-4DFA-A754-E1AC3B1E7925}" 35 | ProjectSection(SolutionItems) = preProject 36 | .appveyor.yml = .appveyor.yml 37 | .travis.yml = .travis.yml 38 | EndProjectSection 39 | EndProject 40 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{D3F67455-9FB9-4354-93EC-B05D755114E9}" 41 | ProjectSection(SolutionItems) = preProject 42 | build\dependencies.props = build\dependencies.props 43 | build\repo.props = build\repo.props 44 | build\sources.props = build\sources.props 45 | EndProjectSection 46 | EndProject 47 | Global 48 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 49 | Debug|Any CPU = Debug|Any CPU 50 | Debug|Mixed Platforms = Debug|Mixed Platforms 51 | Debug|x86 = Debug|x86 52 | Release|Any CPU = Release|Any CPU 53 | Release|Mixed Platforms = Release|Mixed Platforms 54 | Release|x86 = Release|x86 55 | EndGlobalSection 56 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 57 | {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 58 | {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Debug|Any CPU.Build.0 = Debug|Any CPU 59 | {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 60 | {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 61 | {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Debug|x86.ActiveCfg = Debug|Any CPU 62 | {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Release|Any CPU.ActiveCfg = Release|Any CPU 63 | {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Release|Any CPU.Build.0 = Release|Any CPU 64 | {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 65 | {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Release|Mixed Platforms.Build.0 = Release|Any CPU 66 | {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Release|x86.ActiveCfg = Release|Any CPU 67 | {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 68 | {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Debug|Any CPU.Build.0 = Debug|Any CPU 69 | {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 70 | {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 71 | {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Debug|x86.ActiveCfg = Debug|Any CPU 72 | {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Release|Any CPU.ActiveCfg = Release|Any CPU 73 | {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Release|Any CPU.Build.0 = Release|Any CPU 74 | {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 75 | {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Release|Mixed Platforms.Build.0 = Release|Any CPU 76 | {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Release|x86.ActiveCfg = Release|Any CPU 77 | {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 78 | {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Debug|Any CPU.Build.0 = Debug|Any CPU 79 | {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 80 | {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 81 | {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Debug|x86.ActiveCfg = Debug|Any CPU 82 | {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Release|Any CPU.ActiveCfg = Release|Any CPU 83 | {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Release|Any CPU.Build.0 = Release|Any CPU 84 | {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 85 | {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Release|Mixed Platforms.Build.0 = Release|Any CPU 86 | {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Release|x86.ActiveCfg = Release|Any CPU 87 | {FDF0539C-1F62-4B78-91B1-C687886931CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 88 | {FDF0539C-1F62-4B78-91B1-C687886931CA}.Debug|Any CPU.Build.0 = Debug|Any CPU 89 | {FDF0539C-1F62-4B78-91B1-C687886931CA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 90 | {FDF0539C-1F62-4B78-91B1-C687886931CA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 91 | {FDF0539C-1F62-4B78-91B1-C687886931CA}.Debug|x86.ActiveCfg = Debug|Any CPU 92 | {FDF0539C-1F62-4B78-91B1-C687886931CA}.Debug|x86.Build.0 = Debug|Any CPU 93 | {FDF0539C-1F62-4B78-91B1-C687886931CA}.Release|Any CPU.ActiveCfg = Release|Any CPU 94 | {FDF0539C-1F62-4B78-91B1-C687886931CA}.Release|Any CPU.Build.0 = Release|Any CPU 95 | {FDF0539C-1F62-4B78-91B1-C687886931CA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 96 | {FDF0539C-1F62-4B78-91B1-C687886931CA}.Release|Mixed Platforms.Build.0 = Release|Any CPU 97 | {FDF0539C-1F62-4B78-91B1-C687886931CA}.Release|x86.ActiveCfg = Release|Any CPU 98 | {FDF0539C-1F62-4B78-91B1-C687886931CA}.Release|x86.Build.0 = Release|Any CPU 99 | {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 100 | {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Debug|Any CPU.Build.0 = Debug|Any CPU 101 | {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 102 | {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 103 | {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Debug|x86.ActiveCfg = Debug|Any CPU 104 | {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Debug|x86.Build.0 = Debug|Any CPU 105 | {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Release|Any CPU.ActiveCfg = Release|Any CPU 106 | {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Release|Any CPU.Build.0 = Release|Any CPU 107 | {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 108 | {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Release|Mixed Platforms.Build.0 = Release|Any CPU 109 | {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Release|x86.ActiveCfg = Release|Any CPU 110 | {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Release|x86.Build.0 = Release|Any CPU 111 | EndGlobalSection 112 | GlobalSection(SolutionProperties) = preSolution 113 | HideSolutionNode = FALSE 114 | EndGlobalSection 115 | GlobalSection(NestedProjects) = preSolution 116 | {8D7BC5A4-F19C-4184-8338-A6B42997218C} = {40EE0889-960E-41B4-A3D3-9CE963EB0797} 117 | {092141D9-305A-4FC5-AE74-CB23982CA8D4} = {8B21A3A9-9CA6-4857-A6E0-1A3203404B60} 118 | {CC87FE7D-8F42-4BE9-A152-9625E837C1E5} = {EF02AFE8-7C15-4DDB-8B2C-58A676112A98} 119 | {FDF0539C-1F62-4B78-91B1-C687886931CA} = {EF02AFE8-7C15-4DDB-8B2C-58A676112A98} 120 | {D3D752C4-4CDF-4F18-AC7F-48CB980A69DA} = {EF02AFE8-7C15-4DDB-8B2C-58A676112A98} 121 | {DB6A1D14-B8A2-488F-9C4B-422FD45C8853} = {360DC2F8-EEB4-4C69-9784-C686EAD78279} 122 | {D3F67455-9FB9-4354-93EC-B05D755114E9} = {DE015849-E126-4DFA-A754-E1AC3B1E7925} 123 | EndGlobalSection 124 | GlobalSection(ExtensibilityGlobals) = postSolution 125 | SolutionGuid = {AF7F03BE-0DB2-48EF-8185-7698995658A6} 126 | EndGlobalSection 127 | EndGlobal 128 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.StaticFiles.Tests/DefaultFilesMiddlewareTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Net; 8 | using System.Net.Http; 9 | using System.Threading.Tasks; 10 | using Microsoft.AspNetCore.Builder; 11 | using Microsoft.AspNetCore.Http; 12 | using Microsoft.AspNetCore.Http.Extensions; 13 | using Microsoft.AspNetCore.TestHost; 14 | using Microsoft.AspNetCore.Testing.xunit; 15 | using Microsoft.Extensions.FileProviders; 16 | using Xunit; 17 | 18 | namespace Microsoft.AspNetCore.StaticFiles 19 | { 20 | public class DefaultFilesMiddlewareTests 21 | { 22 | [Fact] 23 | public async Task NullArguments() 24 | { 25 | // No exception, default provided 26 | StaticFilesTestServer.Create(app => app.UseDefaultFiles(new DefaultFilesOptions { FileProvider = null })); 27 | 28 | // PathString(null) is OK. 29 | var server = StaticFilesTestServer.Create(app => app.UseDefaultFiles((string)null)); 30 | var response = await server.CreateClient().GetAsync("/"); 31 | Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); 32 | } 33 | 34 | [Theory] 35 | [InlineData("", @".", "/missing.dir")] 36 | [InlineData("", @".", "/missing.dir/")] 37 | [InlineData("/subdir", @".", "/subdir/missing.dir")] 38 | [InlineData("/subdir", @".", "/subdir/missing.dir/")] 39 | [InlineData("", @"./", "/missing.dir")] 40 | public async Task NoMatch_PassesThrough_All(string baseUrl, string baseDir, string requestUrl) 41 | { 42 | await NoMatch_PassesThrough(baseUrl, baseDir, requestUrl); 43 | } 44 | 45 | [ConditionalTheory] 46 | [OSSkipCondition(OperatingSystems.Linux)] 47 | [OSSkipCondition(OperatingSystems.MacOSX)] 48 | [InlineData("", @".\", "/missing.dir")] 49 | [InlineData("", @".\", "/Missing.dir")] 50 | public async Task NoMatch_PassesThrough_Windows(string baseUrl, string baseDir, string requestUrl) 51 | { 52 | await NoMatch_PassesThrough(baseUrl, baseDir, requestUrl); 53 | } 54 | 55 | private async Task NoMatch_PassesThrough(string baseUrl, string baseDir, string requestUrl) 56 | { 57 | using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) 58 | { 59 | var server = StaticFilesTestServer.Create(app => 60 | { 61 | app.UseDefaultFiles(new DefaultFilesOptions 62 | { 63 | RequestPath = new PathString(baseUrl), 64 | FileProvider = fileProvider 65 | }); 66 | app.Run(context => context.Response.WriteAsync(context.Request.Path.Value)); 67 | }); 68 | 69 | var response = await server.CreateClient().GetAsync(requestUrl); 70 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 71 | Assert.Equal(requestUrl, await response.Content.ReadAsStringAsync()); // Should not be modified 72 | } 73 | } 74 | 75 | [Theory] 76 | [InlineData("", @".", "/SubFolder/")] 77 | [InlineData("", @"./", "/SubFolder/")] 78 | [InlineData("", @"./SubFolder", "/")] 79 | [InlineData("", @"./SubFolder", "/你好/")] 80 | [InlineData("", @"./SubFolder", "/你好/世界/")] 81 | public async Task FoundDirectoryWithDefaultFile_PathModified_All(string baseUrl, string baseDir, string requestUrl) 82 | { 83 | await FoundDirectoryWithDefaultFile_PathModified(baseUrl, baseDir, requestUrl); 84 | } 85 | 86 | [ConditionalTheory] 87 | [OSSkipCondition(OperatingSystems.Linux)] 88 | [OSSkipCondition(OperatingSystems.MacOSX)] 89 | [InlineData("", @".\", "/SubFolder/")] 90 | [InlineData("", @".\subFolder", "/")] 91 | [InlineData("", @".\SubFolder", "/你好/")] 92 | [InlineData("", @".\SubFolder", "/你好/世界/")] 93 | public async Task FoundDirectoryWithDefaultFile_PathModified_Windows(string baseUrl, string baseDir, string requestUrl) 94 | { 95 | await FoundDirectoryWithDefaultFile_PathModified(baseUrl, baseDir, requestUrl); 96 | } 97 | 98 | private async Task FoundDirectoryWithDefaultFile_PathModified(string baseUrl, string baseDir, string requestUrl) 99 | { 100 | using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) 101 | { 102 | var server = StaticFilesTestServer.Create(app => 103 | { 104 | app.UseDefaultFiles(new DefaultFilesOptions 105 | { 106 | RequestPath = new PathString(baseUrl), 107 | FileProvider = fileProvider 108 | }); 109 | app.Run(context => context.Response.WriteAsync(context.Request.Path.Value)); 110 | }); 111 | 112 | var response = await server.CreateClient().GetAsync(requestUrl); 113 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 114 | Assert.Equal(requestUrl + "default.html", await response.Content.ReadAsStringAsync()); // Should be modified 115 | } 116 | } 117 | 118 | [Theory] 119 | [InlineData("", @".", "/SubFolder", "")] 120 | [InlineData("", @"./", "/SubFolder", "")] 121 | [InlineData("", @"./", "/SubFolder", "?a=b")] 122 | [InlineData("", @"./SubFolder", "/你好", "?a=b")] 123 | [InlineData("", @"./SubFolder", "/你好/世界", "?a=b")] 124 | public async Task NearMatch_RedirectAddSlash_All(string baseUrl, string baseDir, string requestUrl, string queryString) 125 | { 126 | await NearMatch_RedirectAddSlash(baseUrl, baseDir, requestUrl, queryString); 127 | } 128 | 129 | [ConditionalTheory] 130 | [OSSkipCondition(OperatingSystems.Linux)] 131 | [OSSkipCondition(OperatingSystems.MacOSX)] 132 | [InlineData("", @".\", "/SubFolder", "")] 133 | [InlineData("", @".\", "/SubFolder", "?a=b")] 134 | [InlineData("", @".\SubFolder", "/你好", "?a=b")] 135 | [InlineData("", @".\SubFolder", "/你好/世界", "?a=b")] 136 | public async Task NearMatch_RedirectAddSlash_Windows(string baseUrl, string baseDir, string requestUrl, string queryString) 137 | { 138 | await NearMatch_RedirectAddSlash(baseUrl, baseDir, requestUrl, queryString); 139 | } 140 | 141 | private async Task NearMatch_RedirectAddSlash(string baseUrl, string baseDir, string requestUrl, string queryString) 142 | { 143 | using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) 144 | { 145 | var server = StaticFilesTestServer.Create(app => app.UseDefaultFiles(new DefaultFilesOptions 146 | { 147 | RequestPath = new PathString(baseUrl), 148 | FileProvider = fileProvider 149 | })); 150 | var response = await server.CreateRequest(requestUrl + queryString).GetAsync(); 151 | 152 | Assert.Equal(HttpStatusCode.Moved, response.StatusCode); 153 | // the url in the header of `Location: /xxx/xxx` should be encoded 154 | var expectedURL = UriHelper.BuildRelative(baseUrl, requestUrl + "/", new QueryString(queryString), new FragmentString()); 155 | var actualURL = response.Headers.GetValues("Location").FirstOrDefault(); 156 | Assert.Equal(expectedURL, actualURL); 157 | Assert.Empty((await response.Content.ReadAsByteArrayAsync())); 158 | } 159 | } 160 | 161 | [Theory] 162 | [InlineData("/SubFolder", @"./", "/SubFolder/")] 163 | [InlineData("/SubFolder", @".", "/somedir/")] 164 | [InlineData("", @"./SubFolder", "/")] 165 | [InlineData("", @"./SubFolder/", "/")] 166 | public async Task PostDirectory_PassesThrough_All(string baseUrl, string baseDir, string requestUrl) 167 | { 168 | await PostDirectory_PassesThrough(baseUrl, baseDir, requestUrl); 169 | } 170 | 171 | [ConditionalTheory] 172 | [OSSkipCondition(OperatingSystems.Linux)] 173 | [OSSkipCondition(OperatingSystems.MacOSX)] 174 | [InlineData("/SubFolder", @".\", "/SubFolder/")] 175 | [InlineData("", @".\SubFolder", "/")] 176 | [InlineData("", @".\SubFolder\", "/")] 177 | public async Task PostDirectory_PassesThrough_Windows(string baseUrl, string baseDir, string requestUrl) 178 | { 179 | await PostDirectory_PassesThrough(baseUrl, baseDir, requestUrl); 180 | } 181 | 182 | private async Task PostDirectory_PassesThrough(string baseUrl, string baseDir, string requestUrl) 183 | { 184 | using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) 185 | { 186 | var server = StaticFilesTestServer.Create(app => app.UseDefaultFiles(new DefaultFilesOptions 187 | { 188 | RequestPath = new PathString(baseUrl), 189 | FileProvider = fileProvider 190 | })); 191 | var response = await server.CreateRequest(requestUrl).GetAsync(); 192 | 193 | Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); // Passed through 194 | } 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # 6 | # variables 7 | # 8 | 9 | RESET="\033[0m" 10 | RED="\033[0;31m" 11 | YELLOW="\033[0;33m" 12 | MAGENTA="\033[0;95m" 13 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 14 | [ -z "${DOTNET_HOME:-}" ] && DOTNET_HOME="$HOME/.dotnet" 15 | verbose=false 16 | update=false 17 | reinstall=false 18 | repo_path="$DIR" 19 | channel='' 20 | tools_source='' 21 | tools_source_suffix='' 22 | ci=false 23 | 24 | # 25 | # Functions 26 | # 27 | __usage() { 28 | echo "Usage: $(basename "${BASH_SOURCE[0]}") command [options] [[--] ...]" 29 | echo "" 30 | echo "Arguments:" 31 | echo " command The command to be run." 32 | echo " ... Arguments passed to the command. Variable number of arguments allowed." 33 | echo "" 34 | echo "Options:" 35 | echo " --verbose Show verbose output." 36 | echo " -c|--channel The channel of KoreBuild to download. Overrides the value from the config file.." 37 | echo " --config-file The path to the configuration file that stores values. Defaults to korebuild.json." 38 | echo " -d|--dotnet-home The directory where .NET Core tools will be stored. Defaults to '\$DOTNET_HOME' or '\$HOME/.dotnet." 39 | echo " --path The directory to build. Defaults to the directory containing the script." 40 | echo " -s|--tools-source|-ToolsSource The base url where build tools can be downloaded. Overrides the value from the config file." 41 | echo " --tools-source-suffix|-ToolsSourceSuffix The suffix to append to tools-source. Useful for query strings." 42 | echo " -u|--update Update to the latest KoreBuild even if the lock file is present." 43 | echo " --reinstall Reinstall KoreBuild." 44 | echo " --ci Apply CI specific settings and environment variables." 45 | echo "" 46 | echo "Description:" 47 | echo " This function will create a file \$DIR/korebuild-lock.txt. This lock file can be committed to source, but does not have to be." 48 | echo " When the lockfile is not present, KoreBuild will create one using latest available version from \$channel." 49 | 50 | if [[ "${1:-}" != '--no-exit' ]]; then 51 | exit 2 52 | fi 53 | } 54 | 55 | get_korebuild() { 56 | local version 57 | local lock_file="$repo_path/korebuild-lock.txt" 58 | if [ ! -f "$lock_file" ] || [ "$update" = true ]; then 59 | __get_remote_file "$tools_source/korebuild/channels/$channel/latest.txt" "$lock_file" "$tools_source_suffix" 60 | fi 61 | version="$(grep 'version:*' -m 1 "$lock_file")" 62 | if [[ "$version" == '' ]]; then 63 | __error "Failed to parse version from $lock_file. Expected a line that begins with 'version:'" 64 | return 1 65 | fi 66 | version="$(echo "${version#version:}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" 67 | local korebuild_path="$DOTNET_HOME/buildtools/korebuild/$version" 68 | 69 | if [ "$reinstall" = true ] && [ -d "$korebuild_path" ]; then 70 | rm -rf "$korebuild_path" 71 | fi 72 | 73 | { 74 | if [ ! -d "$korebuild_path" ]; then 75 | mkdir -p "$korebuild_path" 76 | local remote_path="$tools_source/korebuild/artifacts/$version/korebuild.$version.zip" 77 | tmpfile="$(mktemp)" 78 | echo -e "${MAGENTA}Downloading KoreBuild ${version}${RESET}" 79 | if __get_remote_file "$remote_path" "$tmpfile" "$tools_source_suffix"; then 80 | unzip -q -d "$korebuild_path" "$tmpfile" 81 | fi 82 | rm "$tmpfile" || true 83 | fi 84 | 85 | source "$korebuild_path/KoreBuild.sh" 86 | } || { 87 | if [ -d "$korebuild_path" ]; then 88 | echo "Cleaning up after failed installation" 89 | rm -rf "$korebuild_path" || true 90 | fi 91 | return 1 92 | } 93 | } 94 | 95 | __error() { 96 | echo -e "${RED}error: $*${RESET}" 1>&2 97 | } 98 | 99 | __warn() { 100 | echo -e "${YELLOW}warning: $*${RESET}" 101 | } 102 | 103 | __machine_has() { 104 | hash "$1" > /dev/null 2>&1 105 | return $? 106 | } 107 | 108 | __get_remote_file() { 109 | local remote_path=$1 110 | local local_path=$2 111 | local remote_path_suffix=$3 112 | 113 | if [[ "$remote_path" != 'http'* ]]; then 114 | cp "$remote_path" "$local_path" 115 | return 0 116 | fi 117 | 118 | local failed=false 119 | if __machine_has wget; then 120 | wget --tries 10 --quiet -O "$local_path" "${remote_path}${remote_path_suffix}" || failed=true 121 | else 122 | failed=true 123 | fi 124 | 125 | if [ "$failed" = true ] && __machine_has curl; then 126 | failed=false 127 | curl --retry 10 -sSL -f --create-dirs -o "$local_path" "${remote_path}${remote_path_suffix}" || failed=true 128 | fi 129 | 130 | if [ "$failed" = true ]; then 131 | __error "Download failed: $remote_path" 1>&2 132 | return 1 133 | fi 134 | } 135 | 136 | # 137 | # main 138 | # 139 | 140 | command="${1:-}" 141 | shift 142 | 143 | while [[ $# -gt 0 ]]; do 144 | case $1 in 145 | -\?|-h|--help) 146 | __usage --no-exit 147 | exit 0 148 | ;; 149 | -c|--channel|-Channel) 150 | shift 151 | channel="${1:-}" 152 | [ -z "$channel" ] && __usage 153 | ;; 154 | --config-file|-ConfigFile) 155 | shift 156 | config_file="${1:-}" 157 | [ -z "$config_file" ] && __usage 158 | if [ ! -f "$config_file" ]; then 159 | __error "Invalid value for --config-file. $config_file does not exist." 160 | exit 1 161 | fi 162 | ;; 163 | -d|--dotnet-home|-DotNetHome) 164 | shift 165 | DOTNET_HOME="${1:-}" 166 | [ -z "$DOTNET_HOME" ] && __usage 167 | ;; 168 | --path|-Path) 169 | shift 170 | repo_path="${1:-}" 171 | [ -z "$repo_path" ] && __usage 172 | ;; 173 | -s|--tools-source|-ToolsSource) 174 | shift 175 | tools_source="${1:-}" 176 | [ -z "$tools_source" ] && __usage 177 | ;; 178 | --tools-source-suffix|-ToolsSourceSuffix) 179 | shift 180 | tools_source_suffix="${1:-}" 181 | [ -z "$tools_source_suffix" ] && __usage 182 | ;; 183 | -u|--update|-Update) 184 | update=true 185 | ;; 186 | --reinstall|-[Rr]einstall) 187 | reinstall=true 188 | ;; 189 | --ci|-[Cc][Ii]) 190 | ci=true 191 | ;; 192 | --verbose|-Verbose) 193 | verbose=true 194 | ;; 195 | --) 196 | shift 197 | break 198 | ;; 199 | *) 200 | break 201 | ;; 202 | esac 203 | shift 204 | done 205 | 206 | if ! __machine_has unzip; then 207 | __error 'Missing required command: unzip' 208 | exit 1 209 | fi 210 | 211 | if ! __machine_has curl && ! __machine_has wget; then 212 | __error 'Missing required command. Either wget or curl is required.' 213 | exit 1 214 | fi 215 | 216 | [ -z "${config_file:-}" ] && config_file="$repo_path/korebuild.json" 217 | if [ -f "$config_file" ]; then 218 | if __machine_has jq ; then 219 | if jq '.' "$config_file" >/dev/null ; then 220 | config_channel="$(jq -r 'select(.channel!=null) | .channel' "$config_file")" 221 | config_tools_source="$(jq -r 'select(.toolsSource!=null) | .toolsSource' "$config_file")" 222 | else 223 | __error "$config_file contains invalid JSON." 224 | exit 1 225 | fi 226 | elif __machine_has python ; then 227 | if python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'))" >/dev/null ; then 228 | config_channel="$(python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['channel'] if 'channel' in obj else '')")" 229 | config_tools_source="$(python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['toolsSource'] if 'toolsSource' in obj else '')")" 230 | else 231 | __error "$config_file contains invalid JSON." 232 | exit 1 233 | fi 234 | elif __machine_has python3 ; then 235 | if python3 -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'))" >/dev/null ; then 236 | config_channel="$(python3 -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['channel'] if 'channel' in obj else '')")" 237 | config_tools_source="$(python3 -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['toolsSource'] if 'toolsSource' in obj else '')")" 238 | else 239 | __error "$config_file contains invalid JSON." 240 | exit 1 241 | fi 242 | else 243 | __error 'Missing required command: jq or python. Could not parse the JSON file.' 244 | exit 1 245 | fi 246 | 247 | [ ! -z "${config_channel:-}" ] && channel="$config_channel" 248 | [ ! -z "${config_tools_source:-}" ] && tools_source="$config_tools_source" 249 | fi 250 | 251 | [ -z "$channel" ] && channel='master' 252 | [ -z "$tools_source" ] && tools_source='https://aspnetcore.blob.core.windows.net/buildtools' 253 | 254 | get_korebuild 255 | set_korebuildsettings "$tools_source" "$DOTNET_HOME" "$repo_path" "$config_file" "$ci" 256 | invoke_korebuild_command "$command" "$@" 257 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.StaticFiles.FunctionalTests/StaticFileMiddlewareTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Net; 9 | using System.Net.Http; 10 | using System.Net.Sockets; 11 | using System.Text; 12 | using System.Threading; 13 | using System.Threading.Tasks; 14 | using Microsoft.AspNetCore.Builder; 15 | using Microsoft.AspNetCore.Hosting; 16 | using Microsoft.AspNetCore.Http; 17 | using Microsoft.AspNetCore.Server.IntegrationTesting; 18 | using Microsoft.AspNetCore.Server.IntegrationTesting.Common; 19 | using Microsoft.AspNetCore.Testing.xunit; 20 | using Microsoft.Extensions.DependencyInjection; 21 | using Microsoft.Extensions.Logging.Testing; 22 | using Xunit; 23 | 24 | namespace Microsoft.AspNetCore.StaticFiles 25 | { 26 | public class StaticFileMiddlewareTests : LoggedTest 27 | { 28 | [Fact] 29 | public async Task ReturnsNotFoundWithoutWwwroot() 30 | { 31 | var builder = new WebHostBuilder() 32 | .ConfigureServices(services => services.AddSingleton(LoggerFactory)) 33 | .UseKestrel() 34 | .Configure(app => app.UseStaticFiles()); 35 | 36 | using (var server = builder.Start(TestUrlHelper.GetTestUrl(ServerType.Kestrel))) 37 | { 38 | using (var client = new HttpClient { BaseAddress = new Uri(server.GetAddress()) }) 39 | { 40 | var response = await client.GetAsync("TestDocument.txt"); 41 | 42 | Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); 43 | } 44 | } 45 | } 46 | 47 | [Fact] 48 | public async Task FoundFile_LastModifiedTrimsSeconds() 49 | { 50 | var builder = new WebHostBuilder() 51 | .ConfigureServices(services => services.AddSingleton(LoggerFactory)) 52 | .UseKestrel() 53 | .UseWebRoot(AppContext.BaseDirectory) 54 | .Configure(app => app.UseStaticFiles()); 55 | 56 | using (var server = builder.Start(TestUrlHelper.GetTestUrl(ServerType.Kestrel))) 57 | { 58 | using (var client = new HttpClient { BaseAddress = new Uri(server.GetAddress()) }) 59 | { 60 | var last = File.GetLastWriteTimeUtc(Path.Combine(AppContext.BaseDirectory, "TestDocument.txt")); 61 | var response = await client.GetAsync("TestDocument.txt"); 62 | 63 | var trimmed = new DateTimeOffset(last.Year, last.Month, last.Day, last.Hour, last.Minute, last.Second, TimeSpan.Zero).ToUniversalTime(); 64 | 65 | Assert.Equal(response.Content.Headers.LastModified.Value, trimmed); 66 | } 67 | } 68 | } 69 | 70 | [Theory] 71 | [MemberData(nameof(ExistingFiles))] 72 | public async Task FoundFile_Served_All(string baseUrl, string baseDir, string requestUrl) 73 | { 74 | await FoundFile_Served(baseUrl, baseDir, requestUrl); 75 | } 76 | 77 | [ConditionalTheory] 78 | [OSSkipCondition(OperatingSystems.Linux)] 79 | [OSSkipCondition(OperatingSystems.MacOSX)] 80 | [InlineData("", @".", "/testDocument.Txt")] 81 | [InlineData("/somedir", @".", "/somedir/Testdocument.TXT")] 82 | [InlineData("/SomeDir", @".", "/soMediR/testdocument.txT")] 83 | [InlineData("/somedir", @"SubFolder", "/somedir/Ranges.tXt")] 84 | public async Task FoundFile_Served_Windows(string baseUrl, string baseDir, string requestUrl) 85 | { 86 | await FoundFile_Served(baseUrl, baseDir, requestUrl); 87 | } 88 | 89 | private async Task FoundFile_Served(string baseUrl, string baseDir, string requestUrl) 90 | { 91 | var builder = new WebHostBuilder() 92 | .ConfigureServices(services => services.AddSingleton(LoggerFactory)) 93 | .UseKestrel() 94 | .UseWebRoot(Path.Combine(AppContext.BaseDirectory, baseDir)) 95 | .Configure(app => app.UseStaticFiles(new StaticFileOptions 96 | { 97 | RequestPath = new PathString(baseUrl), 98 | })); 99 | 100 | using (var server = builder.Start(TestUrlHelper.GetTestUrl(ServerType.Kestrel))) 101 | { 102 | var hostingEnvironment = server.Services.GetService(); 103 | 104 | using (var client = new HttpClient { BaseAddress = new Uri(server.GetAddress()) }) 105 | { 106 | var fileInfo = hostingEnvironment.WebRootFileProvider.GetFileInfo(Path.GetFileName(requestUrl)); 107 | var response = await client.GetAsync(requestUrl); 108 | var responseContent = await response.Content.ReadAsByteArrayAsync(); 109 | 110 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 111 | Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); 112 | Assert.True(response.Content.Headers.ContentLength == fileInfo.Length); 113 | Assert.Equal(response.Content.Headers.ContentLength, responseContent.Length); 114 | 115 | using (var stream = fileInfo.CreateReadStream()) 116 | { 117 | var fileContents = new byte[stream.Length]; 118 | stream.Read(fileContents, 0, (int)stream.Length); 119 | Assert.True(responseContent.SequenceEqual(fileContents)); 120 | } 121 | } 122 | } 123 | } 124 | 125 | [Theory] 126 | [MemberData(nameof(ExistingFiles))] 127 | public async Task HeadFile_HeadersButNotBodyServed(string baseUrl, string baseDir, string requestUrl) 128 | { 129 | var builder = new WebHostBuilder() 130 | .ConfigureServices(services => services.AddSingleton(LoggerFactory)) 131 | .UseKestrel() 132 | .UseWebRoot(Path.Combine(AppContext.BaseDirectory, baseDir)) 133 | .Configure(app => app.UseStaticFiles(new StaticFileOptions 134 | { 135 | RequestPath = new PathString(baseUrl), 136 | })); 137 | 138 | using (var server = builder.Start(TestUrlHelper.GetTestUrl(ServerType.Kestrel))) 139 | { 140 | var hostingEnvironment = server.Services.GetService(); 141 | 142 | using (var client = new HttpClient { BaseAddress = new Uri(server.GetAddress()) }) 143 | { 144 | var fileInfo = hostingEnvironment.WebRootFileProvider.GetFileInfo(Path.GetFileName(requestUrl)); 145 | var request = new HttpRequestMessage(HttpMethod.Head, requestUrl); 146 | var response = await client.SendAsync(request); 147 | 148 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 149 | Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); 150 | Assert.True(response.Content.Headers.ContentLength == fileInfo.Length); 151 | Assert.Empty((await response.Content.ReadAsByteArrayAsync())); 152 | } 153 | } 154 | } 155 | 156 | public static IEnumerable ExistingFiles => new[] 157 | { 158 | new[] {"", @".", "/TestDocument.txt"}, 159 | new[] {"/somedir", @".", "/somedir/TestDocument.txt"}, 160 | new[] {"/SomeDir", @".", "/soMediR/TestDocument.txt"}, 161 | new[] {"", @"SubFolder", "/ranges.txt"}, 162 | new[] {"/somedir", @"SubFolder", "/somedir/ranges.txt"}, 163 | new[] {"", @"SubFolder", "/Empty.txt"} 164 | }; 165 | 166 | [Fact] 167 | public void ClientDisconnect_Kestrel_NoWriteExceptionThrown() 168 | { 169 | ClientDisconnect_NoWriteExceptionThrown(ServerType.Kestrel); 170 | } 171 | 172 | [ConditionalFact] 173 | [OSSkipCondition(OperatingSystems.Linux)] 174 | [OSSkipCondition(OperatingSystems.MacOSX)] 175 | public void ClientDisconnect_WebListener_NoWriteExceptionThrown() 176 | { 177 | ClientDisconnect_NoWriteExceptionThrown(ServerType.HttpSys); 178 | } 179 | 180 | private void ClientDisconnect_NoWriteExceptionThrown(ServerType serverType) 181 | { 182 | var interval = TimeSpan.FromSeconds(15); 183 | var requestReceived = new ManualResetEvent(false); 184 | var requestCancelled = new ManualResetEvent(false); 185 | var responseComplete = new ManualResetEvent(false); 186 | Exception exception = null; 187 | var builder = new WebHostBuilder() 188 | .ConfigureServices(services => services.AddSingleton(LoggerFactory)) 189 | .UseWebRoot(Path.Combine(AppContext.BaseDirectory)) 190 | .Configure(app => 191 | { 192 | app.Use(async (context, next) => 193 | { 194 | try 195 | { 196 | requestReceived.Set(); 197 | Assert.True(requestCancelled.WaitOne(interval), "not cancelled"); 198 | Assert.True(context.RequestAborted.WaitHandle.WaitOne(interval), "not aborted"); 199 | await next(); 200 | } 201 | catch (Exception ex) 202 | { 203 | exception = ex; 204 | } 205 | responseComplete.Set(); 206 | }); 207 | app.UseStaticFiles(); 208 | }); 209 | 210 | if (serverType == ServerType.HttpSys) 211 | { 212 | builder.UseHttpSys(); 213 | } 214 | else if (serverType == ServerType.Kestrel) 215 | { 216 | builder.UseKestrel(); 217 | } 218 | 219 | using (var server = builder.Start(TestUrlHelper.GetTestUrl(serverType))) 220 | { 221 | // We don't use HttpClient here because it's disconnect behavior varies across platforms. 222 | var socket = SendSocketRequestAsync(server.GetAddress(), "/TestDocument1MB.txt"); 223 | Assert.True(requestReceived.WaitOne(interval), "not received"); 224 | 225 | socket.LingerState = new LingerOption(true, 0); 226 | socket.Dispose(); 227 | requestCancelled.Set(); 228 | 229 | Assert.True(responseComplete.WaitOne(interval), "not completed"); 230 | Assert.Null(exception); 231 | } 232 | } 233 | 234 | private Socket SendSocketRequestAsync(string address, string path, string method = "GET") 235 | { 236 | var uri = new Uri(address); 237 | var builder = new StringBuilder(); 238 | builder.Append($"{method} {path} HTTP/1.1\r\n"); 239 | builder.Append($"HOST: {uri.Authority}\r\n\r\n"); 240 | 241 | byte[] request = Encoding.ASCII.GetBytes(builder.ToString()); 242 | 243 | var socket = new Socket(SocketType.Stream, ProtocolType.Tcp); 244 | socket.Connect(IPAddress.Loopback, uri.Port); 245 | socket.Send(request); 246 | return socket; 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright (c) .NET Foundation and Contributors 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.StaticFiles.Tests/DirectoryBrowserMiddlewareTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Net; 8 | using System.Net.Http; 9 | using System.Threading.Tasks; 10 | using Microsoft.AspNetCore.Builder; 11 | using Microsoft.AspNetCore.Http; 12 | using Microsoft.AspNetCore.TestHost; 13 | using Microsoft.AspNetCore.Testing.xunit; 14 | using Microsoft.Extensions.DependencyInjection; 15 | using Microsoft.Extensions.FileProviders; 16 | using Xunit; 17 | 18 | namespace Microsoft.AspNetCore.StaticFiles 19 | { 20 | public class DirectoryBrowserMiddlewareTests 21 | { 22 | [Fact] 23 | public void WorksWithoutEncoderRegistered() 24 | { 25 | // No exception, uses HtmlEncoder.Default 26 | StaticFilesTestServer.Create( 27 | app => app.UseDirectoryBrowser()); 28 | } 29 | 30 | [Fact] 31 | public async Task NullArguments() 32 | { 33 | // No exception, default provided 34 | StaticFilesTestServer.Create( 35 | app => app.UseDirectoryBrowser(new DirectoryBrowserOptions { Formatter = null }), 36 | services => services.AddDirectoryBrowser()); 37 | 38 | // No exception, default provided 39 | StaticFilesTestServer.Create( 40 | app => app.UseDirectoryBrowser(new DirectoryBrowserOptions { FileProvider = null }), 41 | services => services.AddDirectoryBrowser()); 42 | 43 | // PathString(null) is OK. 44 | var server = StaticFilesTestServer.Create( 45 | app => app.UseDirectoryBrowser((string)null), 46 | services => services.AddDirectoryBrowser()); 47 | 48 | var response = await server.CreateClient().GetAsync("/"); 49 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 50 | } 51 | 52 | [Theory] 53 | [InlineData("", @".", "/missing.dir")] 54 | [InlineData("", @".", "/missing.dir/")] 55 | [InlineData("/subdir", @".", "/subdir/missing.dir")] 56 | [InlineData("/subdir", @".", "/subdir/missing.dir/")] 57 | [InlineData("", @"./", "/missing.dir")] 58 | public async Task NoMatch_PassesThrough_All(string baseUrl, string baseDir, string requestUrl) 59 | { 60 | await NoMatch_PassesThrough(baseUrl, baseDir, requestUrl); 61 | } 62 | 63 | [ConditionalTheory] 64 | [OSSkipCondition(OperatingSystems.Linux)] 65 | [OSSkipCondition(OperatingSystems.MacOSX)] 66 | [InlineData("", @".\", "/missing.dir")] 67 | [InlineData("", @".\", "/Missing.dir")] 68 | public async Task NoMatch_PassesThrough_Windows(string baseUrl, string baseDir, string requestUrl) 69 | { 70 | await NoMatch_PassesThrough(baseUrl, baseDir, requestUrl); 71 | } 72 | 73 | private async Task NoMatch_PassesThrough(string baseUrl, string baseDir, string requestUrl) 74 | { 75 | using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) 76 | { 77 | var server = StaticFilesTestServer.Create( 78 | app => app.UseDirectoryBrowser(new DirectoryBrowserOptions 79 | { 80 | RequestPath = new PathString(baseUrl), 81 | FileProvider = fileProvider 82 | }), 83 | services => services.AddDirectoryBrowser()); 84 | var response = await server.CreateRequest(requestUrl).GetAsync(); 85 | Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); 86 | } 87 | } 88 | 89 | [Theory] 90 | [InlineData("", @".", "/")] 91 | [InlineData("", @".", "/SubFolder/")] 92 | [InlineData("/somedir", @".", "/somedir/")] 93 | [InlineData("/somedir", @"./", "/somedir/")] 94 | [InlineData("/somedir", @".", "/somedir/SubFolder/")] 95 | public async Task FoundDirectory_Served_All(string baseUrl, string baseDir, string requestUrl) 96 | { 97 | await FoundDirectory_Served(baseUrl, baseDir, requestUrl); 98 | } 99 | 100 | [ConditionalTheory] 101 | [OSSkipCondition(OperatingSystems.Linux)] 102 | [OSSkipCondition(OperatingSystems.MacOSX)] 103 | [InlineData("/somedir", @".\", "/somedir/")] 104 | [InlineData("/somedir", @".", "/somedir/subFolder/")] 105 | public async Task FoundDirectory_Served_Windows(string baseUrl, string baseDir, string requestUrl) 106 | { 107 | await FoundDirectory_Served(baseUrl, baseDir, requestUrl); 108 | } 109 | 110 | private async Task FoundDirectory_Served(string baseUrl, string baseDir, string requestUrl) 111 | { 112 | using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) 113 | { 114 | var server = StaticFilesTestServer.Create( 115 | app => app.UseDirectoryBrowser(new DirectoryBrowserOptions 116 | { 117 | RequestPath = new PathString(baseUrl), 118 | FileProvider = fileProvider 119 | }), 120 | services => services.AddDirectoryBrowser()); 121 | var response = await server.CreateRequest(requestUrl).GetAsync(); 122 | 123 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 124 | Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType.ToString()); 125 | Assert.True(response.Content.Headers.ContentLength > 0); 126 | Assert.Equal(response.Content.Headers.ContentLength, (await response.Content.ReadAsByteArrayAsync()).Length); 127 | } 128 | } 129 | 130 | [Theory] 131 | [InlineData("", @".", "/SubFolder", "")] 132 | [InlineData("/somedir", @".", "/somedir", "")] 133 | [InlineData("/somedir", @".", "/somedir/SubFolder", "")] 134 | [InlineData("", @".", "/SubFolder", "?a=b")] 135 | [InlineData("/somedir", @".", "/somedir", "?a=b")] 136 | [InlineData("/somedir", @".", "/somedir/SubFolder", "?a=b")] 137 | public async Task NearMatch_RedirectAddSlash_All(string baseUrl, string baseDir, string requestUrl, string queryString) 138 | { 139 | await NearMatch_RedirectAddSlash(baseUrl, baseDir, requestUrl, queryString); 140 | } 141 | 142 | [ConditionalTheory] 143 | [OSSkipCondition(OperatingSystems.Linux)] 144 | [OSSkipCondition(OperatingSystems.MacOSX)] 145 | [InlineData("/somedir", @".", "/somedir/subFolder", "")] 146 | [InlineData("/somedir", @".", "/somedir/subFolder", "?a=b")] 147 | public async Task NearMatch_RedirectAddSlash_Windows(string baseUrl, string baseDir, string requestUrl, string queryString) 148 | { 149 | await NearMatch_RedirectAddSlash(baseUrl, baseDir, requestUrl, queryString); 150 | } 151 | 152 | private async Task NearMatch_RedirectAddSlash(string baseUrl, string baseDir, string requestUrl, string queryString) 153 | { 154 | using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) 155 | { 156 | var server = StaticFilesTestServer.Create( 157 | app => app.UseDirectoryBrowser(new DirectoryBrowserOptions 158 | { 159 | RequestPath = new PathString(baseUrl), 160 | FileProvider = fileProvider 161 | }), 162 | services => services.AddDirectoryBrowser()); 163 | 164 | var response = await server.CreateRequest(requestUrl + queryString).GetAsync(); 165 | 166 | Assert.Equal(HttpStatusCode.Moved, response.StatusCode); 167 | Assert.Equal(requestUrl + "/" + queryString, response.Headers.GetValues("Location").FirstOrDefault()); 168 | Assert.Empty((await response.Content.ReadAsByteArrayAsync())); 169 | } 170 | } 171 | 172 | [Theory] 173 | [InlineData("", @".", "/")] 174 | [InlineData("", @".", "/SubFolder/")] 175 | [InlineData("/somedir", @".", "/somedir/")] 176 | [InlineData("/somedir", @".", "/somedir/SubFolder/")] 177 | public async Task PostDirectory_PassesThrough_All(string baseUrl, string baseDir, string requestUrl) 178 | { 179 | await PostDirectory_PassesThrough(baseUrl, baseDir, requestUrl); 180 | } 181 | 182 | [ConditionalTheory] 183 | [OSSkipCondition(OperatingSystems.Linux)] 184 | [OSSkipCondition(OperatingSystems.MacOSX)] 185 | [InlineData("/somedir", @".", "/somedir/subFolder/")] 186 | public async Task PostDirectory_PassesThrough_Windows(string baseUrl, string baseDir, string requestUrl) 187 | { 188 | await PostDirectory_PassesThrough(baseUrl, baseDir, requestUrl); 189 | } 190 | 191 | private async Task PostDirectory_PassesThrough(string baseUrl, string baseDir, string requestUrl) 192 | { 193 | using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) 194 | { 195 | var server = StaticFilesTestServer.Create( 196 | app => app.UseDirectoryBrowser(new DirectoryBrowserOptions 197 | { 198 | RequestPath = new PathString(baseUrl), 199 | FileProvider = fileProvider 200 | }), 201 | services => services.AddDirectoryBrowser()); 202 | 203 | var response = await server.CreateRequest(requestUrl).PostAsync(); 204 | Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); 205 | } 206 | } 207 | 208 | [Theory] 209 | [InlineData("", @".", "/")] 210 | [InlineData("", @".", "/SubFolder/")] 211 | [InlineData("/somedir", @".", "/somedir/")] 212 | [InlineData("/somedir", @".", "/somedir/SubFolder/")] 213 | public async Task HeadDirectory_HeadersButNotBodyServed_All(string baseUrl, string baseDir, string requestUrl) 214 | { 215 | await HeadDirectory_HeadersButNotBodyServed(baseUrl, baseDir, requestUrl); 216 | } 217 | 218 | [ConditionalTheory] 219 | [OSSkipCondition(OperatingSystems.Linux)] 220 | [OSSkipCondition(OperatingSystems.MacOSX)] 221 | [InlineData("/somedir", @".", "/somedir/subFolder/")] 222 | public async Task HeadDirectory_HeadersButNotBodyServed_Windows(string baseUrl, string baseDir, string requestUrl) 223 | { 224 | await HeadDirectory_HeadersButNotBodyServed(baseUrl, baseDir, requestUrl); 225 | } 226 | 227 | private async Task HeadDirectory_HeadersButNotBodyServed(string baseUrl, string baseDir, string requestUrl) 228 | { 229 | using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) 230 | { 231 | var server = StaticFilesTestServer.Create( 232 | app => app.UseDirectoryBrowser(new DirectoryBrowserOptions 233 | { 234 | RequestPath = new PathString(baseUrl), 235 | FileProvider = fileProvider 236 | }), 237 | services => services.AddDirectoryBrowser()); 238 | 239 | var response = await server.CreateRequest(requestUrl).SendAsync("HEAD"); 240 | 241 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 242 | Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType.ToString()); 243 | Assert.True(response.Content.Headers.ContentLength == 0); 244 | Assert.Empty((await response.Content.ReadAsByteArrayAsync())); 245 | } 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.StaticFiles.Tests/StaticFileMiddlewareTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Net; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using Microsoft.AspNetCore.Builder; 13 | using Microsoft.AspNetCore.Hosting; 14 | using Microsoft.AspNetCore.Http; 15 | using Microsoft.AspNetCore.Http.Features; 16 | using Microsoft.AspNetCore.TestHost; 17 | using Microsoft.AspNetCore.Testing.xunit; 18 | using Microsoft.Extensions.FileProviders; 19 | using Moq; 20 | using Xunit; 21 | 22 | namespace Microsoft.AspNetCore.StaticFiles 23 | { 24 | public class StaticFileMiddlewareTests 25 | { 26 | [Fact] 27 | public async Task ReturnsNotFoundWithoutWwwroot() 28 | { 29 | var builder = new WebHostBuilder() 30 | .Configure(app => app.UseStaticFiles()); 31 | var server = new TestServer(builder); 32 | 33 | var response = await server.CreateClient().GetAsync("/ranges.txt"); 34 | 35 | Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); 36 | Assert.Null(response.Headers.ETag); 37 | } 38 | 39 | [ConditionalFact] 40 | [OSSkipCondition(OperatingSystems.Windows, SkipReason = "Symlinks not supported on Windows")] 41 | public async Task ReturnsNotFoundForBrokenSymlink() 42 | { 43 | var badLink = Path.Combine(AppContext.BaseDirectory, Path.GetRandomFileName() + ".txt"); 44 | 45 | Process.Start("ln", $"-s \"/tmp/{Path.GetRandomFileName()}\" \"{badLink}\"").WaitForExit(); 46 | Assert.True(File.Exists(badLink), "Should have created a symlink"); 47 | 48 | try 49 | { 50 | var builder = new WebHostBuilder() 51 | .Configure(app => app.UseStaticFiles(new StaticFileOptions { ServeUnknownFileTypes = true })) 52 | .UseWebRoot(AppContext.BaseDirectory); 53 | var server = new TestServer(builder); 54 | 55 | var response = await server.CreateClient().GetAsync(Path.GetFileName(badLink)); 56 | 57 | Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); 58 | Assert.Null(response.Headers.ETag); 59 | } 60 | finally 61 | { 62 | File.Delete(badLink); 63 | } 64 | } 65 | 66 | [Fact] 67 | public async Task ReturnsNotFoundIfSendFileThrows() 68 | { 69 | var mockSendFile = new Mock(); 70 | mockSendFile.Setup(m => m.SendFileAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) 71 | .ThrowsAsync(new FileNotFoundException()); 72 | var builder = new WebHostBuilder() 73 | .Configure(app => 74 | { 75 | app.Use(async (ctx, next) => 76 | { 77 | ctx.Features.Set(mockSendFile.Object); 78 | await next(); 79 | }); 80 | app.UseStaticFiles(new StaticFileOptions { ServeUnknownFileTypes = true }); 81 | }) 82 | .UseWebRoot(AppContext.BaseDirectory); 83 | var server = new TestServer(builder); 84 | 85 | var response = await server.CreateClient().GetAsync("TestDocument.txt"); 86 | 87 | Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); 88 | Assert.Null(response.Headers.ETag); 89 | } 90 | 91 | [Fact] 92 | public async Task FoundFile_LastModifiedTrimsSeconds() 93 | { 94 | using (var fileProvider = new PhysicalFileProvider(AppContext.BaseDirectory)) 95 | { 96 | var server = StaticFilesTestServer.Create(app => app.UseStaticFiles(new StaticFileOptions 97 | { 98 | FileProvider = fileProvider 99 | })); 100 | var fileInfo = fileProvider.GetFileInfo("TestDocument.txt"); 101 | var response = await server.CreateRequest("TestDocument.txt").GetAsync(); 102 | 103 | var last = fileInfo.LastModified; 104 | var trimmed = new DateTimeOffset(last.Year, last.Month, last.Day, last.Hour, last.Minute, last.Second, last.Offset).ToUniversalTime(); 105 | 106 | Assert.Equal(response.Content.Headers.LastModified.Value, trimmed); 107 | } 108 | } 109 | 110 | [Fact] 111 | public async Task NullArguments() 112 | { 113 | // No exception, default provided 114 | StaticFilesTestServer.Create(app => app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = null })); 115 | 116 | // No exception, default provided 117 | StaticFilesTestServer.Create(app => app.UseStaticFiles(new StaticFileOptions { FileProvider = null })); 118 | 119 | // PathString(null) is OK. 120 | var server = StaticFilesTestServer.Create(app => app.UseStaticFiles((string)null)); 121 | var response = await server.CreateClient().GetAsync("/"); 122 | Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); 123 | } 124 | 125 | [Theory] 126 | [MemberData(nameof(ExistingFiles))] 127 | public async Task FoundFile_Served_All(string baseUrl, string baseDir, string requestUrl) 128 | { 129 | await FoundFile_Served(baseUrl, baseDir, requestUrl); 130 | } 131 | 132 | [ConditionalTheory] 133 | [OSSkipCondition(OperatingSystems.Linux)] 134 | [OSSkipCondition(OperatingSystems.MacOSX)] 135 | [InlineData("", @".", "/testDocument.Txt")] 136 | [InlineData("/somedir", @".", "/somedir/Testdocument.TXT")] 137 | [InlineData("/SomeDir", @".", "/soMediR/testdocument.txT")] 138 | [InlineData("/somedir", @"SubFolder", "/somedir/Ranges.tXt")] 139 | public async Task FoundFile_Served_Windows(string baseUrl, string baseDir, string requestUrl) 140 | { 141 | await FoundFile_Served(baseUrl, baseDir, requestUrl); 142 | } 143 | 144 | private async Task FoundFile_Served(string baseUrl, string baseDir, string requestUrl) 145 | { 146 | using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) 147 | { 148 | var server = StaticFilesTestServer.Create(app => app.UseStaticFiles(new StaticFileOptions 149 | { 150 | RequestPath = new PathString(baseUrl), 151 | FileProvider = fileProvider 152 | })); 153 | var fileInfo = fileProvider.GetFileInfo(Path.GetFileName(requestUrl)); 154 | var response = await server.CreateRequest(requestUrl).GetAsync(); 155 | var responseContent = await response.Content.ReadAsByteArrayAsync(); 156 | 157 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 158 | Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); 159 | Assert.True(response.Content.Headers.ContentLength == fileInfo.Length); 160 | Assert.Equal(response.Content.Headers.ContentLength, responseContent.Length); 161 | Assert.NotNull(response.Headers.ETag); 162 | 163 | using (var stream = fileInfo.CreateReadStream()) 164 | { 165 | var fileContents = new byte[stream.Length]; 166 | stream.Read(fileContents, 0, (int)stream.Length); 167 | Assert.True(responseContent.SequenceEqual(fileContents)); 168 | } 169 | } 170 | } 171 | 172 | [Theory] 173 | [MemberData(nameof(ExistingFiles))] 174 | public async Task HeadFile_HeadersButNotBodyServed(string baseUrl, string baseDir, string requestUrl) 175 | { 176 | using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) 177 | { 178 | var server = StaticFilesTestServer.Create(app => app.UseStaticFiles(new StaticFileOptions 179 | { 180 | RequestPath = new PathString(baseUrl), 181 | FileProvider = fileProvider 182 | })); 183 | var fileInfo = fileProvider.GetFileInfo(Path.GetFileName(requestUrl)); 184 | var response = await server.CreateRequest(requestUrl).SendAsync("HEAD"); 185 | 186 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 187 | Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); 188 | Assert.True(response.Content.Headers.ContentLength == fileInfo.Length); 189 | Assert.Empty((await response.Content.ReadAsByteArrayAsync())); 190 | } 191 | } 192 | 193 | [Theory] 194 | [MemberData(nameof(MissingFiles))] 195 | public async Task Get_NoMatch_PassesThrough(string baseUrl, string baseDir, string requestUrl) => 196 | await PassesThrough("GET", baseUrl, baseDir, requestUrl); 197 | 198 | [Theory] 199 | [MemberData(nameof(MissingFiles))] 200 | public async Task Head_NoMatch_PassesThrough(string baseUrl, string baseDir, string requestUrl) => 201 | await PassesThrough("HEAD", baseUrl, baseDir, requestUrl); 202 | 203 | [Theory] 204 | [MemberData(nameof(MissingFiles))] 205 | public async Task Unknown_NoMatch_PassesThrough(string baseUrl, string baseDir, string requestUrl) => 206 | await PassesThrough("VERB", baseUrl, baseDir, requestUrl); 207 | 208 | [Theory] 209 | [MemberData(nameof(ExistingFiles))] 210 | public async Task Options_Match_PassesThrough(string baseUrl, string baseDir, string requestUrl) => 211 | await PassesThrough("OPTIONS", baseUrl, baseDir, requestUrl); 212 | 213 | [Theory] 214 | [MemberData(nameof(ExistingFiles))] 215 | public async Task Trace_Match_PassesThrough(string baseUrl, string baseDir, string requestUrl) => 216 | await PassesThrough("TRACE", baseUrl, baseDir, requestUrl); 217 | 218 | [Theory] 219 | [MemberData(nameof(ExistingFiles))] 220 | public async Task Post_Match_PassesThrough(string baseUrl, string baseDir, string requestUrl) => 221 | await PassesThrough("POST", baseUrl, baseDir, requestUrl); 222 | 223 | [Theory] 224 | [MemberData(nameof(ExistingFiles))] 225 | public async Task Put_Match_PassesThrough(string baseUrl, string baseDir, string requestUrl) => 226 | await PassesThrough("PUT", baseUrl, baseDir, requestUrl); 227 | 228 | [Theory] 229 | [MemberData(nameof(ExistingFiles))] 230 | public async Task Unknown_Match_PassesThrough(string baseUrl, string baseDir, string requestUrl) => 231 | await PassesThrough("VERB", baseUrl, baseDir, requestUrl); 232 | 233 | private async Task PassesThrough(string method, string baseUrl, string baseDir, string requestUrl) 234 | { 235 | using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, baseDir))) 236 | { 237 | var server = StaticFilesTestServer.Create(app => app.UseStaticFiles(new StaticFileOptions 238 | { 239 | RequestPath = new PathString(baseUrl), 240 | FileProvider = fileProvider 241 | })); 242 | var response = await server.CreateRequest(requestUrl).SendAsync(method); 243 | Assert.Null(response.Content.Headers.LastModified); 244 | Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); 245 | } 246 | } 247 | 248 | public static IEnumerable MissingFiles => new[] 249 | { 250 | new[] {"", @".", "/missing.file"}, 251 | new[] {"/subdir", @".", "/subdir/missing.file"}, 252 | new[] {"/missing.file", @"./", "/missing.file"}, 253 | new[] {"", @"./", "/xunit.xml"} 254 | }; 255 | 256 | public static IEnumerable ExistingFiles => new[] 257 | { 258 | new[] {"", @".", "/TestDocument.txt"}, 259 | new[] {"/somedir", @".", "/somedir/TestDocument.txt"}, 260 | new[] {"/SomeDir", @".", "/soMediR/TestDocument.txt"}, 261 | new[] {"", @"SubFolder", "/ranges.txt"}, 262 | new[] {"/somedir", @"SubFolder", "/somedir/ranges.txt"}, 263 | new[] {"", @"SubFolder", "/Empty.txt"} 264 | }; 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.StaticFiles/StaticFileContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using Microsoft.AspNetCore.Builder; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.AspNetCore.Http.Extensions; 12 | using Microsoft.AspNetCore.Http.Features; 13 | using Microsoft.AspNetCore.Http.Headers; 14 | using Microsoft.AspNetCore.Internal; 15 | using Microsoft.Extensions.FileProviders; 16 | using Microsoft.Extensions.Logging; 17 | using Microsoft.Net.Http.Headers; 18 | 19 | namespace Microsoft.AspNetCore.StaticFiles 20 | { 21 | internal struct StaticFileContext 22 | { 23 | private const int StreamCopyBufferSize = 64 * 1024; 24 | private readonly HttpContext _context; 25 | private readonly StaticFileOptions _options; 26 | private readonly PathString _matchUrl; 27 | private readonly HttpRequest _request; 28 | private readonly HttpResponse _response; 29 | private readonly ILogger _logger; 30 | private readonly IFileProvider _fileProvider; 31 | private readonly IContentTypeProvider _contentTypeProvider; 32 | private string _method; 33 | private bool _isGet; 34 | private bool _isHead; 35 | private PathString _subPath; 36 | private string _contentType; 37 | private IFileInfo _fileInfo; 38 | private long _length; 39 | private DateTimeOffset _lastModified; 40 | private EntityTagHeaderValue _etag; 41 | 42 | private RequestHeaders _requestHeaders; 43 | private ResponseHeaders _responseHeaders; 44 | 45 | private PreconditionState _ifMatchState; 46 | private PreconditionState _ifNoneMatchState; 47 | private PreconditionState _ifModifiedSinceState; 48 | private PreconditionState _ifUnmodifiedSinceState; 49 | 50 | private RangeItemHeaderValue _range; 51 | private bool _isRangeRequest; 52 | 53 | public StaticFileContext(HttpContext context, StaticFileOptions options, PathString matchUrl, ILogger logger, IFileProvider fileProvider, IContentTypeProvider contentTypeProvider) 54 | { 55 | _context = context; 56 | _options = options; 57 | _matchUrl = matchUrl; 58 | _request = context.Request; 59 | _response = context.Response; 60 | _logger = logger; 61 | _requestHeaders = _request.GetTypedHeaders(); 62 | _responseHeaders = _response.GetTypedHeaders(); 63 | _fileProvider = fileProvider; 64 | _contentTypeProvider = contentTypeProvider; 65 | 66 | _method = null; 67 | _isGet = false; 68 | _isHead = false; 69 | _subPath = PathString.Empty; 70 | _contentType = null; 71 | _fileInfo = null; 72 | _length = 0; 73 | _lastModified = new DateTimeOffset(); 74 | _etag = null; 75 | _ifMatchState = PreconditionState.Unspecified; 76 | _ifNoneMatchState = PreconditionState.Unspecified; 77 | _ifModifiedSinceState = PreconditionState.Unspecified; 78 | _ifUnmodifiedSinceState = PreconditionState.Unspecified; 79 | _range = null; 80 | _isRangeRequest = false; 81 | } 82 | 83 | internal enum PreconditionState 84 | { 85 | Unspecified, 86 | NotModified, 87 | ShouldProcess, 88 | PreconditionFailed 89 | } 90 | 91 | public bool IsHeadMethod 92 | { 93 | get { return _isHead; } 94 | } 95 | 96 | public bool IsRangeRequest 97 | { 98 | get { return _isRangeRequest; } 99 | } 100 | 101 | public string SubPath 102 | { 103 | get { return _subPath.Value; } 104 | } 105 | 106 | public string PhysicalPath 107 | { 108 | get { return _fileInfo?.PhysicalPath; } 109 | } 110 | 111 | public bool ValidateMethod() 112 | { 113 | _method = _request.Method; 114 | _isGet = HttpMethods.IsGet(_method); 115 | _isHead = HttpMethods.IsHead(_method); 116 | return _isGet || _isHead; 117 | } 118 | 119 | // Check if the URL matches any expected paths 120 | public bool ValidatePath() 121 | { 122 | return Helpers.TryMatchPath(_context, _matchUrl, forDirectory: false, subpath: out _subPath); 123 | } 124 | 125 | public bool LookupContentType() 126 | { 127 | if (_contentTypeProvider.TryGetContentType(_subPath.Value, out _contentType)) 128 | { 129 | return true; 130 | } 131 | 132 | if (_options.ServeUnknownFileTypes) 133 | { 134 | _contentType = _options.DefaultContentType; 135 | return true; 136 | } 137 | 138 | return false; 139 | } 140 | 141 | public bool LookupFileInfo() 142 | { 143 | _fileInfo = _fileProvider.GetFileInfo(_subPath.Value); 144 | if (_fileInfo.Exists) 145 | { 146 | _length = _fileInfo.Length; 147 | 148 | DateTimeOffset last = _fileInfo.LastModified; 149 | // Truncate to the second. 150 | _lastModified = new DateTimeOffset(last.Year, last.Month, last.Day, last.Hour, last.Minute, last.Second, last.Offset).ToUniversalTime(); 151 | 152 | long etagHash = _lastModified.ToFileTime() ^ _length; 153 | _etag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"'); 154 | } 155 | return _fileInfo.Exists; 156 | } 157 | 158 | public void ComprehendRequestHeaders() 159 | { 160 | ComputeIfMatch(); 161 | 162 | ComputeIfModifiedSince(); 163 | 164 | ComputeRange(); 165 | 166 | ComputeIfRange(); 167 | } 168 | 169 | private void ComputeIfMatch() 170 | { 171 | // 14.24 If-Match 172 | var ifMatch = _requestHeaders.IfMatch; 173 | if (ifMatch != null && ifMatch.Any()) 174 | { 175 | _ifMatchState = PreconditionState.PreconditionFailed; 176 | foreach (var etag in ifMatch) 177 | { 178 | if (etag.Equals(EntityTagHeaderValue.Any) || etag.Compare(_etag, useStrongComparison: true)) 179 | { 180 | _ifMatchState = PreconditionState.ShouldProcess; 181 | break; 182 | } 183 | } 184 | } 185 | 186 | // 14.26 If-None-Match 187 | var ifNoneMatch = _requestHeaders.IfNoneMatch; 188 | if (ifNoneMatch != null && ifNoneMatch.Any()) 189 | { 190 | _ifNoneMatchState = PreconditionState.ShouldProcess; 191 | foreach (var etag in ifNoneMatch) 192 | { 193 | if (etag.Equals(EntityTagHeaderValue.Any) || etag.Compare(_etag, useStrongComparison: true)) 194 | { 195 | _ifNoneMatchState = PreconditionState.NotModified; 196 | break; 197 | } 198 | } 199 | } 200 | } 201 | 202 | private void ComputeIfModifiedSince() 203 | { 204 | var now = DateTimeOffset.UtcNow; 205 | 206 | // 14.25 If-Modified-Since 207 | var ifModifiedSince = _requestHeaders.IfModifiedSince; 208 | if (ifModifiedSince.HasValue && ifModifiedSince <= now) 209 | { 210 | bool modified = ifModifiedSince < _lastModified; 211 | _ifModifiedSinceState = modified ? PreconditionState.ShouldProcess : PreconditionState.NotModified; 212 | } 213 | 214 | // 14.28 If-Unmodified-Since 215 | var ifUnmodifiedSince = _requestHeaders.IfUnmodifiedSince; 216 | if (ifUnmodifiedSince.HasValue && ifUnmodifiedSince <= now) 217 | { 218 | bool unmodified = ifUnmodifiedSince >= _lastModified; 219 | _ifUnmodifiedSinceState = unmodified ? PreconditionState.ShouldProcess : PreconditionState.PreconditionFailed; 220 | } 221 | } 222 | 223 | private void ComputeIfRange() 224 | { 225 | // 14.27 If-Range 226 | var ifRangeHeader = _requestHeaders.IfRange; 227 | if (ifRangeHeader != null) 228 | { 229 | // If the validator given in the If-Range header field matches the 230 | // current validator for the selected representation of the target 231 | // resource, then the server SHOULD process the Range header field as 232 | // requested. If the validator does not match, the server MUST ignore 233 | // the Range header field. 234 | if (ifRangeHeader.LastModified.HasValue) 235 | { 236 | if (_lastModified !=null && _lastModified > ifRangeHeader.LastModified) 237 | { 238 | _isRangeRequest = false; 239 | } 240 | } 241 | else if (_etag != null && ifRangeHeader.EntityTag != null && !ifRangeHeader.EntityTag.Compare(_etag, useStrongComparison: true)) 242 | { 243 | _isRangeRequest = false; 244 | } 245 | } 246 | } 247 | 248 | private void ComputeRange() 249 | { 250 | // 14.35 Range 251 | // http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-24 252 | 253 | // A server MUST ignore a Range header field received with a request method other 254 | // than GET. 255 | if (!_isGet) 256 | { 257 | return; 258 | } 259 | 260 | (_isRangeRequest, _range) = RangeHelper.ParseRange(_context, _requestHeaders, _length, _logger); 261 | } 262 | 263 | public void ApplyResponseHeaders(int statusCode) 264 | { 265 | _response.StatusCode = statusCode; 266 | if (statusCode < 400) 267 | { 268 | // these headers are returned for 200, 206, and 304 269 | // they are not returned for 412 and 416 270 | if (!string.IsNullOrEmpty(_contentType)) 271 | { 272 | _response.ContentType = _contentType; 273 | } 274 | _responseHeaders.LastModified = _lastModified; 275 | _responseHeaders.ETag = _etag; 276 | _responseHeaders.Headers[HeaderNames.AcceptRanges] = "bytes"; 277 | } 278 | if (statusCode == Constants.Status200Ok) 279 | { 280 | // this header is only returned here for 200 281 | // it already set to the returned range for 206 282 | // it is not returned for 304, 412, and 416 283 | _response.ContentLength = _length; 284 | } 285 | 286 | _options.OnPrepareResponse(new StaticFileResponseContext(_context, _fileInfo)); 287 | } 288 | 289 | public PreconditionState GetPreconditionState() 290 | { 291 | return GetMaxPreconditionState(_ifMatchState, _ifNoneMatchState, 292 | _ifModifiedSinceState, _ifUnmodifiedSinceState); 293 | } 294 | 295 | private static PreconditionState GetMaxPreconditionState(params PreconditionState[] states) 296 | { 297 | PreconditionState max = PreconditionState.Unspecified; 298 | for (int i = 0; i < states.Length; i++) 299 | { 300 | if (states[i] > max) 301 | { 302 | max = states[i]; 303 | } 304 | } 305 | return max; 306 | } 307 | 308 | public Task SendStatusAsync(int statusCode) 309 | { 310 | ApplyResponseHeaders(statusCode); 311 | 312 | _logger.LogHandled(statusCode, SubPath); 313 | return Task.CompletedTask; 314 | } 315 | 316 | public async Task SendAsync() 317 | { 318 | ApplyResponseHeaders(Constants.Status200Ok); 319 | string physicalPath = _fileInfo.PhysicalPath; 320 | var sendFile = _context.Features.Get(); 321 | if (sendFile != null && !string.IsNullOrEmpty(physicalPath)) 322 | { 323 | // We don't need to directly cancel this, if the client disconnects it will fail silently. 324 | await sendFile.SendFileAsync(physicalPath, 0, _length, CancellationToken.None); 325 | return; 326 | } 327 | 328 | try 329 | { 330 | using (var readStream = _fileInfo.CreateReadStream()) 331 | { 332 | // Larger StreamCopyBufferSize is required because in case of FileStream readStream isn't going to be buffering 333 | await StreamCopyOperation.CopyToAsync(readStream, _response.Body, _length, StreamCopyBufferSize, _context.RequestAborted); 334 | } 335 | } 336 | catch (OperationCanceledException ex) 337 | { 338 | _logger.LogWriteCancelled(ex); 339 | // Don't throw this exception, it's most likely caused by the client disconnecting. 340 | // However, if it was cancelled for any other reason we need to prevent empty responses. 341 | _context.Abort(); 342 | } 343 | } 344 | 345 | // When there is only a single range the bytes are sent directly in the body. 346 | internal async Task SendRangeAsync() 347 | { 348 | if (_range == null) 349 | { 350 | // 14.16 Content-Range - A server sending a response with status code 416 (Requested range not satisfiable) 351 | // SHOULD include a Content-Range field with a byte-range-resp-spec of "*". The instance-length specifies 352 | // the current length of the selected resource. e.g. */length 353 | _responseHeaders.ContentRange = new ContentRangeHeaderValue(_length); 354 | ApplyResponseHeaders(Constants.Status416RangeNotSatisfiable); 355 | 356 | _logger.LogRangeNotSatisfiable(SubPath); 357 | return; 358 | } 359 | 360 | _responseHeaders.ContentRange = ComputeContentRange(_range, out var start, out var length); 361 | _response.ContentLength = length; 362 | ApplyResponseHeaders(Constants.Status206PartialContent); 363 | 364 | string physicalPath = _fileInfo.PhysicalPath; 365 | var sendFile = _context.Features.Get(); 366 | if (sendFile != null && !string.IsNullOrEmpty(physicalPath)) 367 | { 368 | _logger.LogSendingFileRange(_response.Headers[HeaderNames.ContentRange], physicalPath); 369 | // We don't need to directly cancel this, if the client disconnects it will fail silently. 370 | await sendFile.SendFileAsync(physicalPath, start, length, CancellationToken.None); 371 | return; 372 | } 373 | 374 | try 375 | { 376 | using (var readStream = _fileInfo.CreateReadStream()) 377 | { 378 | readStream.Seek(start, SeekOrigin.Begin); // TODO: What if !CanSeek? 379 | _logger.LogCopyingFileRange(_response.Headers[HeaderNames.ContentRange], SubPath); 380 | await StreamCopyOperation.CopyToAsync(readStream, _response.Body, length, _context.RequestAborted); 381 | } 382 | } 383 | catch (OperationCanceledException ex) 384 | { 385 | _logger.LogWriteCancelled(ex); 386 | // Don't throw this exception, it's most likely caused by the client disconnecting. 387 | // However, if it was cancelled for any other reason we need to prevent empty responses. 388 | _context.Abort(); 389 | } 390 | } 391 | 392 | // Note: This assumes ranges have been normalized to absolute byte offsets. 393 | private ContentRangeHeaderValue ComputeContentRange(RangeItemHeaderValue range, out long start, out long length) 394 | { 395 | start = range.From.Value; 396 | long end = range.To.Value; 397 | length = end - start + 1; 398 | return new ContentRangeHeaderValue(start, end, _length); 399 | } 400 | } 401 | } 402 | --------------------------------------------------------------------------------