jellyfin/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs
2014-07-08 20:46:11 -04:00

560 lines
19 KiB
C#

using Amib.Threading;
using Funq;
using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Logging;
using ServiceStack;
using ServiceStack.Api.Swagger;
using ServiceStack.Host;
using ServiceStack.Host.Handlers;
using ServiceStack.Host.HttpListener;
using ServiceStack.Logging;
using ServiceStack.Web;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Server.Implementations.HttpServer
{
public class HttpListenerHost : ServiceStackHost, IHttpServer
{
private string HandlerPath { get; set; }
private string DefaultRedirectPath { get; set; }
private readonly ILogger _logger;
public IEnumerable<string> UrlPrefixes { get; private set; }
private readonly List<IRestfulService> _restServices = new List<IRestfulService>();
private HttpListener Listener { get; set; }
protected bool IsStarted = false;
private readonly AutoResetEvent _listenForNextRequest = new AutoResetEvent(false);
private readonly SmartThreadPool _threadPoolManager;
private const int IdleTimeout = 300;
private readonly ContainerAdapter _containerAdapter;
private readonly ConcurrentDictionary<string, string> _localEndPoints = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public event EventHandler<WebSocketConnectEventArgs> WebSocketConnected;
/// <summary>
/// Gets the local end points.
/// </summary>
/// <value>The local end points.</value>
public IEnumerable<string> LocalEndPoints
{
get { return _localEndPoints.Keys.ToList(); }
}
public HttpListenerHost(IApplicationHost applicationHost, ILogManager logManager, string serviceName, string handlerPath, string defaultRedirectPath, params Assembly[] assembliesWithServices)
: base(serviceName, assembliesWithServices)
{
DefaultRedirectPath = defaultRedirectPath;
HandlerPath = handlerPath;
_logger = logManager.GetLogger("HttpServer");
_containerAdapter = new ContainerAdapter(applicationHost);
_threadPoolManager = new SmartThreadPool(IdleTimeout,
maxWorkerThreads: Math.Max(16, Environment.ProcessorCount * 2));
}
public override void Configure(Container container)
{
HostConfig.Instance.DefaultRedirectPath = DefaultRedirectPath;
HostConfig.Instance.MapExceptionToStatusCode = new Dictionary<Type, int>
{
{typeof (InvalidOperationException), 422},
{typeof (ResourceNotFoundException), 404},
{typeof (FileNotFoundException), 404},
{typeof (DirectoryNotFoundException), 404}
};
HostConfig.Instance.DebugMode = true;
HostConfig.Instance.LogFactory = LogManager.LogFactory;
// The Markdown feature causes slow startup times (5 mins+) on cold boots for some users
// Custom format allows images
HostConfig.Instance.EnableFeatures = Feature.Csv | Feature.Html | Feature.Json | Feature.Jsv | Feature.Metadata | Feature.Xml | Feature.CustomFormat;
container.Adapter = _containerAdapter;
Plugins.Add(new SwaggerFeature());
Plugins.Add(new CorsFeature(allowedHeaders: "Content-Type, Authorization"));
//Plugins.Add(new AuthFeature(() => new AuthUserSession(), new IAuthProvider[] {
// new SessionAuthProvider(_containerAdapter.Resolve<ISessionContext>()),
//}));
HostContext.GlobalResponseFilters.Add(new ResponseFilter(_logger).FilterResponse);
}
public override void OnAfterInit()
{
SetAppDomainData();
base.OnAfterInit();
}
public override void OnConfigLoad()
{
base.OnConfigLoad();
Config.HandlerFactoryPath = string.IsNullOrEmpty(HandlerPath)
? null
: HandlerPath;
Config.MetadataRedirectPath = string.IsNullOrEmpty(HandlerPath)
? "metadata"
: PathUtils.CombinePaths(HandlerPath, "metadata");
}
protected override ServiceController CreateServiceController(params Assembly[] assembliesWithServices)
{
var types = _restServices.Select(r => r.GetType()).ToArray();
return new ServiceController(this, () => types);
}
public virtual void SetAppDomainData()
{
//Required for Mono to resolve VirtualPathUtility and Url.Content urls
var domain = Thread.GetDomain(); // or AppDomain.Current
domain.SetData(".appDomain", "1");
domain.SetData(".appVPath", "/");
domain.SetData(".appPath", domain.BaseDirectory);
if (string.IsNullOrEmpty(domain.GetData(".appId") as string))
{
domain.SetData(".appId", "1");
}
if (string.IsNullOrEmpty(domain.GetData(".domainId") as string))
{
domain.SetData(".domainId", "1");
}
}
public override ServiceStackHost Start(string listeningAtUrlBase)
{
StartListener(Listen);
return this;
}
/// <summary>
/// Starts the Web Service
/// </summary>
private void StartListener(WaitCallback listenCallback)
{
// *** Already running - just leave it in place
if (IsStarted)
return;
if (Listener == null)
Listener = new HttpListener();
HostContext.Config.HandlerFactoryPath = ListenerRequest.GetHandlerPathIfAny(UrlPrefixes.First());
foreach (var prefix in UrlPrefixes)
{
_logger.Info("Adding HttpListener prefix " + prefix);
Listener.Prefixes.Add(prefix);
}
IsStarted = true;
_logger.Info("Starting HttpListner");
Listener.Start();
_logger.Info("HttpListener started");
ThreadPool.QueueUserWorkItem(listenCallback);
}
private bool IsListening
{
get { return this.IsStarted && this.Listener != null && this.Listener.IsListening; }
}
// Loop here to begin processing of new requests.
private void Listen(object state)
{
while (IsListening)
{
if (Listener == null) return;
try
{
Listener.BeginGetContext(ListenerCallback, Listener);
_listenForNextRequest.WaitOne();
}
catch (Exception ex)
{
_logger.Error("Listen()", ex);
return;
}
if (Listener == null) return;
}
}
// Handle the processing of a request in here.
private void ListenerCallback(IAsyncResult asyncResult)
{
var listener = asyncResult.AsyncState as HttpListener;
HttpListenerContext context;
if (listener == null) return;
var isListening = listener.IsListening;
try
{
if (!isListening)
{
_logger.Debug("Ignoring ListenerCallback() as HttpListener is no longer listening"); return;
}
// The EndGetContext() method, as with all Begin/End asynchronous methods in the .NET Framework,
// blocks until there is a request to be processed or some type of data is available.
context = listener.EndGetContext(asyncResult);
}
catch (Exception ex)
{
// You will get an exception when httpListener.Stop() is called
// because there will be a thread stopped waiting on the .EndGetContext()
// method, and again, that is just the way most Begin/End asynchronous
// methods of the .NET Framework work.
var errMsg = ex + ": " + IsListening;
_logger.Warn(errMsg);
return;
}
finally
{
// Once we know we have a request (or exception), we signal the other thread
// so that it calls the BeginGetContext() (or possibly exits if we're not
// listening any more) method to start handling the next incoming request
// while we continue to process this request on a different thread.
_listenForNextRequest.Set();
}
_threadPoolManager.QueueWorkItem(() => InitTask(context));
}
public virtual void InitTask(HttpListenerContext context)
{
try
{
var task = this.ProcessRequestAsync(context);
task.ContinueWith(x => HandleError(x.Exception, context), TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.AttachedToParent);
if (task.Status == TaskStatus.Created)
{
task.RunSynchronously();
}
}
catch (Exception ex)
{
HandleError(ex, context);
}
}
/// <summary>
/// Logs the HTTP request.
/// </summary>
/// <param name="request">The request.</param>
private void LogHttpRequest(HttpListenerRequest request)
{
var endpoint = request.LocalEndPoint;
if (endpoint != null)
{
var address = endpoint.ToString();
_localEndPoints.GetOrAdd(address, address);
}
if (EnableHttpRequestLogging)
{
LoggerUtils.LogRequest(_logger, request);
}
}
/// <summary>
/// Processes the web socket request.
/// </summary>
/// <param name="ctx">The CTX.</param>
/// <returns>Task.</returns>
private async Task ProcessWebSocketRequest(HttpListenerContext ctx)
{
#if !__MonoCS__
try
{
var webSocketContext = await ctx.AcceptWebSocketAsync(null).ConfigureAwait(false);
if (WebSocketConnected != null)
{
WebSocketConnected(this, new WebSocketConnectEventArgs { WebSocket = new NativeWebSocket(webSocketContext.WebSocket, _logger), Endpoint = ctx.Request.RemoteEndPoint.ToString() });
}
}
catch (Exception ex)
{
_logger.ErrorException("AcceptWebSocketAsync error", ex);
ctx.Response.StatusCode = 500;
ctx.Response.Close();
}
#endif
}
private void HandleError(Exception ex, HttpListenerContext context)
{
try
{
var operationName = context.Request.GetOperationName();
var httpReq = GetRequest(context, operationName);
var httpRes = httpReq.Response;
if (httpRes.IsClosed)
{
return;
}
var errorResponse = new ErrorResponse
{
ResponseStatus = new ResponseStatus
{
ErrorCode = ex.GetType().GetOperationName(),
Message = ex.Message,
StackTrace = ex.StackTrace,
}
};
var contentType = httpReq.ResponseContentType;
var serializer = HostContext.ContentTypes.GetResponseSerializer(contentType);
if (serializer == null)
{
contentType = HostContext.Config.DefaultContentType;
serializer = HostContext.ContentTypes.GetResponseSerializer(contentType);
}
var httpError = ex as IHttpError;
if (httpError != null)
{
httpRes.StatusCode = httpError.Status;
httpRes.StatusDescription = httpError.StatusDescription;
}
else
{
httpRes.StatusCode = 500;
}
httpRes.ContentType = contentType;
serializer(httpReq, errorResponse, httpRes);
httpRes.Close();
}
catch (Exception errorEx)
{
_logger.ErrorException("Error this.ProcessRequest(context)(Exception while writing error to the response)", errorEx);
}
}
private static ListenerRequest GetRequest(HttpListenerContext httpContext, string operationName)
{
var req = new ListenerRequest(httpContext, operationName, RequestAttributes.None);
req.RequestAttributes = req.GetAttributes();
return req;
}
/// <summary>
/// Shut down the Web Service
/// </summary>
public void Stop()
{
if (Listener != null)
{
foreach (var prefix in UrlPrefixes)
{
Listener.Prefixes.Remove(prefix);
}
Listener.Close();
}
}
/// <summary>
/// Overridable method that can be used to implement a custom hnandler
/// </summary>
/// <param name="context"></param>
protected Task ProcessRequestAsync(HttpListenerContext context)
{
var request = context.Request;
LogHttpRequest(request);
if (request.IsWebSocketRequest)
{
return ProcessWebSocketRequest(context);
}
var localPath = request.Url.LocalPath;
if (string.Equals(localPath, "/" + HandlerPath + "/", StringComparison.OrdinalIgnoreCase))
{
context.Response.Redirect(DefaultRedirectPath);
context.Response.Close();
return Task.FromResult(true);
}
if (string.Equals(localPath, "/" + HandlerPath, StringComparison.OrdinalIgnoreCase))
{
context.Response.Redirect(HandlerPath + "/" + DefaultRedirectPath);
context.Response.Close();
return Task.FromResult(true);
}
if (string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase))
{
context.Response.Redirect(HandlerPath + "/" + DefaultRedirectPath);
context.Response.Close();
return Task.FromResult(true);
}
if (string.IsNullOrEmpty(localPath))
{
context.Response.Redirect("/" + HandlerPath + "/" + DefaultRedirectPath);
context.Response.Close();
return Task.FromResult(true);
}
var date = DateTime.Now;
if (string.IsNullOrEmpty(context.Request.RawUrl))
return ((object)null).AsTaskResult();
var operationName = context.Request.GetOperationName();
var httpReq = GetRequest(context, operationName);
var httpRes = httpReq.Response;
var handler = HttpHandlerFactory.GetHandler(httpReq);
var url = request.Url.ToString();
var remoteIp = httpReq.RemoteIp;
var serviceStackHandler = handler as IServiceStackHandler;
if (serviceStackHandler != null)
{
var restHandler = serviceStackHandler as RestHandler;
if (restHandler != null)
{
httpReq.OperationName = operationName = restHandler.RestPath.RequestType.GetOperationName();
}
var task = serviceStackHandler.ProcessRequestAsync(httpReq, httpRes, operationName);
task.ContinueWith(x => httpRes.Close(), TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.AttachedToParent);
//Matches Exceptions handled in HttpListenerBase.InitTask()
var statusCode = httpRes.StatusCode;
task.ContinueWith(x =>
{
var duration = DateTime.Now - date;
if (EnableHttpRequestLogging)
{
LoggerUtils.LogResponse(_logger, statusCode, url, remoteIp, duration);
}
}, TaskContinuationOptions.None);
return task;
}
return new NotImplementedException("Cannot execute handler: " + handler + " at PathInfo: " + httpReq.PathInfo)
.AsTaskException();
}
/// <summary>
/// Gets or sets a value indicating whether [enable HTTP request logging].
/// </summary>
/// <value><c>true</c> if [enable HTTP request logging]; otherwise, <c>false</c>.</value>
public bool EnableHttpRequestLogging { get; set; }
/// <summary>
/// Adds the rest handlers.
/// </summary>
/// <param name="services">The services.</param>
public void Init(IEnumerable<IRestfulService> services)
{
_restServices.AddRange(services);
ServiceController = CreateServiceController();
_logger.Info("Calling ServiceStack AppHost.Init");
base.Init();
}
//public override RouteAttribute[] GetRouteAttributes(System.Type requestType)
//{
// var routes = base.GetRouteAttributes(requestType);
// routes.Each(x => x.Path = "/api" + x.Path);
// return routes;
//}
/// <summary>
/// Releases the specified instance.
/// </summary>
/// <param name="instance">The instance.</param>
public override void Release(object instance)
{
// Leave this empty so SS doesn't try to dispose our objects
}
private bool _disposed;
private readonly object _disposeLock = new object();
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
base.Dispose();
lock (_disposeLock)
{
if (_disposed) return;
if (disposing)
{
_threadPoolManager.Dispose();
Stop();
}
//release unmanaged resources here...
_disposed = true;
}
}
public override void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public void StartServer(IEnumerable<string> urlPrefixes)
{
UrlPrefixes = urlPrefixes.ToList();
Start(UrlPrefixes.First());
}
public bool SupportsWebSockets
{
get { return NativeWebSocket.IsSupported; }
}
}
}