Added the ability for the server to handle byte-range requests, and also added a static file handler to utilize it
This commit is contained in:
parent
dce7706382
commit
2536011247
|
@ -46,9 +46,7 @@ namespace MediaBrowser.Api.HttpHandlers
|
|||
}
|
||||
}
|
||||
|
||||
public override DateTime? LastDateModified
|
||||
{
|
||||
get
|
||||
protected override DateTime? GetLastDateModified()
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -56,8 +54,7 @@ namespace MediaBrowser.Api.HttpHandlers
|
|||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return base.GetLastDateModified();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
<Compile Include="Configuration\ApplicationPaths.cs" />
|
||||
<Compile Include="Configuration\BaseApplicationConfiguration.cs" />
|
||||
<Compile Include="Events\GenericItemEventArgs.cs" />
|
||||
<Compile Include="Net\Handlers\StaticFileHandler.cs" />
|
||||
<Compile Include="Net\MimeTypes.cs" />
|
||||
<Compile Include="Serialization\JsonSerializer.cs" />
|
||||
<Compile Include="Kernel\BaseKernel.cs" />
|
||||
|
@ -73,7 +74,6 @@
|
|||
<Compile Include="Net\Handlers\BaseJsonHandler.cs" />
|
||||
<Compile Include="Net\HttpServer.cs" />
|
||||
<Compile Include="Net\Request.cs" />
|
||||
<Compile Include="Net\RequestContext.cs" />
|
||||
<Compile Include="Net\StreamExtensions.cs" />
|
||||
<Compile Include="Plugins\BasePlugin.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.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
|
||||
{
|
||||
/// <summary>
|
||||
/// Response headers
|
||||
/// </summary>
|
||||
public IDictionary<string, string> Headers = new Dictionary<string, string>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The action to write the response to the output stream
|
||||
/// </summary>
|
||||
public Action<Stream> WriteStream
|
||||
protected virtual bool SupportsByteRangeRequests
|
||||
{
|
||||
get
|
||||
{
|
||||
return s =>
|
||||
{
|
||||
WriteReponse(s);
|
||||
|
||||
if (!IsAsyncHandler)
|
||||
{
|
||||
DisposeResponseStream();
|
||||
}
|
||||
};
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The original RequestContext
|
||||
/// The original HttpListenerContext
|
||||
/// </summary>
|
||||
public RequestContext RequestContext { get; set; }
|
||||
protected HttpListenerContext HttpListenerContext { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The original QueryString
|
||||
|
@ -75,7 +68,54 @@ namespace MediaBrowser.Common.Net.Handlers
|
|||
{
|
||||
get
|
||||
{
|
||||
return RequestContext.Request.QueryString;
|
||||
return HttpListenerContext.Request.QueryString;
|
||||
}
|
||||
}
|
||||
|
||||
protected List<KeyValuePair<long, long?>> _RequestedRanges = null;
|
||||
protected IEnumerable<KeyValuePair<long, long?>> RequestedRanges
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_RequestedRanges == null)
|
||||
{
|
||||
_RequestedRanges = new List<KeyValuePair<long, long?>>();
|
||||
|
||||
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<long, long?>(start, end));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _RequestedRanges;
|
||||
}
|
||||
}
|
||||
|
||||
protected bool IsRangeRequest
|
||||
{
|
||||
get
|
||||
{
|
||||
return HttpListenerContext.Request.Headers.AllKeys.Contains("Range");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,13 +127,7 @@ namespace MediaBrowser.Common.Net.Handlers
|
|||
/// <summary>
|
||||
/// Gets the status code to include in the response headers
|
||||
/// </summary>
|
||||
public virtual int StatusCode
|
||||
{
|
||||
get
|
||||
{
|
||||
return 200;
|
||||
}
|
||||
}
|
||||
protected int StatusCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cache duration to include in the response headers
|
||||
|
@ -106,15 +140,22 @@ namespace MediaBrowser.Common.Net.Handlers
|
|||
}
|
||||
}
|
||||
|
||||
private bool _LastDateModifiedDiscovered = false;
|
||||
private DateTime? _LastDateModified = null;
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public virtual DateTime? LastDateModified
|
||||
public DateTime? LastDateModified
|
||||
{
|
||||
get
|
||||
{
|
||||
return null;
|
||||
if (!_LastDateModifiedDiscovered)
|
||||
{
|
||||
_LastDateModified = GetLastDateModified();
|
||||
}
|
||||
|
||||
return _LastDateModified;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
HttpListenerContext = ctx;
|
||||
|
||||
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])));
|
||||
|
||||
ctx.Response.AddHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
ctx.Response.KeepAlive = true;
|
||||
|
||||
if (SupportsByteRangeRequests && IsRangeRequest)
|
||||
{
|
||||
response.SendChunked = false;
|
||||
ctx.Response.Headers["Accept-Ranges"] = "bytes";
|
||||
}
|
||||
|
||||
if (ContentLength.HasValue)
|
||||
{
|
||||
response.ContentLength64 = ContentLength.Value;
|
||||
}
|
||||
// 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;
|
||||
|
||||
if (CompressResponse && ClientSupportsCompression)
|
||||
{
|
||||
response.AddHeader("Content-Encoding", CompressionMethod);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
282
MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs
Normal file
282
MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs
Normal file
|
@ -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<long, long?> 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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a range request of "bytes=0-"
|
||||
/// This will serve the complete content and add the content-range header
|
||||
/// </summary>
|
||||
private async Task ServeCompleteRangeRequest(KeyValuePair<long, long?> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serves a partial range request where the total content length is not known
|
||||
/// </summary>
|
||||
private async Task ServePartialRangeRequestWithUnknownTotalContentLength(KeyValuePair<long, long?> 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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serves a partial range request where the total content length is already known
|
||||
/// </summary>
|
||||
private async Task ServePartialRangeRequestWithKnownTotalContentLength(KeyValuePair<long, long?> 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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads bytes from a stream
|
||||
/// </summary>
|
||||
/// <param name="input">The input stream</param>
|
||||
/// <param name="start">The starting position</param>
|
||||
/// <param name="count">The number of bytes to read, or null to read to the end.</param>
|
||||
private async Task<byte[]> 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,10 +4,10 @@ using System.Reactive.Linq;
|
|||
|
||||
namespace MediaBrowser.Common.Net
|
||||
{
|
||||
public class HttpServer : IObservable<RequestContext>, IDisposable
|
||||
public class HttpServer : IObservable<HttpListenerContext>, IDisposable
|
||||
{
|
||||
private readonly HttpListener listener;
|
||||
private readonly IObservable<RequestContext> stream;
|
||||
private readonly IObservable<HttpListenerContext> stream;
|
||||
|
||||
public HttpServer(string url)
|
||||
{
|
||||
|
@ -17,12 +17,11 @@ namespace MediaBrowser.Common.Net
|
|||
stream = ObservableHttpContext();
|
||||
}
|
||||
|
||||
private IObservable<RequestContext> ObservableHttpContext()
|
||||
private IObservable<HttpListenerContext> ObservableHttpContext()
|
||||
{
|
||||
return Observable.Create<RequestContext>(obs =>
|
||||
return Observable.Create<HttpListenerContext>(obs =>
|
||||
Observable.FromAsyncPattern<HttpListenerContext>(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<RequestContext> observer)
|
||||
public IDisposable Subscribe(IObserver<HttpListenerContext> observer)
|
||||
{
|
||||
return stream.Subscribe(observer);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that
|
||||
/// </summary>
|
||||
private DateTime NormalizeDateForComparison(DateTime date)
|
||||
{
|
||||
return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user