diff options
Diffstat (limited to 'MediaBrowser.Server.Implementations/HttpServer')
13 files changed, 2253 insertions, 247 deletions
diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs index 340149182..62a43751d 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -5,6 +5,8 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Logging; +using MediaBrowser.Server.Implementations.HttpServer.NetListener; +using MediaBrowser.Server.Implementations.HttpServer.SocketSharp; using ServiceStack; using ServiceStack.Api.Swagger; using ServiceStack.Host; @@ -17,7 +19,6 @@ 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; @@ -34,17 +35,13 @@ namespace MediaBrowser.Server.Implementations.HttpServer private readonly List<IRestfulService> _restServices = new List<IRestfulService>(); - private HttpListener Listener { get; set; } - protected bool IsStarted = false; + private IHttpListener _listener; - private readonly AutoResetEvent _listenForNextRequest = new AutoResetEvent(false); private readonly SmartThreadPool _threadPoolManager; + private const int IdleTimeout = 300; - 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> @@ -53,7 +50,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer /// <value>The local end points.</value> public IEnumerable<string> LocalEndPoints { - get { return _localEndPoints.Keys.ToList(); } + get { return _listener == null ? new List<string>() : _listener.LocalEndPoints; } } public HttpListenerHost(IApplicationHost applicationHost, ILogManager logManager, string serviceName, string handlerPath, string defaultRedirectPath, params Assembly[] assembliesWithServices) @@ -148,175 +145,40 @@ namespace MediaBrowser.Server.Implementations.HttpServer public override ServiceStackHost Start(string listeningAtUrlBase) { - StartListener(Listen); + StartListener(); return this; } /// <summary> /// Starts the Web Service /// </summary> - private void StartListener(WaitCallback listenCallback) + 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(); - _logger.Info("HttpListener started"); + _listener = NativeWebSocket.IsSupported + ? _listener = new HttpListenerServer(_logger, _threadPoolManager) + : _listener = new WebSocketSharpListener(_logger, _threadPoolManager); - ThreadPool.QueueUserWorkItem(listenCallback); - } + _listener.WebSocketHandler = WebSocketHandler; + _listener.ErrorHandler = ErrorHandler; + _listener.RequestHandler = RequestHandler; - private bool IsListening - { - get { return this.IsStarted && this.Listener != null && this.Listener.IsListening; } + _listener.Start(UrlPrefixes); } - // Loop here to begin processing of new requests. - private void Listen(object state) + private void WebSocketHandler(WebSocketConnectEventArgs args) { - while (IsListening) + if (WebSocketConnected != null) { - if (Listener == null) return; - - try - { - Listener.BeginGetContext(ListenerCallback, Listener); - _listenForNextRequest.WaitOne(); - } - catch (Exception ex) - { - _logger.Error("Listen()", ex); - return; - } - if (Listener == null) return; + WebSocketConnected(this, args); } } - // Handle the processing of a request in here. - private void ListenerCallback(IAsyncResult asyncResult) + private void ErrorHandler(Exception ex, IRequest httpReq) { - 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) @@ -366,84 +228,55 @@ namespace MediaBrowser.Server.Implementations.HttpServer } } - 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) + if (_listener != null) { - foreach (var prefix in UrlPrefixes) - { - Listener.Prefixes.Remove(prefix); - } - - Listener.Close(); + _listener.Stop(); } } /// <summary> /// Overridable method that can be used to implement a custom hnandler /// </summary> - /// <param name="context"></param> - protected Task ProcessRequestAsync(HttpListenerContext context) + /// <param name="httpReq">The HTTP req.</param> + /// <param name="url">The URL.</param> + /// <returns>Task.</returns> + protected Task RequestHandler(IHttpRequest httpReq, Uri url) { - var request = context.Request; - - LogHttpRequest(request); + var date = DateTime.Now; - if (request.IsWebSocketRequest) - { - return ProcessWebSocketRequest(context); - } + var httpRes = httpReq.Response; - var localPath = request.Url.LocalPath; + var operationName = httpReq.OperationName; + var localPath = url.LocalPath; if (string.Equals(localPath, "/" + HandlerPath + "/", StringComparison.OrdinalIgnoreCase)) { - context.Response.Redirect(DefaultRedirectPath); - context.Response.Close(); + httpRes.RedirectToUrl(DefaultRedirectPath); return Task.FromResult(true); } if (string.Equals(localPath, "/" + HandlerPath, StringComparison.OrdinalIgnoreCase)) { - context.Response.Redirect(HandlerPath + "/" + DefaultRedirectPath); - context.Response.Close(); + httpRes.RedirectToUrl(HandlerPath + "/" + DefaultRedirectPath); return Task.FromResult(true); } if (string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)) { - context.Response.Redirect(HandlerPath + "/" + DefaultRedirectPath); - context.Response.Close(); + httpRes.RedirectToUrl(HandlerPath + "/" + DefaultRedirectPath); return Task.FromResult(true); } if (string.IsNullOrEmpty(localPath)) { - context.Response.Redirect("/" + HandlerPath + "/" + DefaultRedirectPath); - context.Response.Close(); + httpRes.RedirectToUrl("/" + HandlerPath + "/" + DefaultRedirectPath); 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; @@ -460,19 +293,17 @@ namespace MediaBrowser.Server.Implementations.HttpServer task.ContinueWith(x => httpRes.Close(), TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.AttachedToParent); //Matches Exceptions handled in HttpListenerBase.InitTask() - var statusCode = httpRes.StatusCode; + var urlString = url.ToString(); task.ContinueWith(x => { + var statusCode = httpRes.StatusCode; + var duration = DateTime.Now - date; - if (EnableHttpRequestLogging) - { - LoggerUtils.LogResponse(_logger, statusCode, url, remoteIp, duration); - } + LoggerUtils.LogResponse(_logger, statusCode, urlString, remoteIp, duration); }, TaskContinuationOptions.None); - return task; } @@ -481,12 +312,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer } /// <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> @@ -530,8 +355,8 @@ namespace MediaBrowser.Server.Implementations.HttpServer if (disposing) { - _threadPoolManager.Dispose(); - + _threadPoolManager.Dispose(); + Stop(); } @@ -551,10 +376,5 @@ namespace MediaBrowser.Server.Implementations.HttpServer UrlPrefixes = urlPrefixes.ToList(); Start(UrlPrefixes.First()); } - - public bool SupportsWebSockets - { - get { return NativeWebSocket.IsSupported; } - } } }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs index 8831d635c..6a60e5ea6 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -306,11 +306,15 @@ namespace MediaBrowser.Server.Implementations.HttpServer throw new ArgumentNullException("path"); } - return GetStaticFileResult(requestContext, path, MimeTypes.GetMimeType(path), fileShare, responseHeaders, isHeadRequest); + return GetStaticFileResult(requestContext, path, MimeTypes.GetMimeType(path), null, fileShare, responseHeaders, isHeadRequest); } - public object GetStaticFileResult(IRequest requestContext, string path, string contentType, - FileShare fileShare = FileShare.Read, IDictionary<string, string> responseHeaders = null, + public object GetStaticFileResult(IRequest requestContext, + string path, + string contentType, + TimeSpan? cacheCuration = null, + FileShare fileShare = FileShare.Read, + IDictionary<string, string> responseHeaders = null, bool isHeadRequest = false) { if (string.IsNullOrEmpty(path)) @@ -327,7 +331,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer var cacheKey = path + dateModified.Ticks; - return GetStaticResult(requestContext, cacheKey.GetMD5(), dateModified, null, contentType, () => Task.FromResult(GetFileStream(path, fileShare)), responseHeaders, isHeadRequest); + return GetStaticResult(requestContext, cacheKey.GetMD5(), dateModified, cacheCuration, contentType, () => Task.FromResult(GetFileStream(path, fileShare)), responseHeaders, isHeadRequest); } /// <summary> diff --git a/MediaBrowser.Server.Implementations/HttpServer/IHttpListener.cs b/MediaBrowser.Server.Implementations/HttpServer/IHttpListener.cs new file mode 100644 index 000000000..1d80a263c --- /dev/null +++ b/MediaBrowser.Server.Implementations/HttpServer/IHttpListener.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using ServiceStack.Web; +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Server.Implementations.HttpServer +{ + public interface IHttpListener : IDisposable + { + IEnumerable<string> LocalEndPoints { get; } + + /// <summary> + /// Gets or sets the error handler. + /// </summary> + /// <value>The error handler.</value> + Action<Exception, IRequest> ErrorHandler { get; set; } + + /// <summary> + /// Gets or sets the request handler. + /// </summary> + /// <value>The request handler.</value> + Func<IHttpRequest, Uri, Task> RequestHandler { get; set; } + + /// <summary> + /// Gets or sets the web socket handler. + /// </summary> + /// <value>The web socket handler.</value> + Action<WebSocketConnectEventArgs> WebSocketHandler { get; set; } + + /// <summary> + /// Starts this instance. + /// </summary> + /// <param name="urlPrefixes">The URL prefixes.</param> + void Start(IEnumerable<string> urlPrefixes); + + /// <summary> + /// Stops this instance. + /// </summary> + void Stop(); + } +} diff --git a/MediaBrowser.Server.Implementations/HttpServer/LoggerUtils.cs b/MediaBrowser.Server.Implementations/HttpServer/LoggerUtils.cs index 19d2f9c45..955c4ed2d 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/LoggerUtils.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/LoggerUtils.cs @@ -8,24 +8,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer public static class LoggerUtils { /// <summary> - /// Logs the request. - /// </summary> - /// <param name="logger">The logger.</param> - /// <param name="request">The request.</param> - public static void LogRequest(ILogger logger, HttpListenerRequest request) - { - var log = new StringBuilder(); - - //var headers = string.Join(",", request.Headers.AllKeys.Where(i => !string.Equals(i, "cookie", StringComparison.OrdinalIgnoreCase) && !string.Equals(i, "Referer", StringComparison.OrdinalIgnoreCase)).Select(k => k + "=" + request.Headers[k])); - - //log.AppendLine("Ip: " + request.RemoteEndPoint + ". Headers: " + headers); - - var type = request.IsWebSocketRequest ? "Web Socket" : "HTTP " + request.HttpMethod; - - logger.LogMultiline(type + " " + request.Url, LogSeverity.Debug, log); - } - - /// <summary> /// Logs the response. /// </summary> /// <param name="logger">The logger.</param> diff --git a/MediaBrowser.Server.Implementations/HttpServer/NetListener/HttpListenerServer.cs b/MediaBrowser.Server.Implementations/HttpServer/NetListener/HttpListenerServer.cs new file mode 100644 index 000000000..7f766129e --- /dev/null +++ b/MediaBrowser.Server.Implementations/HttpServer/NetListener/HttpListenerServer.cs @@ -0,0 +1,300 @@ +using System.Text; +using Amib.Threading; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Logging; +using ServiceStack; +using ServiceStack.Host.HttpListener; +using ServiceStack.Web; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.HttpServer.NetListener +{ + public class HttpListenerServer : IHttpListener + { + private readonly ILogger _logger; + private HttpListener _listener; + private readonly AutoResetEvent _listenForNextRequest = new AutoResetEvent(false); + private readonly SmartThreadPool _threadPoolManager; + + public System.Action<Exception, IRequest> ErrorHandler { get; set; } + public Action<WebSocketConnectEventArgs> WebSocketHandler { get; set; } + public System.Func<IHttpRequest, Uri, Task> RequestHandler { get; set; } + + private readonly ConcurrentDictionary<string, string> _localEndPoints = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase); + + public HttpListenerServer(ILogger logger, SmartThreadPool threadPoolManager) + { + _logger = logger; + + _threadPoolManager = threadPoolManager; + } + + /// <summary> + /// Gets the local end points. + /// </summary> + /// <value>The local end points.</value> + public IEnumerable<string> LocalEndPoints + { + get { return _localEndPoints.Keys.ToList(); } + } + + private List<string> UrlPrefixes { get; set; } + + public void Start(IEnumerable<string> urlPrefixes) + { + UrlPrefixes = urlPrefixes.ToList(); + + if (_listener == null) + _listener = new System.Net.HttpListener(); + + //HostContext.Config.HandlerFactoryPath = ListenerRequest.GetHandlerPathIfAny(UrlPrefixes.First()); + + foreach (var prefix in UrlPrefixes) + { + _logger.Info("Adding HttpListener prefix " + prefix); + _listener.Prefixes.Add(prefix); + } + + _listener.Start(); + + ThreadPool.QueueUserWorkItem(Listen); + } + + private bool IsListening + { + get { return _listener != null && _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)); + } + + private 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); + } + } + + private Task ProcessRequestAsync(HttpListenerContext context) + { + var request = context.Request; + + LogHttpRequest(request); + + if (request.IsWebSocketRequest) + { + return ProcessWebSocketRequest(context); + } + + if (string.IsNullOrEmpty(context.Request.RawUrl)) + return ((object)null).AsTaskResult(); + + var operationName = context.Request.GetOperationName(); + + var httpReq = GetRequest(context, operationName); + + return RequestHandler(httpReq, request.Url); + } + + /// <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 (WebSocketHandler != null) + { + WebSocketHandler(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) + { + var operationName = context.Request.GetOperationName(); + var httpReq = GetRequest(context, operationName); + + if (ErrorHandler != null) + { + ErrorHandler(ex, httpReq); + } + } + + private static ListenerRequest GetRequest(HttpListenerContext httpContext, string operationName) + { + var req = new ListenerRequest(httpContext, operationName, RequestAttributes.None); + req.RequestAttributes = req.GetAttributes(); + + return req; + } + + /// <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); + } + + LogRequest(_logger, request); + } + + /// <summary> + /// Logs the request. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="request">The request.</param> + private static void LogRequest(ILogger logger, HttpListenerRequest request) + { + var log = new StringBuilder(); + + //var headers = string.Join(",", request.Headers.AllKeys.Where(i => !string.Equals(i, "cookie", StringComparison.OrdinalIgnoreCase) && !string.Equals(i, "Referer", StringComparison.OrdinalIgnoreCase)).Select(k => k + "=" + request.Headers[k])); + + //log.AppendLine("Ip: " + request.RemoteEndPoint + ". Headers: " + headers); + + var type = request.IsWebSocketRequest ? "Web Socket" : "HTTP " + request.HttpMethod; + + logger.LogMultiline(type + " " + request.Url, LogSeverity.Debug, log); + } + + public void Stop() + { + if (_listener != null) + { + foreach (var prefix in UrlPrefixes) + { + _listener.Prefixes.Remove(prefix); + } + + _listener.Close(); + } + } + + public void Dispose() + { + Dispose(true); + } + + private bool _disposed; + private readonly object _disposeLock = new object(); + protected virtual void Dispose(bool disposing) + { + if (_disposed) return; + + lock (_disposeLock) + { + if (_disposed) return; + + if (disposing) + { + _threadPoolManager.Dispose(); + + Stop(); + } + + //release unmanaged resources here... + _disposed = true; + } + } + } +} diff --git a/MediaBrowser.Server.Implementations/HttpServer/ResponseFilter.cs b/MediaBrowser.Server.Implementations/HttpServer/ResponseFilter.cs index ac1621709..e0a5764d5 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/ResponseFilter.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/ResponseFilter.cs @@ -1,4 +1,5 @@ using MediaBrowser.Model.Logging; +using MediaBrowser.Server.Implementations.HttpServer.SocketSharp; using ServiceStack; using ServiceStack.Web; using System; @@ -66,13 +67,23 @@ namespace MediaBrowser.Server.Implementations.HttpServer if (length > 0) { - var response = (HttpListenerResponse)res.OriginalResponse; - - response.ContentLength64 = length; - - // Disable chunked encoding. Technically this is only needed when using Content-Range, but - // anytime we know the content length there's no need for it - response.SendChunked = false; + res.SetContentLength(length); + + var listenerResponse = res.OriginalResponse as HttpListenerResponse; + + if (listenerResponse != null) + { + // Disable chunked encoding. Technically this is only needed when using Content-Range, but + // anytime we know the content length there's no need for it + listenerResponse.SendChunked = false; + return; + } + + var sharpResponse = res as WebSocketSharpResponse; + if (sharpResponse != null) + { + sharpResponse.SendChunked = false; + } } } } diff --git a/MediaBrowser.Server.Implementations/HttpServer/Security/AuthService.cs b/MediaBrowser.Server.Implementations/HttpServer/Security/AuthService.cs index 2d7c798ad..1933cc716 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/Security/AuthService.cs @@ -66,6 +66,12 @@ namespace MediaBrowser.Server.Implementations.HttpServer.Security ? null : UserManager.GetUserById(new Guid(auth.UserId)); + if (user == null & !string.IsNullOrWhiteSpace(auth.UserId)) + { + // TODO: Re-enable + //throw new ArgumentException("User with Id " + auth.UserId + " not found"); + } + if (user != null && user.Configuration.IsDisabled) { throw new UnauthorizedAccessException("User account has been disabled."); diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/Extensions.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/Extensions.cs new file mode 100644 index 000000000..63d57b6be --- /dev/null +++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/Extensions.cs @@ -0,0 +1,28 @@ +using System; +using MediaBrowser.Model.Logging; +using WebSocketSharp.Net; + +namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp +{ + public static class Extensions + { + public static string GetOperationName(this HttpListenerRequest request) + { + return request.Url.Segments[request.Url.Segments.Length - 1]; + } + + public static void CloseOutputStream(this HttpListenerResponse response, ILogger logger) + { + try + { + response.OutputStream.Flush(); + response.OutputStream.Close(); + response.Close(); + } + catch (Exception ex) + { + logger.ErrorException("Error in HttpListenerResponseWrapper: " + ex.Message, ex); + } + } + } +} diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs new file mode 100644 index 000000000..226d97b3c --- /dev/null +++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs @@ -0,0 +1,918 @@ +using System; +using System.Collections.Specialized; +using System.Globalization; +using System.IO; +using System.Text; +using System.Web; +using ServiceStack.Web; + +namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp +{ + public partial class WebSocketSharpRequest : IHttpRequest + { + static internal string GetParameter(string header, string attr) + { + int ap = header.IndexOf(attr); + if (ap == -1) + return null; + + ap += attr.Length; + if (ap >= header.Length) + return null; + + char ending = header[ap]; + if (ending != '"') + ending = ' '; + + int end = header.IndexOf(ending, ap + 1); + if (end == -1) + return (ending == '"') ? null : header.Substring(ap); + + return header.Substring(ap + 1, end - ap - 1); + } + + void LoadMultiPart() + { + string boundary = GetParameter(ContentType, "; boundary="); + if (boundary == null) + return; + + var input = GetSubStream(InputStream); + + //DB: 30/01/11 - Hack to get around non-seekable stream and received HTTP request + //Not ending with \r\n? + var ms = new MemoryStream(32 * 1024); + input.CopyTo(ms); + input = ms; + ms.WriteByte((byte)'\r'); + ms.WriteByte((byte)'\n'); + + input.Position = 0; + + //Uncomment to debug + //var content = new StreamReader(ms).ReadToEnd(); + //Console.WriteLine(boundary + "::" + content); + //input.Position = 0; + + var multi_part = new HttpMultipart(input, boundary, ContentEncoding); + + HttpMultipart.Element e; + while ((e = multi_part.ReadNextElement()) != null) + { + if (e.Filename == null) + { + byte[] copy = new byte[e.Length]; + + input.Position = e.Start; + input.Read(copy, 0, (int)e.Length); + + form.Add(e.Name, (e.Encoding ?? ContentEncoding).GetString(copy)); + } + else + { + // + // We use a substream, as in 2.x we will support large uploads streamed to disk, + // + HttpPostedFile sub = new HttpPostedFile(e.Filename, e.ContentType, input, e.Start, e.Length); + files.AddFile(e.Name, sub); + } + } + EndSubStream(input); + } + + public NameValueCollection Form + { + get + { + if (form == null) + { + form = new WebROCollection(); + files = new HttpFileCollection(); + + if (IsContentType("multipart/form-data", true)) + LoadMultiPart(); + else if ( + IsContentType("application/x-www-form-urlencoded", true)) + LoadWwwForm(); + + form.Protect(); + } + +#if NET_4_0 + if (validateRequestNewMode && !checked_form) { + // Setting this before calling the validator prevents + // possible endless recursion + checked_form = true; + ValidateNameValueCollection ("Form", query_string_nvc, RequestValidationSource.Form); + } else +#endif + if (validate_form && !checked_form) + { + checked_form = true; + ValidateNameValueCollection("Form", form); + } + + return form; + } + } + + + protected bool validate_cookies, validate_query_string, validate_form; + protected bool checked_cookies, checked_query_string, checked_form; + + static void ThrowValidationException(string name, string key, string value) + { + string v = "\"" + value + "\""; + if (v.Length > 20) + v = v.Substring(0, 16) + "...\""; + + string msg = String.Format("A potentially dangerous Request.{0} value was " + + "detected from the client ({1}={2}).", name, key, v); + + throw new HttpRequestValidationException(msg); + } + + static void ValidateNameValueCollection(string name, NameValueCollection coll) + { + if (coll == null) + return; + + foreach (string key in coll.Keys) + { + string val = coll[key]; + if (val != null && val.Length > 0 && IsInvalidString(val)) + ThrowValidationException(name, key, val); + } + } + + internal static bool IsInvalidString(string val) + { + int validationFailureIndex; + + return IsInvalidString(val, out validationFailureIndex); + } + + internal static bool IsInvalidString(string val, out int validationFailureIndex) + { + validationFailureIndex = 0; + + int len = val.Length; + if (len < 2) + return false; + + char current = val[0]; + for (int idx = 1; idx < len; idx++) + { + char next = val[idx]; + // See http://secunia.com/advisories/14325 + if (current == '<' || current == '\xff1c') + { + if (next == '!' || next < ' ' + || (next >= 'a' && next <= 'z') + || (next >= 'A' && next <= 'Z')) + { + validationFailureIndex = idx - 1; + return true; + } + } + else if (current == '&' && next == '#') + { + validationFailureIndex = idx - 1; + return true; + } + + current = next; + } + + return false; + } + + public void ValidateInput() + { + validate_cookies = true; + validate_query_string = true; + validate_form = true; + } + + bool IsContentType(string ct, bool starts_with) + { + if (ct == null || ContentType == null) return false; + + if (starts_with) + return StrUtils.StartsWith(ContentType, ct, true); + + return String.Compare(ContentType, ct, true, Helpers.InvariantCulture) == 0; + } + + + + + + void LoadWwwForm() + { + using (Stream input = GetSubStream(InputStream)) + { + using (StreamReader s = new StreamReader(input, ContentEncoding)) + { + StringBuilder key = new StringBuilder(); + StringBuilder value = new StringBuilder(); + int c; + + while ((c = s.Read()) != -1) + { + if (c == '=') + { + value.Length = 0; + while ((c = s.Read()) != -1) + { + if (c == '&') + { + AddRawKeyValue(key, value); + break; + } + else + value.Append((char)c); + } + if (c == -1) + { + AddRawKeyValue(key, value); + return; + } + } + else if (c == '&') + AddRawKeyValue(key, value); + else + key.Append((char)c); + } + if (c == -1) + AddRawKeyValue(key, value); + + EndSubStream(input); + } + } + } + + void AddRawKeyValue(StringBuilder key, StringBuilder value) + { + string decodedKey = HttpUtility.UrlDecode(key.ToString(), ContentEncoding); + form.Add(decodedKey, + HttpUtility.UrlDecode(value.ToString(), ContentEncoding)); + + key.Length = 0; + value.Length = 0; + } + + WebROCollection form; + + HttpFileCollection files; + + public sealed class HttpFileCollection : NameObjectCollectionBase + { + internal HttpFileCollection() + { + } + + internal void AddFile(string name, HttpPostedFile file) + { + BaseAdd(name, file); + } + + public void CopyTo(Array dest, int index) + { + /* XXX this is kind of gross and inefficient + * since it makes a copy of the superclass's + * list */ + object[] values = BaseGetAllValues(); + values.CopyTo(dest, index); + } + + public string GetKey(int index) + { + return BaseGetKey(index); + } + + public HttpPostedFile Get(int index) + { + return (HttpPostedFile)BaseGet(index); + } + + public HttpPostedFile Get(string key) + { + return (HttpPostedFile)BaseGet(key); + } + + public HttpPostedFile this[string key] + { + get + { + return Get(key); + } + } + + public HttpPostedFile this[int index] + { + get + { + return Get(index); + } + } + + public string[] AllKeys + { + get + { + return BaseGetAllKeys(); + } + } + } + class WebROCollection : NameValueCollection + { + bool got_id; + int id; + + public bool GotID + { + get { return got_id; } + } + + public int ID + { + get { return id; } + set + { + got_id = true; + id = value; + } + } + public void Protect() + { + IsReadOnly = true; + } + + public void Unprotect() + { + IsReadOnly = false; + } + + public override string ToString() + { + StringBuilder result = new StringBuilder(); + foreach (string key in AllKeys) + { + if (result.Length > 0) + result.Append('&'); + + if (key != null && key.Length > 0) + { + result.Append(key); + result.Append('='); + } + result.Append(Get(key)); + } + + return result.ToString(); + } + } + + public sealed class HttpPostedFile + { + string name; + string content_type; + Stream stream; + + class ReadSubStream : Stream + { + Stream s; + long offset; + long end; + long position; + + public ReadSubStream(Stream s, long offset, long length) + { + this.s = s; + this.offset = offset; + this.end = offset + length; + position = offset; + } + + public override void Flush() + { + } + + public override int Read(byte[] buffer, int dest_offset, int count) + { + if (buffer == null) + throw new ArgumentNullException("buffer"); + + if (dest_offset < 0) + throw new ArgumentOutOfRangeException("dest_offset", "< 0"); + + if (count < 0) + throw new ArgumentOutOfRangeException("count", "< 0"); + + int len = buffer.Length; + if (dest_offset > len) + throw new ArgumentException("destination offset is beyond array size"); + // reordered to avoid possible integer overflow + if (dest_offset > len - count) + throw new ArgumentException("Reading would overrun buffer"); + + if (count > end - position) + count = (int)(end - position); + + if (count <= 0) + return 0; + + s.Position = position; + int result = s.Read(buffer, dest_offset, count); + if (result > 0) + position += result; + else + position = end; + + return result; + } + + public override int ReadByte() + { + if (position >= end) + return -1; + + s.Position = position; + int result = s.ReadByte(); + if (result < 0) + position = end; + else + position++; + + return result; + } + + public override long Seek(long d, SeekOrigin origin) + { + long real; + switch (origin) + { + case SeekOrigin.Begin: + real = offset + d; + break; + case SeekOrigin.End: + real = end + d; + break; + case SeekOrigin.Current: + real = position + d; + break; + default: + throw new ArgumentException(); + } + + long virt = real - offset; + if (virt < 0 || virt > Length) + throw new ArgumentException(); + + position = s.Seek(real, SeekOrigin.Begin); + return position; + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override bool CanRead + { + get { return true; } + } + public override bool CanSeek + { + get { return true; } + } + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { return end - offset; } + } + + public override long Position + { + get + { + return position - offset; + } + set + { + if (value > Length) + throw new ArgumentOutOfRangeException(); + + position = Seek(value, SeekOrigin.Begin); + } + } + } + + internal HttpPostedFile(string name, string content_type, Stream base_stream, long offset, long length) + { + this.name = name; + this.content_type = content_type; + this.stream = new ReadSubStream(base_stream, offset, length); + } + + public string ContentType + { + get + { + return (content_type); + } + } + + public int ContentLength + { + get + { + return (int)stream.Length; + } + } + + public string FileName + { + get + { + return (name); + } + } + + public Stream InputStream + { + get + { + return (stream); + } + } + + public void SaveAs(string filename) + { + byte[] buffer = new byte[16 * 1024]; + long old_post = stream.Position; + + try + { + File.Delete(filename); + using (FileStream fs = File.Create(filename)) + { + stream.Position = 0; + int n; + + while ((n = stream.Read(buffer, 0, 16 * 1024)) != 0) + { + fs.Write(buffer, 0, n); + } + } + } + finally + { + stream.Position = old_post; + } + } + } + + class Helpers + { + public static readonly CultureInfo InvariantCulture = CultureInfo.InvariantCulture; + } + + internal sealed class StrUtils + { + StrUtils() { } + + public static bool StartsWith(string str1, string str2) + { + return StartsWith(str1, str2, false); + } + + public static bool StartsWith(string str1, string str2, bool ignore_case) + { + int l2 = str2.Length; + if (l2 == 0) + return true; + + int l1 = str1.Length; + if (l2 > l1) + return false; + + return (0 == String.Compare(str1, 0, str2, 0, l2, ignore_case, Helpers.InvariantCulture)); + } + + public static bool EndsWith(string str1, string str2) + { + return EndsWith(str1, str2, false); + } + + public static bool EndsWith(string str1, string str2, bool ignore_case) + { + int l2 = str2.Length; + if (l2 == 0) + return true; + + int l1 = str1.Length; + if (l2 > l1) + return false; + + return (0 == String.Compare(str1, l1 - l2, str2, 0, l2, ignore_case, Helpers.InvariantCulture)); + } + } + + class HttpMultipart + { + + public class Element + { + public string ContentType; + public string Name; + public string Filename; + public Encoding Encoding; + public long Start; + public long Length; + + public override string ToString() + { + return "ContentType " + ContentType + ", Name " + Name + ", Filename " + Filename + ", Start " + + Start.ToString() + ", Length " + Length.ToString(); + } + } + + Stream data; + string boundary; + byte[] boundary_bytes; + byte[] buffer; + bool at_eof; + Encoding encoding; + StringBuilder sb; + + const byte HYPHEN = (byte)'-', LF = (byte)'\n', CR = (byte)'\r'; + + // See RFC 2046 + // In the case of multipart entities, in which one or more different + // sets of data are combined in a single body, a "multipart" media type + // field must appear in the entity's header. The body must then contain + // one or more body parts, each preceded by a boundary delimiter line, + // and the last one followed by a closing boundary delimiter line. + // After its boundary delimiter line, each body part then consists of a + // header area, a blank line, and a body area. Thus a body part is + // similar to an RFC 822 message in syntax, but different in meaning. + + public HttpMultipart(Stream data, string b, Encoding encoding) + { + this.data = data; + //DB: 30/01/11: cannot set or read the Position in HttpListener in Win.NET + //var ms = new MemoryStream(32 * 1024); + //data.CopyTo(ms); + //this.data = ms; + + boundary = b; + boundary_bytes = encoding.GetBytes(b); + buffer = new byte[boundary_bytes.Length + 2]; // CRLF or '--' + this.encoding = encoding; + sb = new StringBuilder(); + } + + string ReadLine() + { + // CRLF or LF are ok as line endings. + bool got_cr = false; + int b = 0; + sb.Length = 0; + while (true) + { + b = data.ReadByte(); + if (b == -1) + { + return null; + } + + if (b == LF) + { + break; + } + got_cr = (b == CR); + sb.Append((char)b); + } + + if (got_cr) + sb.Length--; + + return sb.ToString(); + + } + + static string GetContentDispositionAttribute(string l, string name) + { + int idx = l.IndexOf(name + "=\""); + if (idx < 0) + return null; + int begin = idx + name.Length + "=\"".Length; + int end = l.IndexOf('"', begin); + if (end < 0) + return null; + if (begin == end) + return ""; + return l.Substring(begin, end - begin); + } + + string GetContentDispositionAttributeWithEncoding(string l, string name) + { + int idx = l.IndexOf(name + "=\""); + if (idx < 0) + return null; + int begin = idx + name.Length + "=\"".Length; + int end = l.IndexOf('"', begin); + if (end < 0) + return null; + if (begin == end) + return ""; + + string temp = l.Substring(begin, end - begin); + byte[] source = new byte[temp.Length]; + for (int i = temp.Length - 1; i >= 0; i--) + source[i] = (byte)temp[i]; + + return encoding.GetString(source); + } + + bool ReadBoundary() + { + try + { + string line = ReadLine(); + while (line == "") + line = ReadLine(); + if (line[0] != '-' || line[1] != '-') + return false; + + if (!StrUtils.EndsWith(line, boundary, false)) + return true; + } + catch + { + } + + return false; + } + + string ReadHeaders() + { + string s = ReadLine(); + if (s == "") + return null; + + return s; + } + + bool CompareBytes(byte[] orig, byte[] other) + { + for (int i = orig.Length - 1; i >= 0; i--) + if (orig[i] != other[i]) + return false; + + return true; + } + + long MoveToNextBoundary() + { + long retval = 0; + bool got_cr = false; + + int state = 0; + int c = data.ReadByte(); + while (true) + { + if (c == -1) + return -1; + + if (state == 0 && c == LF) + { + retval = data.Position - 1; + if (got_cr) + retval--; + state = 1; + c = data.ReadByte(); + } + else if (state == 0) + { + got_cr = (c == CR); + c = data.ReadByte(); + } + else if (state == 1 && c == '-') + { + c = data.ReadByte(); + if (c == -1) + return -1; + + if (c != '-') + { + state = 0; + got_cr = false; + continue; // no ReadByte() here + } + + int nread = data.Read(buffer, 0, buffer.Length); + int bl = buffer.Length; + if (nread != bl) + return -1; + + if (!CompareBytes(boundary_bytes, buffer)) + { + state = 0; + data.Position = retval + 2; + if (got_cr) + { + data.Position++; + got_cr = false; + } + c = data.ReadByte(); + continue; + } + + if (buffer[bl - 2] == '-' && buffer[bl - 1] == '-') + { + at_eof = true; + } + else if (buffer[bl - 2] != CR || buffer[bl - 1] != LF) + { + state = 0; + data.Position = retval + 2; + if (got_cr) + { + data.Position++; + got_cr = false; + } + c = data.ReadByte(); + continue; + } + data.Position = retval + 2; + if (got_cr) + data.Position++; + break; + } + else + { + // state == 1 + state = 0; // no ReadByte() here + } + } + + return retval; + } + + public Element ReadNextElement() + { + if (at_eof || ReadBoundary()) + return null; + + Element elem = new Element(); + string header; + while ((header = ReadHeaders()) != null) + { + if (StrUtils.StartsWith(header, "Content-Disposition:", true)) + { + elem.Name = GetContentDispositionAttribute(header, "name"); + elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename")); + } + else if (StrUtils.StartsWith(header, "Content-Type:", true)) + { + elem.ContentType = header.Substring("Content-Type:".Length).Trim(); + elem.Encoding = GetEncoding(elem.ContentType); + } + } + + long start = 0; + start = data.Position; + elem.Start = start; + long pos = MoveToNextBoundary(); + if (pos == -1) + return null; + + elem.Length = pos - start; + return elem; + } + + static string StripPath(string path) + { + if (path == null || path.Length == 0) + return path; + + if (path.IndexOf(":\\") != 1 && !path.StartsWith("\\\\")) + return path; + return path.Substring(path.LastIndexOf('\\') + 1); + } + } + + } +} diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs new file mode 100644 index 000000000..412789240 --- /dev/null +++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs @@ -0,0 +1,157 @@ +using System.Text; +using MediaBrowser.Common.Events; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; +using WebSocketMessageType = MediaBrowser.Model.Net.WebSocketMessageType; +using WebSocketState = MediaBrowser.Model.Net.WebSocketState; + +namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp +{ + public class SharpWebSocket : IWebSocket + { + /// <summary> + /// The logger + /// </summary> + private readonly ILogger _logger; + + public event EventHandler<EventArgs> Closed; + + /// <summary> + /// Gets or sets the web socket. + /// </summary> + /// <value>The web socket.</value> + private WebSocketSharp.WebSocket WebSocket { get; set; } + + private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + + /// <summary> + /// Initializes a new instance of the <see cref="NativeWebSocket" /> class. + /// </summary> + /// <param name="socket">The socket.</param> + /// <param name="logger">The logger.</param> + /// <exception cref="System.ArgumentNullException">socket</exception> + public SharpWebSocket(WebSocketSharp.WebSocket socket, ILogger logger) + { + if (socket == null) + { + throw new ArgumentNullException("socket"); + } + + if (logger == null) + { + throw new ArgumentNullException("logger"); + } + + _logger = logger; + WebSocket = socket; + + socket.OnMessage += socket_OnMessage; + socket.OnClose += socket_OnClose; + socket.OnError += socket_OnError; + + WebSocket.ConnectAsServer(); + } + + void socket_OnError(object sender, WebSocketSharp.ErrorEventArgs e) + { + EventHelper.FireEventIfNotNull(Closed, this, EventArgs.Empty, _logger); + } + + void socket_OnClose(object sender, WebSocketSharp.CloseEventArgs e) + { + EventHelper.FireEventIfNotNull(Closed, this, EventArgs.Empty, _logger); + } + + void socket_OnMessage(object sender, WebSocketSharp.MessageEventArgs e) + { + if (OnReceive != null) + { + OnReceiveBytes(e.RawData); + } + } + + /// <summary> + /// Gets or sets the state. + /// </summary> + /// <value>The state.</value> + public WebSocketState State + { + get + { + WebSocketState commonState; + + if (!Enum.TryParse(WebSocket.ReadyState.ToString(), true, out commonState)) + { + _logger.Warn("Unrecognized WebSocketState: {0}", WebSocket.ReadyState.ToString()); + } + + return commonState; + } + } + + /// <summary> + /// Sends the async. + /// </summary> + /// <param name="bytes">The bytes.</param> + /// <param name="type">The type.</param> + /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public Task SendAsync(byte[] bytes, WebSocketMessageType type, bool endOfMessage, CancellationToken cancellationToken) + { + System.Net.WebSockets.WebSocketMessageType nativeType; + + if (!Enum.TryParse(type.ToString(), true, out nativeType)) + { + _logger.Warn("Unrecognized WebSocketMessageType: {0}", type.ToString()); + } + + var completionSource = new TaskCompletionSource<bool>(); + + WebSocket.SendAsync(Encoding.UTF8.GetString(bytes), res => completionSource.TrySetResult(true)); + + return completionSource.Task; + } + + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> + public void Dispose() + { + Dispose(true); + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources. + /// </summary> + /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + WebSocket.OnMessage -= socket_OnMessage; + WebSocket.OnClose -= socket_OnClose; + WebSocket.OnError -= socket_OnError; + + _cancellationTokenSource.Cancel(); + + WebSocket.Close(); + } + } + + /// <summary> + /// Gets or sets the receive action. + /// </summary> + /// <value>The receive action.</value> + public Action<byte[]> OnReceiveBytes { get; set; } + + /// <summary> + /// Gets or sets the on receive. + /// </summary> + /// <value>The on receive.</value> + public Action<string> OnReceive { get; set; } + } +} diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs new file mode 100644 index 000000000..cf756d9f2 --- /dev/null +++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Amib.Threading; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Logging; +using ServiceStack; +using ServiceStack.Web; +using WebSocketSharp.Net; +using WebSocketSharp.Server; + +namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp +{ + public class WebSocketSharpListener : IHttpListener + { + private readonly ConcurrentDictionary<string, string> _localEndPoints = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase); + private WebSocketSharp.Server.HttpServer _httpsv; + + private readonly ILogger _logger; + private readonly SmartThreadPool _threadPoolManager; + + public WebSocketSharpListener(ILogger logger, SmartThreadPool threadPoolManager) + { + _logger = logger; + _threadPoolManager = threadPoolManager; + } + + public IEnumerable<string> LocalEndPoints + { + get { return _localEndPoints.Keys.ToList(); } + } + + public System.Action<Exception, IRequest> ErrorHandler { get; set; } + + public System.Func<IHttpRequest, Uri, Task> RequestHandler { get; set; } + + public Action<WebSocketConnectEventArgs> WebSocketHandler { get; set; } + + public void Start(IEnumerable<string> urlPrefixes) + { + _httpsv = new WebSocketSharp.Server.HttpServer(8096, false, urlPrefixes.First()); + + _httpsv.OnRequest += _httpsv_OnRequest; + + _httpsv.Start(); + } + + void _httpsv_OnRequest(object sender, HttpRequestEventArgs e) + { + _threadPoolManager.QueueWorkItem(() => InitTask(e.Context)); + } + + private 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); + } + } + + private Task ProcessRequestAsync(HttpListenerContext context) + { + var request = context.Request; + + LogHttpRequest(request); + + if (request.IsWebSocketRequest) + { + ProcessWebSocketRequest(context); + return Task.FromResult(true); + } + + if (string.IsNullOrEmpty(context.Request.RawUrl)) + return ((object)null).AsTaskResult(); + + var httpReq = GetRequest(context); + + return RequestHandler(httpReq, request.Url); + } + + /// <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); + } + + LogRequest(_logger, request); + } + + private void ProcessWebSocketRequest(HttpListenerContext ctx) + { + try + { + var webSocketContext = ctx.AcceptWebSocket(null, null); + + if (WebSocketHandler != null) + { + WebSocketHandler(new WebSocketConnectEventArgs + { + WebSocket = new SharpWebSocket(webSocketContext.WebSocket, _logger), + Endpoint = ctx.Request.RemoteEndPoint.ToString() + }); + } + } + catch (Exception ex) + { + _logger.ErrorException("AcceptWebSocketAsync error", ex); + ctx.Response.StatusCode = 500; + ctx.Response.Close(); + } + } + + private IHttpRequest GetRequest(HttpListenerContext httpContext) + { + var operationName = httpContext.Request.GetOperationName(); + + var req = new WebSocketSharpRequest(httpContext, operationName, RequestAttributes.None, _logger); + req.RequestAttributes = req.GetAttributes(); + + return req; + } + + /// <summary> + /// Logs the request. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="request">The request.</param> + private static void LogRequest(ILogger logger, HttpListenerRequest request) + { + var log = new StringBuilder(); + + //var headers = string.Join(",", request.Headers.AllKeys.Where(i => !string.Equals(i, "cookie", StringComparison.OrdinalIgnoreCase) && !string.Equals(i, "Referer", StringComparison.OrdinalIgnoreCase)).Select(k => k + "=" + request.Headers[k])); + + //log.AppendLine("Ip: " + request.RemoteEndPoint + ". Headers: " + headers); + + var type = request.IsWebSocketRequest ? "Web Socket" : "HTTP " + request.HttpMethod; + + logger.LogMultiline(type + " " + request.Url, LogSeverity.Debug, log); + } + + private void HandleError(Exception ex, HttpListenerContext context) + { + var httpReq = GetRequest(context); + + if (ErrorHandler != null) + { + ErrorHandler(ex, httpReq); + } + } + + public void Stop() + { + _httpsv.Stop(); + } + + private readonly object _disposeLock = new object(); + public void Dispose() + { + lock (_disposeLock) + { + if (_httpsv != null) + { + _httpsv.OnRequest -= _httpsv_OnRequest; + _httpsv.Stop(); + _httpsv = null; + } + } + } + } +} diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs new file mode 100644 index 000000000..7a5f6fbdc --- /dev/null +++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs @@ -0,0 +1,402 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Funq; +using MediaBrowser.Model.Logging; +using ServiceStack; +using ServiceStack.Host; +using ServiceStack.Web; +using WebSocketSharp.Net; + +namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp +{ + public partial class WebSocketSharpRequest : IHttpRequest + { + public Container Container { get; set; } + private readonly HttpListenerRequest request; + private readonly IHttpResponse response; + + public WebSocketSharpRequest(HttpListenerContext httpContext, string operationName, RequestAttributes requestAttributes, ILogger logger) + { + this.OperationName = operationName; + this.RequestAttributes = requestAttributes; + this.request = httpContext.Request; + this.response = new WebSocketSharpResponse(logger, httpContext.Response); + + this.RequestPreferences = new RequestPreferences(this); + } + + public HttpListenerRequest HttpRequest + { + get { return request; } + } + + public object OriginalRequest + { + get { return request; } + } + + public IResponse Response + { + get { return response; } + } + + public IHttpResponse HttpResponse + { + get { return response; } + } + + public RequestAttributes RequestAttributes { get; set; } + + public IRequestPreferences RequestPreferences { get; private set; } + + public T TryResolve<T>() + { + if (typeof(T) == typeof(IHttpRequest)) + throw new Exception("You don't need to use IHttpRequest.TryResolve<IHttpRequest> to resolve itself"); + + if (typeof(T) == typeof(IHttpResponse)) + throw new Exception("Resolve IHttpResponse with 'Response' property instead of IHttpRequest.TryResolve<IHttpResponse>"); + + return Container == null + ? HostContext.TryResolve<T>() + : Container.TryResolve<T>(); + } + + public string OperationName { get; set; } + + public object Dto { get; set; } + + public string GetRawBody() + { + if (bufferedStream != null) + { + return bufferedStream.ToArray().FromUtf8Bytes(); + } + + using (var reader = new StreamReader(InputStream)) + { + return reader.ReadToEnd(); + } + } + + public string RawUrl + { + get { return request.RawUrl; } + } + + public string AbsoluteUri + { + get { return request.Url.AbsoluteUri.TrimEnd('/'); } + } + + public string UserHostAddress + { + get { return request.UserHostAddress; } + } + + public string XForwardedFor + { + get + { + return String.IsNullOrEmpty(request.Headers[HttpHeaders.XForwardedFor]) ? null : request.Headers[HttpHeaders.XForwardedFor]; + } + } + + public int? XForwardedPort + { + get + { + return string.IsNullOrEmpty(request.Headers[HttpHeaders.XForwardedPort]) ? (int?)null : int.Parse(request.Headers[HttpHeaders.XForwardedPort]); + } + } + + public string XForwardedProtocol + { + get + { + return string.IsNullOrEmpty(request.Headers[HttpHeaders.XForwardedProtocol]) ? null : request.Headers[HttpHeaders.XForwardedProtocol]; + } + } + + public string XRealIp + { + get + { + return String.IsNullOrEmpty(request.Headers[HttpHeaders.XRealIp]) ? null : request.Headers[HttpHeaders.XRealIp]; + } + } + + private string remoteIp; + public string RemoteIp + { + get + { + return remoteIp ?? + (remoteIp = XForwardedFor ?? + (XRealIp ?? + ((request.RemoteEndPoint != null) ? request.RemoteEndPoint.Address.ToString() : null))); + } + } + + public bool IsSecureConnection + { + get { return request.IsSecureConnection || XForwardedProtocol == "https"; } + } + + public string[] AcceptTypes + { + get { return request.AcceptTypes; } + } + + private Dictionary<string, object> items; + public Dictionary<string, object> Items + { + get { return items ?? (items = new Dictionary<string, object>()); } + } + + private string responseContentType; + public string ResponseContentType + { + get + { + return responseContentType + ?? (responseContentType = this.GetResponseContentType()); + } + set + { + this.responseContentType = value; + HasExplicitResponseContentType = true; + } + } + + public bool HasExplicitResponseContentType { get; private set; } + + private string pathInfo; + public string PathInfo + { + get + { + if (this.pathInfo == null) + { + var mode = HostContext.Config.HandlerFactoryPath; + + var pos = request.RawUrl.IndexOf("?"); + if (pos != -1) + { + var path = request.RawUrl.Substring(0, pos); + this.pathInfo = HttpRequestExtensions.GetPathInfo( + path, + mode, + mode ?? ""); + } + else + { + this.pathInfo = request.RawUrl; + } + + this.pathInfo = this.pathInfo.UrlDecode(); + this.pathInfo = NormalizePathInfo(pathInfo, mode); + } + return this.pathInfo; + } + } + + private Dictionary<string, System.Net.Cookie> cookies; + public IDictionary<string, System.Net.Cookie> Cookies + { + get + { + if (cookies == null) + { + cookies = new Dictionary<string, System.Net.Cookie>(); + for (var i = 0; i < this.request.Cookies.Count; i++) + { + var httpCookie = this.request.Cookies[i]; + cookies[httpCookie.Name] = new System.Net.Cookie(httpCookie.Name, httpCookie.Value, httpCookie.Path, httpCookie.Domain); + } + } + + return cookies; + } + } + + public string UserAgent + { + get { return request.UserAgent; } + } + + private NameValueCollectionWrapper headers; + public INameValueCollection Headers + { + get { return headers ?? (headers = new NameValueCollectionWrapper(request.Headers)); } + } + + private NameValueCollectionWrapper queryString; + public INameValueCollection QueryString + { + get { return queryString ?? (queryString = new NameValueCollectionWrapper(HttpUtility.ParseQueryString(request.Url.Query))); } + } + + private NameValueCollectionWrapper formData; + public INameValueCollection FormData + { + get { return formData ?? (formData = new NameValueCollectionWrapper(this.Form)); } + } + + public bool IsLocal + { + get { return request.IsLocal; } + } + + private string httpMethod; + public string HttpMethod + { + get + { + return httpMethod + ?? (httpMethod = Param(HttpHeaders.XHttpMethodOverride) + ?? request.HttpMethod); + } + } + + public string Verb + { + get { return HttpMethod; } + } + + public string Param(string name) + { + return Headers[name] + ?? QueryString[name] + ?? FormData[name]; + } + + public string ContentType + { + get { return request.ContentType; } + } + + public Encoding contentEncoding; + public Encoding ContentEncoding + { + get { return contentEncoding ?? request.ContentEncoding; } + set { contentEncoding = value; } + } + + public Uri UrlReferrer + { + get { return request.UrlReferrer; } + } + + public static Encoding GetEncoding(string contentTypeHeader) + { + var param = GetParameter(contentTypeHeader, "charset="); + if (param == null) return null; + try + { + return Encoding.GetEncoding(param); + } + catch (ArgumentException) + { + return null; + } + } + + public bool UseBufferedStream + { + get { return bufferedStream != null; } + set + { + bufferedStream = value + ? bufferedStream ?? new MemoryStream(request.InputStream.ReadFully()) + : null; + } + } + + private MemoryStream bufferedStream; + public Stream InputStream + { + get { return bufferedStream ?? request.InputStream; } + } + + public long ContentLength + { + get { return request.ContentLength64; } + } + + private IHttpFile[] httpFiles; + public IHttpFile[] Files + { + get + { + if (httpFiles == null) + { + if (files == null) + return httpFiles = new IHttpFile[0]; + + httpFiles = new IHttpFile[files.Count]; + for (var i = 0; i < files.Count; i++) + { + var reqFile = files[i]; + + httpFiles[i] = new HttpFile + { + ContentType = reqFile.ContentType, + ContentLength = reqFile.ContentLength, + FileName = reqFile.FileName, + InputStream = reqFile.InputStream, + }; + } + } + return httpFiles; + } + } + + static Stream GetSubStream(Stream stream) + { + if (stream is MemoryStream) + { + var other = (MemoryStream)stream; + try + { + return new MemoryStream(other.GetBuffer(), 0, (int)other.Length, false, true); + } + catch (UnauthorizedAccessException) + { + return new MemoryStream(other.ToArray(), 0, (int)other.Length, false, true); + } + } + + return stream; + } + + static void EndSubStream(Stream stream) + { + } + + public static string GetHandlerPathIfAny(string listenerUrl) + { + if (listenerUrl == null) return null; + var pos = listenerUrl.IndexOf("://", StringComparison.InvariantCultureIgnoreCase); + if (pos == -1) return null; + var startHostUrl = listenerUrl.Substring(pos + "://".Length); + var endPos = startHostUrl.IndexOf('/'); + if (endPos == -1) return null; + var endHostUrl = startHostUrl.Substring(endPos + 1); + return String.IsNullOrEmpty(endHostUrl) ? null : endHostUrl.TrimEnd('/'); + } + + public static string NormalizePathInfo(string pathInfo, string handlerPath) + { + if (handlerPath != null && pathInfo.TrimStart('/').StartsWith( + handlerPath, StringComparison.InvariantCultureIgnoreCase)) + { + return pathInfo.TrimStart('/').Substring(handlerPath.Length); + } + + return pathInfo; + } + } +} diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs new file mode 100644 index 000000000..2e3828512 --- /dev/null +++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs @@ -0,0 +1,144 @@ +using System; +using System.IO; +using System.Net; +using MediaBrowser.Model.Logging; +using ServiceStack; +using ServiceStack.Host; +using ServiceStack.Web; +using HttpListenerResponse = WebSocketSharp.Net.HttpListenerResponse; + +namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp +{ + public class WebSocketSharpResponse : IHttpResponse + { + private readonly ILogger _logger; + private readonly HttpListenerResponse response; + + public WebSocketSharpResponse(ILogger logger, HttpListenerResponse response) + { + _logger = logger; + this.response = response; + } + + public bool UseBufferedStream { get; set; } + + public object OriginalResponse + { + get { return response; } + } + + public int StatusCode + { + get { return this.response.StatusCode; } + set { this.response.StatusCode = value; } + } + + public string StatusDescription + { + get { return this.response.StatusDescription; } + set { this.response.StatusDescription = value; } + } + + public string ContentType + { + get { return response.ContentType; } + set { response.ContentType = value; } + } + + public ICookies Cookies { get; set; } + + public void AddHeader(string name, string value) + { + if (string.Equals(name, "Content-Type", StringComparison.OrdinalIgnoreCase)) + { + ContentType = value; + return; + } + + response.AddHeader(name, value); + } + + public void Redirect(string url) + { + response.Redirect(url); + } + + public Stream OutputStream + { + get { return response.OutputStream; } + } + + public object Dto { get; set; } + + public void Write(string text) + { + try + { + var bOutput = System.Text.Encoding.UTF8.GetBytes(text); + response.ContentLength64 = bOutput.Length; + + var outputStream = response.OutputStream; + outputStream.Write(bOutput, 0, bOutput.Length); + Close(); + } + catch (Exception ex) + { + _logger.ErrorException("Could not WriteTextToResponse: " + ex.Message, ex); + throw; + } + } + + public void Close() + { + if (!this.IsClosed) + { + this.IsClosed = true; + + try + { + this.response.CloseOutputStream(_logger); + } + catch (Exception ex) + { + _logger.ErrorException("Error closing HttpListener output stream", ex); + } + } + } + + public void End() + { + Close(); + } + + public void Flush() + { + response.OutputStream.Flush(); + } + + public bool IsClosed + { + get; + private set; + } + + public void SetContentLength(long contentLength) + { + //you can happily set the Content-Length header in Asp.Net + //but HttpListener will complain if you do - you have to set ContentLength64 on the response. + //workaround: HttpListener throws "The parameter is incorrect" exceptions when we try to set the Content-Length header + response.ContentLength64 = contentLength; + } + + public void SetCookie(Cookie cookie) + { + var cookieStr = cookie.AsHeaderValue(); + response.Headers.Add(HttpHeaders.SetCookie, cookieStr); + } + + public bool SendChunked + { + get { return response.SendChunked; } + set { response.SendChunked = value; } + } + } +} |
