Respond to RangeHelper refactor (#6348)
Respond to https://github.com/aspnet/StaticFiles/pull/200
This commit is contained in:
parent
f1dd475eae
commit
7ffd88757d
|
|
@ -31,8 +31,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Unspecified,
|
Unspecified,
|
||||||
NotModified,
|
NotModified,
|
||||||
ShouldProcess,
|
ShouldProcess,
|
||||||
PreconditionFailed,
|
PreconditionFailed
|
||||||
IgnoreRangeRequest
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ILogger Logger { get; }
|
protected ILogger Logger { get; }
|
||||||
|
|
@ -61,15 +60,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
var httpRequestHeaders = request.GetTypedHeaders();
|
var httpRequestHeaders = request.GetTypedHeaders();
|
||||||
var response = context.HttpContext.Response;
|
var response = context.HttpContext.Response;
|
||||||
var httpResponseHeaders = response.GetTypedHeaders();
|
var httpResponseHeaders = response.GetTypedHeaders();
|
||||||
if (fileLength.HasValue)
|
|
||||||
{
|
|
||||||
SetAcceptRangeHeader(context);
|
|
||||||
// Assuming the request is not a range request, the Content-Length header is set to the length of the entire file.
|
|
||||||
// If the request is a valid range request, this header is overwritten with the length of the range as part of the
|
|
||||||
// range processing (see method SetContentLength).
|
|
||||||
response.ContentLength = fileLength.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastModified.HasValue)
|
if (lastModified.HasValue)
|
||||||
{
|
{
|
||||||
httpResponseHeaders.LastModified = lastModified;
|
httpResponseHeaders.LastModified = lastModified;
|
||||||
|
|
@ -80,24 +70,35 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
var serveBody = !HttpMethods.IsHead(request.Method);
|
var serveBody = !HttpMethods.IsHead(request.Method);
|
||||||
if (HttpMethods.IsHead(request.Method) || HttpMethods.IsGet(request.Method))
|
var preconditionState = GetPreconditionState(context, httpRequestHeaders, lastModified, etag);
|
||||||
|
if (preconditionState == PreconditionState.NotModified)
|
||||||
{
|
{
|
||||||
var preconditionState = GetPreconditionState(context, httpRequestHeaders, lastModified, etag);
|
serveBody = false;
|
||||||
if (request.Headers.ContainsKey(HeaderNames.Range) &&
|
response.StatusCode = StatusCodes.Status304NotModified;
|
||||||
(preconditionState == PreconditionState.Unspecified ||
|
}
|
||||||
preconditionState == PreconditionState.ShouldProcess))
|
else if (preconditionState == PreconditionState.PreconditionFailed)
|
||||||
|
{
|
||||||
|
serveBody = false;
|
||||||
|
response.StatusCode = StatusCodes.Status412PreconditionFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileLength.HasValue)
|
||||||
|
{
|
||||||
|
SetAcceptRangeHeader(context);
|
||||||
|
// Assuming the request is not a range request, the Content-Length header is set to the length of the entire file.
|
||||||
|
// If the request is a valid range request, this header is overwritten with the length of the range as part of the
|
||||||
|
// range processing (see method SetContentLength).
|
||||||
|
response.ContentLength = fileLength.Value;
|
||||||
|
if (HttpMethods.IsHead(request.Method) || HttpMethods.IsGet(request.Method))
|
||||||
{
|
{
|
||||||
return SetRangeHeaders(context, httpRequestHeaders, fileLength, lastModified, etag);
|
if ((preconditionState == PreconditionState.Unspecified ||
|
||||||
}
|
preconditionState == PreconditionState.ShouldProcess))
|
||||||
if (preconditionState == PreconditionState.NotModified)
|
{
|
||||||
{
|
if (IfRangeValid(context, httpRequestHeaders, lastModified, etag))
|
||||||
serveBody = false;
|
{
|
||||||
response.StatusCode = StatusCodes.Status304NotModified;
|
return SetRangeHeaders(context, httpRequestHeaders, fileLength);
|
||||||
}
|
}
|
||||||
else if (preconditionState == PreconditionState.PreconditionFailed)
|
}
|
||||||
{
|
|
||||||
serveBody = false;
|
|
||||||
response.StatusCode = StatusCodes.Status412PreconditionFailed;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,6 +156,37 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
return PreconditionState.Unspecified;
|
return PreconditionState.Unspecified;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static bool IfRangeValid(
|
||||||
|
ActionContext context,
|
||||||
|
RequestHeaders httpRequestHeaders,
|
||||||
|
DateTimeOffset? lastModified = null,
|
||||||
|
EntityTagHeaderValue etag = null)
|
||||||
|
{
|
||||||
|
// 14.27 If-Range
|
||||||
|
var ifRange = httpRequestHeaders.IfRange;
|
||||||
|
if (ifRange != null)
|
||||||
|
{
|
||||||
|
// If the validator given in the If-Range header field matches the
|
||||||
|
// current validator for the selected representation of the target
|
||||||
|
// resource, then the server SHOULD process the Range header field as
|
||||||
|
// requested. If the validator does not match, the server MUST ignore
|
||||||
|
// the Range header field.
|
||||||
|
if (ifRange.LastModified.HasValue)
|
||||||
|
{
|
||||||
|
if (lastModified.HasValue && lastModified > ifRange.LastModified)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (etag != null && ifRange.EntityTag != null && !ifRange.EntityTag.Compare(etag, useStrongComparison: true))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Internal for testing
|
// Internal for testing
|
||||||
internal static PreconditionState GetPreconditionState(
|
internal static PreconditionState GetPreconditionState(
|
||||||
ActionContext context,
|
ActionContext context,
|
||||||
|
|
@ -166,7 +198,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
var ifNoneMatchState = PreconditionState.Unspecified;
|
var ifNoneMatchState = PreconditionState.Unspecified;
|
||||||
var ifModifiedSinceState = PreconditionState.Unspecified;
|
var ifModifiedSinceState = PreconditionState.Unspecified;
|
||||||
var ifUnmodifiedSinceState = PreconditionState.Unspecified;
|
var ifUnmodifiedSinceState = PreconditionState.Unspecified;
|
||||||
var ifRangeState = PreconditionState.Unspecified;
|
|
||||||
|
|
||||||
// 14.24 If-Match
|
// 14.24 If-Match
|
||||||
var ifMatch = httpRequestHeaders.IfMatch;
|
var ifMatch = httpRequestHeaders.IfMatch;
|
||||||
|
|
@ -208,28 +239,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
ifUnmodifiedSinceState = unmodified ? PreconditionState.ShouldProcess : PreconditionState.PreconditionFailed;
|
ifUnmodifiedSinceState = unmodified ? PreconditionState.ShouldProcess : PreconditionState.PreconditionFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
var ifRange = httpRequestHeaders.IfRange;
|
var state = GetMaxPreconditionState(ifMatchState, ifNoneMatchState, ifModifiedSinceState, ifUnmodifiedSinceState);
|
||||||
if (ifRange != null)
|
|
||||||
{
|
|
||||||
// If the validator given in the If-Range header field matches the
|
|
||||||
// current validator for the selected representation of the target
|
|
||||||
// resource, then the server SHOULD process the Range header field as
|
|
||||||
// requested. If the validator does not match, the server MUST ignore
|
|
||||||
// the Range header field.
|
|
||||||
if (ifRange.LastModified.HasValue)
|
|
||||||
{
|
|
||||||
if (lastModified.HasValue && lastModified > ifRange.LastModified)
|
|
||||||
{
|
|
||||||
ifRangeState = PreconditionState.IgnoreRangeRequest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (etag != null && ifRange.EntityTag != null && !ifRange.EntityTag.Compare(etag, useStrongComparison: true))
|
|
||||||
{
|
|
||||||
ifRangeState = PreconditionState.IgnoreRangeRequest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var state = GetMaxPreconditionState(ifMatchState, ifNoneMatchState, ifModifiedSinceState, ifUnmodifiedSinceState, ifRangeState);
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -250,16 +260,23 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
private static (RangeItemHeaderValue range, long rangeLength, bool serveBody) SetRangeHeaders(
|
private static (RangeItemHeaderValue range, long rangeLength, bool serveBody) SetRangeHeaders(
|
||||||
ActionContext context,
|
ActionContext context,
|
||||||
RequestHeaders httpRequestHeaders,
|
RequestHeaders httpRequestHeaders,
|
||||||
long? fileLength,
|
long? fileLength)
|
||||||
DateTimeOffset? lastModified = null,
|
|
||||||
EntityTagHeaderValue etag = null)
|
|
||||||
{
|
{
|
||||||
var response = context.HttpContext.Response;
|
var response = context.HttpContext.Response;
|
||||||
var httpResponseHeaders = response.GetTypedHeaders();
|
var httpResponseHeaders = response.GetTypedHeaders();
|
||||||
|
|
||||||
// Checked for presence of Range header explicitly before calling this method.
|
// Range may be null for empty range header, invalid ranges, parsing errors, multiple ranges
|
||||||
// Range may be null for parsing errors, multiple ranges and when the file length is missing.
|
// and when the file length is zero.
|
||||||
var range = fileLength.HasValue ? ParseRange(context, httpRequestHeaders, fileLength.Value, lastModified, etag) : null;
|
var (isRangeRequest, range) = RangeHelper.ParseRange(
|
||||||
|
context.HttpContext,
|
||||||
|
httpRequestHeaders,
|
||||||
|
fileLength.Value);
|
||||||
|
|
||||||
|
if (!isRangeRequest)
|
||||||
|
{
|
||||||
|
return (range: null, rangeLength: 0, serveBody: true);
|
||||||
|
}
|
||||||
|
|
||||||
if (range == null)
|
if (range == null)
|
||||||
{
|
{
|
||||||
// 14.16 Content-Range - A server sending a response with status code 416 (Requested range not satisfiable)
|
// 14.16 Content-Range - A server sending a response with status code 416 (Requested range not satisfiable)
|
||||||
|
|
@ -295,32 +312,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
return length;
|
return length;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static RangeItemHeaderValue ParseRange(
|
|
||||||
ActionContext context,
|
|
||||||
RequestHeaders httpRequestHeaders,
|
|
||||||
long fileLength,
|
|
||||||
DateTimeOffset? lastModified = null,
|
|
||||||
EntityTagHeaderValue etag = null)
|
|
||||||
{
|
|
||||||
var httpContext = context.HttpContext;
|
|
||||||
var response = httpContext.Response;
|
|
||||||
|
|
||||||
var range = RangeHelper.ParseRange(httpContext, httpRequestHeaders, lastModified, etag);
|
|
||||||
|
|
||||||
if (range != null)
|
|
||||||
{
|
|
||||||
var normalizedRanges = RangeHelper.NormalizeRanges(range, fileLength);
|
|
||||||
if (normalizedRanges == null || normalizedRanges.Count == 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalizedRanges.Single();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static ILogger CreateLogger<T>(ILoggerFactory factory)
|
protected static ILogger CreateLogger<T>(ILoggerFactory factory)
|
||||||
{
|
{
|
||||||
if (factory == null)
|
if (factory == null)
|
||||||
|
|
|
||||||
|
|
@ -233,8 +233,47 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("0-5")]
|
[InlineData("0-5")]
|
||||||
[InlineData("bytes = 11-0")]
|
[InlineData("bytes = ")]
|
||||||
[InlineData("bytes = 1-4, 5-11")]
|
[InlineData("bytes = 1-4, 5-11")]
|
||||||
|
public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnored(string rangeString)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var contentType = "text/plain";
|
||||||
|
var lastModified = new DateTimeOffset();
|
||||||
|
var entityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||||
|
var byteArray = Encoding.ASCII.GetBytes("Hello World");
|
||||||
|
|
||||||
|
var result = new FileContentResult(byteArray, contentType)
|
||||||
|
{
|
||||||
|
LastModified = lastModified,
|
||||||
|
EntityTag = entityTag
|
||||||
|
};
|
||||||
|
|
||||||
|
var httpContext = GetHttpContext();
|
||||||
|
httpContext.Request.Headers[HeaderNames.Range] = rangeString;
|
||||||
|
httpContext.Request.Method = HttpMethods.Get;
|
||||||
|
httpContext.Response.Body = new MemoryStream();
|
||||||
|
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await result.ExecuteResultAsync(actionContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var httpResponse = actionContext.HttpContext.Response;
|
||||||
|
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||||
|
var streamReader = new StreamReader(httpResponse.Body);
|
||||||
|
var body = streamReader.ReadToEndAsync().Result;
|
||||||
|
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
|
||||||
|
Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
|
||||||
|
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||||
|
Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
|
||||||
|
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
|
||||||
|
Assert.Equal("Hello World", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("bytes = 12-13")]
|
||||||
|
[InlineData("bytes = -0")]
|
||||||
public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(string rangeString)
|
public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(string rangeString)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
|
||||||
|
|
@ -378,8 +378,10 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
Assert.Equal(FileResultExecutorBase.PreconditionState.NotModified, state);
|
Assert.Equal(FileResultExecutorBase.PreconditionState.NotModified, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public void GetPreconditionState_ShouldNotProcess_IgnoreRangeRequest()
|
[InlineData("\"NotEtag\"", false)]
|
||||||
|
[InlineData("\"Etag\"", true)]
|
||||||
|
public void IfRangeValid_IgnoreRangeRequest(string ifRangeString, bool expected)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var actionContext = new ActionContext();
|
var actionContext = new ActionContext();
|
||||||
|
|
@ -389,19 +391,19 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
var lastModified = DateTimeOffset.MinValue;
|
var lastModified = DateTimeOffset.MinValue;
|
||||||
lastModified = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0));
|
lastModified = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0));
|
||||||
var etag = new EntityTagHeaderValue("\"Etag\"");
|
var etag = new EntityTagHeaderValue("\"Etag\"");
|
||||||
httpRequestHeaders.IfRange = new RangeConditionHeaderValue("\"NotEtag\"");
|
httpRequestHeaders.IfRange = new RangeConditionHeaderValue(ifRangeString);
|
||||||
httpRequestHeaders.IfModifiedSince = lastModified;
|
httpRequestHeaders.IfModifiedSince = lastModified;
|
||||||
actionContext.HttpContext = httpContext;
|
actionContext.HttpContext = httpContext;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var state = FileResultExecutorBase.GetPreconditionState(
|
var ifRangeIsValid = FileResultExecutorBase.IfRangeValid(
|
||||||
actionContext,
|
actionContext,
|
||||||
httpRequestHeaders,
|
httpRequestHeaders,
|
||||||
lastModified,
|
lastModified,
|
||||||
etag);
|
etag);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(FileResultExecutorBase.PreconditionState.IgnoreRangeRequest, state);
|
Assert.Equal(expected, ifRangeIsValid);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IServiceCollection CreateServices()
|
private static IServiceCollection CreateServices()
|
||||||
|
|
|
||||||
|
|
@ -219,9 +219,9 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("0-5")]
|
[InlineData("0-5")]
|
||||||
[InlineData("bytes = 11-0")]
|
[InlineData("bytes = ")]
|
||||||
[InlineData("bytes = 1-4, 5-11")]
|
[InlineData("bytes = 1-4, 5-11")]
|
||||||
public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(string rangeString)
|
public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnored(string rangeString)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var contentType = "text/plain";
|
var contentType = "text/plain";
|
||||||
|
|
@ -246,6 +246,46 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
// Act
|
// Act
|
||||||
await result.ExecuteResultAsync(actionContext);
|
await result.ExecuteResultAsync(actionContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var httpResponse = actionContext.HttpContext.Response;
|
||||||
|
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||||
|
var streamReader = new StreamReader(httpResponse.Body);
|
||||||
|
var body = streamReader.ReadToEndAsync().Result;
|
||||||
|
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
|
||||||
|
Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
|
||||||
|
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||||
|
Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
|
||||||
|
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
|
||||||
|
Assert.Equal("Hello World", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("bytes = 12-13")]
|
||||||
|
[InlineData("bytes = -0")]
|
||||||
|
public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(string rangeString)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var contentType = "text/plain";
|
||||||
|
var lastModified = new DateTimeOffset();
|
||||||
|
var entityTag = new EntityTagHeaderValue("\"Etag\"");
|
||||||
|
var byteArray = Encoding.ASCII.GetBytes("Hello World");
|
||||||
|
var readStream = new MemoryStream(byteArray);
|
||||||
|
|
||||||
|
var result = new FileStreamResult(readStream, contentType)
|
||||||
|
{
|
||||||
|
LastModified = lastModified,
|
||||||
|
EntityTag = entityTag,
|
||||||
|
};
|
||||||
|
|
||||||
|
var httpContext = GetHttpContext();
|
||||||
|
httpContext.Request.Headers[HeaderNames.Range] = rangeString;
|
||||||
|
httpContext.Request.Method = HttpMethods.Get;
|
||||||
|
httpContext.Response.Body = new MemoryStream();
|
||||||
|
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await result.ExecuteResultAsync(actionContext);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var httpResponse = actionContext.HttpContext.Response;
|
var httpResponse = actionContext.HttpContext.Response;
|
||||||
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||||
var contentRange = new ContentRangeHeaderValue(0, 3, 34);
|
var contentRange = new ContentRangeHeaderValue(0, 3, 34);
|
||||||
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
|
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
|
||||||
|
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
|
||||||
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
|
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
|
||||||
Assert.Equal(4, httpResponse.ContentLength);
|
Assert.Equal(4, httpResponse.ContentLength);
|
||||||
Assert.Equal("File", body);
|
Assert.Equal("File", body);
|
||||||
|
|
@ -148,14 +149,46 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
var body = streamReader.ReadToEndAsync().Result;
|
var body = streamReader.ReadToEndAsync().Result;
|
||||||
Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
|
Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
|
||||||
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||||
|
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
|
||||||
Assert.Equal("FilePathResultTestFile contents<74>", body);
|
Assert.Equal("FilePathResultTestFile contents<74>", body);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("0-5")]
|
[InlineData("0-5")]
|
||||||
[InlineData("bytes = 11-0")]
|
[InlineData("bytes = ")]
|
||||||
[InlineData("bytes = 1-4, 5-11")]
|
[InlineData("bytes = 1-4, 5-11")]
|
||||||
public async Task WriteFileAsync_RangeRequested_NotSatisfiable(string rangeString)
|
public async Task WriteFileAsync_RangeRequestIgnored(string rangeString)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
|
||||||
|
var result = new TestPhysicalFileResult(path, "text/plain");
|
||||||
|
var httpContext = GetHttpContext();
|
||||||
|
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||||
|
requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
|
||||||
|
httpContext.Request.Headers[HeaderNames.Range] = rangeString;
|
||||||
|
httpContext.Request.Method = HttpMethods.Get;
|
||||||
|
httpContext.Response.Body = new MemoryStream();
|
||||||
|
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await result.ExecuteResultAsync(actionContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var httpResponse = actionContext.HttpContext.Response;
|
||||||
|
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||||
|
var streamReader = new StreamReader(httpResponse.Body);
|
||||||
|
var body = streamReader.ReadToEndAsync().Result;
|
||||||
|
Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
|
||||||
|
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||||
|
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
|
||||||
|
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
|
||||||
|
Assert.Equal("FilePathResultTestFile contents<74>", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("bytes = 35-36")]
|
||||||
|
[InlineData("bytes = -0")]
|
||||||
|
public async Task WriteFileAsync_RangeRequestedNotSatisfiable(string rangeString)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
|
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
|
||||||
|
|
|
||||||
|
|
@ -191,9 +191,9 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("0-5")]
|
[InlineData("0-5")]
|
||||||
[InlineData("bytes = 11-0")]
|
[InlineData("bytes = ")]
|
||||||
[InlineData("bytes = 1-4, 5-11")]
|
[InlineData("bytes = 1-4, 5-11")]
|
||||||
public async Task WriteFileAsync_RangeRequested_NotSatisfiable(string rangeString)
|
public async Task WriteFileAsync_RangeRequestIgnored(string rangeString)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var path = Path.GetFullPath("helllo.txt");
|
var path = Path.GetFullPath("helllo.txt");
|
||||||
|
|
@ -201,15 +201,58 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
var result = new TestVirtualFileResult(path, contentType);
|
var result = new TestVirtualFileResult(path, contentType);
|
||||||
var appEnvironment = new Mock<IHostingEnvironment>();
|
var appEnvironment = new Mock<IHostingEnvironment>();
|
||||||
appEnvironment.Setup(app => app.WebRootFileProvider)
|
appEnvironment.Setup(app => app.WebRootFileProvider)
|
||||||
.Returns(GetFileProvider(path));
|
.Returns(GetFileProvider(path));
|
||||||
|
|
||||||
var httpContext = GetHttpContext();
|
var httpContext = GetHttpContext();
|
||||||
httpContext.Response.Body = new MemoryStream();
|
httpContext.Response.Body = new MemoryStream();
|
||||||
httpContext.RequestServices = new ServiceCollection()
|
httpContext.RequestServices = new ServiceCollection()
|
||||||
.AddSingleton(appEnvironment.Object)
|
.AddSingleton(appEnvironment.Object)
|
||||||
.AddTransient<TestVirtualFileResultExecutor>()
|
.AddTransient<TestVirtualFileResultExecutor>()
|
||||||
.AddTransient<ILoggerFactory, LoggerFactory>()
|
.AddTransient<ILoggerFactory, LoggerFactory>()
|
||||||
.BuildServiceProvider();
|
.BuildServiceProvider();
|
||||||
|
|
||||||
|
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||||
|
httpContext.Request.Headers[HeaderNames.Range] = rangeString;
|
||||||
|
requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1);
|
||||||
|
httpContext.Request.Method = HttpMethods.Get;
|
||||||
|
httpContext.Response.Body = new MemoryStream();
|
||||||
|
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await result.ExecuteResultAsync(actionContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var httpResponse = actionContext.HttpContext.Response;
|
||||||
|
httpResponse.Body.Seek(0, SeekOrigin.Begin);
|
||||||
|
var streamReader = new StreamReader(httpResponse.Body);
|
||||||
|
var body = streamReader.ReadToEndAsync().Result;
|
||||||
|
Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
|
||||||
|
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
|
||||||
|
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
|
||||||
|
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
|
||||||
|
Assert.Equal("FilePathResultTestFile contents¡", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("bytes = 35-36")]
|
||||||
|
[InlineData("bytes = -0")]
|
||||||
|
public async Task WriteFileAsync_RangeRequestedNotSatisfiable(string rangeString)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var path = Path.GetFullPath("helllo.txt");
|
||||||
|
var contentType = "text/plain; charset=us-ascii; p1=p1-value";
|
||||||
|
var result = new TestVirtualFileResult(path, contentType);
|
||||||
|
var appEnvironment = new Mock<IHostingEnvironment>();
|
||||||
|
appEnvironment.Setup(app => app.WebRootFileProvider)
|
||||||
|
.Returns(GetFileProvider(path));
|
||||||
|
|
||||||
|
var httpContext = GetHttpContext();
|
||||||
|
httpContext.Response.Body = new MemoryStream();
|
||||||
|
httpContext.RequestServices = new ServiceCollection()
|
||||||
|
.AddSingleton(appEnvironment.Object)
|
||||||
|
.AddTransient<TestVirtualFileResultExecutor>()
|
||||||
|
.AddTransient<ILoggerFactory, LoggerFactory>()
|
||||||
|
.BuildServiceProvider();
|
||||||
|
|
||||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||||
httpContext.Request.Headers[HeaderNames.Range] = rangeString;
|
httpContext.Request.Headers[HeaderNames.Range] = rangeString;
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,28 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("0-6")]
|
[InlineData("0-6")]
|
||||||
[InlineData("bytes = 11-6")]
|
[InlineData("bytes = ")]
|
||||||
[InlineData("bytes = 1-4, 5-11")]
|
[InlineData("bytes = 1-4, 5-11")]
|
||||||
|
public async Task FileFromDisk_CanBeEnabled_WithMiddleware_RangeRequestIgnored(string rangeString)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDisk");
|
||||||
|
httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await Client.SendAsync(httpRequestMessage);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.NotNull(response.Content.Headers.ContentType);
|
||||||
|
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Equal("This is a sample text file", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("bytes = 35-36")]
|
||||||
|
[InlineData("bytes = -0")]
|
||||||
public async Task FileFromDisk_CanBeEnabled_WithMiddleware_RangeRequestNotSatisfiable(string rangeString)
|
public async Task FileFromDisk_CanBeEnabled_WithMiddleware_RangeRequestNotSatisfiable(string rangeString)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
@ -107,8 +127,28 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("0-6")]
|
[InlineData("0-6")]
|
||||||
[InlineData("bytes = 11-6")]
|
[InlineData("bytes = ")]
|
||||||
[InlineData("bytes = 1-4, 5-11")]
|
[InlineData("bytes = 1-4, 5-11")]
|
||||||
|
public async Task FileFromDisk_CanBeEnabled_WithMiddleware_RangeRequestIgnored_WithLastModifiedAndEtag(string rangeString)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDiskWithFileName_WithLastModifiedAndEtag");
|
||||||
|
httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await Client.SendAsync(httpRequestMessage);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.NotNull(response.Content.Headers.ContentType);
|
||||||
|
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Equal("This is a sample text file", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("bytes = 35-36")]
|
||||||
|
[InlineData("bytes = -0")]
|
||||||
public async Task FileFromDisk_CanBeEnabled_WithMiddleware_RangeRequestNotSatisfiable_WithLastModifiedAndEtag(string rangeString)
|
public async Task FileFromDisk_CanBeEnabled_WithMiddleware_RangeRequestNotSatisfiable_WithLastModifiedAndEtag(string rangeString)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
@ -229,8 +269,28 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("0-6")]
|
[InlineData("0-6")]
|
||||||
[InlineData("bytes = 11-6")]
|
[InlineData("bytes = ")]
|
||||||
[InlineData("bytes = 1-4, 5-11")]
|
[InlineData("bytes = 1-4, 5-11")]
|
||||||
|
public async Task FileFromStream_ReturnsFile_RangeRequestIgnored(string rangeString)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromStream");
|
||||||
|
httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await Client.SendAsync(httpRequestMessage);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.NotNull(response.Content.Headers.ContentType);
|
||||||
|
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Equal("This is sample text from a stream", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("bytes = 35-36")]
|
||||||
|
[InlineData("bytes = -0")]
|
||||||
public async Task FileFromStream_ReturnsFile_RangeRequestNotSatisfiable(string rangeString)
|
public async Task FileFromStream_ReturnsFile_RangeRequestNotSatisfiable(string rangeString)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
@ -349,8 +409,28 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("0-6")]
|
[InlineData("0-6")]
|
||||||
[InlineData("bytes = 11-6")]
|
[InlineData("bytes = ")]
|
||||||
[InlineData("bytes = 1-4, 5-11")]
|
[InlineData("bytes = 1-4, 5-11")]
|
||||||
|
public async Task FileFromBinaryData_ReturnsFile_RangeRequestIgnored(string rangeString)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromBinaryData");
|
||||||
|
httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await Client.SendAsync(httpRequestMessage);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.NotNull(response.Content.Headers.ContentType);
|
||||||
|
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Equal("This is a sample text from a binary array", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("bytes = 45-46")]
|
||||||
|
[InlineData("bytes = -0")]
|
||||||
public async Task FileFromBinaryData_ReturnsFile_RangeRequestNotSatisfiable(string rangeString)
|
public async Task FileFromBinaryData_ReturnsFile_RangeRequestNotSatisfiable(string rangeString)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
@ -524,8 +604,31 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("0-6")]
|
[InlineData("0-6")]
|
||||||
[InlineData("bytes = 11-6")]
|
[InlineData("bytes = ")]
|
||||||
[InlineData("bytes = 1-4, 5-11")]
|
[InlineData("bytes = 1-4, 5-11")]
|
||||||
|
public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_RangeRequestIgnored(string rangeString)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/EmbeddedFiles/DownloadFileWithFileName");
|
||||||
|
httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await Client.SendAsync(httpRequestMessage);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.NotNull(response.Content.Headers.ContentType);
|
||||||
|
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Equal("Sample text file as embedded resource.", body);
|
||||||
|
var contentDisposition = response.Content.Headers.ContentDisposition.ToString();
|
||||||
|
Assert.NotNull(contentDisposition);
|
||||||
|
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("bytes = 45-46")]
|
||||||
|
[InlineData("bytes = -0")]
|
||||||
public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_RangeRequestNotSatisfiable(string rangeString)
|
public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_RangeRequestNotSatisfiable(string rangeString)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue