aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/HttpServer
diff options
context:
space:
mode:
authorLuke Pulverenti <luke.pulverenti@gmail.com>2016-11-08 13:44:23 -0500
committerLuke Pulverenti <luke.pulverenti@gmail.com>2016-11-08 13:44:23 -0500
commita8b340cbb29dbcf7fd5d101e640d66470c6d32bf (patch)
treea626c151e9ccb8809dd6d667fb9146fe4bb3ffea /Emby.Server.Implementations/HttpServer
parent05a5ce58a9293f6669960c735911e9455c5d8188 (diff)
update portable projects
Diffstat (limited to 'Emby.Server.Implementations/HttpServer')
-rw-r--r--Emby.Server.Implementations/HttpServer/LoggerUtils.cs43
-rw-r--r--Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs224
-rw-r--r--Emby.Server.Implementations/HttpServer/ResponseFilter.cs129
-rw-r--r--Emby.Server.Implementations/HttpServer/SocketSharp/Extensions.cs12
-rw-r--r--Emby.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs166
-rw-r--r--Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs203
-rw-r--r--Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs204
7 files changed, 981 insertions, 0 deletions
diff --git a/Emby.Server.Implementations/HttpServer/LoggerUtils.cs b/Emby.Server.Implementations/HttpServer/LoggerUtils.cs
new file mode 100644
index 000000000..8fc92a09a
--- /dev/null
+++ b/Emby.Server.Implementations/HttpServer/LoggerUtils.cs
@@ -0,0 +1,43 @@
+using MediaBrowser.Model.Logging;
+using System;
+using System.Globalization;
+using SocketHttpListener.Net;
+
+namespace Emby.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 url = request.Url.ToString();
+
+ logger.Info("{0} {1}. UserAgent: {2}", request.IsWebSocketRequest ? "WS" : "HTTP " + request.HttpMethod, url, request.UserAgent ?? string.Empty);
+ }
+
+ public static void LogRequest(ILogger logger, string url, string method, string userAgent)
+ {
+ logger.Info("{0} {1}. UserAgent: {2}", "HTTP " + method, url, userAgent ?? string.Empty);
+ }
+
+ /// <summary>
+ /// Logs the response.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="statusCode">The status code.</param>
+ /// <param name="url">The URL.</param>
+ /// <param name="endPoint">The end point.</param>
+ /// <param name="duration">The duration.</param>
+ public static void LogResponse(ILogger logger, int statusCode, string url, string endPoint, TimeSpan duration)
+ {
+ var durationMs = duration.TotalMilliseconds;
+ var logSuffix = durationMs >= 1000 && durationMs < 60000 ? "ms (slow)" : "ms";
+
+ logger.Info("HTTP Response {0} to {1}. Time: {2}{3}. {4}", statusCode, endPoint, Convert.ToInt32(durationMs).ToString(CultureInfo.InvariantCulture), logSuffix, url);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs
new file mode 100644
index 000000000..e88994bec
--- /dev/null
+++ b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs
@@ -0,0 +1,224 @@
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Services;
+
+namespace Emby.Server.Implementations.HttpServer
+{
+ public class RangeRequestWriter : IAsyncStreamWriter, IHttpResult
+ {
+ /// <summary>
+ /// Gets or sets the source stream.
+ /// </summary>
+ /// <value>The source stream.</value>
+ private Stream SourceStream { get; set; }
+ private string RangeHeader { get; set; }
+ private bool IsHeadRequest { get; set; }
+
+ private long RangeStart { get; set; }
+ private long RangeEnd { get; set; }
+ private long RangeLength { get; set; }
+ private long TotalContentLength { get; set; }
+
+ public Action OnComplete { get; set; }
+ private readonly ILogger _logger;
+
+ private const int BufferSize = 81920;
+
+ /// <summary>
+ /// The _options
+ /// </summary>
+ private readonly Dictionary<string, string> _options = new Dictionary<string, string>();
+
+ /// <summary>
+ /// The us culture
+ /// </summary>
+ private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+ public List<Cookie> Cookies { get; private set; }
+
+ /// <summary>
+ /// Additional HTTP Headers
+ /// </summary>
+ /// <value>The headers.</value>
+ public IDictionary<string, string> Headers
+ {
+ get { return _options; }
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StreamWriter" /> class.
+ /// </summary>
+ /// <param name="rangeHeader">The range header.</param>
+ /// <param name="source">The source.</param>
+ /// <param name="contentType">Type of the content.</param>
+ /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
+ public RangeRequestWriter(string rangeHeader, Stream source, string contentType, bool isHeadRequest, ILogger logger)
+ {
+ if (string.IsNullOrEmpty(contentType))
+ {
+ throw new ArgumentNullException("contentType");
+ }
+
+ RangeHeader = rangeHeader;
+ SourceStream = source;
+ IsHeadRequest = isHeadRequest;
+ this._logger = logger;
+
+ ContentType = contentType;
+ Headers["Content-Type"] = contentType;
+ Headers["Accept-Ranges"] = "bytes";
+ StatusCode = HttpStatusCode.PartialContent;
+
+ Cookies = new List<Cookie>();
+ SetRangeValues();
+ }
+
+ /// <summary>
+ /// Sets the range values.
+ /// </summary>
+ private void SetRangeValues()
+ {
+ var requestedRange = RequestedRanges[0];
+
+ TotalContentLength = SourceStream.Length;
+
+ // If the requested range is "0-", we can optimize by just doing a stream copy
+ if (!requestedRange.Value.HasValue)
+ {
+ RangeEnd = TotalContentLength - 1;
+ }
+ else
+ {
+ RangeEnd = requestedRange.Value.Value;
+ }
+
+ RangeStart = requestedRange.Key;
+ RangeLength = 1 + RangeEnd - RangeStart;
+
+ // Content-Length is the length of what we're serving, not the original content
+ Headers["Content-Length"] = RangeLength.ToString(UsCulture);
+ Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", RangeStart, RangeEnd, TotalContentLength);
+
+ if (RangeStart > 0)
+ {
+ SourceStream.Position = RangeStart;
+ }
+ }
+
+ /// <summary>
+ /// The _requested ranges
+ /// </summary>
+ private List<KeyValuePair<long, long?>> _requestedRanges;
+ /// <summary>
+ /// Gets the requested ranges.
+ /// </summary>
+ /// <value>The requested ranges.</value>
+ protected List<KeyValuePair<long, long?>> RequestedRanges
+ {
+ get
+ {
+ if (_requestedRanges == null)
+ {
+ _requestedRanges = new List<KeyValuePair<long, long?>>();
+
+ // Example: bytes=0-,32-63
+ var ranges = RangeHeader.Split('=')[1].Split(',');
+
+ foreach (var range in ranges)
+ {
+ var vals = range.Split('-');
+
+ long start = 0;
+ long? end = null;
+
+ if (!string.IsNullOrEmpty(vals[0]))
+ {
+ start = long.Parse(vals[0], UsCulture);
+ }
+ if (!string.IsNullOrEmpty(vals[1]))
+ {
+ end = long.Parse(vals[1], UsCulture);
+ }
+
+ _requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
+ }
+ }
+
+ return _requestedRanges;
+ }
+ }
+
+ public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
+ {
+ try
+ {
+ // Headers only
+ if (IsHeadRequest)
+ {
+ return;
+ }
+
+ using (var source = SourceStream)
+ {
+ // If the requested range is "0-", we can optimize by just doing a stream copy
+ if (RangeEnd >= TotalContentLength - 1)
+ {
+ await source.CopyToAsync(responseStream, BufferSize).ConfigureAwait(false);
+ }
+ else
+ {
+ await CopyToInternalAsync(source, responseStream, RangeLength).ConfigureAwait(false);
+ }
+ }
+ }
+ finally
+ {
+ if (OnComplete != null)
+ {
+ OnComplete();
+ }
+ }
+ }
+
+ private async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength)
+ {
+ var array = new byte[BufferSize];
+ int count;
+ while ((count = await source.ReadAsync(array, 0, array.Length).ConfigureAwait(false)) != 0)
+ {
+ var bytesToCopy = Math.Min(count, copyLength);
+
+ await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy)).ConfigureAwait(false);
+
+ copyLength -= bytesToCopy;
+
+ if (copyLength <= 0)
+ {
+ break;
+ }
+ }
+ }
+
+ public string ContentType { get; set; }
+
+ public IRequest RequestContext { get; set; }
+
+ public object Response { get; set; }
+
+ public int Status { get; set; }
+
+ public HttpStatusCode StatusCode
+ {
+ get { return (HttpStatusCode)Status; }
+ set { Status = (int)value; }
+ }
+
+ public string StatusDescription { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs
new file mode 100644
index 000000000..6d9d7d921
--- /dev/null
+++ b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs
@@ -0,0 +1,129 @@
+using MediaBrowser.Model.Logging;
+using System;
+using System.Globalization;
+using System.Text;
+using Emby.Server.Implementations.HttpServer.SocketSharp;
+using MediaBrowser.Model.Services;
+
+namespace Emby.Server.Implementations.HttpServer
+{
+ public class ResponseFilter
+ {
+ private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+ private readonly ILogger _logger;
+
+ public ResponseFilter(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ /// <summary>
+ /// Filters the response.
+ /// </summary>
+ /// <param name="req">The req.</param>
+ /// <param name="res">The res.</param>
+ /// <param name="dto">The dto.</param>
+ public void FilterResponse(IRequest req, IResponse res, object dto)
+ {
+ // Try to prevent compatibility view
+ res.AddHeader("X-UA-Compatible", "IE=Edge");
+ res.AddHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization");
+ res.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
+ res.AddHeader("Access-Control-Allow-Origin", "*");
+
+ var exception = dto as Exception;
+
+ if (exception != null)
+ {
+ _logger.ErrorException("Error processing request for {0}", exception, req.RawUrl);
+
+ if (!string.IsNullOrEmpty(exception.Message))
+ {
+ var error = exception.Message.Replace(Environment.NewLine, " ");
+ error = RemoveControlCharacters(error);
+
+ res.AddHeader("X-Application-Error-Code", error);
+ }
+ }
+
+ var vary = "Accept-Encoding";
+
+ var hasHeaders = dto as IHasHeaders;
+ var sharpResponse = res as WebSocketSharpResponse;
+
+ if (hasHeaders != null)
+ {
+ if (!hasHeaders.Headers.ContainsKey("Server"))
+ {
+ hasHeaders.Headers["Server"] = "Mono-HTTPAPI/1.1, UPnP/1.0 DLNADOC/1.50";
+ //hasHeaders.Headers["Server"] = "Mono-HTTPAPI/1.1";
+ }
+
+ // Content length has to be explicitly set on on HttpListenerResponse or it won't be happy
+ string contentLength;
+
+ if (hasHeaders.Headers.TryGetValue("Content-Length", out contentLength) && !string.IsNullOrEmpty(contentLength))
+ {
+ var length = long.Parse(contentLength, UsCulture);
+
+ if (length > 0)
+ {
+ 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;
+ //}
+
+ if (sharpResponse != null)
+ {
+ sharpResponse.SendChunked = false;
+ }
+ }
+ }
+
+ string hasHeadersVary;
+ if (hasHeaders.Headers.TryGetValue("Vary", out hasHeadersVary))
+ {
+ vary = hasHeadersVary;
+ }
+
+ hasHeaders.Headers["Vary"] = vary;
+ }
+
+ //res.KeepAlive = false;
+
+ // Per Google PageSpeed
+ // This instructs the proxies to cache two versions of the resource: one compressed, and one uncompressed.
+ // The correct version of the resource is delivered based on the client request header.
+ // This is a good choice for applications that are singly homed and depend on public proxies for user locality.
+ res.AddHeader("Vary", vary);
+ }
+
+ /// <summary>
+ /// Removes the control characters.
+ /// </summary>
+ /// <param name="inString">The in string.</param>
+ /// <returns>System.String.</returns>
+ public static string RemoveControlCharacters(string inString)
+ {
+ if (inString == null) return null;
+
+ var newString = new StringBuilder();
+
+ foreach (var ch in inString)
+ {
+ if (!char.IsControl(ch))
+ {
+ newString.Append(ch);
+ }
+ }
+ return newString.ToString();
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/Extensions.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/Extensions.cs
new file mode 100644
index 000000000..07a338f19
--- /dev/null
+++ b/Emby.Server.Implementations/HttpServer/SocketSharp/Extensions.cs
@@ -0,0 +1,12 @@
+using SocketHttpListener.Net;
+
+namespace Emby.Server.Implementations.HttpServer.SocketSharp
+{
+ public static class Extensions
+ {
+ public static string GetOperationName(this HttpListenerRequest request)
+ {
+ return request.Url.Segments[request.Url.Segments.Length - 1];
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs
new file mode 100644
index 000000000..0a312f7b9
--- /dev/null
+++ b/Emby.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs
@@ -0,0 +1,166 @@
+using MediaBrowser.Common.Events;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using WebSocketState = MediaBrowser.Model.Net.WebSocketState;
+
+namespace Emby.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 SocketHttpListener.WebSocket WebSocket { get; set; }
+
+ private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
+
+ public SharpWebSocket(SocketHttpListener.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, SocketHttpListener.ErrorEventArgs e)
+ {
+ _logger.Error("Error in SharpWebSocket: {0}", e.Message ?? string.Empty);
+ //EventHelper.FireEventIfNotNull(Closed, this, EventArgs.Empty, _logger);
+ }
+
+ void socket_OnClose(object sender, SocketHttpListener.CloseEventArgs e)
+ {
+ EventHelper.FireEventIfNotNull(Closed, this, EventArgs.Empty, _logger);
+ }
+
+ void socket_OnMessage(object sender, SocketHttpListener.MessageEventArgs e)
+ {
+ //if (!string.IsNullOrWhiteSpace(e.Data))
+ //{
+ // if (OnReceive != null)
+ // {
+ // OnReceive(e.Data);
+ // }
+ // return;
+ //}
+ if (OnReceiveBytes != 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="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, bool endOfMessage, CancellationToken cancellationToken)
+ {
+ var completionSource = new TaskCompletionSource<bool>();
+
+ WebSocket.SendAsync(bytes, res => completionSource.TrySetResult(true));
+
+ return completionSource.Task;
+ }
+
+ /// <summary>
+ /// Sends the asynchronous.
+ /// </summary>
+ /// <param name="text">The text.</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(string text, bool endOfMessage, CancellationToken cancellationToken)
+ {
+ var completionSource = new TaskCompletionSource<bool>();
+
+ WebSocket.SendAsync(text, 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/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs
new file mode 100644
index 000000000..0cb4d428b
--- /dev/null
+++ b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs
@@ -0,0 +1,203 @@
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Logging;
+using SocketHttpListener.Net;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Emby.Server.Implementations.Logging;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Model.Cryptography;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Services;
+using MediaBrowser.Model.Text;
+using SocketHttpListener.Primitives;
+
+namespace Emby.Server.Implementations.HttpServer.SocketSharp
+{
+ public class WebSocketSharpListener : IHttpListener
+ {
+ private HttpListener _listener;
+
+ private readonly ILogger _logger;
+ private readonly ICertificate _certificate;
+ private readonly IMemoryStreamFactory _memoryStreamProvider;
+ private readonly ITextEncoding _textEncoding;
+ private readonly INetworkManager _networkManager;
+ private readonly ISocketFactory _socketFactory;
+ private readonly ICryptoProvider _cryptoProvider;
+ private readonly IStreamFactory _streamFactory;
+ private readonly Func<HttpListenerContext, IHttpRequest> _httpRequestFactory;
+
+ public WebSocketSharpListener(ILogger logger, ICertificate certificate, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, INetworkManager networkManager, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, Func<HttpListenerContext, IHttpRequest> httpRequestFactory)
+ {
+ _logger = logger;
+ _certificate = certificate;
+ _memoryStreamProvider = memoryStreamProvider;
+ _textEncoding = textEncoding;
+ _networkManager = networkManager;
+ _socketFactory = socketFactory;
+ _cryptoProvider = cryptoProvider;
+ _streamFactory = streamFactory;
+ _httpRequestFactory = httpRequestFactory;
+ }
+
+ public Action<Exception, IRequest> ErrorHandler { get; set; }
+ public Func<IHttpRequest, Uri, Task> RequestHandler { get; set; }
+
+ public Action<WebSocketConnectingEventArgs> WebSocketConnecting { get; set; }
+
+ public Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }
+
+ public void Start(IEnumerable<string> urlPrefixes)
+ {
+ if (_listener == null)
+ _listener = new HttpListener(new PatternsLogger(_logger), _cryptoProvider, _streamFactory, _socketFactory, _networkManager, _textEncoding, _memoryStreamProvider);
+
+ if (_certificate != null)
+ {
+ _listener.LoadCert(_certificate);
+ }
+
+ foreach (var prefix in urlPrefixes)
+ {
+ _logger.Info("Adding HttpListener prefix " + prefix);
+ _listener.Prefixes.Add(prefix);
+ }
+
+ _listener.OnContext = ProcessContext;
+
+ _listener.Start();
+ }
+
+ private void ProcessContext(HttpListenerContext context)
+ {
+ Task.Factory.StartNew(() => InitTask(context));
+ }
+
+ private Task InitTask(HttpListenerContext context)
+ {
+ IHttpRequest httpReq = null;
+ var request = context.Request;
+
+ try
+ {
+ if (request.IsWebSocketRequest)
+ {
+ LoggerUtils.LogRequest(_logger, request);
+
+ ProcessWebSocketRequest(context);
+ return Task.FromResult(true);
+ }
+
+ httpReq = GetRequest(context);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error processing request", ex);
+
+ httpReq = httpReq ?? GetRequest(context);
+ ErrorHandler(ex, httpReq);
+ return Task.FromResult(true);
+ }
+
+ return RequestHandler(httpReq, request.Url);
+ }
+
+ private void ProcessWebSocketRequest(HttpListenerContext ctx)
+ {
+ try
+ {
+ var endpoint = ctx.Request.RemoteEndPoint.ToString();
+ var url = ctx.Request.RawUrl;
+
+ var connectingArgs = new WebSocketConnectingEventArgs
+ {
+ Url = url,
+ QueryString = ctx.Request.QueryString,
+ Endpoint = endpoint
+ };
+
+ if (WebSocketConnecting != null)
+ {
+ WebSocketConnecting(connectingArgs);
+ }
+
+ if (connectingArgs.AllowConnection)
+ {
+ _logger.Debug("Web socket connection allowed");
+
+ var webSocketContext = ctx.AcceptWebSocket(null);
+
+ if (WebSocketConnected != null)
+ {
+ WebSocketConnected(new WebSocketConnectEventArgs
+ {
+ Url = url,
+ QueryString = ctx.Request.QueryString,
+ WebSocket = new SharpWebSocket(webSocketContext.WebSocket, _logger),
+ Endpoint = endpoint
+ });
+ }
+ }
+ else
+ {
+ _logger.Warn("Web socket connection not allowed");
+ ctx.Response.StatusCode = 401;
+ ctx.Response.Close();
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("AcceptWebSocketAsync error", ex);
+ ctx.Response.StatusCode = 500;
+ ctx.Response.Close();
+ }
+ }
+
+ private IHttpRequest GetRequest(HttpListenerContext httpContext)
+ {
+ return _httpRequestFactory(httpContext);
+ }
+
+ public void Stop()
+ {
+ if (_listener != null)
+ {
+ foreach (var prefix in _listener.Prefixes.ToList())
+ {
+ _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)
+ {
+ Stop();
+ }
+
+ //release unmanaged resources here...
+ _disposed = true;
+ }
+ }
+ }
+
+} \ No newline at end of file
diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs
new file mode 100644
index 000000000..de0b33fe3
--- /dev/null
+++ b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs
@@ -0,0 +1,204 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Text;
+using MediaBrowser.Model.Logging;
+using HttpListenerResponse = SocketHttpListener.Net.HttpListenerResponse;
+using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse;
+using IRequest = MediaBrowser.Model.Services.IRequest;
+
+namespace Emby.Server.Implementations.HttpServer.SocketSharp
+{
+ public class WebSocketSharpResponse : IHttpResponse
+ {
+ private readonly ILogger _logger;
+ private readonly HttpListenerResponse _response;
+
+ public WebSocketSharpResponse(ILogger logger, HttpListenerResponse response, IRequest request)
+ {
+ _logger = logger;
+ this._response = response;
+ Items = new Dictionary<string, object>();
+ Request = request;
+ }
+
+ public IRequest Request { get; private set; }
+ public bool UseBufferedStream { get; set; }
+ public Dictionary<string, object> Items { get; private 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 string GetHeader(string name)
+ {
+ return _response.Headers[name];
+ }
+
+ 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)
+ {
+ var bOutput = System.Text.Encoding.UTF8.GetBytes(text);
+ _response.ContentLength64 = bOutput.Length;
+
+ var outputStream = _response.OutputStream;
+ outputStream.Write(bOutput, 0, bOutput.Length);
+ Close();
+ }
+
+ public void Close()
+ {
+ if (!this.IsClosed)
+ {
+ this.IsClosed = true;
+
+ try
+ {
+ CloseOutputStream(this._response);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error closing HttpListener output stream", ex);
+ }
+ }
+ }
+
+ public void CloseOutputStream(HttpListenerResponse response)
+ {
+ try
+ {
+ response.OutputStream.Flush();
+ response.OutputStream.Dispose();
+ response.Close();
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error in HttpListenerResponseWrapper: " + ex.Message, 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 = AsHeaderValue(cookie);
+ _response.Headers.Add("Set-Cookie", cookieStr);
+ }
+
+ public static string AsHeaderValue(Cookie cookie)
+ {
+ var defaultExpires = DateTime.MinValue;
+
+ var path = cookie.Expires == defaultExpires
+ ? "/"
+ : cookie.Path ?? "/";
+
+ var sb = new StringBuilder();
+
+ sb.Append($"{cookie.Name}={cookie.Value};path={path}");
+
+ if (cookie.Expires != defaultExpires)
+ {
+ sb.Append($";expires={cookie.Expires:R}");
+ }
+
+ if (!string.IsNullOrEmpty(cookie.Domain))
+ {
+ sb.Append($";domain={cookie.Domain}");
+ }
+ //else if (restrictAllCookiesToDomain != null)
+ //{
+ // sb.Append($";domain={restrictAllCookiesToDomain}");
+ //}
+
+ if (cookie.Secure)
+ {
+ sb.Append(";Secure");
+ }
+ if (cookie.HttpOnly)
+ {
+ sb.Append(";HttpOnly");
+ }
+
+ return sb.ToString();
+ }
+
+
+ public bool SendChunked
+ {
+ get { return _response.SendChunked; }
+ set { _response.SendChunked = value; }
+ }
+
+ public bool KeepAlive { get; set; }
+
+ public void ClearCookies()
+ {
+ }
+ }
+}