From 6d7269dafbf83b40ab4944758273e50b15f8bee7 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Wed, 21 Feb 2018 13:22:31 -0800 Subject: [PATCH] Return HTTP 404 if FileNotFoundException is thrown when attempting to send a file (#232) --- .../StaticFileContext.cs | 7 +++-- .../StaticFileMiddleware.cs | 31 ++++++++++++++----- .../StaticFileMiddlewareTests.cs | 24 ++++++++++++++ 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.AspNetCore.StaticFiles/StaticFileContext.cs b/src/Microsoft.AspNetCore.StaticFiles/StaticFileContext.cs index b50b8fe433..4f6e1437e2 100644 --- a/src/Microsoft.AspNetCore.StaticFiles/StaticFileContext.cs +++ b/src/Microsoft.AspNetCore.StaticFiles/StaticFileContext.cs @@ -233,7 +233,7 @@ namespace Microsoft.AspNetCore.StaticFiles // the Range header field. if (ifRangeHeader.LastModified.HasValue) { - if (_lastModified !=null && _lastModified > ifRangeHeader.LastModified) + if (_lastModified != null && _lastModified > ifRangeHeader.LastModified) { _isRangeRequest = false; } @@ -318,11 +318,11 @@ namespace Microsoft.AspNetCore.StaticFiles public async Task SendAsync() { - ApplyResponseHeaders(Constants.Status200Ok); string physicalPath = _fileInfo.PhysicalPath; var sendFile = _context.Features.Get(); if (sendFile != null && !string.IsNullOrEmpty(physicalPath)) { + ApplyResponseHeaders(Constants.Status200Ok); // We don't need to directly cancel this, if the client disconnects it will fail silently. await sendFile.SendFileAsync(physicalPath, 0, _length, CancellationToken.None); return; @@ -332,6 +332,9 @@ namespace Microsoft.AspNetCore.StaticFiles { using (var readStream = _fileInfo.CreateReadStream()) { + // Don't apply headers until we are sure we can open this file. + ApplyResponseHeaders(Constants.Status200Ok); + // Larger StreamCopyBufferSize is required because in case of FileStream readStream isn't going to be buffering await StreamCopyOperation.CopyToAsync(readStream, _response.Body, _length, StreamCopyBufferSize, _context.RequestAborted); } diff --git a/src/Microsoft.AspNetCore.StaticFiles/StaticFileMiddleware.cs b/src/Microsoft.AspNetCore.StaticFiles/StaticFileMiddleware.cs index d8910bcdbf..30fb313142 100644 --- a/src/Microsoft.AspNetCore.StaticFiles/StaticFileMiddleware.cs +++ b/src/Microsoft.AspNetCore.StaticFiles/StaticFileMiddleware.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -67,7 +68,7 @@ namespace Microsoft.AspNetCore.StaticFiles /// /// /// - public Task Invoke(HttpContext context) + public async Task Invoke(HttpContext context) { var fileContext = new StaticFileContext(context, _options, _matchUrl, _logger, _fileProvider, _contentTypeProvider); @@ -97,21 +98,35 @@ namespace Microsoft.AspNetCore.StaticFiles case StaticFileContext.PreconditionState.ShouldProcess: if (fileContext.IsHeadMethod) { - return fileContext.SendStatusAsync(Constants.Status200Ok); + await fileContext.SendStatusAsync(Constants.Status200Ok); + return; } if (fileContext.IsRangeRequest) { - return fileContext.SendRangeAsync(); + await fileContext.SendRangeAsync(); + return; + } + try + { + await fileContext.SendAsync(); + _logger.LogFileServed(fileContext.SubPath, fileContext.PhysicalPath); + return; + } + catch (FileNotFoundException) + { + _logger.LogFileNotFound(fileContext.SubPath); + await fileContext.SendStatusAsync(StatusCodes.Status404NotFound); + return; } - _logger.LogFileServed(fileContext.SubPath, fileContext.PhysicalPath); - return fileContext.SendAsync(); case StaticFileContext.PreconditionState.NotModified: _logger.LogPathNotModified(fileContext.SubPath); - return fileContext.SendStatusAsync(Constants.Status304NotModified); + await fileContext.SendStatusAsync(Constants.Status304NotModified); + return; case StaticFileContext.PreconditionState.PreconditionFailed: _logger.LogPreconditionFailed(fileContext.SubPath); - return fileContext.SendStatusAsync(Constants.Status412PreconditionFailed); + await fileContext.SendStatusAsync(Constants.Status412PreconditionFailed); + return; default: var exception = new NotImplementedException(fileContext.GetPreconditionState().ToString()); @@ -120,7 +135,7 @@ namespace Microsoft.AspNetCore.StaticFiles } } - return _next(context); + await _next(context); } } } diff --git a/test/Microsoft.AspNetCore.StaticFiles.Tests/StaticFileMiddlewareTests.cs b/test/Microsoft.AspNetCore.StaticFiles.Tests/StaticFileMiddlewareTests.cs index 7517782e51..0a0608a825 100644 --- a/test/Microsoft.AspNetCore.StaticFiles.Tests/StaticFileMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.StaticFiles.Tests/StaticFileMiddlewareTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Net; @@ -13,6 +14,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.AspNetCore.Testing.xunit; using Microsoft.Extensions.FileProviders; +using Microsoft.Net.Http.Headers; using Xunit; namespace Microsoft.AspNetCore.StaticFiles @@ -29,6 +31,27 @@ namespace Microsoft.AspNetCore.StaticFiles var response = await server.CreateClient().GetAsync("/ranges.txt"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + Assert.Null(response.Headers.ETag); + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Windows, SkipReason = "Symlinks not supported on Windows")] + public async Task ReturnsNotFoundForBrokenSymlink() + { + var badLink = Path.Combine(AppContext.BaseDirectory, Path.GetRandomFileName() + ".txt"); + + Process.Start("ln", $"-s \"/tmp/{Path.GetRandomFileName()}\" \"{badLink}\"").WaitForExit(); + Assert.True(File.Exists(badLink), "Should have created a symlink"); + + var builder = new WebHostBuilder() + .Configure(app => app.UseStaticFiles(new StaticFileOptions { ServeUnknownFileTypes = true })) + .UseWebRoot(AppContext.BaseDirectory); + var server = new TestServer(builder); + + var response = await server.CreateClient().GetAsync(Path.GetFileName(badLink)); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + Assert.Null(response.Headers.ETag); } [Fact] @@ -101,6 +124,7 @@ namespace Microsoft.AspNetCore.StaticFiles Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); Assert.True(response.Content.Headers.ContentLength == fileInfo.Length); Assert.Equal(response.Content.Headers.ContentLength, responseContent.Length); + Assert.NotNull(response.Headers.ETag); using (var stream = fileInfo.CreateReadStream()) {