using System.Net.Sockets; using System.Runtime.Serialization; 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 ServerName { get; set; } private string HandlerPath { get; set; } private string DefaultRedirectPath { get; set; } private readonly ILogger _logger; public IEnumerable UrlPrefixes { get; private set; } private readonly List _restServices = new List(); private HttpListener Listener { get; set; } protected bool IsStarted = false; private readonly List _autoResetEvents = new List(); private readonly ContainerAdapter _containerAdapter; private readonly ConcurrentDictionary _localEndPoints = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); public event EventHandler WebSocketConnected; /// /// Gets the local end points. /// /// The local end points. public IEnumerable 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; ServerName = serviceName; HandlerPath = handlerPath; _logger = logManager.GetLogger("HttpServer"); _containerAdapter = new ContainerAdapter(applicationHost); for (var i = 0; i < 1; i++) { _autoResetEvents.Add(new AutoResetEvent(false)); } } public override void Configure(Container container) { HostConfig.Instance.DefaultRedirectPath = DefaultRedirectPath; HostConfig.Instance.MapExceptionToStatusCode = new Dictionary { {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")); 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(); return this; } /// /// Starts the Web Service /// private void StartListener() { // *** 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(); for (var i = 0; i < _autoResetEvents.Count; i++) { var index = i; ThreadPool.QueueUserWorkItem(o => Listen(o, index)); } } 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, int index) { while (IsListening) { if (Listener == null) return; try { Listener.BeginGetContext(c => ListenerCallback(c, index), Listener); _autoResetEvents[index].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, int index) { var listener = asyncResult.AsyncState as HttpListener; HttpListenerContext context = null; if (listener == null) return; 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. _autoResetEvents[index].Set(); } var date = DateTime.Now; Task.Factory.StartNew(async () => { try { var request = context.Request; LogHttpRequest(request, index); if (request.IsWebSocketRequest) { await ProcessWebSocketRequest(context).ConfigureAwait(false); return; } var localPath = request.Url.LocalPath; if (string.Equals(localPath, "/mediabrowser/", StringComparison.OrdinalIgnoreCase)) { context.Response.Redirect(DefaultRedirectPath); context.Response.Close(); return; } if (string.Equals(localPath, "/mediabrowser", StringComparison.OrdinalIgnoreCase)) { context.Response.Redirect("mediabrowser/" + DefaultRedirectPath); context.Response.Close(); return; } if (string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)) { context.Response.Redirect("mediabrowser/" + DefaultRedirectPath); context.Response.Close(); return; } if (string.IsNullOrEmpty(localPath)) { context.Response.Redirect("/mediabrowser/" + DefaultRedirectPath); context.Response.Close(); return; } var url = request.Url.ToString(); var endPoint = request.RemoteEndPoint; await ProcessRequestAsync(context).ConfigureAwait(false); var duration = DateTime.Now - date; if (EnableHttpRequestLogging) { LoggerUtils.LogResponse(_logger, context.Response, url, endPoint, duration); } } catch (Exception ex) { _logger.ErrorException("ProcessRequest failure", ex); HandleError(ex, context, _logger); } }); } /// /// Logs the HTTP request. /// /// The request. /// The index. private void LogHttpRequest(HttpListenerRequest request, int index) { var endpoint = request.LocalEndPoint; if (endpoint != null) { var address = endpoint.ToString(); _localEndPoints.GetOrAdd(address, address); } if (EnableHttpRequestLogging) { LoggerUtils.LogRequest(_logger, request, index); } } /// /// Processes the web socket request. /// /// The CTX. /// Task. 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 } public static void HandleError(Exception ex, HttpListenerContext context, ILogger logger) { try { var errorResponse = new ErrorResponse { ResponseStatus = new ResponseStatus { ErrorCode = ex.GetType().GetOperationName(), Message = ex.Message, StackTrace = ex.StackTrace, } }; var operationName = context.Request.GetOperationName(); var httpReq = GetRequest(context, operationName); var httpRes = httpReq.Response; 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; } /// /// Shut down the Web Service /// public void Stop() { if (Listener != null) { foreach (var prefix in UrlPrefixes) { Listener.Prefixes.Remove(prefix); } Listener.Close(); } } /// /// Overridable method that can be used to implement a custom hnandler /// /// protected Task ProcessRequestAsync(HttpListenerContext context) { 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 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()); return task; } return new NotImplementedException("Cannot execute handler: " + handler + " at PathInfo: " + httpReq.PathInfo) .AsTaskException(); } /// /// Gets or sets a value indicating whether [enable HTTP request logging]. /// /// true if [enable HTTP request logging]; otherwise, false. public bool EnableHttpRequestLogging { get; set; } /// /// Adds the rest handlers. /// /// The services. public void Init(IEnumerable services) { _restServices.AddRange(services); ServiceController = CreateServiceController(); _logger.Info("Calling ServiceStack AppHost.Init"); base.Init(); } /// /// Releases the specified instance. /// /// The instance. 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) { Stop(); } //release unmanaged resources here... _disposed = true; } } public override void Dispose() { Dispose(true); GC.SuppressFinalize(this); } public void StartServer(IEnumerable urlPrefixes) { UrlPrefixes = urlPrefixes.ToList(); Start(UrlPrefixes.First()); } public bool SupportsWebSockets { get { return NativeWebSocket.IsSupported; } } } }