diff --git a/src/Microsoft.AspNet.StaticFiles/Constants.cs b/src/Microsoft.AspNet.StaticFiles/Constants.cs index 72468bd921..caa592e6c4 100644 --- a/src/Microsoft.AspNet.StaticFiles/Constants.cs +++ b/src/Microsoft.AspNet.StaticFiles/Constants.cs @@ -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; diff --git a/src/Microsoft.AspNet.StaticFiles/DefaultFilesMiddleware.cs b/src/Microsoft.AspNet.StaticFiles/DefaultFilesMiddleware.cs index 787cc0421e..e5bb73a6ad 100644 --- a/src/Microsoft.AspNet.StaticFiles/DefaultFilesMiddleware.cs +++ b/src/Microsoft.AspNet.StaticFiles/DefaultFilesMiddleware.cs @@ -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; } diff --git a/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserMiddleware.cs b/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserMiddleware.cs index 596dbf094e..3a4e1a0150 100644 --- a/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserMiddleware.cs +++ b/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserMiddleware.cs @@ -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; } diff --git a/src/Microsoft.AspNet.StaticFiles/Helpers.cs b/src/Microsoft.AspNet.StaticFiles/Helpers.cs index 669d655dfd..39e5fa2532 100644 --- a/src/Microsoft.AspNet.StaticFiles/Helpers.cs +++ b/src/Microsoft.AspNet.StaticFiles/Helpers.cs @@ -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)); - } } } diff --git a/src/Microsoft.AspNet.StaticFiles/HtmlDirectoryFormatter.cs b/src/Microsoft.AspNet.StaticFiles/HtmlDirectoryFormatter.cs index bd14beba4d..b6ad7fdc73 100644 --- a/src/Microsoft.AspNet.StaticFiles/HtmlDirectoryFormatter.cs +++ b/src/Microsoft.AspNet.StaticFiles/HtmlDirectoryFormatter.cs @@ -18,6 +18,8 @@ namespace Microsoft.AspNet.StaticFiles /// public class HtmlDirectoryFormatter : IDirectoryFormatter { + private const string TextHtmlUtf8 = "text/html; charset=utf-8"; + /// /// Generates an HTML view for a directory. /// @@ -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)) { diff --git a/src/Microsoft.AspNet.StaticFiles/Infrastructure/RangeHelpers.cs b/src/Microsoft.AspNet.StaticFiles/Infrastructure/RangeHelpers.cs index 7599ed8778..b1b101d8c3 100644 --- a/src/Microsoft.AspNet.StaticFiles/Infrastructure/RangeHelpers.cs +++ b/src/Microsoft.AspNet.StaticFiles/Infrastructure/RangeHelpers.cs @@ -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> 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> ranges = new List>(); - - 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(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> NormalizeRanges(IList> ranges, long length) + internal static IList NormalizeRanges(ICollection ranges, long length) { - IList> normalizedRanges = new List>(ranges.Count); - for (int i = 0; i < ranges.Count; i++) + IList normalizedRanges = new List(ranges.Count); + foreach (var range in ranges) { - Tuple 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(start.Value, end.Value)); + normalizedRanges.Add(new RangeItemHeaderValue(start.Value, end.Value)); } return normalizedRanges; } diff --git a/src/Microsoft.AspNet.StaticFiles/Microsoft.AspNet.StaticFiles.kproj b/src/Microsoft.AspNet.StaticFiles/Microsoft.AspNet.StaticFiles.kproj index 333cd199d5..b2a8a6a04a 100644 --- a/src/Microsoft.AspNet.StaticFiles/Microsoft.AspNet.StaticFiles.kproj +++ b/src/Microsoft.AspNet.StaticFiles/Microsoft.AspNet.StaticFiles.kproj @@ -1,4 +1,4 @@ - + 14.0 @@ -14,4 +14,9 @@ 2.0 - + + + + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.StaticFiles/StaticFileContext.cs b/src/Microsoft.AspNet.StaticFiles/StaticFileContext.cs index 37326059e5..d326b4c839 100644 --- a/src/Microsoft.AspNet.StaticFiles/StaticFileContext.cs +++ b/src/Microsoft.AspNet.StaticFiles/StaticFileContext.cs @@ -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> _ranges; + private IList _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 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 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> 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 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); } } }