Use strongly typed headers.

This commit is contained in:
Chris Ross 2015-01-09 11:31:59 -08:00
parent 82be0d3d4a
commit 212c264ed3
8 changed files with 70 additions and 181 deletions

View File

@ -11,21 +11,6 @@ namespace Microsoft.AspNet.StaticFiles
internal const string SendFileVersionKey = "sendfile.Version";
internal const string SendFileVersion = "1.0";
internal const string Location = "Location";
internal const string IfMatch = "If-Match";
internal const string IfNoneMatch = "If-None-Match";
internal const string IfModifiedSince = "If-Modified-Since";
internal const string IfUnmodifiedSince = "If-Unmodified-Since";
internal const string IfRange = "If-Range";
internal const string Range = "Range";
internal const string ContentRange = "Content-Range";
internal const string LastModified = "Last-Modified";
internal const string ETag = "ETag";
internal const string HttpDateFormat = "r";
internal const string TextHtmlUtf8 = "text/html; charset=utf-8";
internal const int Status200Ok = 200;
internal const int Status206PartialContent = 206;
internal const int Status304NotModified = 304;

View File

@ -1,13 +1,11 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.FileSystems;
using Microsoft.AspNet.Hosting;
using Microsoft.AspNet.Http;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNet.StaticFiles
{
@ -65,7 +63,7 @@ namespace Microsoft.AspNet.StaticFiles
if (!Helpers.PathEndsInSlash(context.Request.Path))
{
context.Response.StatusCode = 301;
context.Response.Headers[Constants.Location] = context.Request.PathBase + context.Request.Path + "/" + context.Request.QueryString;
context.Response.Headers[HeaderNames.Location] = context.Request.PathBase + context.Request.Path + "/" + context.Request.QueryString;
return Constants.CompletedTask;
}

View File

@ -2,12 +2,12 @@
// 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.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.FileSystems;
using Microsoft.AspNet.Hosting;
using Microsoft.AspNet.Http;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNet.StaticFiles
{
@ -57,7 +57,7 @@ namespace Microsoft.AspNet.StaticFiles
if (!Helpers.PathEndsInSlash(context.Request.Path))
{
context.Response.StatusCode = 301;
context.Response.Headers[Constants.Location] = context.Request.PathBase + context.Request.Path + "/" + context.Request.QueryString;
context.Response.Headers[HeaderNames.Location] = context.Request.PathBase + context.Request.Path + "/" + context.Request.QueryString;
return Constants.CompletedTask;
}

View File

@ -2,8 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Globalization;
using System.IO;
using Microsoft.AspNet.Http;
namespace Microsoft.AspNet.StaticFiles
@ -45,15 +43,5 @@ namespace Microsoft.AspNet.StaticFiles
}
return false;
}
internal static bool TryParseHttpDate(string dateString, out DateTimeOffset parsedDate)
{
return DateTimeOffset.TryParseExact(dateString, Constants.HttpDateFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out parsedDate);
}
internal static string ResolveRootPath(string webRoot, PathString path)
{
return Path.GetFullPath(Path.Combine(webRoot, path.Value ?? string.Empty));
}
}
}

View File

