| File: src\aspnetcore\src\Shared\RangeHelper\RangeHelper.cs | Web Access |
| Project: src\aspnetcore\src\StaticAssets\src\Microsoft.AspNetCore.StaticAssets.csproj (Microsoft.AspNetCore.StaticAssets) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #nullable enable using System; using System.Diagnostics; using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Headers; using Microsoft.Extensions.Logging; 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 normalized form of 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="length">The total length of the file representation requested.</param> /// <param name="logger">The <see cref="ILogger"/>.</param> /// <returns>A boolean value which represents if the <paramref name="requestHeaders"/> contain a single valid /// range request. A <see cref="RangeItemHeaderValue"/> which represents the normalized form of the /// range parsed from the <paramref name="requestHeaders"/> or <c>null</c> if it cannot be normalized.</returns> /// <remark>If the Range header exists but cannot be parsed correctly, or if the provided length is 0, then the range request cannot be satisfied (status 416). /// This results in (<c>true</c>,<c>null</c>) return values.</remark> public static (bool isRangeRequest, RangeItemHeaderValue? range) ParseRange( HttpContext context, RequestHeaders requestHeaders, long length, ILogger logger) { var rawRangeHeader = context.Request.Headers.Range; if (StringValues.IsNullOrEmpty(rawRangeHeader)) { logger.LogTrace("Range header's value is empty."); return (false, null); } // Perf: Check for a single entry before parsing it if (rawRangeHeader.Count > 1 || (rawRangeHeader[0] ?? string.Empty).Contains(',')) { logger.LogDebug("Multiple ranges are not supported."); // 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 (false, null); } var rangeHeader = requestHeaders.Range; if (rangeHeader == null) { logger.LogDebug("Range header's value is invalid."); // Invalid return (false, null); } // Already verified above Debug.Assert(rangeHeader.Ranges.Count == 1); var ranges = rangeHeader.Ranges; if (ranges == null) { logger.LogDebug("Range header's value is invalid."); return (false, null); } if (ranges.Count == 0) { return (true, null); } if (length == 0) { return (true, null); } // Normalize the ranges var range = NormalizeRange(ranges.Single(), length); // Return the single range return (true, range); } // Internal for testing internal 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 if (end.HasValue) { // 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; } return new RangeItemHeaderValue(start, end); } }