diff --git a/MediaBrowser.Api/HttpHandlers/ImageHandler.cs b/MediaBrowser.Api/HttpHandlers/ImageHandler.cs index c61768a6d..0427a2d06 100644 --- a/MediaBrowser.Api/HttpHandlers/ImageHandler.cs +++ b/MediaBrowser.Api/HttpHandlers/ImageHandler.cs @@ -46,18 +46,15 @@ namespace MediaBrowser.Api.HttpHandlers } } - public override DateTime? LastDateModified + protected override DateTime? GetLastDateModified() { - get + try { - try - { - return File.GetLastWriteTime(ImagePath); - } - catch - { - return null; - } + return File.GetLastWriteTime(ImagePath); + } + catch + { + return base.GetLastDateModified(); } } diff --git a/MediaBrowser.Api/Plugin.cs b/MediaBrowser.Api/Plugin.cs index b6b1c8095..fe061a48b 100644 --- a/MediaBrowser.Api/Plugin.cs +++ b/MediaBrowser.Api/Plugin.cs @@ -1,8 +1,8 @@ using System; using System.ComponentModel.Composition; +using System.Net; using System.Reactive.Linq; using MediaBrowser.Api.HttpHandlers; -using MediaBrowser.Common.Net; using MediaBrowser.Common.Net.Handlers; using MediaBrowser.Common.Plugins; using MediaBrowser.Controller; @@ -22,78 +22,75 @@ namespace MediaBrowser.Api { var httpServer = Kernel.Instance.HttpServer; - httpServer.Where(ctx => ctx.LocalPath.IndexOf("/api/", StringComparison.OrdinalIgnoreCase) != -1).Subscribe(ctx => + httpServer.Where(ctx => ctx.Request.Url.LocalPath.IndexOf("/api/", StringComparison.OrdinalIgnoreCase) != -1).Subscribe(ctx => { BaseHandler handler = GetHandler(ctx); if (handler != null) { - ctx.Respond(handler); + handler.ProcessRequest(ctx); } }); } - private BaseHandler GetHandler(RequestContext ctx) + private BaseHandler GetHandler(HttpListenerContext ctx) { - BaseHandler handler = null; - - string localPath = ctx.LocalPath; + string localPath = ctx.Request.Url.LocalPath; if (localPath.EndsWith("/api/item", StringComparison.OrdinalIgnoreCase)) { - handler = new ItemHandler(); + return new ItemHandler(); } else if (localPath.EndsWith("/api/image", StringComparison.OrdinalIgnoreCase)) { - handler = new ImageHandler(); + return new ImageHandler(); } else if (localPath.EndsWith("/api/users", StringComparison.OrdinalIgnoreCase)) { - handler = new UsersHandler(); + return new UsersHandler(); } else if (localPath.EndsWith("/api/genre", StringComparison.OrdinalIgnoreCase)) { - handler = new GenreHandler(); + return new GenreHandler(); } else if (localPath.EndsWith("/api/genres", StringComparison.OrdinalIgnoreCase)) { - handler = new GenresHandler(); + return new GenresHandler(); } else if (localPath.EndsWith("/api/studio", StringComparison.OrdinalIgnoreCase)) { - handler = new StudioHandler(); + return new StudioHandler(); } else if (localPath.EndsWith("/api/studios", StringComparison.OrdinalIgnoreCase)) { - handler = new StudiosHandler(); + return new StudiosHandler(); } else if (localPath.EndsWith("/api/recentlyaddeditems", StringComparison.OrdinalIgnoreCase)) { - handler = new RecentlyAddedItemsHandler(); + return new RecentlyAddedItemsHandler(); } else if (localPath.EndsWith("/api/inprogressitems", StringComparison.OrdinalIgnoreCase)) { - handler = new InProgressItemsHandler(); + return new InProgressItemsHandler(); } else if (localPath.EndsWith("/api/userconfiguration", StringComparison.OrdinalIgnoreCase)) { - handler = new UserConfigurationHandler(); + return new UserConfigurationHandler(); } else if (localPath.EndsWith("/api/plugins", StringComparison.OrdinalIgnoreCase)) { - handler = new PluginsHandler(); + return new PluginsHandler(); } else if (localPath.EndsWith("/api/pluginconfiguration", StringComparison.OrdinalIgnoreCase)) { - handler = new PluginConfigurationHandler(); + return new PluginConfigurationHandler(); } - - if (handler != null) + else if (localPath.EndsWith("/api/static", StringComparison.OrdinalIgnoreCase)) { - handler.RequestContext = ctx; + return new StaticFileHandler(); } - return handler; + return null; } } } diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 7f6c66748..d5f6230e1 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -58,6 +58,7 @@ + @@ -73,7 +74,6 @@ - diff --git a/MediaBrowser.Common/Net/Handlers/BaseHandler.cs b/MediaBrowser.Common/Net/Handlers/BaseHandler.cs index 42d2f8190..5cb476e02 100644 --- a/MediaBrowser.Common/Net/Handlers/BaseHandler.cs +++ b/MediaBrowser.Common/Net/Handlers/BaseHandler.cs @@ -3,28 +3,17 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.IO; using System.IO.Compression; +using System.Linq; using System.Net; +using MediaBrowser.Common.Logging; namespace MediaBrowser.Common.Net.Handlers { public abstract class BaseHandler { - /// - /// Response headers - /// - public IDictionary Headers = new Dictionary(); - private Stream CompressedStream { get; set; } - public virtual bool UseChunkedEncoding - { - get - { - return true; - } - } - - public virtual long? ContentLength + public virtual bool? UseChunkedEncoding { get { @@ -32,6 +21,21 @@ namespace MediaBrowser.Common.Net.Handlers } } + private bool _TotalContentLengthDiscovered = false; + private long? _TotalContentLength = null; + public long? TotalContentLength + { + get + { + if (!_TotalContentLengthDiscovered) + { + _TotalContentLength = GetTotalContentLength(); + } + + return _TotalContentLength; + } + } + /// /// Returns true or false indicating if the handler writes to the stream asynchronously. /// If so the subclass will be responsible for disposing the stream when complete. @@ -44,29 +48,18 @@ namespace MediaBrowser.Common.Net.Handlers } } - /// - /// The action to write the response to the output stream - /// - public Action WriteStream + protected virtual bool SupportsByteRangeRequests { get { - return s => - { - WriteReponse(s); - - if (!IsAsyncHandler) - { - DisposeResponseStream(); - } - }; + return false; } } /// - /// The original RequestContext + /// The original HttpListenerContext /// - public RequestContext RequestContext { get; set; } + protected HttpListenerContext HttpListenerContext { get; private set; } /// /// The original QueryString @@ -75,7 +68,54 @@ namespace MediaBrowser.Common.Net.Handlers { get { - return RequestContext.Request.QueryString; + return HttpListenerContext.Request.QueryString; + } + } + + protected List> _RequestedRanges = null; + protected IEnumerable> RequestedRanges + { + get + { + if (_RequestedRanges == null) + { + _RequestedRanges = new List>(); + + if (IsRangeRequest) + { + // Example: bytes=0-,32-63 + string[] ranges = HttpListenerContext.Request.Headers["Range"].Split('=')[1].Split(','); + + foreach (string range in ranges) + { + string[] vals = range.Split('-'); + + long start = 0; + long? end = null; + + if (!string.IsNullOrEmpty(vals[0])) + { + start = long.Parse(vals[0]); + } + if (!string.IsNullOrEmpty(vals[1])) + { + end = long.Parse(vals[1]); + } + + _RequestedRanges.Add(new KeyValuePair(start, end)); + } + } + } + + return _RequestedRanges; + } + } + + protected bool IsRangeRequest + { + get + { + return HttpListenerContext.Request.Headers.AllKeys.Contains("Range"); } } @@ -87,13 +127,7 @@ namespace MediaBrowser.Common.Net.Handlers /// /// Gets the status code to include in the response headers /// - public virtual int StatusCode - { - get - { - return 200; - } - } + protected int StatusCode { get; set; } /// /// Gets the cache duration to include in the response headers @@ -106,18 +140,25 @@ namespace MediaBrowser.Common.Net.Handlers } } + private bool _LastDateModifiedDiscovered = false; + private DateTime? _LastDateModified = null; /// /// Gets the last date modified of the content being returned, if this can be determined. /// This will be used to invalidate the cache, so it's not needed if CacheDuration is 0. /// - public virtual DateTime? LastDateModified + public DateTime? LastDateModified { get { - return null; + if (!_LastDateModifiedDiscovered) + { + _LastDateModified = GetLastDateModified(); + } + + return _LastDateModified; } } - + public virtual bool CompressResponse { get @@ -130,7 +171,7 @@ namespace MediaBrowser.Common.Net.Handlers { get { - string enc = RequestContext.Request.Headers["Accept-Encoding"] ?? string.Empty; + string enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty; return enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1 || enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1; } @@ -140,7 +181,7 @@ namespace MediaBrowser.Common.Net.Handlers { get { - string enc = RequestContext.Request.Headers["Accept-Encoding"] ?? string.Empty; + string enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty; if (enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1) { @@ -155,30 +196,127 @@ namespace MediaBrowser.Common.Net.Handlers } } - protected virtual void PrepareResponseBeforeWriteOutput(HttpListenerResponse response) + public void ProcessRequest(HttpListenerContext ctx) { - // Don't force this to true. HttpListener will default it to true if supported by the client. - if (!UseChunkedEncoding) - { - response.SendChunked = false; - } + HttpListenerContext = ctx; - if (ContentLength.HasValue) - { - response.ContentLength64 = ContentLength.Value; - } + Logger.LogInfo("Http Server received request at: " + ctx.Request.Url.ToString()); + Logger.LogInfo("Http Headers: " + string.Join(",", ctx.Request.Headers.AllKeys.Select(k => k + "=" + ctx.Request.Headers[k]))); - if (CompressResponse && ClientSupportsCompression) + ctx.Response.AddHeader("Access-Control-Allow-Origin", "*"); + + ctx.Response.KeepAlive = true; + + if (SupportsByteRangeRequests && IsRangeRequest) { - response.AddHeader("Content-Encoding", CompressionMethod); + ctx.Response.Headers["Accept-Ranges"] = "bytes"; } + + // Set the initial status code + // When serving a range request, we need to return status code 206 to indicate a partial response body + StatusCode = SupportsByteRangeRequests && IsRangeRequest ? 206 : 200; + + ctx.Response.ContentType = ContentType; TimeSpan cacheDuration = CacheDuration; - + + if (ctx.Request.Headers.AllKeys.Contains("If-Modified-Since")) + { + DateTime ifModifiedSince; + + if (DateTime.TryParse(ctx.Request.Headers["If-Modified-Since"].Replace(" GMT", string.Empty), out ifModifiedSince)) + { + // If the cache hasn't expired yet just return a 304 + if (IsCacheValid(ifModifiedSince, cacheDuration, LastDateModified)) + { + StatusCode = 304; + } + } + } + + if (StatusCode == 200 || StatusCode == 206) + { + ProcessUncachedResponse(ctx, cacheDuration); + } + else + { + ctx.Response.StatusCode = StatusCode; + ctx.Response.SendChunked = false; + DisposeResponseStream(); + } + } + + private void ProcessUncachedResponse(HttpListenerContext ctx, TimeSpan cacheDuration) + { + long? totalContentLength = TotalContentLength; + + // By default, use chunked encoding if we don't know the content length + bool useChunkedEncoding = UseChunkedEncoding == null ? (totalContentLength == null) : UseChunkedEncoding.Value; + + // Don't force this to true. HttpListener will default it to true if supported by the client. + if (!useChunkedEncoding) + { + ctx.Response.SendChunked = false; + } + + // Set the content length, if we know it + if (totalContentLength.HasValue) + { + ctx.Response.ContentLength64 = totalContentLength.Value; + } + + // Add the compression header + if (CompressResponse && ClientSupportsCompression) + { + ctx.Response.AddHeader("Content-Encoding", CompressionMethod); + } + + // Add caching headers if (cacheDuration.Ticks > 0) { - CacheResponse(response, cacheDuration, LastDateModified); + CacheResponse(ctx.Response, cacheDuration, LastDateModified); } + + PrepareUncachedResponse(ctx, cacheDuration); + + // Set the status code + ctx.Response.StatusCode = StatusCode; + + if (StatusCode == 200 || StatusCode == 206) + { + // Finally, write the response data + Stream outputStream = ctx.Response.OutputStream; + + if (CompressResponse && ClientSupportsCompression) + { + if (CompressionMethod.Equals("deflate", StringComparison.OrdinalIgnoreCase)) + { + CompressedStream = new DeflateStream(outputStream, CompressionLevel.Fastest, false); + } + else + { + CompressedStream = new GZipStream(outputStream, CompressionLevel.Fastest, false); + } + + outputStream = CompressedStream; + } + + WriteResponseToOutputStream(outputStream); + + if (!IsAsyncHandler) + { + DisposeResponseStream(); + } + } + else + { + ctx.Response.SendChunked = false; + DisposeResponseStream(); + } + } + + protected virtual void PrepareUncachedResponse(HttpListenerContext ctx, TimeSpan cacheDuration) + { } private void CacheResponse(HttpListenerResponse response, TimeSpan duration, DateTime? dateModified) @@ -190,29 +328,6 @@ namespace MediaBrowser.Common.Net.Handlers response.Headers[HttpResponseHeader.LastModified] = lastModified.ToString("r"); } - private void WriteReponse(Stream stream) - { - PrepareResponseBeforeWriteOutput(RequestContext.Response); - - if (CompressResponse && ClientSupportsCompression) - { - if (CompressionMethod.Equals("deflate", StringComparison.OrdinalIgnoreCase)) - { - CompressedStream = new DeflateStream(stream, CompressionLevel.Fastest, false); - } - else - { - CompressedStream = new GZipStream(stream, CompressionLevel.Fastest, false); - } - - WriteResponseToOutputStream(CompressedStream); - } - else - { - WriteResponseToOutputStream(stream); - } - } - protected abstract void WriteResponseToOutputStream(Stream stream); protected void DisposeResponseStream() @@ -222,7 +337,45 @@ namespace MediaBrowser.Common.Net.Handlers CompressedStream.Dispose(); } - RequestContext.Response.OutputStream.Dispose(); + HttpListenerContext.Response.OutputStream.Dispose(); + } + + private bool IsCacheValid(DateTime ifModifiedSince, TimeSpan cacheDuration, DateTime? dateModified) + { + if (dateModified.HasValue) + { + DateTime lastModified = NormalizeDateForComparison(dateModified.Value); + ifModifiedSince = NormalizeDateForComparison(ifModifiedSince); + + return lastModified <= ifModifiedSince; + } + + DateTime cacheExpirationDate = ifModifiedSince.Add(cacheDuration); + + if (DateTime.Now < cacheExpirationDate) + { + return true; + } + + return false; + } + + /// + /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that + /// + private DateTime NormalizeDateForComparison(DateTime date) + { + return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second); + } + + protected virtual long? GetTotalContentLength() + { + return null; + } + + protected virtual DateTime? GetLastDateModified() + { + return null; } } } \ No newline at end of file diff --git a/MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs b/MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs new file mode 100644 index 000000000..9c9912152 --- /dev/null +++ b/MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using MediaBrowser.Common.Logging; + +namespace MediaBrowser.Common.Net.Handlers +{ + public class StaticFileHandler : BaseHandler + { + public string Path + { + get + { + return QueryString["path"]; + } + } + + private bool FileStreamDiscovered = false; + private FileStream _FileStream = null; + private FileStream FileStream + { + get + { + if (!FileStreamDiscovered) + { + try + { + _FileStream = File.OpenRead(Path); + } + catch (FileNotFoundException) + { + StatusCode = 404; + } + catch (DirectoryNotFoundException) + { + StatusCode = 404; + } + catch (UnauthorizedAccessException) + { + StatusCode = 403; + } + finally + { + FileStreamDiscovered = true; + } + } + + return _FileStream; + } + } + + protected override bool SupportsByteRangeRequests + { + get + { + return true; + } + } + + public override bool CompressResponse + { + get + { + string contentType = ContentType; + + // Can't compress these + if (IsRangeRequest) + { + return false; + } + + // Don't compress media + if (contentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase) || contentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // It will take some work to support compression within this handler + return false; + } + } + + protected override long? GetTotalContentLength() + { + try + { + return FileStream.Length; + } + catch + { + return base.GetTotalContentLength(); + } + } + + protected override DateTime? GetLastDateModified() + { + try + { + return File.GetLastWriteTime(Path); + } + catch + { + return base.GetLastDateModified(); + } + } + + protected override bool IsAsyncHandler + { + get + { + return true; + } + } + + public override string ContentType + { + get + { + return MimeTypes.GetMimeType(Path); + } + } + + protected async override void WriteResponseToOutputStream(Stream stream) + { + try + { + if (FileStream != null) + { + if (IsRangeRequest) + { + KeyValuePair requestedRange = RequestedRanges.First(); + + // If the requested range is "0-" and we know the total length, we can optimize by avoiding having to buffer the content into memory + if (requestedRange.Value == null && TotalContentLength != null) + { + await ServeCompleteRangeRequest(requestedRange, stream); + } + else if (TotalContentLength.HasValue) + { + // This will have to buffer a portion of the content into memory + await ServePartialRangeRequestWithKnownTotalContentLength(requestedRange, stream); + } + else + { + // This will have to buffer the entire content into memory + await ServePartialRangeRequestWithUnknownTotalContentLength(requestedRange, stream); + } + } + else + { + await FileStream.CopyToAsync(stream); + } + } + } + catch (Exception ex) + { + Logger.LogException("WriteResponseToOutputStream", ex); + } + finally + { + if (FileStream != null) + { + FileStream.Dispose(); + } + + DisposeResponseStream(); + } + } + + /// + /// Handles a range request of "bytes=0-" + /// This will serve the complete content and add the content-range header + /// + private async Task ServeCompleteRangeRequest(KeyValuePair requestedRange, Stream responseStream) + { + long totalContentLength = TotalContentLength.Value; + + long rangeStart = requestedRange.Key; + long rangeEnd = totalContentLength - 1; + long rangeLength = 1 + rangeEnd - rangeStart; + + // Content-Length is the length of what we're serving, not the original content + HttpListenerContext.Response.ContentLength64 = rangeLength; + HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength); + + if (rangeStart > 0) + { + FileStream.Position = rangeStart; + } + + await FileStream.CopyToAsync(responseStream); + } + + /// + /// Serves a partial range request where the total content length is not known + /// + private async Task ServePartialRangeRequestWithUnknownTotalContentLength(KeyValuePair requestedRange, Stream responseStream) + { + // Read the entire stream so that we can determine the length + byte[] bytes = await ReadBytes(FileStream, 0, null); + + long totalContentLength = bytes.LongLength; + + long rangeStart = requestedRange.Key; + long rangeEnd = requestedRange.Value ?? (totalContentLength - 1); + long rangeLength = 1 + rangeEnd - rangeStart; + + // Content-Length is the length of what we're serving, not the original content + HttpListenerContext.Response.ContentLength64 = rangeLength; + HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength); + + await responseStream.WriteAsync(bytes, Convert.ToInt32(rangeStart), Convert.ToInt32(rangeLength)); + } + + /// + /// Serves a partial range request where the total content length is already known + /// + private async Task ServePartialRangeRequestWithKnownTotalContentLength(KeyValuePair requestedRange, Stream responseStream) + { + long totalContentLength = TotalContentLength.Value; + long rangeStart = requestedRange.Key; + long rangeEnd = requestedRange.Value ?? (totalContentLength - 1); + long rangeLength = 1 + rangeEnd - rangeStart; + + // Only read the bytes we need + byte[] bytes = await ReadBytes(FileStream, Convert.ToInt32(rangeStart), Convert.ToInt32(rangeLength)); + + // Content-Length is the length of what we're serving, not the original content + HttpListenerContext.Response.ContentLength64 = rangeLength; + + HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength); + + await responseStream.WriteAsync(bytes, 0, Convert.ToInt32(rangeLength)); + } + + /// + /// Reads bytes from a stream + /// + /// The input stream + /// The starting position + /// The number of bytes to read, or null to read to the end. + private async Task ReadBytes(Stream input, int start, int? count) + { + if (start > 0) + { + input.Position = start; + } + + if (count == null) + { + byte[] buffer = new byte[16 * 1024]; + + using (MemoryStream ms = new MemoryStream()) + { + int read; + while ((read = await input.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + await ms.WriteAsync(buffer, 0, read); + } + return ms.ToArray(); + } + } + else + { + byte[] buffer = new byte[count.Value]; + + using (MemoryStream ms = new MemoryStream()) + { + int read = await input.ReadAsync(buffer, 0, buffer.Length); + + await ms.WriteAsync(buffer, 0, read); + + return ms.ToArray(); + } + } + + } + } +} diff --git a/MediaBrowser.Common/Net/HttpServer.cs b/MediaBrowser.Common/Net/HttpServer.cs index fad8d13eb..9acccbf2d 100644 --- a/MediaBrowser.Common/Net/HttpServer.cs +++ b/MediaBrowser.Common/Net/HttpServer.cs @@ -4,10 +4,10 @@ using System.Reactive.Linq; namespace MediaBrowser.Common.Net { - public class HttpServer : IObservable, IDisposable + public class HttpServer : IObservable, IDisposable { private readonly HttpListener listener; - private readonly IObservable stream; + private readonly IObservable stream; public HttpServer(string url) { @@ -17,12 +17,11 @@ namespace MediaBrowser.Common.Net stream = ObservableHttpContext(); } - private IObservable ObservableHttpContext() + private IObservable ObservableHttpContext() { - return Observable.Create(obs => + return Observable.Create(obs => Observable.FromAsyncPattern(listener.BeginGetContext, listener.EndGetContext)() - .Select(c => new RequestContext(c)) .Subscribe(obs)) .Repeat() .Retry() @@ -34,7 +33,7 @@ namespace MediaBrowser.Common.Net listener.Stop(); } - public IDisposable Subscribe(IObserver observer) + public IDisposable Subscribe(IObserver observer) { return stream.Subscribe(observer); } diff --git a/MediaBrowser.Common/Net/RequestContext.cs b/MediaBrowser.Common/Net/RequestContext.cs deleted file mode 100644 index 461f28601..000000000 --- a/MediaBrowser.Common/Net/RequestContext.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Linq; -using System.Net; -using MediaBrowser.Common.Logging; -using MediaBrowser.Common.Net.Handlers; - -namespace MediaBrowser.Common.Net -{ - public class RequestContext - { - public HttpListenerRequest Request { get; private set; } - public HttpListenerResponse Response { get; private set; } - - public string LocalPath - { - get - { - return Request.Url.LocalPath; - } - } - - public RequestContext(HttpListenerContext context) - { - Response = context.Response; - Request = context.Request; - } - - public void Respond(BaseHandler handler) - { - Logger.LogInfo("Http Server received request at: " + Request.Url.ToString()); - Logger.LogInfo("Http Headers: " + string.Join(",", Request.Headers.AllKeys.Select(k => k + "=" + Request.Headers[k]))); - - Response.AddHeader("Access-Control-Allow-Origin", "*"); - - Response.KeepAlive = true; - - foreach (var header in handler.Headers) - { - Response.AddHeader(header.Key, header.Value); - } - - int statusCode = handler.StatusCode; - Response.ContentType = handler.ContentType; - - TimeSpan cacheDuration = handler.CacheDuration; - - if (Request.Headers.AllKeys.Contains("If-Modified-Since")) - { - DateTime ifModifiedSince; - - if (DateTime.TryParse(Request.Headers["If-Modified-Since"].Replace(" GMT", string.Empty), out ifModifiedSince)) - { - // If the cache hasn't expired yet just return a 304 - if (IsCacheValid(ifModifiedSince, cacheDuration, handler.LastDateModified)) - { - statusCode = 304; - } - } - } - - Response.StatusCode = statusCode; - - if (statusCode == 200 || statusCode == 206) - { - handler.WriteStream(Response.OutputStream); - } - else - { - Response.SendChunked = false; - Response.OutputStream.Dispose(); - } - } - - private bool IsCacheValid(DateTime ifModifiedSince, TimeSpan cacheDuration, DateTime? dateModified) - { - if (dateModified.HasValue) - { - DateTime lastModified = NormalizeDateForComparison(dateModified.Value); - ifModifiedSince = NormalizeDateForComparison(ifModifiedSince); - - return lastModified <= ifModifiedSince; - } - - DateTime cacheExpirationDate = ifModifiedSince.Add(cacheDuration); - - if (DateTime.Now < cacheExpirationDate) - { - return true; - } - - return false; - } - - /// - /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that - /// - private DateTime NormalizeDateForComparison(DateTime date) - { - return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second); - } - - } -} \ No newline at end of file