parent
ff89987243
commit
033c8adc3a
|
|
@ -1,7 +1,13 @@
|
||||||
{
|
{
|
||||||
"Default": {
|
"adx-nonshipping": {
|
||||||
"rules": [
|
"rules": [],
|
||||||
"DefaultCompositeRule"
|
"packages": {
|
||||||
|
"Microsoft.AspNetCore.RangeHelper.Sources": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Default": {
|
||||||
|
"rules": [
|
||||||
|
"DefaultCompositeRule"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio 15
|
# Visual Studio 15
|
||||||
VisualStudioVersion = 15.0.26020.0
|
VisualStudioVersion = 15.0.26228.9
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{40EE0889-960E-41B4-A3D3-9CE963EB0797}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{40EE0889-960E-41B4-A3D3-9CE963EB0797}"
|
||||||
EndProject
|
EndProject
|
||||||
|
|
@ -16,6 +16,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Static
|
||||||
EndProject
|
EndProject
|
||||||
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}"
|
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}"
|
||||||
EndProject
|
EndProject
|
||||||
|
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}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{360DC2F8-EEB4-4C69-9784-C686EAD78279}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microsoft.AspNetCore.RangeHelper.Sources", "Microsoft.AspNetCore.RangeHelper.Sources", "{DB6A1D14-B8A2-488F-9C4B-422FD45C8853}"
|
||||||
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
shared\Microsoft.AspNetCore.RangeHelper.Sources\RangeHelper.cs = shared\Microsoft.AspNetCore.RangeHelper.Sources\RangeHelper.cs
|
||||||
|
EndProjectSection
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|
@ -68,6 +77,18 @@ Global
|
||||||
{FDF0539C-1F62-4B78-91B1-C687886931CA}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
{FDF0539C-1F62-4B78-91B1-C687886931CA}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||||
{FDF0539C-1F62-4B78-91B1-C687886931CA}.Release|x86.ActiveCfg = Release|Any CPU
|
{FDF0539C-1F62-4B78-91B1-C687886931CA}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{FDF0539C-1F62-4B78-91B1-C687886931CA}.Release|x86.Build.0 = Release|Any CPU
|
{FDF0539C-1F62-4B78-91B1-C687886931CA}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||||
|
{D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||||
|
{D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||||
|
{D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||||
|
{D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{D3D752C4-4CDF-4F18-AC7F-48CB980A69DA}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -77,5 +98,7 @@ Global
|
||||||
{092141D9-305A-4FC5-AE74-CB23982CA8D4} = {8B21A3A9-9CA6-4857-A6E0-1A3203404B60}
|
{092141D9-305A-4FC5-AE74-CB23982CA8D4} = {8B21A3A9-9CA6-4857-A6E0-1A3203404B60}
|
||||||
{CC87FE7D-8F42-4BE9-A152-9625E837C1E5} = {EF02AFE8-7C15-4DDB-8B2C-58A676112A98}
|
{CC87FE7D-8F42-4BE9-A152-9625E837C1E5} = {EF02AFE8-7C15-4DDB-8B2C-58A676112A98}
|
||||||
{FDF0539C-1F62-4B78-91B1-C687886931CA} = {EF02AFE8-7C15-4DDB-8B2C-58A676112A98}
|
{FDF0539C-1F62-4B78-91B1-C687886931CA} = {EF02AFE8-7C15-4DDB-8B2C-58A676112A98}
|
||||||
|
{D3D752C4-4CDF-4F18-AC7F-48CB980A69DA} = {EF02AFE8-7C15-4DDB-8B2C-58A676112A98}
|
||||||
|
{DB6A1D14-B8A2-488F-9C4B-422FD45C8853} = {360DC2F8-EEB4-4C69-9784-C686EAD78279}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
// 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.Headers;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Internal
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides a parser for the Range Header in an <see cref="HttpContext.Request"/>.
|
||||||
|
/// </summary>
|
||||||
|
internal static class RangeHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the requested range if the Range Header in the <see cref="HttpContext.Request"/> is valid.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The <see cref="HttpContext"/> associated with the request.</param>
|
||||||
|
/// <param name="requestHeaders">The <see cref="RequestHeaders"/> associated with the given <paramref name="context"/>.</param>
|
||||||
|
/// <param name="lastModified">The <see cref="DateTimeOffset"/> representation of the last modified date of the file.</param>
|
||||||
|
/// <param name="etag">The <see cref="EntityTagHeaderValue"/> provided in the <see cref="HttpContext.Request"/>.</param>
|
||||||
|
/// <returns>A collection of <see cref="RangeItemHeaderValue"/> containing the ranges parsed from the <paramref name="requestHeaders"/>.</returns>
|
||||||
|
public static ICollection<RangeItemHeaderValue> ParseRange(HttpContext context, RequestHeaders requestHeaders, DateTimeOffset? lastModified = null, EntityTagHeaderValue etag = null)
|
||||||
|
{
|
||||||
|
var rawRangeHeader = context.Request.Headers[HeaderNames.Range];
|
||||||
|
if (StringValues.IsNullOrEmpty(rawRangeHeader))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perf: Check for a single entry before parsing it
|
||||||
|
if (rawRangeHeader.Count > 1 || rawRangeHeader[0].IndexOf(',') >= 0)
|
||||||
|
{
|
||||||
|
// The spec allows for multiple ranges but we choose not to support them because the client may request
|
||||||
|
// very strange ranges (e.g. each byte separately, overlapping ranges, etc.) that could negatively
|
||||||
|
// impact the server. Ignore the header and serve the response normally.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rangeHeader = requestHeaders.Range;
|
||||||
|
if (rangeHeader == null)
|
||||||
|
{
|
||||||
|
// Invalid
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already verified above
|
||||||
|
Debug.Assert(rangeHeader.Ranges.Count == 1);
|
||||||
|
|
||||||
|
// 14.27 If-Range
|
||||||
|
var ifRangeHeader = requestHeaders.IfRange;
|
||||||
|
if (ifRangeHeader != 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.
|
||||||
|
bool ignoreRangeHeader = false;
|
||||||
|
if (ifRangeHeader.LastModified.HasValue)
|
||||||
|
{
|
||||||
|
if (lastModified.HasValue && lastModified > ifRangeHeader.LastModified)
|
||||||
|
{
|
||||||
|
ignoreRangeHeader = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (etag != null && ifRangeHeader.EntityTag != null && !ifRangeHeader.EntityTag.Compare(etag, useStrongComparison: true))
|
||||||
|
{
|
||||||
|
ignoreRangeHeader = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ignoreRangeHeader)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rangeHeader.Ranges;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A helper method to normalize a collection of <see cref="RangeItemHeaderValue"/>s.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ranges">A collection of <see cref="RangeItemHeaderValue"/> to normalize.</param>
|
||||||
|
/// <param name="length">The length of the provided <see cref="RangeItemHeaderValue"/>.</param>
|
||||||
|
/// <returns>A normalized list of <see cref="RangeItemHeaderValue"/>s.</returns>
|
||||||
|
// 14.35.1 Byte Ranges - If a syntactically valid byte-range-set includes at least one byte-range-spec whose
|
||||||
|
// first-byte-pos is less than the current length of the entity-body, or at least one suffix-byte-range-spec
|
||||||
|
// with a non-zero suffix-length, then the byte-range-set is satisfiable.
|
||||||
|
// Adjusts ranges to be absolute and within bounds.
|
||||||
|
public static IList<RangeItemHeaderValue> NormalizeRanges(ICollection<RangeItemHeaderValue> ranges, long length)
|
||||||
|
{
|
||||||
|
if (ranges == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ranges.Count == 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<RangeItemHeaderValue>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length == 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<RangeItemHeaderValue>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedRanges = new List<RangeItemHeaderValue>(ranges.Count);
|
||||||
|
foreach (var range in ranges)
|
||||||
|
{
|
||||||
|
var normalizedRange = NormalizeRange(range, length);
|
||||||
|
|
||||||
|
if (normalizedRange != null)
|
||||||
|
{
|
||||||
|
normalizedRanges.Add(normalizedRange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedRanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A helper method to normalize a <see cref="RangeItemHeaderValue"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="range">The <see cref="RangeItemHeaderValue"/> to normalize.</param>
|
||||||
|
/// <param name="length">The length of the provided <see cref="RangeItemHeaderValue"/>.</param>
|
||||||
|
/// <returns>A normalized <see cref="RangeItemHeaderValue"/>.</returns>
|
||||||
|
public static RangeItemHeaderValue NormalizeRange(RangeItemHeaderValue range, long length)
|
||||||
|
{
|
||||||
|
var start = range.From;
|
||||||
|
var end = range.To;
|
||||||
|
|
||||||
|
// X-[Y]
|
||||||
|
if (start.HasValue)
|
||||||
|
{
|
||||||
|
if (start.Value >= length)
|
||||||
|
{
|
||||||
|
// Not satisfiable, skip/discard.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!end.HasValue || end.Value >= length)
|
||||||
|
{
|
||||||
|
end = length - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// suffix range "-X" e.g. the last X bytes, resolve
|
||||||
|
if (end.Value == 0)
|
||||||
|
{
|
||||||
|
// Not satisfiable, skip/discard.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = Math.Min(end.Value, length);
|
||||||
|
start = length - bytes;
|
||||||
|
end = start + bytes - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedRange = new RangeItemHeaderValue(start, end);
|
||||||
|
return normalizedRange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
// 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;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Microsoft.Net.Http.Headers;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.StaticFiles.Infrastructure
|
|
||||||
{
|
|
||||||
internal static class RangeHelpers
|
|
||||||
{
|
|
||||||
// 14.35.1 Byte Ranges - If a syntactically valid byte-range-set includes at least one byte-range-spec whose
|
|
||||||
// first-byte-pos is less than the current length of the entity-body, or at least one suffix-byte-range-spec
|
|
||||||
// with a non-zero suffix-length, then the byte-range-set is satisfiable.
|
|
||||||
// Adjusts ranges to be absolute and within bounds.
|
|
||||||
internal static IList<RangeItemHeaderValue> NormalizeRanges(ICollection<RangeItemHeaderValue> ranges, long length)
|
|
||||||
{
|
|
||||||
IList<RangeItemHeaderValue> normalizedRanges = new List<RangeItemHeaderValue>(ranges.Count);
|
|
||||||
if (length == 0)
|
|
||||||
{
|
|
||||||
return normalizedRanges;
|
|
||||||
}
|
|
||||||
foreach (var range in ranges)
|
|
||||||
{
|
|
||||||
long? start = range.From;
|
|
||||||
long? end = range.To;
|
|
||||||
|
|
||||||
// X-[Y]
|
|
||||||
if (start.HasValue)
|
|
||||||
{
|
|
||||||
if (start.Value >= length)
|
|
||||||
{
|
|
||||||
// Not satisfiable, skip/discard.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!end.HasValue || end.Value >= length)
|
|
||||||
{
|
|
||||||
end = length - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// suffix range "-X" e.g. the last X bytes, resolve
|
|
||||||
if (end.Value == 0)
|
|
||||||
{
|
|
||||||
// Not satisfiable, skip/discard.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
long bytes = Math.Min(end.Value, length);
|
|
||||||
start = length - bytes;
|
|
||||||
end = start + bytes - 1;
|
|
||||||
}
|
|
||||||
normalizedRanges.Add(new RangeItemHeaderValue(start.Value, end.Value));
|
|
||||||
}
|
|
||||||
return normalizedRanges;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -24,7 +24,6 @@ namespace Microsoft.AspNetCore.StaticFiles
|
||||||
private static Action<ILogger, StringValues, string, Exception> _logSendingFileRange;
|
private static Action<ILogger, StringValues, string, Exception> _logSendingFileRange;
|
||||||
private static Action<ILogger, StringValues, string, Exception> _logCopyingFileRange;
|
private static Action<ILogger, StringValues, string, Exception> _logCopyingFileRange;
|
||||||
private static Action<ILogger, long, string, string, Exception> _logCopyingBytesToResponse;
|
private static Action<ILogger, long, string, string, Exception> _logCopyingBytesToResponse;
|
||||||
private static Action<ILogger, string, Exception> _logMultipleFileRanges;
|
|
||||||
private static Action<ILogger, Exception> _logWriteCancelled;
|
private static Action<ILogger, Exception> _logWriteCancelled;
|
||||||
|
|
||||||
static LoggerExtensions()
|
static LoggerExtensions()
|
||||||
|
|
@ -77,10 +76,6 @@ namespace Microsoft.AspNetCore.StaticFiles
|
||||||
logLevel: LogLevel.Debug,
|
logLevel: LogLevel.Debug,
|
||||||
eventId: 12,
|
eventId: 12,
|
||||||
formatString: "Copying bytes {Start}-{End} of file {Path} to response body");
|
formatString: "Copying bytes {Start}-{End} of file {Path} to response body");
|
||||||
_logMultipleFileRanges = LoggerMessage.Define<string>(
|
|
||||||
logLevel: LogLevel.Warning,
|
|
||||||
eventId: 13,
|
|
||||||
formatString: "Multiple ranges are not allowed: '{Ranges}'");
|
|
||||||
_logWriteCancelled = LoggerMessage.Define(
|
_logWriteCancelled = LoggerMessage.Define(
|
||||||
logLevel: LogLevel.Debug,
|
logLevel: LogLevel.Debug,
|
||||||
eventId: 14,
|
eventId: 14,
|
||||||
|
|
@ -156,11 +151,6 @@ namespace Microsoft.AspNetCore.StaticFiles
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void LogMultipleFileRanges(this ILogger logger, string range)
|
|
||||||
{
|
|
||||||
_logMultipleFileRanges(logger, range, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void LogWriteCancelled(this ILogger logger, Exception ex)
|
public static void LogWriteCancelled(this ILogger logger, Exception ex)
|
||||||
{
|
{
|
||||||
_logWriteCancelled(logger, ex);
|
_logWriteCancelled(logger, ex);
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@
|
||||||
<PackageTags>aspnetcore;staticfiles</PackageTags>
|
<PackageTags>aspnetcore;staticfiles</PackageTags>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="..\..\shared\Microsoft.AspNetCore.RangeHelper.Sources\**\*.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="$(AspNetCoreVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="$(AspNetCoreVersion)" />
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,9 @@ using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Extensions;
|
using Microsoft.AspNetCore.Http.Extensions;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Http.Headers;
|
using Microsoft.AspNetCore.Http.Headers;
|
||||||
using Microsoft.AspNetCore.StaticFiles.Infrastructure;
|
using Microsoft.AspNetCore.Internal;
|
||||||
using Microsoft.Extensions.FileProviders;
|
using Microsoft.Extensions.FileProviders;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Primitives;
|
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.StaticFiles
|
namespace Microsoft.AspNetCore.StaticFiles
|
||||||
|
|
@ -231,60 +230,8 @@ namespace Microsoft.AspNetCore.StaticFiles
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var rawRangeHeader = _request.Headers[HeaderNames.Range];
|
var parsedRange = RangeHelper.ParseRange(_context, _requestHeaders, _lastModified, _etag);
|
||||||
if (StringValues.IsNullOrEmpty(rawRangeHeader))
|
_ranges = RangeHelper.NormalizeRanges(parsedRange, _length);
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perf: Check for a single entry before parsing it
|
|
||||||
if (rawRangeHeader.Count > 1 || rawRangeHeader[0].IndexOf(',') >= 0)
|
|
||||||
{
|
|
||||||
// The spec allows for multiple ranges but we choose not to support them because the client may request
|
|
||||||
// very strange ranges (e.g. each byte separately, overlapping ranges, etc.) that could negatively
|
|
||||||
// impact the server. Ignore the header and serve the response normally.
|
|
||||||
_logger.LogMultipleFileRanges(rawRangeHeader.ToString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var rangeHeader = _requestHeaders.Range;
|
|
||||||
if (rangeHeader == null)
|
|
||||||
{
|
|
||||||
// Invalid
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Already verified above
|
|
||||||
Debug.Assert(rangeHeader.Ranges.Count == 1);
|
|
||||||
|
|
||||||
// 14.27 If-Range
|
|
||||||
var ifRangeHeader = _requestHeaders.IfRange;
|
|
||||||
if (ifRangeHeader != 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.
|
|
||||||
bool ignoreRangeHeader = false;
|
|
||||||
if (ifRangeHeader.LastModified.HasValue)
|
|
||||||
{
|
|
||||||
if (_lastModified > ifRangeHeader.LastModified)
|
|
||||||
{
|
|
||||||
ignoreRangeHeader = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (ifRangeHeader.EntityTag != null && !ifRangeHeader.EntityTag.Compare(_etag, useStrongComparison: true))
|
|
||||||
{
|
|
||||||
ignoreRangeHeader = true;
|
|
||||||
}
|
|
||||||
if (ignoreRangeHeader)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ranges = RangeHelpers.NormalizeRanges(rangeHeader.Ranges, _length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ApplyResponseHeaders(int statusCode)
|
public void ApplyResponseHeaders(int statusCode)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<Import Project="..\..\build\common.props" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>netcoreapp2.0;net46</TargetFrameworks>
|
||||||
|
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netcoreapp2.0</TargetFrameworks>
|
||||||
|
<RuntimeIdentifier Condition="'$(TargetFramework)'!='netcoreapp2.0'">win7-x64</RuntimeIdentifier>
|
||||||
|
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||||
|
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="..\..\shared\Microsoft.AspNetCore.RangeHelper.Sources\**\*.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Http" Version="$(AspNetCoreVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="$(AspNetCoreVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitVersion)" />
|
||||||
|
<PackageReference Include="xunit" Version="$(XunitVersion)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,254 @@
|
||||||
|
// 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Internal
|
||||||
|
{
|
||||||
|
public class RangeHelperTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void NormalizeRanges_ReturnsEmptyArrayWhenRangeCountZero()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var ranges = new List<RangeItemHeaderValue>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var normalizedRanges = RangeHelper.NormalizeRanges(ranges, 2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(normalizedRanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NormalizeRanges_ReturnsEmptyArrayWhenLengthZero()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var ranges = new[]
|
||||||
|
{
|
||||||
|
new RangeItemHeaderValue(0, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var normalizedRanges = RangeHelper.NormalizeRanges(ranges, 0);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(normalizedRanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(1, 2)]
|
||||||
|
[InlineData(2, 3)]
|
||||||
|
public void NormalizeRanges_SkipsItemWhenRangeStartEqualOrGreaterThanLength(long start, long end)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var ranges = new[]
|
||||||
|
{
|
||||||
|
new RangeItemHeaderValue(start, end),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var normalizedRanges = RangeHelper.NormalizeRanges(ranges, 1);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(normalizedRanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NormalizeRanges_SkipsItemWhenRangeEndEqualsZero()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var ranges = new[]
|
||||||
|
{
|
||||||
|
new RangeItemHeaderValue(null, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var normalizedRanges = RangeHelper.NormalizeRanges(ranges, 1);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(normalizedRanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(null, null)]
|
||||||
|
[InlineData(null, 0)]
|
||||||
|
[InlineData(0, null)]
|
||||||
|
[InlineData(0, 0)]
|
||||||
|
public void NormalizeRanges_ReturnsNormalizedRange(long start, long end)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var ranges = new[]
|
||||||
|
{
|
||||||
|
new RangeItemHeaderValue(start, end),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var normalizedRanges = RangeHelper.NormalizeRanges(ranges, 1);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var range = Assert.Single(normalizedRanges);
|
||||||
|
Assert.Equal(0, range.From);
|
||||||
|
Assert.Equal(0, range.To);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NormalizeRanges_ReturnsRangeWithNoChange()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var ranges = new[]
|
||||||
|
{
|
||||||
|
new RangeItemHeaderValue(1, 3),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var normalizedRanges = RangeHelper.NormalizeRanges(ranges, 4);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var range = Assert.Single(normalizedRanges);
|
||||||
|
Assert.Equal(1, range.From);
|
||||||
|
Assert.Equal(3, range.To);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(null, null)]
|
||||||
|
[InlineData(null, 0)]
|
||||||
|
[InlineData(0, null)]
|
||||||
|
[InlineData(0, 0)]
|
||||||
|
public void NormalizeRanges_MultipleRanges_ReturnsNormalizedRange(long start, long end)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var ranges = new[]
|
||||||
|
{
|
||||||
|
new RangeItemHeaderValue(start, end),
|
||||||
|
new RangeItemHeaderValue(1, 2),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var normalizedRanges = RangeHelper.NormalizeRanges(ranges, 3);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(normalizedRanges,
|
||||||
|
range =>
|
||||||
|
{
|
||||||
|
Assert.Equal(0, range.From);
|
||||||
|
Assert.Equal(0, range.To);
|
||||||
|
},
|
||||||
|
range =>
|
||||||
|
{
|
||||||
|
Assert.Equal(1, range.From);
|
||||||
|
Assert.Equal(2, range.To);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(null)]
|
||||||
|
[InlineData("")]
|
||||||
|
public void ParseRange_ReturnsNullWhenRangeHeaderNotProvided(string range)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
httpContext.Request.Headers[HeaderNames.Range] = range;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var parsedRangeResult = RangeHelper.ParseRange(httpContext, httpContext.Request.GetTypedHeaders(), new DateTimeOffset(), null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(parsedRangeResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("1-2, 3-4")]
|
||||||
|
[InlineData("1-2, ")]
|
||||||
|
public void ParseRange_ReturnsNullWhenMultipleRangesProvidedInRangeHeader(string range)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
httpContext.Request.Headers[HeaderNames.Range] = range;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var parsedRangeResult = RangeHelper.ParseRange(httpContext, httpContext.Request.GetTypedHeaders(), new DateTimeOffset(), null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(parsedRangeResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRange_ReturnsNullWhenLastModifiedGreaterThanIfRangeHeaderLastModified()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
var range = new RangeHeaderValue(1, 2);
|
||||||
|
httpContext.Request.Headers[HeaderNames.Range] = range.ToString();
|
||||||
|
var lastModified = new RangeConditionHeaderValue(DateTime.Now);
|
||||||
|
httpContext.Request.Headers[HeaderNames.IfRange] = lastModified.ToString();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var parsedRangeResult = RangeHelper.ParseRange(httpContext, httpContext.Request.GetTypedHeaders(), DateTime.Now.AddMilliseconds(2), null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(parsedRangeResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRange_ReturnsNullWhenETagNotEqualToIfRangeHeaderEntityTag()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
var range = new RangeHeaderValue(1, 2);
|
||||||
|
httpContext.Request.Headers[HeaderNames.Range] = range.ToString();
|
||||||
|
var etag = new RangeConditionHeaderValue("\"tag\"");
|
||||||
|
httpContext.Request.Headers[HeaderNames.IfRange] = etag.ToString();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var parsedRangeResult = RangeHelper.ParseRange(httpContext, httpContext.Request.GetTypedHeaders(), DateTime.Now, new EntityTagHeaderValue("\"etag\""));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(parsedRangeResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRange_ReturnsSingleRangeWhenInputValid()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
var range = new RangeHeaderValue(1, 2);
|
||||||
|
httpContext.Request.Headers[HeaderNames.Range] = range.ToString();
|
||||||
|
var lastModified = new RangeConditionHeaderValue(DateTime.Now);
|
||||||
|
httpContext.Request.Headers[HeaderNames.IfRange] = lastModified.ToString();
|
||||||
|
var etag = new RangeConditionHeaderValue("\"etag\"");
|
||||||
|
httpContext.Request.Headers[HeaderNames.IfRange] = etag.ToString();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var parsedRangeResult = RangeHelper.ParseRange(httpContext, httpContext.Request.GetTypedHeaders(), DateTime.Now, new EntityTagHeaderValue("\"etag\""));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var parsedRange = Assert.Single(parsedRangeResult);
|
||||||
|
Assert.Equal(1, parsedRange.From);
|
||||||
|
Assert.Equal(2, parsedRange.To);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseRange_ReturnsRangeWhenLastModifiedAndEtagNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
var range = new RangeHeaderValue(1, 2);
|
||||||
|
httpContext.Request.Headers[HeaderNames.Range] = range.ToString();
|
||||||
|
var lastModified = new RangeConditionHeaderValue(DateTime.Now);
|
||||||
|
httpContext.Request.Headers[HeaderNames.IfRange] = lastModified.ToString();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var parsedRangeResult = RangeHelper.ParseRange(httpContext, httpContext.Request.GetTypedHeaders());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var parsedRange = Assert.Single(parsedRangeResult);
|
||||||
|
Assert.Equal(1, parsedRange.From);
|
||||||
|
Assert.Equal(2, parsedRange.To);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -36,4 +36,8 @@
|
||||||
<PackageReference Include="xunit" Version="$(XunitVersion)" />
|
<PackageReference Include="xunit" Version="$(XunitVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -29,4 +29,8 @@
|
||||||
<PackageReference Include="xunit" Version="$(XunitVersion)" />
|
<PackageReference Include="xunit" Version="$(XunitVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue