From bc657237aa4c541fe0079fcbb7616dbe87bbf0a7 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Fri, 18 Jul 2014 21:28:40 -0400 Subject: [PATCH] consolidate web socket onto one port --- .../MediaBrowser.Common.csproj | 1 - MediaBrowser.Common/Net/IServerManager.cs | 11 - MediaBrowser.Common/Net/IWebSocketServer.cs | 32 - MediaBrowser.Controller/Net/IHttpServer.cs | 6 - .../HttpServer/HttpListenerHost.cs | 44 +- .../HttpServer/LoggerUtils.cs | 18 - .../NetListener/HttpListenerServer.cs | 27 +- .../HttpServer/ResponseFilter.cs | 23 +- .../HttpServer/SocketSharp/Extensions.cs | 28 + .../HttpServer/SocketSharp/RequestMono.cs | 918 ++++++++++++++++++ .../HttpServer/SocketSharp/SharpWebSocket.cs | 157 +++ .../SocketSharp/WebSocketSharpListener.cs | 192 ++++ .../SocketSharp/WebSocketSharpRequest.cs | 402 ++++++++ .../SocketSharp/WebSocketSharpResponse.cs | 144 +++ ...MediaBrowser.Server.Implementations.csproj | 11 +- .../ServerManager/ServerManager.cs | 52 +- .../WebSocket/AlchemyServer.cs | 165 ---- .../WebSocket/AlchemyWebSocket.cs | 134 --- .../packages.config | 1 - .../ApplicationHost.cs | 12 +- 20 files changed, 1900 insertions(+), 478 deletions(-) delete mode 100644 MediaBrowser.Common/Net/IWebSocketServer.cs create mode 100644 MediaBrowser.Server.Implementations/HttpServer/SocketSharp/Extensions.cs create mode 100644 MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs create mode 100644 MediaBrowser.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs create mode 100644 MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs create mode 100644 MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs create mode 100644 MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs delete mode 100644 MediaBrowser.Server.Implementations/WebSocket/AlchemyServer.cs delete mode 100644 MediaBrowser.Server.Implementations/WebSocket/AlchemyWebSocket.cs diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 9d59843171..f1f8ebca47 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -78,7 +78,6 @@ - diff --git a/MediaBrowser.Common/Net/IServerManager.cs b/MediaBrowser.Common/Net/IServerManager.cs index 7c14f98ce9..f6ac0ab68a 100644 --- a/MediaBrowser.Common/Net/IServerManager.cs +++ b/MediaBrowser.Common/Net/IServerManager.cs @@ -10,12 +10,6 @@ namespace MediaBrowser.Common.Net /// public interface IServerManager : IDisposable { - /// - /// Gets a value indicating whether [supports web socket]. - /// - /// true if [supports web socket]; otherwise, false. - bool SupportsNativeWebSocket { get; } - /// /// Gets the web socket port number. /// @@ -28,11 +22,6 @@ namespace MediaBrowser.Common.Net /// The URL prefixes. void Start(IEnumerable urlPrefixes); - /// - /// Starts the web socket server. - /// - void StartWebSocketServer(); - /// /// Sends a message to all clients currently connected via a web socket /// diff --git a/MediaBrowser.Common/Net/IWebSocketServer.cs b/MediaBrowser.Common/Net/IWebSocketServer.cs deleted file mode 100644 index 187e03e09f..0000000000 --- a/MediaBrowser.Common/Net/IWebSocketServer.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; - -namespace MediaBrowser.Common.Net -{ - /// - /// Interface IWebSocketServer - /// - public interface IWebSocketServer : IDisposable - { - /// - /// Starts the specified port number. - /// - /// The port number. - void Start(int portNumber); - - /// - /// Stops this instance. - /// - void Stop(); - - /// - /// Occurs when [web socket connected]. - /// - event EventHandler WebSocketConnected; - - /// - /// Gets the port. - /// - /// The port. - int Port { get; } - } -} diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs index 9a3cc6cb5d..5b179d479a 100644 --- a/MediaBrowser.Controller/Net/IHttpServer.cs +++ b/MediaBrowser.Controller/Net/IHttpServer.cs @@ -21,12 +21,6 @@ namespace MediaBrowser.Controller.Net /// The URL prefixes. void StartServer(IEnumerable urlPrefixes); - /// - /// Gets a value indicating whether [supports web sockets]. - /// - /// true if [supports web sockets]; otherwise, false. - bool SupportsWebSockets { get; } - /// /// Gets the local end points. /// diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs index 68b69cdf02..62a43751db 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -6,6 +6,7 @@ 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; @@ -18,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; @@ -42,7 +42,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer private readonly ContainerAdapter _containerAdapter; - private readonly ConcurrentDictionary _localEndPoints = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); public event EventHandler WebSocketConnected; /// @@ -157,12 +156,13 @@ namespace MediaBrowser.Server.Implementations.HttpServer { HostContext.Config.HandlerFactoryPath = ListenerRequest.GetHandlerPathIfAny(UrlPrefixes.First()); - _listener = new HttpListenerServer(_logger, _threadPoolManager) - { - WebSocketHandler = WebSocketHandler, - ErrorHandler = ErrorHandler, - RequestHandler = RequestHandler - }; + _listener = NativeWebSocket.IsSupported + ? _listener = new HttpListenerServer(_logger, _threadPoolManager) + : _listener = new WebSocketSharpListener(_logger, _threadPoolManager); + + _listener.WebSocketHandler = WebSocketHandler; + _listener.ErrorHandler = ErrorHandler; + _listener.RequestHandler = RequestHandler; _listener.Start(UrlPrefixes); } @@ -175,24 +175,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer } } - /// - /// Logs the HTTP request. - /// - /// The request. - private void LogHttpRequest(HttpListenerRequest request) - { - var endpoint = request.LocalEndPoint; - - if (endpoint != null) - { - var address = endpoint.ToString(); - - _localEndPoints.GetOrAdd(address, address); - } - - LoggerUtils.LogRequest(_logger, request); - } - private void ErrorHandler(Exception ex, IRequest httpReq) { try @@ -261,6 +243,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer /// Overridable method that can be used to implement a custom hnandler /// /// The HTTP req. + /// The URL. /// Task. protected Task RequestHandler(IHttpRequest httpReq, Uri url) { @@ -310,17 +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; LoggerUtils.LogResponse(_logger, statusCode, urlString, remoteIp, duration); }, TaskContinuationOptions.None); - return task; } @@ -393,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/LoggerUtils.cs b/MediaBrowser.Server.Implementations/HttpServer/LoggerUtils.cs index 19d2f9c45e..955c4ed2d0 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/LoggerUtils.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/LoggerUtils.cs @@ -7,24 +7,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer { public static class LoggerUtils { - /// - /// Logs the request. - /// - /// The logger. - /// The request. - 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); - } - /// /// Logs the response. /// diff --git a/MediaBrowser.Server.Implementations/HttpServer/NetListener/HttpListenerServer.cs b/MediaBrowser.Server.Implementations/HttpServer/NetListener/HttpListenerServer.cs index 51f0554d73..7f766129e7 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/NetListener/HttpListenerServer.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/NetListener/HttpListenerServer.cs @@ -1,4 +1,5 @@ -using Amib.Threading; +using System.Text; +using Amib.Threading; using MediaBrowser.Common.Net; using MediaBrowser.Model.Logging; using ServiceStack; @@ -132,7 +133,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.NetListener _threadPoolManager.QueueWorkItem(() => InitTask(context)); } - public virtual void InitTask(HttpListenerContext context) + private void InitTask(HttpListenerContext context) { try { @@ -150,7 +151,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.NetListener } } - protected Task ProcessRequestAsync(HttpListenerContext context) + private Task ProcessRequestAsync(HttpListenerContext context) { var request = context.Request; @@ -235,7 +236,25 @@ namespace MediaBrowser.Server.Implementations.HttpServer.NetListener _localEndPoints.GetOrAdd(address, address); } - LoggerUtils.LogRequest(_logger, request); + LogRequest(_logger, request); + } + + /// + /// Logs the request. + /// + /// The logger. + /// The request. + 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() diff --git a/MediaBrowser.Server.Implementations/HttpServer/ResponseFilter.cs b/MediaBrowser.Server.Implementations/HttpServer/ResponseFilter.cs index ac16217090..e0a5764d5c 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; + res.SetContentLength(length); + + var listenerResponse = res.OriginalResponse as HttpListenerResponse; - 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; + 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/SocketSharp/Extensions.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/Extensions.cs new file mode 100644 index 0000000000..63d57b6be2 --- /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 0000000000..226d97b3cd --- /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 0000000000..4127892409 --- /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 + { + /// + /// The logger + /// + private readonly ILogger _logger; + + public event EventHandler Closed; + + /// + /// Gets or sets the web socket. + /// + /// The web socket. + private WebSocketSharp.WebSocket WebSocket { get; set; } + + private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + + /// + /// Initializes a new instance of the class. + /// + /// The socket. + /// The logger. + /// socket + 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); + } + } + + /// + /// Gets or sets the state. + /// + /// The state. + 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; + } + } + + /// + /// Sends the async. + /// + /// The bytes. + /// The type. + /// if set to true [end of message]. + /// The cancellation token. + /// Task. + 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(); + + WebSocket.SendAsync(Encoding.UTF8.GetString(bytes), res => completionSource.TrySetResult(true)); + + return completionSource.Task; + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + WebSocket.OnMessage -= socket_OnMessage; + WebSocket.OnClose -= socket_OnClose; + WebSocket.OnError -= socket_OnError; + + _cancellationTokenSource.Cancel(); + + WebSocket.Close(); + } + } + + /// + /// Gets or sets the receive action. + /// + /// The receive action. + public Action OnReceiveBytes { get; set; } + + /// + /// Gets or sets the on receive. + /// + /// The on receive. + public Action 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 0000000000..cf756d9f27 --- /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 _localEndPoints = new ConcurrentDictionary(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 LocalEndPoints + { + get { return _localEndPoints.Keys.ToList(); } + } + + public System.Action ErrorHandler { get; set; } + + public System.Func RequestHandler { get; set; } + + public Action WebSocketHandler { get; set; } + + public void Start(IEnumerable 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); + } + + /// + /// Logs the HTTP request. + /// + /// The request. + 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; + } + + /// + /// Logs the request. + /// + /// The logger. + /// The request. + 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 0000000000..7a5f6fbdc6 --- /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() + { + if (typeof(T) == typeof(IHttpRequest)) + throw new Exception("You don't need to use IHttpRequest.TryResolve to resolve itself"); + + if (typeof(T) == typeof(IHttpResponse)) + throw new Exception("Resolve IHttpResponse with 'Response' property instead of IHttpRequest.TryResolve"); + + return Container == null + ? HostContext.TryResolve() + : Container.TryResolve(); + } + + 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 items; + public Dictionary Items + { + get { return items ?? (items = new Dictionary()); } + } + + 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 cookies; + public IDictionary Cookies + { + get + { + if (cookies == null) + { + cookies = new Dictionary(); + 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 0000000000..2e38285128 --- /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; } + } + } +} diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index c24e9574f7..38600922b2 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -45,9 +45,6 @@ v4.5 - - ..\packages\Alchemy.2.2.1\lib\net40\Alchemy.dll - False ..\packages\Mono.Nat.1.2.20.0\lib\net40\Mono.Nat.dll @@ -154,9 +151,15 @@ + + + + + + @@ -270,8 +273,6 @@ - - diff --git a/MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs b/MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs index 5931d7718f..6f14bb3223 100644 --- a/MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs +++ b/MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs @@ -45,12 +45,6 @@ namespace MediaBrowser.Server.Implementations.ServerManager get { return _webSocketConnections; } } - /// - /// Gets or sets the external web socket server. - /// - /// The external web socket server. - private IWebSocketServer ExternalWebSocketServer { get; set; } - /// /// The _logger /// @@ -67,22 +61,13 @@ namespace MediaBrowser.Server.Implementations.ServerManager /// The configuration manager. private IServerConfigurationManager ConfigurationManager { get; set; } - /// - /// Gets a value indicating whether [supports web socket]. - /// - /// true if [supports web socket]; otherwise, false. - public bool SupportsNativeWebSocket - { - get { return HttpServer != null && HttpServer.SupportsWebSockets; } - } - /// /// Gets the web socket port number. /// /// The web socket port number. public int WebSocketPortNumber { - get { return SupportsNativeWebSocket ? ConfigurationManager.Configuration.HttpServerPortNumber : ConfigurationManager.Configuration.LegacyWebSocketPortNumber; } + get { return ConfigurationManager.Configuration.HttpServerPortNumber; } } /// @@ -128,27 +113,6 @@ namespace MediaBrowser.Server.Implementations.ServerManager ReloadHttpServer(urlPrefixes); } - public void StartWebSocketServer() - { - if (!SupportsNativeWebSocket) - { - ReloadExternalWebSocketServer(ConfigurationManager.Configuration.LegacyWebSocketPortNumber); - } - } - - /// - /// Starts the external web socket server. - /// - private void ReloadExternalWebSocketServer(int portNumber) - { - DisposeExternalWebSocketServer(); - - ExternalWebSocketServer = _applicationHost.Resolve(); - - ExternalWebSocketServer.Start(portNumber); - ExternalWebSocketServer.WebSocketConnected += HttpServer_WebSocketConnected; - } - /// /// Restarts the Http Server, or starts it if not currently running /// @@ -325,8 +289,6 @@ namespace MediaBrowser.Server.Implementations.ServerManager HttpServer.WebSocketConnected -= HttpServer_WebSocketConnected; HttpServer.Dispose(); } - - DisposeExternalWebSocketServer(); } /// @@ -350,18 +312,6 @@ namespace MediaBrowser.Server.Implementations.ServerManager } } - /// - /// Disposes the external web socket server. - /// - private void DisposeExternalWebSocketServer() - { - if (ExternalWebSocketServer != null) - { - _logger.Info("Disposing {0}", ExternalWebSocketServer.GetType().Name); - ExternalWebSocketServer.Dispose(); - } - } - /// /// Adds the web socket listeners. /// diff --git a/MediaBrowser.Server.Implementations/WebSocket/AlchemyServer.cs b/MediaBrowser.Server.Implementations/WebSocket/AlchemyServer.cs deleted file mode 100644 index 454dff4b98..0000000000 --- a/MediaBrowser.Server.Implementations/WebSocket/AlchemyServer.cs +++ /dev/null @@ -1,165 +0,0 @@ -using Alchemy; -using Alchemy.Classes; -using MediaBrowser.Common.Net; -using MediaBrowser.Model.Logging; -using System; -using System.Net; -#if __MonoCS__ -using Mono.Unix.Native; -#endif - -namespace MediaBrowser.Server.Implementations.WebSocket -{ - /// - /// Class AlchemyServer - /// - public class AlchemyServer : IWebSocketServer - { - /// - /// Occurs when [web socket connected]. - /// - public event EventHandler WebSocketConnected; - - /// - /// Gets or sets the web socket server. - /// - /// The web socket server. - private WebSocketServer WebSocketServer { get; set; } - - /// - /// The _logger - /// - private readonly ILogger _logger; - - private bool _hasStopped; - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - /// logger - public AlchemyServer(ILogger logger) - { - if (logger == null) - { - throw new ArgumentNullException("logger"); - } - _logger = logger; - } - - /// - /// Gets the port. - /// - /// The port. - public int Port { get; private set; } - - /// - /// Starts the specified port number. - /// - /// The port number. - public void Start(int portNumber) - { - _logger.Info("Starting Alchemy web socket server on port {0}", portNumber); - - try - { - WebSocketServer = new WebSocketServer(portNumber, IPAddress.Any) - { - OnConnected = OnAlchemyWebSocketClientConnected, - TimeOut = TimeSpan.FromHours(24) - }; - - #if __MonoCS__ - //Linux: port below 1024 require root or cap_net_bind_service - if (Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX) - { - if (Syscall.getuid() == 0) - { - WebSocketServer.FlashAccessPolicyEnabled = true; - } - else - { - WebSocketServer.FlashAccessPolicyEnabled = false; - } - } - #endif - WebSocketServer.Start(); - } - catch (Exception ex) - { - _logger.ErrorException("The web socket server is unable to start on port {0} due to a Socket error. This can occasionally happen when the operating system takes longer than usual to release the IP bindings from the previous session. This can take up to five minutes. Please try waiting or rebooting the system.", ex, portNumber); - - throw; - } - - Port = portNumber; - - _logger.Info("Alchemy Web Socket Server started"); - } - - /// - /// Called when [alchemy web socket client connected]. - /// - /// The context. - private void OnAlchemyWebSocketClientConnected(UserContext context) - { - if (_hasStopped) - { - return; - } - - if (WebSocketConnected != null) - { - var socket = new AlchemyWebSocket(context, _logger); - - WebSocketConnected(this, new WebSocketConnectEventArgs - { - WebSocket = socket, - Endpoint = context.ClientAddress.ToString() - }); - } - } - - /// - /// Stops this instance. - /// - public void Stop() - { - if (WebSocketServer != null) - { - WebSocketServer.Stop(); - } - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private readonly object _syncLock = new object(); - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool dispose) - { - _hasStopped = true; - - lock (_syncLock) - { - if (WebSocketServer != null) - { - _logger.Debug("Disposing alchemy server"); - - WebSocketServer.Dispose(); - WebSocketServer = null; - } - } - } - } -} diff --git a/MediaBrowser.Server.Implementations/WebSocket/AlchemyWebSocket.cs b/MediaBrowser.Server.Implementations/WebSocket/AlchemyWebSocket.cs deleted file mode 100644 index 35c5e780b8..0000000000 --- a/MediaBrowser.Server.Implementations/WebSocket/AlchemyWebSocket.cs +++ /dev/null @@ -1,134 +0,0 @@ -using Alchemy.Classes; -using MediaBrowser.Common.Events; -using MediaBrowser.Common.Net; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Net; -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Server.Implementations.WebSocket -{ - /// - /// Class AlchemyWebSocket - /// - public class AlchemyWebSocket : IWebSocket - { - /// - /// The logger - /// - private readonly ILogger _logger; - - public event EventHandler Closed; - - /// - /// Gets or sets the web socket. - /// - /// The web socket. - private UserContext UserContext { get; set; } - - /// - /// Initializes a new instance of the class. - /// - /// The context. - /// The logger. - /// context - public AlchemyWebSocket(UserContext context, ILogger logger) - { - if (context == null) - { - throw new ArgumentNullException("context"); - } - - _logger = logger; - UserContext = context; - - context.SetOnDisconnect(OnDisconnected); - context.SetOnReceive(OnReceiveContext); - - _logger.Info("Client connected from {0}", context.ClientAddress); - } - - /// - /// The _disconnected - /// - private bool _disconnected; - /// - /// Gets or sets the state. - /// - /// The state. - public WebSocketState State - { - get { return _disconnected ? WebSocketState.Closed : WebSocketState.Open; } - } - - /// - /// Called when [disconnected]. - /// - /// The context. - private void OnDisconnected(UserContext context) - { - _disconnected = true; - - EventHelper.FireEventIfNotNull(Closed, this, EventArgs.Empty, _logger); - } - - /// - /// Called when [receive]. - /// - /// The context. - private void OnReceiveContext(UserContext context) - { - if (OnReceive != null) - { - var json = context.DataFrame.ToString(); - - OnReceive(json); - } - } - - private readonly Task _cachedTask = Task.FromResult(true); - /// - /// Sends the async. - /// - /// The bytes. - /// The type. - /// if set to true [end of message]. - /// The cancellation token. - /// Task. - public Task SendAsync(byte[] bytes, WebSocketMessageType type, bool endOfMessage, CancellationToken cancellationToken) - { - UserContext.Send(bytes); - - return _cachedTask; - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public void Dispose() - { - Dispose(true); - } - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool dispose) - { - } - - /// - /// Gets or sets the receive action. - /// - /// The receive action. - public Action OnReceiveBytes { get; set; } - - /// - /// Gets or sets the on receive. - /// - /// The on receive. - public Action OnReceive { get; set; } - } -} diff --git a/MediaBrowser.Server.Implementations/packages.config b/MediaBrowser.Server.Implementations/packages.config index c22d3bec30..907500ae35 100644 --- a/MediaBrowser.Server.Implementations/packages.config +++ b/MediaBrowser.Server.Implementations/packages.config @@ -1,6 +1,5 @@  - diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index da7f8631b0..3d6865f854 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -72,7 +72,6 @@ using MediaBrowser.Server.Implementations.ServerManager; using MediaBrowser.Server.Implementations.Session; using MediaBrowser.Server.Implementations.Sync; using MediaBrowser.Server.Implementations.Themes; -using MediaBrowser.Server.Implementations.WebSocket; using MediaBrowser.ServerApplication.EntryPoints; using MediaBrowser.ServerApplication.FFMpeg; using MediaBrowser.ServerApplication.IO; @@ -517,8 +516,6 @@ namespace MediaBrowser.ServerApplication LocalizationManager = new LocalizationManager(ServerConfigurationManager, FileSystemManager, JsonSerializer); RegisterSingleInstance(LocalizationManager); - RegisterSingleInstance(() => new AlchemyServer(Logger)); - RegisterSingleInstance(() => new BdInfoExaminer()); UserDataManager = new UserDataManager(LogManager); @@ -868,8 +865,6 @@ namespace MediaBrowser.ServerApplication throw; } } - - ServerManager.StartWebSocketServer(); } /// @@ -885,11 +880,6 @@ namespace MediaBrowser.ServerApplication { NotifyPendingRestart(); } - - else if (!ServerManager.SupportsNativeWebSocket && ServerManager.WebSocketPortNumber != ServerConfigurationManager.Configuration.LegacyWebSocketPortNumber) - { - NotifyPendingRestart(); - } } /// @@ -1022,7 +1012,7 @@ namespace MediaBrowser.ServerApplication Version = ApplicationVersion.ToString(), IsNetworkDeployed = CanSelfUpdate, WebSocketPortNumber = ServerManager.WebSocketPortNumber, - SupportsNativeWebSocket = ServerManager.SupportsNativeWebSocket, + SupportsNativeWebSocket = true, FailedPluginAssemblies = FailedAssemblies.ToList(), InProgressInstallations = InstallationManager.CurrentInstallations.Select(i => i.Item1).ToList(), CompletedInstallations = InstallationManager.CompletedInstallations.ToList(),