@ -18,6 +18,8 @@ namespace Microsoft.AspNet.StaticFiles
/// </summary>
public class HtmlDirectoryFormatter : IDirectoryFormatter
{
private const string TextHtmlUtf8 = "text/html; charset=utf-8";
/// <summary>
/// Generates an HTML view for a directory.
/// </summary>
@ -32,7 +34,7 @@ namespace Microsoft.AspNet.StaticFiles
throw new ArgumentNullException("contents");
}
context.Response.ContentType = Constants.TextHtmlUtf8;
context.Response.ContentType = TextHtmlUtf8;
if (Helpers.IsHeadMethod(context.Request.Method))
{

View File

@ -3,107 +3,23 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNet.StaticFiles.Infrastructure
{
internal static class RangeHelpers
{
// Examples:
// bytes=0-499
// bytes=500-
// bytes=-500
// bytes=0-0,-1
// bytes=500-600,601-999
// Any individual bad range fails the whole parse and the header should be ignored.
internal static bool TryParseRanges(string rangeHeader, out IList<Tuple<long?, long?>> parsedRanges)
{
parsedRanges = null;
if (string.IsNullOrWhiteSpace(rangeHeader)
|| !rangeHeader.StartsWith("bytes=", StringComparison.OrdinalIgnoreCase))
{
return false;
}
string[] subRanges = rangeHeader.Substring("bytes=".Length).Replace(" ", string.Empty).Split(',');
List<Tuple<long?, long?>> ranges = new List<Tuple<long?, long?>>();
for (int i = 0; i < subRanges.Length; i++)
{
long? first = null, second = null;
string subRange = subRanges[i];
int dashIndex = subRange.IndexOf('-');
if (dashIndex < 0)
{
return false;
}
else if (dashIndex == 0)
{
// -500
string remainder = subRange.Substring(1);
if (!TryParseLong(remainder, out second))
{
return false;
}
}
else if (dashIndex == (subRange.Length - 1))
{
// 500-
string remainder = subRange.Substring(0, subRange.Length - 1);
if (!TryParseLong(remainder, out first))
{
return false;
}
}
else
{
// 0-499
string firstString = subRange.Substring(0, dashIndex);
string secondString = subRange.Substring(dashIndex + 1, subRange.Length - dashIndex - 1);
if (!TryParseLong(firstString, out first) || !TryParseLong(secondString, out second)
|| first.Value > second.Value)
{
return false;
}
}
ranges.Add(new Tuple<long?, long?>(first, second));
}
if (ranges.Count > 0)
{
parsedRanges = ranges;
return true;
}
return false;
}
private static bool TryParseLong(string input, out long? result)
{
int temp;
if (!string.IsNullOrWhiteSpace(input)
&& int.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out temp))
{
result = temp;
return true;
}
result = null;
return false;
}
// 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<Tuple<long, long>> NormalizeRanges(IList<Tuple<long?, long?>> ranges, long length)
internal static IList<RangeItemHeaderValue> NormalizeRanges(ICollection<RangeItemHeaderValue> ranges, long length)
{
IList<Tuple<long, long>> normalizedRanges = new List<Tuple<long, long>>(ranges.Count);
for (int i = 0; i < ranges.Count; i++)
IList<RangeItemHeaderValue> normalizedRanges = new List<RangeItemHeaderValue>(ranges.Count);
foreach (var range in ranges)
{
Tuple<long?, long?> range = ranges[i];
long? start = range.Item1, end = range.Item2;
long? start = range.From;
long? end = range.To;
// X-[Y]
if (start.HasValue)
@ -131,7 +47,7 @@ namespace Microsoft.AspNet.StaticFiles.Infrastructure
start = length - bytes;
end = start + bytes - 1;
}
normalizedRanges.Add(new Tuple<long, long>(start.Value, end.Value));
normalizedRanges.Add(new RangeItemHeaderValue(start.Value, end.Value));
}
return normalizedRanges;
}

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
@ -14,4 +14,9 @@
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>
<ProjectExtensions>
<VisualStudio>
<UserProperties project_1json__JSONSchema="http://www.asp.net/media/4878834/project.json" />
</VisualStudio>
</ProjectExtensions>
</Project>

View File

@ -4,14 +4,16 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.FileSystems;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Headers;
using Microsoft.AspNet.HttpFeature;
using Microsoft.AspNet.StaticFiles.Infrastructure;
using Microsoft.Framework.Logging;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNet.StaticFiles
{
@ -31,16 +33,17 @@ namespace Microsoft.AspNet.StaticFiles
private IFileInfo _fileInfo;
private long _length;
private DateTimeOffset _lastModified;
private string _lastModifiedString;
private string _etag;
private string _etagQuoted;
private EntityTagHeaderValue _etag;
private RequestHeaders _requestHeaders;
private ResponseHeaders _responseHeaders;
private PreconditionState _ifMatchState;
private PreconditionState _ifNoneMatchState;
private PreconditionState _ifModifiedSinceState;
private PreconditionState _ifUnmodifiedSinceState;
private IList<Tuple<long, long>> _ranges;
private IList<RangeItemHeaderValue> _ranges;
public StaticFileContext(HttpContext context, StaticFileOptions options, PathString matchUrl, ILogger logger)
{
@ -50,6 +53,8 @@ namespace Microsoft.AspNet.StaticFiles
_request = context.Request;
_response = context.Response;
_logger = logger;
_requestHeaders = _request.GetTypedHeaders();
_responseHeaders = _response.GetTypedHeaders();
_method = null;
_isGet = false;
@ -60,8 +65,6 @@ namespace Microsoft.AspNet.StaticFiles
_length = 0;
_lastModified = new DateTimeOffset();
_etag = null;
_etagQuoted = null;
_lastModifiedString = null;
_ifMatchState = PreconditionState.Unspecified;
_ifNoneMatchState = PreconditionState.Unspecified;
_ifModifiedSinceState = PreconditionState.Unspecified;
@ -86,7 +89,7 @@ namespace Microsoft.AspNet.StaticFiles
{
get { return _ranges != null; }
}
public string SubPath
{
get { return _subPath.Value; }
@ -132,11 +135,9 @@ namespace Microsoft.AspNet.StaticFiles
DateTimeOffset last = _fileInfo.LastModified;
// Truncate to the second.
_lastModified = new DateTimeOffset(last.Year, last.Month, last.Day, last.Hour, last.Minute, last.Second, last.Offset);
_lastModifiedString = _lastModified.ToString(Constants.HttpDateFormat, CultureInfo.InvariantCulture);
long etagHash = _lastModified.ToFileTime() ^ _length;
_etag = Convert.ToString(etagHash, 16);
_etagQuoted = '\"' + _etag + '\"';
_etag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
}
return _fileInfo.Exists;
}
@ -153,14 +154,13 @@ namespace Microsoft.AspNet.StaticFiles
private void ComputeIfMatch()
{
// 14.24 If-Match
IList<string> ifMatch = _request.Headers.GetCommaSeparatedValues(Constants.IfMatch); // Removes quotes
if (ifMatch != null)
var ifMatch = _requestHeaders.IfMatch;
if (ifMatch != null && ifMatch.Any())
{
_ifMatchState = PreconditionState.PreconditionFailed;
foreach (var segment in ifMatch)
foreach (var etag in ifMatch)
{
if (segment.Equals("*", StringComparison.Ordinal)
|| segment.Equals(_etag, StringComparison.Ordinal))
if (etag.Equals(EntityTagHeaderValue.Any) || etag.Equals(_etag))
{
_ifMatchState = PreconditionState.ShouldProcess;
break;
@ -169,14 +169,13 @@ namespace Microsoft.AspNet.StaticFiles
}
// 14.26 If-None-Match
IList<string> ifNoneMatch = _request.Headers.GetCommaSeparatedValues(Constants.IfNoneMatch);
if (ifNoneMatch != null)
var ifNoneMatch = _requestHeaders.IfNoneMatch;
if (ifNoneMatch != null && ifNoneMatch.Any())
{
_ifNoneMatchState = PreconditionState.ShouldProcess;
foreach (var segment in ifNoneMatch)
foreach (var etag in ifNoneMatch)
{
if (segment.Equals("*", StringComparison.Ordinal)
|| segment.Equals(_etag, StringComparison.Ordinal))
if (etag.Equals(EntityTagHeaderValue.Any) || etag.Equals(_etag))
{
_ifNoneMatchState = PreconditionState.NotModified;
break;
@ -188,18 +187,16 @@ namespace Microsoft.AspNet.StaticFiles
private void ComputeIfModifiedSince()
{
// 14.25 If-Modified-Since
string ifModifiedSinceString = _request.Headers.Get(Constants.IfModifiedSince);
DateTimeOffset ifModifiedSince;
if (Helpers.TryParseHttpDate(ifModifiedSinceString, out ifModifiedSince))
var ifModifiedSince = _requestHeaders.IfModifiedSince;
if (ifModifiedSince.HasValue)
{
bool modified = ifModifiedSince < _lastModified;
_ifModifiedSinceState = modified ? PreconditionState.ShouldProcess : PreconditionState.NotModified;
}
// 14.28 If-Unmodified-Since
string ifUnmodifiedSinceString = _request.Headers.Get(Constants.IfUnmodifiedSince);
DateTimeOffset ifUnmodifiedSince;
if (Helpers.TryParseHttpDate(ifUnmodifiedSinceString, out ifUnmodifiedSince))
var ifUnmodifiedSince = _requestHeaders.IfUnmodifiedSince;
if (ifUnmodifiedSince.HasValue)
{
bool unmodified = ifUnmodifiedSince >= _lastModified;
_ifUnmodifiedSinceState = unmodified ? PreconditionState.ShouldProcess : PreconditionState.PreconditionFailed;
@ -218,44 +215,41 @@ namespace Microsoft.AspNet.StaticFiles
return;
}
string rangeHeader = _request.Headers.Get(Constants.Range);
IList<Tuple<long?, long?>> ranges;
if (!RangeHelpers.TryParseRanges(rangeHeader, out ranges))
var rangeHeader = _requestHeaders.Range;
if (rangeHeader == null)
{
return;
}
if (ranges.Count > 1)
if (rangeHeader.Ranges.Count > 1)
{
// multiple range headers not yet supported
_logger.WriteWarning("Multiple range headers not yet supported, {0} ranges in header", ranges.Count.ToString());
// 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.WriteWarning("Multiple ranges are not allowed: '{0}'", rangeHeader.ToString());
return;
}
// 14.27 If-Range
string ifRangeHeader = _request.Headers.Get(Constants.IfRange);
if (!string.IsNullOrWhiteSpace(ifRangeHeader))
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.
DateTimeOffset ifRangeLastModified;
bool ignoreRangeHeader = false;
if (Helpers.TryParseHttpDate(ifRangeHeader, out ifRangeLastModified))
if (ifRangeHeader.LastModified.HasValue)
{
if (_lastModified > ifRangeLastModified)
if (_lastModified > ifRangeHeader.LastModified)
{
ignoreRangeHeader = true;
}
}
else
else if (ifRangeHeader.EntityTag != null && !_etag.Equals(ifRangeHeader.EntityTag))
{
if (!_etagQuoted.Equals(ifRangeHeader))
{
ignoreRangeHeader = true;
}
ignoreRangeHeader = true;
}
if (ignoreRangeHeader)
{
@ -263,7 +257,7 @@ namespace Microsoft.AspNet.StaticFiles
}
}
_ranges = RangeHelpers.NormalizeRanges(ranges, _length);
_ranges = RangeHelpers.NormalizeRanges(rangeHeader.Ranges, _length);
}
public void ApplyResponseHeaders(int statusCode)
@ -277,8 +271,9 @@ namespace Microsoft.AspNet.StaticFiles
{
_response.ContentType = _contentType;
}
_response.Headers.Set(Constants.LastModified, _lastModifiedString);
_response.Headers.Set(Constants.ETag, _etagQuoted);
_responseHeaders.LastModified = _lastModified;
_responseHeaders.ETag = _etag;
_responseHeaders.Headers[HeaderNames.AcceptRanges] = "bytes";
}
if (statusCode == Constants.Status200Ok)
{
@ -361,7 +356,7 @@ namespace Microsoft.AspNet.StaticFiles
// 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
_response.Headers[Constants.ContentRange] = "bytes */" + _length.ToString(CultureInfo.InvariantCulture);
_responseHeaders.ContentRange = new ContentRangeHeaderValue(_length);
ApplyResponseHeaders(Constants.Status416RangeNotSatisfiable);
_logger.WriteWarning("Range not satisfiable for {0}", SubPath);
return;
@ -371,7 +366,7 @@ namespace Microsoft.AspNet.StaticFiles
Debug.Assert(_ranges.Count == 1);
long start, length;
_response.Headers[Constants.ContentRange] = ComputeContentRange(_ranges[0], out start, out length);
_responseHeaders.ContentRange = ComputeContentRange(_ranges[0], out start, out length);
_response.ContentLength = length;
ApplyResponseHeaders(Constants.Status206PartialContent);
@ -381,7 +376,7 @@ namespace Microsoft.AspNet.StaticFiles
{
if (_logger.IsEnabled(LogLevel.Verbose))
{
_logger.WriteVerbose(string.Format("Sending {0} of file {1}", _response.Headers[Constants.ContentRange], physicalPath));
_logger.WriteVerbose(string.Format("Sending {0} of file {1}", _response.Headers[HeaderNames.ContentRange], physicalPath));
}
await sendFile.SendFileAsync(physicalPath, start, length, _context.RequestAborted);
return;
@ -393,7 +388,7 @@ namespace Microsoft.AspNet.StaticFiles
readStream.Seek(start, SeekOrigin.Begin); // TODO: What if !CanSeek?
if (_logger.IsEnabled(LogLevel.Verbose))
{
_logger.WriteVerbose(string.Format("Copying {0} of file {1} to the response body", _response.Headers[Constants.ContentRange], SubPath));
_logger.WriteVerbose(string.Format("Copying {0} of file {1} to the response body", _response.Headers[HeaderNames.ContentRange], SubPath));
}
await StreamCopyOperation.CopyToAsync(readStream, _response.Body, length, _context.RequestAborted);
}
@ -404,12 +399,12 @@ namespace Microsoft.AspNet.StaticFiles
}
// Note: This assumes ranges have been normalized to absolute byte offsets.
private string ComputeContentRange(Tuple<long, long> range, out long start, out long length)
private ContentRangeHeaderValue ComputeContentRange(RangeItemHeaderValue range, out long start, out long length)
{
start = range.Item1;
long end = range.Item2;
start = range.From.Value;
long end = range.To.Value;
length = end - start + 1;
return string.Format(CultureInfo.InvariantCulture, "bytes {0}-{1}/{2}", start, end, _length);
return new ContentRangeHeaderValue(start, end, _length);
}
}
}