mirror of https://github.com/jellyfin/jellyfin.git
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,18 +46,15 @@ namespace MediaBrowser.Api.HttpHandlers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override DateTime? LastDateModified
|
protected override DateTime? GetLastDateModified()
|
||||||
{
|
{
|
||||||
get
|
try
|
||||||
{
|
{
|
||||||
try
|
return File.GetLastWriteTime(ImagePath);
|
||||||
{
|
}
|
||||||
return File.GetLastWriteTime(ImagePath);
|
catch
|
||||||
}
|
{
|
||||||
catch
|
return base.GetLastDateModified();
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel.Composition;
|
using System.ComponentModel.Composition;
|
||||||
|
using System.Net;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
using MediaBrowser.Api.HttpHandlers;
|
using MediaBrowser.Api.HttpHandlers;
|
||||||
using MediaBrowser.Common.Net;
|
|
||||||
using MediaBrowser.Common.Net.Handlers;
|
using MediaBrowser.Common.Net.Handlers;
|
||||||
using MediaBrowser.Common.Plugins;
|
using MediaBrowser.Common.Plugins;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
|
@ -22,78 +22,75 @@ namespace MediaBrowser.Api
|
||||||
{
|
{
|
||||||
var httpServer = Kernel.Instance.HttpServer;
|
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);
|
BaseHandler handler = GetHandler(ctx);
|
||||||
|
|
||||||
if (handler != null)
|
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.Request.Url.LocalPath;
|
||||||
|
|
||||||
string localPath = ctx.LocalPath;
|
|
||||||
|
|
||||||
if (localPath.EndsWith("/api/item", StringComparison.OrdinalIgnoreCase))
|
if (localPath.EndsWith("/api/item", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
handler = new ItemHandler();
|
return new ItemHandler();
|
||||||
}
|
}
|
||||||
else if (localPath.EndsWith("/api/image", StringComparison.OrdinalIgnoreCase))
|
else if (localPath.EndsWith("/api/image", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
handler = new ImageHandler();
|
return new ImageHandler();
|
||||||
}
|
}
|
||||||
else if (localPath.EndsWith("/api/users", StringComparison.OrdinalIgnoreCase))
|
else if (localPath.EndsWith("/api/users", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
handler = new UsersHandler();
|
return new UsersHandler();
|
||||||
}
|
}
|
||||||
else if (localPath.EndsWith("/api/genre", StringComparison.OrdinalIgnoreCase))
|
else if (localPath.EndsWith("/api/genre", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
handler = new GenreHandler();
|
return new GenreHandler();
|
||||||
}
|
}
|
||||||
else if (localPath.EndsWith("/api/genres", StringComparison.OrdinalIgnoreCase))
|
else if (localPath.EndsWith("/api/genres", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
handler = new GenresHandler();
|
return new GenresHandler();
|
||||||
}
|
}
|
||||||
else if (localPath.EndsWith("/api/studio", StringComparison.OrdinalIgnoreCase))
|
else if (localPath.EndsWith("/api/studio", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
handler = new StudioHandler();
|
return new StudioHandler();
|
||||||
}
|
}
|
||||||
else if (localPath.EndsWith("/api/studios", StringComparison.OrdinalIgnoreCase))
|
else if (localPath.EndsWith("/api/studios", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
handler = new StudiosHandler();
|
return new StudiosHandler();
|
||||||
}
|
}
|
||||||
else if (localPath.EndsWith("/api/recentlyaddeditems", StringComparison.OrdinalIgnoreCase))
|
else if (localPath.EndsWith("/api/recentlyaddeditems", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
handler = new RecentlyAddedItemsHandler();
|
return new RecentlyAddedItemsHandler();
|
||||||
}
|
}
|
||||||
else if (localPath.EndsWith("/api/inprogressitems", StringComparison.OrdinalIgnoreCase))
|
else if (localPath.EndsWith("/api/inprogressitems", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
handler = new InProgressItemsHandler();
|
return new InProgressItemsHandler();
|
||||||
}
|
}
|
||||||
else if (localPath.EndsWith("/api/userconfiguration", StringComparison.OrdinalIgnoreCase))
|
else if (localPath.EndsWith("/api/userconfiguration", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
handler = new UserConfigurationHandler();
|
return new UserConfigurationHandler();
|
||||||
}
|
}
|
||||||
else if (localPath.EndsWith("/api/plugins", StringComparison.OrdinalIgnoreCase))
|
else if (localPath.EndsWith("/api/plugins", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
handler = new PluginsHandler();
|
return new PluginsHandler();
|
||||||
}
|
}
|
||||||
else if (localPath.EndsWith("/api/pluginconfiguration", StringComparison.OrdinalIgnoreCase))
|
else if (localPath.EndsWith("/api/pluginconfiguration", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
handler = new PluginConfigurationHandler();
|
return new PluginConfigurationHandler();
|
||||||
}
|
}
|
||||||
|
else if (localPath.EndsWith("/api/static", StringComparison.OrdinalIgnoreCase))
|
||||||
if (handler != null)
|
|
||||||
{
|
{
|
||||||
handler.RequestContext = ctx;
|
return new StaticFileHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
return handler;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,7 @@
|
||||||
<Compile Include="Configuration\ApplicationPaths.cs" />
|
<Compile Include="Configuration\ApplicationPaths.cs" />
|
||||||
<Compile Include="Configuration\BaseApplicationConfiguration.cs" />
|
<Compile Include="Configuration\BaseApplicationConfiguration.cs" />
|
||||||
<Compile Include="Events\GenericItemEventArgs.cs" />
|
<Compile Include="Events\GenericItemEventArgs.cs" />
|
||||||
|
<Compile Include="Net\Handlers\StaticFileHandler.cs" />
|
||||||
<Compile Include="Net\MimeTypes.cs" />
|
<Compile Include="Net\MimeTypes.cs" />
|
||||||
<Compile Include="Serialization\JsonSerializer.cs" />
|
<Compile Include="Serialization\JsonSerializer.cs" />
|
||||||
<Compile Include="Kernel\BaseKernel.cs" />
|
<Compile Include="Kernel\BaseKernel.cs" />
|
||||||
|
@ -73,7 +74,6 @@
|
||||||
<Compile Include="Net\Handlers\BaseJsonHandler.cs" />
|
<Compile Include="Net\Handlers\BaseJsonHandler.cs" />
|
||||||
<Compile Include="Net\HttpServer.cs" />
|
<Compile Include="Net\HttpServer.cs" />
|
||||||
<Compile Include="Net\Request.cs" />
|
<Compile Include="Net\Request.cs" />
|
||||||
<Compile Include="Net\RequestContext.cs" />
|
|
||||||
<Compile Include="Net\StreamExtensions.cs" />
|
<Compile Include="Net\StreamExtensions.cs" />
|
||||||
<Compile Include="Plugins\BasePlugin.cs" />
|
<Compile Include="Plugins\BasePlugin.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
|
|
@ -3,28 +3,17 @@ using System.Collections.Generic;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using MediaBrowser.Common.Logging;
|
||||||
|
|
||||||
namespace MediaBrowser.Common.Net.Handlers
|
namespace MediaBrowser.Common.Net.Handlers
|
||||||
{
|
{
|
||||||
public abstract class BaseHandler
|
public abstract class BaseHandler
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Response headers
|
|
||||||
/// </summary>
|
|
||||||
public IDictionary<string, string> Headers = new Dictionary<string, string>();
|
|
||||||
|
|
||||||
private Stream CompressedStream { get; set; }
|
private Stream CompressedStream { get; set; }
|
||||||
|
|
||||||
public virtual bool UseChunkedEncoding
|
public virtual bool? UseChunkedEncoding
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual long? ContentLength
|
|
||||||
{
|
{
|
||||||
get
|
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>
|
/// <summary>
|
||||||
/// Returns true or false indicating if the handler writes to the stream asynchronously.
|
/// 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.
|
/// If so the subclass will be responsible for disposing the stream when complete.
|
||||||
|
@ -44,29 +48,18 @@ namespace MediaBrowser.Common.Net.Handlers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
protected virtual bool SupportsByteRangeRequests
|
||||||
/// The action to write the response to the output stream
|
|
||||||
/// </summary>
|
|
||||||
public Action<Stream> WriteStream
|
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
return s =>
|
return false;
|
||||||
{
|
|
||||||
WriteReponse(s);
|
|
||||||
|
|
||||||
if (!IsAsyncHandler)
|
|
||||||
{
|
|
||||||
DisposeResponseStream();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The original RequestContext
|
/// The original HttpListenerContext
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public RequestContext RequestContext { get; set; }
|
protected HttpListenerContext HttpListenerContext { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The original QueryString
|
/// The original QueryString
|
||||||
|
@ -75,7 +68,54 @@ namespace MediaBrowser.Common.Net.Handlers
|
||||||
{
|
{
|
||||||
get
|
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>
|
/// <summary>
|
||||||
/// Gets the status code to include in the response headers
|
/// Gets the status code to include in the response headers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual int StatusCode
|
protected int StatusCode { get; set; }
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return 200;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the cache duration to include in the response headers
|
/// 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;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the last date modified of the content being returned, if this can be determined.
|
/// 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.
|
/// This will be used to invalidate the cache, so it's not needed if CacheDuration is 0.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual DateTime? LastDateModified
|
public DateTime? LastDateModified
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
return null;
|
if (!_LastDateModifiedDiscovered)
|
||||||
|
{
|
||||||
|
_LastDateModified = GetLastDateModified();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _LastDateModified;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual bool CompressResponse
|
public virtual bool CompressResponse
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
@ -130,7 +171,7 @@ namespace MediaBrowser.Common.Net.Handlers
|
||||||
{
|
{
|
||||||
get
|
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;
|
return enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1 || enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1;
|
||||||
}
|
}
|
||||||
|
@ -140,7 +181,7 @@ namespace MediaBrowser.Common.Net.Handlers
|
||||||
{
|
{
|
||||||
get
|
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)
|
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.
|
HttpListenerContext = ctx;
|
||||||
if (!UseChunkedEncoding)
|
|
||||||
{
|
|
||||||
response.SendChunked = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ContentLength.HasValue)
|
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])));
|
||||||
response.ContentLength64 = ContentLength.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
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)
|
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)
|
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");
|
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 abstract void WriteResponseToOutputStream(Stream stream);
|
||||||
|
|
||||||
protected void DisposeResponseStream()
|
protected void DisposeResponseStream()
|
||||||
|
@ -222,7 +337,45 @@ namespace MediaBrowser.Common.Net.Handlers
|
||||||
CompressedStream.Dispose();
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
namespace MediaBrowser.Common.Net
|
||||||
{
|
{
|
||||||
public class HttpServer : IObservable<RequestContext>, IDisposable
|
public class HttpServer : IObservable<HttpListenerContext>, IDisposable
|
||||||
{
|
{
|
||||||
private readonly HttpListener listener;
|
private readonly HttpListener listener;
|
||||||
private readonly IObservable<RequestContext> stream;
|
private readonly IObservable<HttpListenerContext> stream;
|
||||||
|
|
||||||
public HttpServer(string url)
|
public HttpServer(string url)
|
||||||
{
|
{
|
||||||
|
@ -17,12 +17,11 @@ namespace MediaBrowser.Common.Net
|
||||||
stream = ObservableHttpContext();
|
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,
|
Observable.FromAsyncPattern<HttpListenerContext>(listener.BeginGetContext,
|
||||||
listener.EndGetContext)()
|
listener.EndGetContext)()
|
||||||
.Select(c => new RequestContext(c))
|
|
||||||
.Subscribe(obs))
|
.Subscribe(obs))
|
||||||
.Repeat()
|
.Repeat()
|
||||||
.Retry()
|
.Retry()
|
||||||
|
@ -34,7 +33,7 @@ namespace MediaBrowser.Common.Net
|
||||||
listener.Stop();
|
listener.Stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDisposable Subscribe(IObserver<RequestContext> observer)
|
public IDisposable Subscribe(IObserver<HttpListenerContext> observer)
|
||||||
{
|
{
|
||||||
return stream.Subscribe(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