diff --git a/src/Middleware/StaticFiles/src/StaticFileContext.cs b/src/Middleware/StaticFiles/src/StaticFileContext.cs
index c21e3df7fe2b..3d5f7aa9e6ba 100644
--- a/src/Middleware/StaticFiles/src/StaticFileContext.cs
+++ b/src/Middleware/StaticFiles/src/StaticFileContext.cs
@@ -2,13 +2,13 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Diagnostics;
using System.IO;
using System.Linq;
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;
@@ -22,128 +22,98 @@ namespace Microsoft.AspNetCore.StaticFiles
internal struct StaticFileContext
{
private const int StreamCopyBufferSize = 64 * 1024;
+
private readonly HttpContext _context;
private readonly StaticFileOptions _options;
- private readonly PathString _matchUrl;
private readonly HttpRequest _request;
private readonly HttpResponse _response;
private readonly ILogger _logger;
private readonly IFileProvider _fileProvider;
- private readonly IContentTypeProvider _contentTypeProvider;
- private string _method;
- private bool _isGet;
- private bool _isHead;
- private PathString _subPath;
- private string _contentType;
+ private readonly string _method;
+ private readonly string _contentType;
+
private IFileInfo _fileInfo;
- private long _length;
- private DateTimeOffset _lastModified;
private EntityTagHeaderValue _etag;
-
private RequestHeaders _requestHeaders;
private ResponseHeaders _responseHeaders;
+ private RangeItemHeaderValue _range;
+
+ private long _length;
+ private readonly PathString _subPath;
+ private DateTimeOffset _lastModified;
private PreconditionState _ifMatchState;
private PreconditionState _ifNoneMatchState;
private PreconditionState _ifModifiedSinceState;
private PreconditionState _ifUnmodifiedSinceState;
- private RangeItemHeaderValue _range;
- private bool _isRangeRequest;
+ private RequestType _requestType;
- public StaticFileContext(HttpContext context, StaticFileOptions options, PathString matchUrl, ILogger logger, IFileProvider fileProvider, IContentTypeProvider contentTypeProvider)
+ public StaticFileContext(HttpContext context, StaticFileOptions options, ILogger logger, IFileProvider fileProvider, string contentType, PathString subPath)
{
_context = context;
_options = options;
- _matchUrl = matchUrl;
_request = context.Request;
_response = context.Response;
_logger = logger;
- _requestHeaders = _request.GetTypedHeaders();
- _responseHeaders = _response.GetTypedHeaders();
_fileProvider = fileProvider;
- _contentTypeProvider = contentTypeProvider;
-
- _method = null;
- _isGet = false;
- _isHead = false;
- _subPath = PathString.Empty;
- _contentType = null;
+ _method = _request.Method;
+ _contentType = contentType;
_fileInfo = null;
+ _etag = null;
+ _requestHeaders = null;
+ _responseHeaders = null;
+ _range = null;
+
_length = 0;
+ _subPath = subPath;
_lastModified = new DateTimeOffset();
- _etag = null;
_ifMatchState = PreconditionState.Unspecified;
_ifNoneMatchState = PreconditionState.Unspecified;
_ifModifiedSinceState = PreconditionState.Unspecified;
_ifUnmodifiedSinceState = PreconditionState.Unspecified;
- _range = null;
- _isRangeRequest = false;
- }
-
- internal enum PreconditionState
- {
- Unspecified,
- NotModified,
- ShouldProcess,
- PreconditionFailed
- }
-
- public bool IsHeadMethod
- {
- get { return _isHead; }
- }
-
- public bool IsRangeRequest
- {
- get { return _isRangeRequest; }
- }
- public string SubPath
- {
- get { return _subPath.Value; }
+ if (HttpMethods.IsGet(_method))
+ {
+ _requestType = RequestType.IsGet;
+ }
+ else if (HttpMethods.IsHead(_method))
+ {
+ _requestType = RequestType.IsHead;
+ }
+ else
+ {
+ _requestType = RequestType.Unspecified;
+ }
}
- public string PhysicalPath
- {
- get { return _fileInfo?.PhysicalPath; }
- }
+ private RequestHeaders RequestHeaders => (_requestHeaders ??= _request.GetTypedHeaders());
- public bool ValidateNoEndpoint()
- {
- // Return true because we only want to run if there is no endpoint.
- return _context.GetEndpoint() == null;
- }
+ private ResponseHeaders ResponseHeaders => (_responseHeaders ??= _response.GetTypedHeaders());
- public bool ValidateMethod()
- {
- _method = _request.Method;
- _isGet = HttpMethods.IsGet(_method);
- _isHead = HttpMethods.IsHead(_method);
- return _isGet || _isHead;
- }
+ public bool IsHeadMethod => _requestType.HasFlag(RequestType.IsHead);
- // Check if the URL matches any expected paths
- public bool ValidatePath()
- {
- return Helpers.TryMatchPath(_context, _matchUrl, forDirectory: false, subpath: out _subPath);
- }
+ public bool IsGetMethod => _requestType.HasFlag(RequestType.IsGet);
- public bool LookupContentType()
+ public bool IsRangeRequest
{
- if (_contentTypeProvider.TryGetContentType(_subPath.Value, out _contentType))
+ get => _requestType.HasFlag(RequestType.IsRange);
+ private set
{
- return true;
+ if (value)
+ {
+ _requestType |= RequestType.IsRange;
+ }
+ else
+ {
+ _requestType &= ~RequestType.IsRange;
+ }
}
+ }
- if (_options.ServeUnknownFileTypes)
- {
- _contentType = _options.DefaultContentType;
- return true;
- }
+ public string SubPath => _subPath.Value;
- return false;
- }
+ public string PhysicalPath => _fileInfo?.PhysicalPath;
public bool LookupFileInfo()
{
@@ -175,8 +145,10 @@ public void ComprehendRequestHeaders()
private void ComputeIfMatch()
{
+ var requestHeaders = RequestHeaders;
+
// 14.24 If-Match
- var ifMatch = _requestHeaders.IfMatch;
+ var ifMatch = requestHeaders.IfMatch;
if (ifMatch != null && ifMatch.Any())
{
_ifMatchState = PreconditionState.PreconditionFailed;
@@ -191,7 +163,7 @@ private void ComputeIfMatch()
}
// 14.26 If-None-Match
- var ifNoneMatch = _requestHeaders.IfNoneMatch;
+ var ifNoneMatch = requestHeaders.IfNoneMatch;
if (ifNoneMatch != null && ifNoneMatch.Any())
{
_ifNoneMatchState = PreconditionState.ShouldProcess;
@@ -208,10 +180,11 @@ private void ComputeIfMatch()
private void ComputeIfModifiedSince()
{
+ var requestHeaders = RequestHeaders;
var now = DateTimeOffset.UtcNow;
// 14.25 If-Modified-Since
- var ifModifiedSince = _requestHeaders.IfModifiedSince;
+ var ifModifiedSince = requestHeaders.IfModifiedSince;
if (ifModifiedSince.HasValue && ifModifiedSince <= now)
{
bool modified = ifModifiedSince < _lastModified;
@@ -219,7 +192,7 @@ private void ComputeIfModifiedSince()
}
// 14.28 If-Unmodified-Since
- var ifUnmodifiedSince = _requestHeaders.IfUnmodifiedSince;
+ var ifUnmodifiedSince = requestHeaders.IfUnmodifiedSince;
if (ifUnmodifiedSince.HasValue && ifUnmodifiedSince <= now)
{
bool unmodified = ifUnmodifiedSince >= _lastModified;
@@ -230,7 +203,7 @@ private void ComputeIfModifiedSince()
private void ComputeIfRange()
{
// 14.27 If-Range
- var ifRangeHeader = _requestHeaders.IfRange;
+ var ifRangeHeader = RequestHeaders.IfRange;
if (ifRangeHeader != null)
{
// If the validator given in the If-Range header field matches the
@@ -242,12 +215,12 @@ private void ComputeIfRange()
{
if (_lastModified > ifRangeHeader.LastModified)
{
- _isRangeRequest = false;
+ IsRangeRequest = false;
}
}
else if (_etag != null && ifRangeHeader.EntityTag != null && !ifRangeHeader.EntityTag.Compare(_etag, useStrongComparison: true))
{
- _isRangeRequest = false;
+ IsRangeRequest = false;
}
}
}
@@ -259,12 +232,15 @@ private void ComputeRange()
// A server MUST ignore a Range header field received with a request method other
// than GET.
- if (!_isGet)
+ if (!IsGetMethod)
{
return;
}
- (_isRangeRequest, _range) = RangeHelper.ParseRange(_context, _requestHeaders, _length, _logger);
+ (var isRangeRequest, var range) = RangeHelper.ParseRange(_context, RequestHeaders, _length, _logger);
+
+ _range = range;
+ IsRangeRequest = isRangeRequest;
}
public void ApplyResponseHeaders(int statusCode)
@@ -278,9 +254,11 @@ public void ApplyResponseHeaders(int statusCode)
{
_response.ContentType = _contentType;
}
- _responseHeaders.LastModified = _lastModified;
- _responseHeaders.ETag = _etag;
- _responseHeaders.Headers[HeaderNames.AcceptRanges] = "bytes";
+
+ var responseHeaders = ResponseHeaders;
+ responseHeaders.LastModified = _lastModified;
+ responseHeaders.ETag = _etag;
+ responseHeaders.Headers[HeaderNames.AcceptRanges] = "bytes";
}
if (statusCode == Constants.Status200Ok)
{
@@ -294,10 +272,7 @@ public void ApplyResponseHeaders(int statusCode)
}
public PreconditionState GetPreconditionState()
- {
- return GetMaxPreconditionState(_ifMatchState, _ifNoneMatchState,
- _ifModifiedSinceState, _ifUnmodifiedSinceState);
- }
+ => GetMaxPreconditionState(_ifMatchState, _ifNoneMatchState, _ifModifiedSinceState, _ifUnmodifiedSinceState);
private static PreconditionState GetMaxPreconditionState(params PreconditionState[] states)
{
@@ -320,6 +295,52 @@ public Task SendStatusAsync(int statusCode)
return Task.CompletedTask;
}
+ public async Task ServeStaticFile(HttpContext context, RequestDelegate next)
+ {
+ ComprehendRequestHeaders();
+ switch (GetPreconditionState())
+ {
+ case PreconditionState.Unspecified:
+ case PreconditionState.ShouldProcess:
+ if (IsHeadMethod)
+ {
+ await SendStatusAsync(Constants.Status200Ok);
+ return;
+ }
+
+ try
+ {
+ if (IsRangeRequest)
+ {
+ await SendRangeAsync();
+ return;
+ }
+
+ await SendAsync();
+ _logger.FileServed(SubPath, PhysicalPath);
+ return;
+ }
+ catch (FileNotFoundException)
+ {
+ context.Response.Clear();
+ }
+ await next(context);
+ return;
+ case PreconditionState.NotModified:
+ _logger.FileNotModified(SubPath);
+ await SendStatusAsync(Constants.Status304NotModified);
+ return;
+ case PreconditionState.PreconditionFailed:
+ _logger.PreconditionFailed(SubPath);
+ await SendStatusAsync(Constants.Status412PreconditionFailed);
+ return;
+ default:
+ var exception = new NotImplementedException(GetPreconditionState().ToString());
+ Debug.Fail(exception.ToString());
+ throw exception;
+ }
+ }
+
public async Task SendAsync()
{
SetCompressionMode();
@@ -358,14 +379,14 @@ internal async Task SendRangeAsync()
// 14.16 Content-Range - A server sending a response with status code 416 (Requested range not satisfiable)
// SHOULD include a Content-Range field with a byte-range-resp-spec of "*". The instance-length specifies
// the current length of the selected resource. e.g. */length
- _responseHeaders.ContentRange = new ContentRangeHeaderValue(_length);
+ ResponseHeaders.ContentRange = new ContentRangeHeaderValue(_length);
ApplyResponseHeaders(Constants.Status416RangeNotSatisfiable);
_logger.RangeNotSatisfiable(SubPath);
return;
}
- _responseHeaders.ContentRange = ComputeContentRange(_range, out var start, out var length);
+ ResponseHeaders.ContentRange = ComputeContentRange(_range, out var start, out var length);
_response.ContentLength = length;
SetCompressionMode();
ApplyResponseHeaders(Constants.Status206PartialContent);
@@ -416,5 +437,22 @@ private void SetCompressionMode()
responseCompressionFeature.Mode = _options.HttpsCompression;
}
}
+
+ internal enum PreconditionState : byte
+ {
+ Unspecified,
+ NotModified,
+ ShouldProcess,
+ PreconditionFailed
+ }
+
+ [Flags]
+ private enum RequestType : byte
+ {
+ Unspecified = 0b_000,
+ IsHead = 0b_001,
+ IsGet = 0b_010,
+ IsRange = 0b_100,
+ }
}
}
diff --git a/src/Middleware/StaticFiles/src/StaticFileMiddleware.cs b/src/Middleware/StaticFiles/src/StaticFileMiddleware.cs
index 5f1dbb4e0f73..b05c58f6e057 100644
--- a/src/Middleware/StaticFiles/src/StaticFileMiddleware.cs
+++ b/src/Middleware/StaticFiles/src/StaticFileMiddleware.cs
@@ -2,12 +2,11 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
-using System.Diagnostics;
-using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Endpoints;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -70,83 +69,83 @@ public StaticFileMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv
///
public Task Invoke(HttpContext context)
{
- var fileContext = new StaticFileContext(context, _options, _matchUrl, _logger, _fileProvider, _contentTypeProvider);
-
- if (!fileContext.ValidateNoEndpoint())
+ if (!ValidateNoEndpoint(context))
{
_logger.EndpointMatched();
}
- else if (!fileContext.ValidateMethod())
+ else if (!ValidateMethod(context))
{
_logger.RequestMethodNotSupported(context.Request.Method);
}
- else if (!fileContext.ValidatePath())
- {
- _logger.PathMismatch(fileContext.SubPath);
- }
- else if (!fileContext.LookupContentType())
+ else if (!ValidatePath(context, _matchUrl, out var subPath))
{
- _logger.FileTypeNotSupported(fileContext.SubPath);
+ _logger.PathMismatch(subPath);
}
- else if (!fileContext.LookupFileInfo())
+ else if (!LookupContentType(_contentTypeProvider, _options, subPath, out var contentType))
{
- _logger.FileNotFound(fileContext.SubPath);
+ _logger.FileTypeNotSupported(subPath);
}
else
{
// If we get here, we can try to serve the file
- return ServeStaticFile(context, fileContext);
+ return TryServeStaticFile(context, contentType, subPath);
}
return _next(context);
}
- private async Task ServeStaticFile(HttpContext context, StaticFileContext fileContext)
+ // Return true because we only want to run if there is no endpoint.
+ private static bool ValidateNoEndpoint(HttpContext context) => context.GetEndpoint() == null;
+
+ private static bool ValidateMethod(HttpContext context)
+ {
+ var method = context.Request.Method;
+ var isValid = false;
+ if (HttpMethods.IsGet(method))
+ {
+ isValid = true;
+ }
+ else if (HttpMethods.IsHead(method))
+ {
+ isValid = true;
+ }
+
+ return isValid;
+ }
+
+ internal static bool ValidatePath(HttpContext context, PathString matchUrl, out PathString subPath) => Helpers.TryMatchPath(context, matchUrl, forDirectory: false, out subPath);
+
+ internal static bool LookupContentType(IContentTypeProvider contentTypeProvider, StaticFileOptions options, PathString subPath, out string contentType)
{
- fileContext.ComprehendRequestHeaders();
- switch (fileContext.GetPreconditionState())
+ if (contentTypeProvider.TryGetContentType(subPath.Value, out contentType))
{
- case StaticFileContext.PreconditionState.Unspecified:
- case StaticFileContext.PreconditionState.ShouldProcess:
- if (fileContext.IsHeadMethod)
- {
- await fileContext.SendStatusAsync(Constants.Status200Ok);
- return;
- }
-
- try
- {
- if (fileContext.IsRangeRequest)
- {
- await fileContext.SendRangeAsync();
- return;
- }
-
- await fileContext.SendAsync();
- _logger.FileServed(fileContext.SubPath, fileContext.PhysicalPath);
- return;
- }
- catch (FileNotFoundException)
- {
- context.Response.Clear();
- }
- await _next(context);
- return;
- case StaticFileContext.PreconditionState.NotModified:
- _logger.FileNotModified(fileContext.SubPath);
- await fileContext.SendStatusAsync(Constants.Status304NotModified);
- return;
-
- case StaticFileContext.PreconditionState.PreconditionFailed:
- _logger.PreconditionFailed(fileContext.SubPath);
- await fileContext.SendStatusAsync(Constants.Status412PreconditionFailed);
- return;
-
- default:
- var exception = new NotImplementedException(fileContext.GetPreconditionState().ToString());
- Debug.Fail(exception.ToString());
- throw exception;
+ return true;
}
+
+ if (options.ServeUnknownFileTypes)
+ {
+ contentType = options.DefaultContentType;
+ return true;
+ }
+
+ return false;
+ }
+
+ private Task TryServeStaticFile(HttpContext context, string contentType, PathString subPath)
+ {
+ var fileContext = new StaticFileContext(context, _options, _logger, _fileProvider, contentType, subPath);
+
+ if (!fileContext.LookupFileInfo())
+ {
+ _logger.FileNotFound(fileContext.SubPath);
+ }
+ else
+ {
+ // If we get here, we can try to serve the file
+ return fileContext.ServeStaticFile(context, _next);
+ }
+
+ return _next(context);
}
}
}
diff --git a/src/Middleware/StaticFiles/test/UnitTests/StaticFileContextTest.cs b/src/Middleware/StaticFiles/test/UnitTests/StaticFileContextTest.cs
index d61609657a26..efea2a08e56d 100644
--- a/src/Middleware/StaticFiles/test/UnitTests/StaticFileContextTest.cs
+++ b/src/Middleware/StaticFiles/test/UnitTests/StaticFileContextTest.cs
@@ -22,14 +22,18 @@ public void LookupFileInfo_ReturnsFalse_IfFileDoesNotExist()
{
// Arrange
var options = new StaticFileOptions();
- var context = new StaticFileContext(new DefaultHttpContext(), options, PathString.Empty, NullLogger.Instance, new TestFileProvider(), new FileExtensionContentTypeProvider());
+ var httpContext = new DefaultHttpContext();
+ var pathString = PathString.Empty;
+ var validateResult = StaticFileMiddleware.ValidatePath(httpContext, pathString, out var subPath);
+ var contentTypeResult = StaticFileMiddleware.LookupContentType(new FileExtensionContentTypeProvider(), options, subPath, out var contentType);
+ var context = new StaticFileContext(httpContext, options, NullLogger.Instance, new TestFileProvider(), contentType, subPath);
// Act
- var validateResult = context.ValidatePath();
var lookupResult = context.LookupFileInfo();
// Assert
Assert.True(validateResult);
+ Assert.False(contentTypeResult);
Assert.False(lookupResult);
}
@@ -46,13 +50,17 @@ public void LookupFileInfo_ReturnsTrue_IfFileExists()
var pathString = new PathString("/test");
var httpContext = new DefaultHttpContext();
httpContext.Request.Path = new PathString("/test/foo.txt");
- var context = new StaticFileContext(httpContext, options, pathString, NullLogger.Instance, fileProvider, new FileExtensionContentTypeProvider());
+ var validateResult = StaticFileMiddleware.ValidatePath(httpContext, pathString, out var subPath);
+ var contentTypeResult = StaticFileMiddleware.LookupContentType(new FileExtensionContentTypeProvider(), options, subPath, out var contentType);
+
+ var context = new StaticFileContext(httpContext, options, NullLogger.Instance, fileProvider, contentType, subPath);
// Act
- context.ValidatePath();
var result = context.LookupFileInfo();
// Assert
+ Assert.True(validateResult);
+ Assert.True(contentTypeResult);
Assert.True(result);
}
@@ -70,10 +78,14 @@ public async Task EnablesHttpsCompression_IfMatched()
var httpsCompressionFeature = new TestHttpsCompressionFeature();
httpContext.Features.Set(httpsCompressionFeature);
httpContext.Request.Path = new PathString("/test/foo.txt");
- var context = new StaticFileContext(httpContext, options, pathString, NullLogger.Instance, fileProvider, new FileExtensionContentTypeProvider());
+ var validateResult = StaticFileMiddleware.ValidatePath(httpContext, pathString, out var subPath);
+ var contentTypeResult = StaticFileMiddleware.LookupContentType(new FileExtensionContentTypeProvider(), options, subPath, out var contentType);
+
+ var context = new StaticFileContext(httpContext, options, NullLogger.Instance, fileProvider, contentType, subPath);
- context.ValidatePath();
var result = context.LookupFileInfo();
+ Assert.True(validateResult);
+ Assert.True(contentTypeResult);
Assert.True(result);
await context.SendAsync();
@@ -95,10 +107,14 @@ public void SkipsHttpsCompression_IfNotMatched()
var httpsCompressionFeature = new TestHttpsCompressionFeature();
httpContext.Features.Set(httpsCompressionFeature);
httpContext.Request.Path = new PathString("/test/bar.txt");
- var context = new StaticFileContext(httpContext, options, pathString, NullLogger.Instance, fileProvider, new FileExtensionContentTypeProvider());
+ var validateResult = StaticFileMiddleware.ValidatePath(httpContext, pathString, out var subPath);
+ var contentTypeResult = StaticFileMiddleware.LookupContentType(new FileExtensionContentTypeProvider(), options, subPath, out var contentType);
+
+ var context = new StaticFileContext(httpContext, options, NullLogger.Instance, fileProvider, contentType, subPath);
- context.ValidatePath();
var result = context.LookupFileInfo();
+ Assert.True(validateResult);
+ Assert.True(contentTypeResult);
Assert.False(result);
Assert.Equal(HttpsCompressionMode.Default, httpsCompressionFeature.Mode);