From 7e63e2da43c3a5f298e782482b2005179b8d2792 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 5 Mar 2019 12:42:42 -0800 Subject: [PATCH] Make StaticFiles Noop for middleware Teaches all of the static files middleware (incl default files, directory browser) to noop when an endpoint is selected. This is desirable so you can place them after routing if you want with no ill effect. --- .../StaticFiles/src/DefaultFilesMiddleware.cs | 4 +- .../src/DirectoryBrowserMiddleware.cs | 6 ++- .../StaticFiles/src/LoggerExtensions.cs | 10 +++++ .../StaticFiles/src/StaticFileContext.cs | 7 ++++ .../StaticFiles/src/StaticFileMiddleware.cs | 6 ++- .../StaticFileMiddlewareTests.cs | 39 +++++++++++++++++++ .../UnitTests/DefaultFilesMiddlewareTests.cs | 39 ++++++++++++++++++- .../DirectoryBrowserMiddlewareTests.cs | 38 ++++++++++++++++++ 8 files changed, 144 insertions(+), 5 deletions(-) diff --git a/src/Middleware/StaticFiles/src/DefaultFilesMiddleware.cs b/src/Middleware/StaticFiles/src/DefaultFilesMiddleware.cs index 1f020dbe1c..627c4b2b5f 100644 --- a/src/Middleware/StaticFiles/src/DefaultFilesMiddleware.cs +++ b/src/Middleware/StaticFiles/src/DefaultFilesMiddleware.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Endpoints; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; @@ -62,7 +63,8 @@ namespace Microsoft.AspNetCore.StaticFiles /// public Task Invoke(HttpContext context) { - if (Helpers.IsGetOrHeadMethod(context.Request.Method) + if (context.GetEndpoint() == null && + Helpers.IsGetOrHeadMethod(context.Request.Method) && Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out var subpath)) { var dirContents = _fileProvider.GetDirectoryContents(subpath.Value); diff --git a/src/Middleware/StaticFiles/src/DirectoryBrowserMiddleware.cs b/src/Middleware/StaticFiles/src/DirectoryBrowserMiddleware.cs index 1ac950ebe9..cc9c9118a6 100644 --- a/src/Middleware/StaticFiles/src/DirectoryBrowserMiddleware.cs +++ b/src/Middleware/StaticFiles/src/DirectoryBrowserMiddleware.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Endpoints; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; @@ -78,8 +79,9 @@ namespace Microsoft.AspNetCore.StaticFiles /// public Task Invoke(HttpContext context) { - // Check if the URL matches any expected paths - if (Helpers.IsGetOrHeadMethod(context.Request.Method) + // Check if the URL matches any expected paths, skip if an endpoint was selected + if (context.GetEndpoint() == null && + Helpers.IsGetOrHeadMethod(context.Request.Method) && Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out var subpath) && TryGetDirectoryInfo(subpath, out var contents)) { diff --git a/src/Middleware/StaticFiles/src/LoggerExtensions.cs b/src/Middleware/StaticFiles/src/LoggerExtensions.cs index 56afbeb5eb..8630cf8fa0 100644 --- a/src/Middleware/StaticFiles/src/LoggerExtensions.cs +++ b/src/Middleware/StaticFiles/src/LoggerExtensions.cs @@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.StaticFiles private static Action _sendingFileRange; private static Action _copyingFileRange; private static Action _writeCancelled; + private static Action _endpointMatched; static LoggerExtensions() { @@ -75,6 +76,10 @@ namespace Microsoft.AspNetCore.StaticFiles logLevel: LogLevel.Debug, eventId: new EventId(14, "WriteCancelled"), formatString: "The file transmission was cancelled"); + _endpointMatched = LoggerMessage.Define( + logLevel: LogLevel.Debug, + eventId: new EventId(15, "EndpointMatched"), + formatString: "Static files was skipped as the request already matched an endpoint."); } public static void RequestMethodNotSupported(this ILogger logger, string method) @@ -91,6 +96,11 @@ namespace Microsoft.AspNetCore.StaticFiles _fileServed(logger, virtualPath, physicalPath, null); } + public static void EndpointMatched(this ILogger logger) + { + _endpointMatched(logger, null); + } + public static void PathMismatch(this ILogger logger, string path) { _pathMismatch(logger, path, null); diff --git a/src/Middleware/StaticFiles/src/StaticFileContext.cs b/src/Middleware/StaticFiles/src/StaticFileContext.cs index 7473db4f36..e38b600f7b 100644 --- a/src/Middleware/StaticFiles/src/StaticFileContext.cs +++ b/src/Middleware/StaticFiles/src/StaticFileContext.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Endpoints; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Headers; @@ -108,6 +109,12 @@ namespace Microsoft.AspNetCore.StaticFiles get { return _fileInfo?.PhysicalPath; } } + public bool ValidateNoEndpoint() + { + // Return true because we only want to run if there is no endpoint. + return _context.GetEndpoint() == null; + } + public bool ValidateMethod() { _method = _request.Method; diff --git a/src/Middleware/StaticFiles/src/StaticFileMiddleware.cs b/src/Middleware/StaticFiles/src/StaticFileMiddleware.cs index 1a97a5e90a..fe9dcd0119 100644 --- a/src/Middleware/StaticFiles/src/StaticFileMiddleware.cs +++ b/src/Middleware/StaticFiles/src/StaticFileMiddleware.cs @@ -72,7 +72,11 @@ namespace Microsoft.AspNetCore.StaticFiles { var fileContext = new StaticFileContext(context, _options, _matchUrl, _logger, _fileProvider, _contentTypeProvider); - if (!fileContext.ValidateMethod()) + if (!fileContext.ValidateNoEndpoint()) + { + _logger.EndpointMatched(); + } + else if (!fileContext.ValidateMethod()) { _logger.RequestMethodNotSupported(context.Request.Method); } diff --git a/src/Middleware/StaticFiles/test/FunctionalTests/StaticFileMiddlewareTests.cs b/src/Middleware/StaticFiles/test/FunctionalTests/StaticFileMiddlewareTests.cs index 7e55b79b78..7413eaf89d 100644 --- a/src/Middleware/StaticFiles/test/FunctionalTests/StaticFileMiddlewareTests.cs +++ b/src/Middleware/StaticFiles/test/FunctionalTests/StaticFileMiddlewareTests.cs @@ -14,6 +14,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Endpoints; using Microsoft.AspNetCore.Server.IntegrationTesting; using Microsoft.AspNetCore.Server.IntegrationTesting.Common; using Microsoft.AspNetCore.Testing; @@ -45,6 +46,44 @@ namespace Microsoft.AspNetCore.StaticFiles } } + [Fact] + public async Task Endpoint_PassesThrough() + { + var builder = new WebHostBuilder() + .ConfigureServices(services => services.AddSingleton(LoggerFactory)) + .UseKestrel() + .UseWebRoot(AppContext.BaseDirectory) + .Configure(app => + { + // Routing first => static files noops + app.Use(next => context => + { + // Assign an endpoint, this will make the default files noop. + context.SetEndpoint(new Endpoint((c) => + { + return context.Response.WriteAsync("Hi from endpoint."); + }, + new EndpointMetadataCollection(), + "test")); + + return next(context); + }); + + app.UseStaticFiles(); + }); + + using (var server = builder.Start(TestUrlHelper.GetTestUrl(ServerType.Kestrel))) + { + using (var client = new HttpClient { BaseAddress = new Uri(server.GetAddress()) }) + { + var response = await client.GetAsync("TestDocument.txt"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Hi from endpoint.", await response.Content.ReadAsStringAsync()); + } + } + } + [Fact] public async Task FoundFile_LastModifiedTrimsSeconds() { diff --git a/src/Middleware/StaticFiles/test/UnitTests/DefaultFilesMiddlewareTests.cs b/src/Middleware/StaticFiles/test/UnitTests/DefaultFilesMiddlewareTests.cs index 7df375d2d4..aff8e01a9c 100644 --- a/src/Middleware/StaticFiles/test/UnitTests/DefaultFilesMiddlewareTests.cs +++ b/src/Middleware/StaticFiles/test/UnitTests/DefaultFilesMiddlewareTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -9,9 +9,11 @@ using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Endpoints; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.TestHost; using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Xunit; @@ -72,6 +74,41 @@ namespace Microsoft.AspNetCore.StaticFiles } } + [Fact] + public async Task Endpoint_PassesThrough() + { + using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, "."))) + { + var server = StaticFilesTestServer.Create( + app => + { + app.Use(next => context => + { + // Assign an endpoint, this will make the default files noop. + context.SetEndpoint(new Endpoint((c) => + { + return context.Response.WriteAsync(context.Request.Path.Value); + }, + new EndpointMetadataCollection(), + "test")); + + return next(context); + }); + + app.UseDefaultFiles(new DefaultFilesOptions + { + RequestPath = new PathString(""), + FileProvider = fileProvider + }); + }, + services => services.AddDirectoryBrowser()); + + var response = await server.CreateRequest("/SubFolder/").GetAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/SubFolder/", await response.Content.ReadAsStringAsync()); // Should not be modified + } + } + [Theory] [InlineData("", @".", "/SubFolder/")] [InlineData("", @"./", "/SubFolder/")] diff --git a/src/Middleware/StaticFiles/test/UnitTests/DirectoryBrowserMiddlewareTests.cs b/src/Middleware/StaticFiles/test/UnitTests/DirectoryBrowserMiddlewareTests.cs index 2a12c85486..8710f46c28 100644 --- a/src/Middleware/StaticFiles/test/UnitTests/DirectoryBrowserMiddlewareTests.cs +++ b/src/Middleware/StaticFiles/test/UnitTests/DirectoryBrowserMiddlewareTests.cs @@ -9,6 +9,8 @@ using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Endpoints; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.TestHost; using Microsoft.AspNetCore.Testing.xunit; using Microsoft.Extensions.DependencyInjection; @@ -86,6 +88,42 @@ namespace Microsoft.AspNetCore.StaticFiles } } + [Fact] + public async Task Endpoint_PassesThrough() + { + using (var fileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, "."))) + { + var server = StaticFilesTestServer.Create( + app => + { + app.Use(next => context => + { + // Assign an endpoint, this will make the directory browser noop + context.SetEndpoint(new Endpoint((c) => + { + c.Response.StatusCode = (int)HttpStatusCode.NotAcceptable; + return c.Response.WriteAsync("Hi from endpoint."); + }, + new EndpointMetadataCollection(), + "test")); + + return next(context); + }); + + app.UseDirectoryBrowser(new DirectoryBrowserOptions + { + RequestPath = new PathString(""), + FileProvider = fileProvider + }); + }, + services => services.AddDirectoryBrowser()); + + var response = await server.CreateRequest("/").GetAsync(); + Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode); + Assert.Equal("Hi from endpoint.", await response.Content.ReadAsStringAsync()); + } + } + [Theory] [InlineData("", @".", "/")] [InlineData("", @".", "/SubFolder/")]