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);
}
}
}