aboutsummaryrefslogtreecommitdiff
path: root/SocketHttpListener
diff options
context:
space:
mode:
authorLuke Pulverenti <luke.pulverenti@gmail.com>2017-05-24 15:12:55 -0400
committerLuke Pulverenti <luke.pulverenti@gmail.com>2017-05-24 15:12:55 -0400
commitf07af448fa11330db93dd7ddcabac37ef9e014c7 (patch)
tree1b52a4f73d674a48258c2f14c94117b96ca4a678 /SocketHttpListener
parent27c3acb2bfde9025c33f584c759a4038020cb702 (diff)
update main projects
Diffstat (limited to 'SocketHttpListener')
-rw-r--r--SocketHttpListener/ByteOrder.cs17
-rw-r--r--SocketHttpListener/CloseEventArgs.cs90
-rw-r--r--SocketHttpListener/CloseStatusCode.cs94
-rw-r--r--SocketHttpListener/CompressionMethod.cs23
-rw-r--r--SocketHttpListener/ErrorEventArgs.cs46
-rw-r--r--SocketHttpListener/Ext.cs1083
-rw-r--r--SocketHttpListener/Fin.cs8
-rw-r--r--SocketHttpListener/HttpBase.cs104
-rw-r--r--SocketHttpListener/HttpResponse.cs161
-rw-r--r--SocketHttpListener/Mask.cs8
-rw-r--r--SocketHttpListener/MessageEventArgs.cs96
-rw-r--r--SocketHttpListener/Net/AuthenticationSchemeSelector.cs6
-rw-r--r--SocketHttpListener/Net/ChunkStream.cs405
-rw-r--r--SocketHttpListener/Net/ChunkedInputStream.cs172
-rw-r--r--SocketHttpListener/Net/CookieHelper.cs144
-rw-r--r--SocketHttpListener/Net/EndPointListener.cs391
-rw-r--r--SocketHttpListener/Net/EndPointManager.cs166
-rw-r--r--SocketHttpListener/Net/HttpConnection.cs547
-rw-r--r--SocketHttpListener/Net/HttpListener.cs293
-rw-r--r--SocketHttpListener/Net/HttpListenerBasicIdentity.cs70
-rw-r--r--SocketHttpListener/Net/HttpListenerContext.cs198
-rw-r--r--SocketHttpListener/Net/HttpListenerPrefixCollection.cs97
-rw-r--r--SocketHttpListener/Net/HttpListenerRequest.cs654
-rw-r--r--SocketHttpListener/Net/HttpListenerResponse.cs525
-rw-r--r--SocketHttpListener/Net/HttpRequestStream.Managed.cs196
-rw-r--r--SocketHttpListener/Net/HttpRequestStream.cs144
-rw-r--r--SocketHttpListener/Net/HttpStatusCode.cs321
-rw-r--r--SocketHttpListener/Net/HttpStreamAsyncResult.cs85
-rw-r--r--SocketHttpListener/Net/HttpVersion.cs16
-rw-r--r--SocketHttpListener/Net/ListenerPrefix.cs148
-rw-r--r--SocketHttpListener/Net/ResponseStream.cs400
-rw-r--r--SocketHttpListener/Net/WebHeaderCollection.cs391
-rw-r--r--SocketHttpListener/Net/WebSockets/HttpListenerWebSocketContext.cs348
-rw-r--r--SocketHttpListener/Net/WebSockets/WebSocketContext.cs183
-rw-r--r--SocketHttpListener/Opcode.cs43
-rw-r--r--SocketHttpListener/PayloadData.cs149
-rw-r--r--SocketHttpListener/Primitives/ICertificate.cs12
-rw-r--r--SocketHttpListener/Primitives/IStreamFactory.cs18
-rw-r--r--SocketHttpListener/Primitives/ITextEncoding.cs17
-rw-r--r--SocketHttpListener/Properties/AssemblyInfo.cs34
-rw-r--r--SocketHttpListener/Rsv.cs8
-rw-r--r--SocketHttpListener/SocketHttpListener.csproj111
-rw-r--r--SocketHttpListener/WebSocket.cs887
-rw-r--r--SocketHttpListener/WebSocketException.cs60
-rw-r--r--SocketHttpListener/WebSocketFrame.cs578
-rw-r--r--SocketHttpListener/WebSocketState.cs35
46 files changed, 9582 insertions, 0 deletions
diff --git a/SocketHttpListener/ByteOrder.cs b/SocketHttpListener/ByteOrder.cs
new file mode 100644
index 000000000..f5db52fd7
--- /dev/null
+++ b/SocketHttpListener/ByteOrder.cs
@@ -0,0 +1,17 @@
+namespace SocketHttpListener
+{
+ /// <summary>
+ /// Contains the values that indicate whether the byte order is a Little-endian or Big-endian.
+ /// </summary>
+ public enum ByteOrder : byte
+ {
+ /// <summary>
+ /// Indicates a Little-endian.
+ /// </summary>
+ Little,
+ /// <summary>
+ /// Indicates a Big-endian.
+ /// </summary>
+ Big
+ }
+}
diff --git a/SocketHttpListener/CloseEventArgs.cs b/SocketHttpListener/CloseEventArgs.cs
new file mode 100644
index 000000000..b1bb4b196
--- /dev/null
+++ b/SocketHttpListener/CloseEventArgs.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Text;
+
+namespace SocketHttpListener
+{
+ /// <summary>
+ /// Contains the event data associated with a <see cref="WebSocket.OnClose"/> event.
+ /// </summary>
+ /// <remarks>
+ /// A <see cref="WebSocket.OnClose"/> event occurs when the WebSocket connection has been closed.
+ /// If you would like to get the reason for the close, you should access the <see cref="Code"/> or
+ /// <see cref="Reason"/> property.
+ /// </remarks>
+ public class CloseEventArgs : EventArgs
+ {
+ #region Private Fields
+
+ private bool _clean;
+ private ushort _code;
+ private string _reason;
+
+ #endregion
+
+ #region Internal Constructors
+
+ internal CloseEventArgs (PayloadData payload)
+ {
+ var data = payload.ApplicationData;
+ var len = data.Length;
+ _code = len > 1
+ ? data.SubArray (0, 2).ToUInt16 (ByteOrder.Big)
+ : (ushort) CloseStatusCode.NoStatusCode;
+
+ _reason = len > 2
+ ? GetUtf8String(data.SubArray (2, len - 2))
+ : String.Empty;
+ }
+
+ private string GetUtf8String(byte[] bytes)
+ {
+ return Encoding.UTF8.GetString(bytes, 0, bytes.Length);
+ }
+
+ #endregion
+
+ #region Public Properties
+
+ /// <summary>
+ /// Gets the status code for the close.
+ /// </summary>
+ /// <value>
+ /// A <see cref="ushort"/> that represents the status code for the close if any.
+ /// </value>
+ public ushort Code {
+ get {
+ return _code;
+ }
+ }
+
+ /// <summary>
+ /// Gets the reason for the close.
+ /// </summary>
+ /// <value>
+ /// A <see cref="string"/> that represents the reason for the close if any.
+ /// </value>
+ public string Reason {
+ get {
+ return _reason;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the WebSocket connection has been closed cleanly.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if the WebSocket connection has been closed cleanly; otherwise, <c>false</c>.
+ /// </value>
+ public bool WasClean {
+ get {
+ return _clean;
+ }
+
+ internal set {
+ _clean = value;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/SocketHttpListener/CloseStatusCode.cs b/SocketHttpListener/CloseStatusCode.cs
new file mode 100644
index 000000000..62a268bce
--- /dev/null
+++ b/SocketHttpListener/CloseStatusCode.cs
@@ -0,0 +1,94 @@
+namespace SocketHttpListener
+{
+ /// <summary>
+ /// Contains the values of the status code for the WebSocket connection close.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// The values of the status code are defined in
+ /// <see href="http://tools.ietf.org/html/rfc6455#section-7.4">Section 7.4</see>
+ /// of RFC 6455.
+ /// </para>
+ /// <para>
+ /// "Reserved value" must not be set as a status code in a close control frame
+ /// by an endpoint. It's designated for use in applications expecting a status
+ /// code to indicate that the connection was closed due to the system grounds.
+ /// </para>
+ /// </remarks>
+ public enum CloseStatusCode : ushort
+ {
+ /// <summary>
+ /// Equivalent to close status 1000.
+ /// Indicates a normal close.
+ /// </summary>
+ Normal = 1000,
+ /// <summary>
+ /// Equivalent to close status 1001.
+ /// Indicates that an endpoint is going away.
+ /// </summary>
+ Away = 1001,
+ /// <summary>
+ /// Equivalent to close status 1002.
+ /// Indicates that an endpoint is terminating the connection due to a protocol error.
+ /// </summary>
+ ProtocolError = 1002,
+ /// <summary>
+ /// Equivalent to close status 1003.
+ /// Indicates that an endpoint is terminating the connection because it has received
+ /// an unacceptable type message.
+ /// </summary>
+ IncorrectData = 1003,
+ /// <summary>
+ /// Equivalent to close status 1004.
+ /// Still undefined. Reserved value.
+ /// </summary>
+ Undefined = 1004,
+ /// <summary>
+ /// Equivalent to close status 1005.
+ /// Indicates that no status code was actually present. Reserved value.
+ /// </summary>
+ NoStatusCode = 1005,
+ /// <summary>
+ /// Equivalent to close status 1006.
+ /// Indicates that the connection was closed abnormally. Reserved value.
+ /// </summary>
+ Abnormal = 1006,
+ /// <summary>
+ /// Equivalent to close status 1007.
+ /// Indicates that an endpoint is terminating the connection because it has received
+ /// a message that contains a data that isn't consistent with the type of the message.
+ /// </summary>
+ InconsistentData = 1007,
+ /// <summary>
+ /// Equivalent to close status 1008.
+ /// Indicates that an endpoint is terminating the connection because it has received
+ /// a message that violates its policy.
+ /// </summary>
+ PolicyViolation = 1008,
+ /// <summary>
+ /// Equivalent to close status 1009.
+ /// Indicates that an endpoint is terminating the connection because it has received
+ /// a message that is too big to process.
+ /// </summary>
+ TooBig = 1009,
+ /// <summary>
+ /// Equivalent to close status 1010.
+ /// Indicates that the client is terminating the connection because it has expected
+ /// the server to negotiate one or more extension, but the server didn't return them
+ /// in the handshake response.
+ /// </summary>
+ IgnoreExtension = 1010,
+ /// <summary>
+ /// Equivalent to close status 1011.
+ /// Indicates that the server is terminating the connection because it has encountered
+ /// an unexpected condition that prevented it from fulfilling the request.
+ /// </summary>
+ ServerError = 1011,
+ /// <summary>
+ /// Equivalent to close status 1015.
+ /// Indicates that the connection was closed due to a failure to perform a TLS handshake.
+ /// Reserved value.
+ /// </summary>
+ TlsHandshakeFailure = 1015
+ }
+}
diff --git a/SocketHttpListener/CompressionMethod.cs b/SocketHttpListener/CompressionMethod.cs
new file mode 100644
index 000000000..36a48d94c
--- /dev/null
+++ b/SocketHttpListener/CompressionMethod.cs
@@ -0,0 +1,23 @@
+namespace SocketHttpListener
+{
+ /// <summary>
+ /// Contains the values of the compression method used to compress the message on the WebSocket
+ /// connection.
+ /// </summary>
+ /// <remarks>
+ /// The values of the compression method are defined in
+ /// <see href="http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-09">Compression
+ /// Extensions for WebSocket</see>.
+ /// </remarks>
+ public enum CompressionMethod : byte
+ {
+ /// <summary>
+ /// Indicates non compression.
+ /// </summary>
+ None,
+ /// <summary>
+ /// Indicates using DEFLATE.
+ /// </summary>
+ Deflate
+ }
+}
diff --git a/SocketHttpListener/ErrorEventArgs.cs b/SocketHttpListener/ErrorEventArgs.cs
new file mode 100644
index 000000000..bf1d6fc95
--- /dev/null
+++ b/SocketHttpListener/ErrorEventArgs.cs
@@ -0,0 +1,46 @@
+using System;
+
+namespace SocketHttpListener
+{
+ /// <summary>
+ /// Contains the event data associated with a <see cref="WebSocket.OnError"/> event.
+ /// </summary>
+ /// <remarks>
+ /// A <see cref="WebSocket.OnError"/> event occurs when the <see cref="WebSocket"/> gets an error.
+ /// If you would like to get the error message, you should access the <see cref="Message"/>
+ /// property.
+ /// </remarks>
+ public class ErrorEventArgs : EventArgs
+ {
+ #region Private Fields
+
+ private string _message;
+
+ #endregion
+
+ #region Internal Constructors
+
+ internal ErrorEventArgs (string message)
+ {
+ _message = message;
+ }
+
+ #endregion
+
+ #region Public Properties
+
+ /// <summary>
+ /// Gets the error message.
+ /// </summary>
+ /// <value>
+ /// A <see cref="string"/> that represents the error message.
+ /// </value>
+ public string Message {
+ get {
+ return _message;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/SocketHttpListener/Ext.cs b/SocketHttpListener/Ext.cs
new file mode 100644
index 000000000..87f0887ed
--- /dev/null
+++ b/SocketHttpListener/Ext.cs
@@ -0,0 +1,1083 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.IO;
+using System.IO.Compression;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Services;
+using SocketHttpListener.Net;
+using HttpListenerResponse = SocketHttpListener.Net.HttpListenerResponse;
+using HttpStatusCode = SocketHttpListener.Net.HttpStatusCode;
+
+namespace SocketHttpListener
+{
+ /// <summary>
+ /// Provides a set of static methods for the websocket-sharp.
+ /// </summary>
+ public static class Ext
+ {
+ #region Private Const Fields
+
+ private const string _tspecials = "()<>@,;:\\\"/[]?={} \t";
+
+ #endregion
+
+ #region Private Methods
+
+ private static MemoryStream compress(this Stream stream)
+ {
+ var output = new MemoryStream();
+ if (stream.Length == 0)
+ return output;
+
+ stream.Position = 0;
+ using (var ds = new DeflateStream(output, CompressionMode.Compress, true))
+ {
+ stream.CopyTo(ds);
+ //ds.Close(); // "BFINAL" set to 1.
+ output.Position = 0;
+
+ return output;
+ }
+ }
+
+ private static byte[] decompress(this byte[] value)
+ {
+ if (value.Length == 0)
+ return value;
+
+ using (var input = new MemoryStream(value))
+ {
+ return input.decompressToArray();
+ }
+ }
+
+ private static MemoryStream decompress(this Stream stream)
+ {
+ var output = new MemoryStream();
+ if (stream.Length == 0)
+ return output;
+
+ stream.Position = 0;
+ using (var ds = new DeflateStream(stream, CompressionMode.Decompress, true))
+ {
+ ds.CopyTo(output, true);
+ return output;
+ }
+ }
+
+ private static byte[] decompressToArray(this Stream stream)
+ {
+ using (var decomp = stream.decompress())
+ {
+ return decomp.ToArray();
+ }
+ }
+
+ private static byte[] readBytes(this Stream stream, byte[] buffer, int offset, int length)
+ {
+ var len = stream.Read(buffer, offset, length);
+ if (len < 1)
+ return buffer.SubArray(0, offset);
+
+ var tmp = 0;
+ while (len < length)
+ {
+ tmp = stream.Read(buffer, offset + len, length - len);
+ if (tmp < 1)
+ break;
+
+ len += tmp;
+ }
+
+ return len < length
+ ? buffer.SubArray(0, offset + len)
+ : buffer;
+ }
+
+ private static bool readBytes(
+ this Stream stream, byte[] buffer, int offset, int length, Stream dest)
+ {
+ var bytes = stream.readBytes(buffer, offset, length);
+ var len = bytes.Length;
+ dest.Write(bytes, 0, len);
+
+ return len == offset + length;
+ }
+
+ #endregion
+
+ #region Internal Methods
+
+ internal static byte[] Append(this ushort code, string reason)
+ {
+ using (var buffer = new MemoryStream())
+ {
+ var tmp = code.ToByteArrayInternally(ByteOrder.Big);
+ buffer.Write(tmp, 0, 2);
+ if (reason != null && reason.Length > 0)
+ {
+ tmp = Encoding.UTF8.GetBytes(reason);
+ buffer.Write(tmp, 0, tmp.Length);
+ }
+
+ return buffer.ToArray();
+ }
+ }
+
+ internal static string CheckIfClosable(this WebSocketState state)
+ {
+ return state == WebSocketState.Closing
+ ? "While closing the WebSocket connection."
+ : state == WebSocketState.Closed
+ ? "The WebSocket connection has already been closed."
+ : null;
+ }
+
+ internal static string CheckIfOpen(this WebSocketState state)
+ {
+ return state == WebSocketState.Connecting
+ ? "A WebSocket connection isn't established."
+ : state == WebSocketState.Closing
+ ? "While closing the WebSocket connection."
+ : state == WebSocketState.Closed
+ ? "The WebSocket connection has already been closed."
+ : null;
+ }
+
+ internal static string CheckIfValidControlData(this byte[] data, string paramName)
+ {
+ return data.Length > 125
+ ? String.Format("'{0}' length must be less.", paramName)
+ : null;
+ }
+
+ internal static string CheckIfValidSendData(this byte[] data)
+ {
+ return data == null
+ ? "'data' must not be null."
+ : null;
+ }
+
+ internal static string CheckIfValidSendData(this string data)
+ {
+ return data == null
+ ? "'data' must not be null."
+ : null;
+ }
+
+ internal static Stream Compress(this Stream stream, CompressionMethod method)
+ {
+ return method == CompressionMethod.Deflate
+ ? stream.compress()
+ : stream;
+ }
+
+ internal static bool Contains<T>(this IEnumerable<T> source, Func<T, bool> condition)
+ {
+ foreach (T elm in source)
+ if (condition(elm))
+ return true;
+
+ return false;
+ }
+
+ internal static void CopyTo(this Stream src, Stream dest, bool setDefaultPosition)
+ {
+ var readLen = 0;
+ var bufferLen = 256;
+ var buffer = new byte[bufferLen];
+ while ((readLen = src.Read(buffer, 0, bufferLen)) > 0)
+ {
+ dest.Write(buffer, 0, readLen);
+ }
+
+ if (setDefaultPosition)
+ dest.Position = 0;
+ }
+
+ internal static byte[] Decompress(this byte[] value, CompressionMethod method)
+ {
+ return method == CompressionMethod.Deflate
+ ? value.decompress()
+ : value;
+ }
+
+ internal static byte[] DecompressToArray(this Stream stream, CompressionMethod method)
+ {
+ return method == CompressionMethod.Deflate
+ ? stream.decompressToArray()
+ : stream.ToByteArray();
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="int"/> equals the specified <see cref="char"/>,
+ /// and invokes the specified Action&lt;int&gt; delegate at the same time.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if <paramref name="value"/> equals <paramref name="c"/>;
+ /// otherwise, <c>false</c>.
+ /// </returns>
+ /// <param name="value">
+ /// An <see cref="int"/> to compare.
+ /// </param>
+ /// <param name="c">
+ /// A <see cref="char"/> to compare.
+ /// </param>
+ /// <param name="action">
+ /// An Action&lt;int&gt; delegate that references the method(s) called at
+ /// the same time as comparing. An <see cref="int"/> parameter to pass to
+ /// the method(s) is <paramref name="value"/>.
+ /// </param>
+ /// <exception cref="ArgumentOutOfRangeException">
+ /// <paramref name="value"/> isn't between 0 and 255.
+ /// </exception>
+ internal static bool EqualsWith(this int value, char c, Action<int> action)
+ {
+ if (value < 0 || value > 255)
+ throw new ArgumentOutOfRangeException("value");
+
+ action(value);
+ return value == c - 0;
+ }
+
+ internal static string GetMessage(this CloseStatusCode code)
+ {
+ return code == CloseStatusCode.ProtocolError
+ ? "A WebSocket protocol error has occurred."
+ : code == CloseStatusCode.IncorrectData
+ ? "An incorrect data has been received."
+ : code == CloseStatusCode.Abnormal
+ ? "An exception has occurred."
+ : code == CloseStatusCode.InconsistentData
+ ? "An inconsistent data has been received."
+ : code == CloseStatusCode.PolicyViolation
+ ? "A policy violation has occurred."
+ : code == CloseStatusCode.TooBig
+ ? "A too big data has been received."
+ : code == CloseStatusCode.IgnoreExtension
+ ? "WebSocket client did not receive expected extension(s)."
+ : code == CloseStatusCode.ServerError
+ ? "WebSocket server got an internal error."
+ : code == CloseStatusCode.TlsHandshakeFailure
+ ? "An error has occurred while handshaking."
+ : String.Empty;
+ }
+
+ internal static string GetNameInternal(this string nameAndValue, string separator)
+ {
+ var i = nameAndValue.IndexOf(separator);
+ return i > 0
+ ? nameAndValue.Substring(0, i).Trim()
+ : null;
+ }
+
+ internal static string GetValueInternal(this string nameAndValue, string separator)
+ {
+ var i = nameAndValue.IndexOf(separator);
+ return i >= 0 && i < nameAndValue.Length - 1
+ ? nameAndValue.Substring(i + 1).Trim()
+ : null;
+ }
+
+ internal static bool IsCompressionExtension(this string value, CompressionMethod method)
+ {
+ return value.StartsWith(method.ToExtensionString());
+ }
+
+ internal static bool IsPortNumber(this int value)
+ {
+ return value > 0 && value < 65536;
+ }
+
+ internal static bool IsReserved(this ushort code)
+ {
+ return code == (ushort)CloseStatusCode.Undefined ||
+ code == (ushort)CloseStatusCode.NoStatusCode ||
+ code == (ushort)CloseStatusCode.Abnormal ||
+ code == (ushort)CloseStatusCode.TlsHandshakeFailure;
+ }
+
+ internal static bool IsReserved(this CloseStatusCode code)
+ {
+ return code == CloseStatusCode.Undefined ||
+ code == CloseStatusCode.NoStatusCode ||
+ code == CloseStatusCode.Abnormal ||
+ code == CloseStatusCode.TlsHandshakeFailure;
+ }
+
+ internal static bool IsText(this string value)
+ {
+ var len = value.Length;
+ for (var i = 0; i < len; i++)
+ {
+ char c = value[i];
+ if (c < 0x20 && !"\r\n\t".Contains(c))
+ return false;
+
+ if (c == 0x7f)
+ return false;
+
+ if (c == '\n' && ++i < len)
+ {
+ c = value[i];
+ if (!" \t".Contains(c))
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ internal static bool IsToken(this string value)
+ {
+ foreach (char c in value)
+ if (c < 0x20 || c >= 0x7f || _tspecials.Contains(c))
+ return false;
+
+ return true;
+ }
+
+ internal static string Quote(this string value)
+ {
+ return value.IsToken()
+ ? value
+ : String.Format("\"{0}\"", value.Replace("\"", "\\\""));
+ }
+
+ internal static byte[] ReadBytes(this Stream stream, int length)
+ {
+ return stream.readBytes(new byte[length], 0, length);
+ }
+
+ internal static byte[] ReadBytes(this Stream stream, long length, int bufferLength)
+ {
+ using (var result = new MemoryStream())
+ {
+ var count = length / bufferLength;
+ var rem = (int)(length % bufferLength);
+
+ var buffer = new byte[bufferLength];
+ var end = false;
+ for (long i = 0; i < count; i++)
+ {
+ if (!stream.readBytes(buffer, 0, bufferLength, result))
+ {
+ end = true;
+ break;
+ }
+ }
+
+ if (!end && rem > 0)
+ stream.readBytes(new byte[rem], 0, rem, result);
+
+ return result.ToArray();
+ }
+ }
+
+ internal static async Task<byte[]> ReadBytesAsync(this Stream stream, int length)
+ {
+ var buffer = new byte[length];
+
+ var len = await stream.ReadAsync(buffer, 0, length).ConfigureAwait(false);
+ var bytes = len < 1
+ ? new byte[0]
+ : len < length
+ ? stream.readBytes(buffer, len, length - len)
+ : buffer;
+
+ return bytes;
+ }
+
+ internal static string RemovePrefix(this string value, params string[] prefixes)
+ {
+ var i = 0;
+ foreach (var prefix in prefixes)
+ {
+ if (value.StartsWith(prefix))
+ {
+ i = prefix.Length;
+ break;
+ }
+ }
+
+ return i > 0
+ ? value.Substring(i)
+ : value;
+ }
+
+ internal static T[] Reverse<T>(this T[] array)
+ {
+ var len = array.Length;
+ T[] reverse = new T[len];
+
+ var end = len - 1;
+ for (var i = 0; i <= end; i++)
+ reverse[i] = array[end - i];
+
+ return reverse;
+ }
+
+ internal static IEnumerable<string> SplitHeaderValue(
+ this string value, params char[] separator)
+ {
+ var len = value.Length;
+ var separators = new string(separator);
+
+ var buffer = new StringBuilder(32);
+ var quoted = false;
+ var escaped = false;
+
+ char c;
+ for (var i = 0; i < len; i++)
+ {
+ c = value[i];
+ if (c == '"')
+ {
+ if (escaped)
+ escaped = !escaped;
+ else
+ quoted = !quoted;
+ }
+ else if (c == '\\')
+ {
+ if (i < len - 1 && value[i + 1] == '"')
+ escaped = true;
+ }
+ else if (separators.Contains(c))
+ {
+ if (!quoted)
+ {
+ yield return buffer.ToString();
+ buffer.Length = 0;
+
+ continue;
+ }
+ }
+ else {
+ }
+
+ buffer.Append(c);
+ }
+
+ if (buffer.Length > 0)
+ yield return buffer.ToString();
+ }
+
+ internal static byte[] ToByteArray(this Stream stream)
+ {
+ using (var output = new MemoryStream())
+ {
+ stream.Position = 0;
+ stream.CopyTo(output);
+
+ return output.ToArray();
+ }
+ }
+
+ internal static byte[] ToByteArrayInternally(this ushort value, ByteOrder order)
+ {
+ var bytes = BitConverter.GetBytes(value);
+ if (!order.IsHostOrder())
+ Array.Reverse(bytes);
+
+ return bytes;
+ }
+
+ internal static byte[] ToByteArrayInternally(this ulong value, ByteOrder order)
+ {
+ var bytes = BitConverter.GetBytes(value);
+ if (!order.IsHostOrder())
+ Array.Reverse(bytes);
+
+ return bytes;
+ }
+
+ internal static string ToExtensionString(
+ this CompressionMethod method, params string[] parameters)
+ {
+ if (method == CompressionMethod.None)
+ return String.Empty;
+
+ var m = String.Format("permessage-{0}", method.ToString().ToLower());
+ if (parameters == null || parameters.Length == 0)
+ return m;
+
+ return String.Format("{0}; {1}", m, parameters.ToString("; "));
+ }
+
+ internal static List<TSource> ToList<TSource>(this IEnumerable<TSource> source)
+ {
+ return new List<TSource>(source);
+ }
+
+ internal static ushort ToUInt16(this byte[] src, ByteOrder srcOrder)
+ {
+ return BitConverter.ToUInt16(src.ToHostOrder(srcOrder), 0);
+ }
+
+ internal static ulong ToUInt64(this byte[] src, ByteOrder srcOrder)
+ {
+ return BitConverter.ToUInt64(src.ToHostOrder(srcOrder), 0);
+ }
+
+ internal static string TrimEndSlash(this string value)
+ {
+ value = value.TrimEnd('/');
+ return value.Length > 0
+ ? value
+ : "/";
+ }
+
+ internal static string Unquote(this string value)
+ {
+ var start = value.IndexOf('\"');
+ var end = value.LastIndexOf('\"');
+ if (start < end)
+ value = value.Substring(start + 1, end - start - 1).Replace("\\\"", "\"");
+
+ return value.Trim();
+ }
+
+ internal static void WriteBytes(this Stream stream, byte[] value)
+ {
+ using (var src = new MemoryStream(value))
+ {
+ src.CopyTo(stream);
+ }
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ /// <summary>
+ /// Determines whether the specified <see cref="string"/> contains any of characters
+ /// in the specified array of <see cref="char"/>.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if <paramref name="value"/> contains any of <paramref name="chars"/>;
+ /// otherwise, <c>false</c>.
+ /// </returns>
+ /// <param name="value">
+ /// A <see cref="string"/> to test.
+ /// </param>
+ /// <param name="chars">
+ /// An array of <see cref="char"/> that contains characters to find.
+ /// </param>
+ public static bool Contains(this string value, params char[] chars)
+ {
+ return chars == null || chars.Length == 0
+ ? true
+ : value == null || value.Length == 0
+ ? false
+ : value.IndexOfAny(chars) != -1;
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="QueryParamCollection"/> contains the entry
+ /// with the specified <paramref name="name"/>.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if <paramref name="collection"/> contains the entry
+ /// with <paramref name="name"/>; otherwise, <c>false</c>.
+ /// </returns>
+ /// <param name="collection">
+ /// A <see cref="QueryParamCollection"/> to test.
+ /// </param>
+ /// <param name="name">
+ /// A <see cref="string"/> that represents the key of the entry to find.
+ /// </param>
+ public static bool Contains(this QueryParamCollection collection, string name)
+ {
+ return collection == null || collection.Count == 0
+ ? false
+ : collection[name] != null;
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="QueryParamCollection"/> contains the entry
+ /// with the specified both <paramref name="name"/> and <paramref name="value"/>.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if <paramref name="collection"/> contains the entry
+ /// with both <paramref name="name"/> and <paramref name="value"/>;
+ /// otherwise, <c>false</c>.
+ /// </returns>
+ /// <param name="collection">
+ /// A <see cref="QueryParamCollection"/> to test.
+ /// </param>
+ /// <param name="name">
+ /// A <see cref="string"/> that represents the key of the entry to find.
+ /// </param>
+ /// <param name="value">
+ /// A <see cref="string"/> that represents the value of the entry to find.
+ /// </param>
+ public static bool Contains(this QueryParamCollection collection, string name, string value)
+ {
+ if (collection == null || collection.Count == 0)
+ return false;
+
+ var values = collection[name];
+ if (values == null)
+ return false;
+
+ foreach (var v in values.Split(','))
+ if (v.Trim().Equals(value, StringComparison.OrdinalIgnoreCase))
+ return true;
+
+ return false;
+ }
+
+ /// <summary>
+ /// Emits the specified <see cref="EventHandler"/> delegate if it isn't <see langword="null"/>.
+ /// </summary>
+ /// <param name="eventHandler">
+ /// A <see cref="EventHandler"/> to emit.
+ /// </param>
+ /// <param name="sender">
+ /// An <see cref="object"/> from which emits this <paramref name="eventHandler"/>.
+ /// </param>
+ /// <param name="e">
+ /// A <see cref="EventArgs"/> that contains no event data.
+ /// </param>
+ public static void Emit(this EventHandler eventHandler, object sender, EventArgs e)
+ {
+ if (eventHandler != null)
+ eventHandler(sender, e);
+ }
+
+ /// <summary>
+ /// Emits the specified <c>EventHandler&lt;TEventArgs&gt;</c> delegate
+ /// if it isn't <see langword="null"/>.
+ /// </summary>
+ /// <param name="eventHandler">
+ /// An <c>EventHandler&lt;TEventArgs&gt;</c> to emit.
+ /// </param>
+ /// <param name="sender">
+ /// An <see cref="object"/> from which emits this <paramref name="eventHandler"/>.
+ /// </param>
+ /// <param name="e">
+ /// A <c>TEventArgs</c> that represents the event data.
+ /// </param>
+ /// <typeparam name="TEventArgs">
+ /// The type of the event data generated by the event.
+ /// </typeparam>
+ public static void Emit<TEventArgs>(
+ this EventHandler<TEventArgs> eventHandler, object sender, TEventArgs e)
+ where TEventArgs : EventArgs
+ {
+ if (eventHandler != null)
+ eventHandler(sender, e);
+ }
+
+ /// <summary>
+ /// Gets the collection of the HTTP cookies from the specified HTTP <paramref name="headers"/>.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="CookieCollection"/> that receives a collection of the HTTP cookies.
+ /// </returns>
+ /// <param name="headers">
+ /// A <see cref="QueryParamCollection"/> that contains a collection of the HTTP headers.
+ /// </param>
+ /// <param name="response">
+ /// <c>true</c> if <paramref name="headers"/> is a collection of the response headers;
+ /// otherwise, <c>false</c>.
+ /// </param>
+ public static CookieCollection GetCookies(this QueryParamCollection headers, bool response)
+ {
+ var name = response ? "Set-Cookie" : "Cookie";
+ return headers == null || !headers.Contains(name)
+ ? new CookieCollection()
+ : CookieHelper.Parse(headers[name], response);
+ }
+
+ /// <summary>
+ /// Gets the description of the specified HTTP status <paramref name="code"/>.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string"/> that represents the description of the HTTP status code.
+ /// </returns>
+ /// <param name="code">
+ /// One of <see cref="HttpStatusCode"/> enum values, indicates the HTTP status codes.
+ /// </param>
+ public static string GetDescription(this HttpStatusCode code)
+ {
+ return ((int)code).GetStatusDescription();
+ }
+
+ /// <summary>
+ /// Gets the name from the specified <see cref="string"/> that contains a pair of name and
+ /// value separated by a separator string.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string"/> that represents the name if any; otherwise, <c>null</c>.
+ /// </returns>
+ /// <param name="nameAndValue">
+ /// A <see cref="string"/> that contains a pair of name and value separated by a separator
+ /// string.
+ /// </param>
+ /// <param name="separator">
+ /// A <see cref="string"/> that represents a separator string.
+ /// </param>
+ public static string GetName(this string nameAndValue, string separator)
+ {
+ return (nameAndValue != null && nameAndValue.Length > 0) &&
+ (separator != null && separator.Length > 0)
+ ? nameAndValue.GetNameInternal(separator)
+ : null;
+ }
+
+ /// <summary>
+ /// Gets the name and value from the specified <see cref="string"/> that contains a pair of
+ /// name and value separated by a separator string.
+ /// </summary>
+ /// <returns>
+ /// A <c>KeyValuePair&lt;string, string&gt;</c> that represents the name and value if any.
+ /// </returns>
+ /// <param name="nameAndValue">
+ /// A <see cref="string"/> that contains a pair of name and value separated by a separator
+ /// string.
+ /// </param>
+ /// <param name="separator">
+ /// A <see cref="string"/> that represents a separator string.
+ /// </param>
+ public static KeyValuePair<string, string> GetNameAndValue(
+ this string nameAndValue, string separator)
+ {
+ var name = nameAndValue.GetName(separator);
+ var value = nameAndValue.GetValue(separator);
+ return name != null
+ ? new KeyValuePair<string, string>(name, value)
+ : new KeyValuePair<string, string>(null, null);
+ }
+
+ /// <summary>
+ /// Gets the description of the specified HTTP status <paramref name="code"/>.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string"/> that represents the description of the HTTP status code.
+ /// </returns>
+ /// <param name="code">
+ /// An <see cref="int"/> that represents the HTTP status code.
+ /// </param>
+ public static string GetStatusDescription(this int code)
+ {
+ switch (code)
+ {
+ case 100: return "Continue";
+ case 101: return "Switching Protocols";
+ case 102: return "Processing";
+ case 200: return "OK";
+ case 201: return "Created";
+ case 202: return "Accepted";
+ case 203: return "Non-Authoritative Information";
+ case 204: return "No Content";
+ case 205: return "Reset Content";
+ case 206: return "Partial Content";
+ case 207: return "Multi-Status";
+ case 300: return "Multiple Choices";
+ case 301: return "Moved Permanently";
+ case 302: return "Found";
+ case 303: return "See Other";
+ case 304: return "Not Modified";
+ case 305: return "Use Proxy";
+ case 307: return "Temporary Redirect";
+ case 400: return "Bad Request";
+ case 401: return "Unauthorized";
+ case 402: return "Payment Required";
+ case 403: return "Forbidden";
+ case 404: return "Not Found";
+ case 405: return "Method Not Allowed";
+ case 406: return "Not Acceptable";
+ case 407: return "Proxy Authentication Required";
+ case 408: return "Request Timeout";
+ case 409: return "Conflict";
+ case 410: return "Gone";
+ case 411: return "Length Required";
+ case 412: return "Precondition Failed";
+ case 413: return "Request Entity Too Large";
+ case 414: return "Request-Uri Too Long";
+ case 415: return "Unsupported Media Type";
+ case 416: return "Requested Range Not Satisfiable";
+ case 417: return "Expectation Failed";
+ case 422: return "Unprocessable Entity";
+ case 423: return "Locked";
+ case 424: return "Failed Dependency";
+ case 500: return "Internal Server Error";
+ case 501: return "Not Implemented";
+ case 502: return "Bad Gateway";
+ case 503: return "Service Unavailable";
+ case 504: return "Gateway Timeout";
+ case 505: return "Http Version Not Supported";
+ case 507: return "Insufficient Storage";
+ }
+
+ return String.Empty;
+ }
+
+ /// <summary>
+ /// Gets the value from the specified <see cref="string"/> that contains a pair of name and
+ /// value separated by a separator string.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string"/> that represents the value if any; otherwise, <c>null</c>.
+ /// </returns>
+ /// <param name="nameAndValue">
+ /// A <see cref="string"/> that contains a pair of name and value separated by a separator
+ /// string.
+ /// </param>
+ /// <param name="separator">
+ /// A <see cref="string"/> that represents a separator string.
+ /// </param>
+ public static string GetValue(this string nameAndValue, string separator)
+ {
+ return (nameAndValue != null && nameAndValue.Length > 0) &&
+ (separator != null && separator.Length > 0)
+ ? nameAndValue.GetValueInternal(separator)
+ : null;
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="ByteOrder"/> is host
+ /// (this computer architecture) byte order.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if <paramref name="order"/> is host byte order;
+ /// otherwise, <c>false</c>.
+ /// </returns>
+ /// <param name="order">
+ /// One of the <see cref="ByteOrder"/> enum values, to test.
+ /// </param>
+ public static bool IsHostOrder(this ByteOrder order)
+ {
+ // true : !(true ^ true) or !(false ^ false)
+ // false: !(true ^ false) or !(false ^ true)
+ return !(BitConverter.IsLittleEndian ^ (order == ByteOrder.Little));
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="string"/> is a predefined scheme.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if <paramref name="value"/> is a predefined scheme; otherwise, <c>false</c>.
+ /// </returns>
+ /// <param name="value">
+ /// A <see cref="string"/> to test.
+ /// </param>
+ public static bool IsPredefinedScheme(this string value)
+ {
+ if (value == null || value.Length < 2)
+ return false;
+
+ var c = value[0];
+ if (c == 'h')
+ return value == "http" || value == "https";
+
+ if (c == 'w')
+ return value == "ws" || value == "wss";
+
+ if (c == 'f')
+ return value == "file" || value == "ftp";
+
+ if (c == 'n')
+ {
+ c = value[1];
+ return c == 'e'
+ ? value == "news" || value == "net.pipe" || value == "net.tcp"
+ : value == "nntp";
+ }
+
+ return (c == 'g' && value == "gopher") || (c == 'm' && value == "mailto");
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="string"/> is a URI string.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if <paramref name="value"/> may be a URI string; otherwise, <c>false</c>.
+ /// </returns>
+ /// <param name="value">
+ /// A <see cref="string"/> to test.
+ /// </param>
+ public static bool MaybeUri(this string value)
+ {
+ if (value == null || value.Length == 0)
+ return false;
+
+ var i = value.IndexOf(':');
+ if (i == -1)
+ return false;
+
+ if (i >= 10)
+ return false;
+
+ return value.Substring(0, i).IsPredefinedScheme();
+ }
+
+ /// <summary>
+ /// Retrieves a sub-array from the specified <paramref name="array"/>.
+ /// A sub-array starts at the specified element position.
+ /// </summary>
+ /// <returns>
+ /// An array of T that receives a sub-array, or an empty array of T if any problems
+ /// with the parameters.
+ /// </returns>
+ /// <param name="array">
+ /// An array of T that contains the data to retrieve a sub-array.
+ /// </param>
+ /// <param name="startIndex">
+ /// An <see cref="int"/> that contains the zero-based starting position of a sub-array
+ /// in <paramref name="array"/>.
+ /// </param>
+ /// <param name="length">
+ /// An <see cref="int"/> that contains the number of elements to retrieve a sub-array.
+ /// </param>
+ /// <typeparam name="T">
+ /// The type of elements in the <paramref name="array"/>.
+ /// </typeparam>
+ public static T[] SubArray<T>(this T[] array, int startIndex, int length)
+ {
+ if (array == null || array.Length == 0)
+ return new T[0];
+
+ if (startIndex < 0 || length <= 0)
+ return new T[0];
+
+ if (startIndex + length > array.Length)
+ return new T[0];
+
+ if (startIndex == 0 && array.Length == length)
+ return array;
+
+ T[] subArray = new T[length];
+ Array.Copy(array, startIndex, subArray, 0, length);
+
+ return subArray;
+ }
+
+ /// <summary>
+ /// Converts the order of the specified array of <see cref="byte"/> to the host byte order.
+ /// </summary>
+ /// <returns>
+ /// An array of <see cref="byte"/> converted from <paramref name="src"/>.
+ /// </returns>
+ /// <param name="src">
+ /// An array of <see cref="byte"/> to convert.
+ /// </param>
+ /// <param name="srcOrder">
+ /// One of the <see cref="ByteOrder"/> enum values, indicates the byte order of
+ /// <paramref name="src"/>.
+ /// </param>
+ /// <exception cref="ArgumentNullException">
+ /// <paramref name="src"/> is <see langword="null"/>.
+ /// </exception>
+ public static byte[] ToHostOrder(this byte[] src, ByteOrder srcOrder)
+ {
+ if (src == null)
+ throw new ArgumentNullException("src");
+
+ return src.Length > 1 && !srcOrder.IsHostOrder()
+ ? src.Reverse()
+ : src;
+ }
+
+ /// <summary>
+ /// Converts the specified <paramref name="array"/> to a <see cref="string"/> that
+ /// concatenates the each element of <paramref name="array"/> across the specified
+ /// <paramref name="separator"/>.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string"/> converted from <paramref name="array"/>,
+ /// or <see cref="String.Empty"/> if <paramref name="array"/> is empty.
+ /// </returns>
+ /// <param name="array">
+ /// An array of T to convert.
+ /// </param>
+ /// <param name="separator">
+ /// A <see cref="string"/> that represents the separator string.
+ /// </param>
+ /// <typeparam name="T">
+ /// The type of elements in <paramref name="array"/>.
+ /// </typeparam>
+ /// <exception cref="ArgumentNullException">
+ /// <paramref name="array"/> is <see langword="null"/>.
+ /// </exception>
+ public static string ToString<T>(this T[] array, string separator)
+ {
+ if (array == null)
+ throw new ArgumentNullException("array");
+
+ var len = array.Length;
+ if (len == 0)
+ return String.Empty;
+
+ if (separator == null)
+ separator = String.Empty;
+
+ var buff = new StringBuilder(64);
+ (len - 1).Times(i => buff.AppendFormat("{0}{1}", array[i].ToString(), separator));
+
+ buff.Append(array[len - 1].ToString());
+ return buff.ToString();
+ }
+
+ /// <summary>
+ /// Executes the specified <c>Action&lt;int&gt;</c> delegate <paramref name="n"/> times.
+ /// </summary>
+ /// <param name="n">
+ /// An <see cref="int"/> is the number of times to execute.
+ /// </param>
+ /// <param name="action">
+ /// An <c>Action&lt;int&gt;</c> delegate that references the method(s) to execute.
+ /// An <see cref="int"/> parameter to pass to the method(s) is the zero-based count of
+ /// iteration.
+ /// </param>
+ public static void Times(this int n, Action<int> action)
+ {
+ if (n > 0 && action != null)
+ for (int i = 0; i < n; i++)
+ action(i);
+ }
+
+ /// <summary>
+ /// Converts the specified <see cref="string"/> to a <see cref="Uri"/>.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="Uri"/> converted from <paramref name="uriString"/>, or <see langword="null"/>
+ /// if <paramref name="uriString"/> isn't successfully converted.
+ /// </returns>
+ /// <param name="uriString">
+ /// A <see cref="string"/> to convert.
+ /// </param>
+ public static Uri ToUri(this string uriString)
+ {
+ Uri res;
+ return Uri.TryCreate(
+ uriString, uriString.MaybeUri() ? UriKind.Absolute : UriKind.Relative, out res)
+ ? res
+ : null;
+ }
+
+ /// <summary>
+ /// URL-decodes the specified <see cref="string"/>.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string"/> that receives the decoded string, or the <paramref name="value"/>
+ /// if it's <see langword="null"/> or empty.
+ /// </returns>
+ /// <param name="value">
+ /// A <see cref="string"/> to decode.
+ /// </param>
+ public static string UrlDecode(this string value)
+ {
+ return value == null || value.Length == 0
+ ? value
+ : WebUtility.UrlDecode(value);
+ }
+
+ #endregion
+ }
+} \ No newline at end of file
diff --git a/SocketHttpListener/Fin.cs b/SocketHttpListener/Fin.cs
new file mode 100644
index 000000000..f91401b99
--- /dev/null
+++ b/SocketHttpListener/Fin.cs
@@ -0,0 +1,8 @@
+namespace SocketHttpListener
+{
+ internal enum Fin : byte
+ {
+ More = 0x0,
+ Final = 0x1
+ }
+}
diff --git a/SocketHttpListener/HttpBase.cs b/SocketHttpListener/HttpBase.cs
new file mode 100644
index 000000000..5172ba497
--- /dev/null
+++ b/SocketHttpListener/HttpBase.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.IO;
+using System.Text;
+using System.Threading;
+using MediaBrowser.Model.Services;
+
+namespace SocketHttpListener
+{
+ internal abstract class HttpBase
+ {
+ #region Private Fields
+
+ private QueryParamCollection _headers;
+ private Version _version;
+
+ #endregion
+
+ #region Internal Fields
+
+ internal byte[] EntityBodyData;
+
+ #endregion
+
+ #region Protected Fields
+
+ protected const string CrLf = "\r\n";
+
+ #endregion
+
+ #region Protected Constructors
+
+ protected HttpBase(Version version, QueryParamCollection headers)
+ {
+ _version = version;
+ _headers = headers;
+ }
+
+ #endregion
+
+ #region Public Properties
+
+ public string EntityBody
+ {
+ get
+ {
+ var data = EntityBodyData;
+
+ return data != null && data.Length > 0
+ ? getEncoding(_headers["Content-Type"]).GetString(data, 0, data.Length)
+ : String.Empty;
+ }
+ }
+
+ public QueryParamCollection Headers
+ {
+ get
+ {
+ return _headers;
+ }
+ }
+
+ public Version ProtocolVersion
+ {
+ get
+ {
+ return _version;
+ }
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ private static Encoding getEncoding(string contentType)
+ {
+ if (contentType == null || contentType.Length == 0)
+ return Encoding.UTF8;
+
+ var i = contentType.IndexOf("charset=", StringComparison.Ordinal);
+ if (i == -1)
+ return Encoding.UTF8;
+
+ var charset = contentType.Substring(i + 8);
+ i = charset.IndexOf(';');
+ if (i != -1)
+ charset = charset.Substring(0, i).TrimEnd();
+
+ return Encoding.GetEncoding(charset.Trim('"'));
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ public byte[] ToByteArray()
+ {
+ return Encoding.UTF8.GetBytes(ToString());
+ }
+
+ #endregion
+ }
+} \ No newline at end of file
diff --git a/SocketHttpListener/HttpResponse.cs b/SocketHttpListener/HttpResponse.cs
new file mode 100644
index 000000000..5aca28c7c
--- /dev/null
+++ b/SocketHttpListener/HttpResponse.cs
@@ -0,0 +1,161 @@
+using System;
+using System.Collections.Specialized;
+using System.IO;
+using System.Net;
+using System.Text;
+using HttpStatusCode = SocketHttpListener.Net.HttpStatusCode;
+using HttpVersion = SocketHttpListener.Net.HttpVersion;
+using System.Linq;
+using MediaBrowser.Model.Services;
+
+namespace SocketHttpListener
+{
+ internal class HttpResponse : HttpBase
+ {
+ #region Private Fields
+
+ private string _code;
+ private string _reason;
+
+ #endregion
+
+ #region Private Constructors
+
+ private HttpResponse(string code, string reason, Version version, QueryParamCollection headers)
+ : base(version, headers)
+ {
+ _code = code;
+ _reason = reason;
+ }
+
+ #endregion
+
+ #region Internal Constructors
+
+ internal HttpResponse(HttpStatusCode code)
+ : this(code, code.GetDescription())
+ {
+ }
+
+ internal HttpResponse(HttpStatusCode code, string reason)
+ : this(((int)code).ToString(), reason, HttpVersion.Version11, new QueryParamCollection())
+ {
+ Headers["Server"] = "websocket-sharp/1.0";
+ }
+
+ #endregion
+
+ #region Public Properties
+
+ public CookieCollection Cookies
+ {
+ get
+ {
+ return Headers.GetCookies(true);
+ }
+ }
+
+ public bool IsProxyAuthenticationRequired
+ {
+ get
+ {
+ return _code == "407";
+ }
+ }
+
+ public bool IsUnauthorized
+ {
+ get
+ {
+ return _code == "401";
+ }
+ }
+
+ public bool IsWebSocketResponse
+ {
+ get
+ {
+ var headers = Headers;
+ return ProtocolVersion > HttpVersion.Version10 &&
+ _code == "101" &&
+ headers.Contains("Upgrade", "websocket") &&
+ headers.Contains("Connection", "Upgrade");
+ }
+ }
+
+ public string Reason
+ {
+ get
+ {
+ return _reason;
+ }
+ }
+
+ public string StatusCode
+ {
+ get
+ {
+ return _code;
+ }
+ }
+
+ #endregion
+
+ #region Internal Methods
+
+ internal static HttpResponse CreateCloseResponse(HttpStatusCode code)
+ {
+ var res = new HttpResponse(code);
+ res.Headers["Connection"] = "close";
+
+ return res;
+ }
+
+ internal static HttpResponse CreateWebSocketResponse()
+ {
+ var res = new HttpResponse(HttpStatusCode.SwitchingProtocols);
+
+ var headers = res.Headers;
+ headers["Upgrade"] = "websocket";
+ headers["Connection"] = "Upgrade";
+
+ return res;
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ public void SetCookies(CookieCollection cookies)
+ {
+ if (cookies == null || cookies.Count == 0)
+ return;
+
+ var headers = Headers;
+ var sorted = cookies.OfType<Cookie>().OrderBy(i => i.Name).ToList();
+
+ foreach (var cookie in sorted)
+ headers.Add("Set-Cookie", cookie.ToString());
+ }
+
+ public override string ToString()
+ {
+ var output = new StringBuilder(64);
+ output.AppendFormat("HTTP/{0} {1} {2}{3}", ProtocolVersion, _code, _reason, CrLf);
+
+ var headers = Headers;
+ foreach (var key in headers.Keys)
+ output.AppendFormat("{0}: {1}{2}", key, headers[key], CrLf);
+
+ output.Append(CrLf);
+
+ var entity = EntityBody;
+ if (entity.Length > 0)
+ output.Append(entity);
+
+ return output.ToString();
+ }
+
+ #endregion
+ }
+} \ No newline at end of file
diff --git a/SocketHttpListener/Mask.cs b/SocketHttpListener/Mask.cs
new file mode 100644
index 000000000..adc2f098e
--- /dev/null
+++ b/SocketHttpListener/Mask.cs
@@ -0,0 +1,8 @@
+namespace SocketHttpListener
+{
+ internal enum Mask : byte
+ {
+ Unmask = 0x0,
+ Mask = 0x1
+ }
+}
diff --git a/SocketHttpListener/MessageEventArgs.cs b/SocketHttpListener/MessageEventArgs.cs
new file mode 100644
index 000000000..9dbadb9ab
--- /dev/null
+++ b/SocketHttpListener/MessageEventArgs.cs
@@ -0,0 +1,96 @@
+using System;
+using System.Text;
+
+namespace SocketHttpListener
+{
+ /// <summary>
+ /// Contains the event data associated with a <see cref="WebSocket.OnMessage"/> event.
+ /// </summary>
+ /// <remarks>
+ /// A <see cref="WebSocket.OnMessage"/> event occurs when the <see cref="WebSocket"/> receives
+ /// a text or binary data frame.
+ /// If you want to get the received data, you access the <see cref="MessageEventArgs.Data"/> or
+ /// <see cref="MessageEventArgs.RawData"/> property.
+ /// </remarks>
+ public class MessageEventArgs : EventArgs
+ {
+ #region Private Fields
+
+ private string _data;
+ private Opcode _opcode;
+ private byte[] _rawData;
+
+ #endregion
+
+ #region Internal Constructors
+
+ internal MessageEventArgs (Opcode opcode, byte[] data)
+ {
+ _opcode = opcode;
+ _rawData = data;
+ _data = convertToString (opcode, data);
+ }
+
+ internal MessageEventArgs (Opcode opcode, PayloadData payload)
+ {
+ _opcode = opcode;
+ _rawData = payload.ApplicationData;
+ _data = convertToString (opcode, _rawData);
+ }
+
+ #endregion
+
+ #region Public Properties
+
+ /// <summary>
+ /// Gets the received data as a <see cref="string"/>.
+ /// </summary>
+ /// <value>
+ /// A <see cref="string"/> that contains the received data.
+ /// </value>
+ public string Data {
+ get {
+ return _data;
+ }
+ }
+
+ /// <summary>
+ /// Gets the received data as an array of <see cref="byte"/>.
+ /// </summary>
+ /// <value>
+ /// An array of <see cref="byte"/> that contains the received data.
+ /// </value>
+ public byte [] RawData {
+ get {
+ return _rawData;
+ }
+ }
+
+ /// <summary>
+ /// Gets the type of the received data.
+ /// </summary>
+ /// <value>
+ /// One of the <see cref="Opcode"/> values, indicates the type of the received data.
+ /// </value>
+ public Opcode Type {
+ get {
+ return _opcode;
+ }
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ private static string convertToString (Opcode opcode, byte [] data)
+ {
+ return data.Length == 0
+ ? String.Empty
+ : opcode == Opcode.Text
+ ? Encoding.UTF8.GetString (data, 0, data.Length)
+ : opcode.ToString ();
+ }
+
+ #endregion
+ }
+}
diff --git a/SocketHttpListener/Net/AuthenticationSchemeSelector.cs b/SocketHttpListener/Net/AuthenticationSchemeSelector.cs
new file mode 100644
index 000000000..c6e7e538e
--- /dev/null
+++ b/SocketHttpListener/Net/AuthenticationSchemeSelector.cs
@@ -0,0 +1,6 @@
+using System.Net;
+
+namespace SocketHttpListener.Net
+{
+ public delegate AuthenticationSchemes AuthenticationSchemeSelector(HttpListenerRequest httpRequest);
+}
diff --git a/SocketHttpListener/Net/ChunkStream.cs b/SocketHttpListener/Net/ChunkStream.cs
new file mode 100644
index 000000000..2de6c2c18
--- /dev/null
+++ b/SocketHttpListener/Net/ChunkStream.cs
@@ -0,0 +1,405 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Text;
+
+namespace SocketHttpListener.Net
+{
+ // Licensed to the .NET Foundation under one or more agreements.
+ // See the LICENSE file in the project root for more information.
+ //
+ // System.Net.ResponseStream
+ //
+ // Author:
+ // Gonzalo Paniagua Javier (gonzalo@novell.com)
+ //
+ // Copyright (c) 2005 Novell, Inc. (http://www.novell.com)
+ //
+ // Permission is hereby granted, free of charge, to any person obtaining
+ // a copy of this software and associated documentation files (the
+ // "Software"), to deal in the Software without restriction, including
+ // without limitation the rights to use, copy, modify, merge, publish,
+ // distribute, sublicense, and/or sell copies of the Software, and to
+ // permit persons to whom the Software is furnished to do so, subject to
+ // the following conditions:
+ //
+ // The above copyright notice and this permission notice shall be
+ // included in all copies or substantial portions of the Software.
+ //
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ //
+
+ internal sealed class ChunkStream
+ {
+ private enum State
+ {
+ None,
+ PartialSize,
+ Body,
+ BodyFinished,
+ Trailer
+ }
+
+ private class Chunk
+ {
+ public byte[] Bytes;
+ public int Offset;
+
+ public Chunk(byte[] chunk)
+ {
+ Bytes = chunk;
+ }
+
+ public int Read(byte[] buffer, int offset, int size)
+ {
+ int nread = (size > Bytes.Length - Offset) ? Bytes.Length - Offset : size;
+ Buffer.BlockCopy(Bytes, Offset, buffer, offset, nread);
+ Offset += nread;
+ return nread;
+ }
+ }
+
+ internal WebHeaderCollection _headers;
+ private int _chunkSize;
+ private int _chunkRead;
+ private int _totalWritten;
+ private State _state;
+ private StringBuilder _saved;
+ private bool _sawCR;
+ private bool _gotit;
+ private int _trailerState;
+ private List<Chunk> _chunks;
+
+ public ChunkStream(byte[] buffer, int offset, int size, WebHeaderCollection headers)
+ : this(headers)
+ {
+ Write(buffer, offset, size);
+ }
+
+ public ChunkStream(WebHeaderCollection headers)
+ {
+ _headers = headers;
+ _saved = new StringBuilder();
+ _chunks = new List<Chunk>();
+ _chunkSize = -1;
+ _totalWritten = 0;
+ }
+
+ public void ResetBuffer()
+ {
+ _chunkSize = -1;
+ _chunkRead = 0;
+ _totalWritten = 0;
+ _chunks.Clear();
+ }
+
+ public void WriteAndReadBack(byte[] buffer, int offset, int size, ref int read)
+ {
+ if (offset + read > 0)
+ Write(buffer, offset, offset + read);
+ read = Read(buffer, offset, size);
+ }
+
+ public int Read(byte[] buffer, int offset, int size)
+ {
+ return ReadFromChunks(buffer, offset, size);
+ }
+
+ private int ReadFromChunks(byte[] buffer, int offset, int size)
+ {
+ int count = _chunks.Count;
+ int nread = 0;
+
+ var chunksForRemoving = new List<Chunk>(count);
+ for (int i = 0; i < count; i++)
+ {
+ Chunk chunk = _chunks[i];
+
+ if (chunk.Offset == chunk.Bytes.Length)
+ {
+ chunksForRemoving.Add(chunk);
+ continue;
+ }
+
+ nread += chunk.Read(buffer, offset + nread, size - nread);
+ if (nread == size)
+ break;
+ }
+
+ foreach (var chunk in chunksForRemoving)
+ _chunks.Remove(chunk);
+
+ return nread;
+ }
+
+ public void Write(byte[] buffer, int offset, int size)
+ {
+ if (offset < size)
+ InternalWrite(buffer, ref offset, size);
+ }
+
+ private void InternalWrite(byte[] buffer, ref int offset, int size)
+ {
+ if (_state == State.None || _state == State.PartialSize)
+ {
+ _state = GetChunkSize(buffer, ref offset, size);
+ if (_state == State.PartialSize)
+ return;
+
+ _saved.Length = 0;
+ _sawCR = false;
+ _gotit = false;
+ }
+
+ if (_state == State.Body && offset < size)
+ {
+ _state = ReadBody(buffer, ref offset, size);
+ if (_state == State.Body)
+ return;
+ }
+
+ if (_state == State.BodyFinished && offset < size)
+ {
+ _state = ReadCRLF(buffer, ref offset, size);
+ if (_state == State.BodyFinished)
+ return;
+
+ _sawCR = false;
+ }
+
+ if (_state == State.Trailer && offset < size)
+ {
+ _state = ReadTrailer(buffer, ref offset, size);
+ if (_state == State.Trailer)
+ return;
+
+ _saved.Length = 0;
+ _sawCR = false;
+ _gotit = false;
+ }
+
+ if (offset < size)
+ InternalWrite(buffer, ref offset, size);
+ }
+
+ public bool WantMore
+ {
+ get { return (_chunkRead != _chunkSize || _chunkSize != 0 || _state != State.None); }
+ }
+
+ public bool DataAvailable
+ {
+ get
+ {
+ int count = _chunks.Count;
+ for (int i = 0; i < count; i++)
+ {
+ Chunk ch = _chunks[i];
+ if (ch == null || ch.Bytes == null)
+ continue;
+ if (ch.Bytes.Length > 0 && ch.Offset < ch.Bytes.Length)
+ return (_state != State.Body);
+ }
+ return false;
+ }
+ }
+
+ public int TotalDataSize
+ {
+ get { return _totalWritten; }
+ }
+
+ public int ChunkLeft
+ {
+ get { return _chunkSize - _chunkRead; }
+ }
+
+ private State ReadBody(byte[] buffer, ref int offset, int size)
+ {
+ if (_chunkSize == 0)
+ return State.BodyFinished;
+
+ int diff = size - offset;
+ if (diff + _chunkRead > _chunkSize)
+ diff = _chunkSize - _chunkRead;
+
+ byte[] chunk = new byte[diff];
+ Buffer.BlockCopy(buffer, offset, chunk, 0, diff);
+ _chunks.Add(new Chunk(chunk));
+ offset += diff;
+ _chunkRead += diff;
+ _totalWritten += diff;
+
+ return (_chunkRead == _chunkSize) ? State.BodyFinished : State.Body;
+ }
+
+ private State GetChunkSize(byte[] buffer, ref int offset, int size)
+ {
+ _chunkRead = 0;
+ _chunkSize = 0;
+ char c = '\0';
+ while (offset < size)
+ {
+ c = (char)buffer[offset++];
+ if (c == '\r')
+ {
+ if (_sawCR)
+ ThrowProtocolViolation("2 CR found");
+
+ _sawCR = true;
+ continue;
+ }
+
+ if (_sawCR && c == '\n')
+ break;
+
+ if (c == ' ')
+ _gotit = true;
+
+ if (!_gotit)
+ _saved.Append(c);
+
+ if (_saved.Length > 20)
+ ThrowProtocolViolation("chunk size too long.");
+ }
+
+ if (!_sawCR || c != '\n')
+ {
+ if (offset < size)
+ ThrowProtocolViolation("Missing \\n");
+
+ try
+ {
+ if (_saved.Length > 0)
+ {
+ _chunkSize = Int32.Parse(RemoveChunkExtension(_saved.ToString()), NumberStyles.HexNumber);
+ }
+ }
+ catch (Exception)
+ {
+ ThrowProtocolViolation("Cannot parse chunk size.");
+ }
+
+ return State.PartialSize;
+ }
+
+ _chunkRead = 0;
+ try
+ {
+ _chunkSize = Int32.Parse(RemoveChunkExtension(_saved.ToString()), NumberStyles.HexNumber);
+ }
+ catch (Exception)
+ {
+ ThrowProtocolViolation("Cannot parse chunk size.");
+ }
+
+ if (_chunkSize == 0)
+ {
+ _trailerState = 2;
+ return State.Trailer;
+ }
+
+ return State.Body;
+ }
+
+ private static string RemoveChunkExtension(string input)
+ {
+ int idx = input.IndexOf(';');
+ if (idx == -1)
+ return input;
+ return input.Substring(0, idx);
+ }
+
+ private State ReadCRLF(byte[] buffer, ref int offset, int size)
+ {
+ if (!_sawCR)
+ {
+ if ((char)buffer[offset++] != '\r')
+ ThrowProtocolViolation("Expecting \\r");
+
+ _sawCR = true;
+ if (offset == size)
+ return State.BodyFinished;
+ }
+
+ if (_sawCR && (char)buffer[offset++] != '\n')
+ ThrowProtocolViolation("Expecting \\n");
+
+ return State.None;
+ }
+
+ private State ReadTrailer(byte[] buffer, ref int offset, int size)
+ {
+ char c = '\0';
+
+ // short path
+ if (_trailerState == 2 && (char)buffer[offset] == '\r' && _saved.Length == 0)
+ {
+ offset++;
+ if (offset < size && (char)buffer[offset] == '\n')
+ {
+ offset++;
+ return State.None;
+ }
+ offset--;
+ }
+
+ int st = _trailerState;
+ string stString = "\r\n\r";
+ while (offset < size && st < 4)
+ {
+ c = (char)buffer[offset++];
+ if ((st == 0 || st == 2) && c == '\r')
+ {
+ st++;
+ continue;
+ }
+
+ if ((st == 1 || st == 3) && c == '\n')
+ {
+ st++;
+ continue;
+ }
+
+ if (st > 0)
+ {
+ _saved.Append(stString.Substring(0, _saved.Length == 0 ? st - 2 : st));
+ st = 0;
+ if (_saved.Length > 4196)
+ ThrowProtocolViolation("Error reading trailer (too long).");
+ }
+ }
+
+ if (st < 4)
+ {
+ _trailerState = st;
+ if (offset < size)
+ ThrowProtocolViolation("Error reading trailer.");
+
+ return State.Trailer;
+ }
+
+ StringReader reader = new StringReader(_saved.ToString());
+ string line;
+ while ((line = reader.ReadLine()) != null && line != "")
+ _headers.Add(line);
+
+ return State.None;
+ }
+
+ private static void ThrowProtocolViolation(string message)
+ {
+ WebException we = new WebException(message, null, WebExceptionStatus.ServerProtocolViolation, null);
+ throw we;
+ }
+ }
+}
diff --git a/SocketHttpListener/Net/ChunkedInputStream.cs b/SocketHttpListener/Net/ChunkedInputStream.cs
new file mode 100644
index 000000000..2e0e1964b
--- /dev/null
+++ b/SocketHttpListener/Net/ChunkedInputStream.cs
@@ -0,0 +1,172 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Runtime.InteropServices;
+using SocketHttpListener.Primitives;
+
+namespace SocketHttpListener.Net
+{
+ // Licensed to the .NET Foundation under one or more agreements.
+ // See the LICENSE file in the project root for more information.
+ //
+ // System.Net.ResponseStream
+ //
+ // Author:
+ // Gonzalo Paniagua Javier (gonzalo@novell.com)
+ //
+ // Copyright (c) 2005 Novell, Inc. (http://www.novell.com)
+ //
+ // Permission is hereby granted, free of charge, to any person obtaining
+ // a copy of this software and associated documentation files (the
+ // "Software"), to deal in the Software without restriction, including
+ // without limitation the rights to use, copy, modify, merge, publish,
+ // distribute, sublicense, and/or sell copies of the Software, and to
+ // permit persons to whom the Software is furnished to do so, subject to
+ // the following conditions:
+ //
+ // The above copyright notice and this permission notice shall be
+ // included in all copies or substantial portions of the Software.
+ //
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ //
+
+ internal sealed class ChunkedInputStream : HttpRequestStream
+ {
+ private ChunkStream _decoder;
+ private readonly HttpListenerContext _context;
+ private bool _no_more_data;
+
+ private class ReadBufferState
+ {
+ public byte[] Buffer;
+ public int Offset;
+ public int Count;
+ public int InitialCount;
+ public HttpStreamAsyncResult Ares;
+ public ReadBufferState(byte[] buffer, int offset, int count, HttpStreamAsyncResult ares)
+ {
+ Buffer = buffer;
+ Offset = offset;
+ Count = count;
+ InitialCount = count;
+ Ares = ares;
+ }
+ }
+
+ public ChunkedInputStream(HttpListenerContext context, Stream stream, byte[] buffer, int offset, int length)
+ : base(stream, buffer, offset, length)
+ {
+ _context = context;
+ WebHeaderCollection coll = (WebHeaderCollection)context.Request.Headers;
+ _decoder = new ChunkStream(coll);
+ }
+
+ public ChunkStream Decoder
+ {
+ get { return _decoder; }
+ set { _decoder = value; }
+ }
+
+ protected override int ReadCore(byte[] buffer, int offset, int count)
+ {
+ IAsyncResult ares = BeginReadCore(buffer, offset, count, null, null);
+ return EndRead(ares);
+ }
+
+ protected override IAsyncResult BeginReadCore(byte[] buffer, int offset, int size, AsyncCallback cback, object state)
+ {
+ HttpStreamAsyncResult ares = new HttpStreamAsyncResult(this);
+ ares._callback = cback;
+ ares._state = state;
+ if (_no_more_data || size == 0 || _closed)
+ {
+ ares.Complete();
+ return ares;
+ }
+ int nread = _decoder.Read(buffer, offset, size);
+ offset += nread;
+ size -= nread;
+ if (size == 0)
+ {
+ // got all we wanted, no need to bother the decoder yet
+ ares._count = nread;
+ ares.Complete();
+ return ares;
+ }
+ if (!_decoder.WantMore)
+ {
+ _no_more_data = nread == 0;
+ ares._count = nread;
+ ares.Complete();
+ return ares;
+ }
+ ares._buffer = new byte[8192];
+ ares._offset = 0;
+ ares._count = 8192;
+ ReadBufferState rb = new ReadBufferState(buffer, offset, size, ares);
+ rb.InitialCount += nread;
+ base.BeginReadCore(ares._buffer, ares._offset, ares._count, OnRead, rb);
+ return ares;
+ }
+
+ private void OnRead(IAsyncResult base_ares)
+ {
+ ReadBufferState rb = (ReadBufferState)base_ares.AsyncState;
+ HttpStreamAsyncResult ares = rb.Ares;
+ try
+ {
+ int nread = base.EndRead(base_ares);
+ _decoder.Write(ares._buffer, ares._offset, nread);
+ nread = _decoder.Read(rb.Buffer, rb.Offset, rb.Count);
+ rb.Offset += nread;
+ rb.Count -= nread;
+ if (rb.Count == 0 || !_decoder.WantMore || nread == 0)
+ {
+ _no_more_data = !_decoder.WantMore && nread == 0;
+ ares._count = rb.InitialCount - rb.Count;
+ ares.Complete();
+ return;
+ }
+ ares._offset = 0;
+ ares._count = Math.Min(8192, _decoder.ChunkLeft + 6);
+ base.BeginReadCore(ares._buffer, ares._offset, ares._count, OnRead, rb);
+ }
+ catch (Exception e)
+ {
+ _context.Connection.SendError(e.Message, 400);
+ ares.Complete(e);
+ }
+ }
+
+ public override int EndRead(IAsyncResult asyncResult)
+ {
+ if (asyncResult == null)
+ throw new ArgumentNullException(nameof(asyncResult));
+
+ HttpStreamAsyncResult ares = asyncResult as HttpStreamAsyncResult;
+ if (ares == null || !ReferenceEquals(this, ares._parent))
+ {
+ throw new ArgumentException("Invalid async result");
+ }
+ if (ares._endCalled)
+ {
+ throw new InvalidOperationException("Invalid end call");
+ }
+ ares._endCalled = true;
+
+ if (!asyncResult.IsCompleted)
+ asyncResult.AsyncWaitHandle.WaitOne();
+
+ if (ares._error != null)
+ throw new HttpListenerException((int)HttpStatusCode.BadRequest, "Bad Request");
+
+ return ares._count;
+ }
+ }
+}
diff --git a/SocketHttpListener/Net/CookieHelper.cs b/SocketHttpListener/Net/CookieHelper.cs
new file mode 100644
index 000000000..470507d6b
--- /dev/null
+++ b/SocketHttpListener/Net/CookieHelper.cs
@@ -0,0 +1,144 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SocketHttpListener.Net
+{
+ public static class CookieHelper
+ {
+ internal static CookieCollection Parse(string value, bool response)
+ {
+ return response
+ ? parseResponse(value)
+ : null;
+ }
+
+ private static string[] splitCookieHeaderValue(string value)
+ {
+ return new List<string>(value.SplitHeaderValue(',', ';')).ToArray();
+ }
+
+ private static CookieCollection parseResponse(string value)
+ {
+ var cookies = new CookieCollection();
+
+ Cookie cookie = null;
+ var pairs = splitCookieHeaderValue(value);
+ for (int i = 0; i < pairs.Length; i++)
+ {
+ var pair = pairs[i].Trim();
+ if (pair.Length == 0)
+ continue;
+
+ if (pair.StartsWith("version", StringComparison.OrdinalIgnoreCase))
+ {
+ if (cookie != null)
+ cookie.Version = Int32.Parse(pair.GetValueInternal("=").Trim('"'));
+ }
+ else if (pair.StartsWith("expires", StringComparison.OrdinalIgnoreCase))
+ {
+ var buffer = new StringBuilder(pair.GetValueInternal("="), 32);
+ if (i < pairs.Length - 1)
+ buffer.AppendFormat(", {0}", pairs[++i].Trim());
+
+ DateTime expires;
+ if (!DateTime.TryParseExact(
+ buffer.ToString(),
+ new[] { "ddd, dd'-'MMM'-'yyyy HH':'mm':'ss 'GMT'", "r" },
+ new CultureInfo("en-US"),
+ DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal,
+ out expires))
+ expires = DateTime.Now;
+
+ if (cookie != null && cookie.Expires == DateTime.MinValue)
+ cookie.Expires = expires.ToLocalTime();
+ }
+ else if (pair.StartsWith("max-age", StringComparison.OrdinalIgnoreCase))
+ {
+ var max = Int32.Parse(pair.GetValueInternal("=").Trim('"'));
+ var expires = DateTime.Now.AddSeconds((double)max);
+ if (cookie != null)
+ cookie.Expires = expires;
+ }
+ else if (pair.StartsWith("path", StringComparison.OrdinalIgnoreCase))
+ {
+ if (cookie != null)
+ cookie.Path = pair.GetValueInternal("=");
+ }
+ else if (pair.StartsWith("domain", StringComparison.OrdinalIgnoreCase))
+ {
+ if (cookie != null)
+ cookie.Domain = pair.GetValueInternal("=");
+ }
+ else if (pair.StartsWith("port", StringComparison.OrdinalIgnoreCase))
+ {
+ var port = pair.Equals("port", StringComparison.OrdinalIgnoreCase)
+ ? "\"\""
+ : pair.GetValueInternal("=");
+
+ if (cookie != null)
+ cookie.Port = port;
+ }
+ else if (pair.StartsWith("comment", StringComparison.OrdinalIgnoreCase))
+ {
+ if (cookie != null)
+ cookie.Comment = pair.GetValueInternal("=").UrlDecode();
+ }
+ else if (pair.StartsWith("commenturl", StringComparison.OrdinalIgnoreCase))
+ {
+ if (cookie != null)
+ cookie.CommentUri = pair.GetValueInternal("=").Trim('"').ToUri();
+ }
+ else if (pair.StartsWith("discard", StringComparison.OrdinalIgnoreCase))
+ {
+ if (cookie != null)
+ cookie.Discard = true;
+ }
+ else if (pair.StartsWith("secure", StringComparison.OrdinalIgnoreCase))
+ {
+ if (cookie != null)
+ cookie.Secure = true;
+ }
+ else if (pair.StartsWith("httponly", StringComparison.OrdinalIgnoreCase))
+ {
+ if (cookie != null)
+ cookie.HttpOnly = true;
+ }
+ else
+ {
+ if (cookie != null)
+ cookies.Add(cookie);
+
+ string name;
+ string val = String.Empty;
+
+ var pos = pair.IndexOf('=');
+ if (pos == -1)
+ {
+ name = pair;
+ }
+ else if (pos == pair.Length - 1)
+ {
+ name = pair.Substring(0, pos).TrimEnd(' ');
+ }
+ else
+ {
+ name = pair.Substring(0, pos).TrimEnd(' ');
+ val = pair.Substring(pos + 1).TrimStart(' ');
+ }
+
+ cookie = new Cookie(name, val);
+ }
+ }
+
+ if (cookie != null)
+ cookies.Add(cookie);
+
+ return cookies;
+ }
+ }
+}
diff --git a/SocketHttpListener/Net/EndPointListener.cs b/SocketHttpListener/Net/EndPointListener.cs
new file mode 100644
index 000000000..2106bbec5
--- /dev/null
+++ b/SocketHttpListener/Net/EndPointListener.cs
@@ -0,0 +1,391 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Threading;
+using MediaBrowser.Model.Cryptography;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.System;
+using MediaBrowser.Model.Text;
+using SocketHttpListener.Primitives;
+
+namespace SocketHttpListener.Net
+{
+ sealed class EndPointListener
+ {
+ HttpListener listener;
+ IpEndPointInfo endpoint;
+ IAcceptSocket sock;
+ Dictionary<ListenerPrefix,HttpListener> prefixes; // Dictionary <ListenerPrefix, HttpListener>
+ List<ListenerPrefix> unhandled; // List<ListenerPrefix> unhandled; host = '*'
+ List<ListenerPrefix> all; // List<ListenerPrefix> all; host = '+'
+ ICertificate cert;
+ bool secure;
+ Dictionary<HttpConnection, HttpConnection> unregistered;
+ private readonly ILogger _logger;
+ private bool _closed;
+ private bool _enableDualMode;
+ private readonly ICryptoProvider _cryptoProvider;
+ private readonly IStreamFactory _streamFactory;
+ private readonly ISocketFactory _socketFactory;
+ private readonly ITextEncoding _textEncoding;
+ private readonly IMemoryStreamFactory _memoryStreamFactory;
+ private readonly IFileSystem _fileSystem;
+ private readonly IEnvironmentInfo _environment;
+
+ public EndPointListener(HttpListener listener, IpAddressInfo addr, int port, bool secure, ICertificate cert, ILogger logger, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, ISocketFactory socketFactory, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding, IFileSystem fileSystem, IEnvironmentInfo environment)
+ {
+ this.listener = listener;
+ _logger = logger;
+ _cryptoProvider = cryptoProvider;
+ _streamFactory = streamFactory;
+ _socketFactory = socketFactory;
+ _memoryStreamFactory = memoryStreamFactory;
+ _textEncoding = textEncoding;
+ _fileSystem = fileSystem;
+ _environment = environment;
+
+ this.secure = secure;
+ this.cert = cert;
+
+ _enableDualMode = addr.Equals(IpAddressInfo.IPv6Any);
+ endpoint = new IpEndPointInfo(addr, port);
+
+ prefixes = new Dictionary<ListenerPrefix, HttpListener>();
+ unregistered = new Dictionary<HttpConnection, HttpConnection>();
+
+ CreateSocket();
+ }
+
+ internal HttpListener Listener
+ {
+ get
+ {
+ return listener;
+ }
+ }
+
+ private void CreateSocket()
+ {
+ try
+ {
+ sock = _socketFactory.CreateSocket(endpoint.IpAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp, _enableDualMode);
+ }
+ catch (SocketCreateException ex)
+ {
+ if (_enableDualMode && endpoint.IpAddress.Equals(IpAddressInfo.IPv6Any) &&
+ (string.Equals(ex.ErrorCode, "AddressFamilyNotSupported", StringComparison.OrdinalIgnoreCase) ||
+ // mono on bsd is throwing this
+ string.Equals(ex.ErrorCode, "ProtocolNotSupported", StringComparison.OrdinalIgnoreCase)))
+ {
+ endpoint = new IpEndPointInfo(IpAddressInfo.Any, endpoint.Port);
+ _enableDualMode = false;
+ sock = _socketFactory.CreateSocket(endpoint.IpAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp, _enableDualMode);
+ }
+ else
+ {
+ throw;
+ }
+ }
+
+ sock.Bind(endpoint);
+
+ // This is the number TcpListener uses.
+ sock.Listen(2147483647);
+
+ sock.StartAccept(ProcessAccept, () => _closed);
+ _closed = false;
+ }
+
+ private async void ProcessAccept(IAcceptSocket accepted)
+ {
+ try
+ {
+ var listener = this;
+
+ if (listener.secure && listener.cert == null)
+ {
+ accepted.Close();
+ return;
+ }
+
+ HttpConnection conn = await HttpConnection.Create(_logger, accepted, listener, listener.secure, listener.cert, _cryptoProvider, _streamFactory, _memoryStreamFactory, _textEncoding, _fileSystem, _environment).ConfigureAwait(false);
+
+ //_logger.Debug("Adding unregistered connection to {0}. Id: {1}", accepted.RemoteEndPoint, connectionId);
+ lock (listener.unregistered)
+ {
+ listener.unregistered[conn] = conn;
+ }
+ conn.BeginReadRequest();
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error in ProcessAccept", ex);
+ }
+ }
+
+ internal void RemoveConnection(HttpConnection conn)
+ {
+ lock (unregistered)
+ {
+ unregistered.Remove(conn);
+ }
+ }
+
+ public bool BindContext(HttpListenerContext context)
+ {
+ HttpListenerRequest req = context.Request;
+ ListenerPrefix prefix;
+ HttpListener listener = SearchListener(req.Url, out prefix);
+ if (listener == null)
+ return false;
+
+ context.Connection.Prefix = prefix;
+ return true;
+ }
+
+ public void UnbindContext(HttpListenerContext context)
+ {
+ if (context == null || context.Request == null)
+ return;
+
+ listener.UnregisterContext(context);
+ }
+
+ HttpListener SearchListener(Uri uri, out ListenerPrefix prefix)
+ {
+ prefix = null;
+ if (uri == null)
+ return null;
+
+ string host = uri.Host;
+ int port = uri.Port;
+ string path = WebUtility.UrlDecode(uri.AbsolutePath);
+ string path_slash = path[path.Length - 1] == '/' ? path : path + "/";
+
+ HttpListener best_match = null;
+ int best_length = -1;
+
+ if (host != null && host != "")
+ {
+ var p_ro = prefixes;
+ foreach (ListenerPrefix p in p_ro.Keys)
+ {
+ string ppath = p.Path;
+ if (ppath.Length < best_length)
+ continue;
+
+ if (p.Host != host || p.Port != port)
+ continue;
+
+ if (path.StartsWith(ppath) || path_slash.StartsWith(ppath))
+ {
+ best_length = ppath.Length;
+ best_match = (HttpListener)p_ro[p];
+ prefix = p;
+ }
+ }
+ if (best_length != -1)
+ return best_match;
+ }
+
+ List<ListenerPrefix> list = unhandled;
+ best_match = MatchFromList(host, path, list, out prefix);
+ if (path != path_slash && best_match == null)
+ best_match = MatchFromList(host, path_slash, list, out prefix);
+ if (best_match != null)
+ return best_match;
+
+ list = all;
+ best_match = MatchFromList(host, path, list, out prefix);
+ if (path != path_slash && best_match == null)
+ best_match = MatchFromList(host, path_slash, list, out prefix);
+ if (best_match != null)
+ return best_match;
+
+ return null;
+ }
+
+ HttpListener MatchFromList(string host, string path, List<ListenerPrefix> list, out ListenerPrefix prefix)
+ {
+ prefix = null;
+ if (list == null)
+ return null;
+
+ HttpListener best_match = null;
+ int best_length = -1;
+
+ foreach (ListenerPrefix p in list)
+ {
+ string ppath = p.Path;
+ if (ppath.Length < best_length)
+ continue;
+
+ if (path.StartsWith(ppath))
+ {
+ best_length = ppath.Length;
+ best_match = p.Listener;
+ prefix = p;
+ }
+ }
+
+ return best_match;
+ }
+
+ void AddSpecial(List<ListenerPrefix> coll, ListenerPrefix prefix)
+ {
+ if (coll == null)
+ return;
+
+ foreach (ListenerPrefix p in coll)
+ {
+ if (p.Path == prefix.Path) //TODO: code
+ throw new HttpListenerException(400, "Prefix already in use.");
+ }
+ coll.Add(prefix);
+ }
+
+ bool RemoveSpecial(List<ListenerPrefix> coll, ListenerPrefix prefix)
+ {
+ if (coll == null)
+ return false;
+
+ int c = coll.Count;
+ for (int i = 0; i < c; i++)
+ {
+ ListenerPrefix p = (ListenerPrefix)coll[i];
+ if (p.Path == prefix.Path)
+ {
+ coll.RemoveAt(i);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void CheckIfRemove()
+ {
+ if (prefixes.Count > 0)
+ return;
+
+ List<ListenerPrefix> list = unhandled;
+ if (list != null && list.Count > 0)
+ return;
+
+ list = all;
+ if (list != null && list.Count > 0)
+ return;
+
+ EndPointManager.RemoveEndPoint(this, endpoint);
+ }
+
+ public void Close()
+ {
+ _closed = true;
+ sock.Close();
+ lock (unregistered)
+ {
+ //
+ // Clone the list because RemoveConnection can be called from Close
+ //
+ var connections = new List<HttpConnection>(unregistered.Keys);
+
+ foreach (HttpConnection c in connections)
+ c.Close(true);
+ unregistered.Clear();
+ }
+ }
+
+ public void AddPrefix(ListenerPrefix prefix, HttpListener listener)
+ {
+ List<ListenerPrefix> current;
+ List<ListenerPrefix> future;
+ if (prefix.Host == "*")
+ {
+ do
+ {
+ current = unhandled;
+ future = (current != null) ? current.ToList() : new List<ListenerPrefix>();
+ prefix.Listener = listener;
+ AddSpecial(future, prefix);
+ } while (Interlocked.CompareExchange(ref unhandled, future, current) != current);
+ return;
+ }
+
+ if (prefix.Host == "+")
+ {
+ do
+ {
+ current = all;
+ future = (current != null) ? current.ToList() : new List<ListenerPrefix>();
+ prefix.Listener = listener;
+ AddSpecial(future, prefix);
+ } while (Interlocked.CompareExchange(ref all, future, current) != current);
+ return;
+ }
+
+ Dictionary<ListenerPrefix, HttpListener> prefs;
+ Dictionary<ListenerPrefix, HttpListener> p2;
+ do
+ {
+ prefs = prefixes;
+ if (prefs.ContainsKey(prefix))
+ {
+ HttpListener other = (HttpListener)prefs[prefix];
+ if (other != listener) // TODO: code.
+ throw new HttpListenerException(400, "There's another listener for " + prefix);
+ return;
+ }
+ p2 = new Dictionary<ListenerPrefix, HttpListener>(prefs);
+ p2[prefix] = listener;
+ } while (Interlocked.CompareExchange(ref prefixes, p2, prefs) != prefs);
+ }
+
+ public void RemovePrefix(ListenerPrefix prefix, HttpListener listener)
+ {
+ List<ListenerPrefix> current;
+ List<ListenerPrefix> future;
+ if (prefix.Host == "*")
+ {
+ do
+ {
+ current = unhandled;
+ future = (current != null) ? current.ToList() : new List<ListenerPrefix>();
+ if (!RemoveSpecial(future, prefix))
+ break; // Prefix not found
+ } while (Interlocked.CompareExchange(ref unhandled, future, current) != current);
+ CheckIfRemove();
+ return;
+ }
+
+ if (prefix.Host == "+")
+ {
+ do
+ {
+ current = all;
+ future = (current != null) ? current.ToList() : new List<ListenerPrefix>();
+ if (!RemoveSpecial(future, prefix))
+ break; // Prefix not found
+ } while (Interlocked.CompareExchange(ref all, future, current) != current);
+ CheckIfRemove();
+ return;
+ }
+
+ Dictionary<ListenerPrefix, HttpListener> prefs;
+ Dictionary<ListenerPrefix, HttpListener> p2;
+ do
+ {
+ prefs = prefixes;
+ if (!prefs.ContainsKey(prefix))
+ break;
+
+ p2 = new Dictionary<ListenerPrefix, HttpListener>(prefs);
+ p2.Remove(prefix);
+ } while (Interlocked.CompareExchange(ref prefixes, p2, prefs) != prefs);
+ CheckIfRemove();
+ }
+ }
+}
diff --git a/SocketHttpListener/Net/EndPointManager.cs b/SocketHttpListener/Net/EndPointManager.cs
new file mode 100644
index 000000000..6a00ed360
--- /dev/null
+++ b/SocketHttpListener/Net/EndPointManager.cs
@@ -0,0 +1,166 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Reflection;
+using System.Threading.Tasks;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Net;
+using SocketHttpListener.Primitives;
+
+namespace SocketHttpListener.Net
+{
+ sealed class EndPointManager
+ {
+ // Dictionary<IPAddress, Dictionary<int, EndPointListener>>
+ static Dictionary<string, Dictionary<int, EndPointListener>> ip_to_endpoints = new Dictionary<string, Dictionary<int, EndPointListener>>();
+
+ private EndPointManager()
+ {
+ }
+
+ public static void AddListener(ILogger logger, HttpListener listener)
+ {
+ List<string> added = new List<string>();
+ try
+ {
+ lock (ip_to_endpoints)
+ {
+ foreach (string prefix in listener.Prefixes)
+ {
+ AddPrefixInternal(logger, prefix, listener);
+ added.Add(prefix);
+ }
+ }
+ }
+ catch
+ {
+ foreach (string prefix in added)
+ {
+ RemovePrefix(logger, prefix, listener);
+ }
+ throw;
+ }
+ }
+
+ public static void AddPrefix(ILogger logger, string prefix, HttpListener listener)
+ {
+ lock (ip_to_endpoints)
+ {
+ AddPrefixInternal(logger, prefix, listener);
+ }
+ }
+
+ static void AddPrefixInternal(ILogger logger, string p, HttpListener listener)
+ {
+ ListenerPrefix lp = new ListenerPrefix(p);
+ if (lp.Path.IndexOf('%') != -1)
+ throw new HttpListenerException(400, "Invalid path.");
+
+ if (lp.Path.IndexOf("//", StringComparison.Ordinal) != -1) // TODO: Code?
+ throw new HttpListenerException(400, "Invalid path.");
+
+ // listens on all the interfaces if host name cannot be parsed by IPAddress.
+ EndPointListener epl = GetEPListener(logger, lp.Host, lp.Port, listener, lp.Secure).Result;
+ epl.AddPrefix(lp, listener);
+ }
+
+ private static IpAddressInfo GetIpAnyAddress(HttpListener listener)
+ {
+ return listener.EnableDualMode ? IpAddressInfo.IPv6Any : IpAddressInfo.Any;
+ }
+
+ static async Task<EndPointListener> GetEPListener(ILogger logger, string host, int port, HttpListener listener, bool secure)
+ {
+ var networkManager = listener.NetworkManager;
+
+ IpAddressInfo addr;
+ if (host == "*" || host == "+")
+ addr = GetIpAnyAddress(listener);
+ else if (networkManager.TryParseIpAddress(host, out addr) == false)
+ {
+ try
+ {
+ addr = (await networkManager.GetHostAddressesAsync(host).ConfigureAwait(false)).FirstOrDefault() ??
+ GetIpAnyAddress(listener);
+ }
+ catch
+ {
+ addr = GetIpAnyAddress(listener);
+ }
+ }
+
+ Dictionary<int, EndPointListener> p = null; // Dictionary<int, EndPointListener>
+ if (!ip_to_endpoints.TryGetValue(addr.Address, out p))
+ {
+ p = new Dictionary<int, EndPointListener>();
+ ip_to_endpoints[addr.Address] = p;
+ }
+
+ EndPointListener epl = null;
+ if (p.ContainsKey(port))
+ {
+ epl = (EndPointListener)p[port];
+ }
+ else
+ {
+ epl = new EndPointListener(listener, addr, port, secure, listener.Certificate, logger, listener.CryptoProvider, listener.StreamFactory, listener.SocketFactory, listener.MemoryStreamFactory, listener.TextEncoding, listener.FileSystem, listener.EnvironmentInfo);
+ p[port] = epl;
+ }
+
+ return epl;
+ }
+
+ public static void RemoveEndPoint(EndPointListener epl, IpEndPointInfo ep)
+ {
+ lock (ip_to_endpoints)
+ {
+ // Dictionary<int, EndPointListener> p
+ Dictionary<int, EndPointListener> p;
+ if (ip_to_endpoints.TryGetValue(ep.IpAddress.Address, out p))
+ {
+ p.Remove(ep.Port);
+ if (p.Count == 0)
+ {
+ ip_to_endpoints.Remove(ep.IpAddress.Address);
+ }
+ }
+ epl.Close();
+ }
+ }
+
+ public static void RemoveListener(ILogger logger, HttpListener listener)
+ {
+ lock (ip_to_endpoints)
+ {
+ foreach (string prefix in listener.Prefixes)
+ {
+ RemovePrefixInternal(logger, prefix, listener);
+ }
+ }
+ }
+
+ public static void RemovePrefix(ILogger logger, string prefix, HttpListener listener)
+ {
+ lock (ip_to_endpoints)
+ {
+ RemovePrefixInternal(logger, prefix, listener);
+ }
+ }
+
+ static void RemovePrefixInternal(ILogger logger, string prefix, HttpListener listener)
+ {
+ ListenerPrefix lp = new ListenerPrefix(prefix);
+ if (lp.Path.IndexOf('%') != -1)
+ return;
+
+ if (lp.Path.IndexOf("//", StringComparison.Ordinal) != -1)
+ return;
+
+ EndPointListener epl = GetEPListener(logger, lp.Host, lp.Port, listener, lp.Secure).Result;
+ epl.RemovePrefix(lp, listener);
+ }
+ }
+}
diff --git a/SocketHttpListener/Net/HttpConnection.cs b/SocketHttpListener/Net/HttpConnection.cs
new file mode 100644
index 000000000..848b80f99
--- /dev/null
+++ b/SocketHttpListener/Net/HttpConnection.cs
@@ -0,0 +1,547 @@
+using System;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Cryptography;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.System;
+using MediaBrowser.Model.Text;
+using SocketHttpListener.Primitives;
+
+namespace SocketHttpListener.Net
+{
+ sealed class HttpConnection
+ {
+ const int BufferSize = 8192;
+ IAcceptSocket sock;
+ Stream stream;
+ EndPointListener epl;
+ MemoryStream ms;
+ byte[] buffer;
+ HttpListenerContext context;
+ StringBuilder current_line;
+ ListenerPrefix prefix;
+ HttpRequestStream i_stream;
+ Stream o_stream;
+ bool chunked;
+ int reuses;
+ bool context_bound;
+ bool secure;
+ int s_timeout = 300000; // 90k ms for first request, 15k ms from then on
+ IpEndPointInfo local_ep;
+ HttpListener last_listener;
+ int[] client_cert_errors;
+ ICertificate cert;
+ Stream ssl_stream;
+
+ private readonly ILogger _logger;
+ private readonly ICryptoProvider _cryptoProvider;
+ private readonly IMemoryStreamFactory _memoryStreamFactory;
+ private readonly ITextEncoding _textEncoding;
+ private readonly IStreamFactory _streamFactory;
+ private readonly IFileSystem _fileSystem;
+ private readonly IEnvironmentInfo _environment;
+
+ private HttpConnection(ILogger logger, IAcceptSocket sock, EndPointListener epl, bool secure, ICertificate cert, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding, IFileSystem fileSystem, IEnvironmentInfo environment)
+ {
+ _logger = logger;
+ this.sock = sock;
+ this.epl = epl;
+ this.secure = secure;
+ this.cert = cert;
+ _cryptoProvider = cryptoProvider;
+ _memoryStreamFactory = memoryStreamFactory;
+ _textEncoding = textEncoding;
+ _fileSystem = fileSystem;
+ _environment = environment;
+ _streamFactory = streamFactory;
+ }
+
+ private async Task InitStream()
+ {
+ if (secure == false)
+ {
+ stream = _streamFactory.CreateNetworkStream(sock, false);
+ }
+ else
+ {
+ //ssl_stream = epl.Listener.CreateSslStream(new NetworkStream(sock, false), false, (t, c, ch, e) =>
+ //{
+ // if (c == null)
+ // return true;
+ // var c2 = c as X509Certificate2;
+ // if (c2 == null)
+ // c2 = new X509Certificate2(c.GetRawCertData());
+ // client_cert = c2;
+ // client_cert_errors = new int[] { (int)e };
+ // return true;
+ //});
+ //stream = ssl_stream.AuthenticatedStream;
+
+ ssl_stream = _streamFactory.CreateSslStream(_streamFactory.CreateNetworkStream(sock, false), false);
+ await _streamFactory.AuthenticateSslStreamAsServer(ssl_stream, cert).ConfigureAwait(false);
+ stream = ssl_stream;
+ }
+ Init();
+ }
+
+ public static async Task<HttpConnection> Create(ILogger logger, IAcceptSocket sock, EndPointListener epl, bool secure, ICertificate cert, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding, IFileSystem fileSystem, IEnvironmentInfo environment)
+ {
+ var connection = new HttpConnection(logger, sock, epl, secure, cert, cryptoProvider, streamFactory, memoryStreamFactory, textEncoding, fileSystem, environment);
+
+ await connection.InitStream().ConfigureAwait(false);
+
+ return connection;
+ }
+
+ public Stream Stream
+ {
+ get
+ {
+ return stream;
+ }
+ }
+
+ internal int[] ClientCertificateErrors
+ {
+ get { return client_cert_errors; }
+ }
+
+ void Init()
+ {
+ if (ssl_stream != null)
+ {
+ //ssl_stream.AuthenticateAsServer(client_cert, true, (SslProtocols)ServicePointManager.SecurityProtocol, false);
+ //_streamFactory.AuthenticateSslStreamAsServer(ssl_stream, cert);
+ }
+
+ context_bound = false;
+ i_stream = null;
+ o_stream = null;
+ prefix = null;
+ chunked = false;
+ ms = _memoryStreamFactory.CreateNew();
+ position = 0;
+ input_state = InputState.RequestLine;
+ line_state = LineState.None;
+ context = new HttpListenerContext(this, _logger, _cryptoProvider, _memoryStreamFactory, _textEncoding, _fileSystem);
+ }
+
+ public bool IsClosed
+ {
+ get { return (sock == null); }
+ }
+
+ public int Reuses
+ {
+ get { return reuses; }
+ }
+
+ public IpEndPointInfo LocalEndPoint
+ {
+ get
+ {
+ if (local_ep != null)
+ return local_ep;
+
+ local_ep = (IpEndPointInfo)sock.LocalEndPoint;
+ return local_ep;
+ }
+ }
+
+ public IpEndPointInfo RemoteEndPoint
+ {
+ get { return (IpEndPointInfo)sock.RemoteEndPoint; }
+ }
+
+ public bool IsSecure
+ {
+ get { return secure; }
+ }
+
+ public ListenerPrefix Prefix
+ {
+ get { return prefix; }
+ set { prefix = value; }
+ }
+
+ public async Task BeginReadRequest()
+ {
+ if (buffer == null)
+ buffer = new byte[BufferSize];
+
+ try
+ {
+ //if (reuses == 1)
+ // s_timeout = 15000;
+ var nRead = await stream.ReadAsync(buffer, 0, BufferSize).ConfigureAwait(false);
+
+ OnReadInternal(nRead);
+ }
+ catch (Exception ex)
+ {
+ OnReadInternalException(ms, ex);
+ }
+ }
+
+ public HttpRequestStream GetRequestStream(bool chunked, long contentlength)
+ {
+ if (i_stream == null)
+ {
+ byte[] buffer;
+ _memoryStreamFactory.TryGetBuffer(ms, out buffer);
+
+ int length = (int)ms.Length;
+ ms = null;
+ if (chunked)
+ {
+ this.chunked = true;
+ //context.Response.SendChunked = true;
+ i_stream = new ChunkedInputStream(context, stream, buffer, position, length - position);
+ }
+ else
+ {
+ i_stream = new HttpRequestStream(stream, buffer, position, length - position, contentlength);
+ }
+ }
+ return i_stream;
+ }
+
+ public Stream GetResponseStream(bool isExpect100Continue = false)
+ {
+ // TODO: can we get this stream before reading the input?
+ if (o_stream == null)
+ {
+ //context.Response.DetermineIfChunked();
+
+ var supportsDirectSocketAccess = !context.Response.SendChunked && !isExpect100Continue && !secure;
+
+ o_stream = new ResponseStream(stream, context.Response, _memoryStreamFactory, _textEncoding, _fileSystem, sock, supportsDirectSocketAccess, _logger, _environment);
+ }
+ return o_stream;
+ }
+
+ void OnReadInternal(int nread)
+ {
+ ms.Write(buffer, 0, nread);
+ if (ms.Length > 32768)
+ {
+ SendError("Bad request", 400);
+ Close(true);
+ return;
+ }
+
+ if (nread == 0)
+ {
+ //if (ms.Length > 0)
+ // SendError (); // Why bother?
+ CloseSocket();
+ Unbind();
+ return;
+ }
+
+ if (ProcessInput(ms))
+ {
+ if (!context.HaveError)
+ context.Request.FinishInitialization();
+
+ if (context.HaveError)
+ {
+ SendError();
+ Close(true);
+ return;
+ }
+
+ if (!epl.BindContext(context))
+ {
+ SendError("Invalid host", 400);
+ Close(true);
+ return;
+ }
+ HttpListener listener = epl.Listener;
+ if (last_listener != listener)
+ {
+ RemoveConnection();
+ listener.AddConnection(this);
+ last_listener = listener;
+ }
+
+ context_bound = true;
+ listener.RegisterContext(context);
+ return;
+ }
+
+ BeginReadRequest();
+ }
+
+ private void OnReadInternalException(MemoryStream ms, Exception ex)
+ {
+ //_logger.ErrorException("Error in HttpConnection.OnReadInternal", ex);
+
+ if (ms != null && ms.Length > 0)
+ SendError();
+ if (sock != null)
+ {
+ CloseSocket();
+ Unbind();
+ }
+ }
+
+ void RemoveConnection()
+ {
+ if (last_listener == null)
+ epl.RemoveConnection(this);
+ else
+ last_listener.RemoveConnection(this);
+ }
+
+ enum InputState
+ {
+ RequestLine,
+ Headers
+ }
+
+ enum LineState
+ {
+ None,
+ CR,
+ LF
+ }
+
+ InputState input_state = InputState.RequestLine;
+ LineState line_state = LineState.None;
+ int position;
+
+ // true -> done processing
+ // false -> need more input
+ bool ProcessInput(MemoryStream ms)
+ {
+ byte[] buffer;
+ _memoryStreamFactory.TryGetBuffer(ms, out buffer);
+
+ int len = (int)ms.Length;
+ int used = 0;
+ string line;
+
+ while (true)
+ {
+ if (context.HaveError)
+ return true;
+
+ if (position >= len)
+ break;
+
+ try
+ {
+ line = ReadLine(buffer, position, len - position, ref used);
+ position += used;
+ }
+ catch
+ {
+ context.ErrorMessage = "Bad request";
+ context.ErrorStatus = 400;
+ return true;
+ }
+
+ if (line == null)
+ break;
+
+ if (line == "")
+ {
+ if (input_state == InputState.RequestLine)
+ continue;
+ current_line = null;
+ ms = null;
+ return true;
+ }
+
+ if (input_state == InputState.RequestLine)
+ {
+ context.Request.SetRequestLine(line);
+ input_state = InputState.Headers;
+ }
+ else
+ {
+ try
+ {
+ context.Request.AddHeader(line);
+ }
+ catch (Exception e)
+ {
+ context.ErrorMessage = e.Message;
+ context.ErrorStatus = 400;
+ return true;
+ }
+ }
+ }
+
+ if (used == len)
+ {
+ ms.SetLength(0);
+ position = 0;
+ }
+ return false;
+ }
+
+ string ReadLine(byte[] buffer, int offset, int len, ref int used)
+ {
+ if (current_line == null)
+ current_line = new StringBuilder(128);
+ int last = offset + len;
+ used = 0;
+
+ for (int i = offset; i < last && line_state != LineState.LF; i++)
+ {
+ used++;
+ byte b = buffer[i];
+ if (b == 13)
+ {
+ line_state = LineState.CR;
+ }
+ else if (b == 10)
+ {
+ line_state = LineState.LF;
+ }
+ else
+ {
+ current_line.Append((char)b);
+ }
+ }
+
+ string result = null;
+ if (line_state == LineState.LF)
+ {
+ line_state = LineState.None;
+ result = current_line.ToString();
+ current_line.Length = 0;
+ }
+
+ return result;
+ }
+
+ public void SendError(string msg, int status)
+ {
+ try
+ {
+ HttpListenerResponse response = context.Response;
+ response.StatusCode = status;
+ response.ContentType = "text/html";
+ string description = HttpListenerResponse.GetStatusDescription(status);
+ string str;
+ if (msg != null)
+ str = String.Format("<h1>{0} ({1})</h1>", description, msg);
+ else
+ str = String.Format("<h1>{0}</h1>", description);
+
+ byte[] error = context.Response.ContentEncoding.GetBytes(str);
+ response.ContentLength64 = error.Length;
+ response.OutputStream.Write(error, 0, (int)error.Length);
+ response.Close();
+ }
+ catch
+ {
+ // response was already closed
+ }
+ }
+
+ public void SendError()
+ {
+ SendError(context.ErrorMessage, context.ErrorStatus);
+ }
+
+ void Unbind()
+ {
+ if (context_bound)
+ {
+ epl.UnbindContext(context);
+ context_bound = false;
+ }
+ }
+
+ public void Close()
+ {
+ Close(false);
+ }
+
+ private void CloseSocket()
+ {
+ if (sock == null)
+ return;
+
+ try
+ {
+ sock.Close();
+ }
+ catch
+ {
+ }
+ finally
+ {
+ sock = null;
+ }
+ RemoveConnection();
+ }
+
+ internal void Close(bool force_close)
+ {
+ if (sock != null)
+ {
+ if (!context.Request.IsWebSocketRequest || force_close)
+ {
+ Stream st = GetResponseStream();
+ if (st != null)
+ {
+ st.Dispose();
+ }
+
+ o_stream = null;
+ }
+ }
+
+ if (sock != null)
+ {
+ force_close |= !context.Request.KeepAlive;
+ if (!force_close)
+ force_close = (string.Equals(context.Response.Headers["connection"], "close", StringComparison.OrdinalIgnoreCase));
+ /*
+ if (!force_close) {
+// bool conn_close = (status_code == 400 || status_code == 408 || status_code == 411 ||
+// status_code == 413 || status_code == 414 || status_code == 500 ||
+// status_code == 503);
+ force_close |= (context.Request.ProtocolVersion <= HttpVersion.Version10);
+ }
+ */
+
+ if (!force_close && context.Request.FlushInput())
+ {
+ reuses++;
+ Unbind();
+ Init();
+ BeginReadRequest();
+ return;
+ }
+
+ IAcceptSocket s = sock;
+ sock = null;
+ try
+ {
+ if (s != null)
+ s.Shutdown(true);
+ }
+ catch
+ {
+ }
+ finally
+ {
+ if (s != null)
+ s.Close();
+ }
+ Unbind();
+ RemoveConnection();
+ return;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/SocketHttpListener/Net/HttpListener.cs b/SocketHttpListener/Net/HttpListener.cs
new file mode 100644
index 000000000..b3e01425c
--- /dev/null
+++ b/SocketHttpListener/Net/HttpListener.cs
@@ -0,0 +1,293 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Model.Cryptography;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.System;
+using MediaBrowser.Model.Text;
+using SocketHttpListener.Primitives;
+
+namespace SocketHttpListener.Net
+{
+ public sealed class HttpListener : IDisposable
+ {
+ internal ICryptoProvider CryptoProvider { get; private set; }
+ internal IStreamFactory StreamFactory { get; private set; }
+ internal ISocketFactory SocketFactory { get; private set; }
+ internal IFileSystem FileSystem { get; private set; }
+ internal ITextEncoding TextEncoding { get; private set; }
+ internal IMemoryStreamFactory MemoryStreamFactory { get; private set; }
+ internal INetworkManager NetworkManager { get; private set; }
+ internal IEnvironmentInfo EnvironmentInfo { get; private set; }
+
+ public bool EnableDualMode { get; set; }
+
+ AuthenticationSchemes auth_schemes;
+ HttpListenerPrefixCollection prefixes;
+ AuthenticationSchemeSelector auth_selector;
+ string realm;
+ bool unsafe_ntlm_auth;
+ bool listening;
+ bool disposed;
+
+ Dictionary<HttpListenerContext, HttpListenerContext> registry; // Dictionary<HttpListenerContext,HttpListenerContext>
+ Dictionary<HttpConnection, HttpConnection> connections;
+ private ILogger _logger;
+ private ICertificate _certificate;
+
+ public Action<HttpListenerContext> OnContext { get; set; }
+
+ public HttpListener(ILogger logger, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, ISocketFactory socketFactory, INetworkManager networkManager, ITextEncoding textEncoding, IMemoryStreamFactory memoryStreamFactory, IFileSystem fileSystem, IEnvironmentInfo environmentInfo)
+ {
+ _logger = logger;
+ CryptoProvider = cryptoProvider;
+ StreamFactory = streamFactory;
+ SocketFactory = socketFactory;
+ NetworkManager = networkManager;
+ TextEncoding = textEncoding;
+ MemoryStreamFactory = memoryStreamFactory;
+ FileSystem = fileSystem;
+ EnvironmentInfo = environmentInfo;
+ prefixes = new HttpListenerPrefixCollection(logger, this);
+ registry = new Dictionary<HttpListenerContext, HttpListenerContext>();
+ connections = new Dictionary<HttpConnection, HttpConnection>();
+ auth_schemes = AuthenticationSchemes.Anonymous;
+ }
+
+ public HttpListener(ICertificate certificate, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, ISocketFactory socketFactory, INetworkManager networkManager, ITextEncoding textEncoding, IMemoryStreamFactory memoryStreamFactory, IFileSystem fileSystem, IEnvironmentInfo environmentInfo)
+ :this(new NullLogger(), certificate, cryptoProvider, streamFactory, socketFactory, networkManager, textEncoding, memoryStreamFactory, fileSystem, environmentInfo)
+ {
+ }
+
+ public HttpListener(ILogger logger, ICertificate certificate, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, ISocketFactory socketFactory, INetworkManager networkManager, ITextEncoding textEncoding, IMemoryStreamFactory memoryStreamFactory, IFileSystem fileSystem, IEnvironmentInfo environmentInfo)
+ : this(logger, cryptoProvider, streamFactory, socketFactory, networkManager, textEncoding, memoryStreamFactory, fileSystem, environmentInfo)
+ {
+ _certificate = certificate;
+ }
+
+ public void LoadCert(ICertificate cert)
+ {
+ _certificate = cert;
+ }
+
+ // TODO: Digest, NTLM and Negotiate require ControlPrincipal
+ public AuthenticationSchemes AuthenticationSchemes
+ {
+ get { return auth_schemes; }
+ set
+ {
+ CheckDisposed();
+ auth_schemes = value;
+ }
+ }
+
+ public AuthenticationSchemeSelector AuthenticationSchemeSelectorDelegate
+ {
+ get { return auth_selector; }
+ set
+ {
+ CheckDisposed();
+ auth_selector = value;
+ }
+ }
+
+ public bool IsListening
+ {
+ get { return listening; }
+ }
+
+ public static bool IsSupported
+ {
+ get { return true; }
+ }
+
+ public HttpListenerPrefixCollection Prefixes
+ {
+ get
+ {
+ CheckDisposed();
+ return prefixes;
+ }
+ }
+
+ // TODO: use this
+ public string Realm
+ {
+ get { return realm; }
+ set
+ {
+ CheckDisposed();
+ realm = value;
+ }
+ }
+
+ public bool UnsafeConnectionNtlmAuthentication
+ {
+ get { return unsafe_ntlm_auth; }
+ set
+ {
+ CheckDisposed();
+ unsafe_ntlm_auth = value;
+ }
+ }
+
+ //internal IMonoSslStream CreateSslStream(Stream innerStream, bool ownsStream, MSI.MonoRemoteCertificateValidationCallback callback)
+ //{
+ // lock (registry)
+ // {
+ // if (tlsProvider == null)
+ // tlsProvider = MonoTlsProviderFactory.GetProviderInternal();
+ // if (tlsSettings == null)
+ // tlsSettings = MSI.MonoTlsSettings.CopyDefaultSettings();
+ // if (tlsSettings.RemoteCertificateValidationCallback == null)
+ // tlsSettings.RemoteCertificateValidationCallback = callback;
+ // return tlsProvider.CreateSslStream(innerStream, ownsStream, tlsSettings);
+ // }
+ //}
+
+ internal ICertificate Certificate
+ {
+ get { return _certificate; }
+ }
+
+ public void Abort()
+ {
+ if (disposed)
+ return;
+
+ if (!listening)
+ {
+ return;
+ }
+
+ Close(true);
+ }
+
+ public void Close()
+ {
+ if (disposed)
+ return;
+
+ if (!listening)
+ {
+ disposed = true;
+ return;
+ }
+
+ Close(true);
+ disposed = true;
+ }
+
+ void Close(bool force)
+ {
+ CheckDisposed();
+ EndPointManager.RemoveListener(_logger, this);
+ Cleanup(force);
+ }
+
+ void Cleanup(bool close_existing)
+ {
+ lock (registry)
+ {
+ if (close_existing)
+ {
+ // Need to copy this since closing will call UnregisterContext
+ ICollection keys = registry.Keys;
+ var all = new HttpListenerContext[keys.Count];
+ keys.CopyTo(all, 0);
+ registry.Clear();
+ for (int i = all.Length - 1; i >= 0; i--)
+ all[i].Connection.Close(true);
+ }
+
+ lock (connections)
+ {
+ ICollection keys = connections.Keys;
+ var conns = new HttpConnection[keys.Count];
+ keys.CopyTo(conns, 0);
+ connections.Clear();
+ for (int i = conns.Length - 1; i >= 0; i--)
+ conns[i].Close(true);
+ }
+ }
+ }
+
+ internal AuthenticationSchemes SelectAuthenticationScheme(HttpListenerContext context)
+ {
+ if (AuthenticationSchemeSelectorDelegate != null)
+ return AuthenticationSchemeSelectorDelegate(context.Request);
+ else
+ return auth_schemes;
+ }
+
+ public void Start()
+ {
+ CheckDisposed();
+ if (listening)
+ return;
+
+ EndPointManager.AddListener(_logger, this);
+ listening = true;
+ }
+
+ public void Stop()
+ {
+ CheckDisposed();
+ listening = false;
+ Close(false);
+ }
+
+ void IDisposable.Dispose()
+ {
+ if (disposed)
+ return;
+
+ Close(true); //TODO: Should we force here or not?
+ disposed = true;
+ }
+
+ internal void CheckDisposed()
+ {
+ if (disposed)
+ throw new ObjectDisposedException(GetType().ToString());
+ }
+
+ internal void RegisterContext(HttpListenerContext context)
+ {
+ if (OnContext != null && IsListening)
+ {
+ OnContext(context);
+ }
+
+ lock (registry)
+ registry[context] = context;
+ }
+
+ internal void UnregisterContext(HttpListenerContext context)
+ {
+ lock (registry)
+ registry.Remove(context);
+ }
+
+ internal void AddConnection(HttpConnection cnc)
+ {
+ lock (connections)
+ {
+ connections[cnc] = cnc;
+ }
+ }
+
+ internal void RemoveConnection(HttpConnection cnc)
+ {
+ lock (connections)
+ {
+ connections.Remove(cnc);
+ }
+ }
+ }
+}
diff --git a/SocketHttpListener/Net/HttpListenerBasicIdentity.cs b/SocketHttpListener/Net/HttpListenerBasicIdentity.cs
new file mode 100644
index 000000000..faa26693d
--- /dev/null
+++ b/SocketHttpListener/Net/HttpListenerBasicIdentity.cs
@@ -0,0 +1,70 @@
+using System.Security.Principal;
+
+namespace SocketHttpListener.Net
+{
+ public class HttpListenerBasicIdentity : GenericIdentity
+ {
+ string password;
+
+ public HttpListenerBasicIdentity(string username, string password)
+ : base(username, "Basic")
+ {
+ this.password = password;
+ }
+
+ public virtual string Password
+ {
+ get { return password; }
+ }
+ }
+
+ public class GenericIdentity : IIdentity
+ {
+ private string m_name;
+ private string m_type;
+
+ public GenericIdentity(string name)
+ {
+ if (name == null)
+ throw new System.ArgumentNullException("name");
+
+ m_name = name;
+ m_type = "";
+ }
+
+ public GenericIdentity(string name, string type)
+ {
+ if (name == null)
+ throw new System.ArgumentNullException("name");
+ if (type == null)
+ throw new System.ArgumentNullException("type");
+
+ m_name = name;
+ m_type = type;
+ }
+
+ public virtual string Name
+ {
+ get
+ {
+ return m_name;
+ }
+ }
+
+ public virtual string AuthenticationType
+ {
+ get
+ {
+ return m_type;
+ }
+ }
+
+ public virtual bool IsAuthenticated
+ {
+ get
+ {
+ return !m_name.Equals("");
+ }
+ }
+ }
+}
diff --git a/SocketHttpListener/Net/HttpListenerContext.cs b/SocketHttpListener/Net/HttpListenerContext.cs
new file mode 100644
index 000000000..58d769f22
--- /dev/null
+++ b/SocketHttpListener/Net/HttpListenerContext.cs
@@ -0,0 +1,198 @@
+using System;
+using System.Net;
+using System.Security.Principal;
+using MediaBrowser.Model.Cryptography;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Text;
+using SocketHttpListener.Net.WebSockets;
+using SocketHttpListener.Primitives;
+
+namespace SocketHttpListener.Net
+{
+ public sealed class HttpListenerContext
+ {
+ HttpListenerRequest request;
+ HttpListenerResponse response;
+ IPrincipal user;
+ HttpConnection cnc;
+ string error;
+ int err_status = 400;
+ private readonly ICryptoProvider _cryptoProvider;
+ private readonly IMemoryStreamFactory _memoryStreamFactory;
+ private readonly ITextEncoding _textEncoding;
+
+ internal HttpListenerContext(HttpConnection cnc, ILogger logger, ICryptoProvider cryptoProvider, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding, IFileSystem fileSystem)
+ {
+ this.cnc = cnc;
+ _cryptoProvider = cryptoProvider;
+ _memoryStreamFactory = memoryStreamFactory;
+ _textEncoding = textEncoding;
+ request = new HttpListenerRequest(this, _textEncoding);
+ response = new HttpListenerResponse(this, logger, _textEncoding, fileSystem);
+ }
+
+ internal int ErrorStatus
+ {
+ get { return err_status; }
+ set { err_status = value; }
+ }
+
+ internal string ErrorMessage
+ {
+ get { return error; }
+ set { error = value; }
+ }
+
+ internal bool HaveError
+ {
+ get { return (error != null); }
+ }
+
+ internal HttpConnection Connection
+ {
+ get { return cnc; }
+ }
+
+ public HttpListenerRequest Request
+ {
+ get { return request; }
+ }
+
+ public HttpListenerResponse Response
+ {
+ get { return response; }
+ }
+
+ public IPrincipal User
+ {
+ get { return user; }
+ }
+
+ internal void ParseAuthentication(AuthenticationSchemes expectedSchemes)
+ {
+ if (expectedSchemes == AuthenticationSchemes.Anonymous)
+ return;
+
+ // TODO: Handle NTLM/Digest modes
+ string header = request.Headers["Authorization"];
+ if (header == null || header.Length < 2)
+ return;
+
+ string[] authenticationData = header.Split(new char[] { ' ' }, 2);
+ if (string.Equals(authenticationData[0], "basic", StringComparison.OrdinalIgnoreCase))
+ {
+ user = ParseBasicAuthentication(authenticationData[1]);
+ }
+ // TODO: throw if malformed -> 400 bad request
+ }
+
+ internal IPrincipal ParseBasicAuthentication(string authData)
+ {
+ try
+ {
+ // Basic AUTH Data is a formatted Base64 String
+ //string domain = null;
+ string user = null;
+ string password = null;
+ int pos = -1;
+ var authDataBytes = Convert.FromBase64String(authData);
+ string authString = _textEncoding.GetDefaultEncoding().GetString(authDataBytes, 0, authDataBytes.Length);
+
+ // The format is DOMAIN\username:password
+ // Domain is optional
+
+ pos = authString.IndexOf(':');
+
+ // parse the password off the end
+ password = authString.Substring(pos + 1);
+
+ // discard the password
+ authString = authString.Substring(0, pos);
+
+ // check if there is a domain
+ pos = authString.IndexOf('\\');
+
+ if (pos > 0)
+ {
+ //domain = authString.Substring (0, pos);
+ user = authString.Substring(pos);
+ }
+ else
+ {
+ user = authString;
+ }
+
+ HttpListenerBasicIdentity identity = new HttpListenerBasicIdentity(user, password);
+ // TODO: What are the roles MS sets
+ return new GenericPrincipal(identity, new string[0]);
+ }
+ catch (Exception)
+ {
+ // Invalid auth data is swallowed silently
+ return null;
+ }
+ }
+
+ public HttpListenerWebSocketContext AcceptWebSocket(string protocol)
+ {
+ if (protocol != null)
+ {
+ if (protocol.Length == 0)
+ throw new ArgumentException("An empty string.", "protocol");
+
+ if (!protocol.IsToken())
+ throw new ArgumentException("Contains an invalid character.", "protocol");
+ }
+
+ return new HttpListenerWebSocketContext(this, protocol, _cryptoProvider, _memoryStreamFactory);
+ }
+ }
+
+ public class GenericPrincipal : IPrincipal
+ {
+ private IIdentity m_identity;
+ private string[] m_roles;
+
+ public GenericPrincipal(IIdentity identity, string[] roles)
+ {
+ if (identity == null)
+ throw new ArgumentNullException("identity");
+
+ m_identity = identity;
+ if (roles != null)
+ {
+ m_roles = new string[roles.Length];
+ for (int i = 0; i < roles.Length; ++i)
+ {
+ m_roles[i] = roles[i];
+ }
+ }
+ else
+ {
+ m_roles = null;
+ }
+ }
+
+ public virtual IIdentity Identity
+ {
+ get
+ {
+ return m_identity;
+ }
+ }
+
+ public virtual bool IsInRole(string role)
+ {
+ if (role == null || m_roles == null)
+ return false;
+
+ for (int i = 0; i < m_roles.Length; ++i)
+ {
+ if (m_roles[i] != null && String.Compare(m_roles[i], role, StringComparison.OrdinalIgnoreCase) == 0)
+ return true;
+ }
+ return false;
+ }
+ }
+}
diff --git a/SocketHttpListener/Net/HttpListenerPrefixCollection.cs b/SocketHttpListener/Net/HttpListenerPrefixCollection.cs
new file mode 100644
index 000000000..0b05539ee
--- /dev/null
+++ b/SocketHttpListener/Net/HttpListenerPrefixCollection.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using MediaBrowser.Model.Logging;
+
+namespace SocketHttpListener.Net
+{
+ public class HttpListenerPrefixCollection : ICollection<string>, IEnumerable<string>, IEnumerable
+ {
+ List<string> prefixes = new List<string>();
+ HttpListener listener;
+
+ private ILogger _logger;
+
+ internal HttpListenerPrefixCollection(ILogger logger, HttpListener listener)
+ {
+ _logger = logger;
+ this.listener = listener;
+ }
+
+ public int Count
+ {
+ get { return prefixes.Count; }
+ }
+
+ public bool IsReadOnly
+ {
+ get { return false; }
+ }
+
+ public bool IsSynchronized
+ {
+ get { return false; }
+ }
+
+ public void Add(string uriPrefix)
+ {
+ listener.CheckDisposed();
+ ListenerPrefix.CheckUri(uriPrefix);
+ if (prefixes.Contains(uriPrefix))
+ return;
+
+ prefixes.Add(uriPrefix);
+ if (listener.IsListening)
+ EndPointManager.AddPrefix(_logger, uriPrefix, listener);
+ }
+
+ public void Clear()
+ {
+ listener.CheckDisposed();
+ prefixes.Clear();
+ if (listener.IsListening)
+ EndPointManager.RemoveListener(_logger, listener);
+ }
+
+ public bool Contains(string uriPrefix)
+ {
+ listener.CheckDisposed();
+ return prefixes.Contains(uriPrefix);
+ }
+
+ public void CopyTo(string[] array, int offset)
+ {
+ listener.CheckDisposed();
+ prefixes.CopyTo(array, offset);
+ }
+
+ public void CopyTo(Array array, int offset)
+ {
+ listener.CheckDisposed();
+ ((ICollection)prefixes).CopyTo(array, offset);
+ }
+
+ public IEnumerator<string> GetEnumerator()
+ {
+ return prefixes.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return prefixes.GetEnumerator();
+ }
+
+ public bool Remove(string uriPrefix)
+ {
+ listener.CheckDisposed();
+ if (uriPrefix == null)
+ throw new ArgumentNullException("uriPrefix");
+
+ bool result = prefixes.Remove(uriPrefix);
+ if (result && listener.IsListening)
+ EndPointManager.RemovePrefix(_logger, uriPrefix, listener);
+
+ return result;
+ }
+ }
+}
diff --git a/SocketHttpListener/Net/HttpListenerRequest.cs b/SocketHttpListener/Net/HttpListenerRequest.cs
new file mode 100644
index 000000000..cfbd49203
--- /dev/null
+++ b/SocketHttpListener/Net/HttpListenerRequest.cs
@@ -0,0 +1,654 @@
+using System;
+using System.Collections.Specialized;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Services;
+using MediaBrowser.Model.Text;
+using SocketHttpListener.Primitives;
+
+namespace SocketHttpListener.Net
+{
+ public sealed class HttpListenerRequest
+ {
+ string[] accept_types;
+ Encoding content_encoding;
+ long content_length;
+ bool cl_set;
+ CookieCollection cookies;
+ WebHeaderCollection headers;
+ string method;
+ Stream input_stream;
+ Version version;
+ QueryParamCollection query_string; // check if null is ok, check if read-only, check case-sensitiveness
+ string raw_url;
+ Uri url;
+ Uri referrer;
+ string[] user_languages;
+ HttpListenerContext context;
+ bool is_chunked;
+ bool ka_set;
+ bool keep_alive;
+
+ private readonly ITextEncoding _textEncoding;
+
+ internal HttpListenerRequest(HttpListenerContext context, ITextEncoding textEncoding)
+ {
+ this.context = context;
+ _textEncoding = textEncoding;
+ headers = new WebHeaderCollection();
+ version = HttpVersion.Version10;
+ }
+
+ static char[] separators = new char[] { ' ' };
+
+ internal void SetRequestLine(string req)
+ {
+ string[] parts = req.Split(separators, 3);
+ if (parts.Length != 3)
+ {
+ context.ErrorMessage = "Invalid request line (parts).";
+ return;
+ }
+
+ method = parts[0];
+ foreach (char c in method)
+ {
+ int ic = (int)c;
+
+ if ((ic >= 'A' && ic <= 'Z') ||
+ (ic > 32 && c < 127 && c != '(' && c != ')' && c != '<' &&
+ c != '<' && c != '>' && c != '@' && c != ',' && c != ';' &&
+ c != ':' && c != '\\' && c != '"' && c != '/' && c != '[' &&
+ c != ']' && c != '?' && c != '=' && c != '{' && c != '}'))
+ continue;
+
+ context.ErrorMessage = "(Invalid verb)";
+ return;
+ }
+
+ raw_url = parts[1];
+ if (parts[2].Length != 8 || !parts[2].StartsWith("HTTP/"))
+ {
+ context.ErrorMessage = "Invalid request line (version).";
+ return;
+ }
+
+ try
+ {
+ version = new Version(parts[2].Substring(5));
+ if (version.Major < 1)
+ throw new Exception();
+ }
+ catch
+ {
+ context.ErrorMessage = "Invalid request line (version).";
+ return;
+ }
+ }
+
+ void CreateQueryString(string query)
+ {
+ if (query == null || query.Length == 0)
+ {
+ query_string = new QueryParamCollection();
+ return;
+ }
+
+ query_string = new QueryParamCollection();
+ if (query[0] == '?')
+ query = query.Substring(1);
+ string[] components = query.Split('&');
+ foreach (string kv in components)
+ {
+ int pos = kv.IndexOf('=');
+ if (pos == -1)
+ {
+ query_string.Add(null, WebUtility.UrlDecode(kv));
+ }
+ else
+ {
+ string key = WebUtility.UrlDecode(kv.Substring(0, pos));
+ string val = WebUtility.UrlDecode(kv.Substring(pos + 1));
+
+ query_string.Add(key, val);
+ }
+ }
+ }
+
+ internal void FinishInitialization()
+ {
+ string host = UserHostName;
+ if (version > HttpVersion.Version10 && (host == null || host.Length == 0))
+ {
+ context.ErrorMessage = "Invalid host name";
+ return;
+ }
+
+ string path;
+ Uri raw_uri = null;
+ if (MaybeUri(raw_url.ToLowerInvariant()) && Uri.TryCreate(raw_url, UriKind.Absolute, out raw_uri))
+ path = raw_uri.PathAndQuery;
+ else
+ path = raw_url;
+
+ if ((host == null || host.Length == 0))
+ host = UserHostAddress;
+
+ if (raw_uri != null)
+ host = raw_uri.Host;
+
+ int colon = host.LastIndexOf(':');
+ if (colon >= 0)
+ host = host.Substring(0, colon);
+
+ string base_uri = String.Format("{0}://{1}:{2}",
+ (IsSecureConnection) ? (IsWebSocketRequest ? "wss" : "https") : (IsWebSocketRequest ? "ws" : "http"),
+ host, LocalEndPoint.Port);
+
+ if (!Uri.TryCreate(base_uri + path, UriKind.Absolute, out url))
+ {
+ context.ErrorMessage = WebUtility.HtmlEncode("Invalid url: " + base_uri + path);
+ return; return;
+ }
+
+ CreateQueryString(url.Query);
+
+ if (version >= HttpVersion.Version11)
+ {
+ string t_encoding = Headers["Transfer-Encoding"];
+ is_chunked = (t_encoding != null && String.Compare(t_encoding, "chunked", StringComparison.OrdinalIgnoreCase) == 0);
+ // 'identity' is not valid!
+ if (t_encoding != null && !is_chunked)
+ {
+ context.Connection.SendError(null, 501);
+ return;
+ }
+ }
+
+ if (!is_chunked && !cl_set)
+ {
+ if (String.Compare(method, "POST", StringComparison.OrdinalIgnoreCase) == 0 ||
+ String.Compare(method, "PUT", StringComparison.OrdinalIgnoreCase) == 0)
+ {
+ context.Connection.SendError(null, 411);
+ return;
+ }
+ }
+
+ if (String.Compare(Headers["Expect"], "100-continue", StringComparison.OrdinalIgnoreCase) == 0)
+ {
+ var output = (ResponseStream)context.Connection.GetResponseStream(true);
+
+ var _100continue = _textEncoding.GetASCIIEncoding().GetBytes("HTTP/1.1 100 Continue\r\n\r\n");
+
+ output.InternalWrite(_100continue, 0, _100continue.Length);
+ }
+ }
+
+ static bool MaybeUri(string s)
+ {
+ int p = s.IndexOf(':');
+ if (p == -1)
+ return false;
+
+ if (p >= 10)
+ return false;
+
+ return IsPredefinedScheme(s.Substring(0, p));
+ }
+
+ //
+ // Using a simple block of if's is twice as slow as the compiler generated
+ // switch statement. But using this tuned code is faster than the
+ // compiler generated code, with a million loops on x86-64:
+ //
+ // With "http": .10 vs .51 (first check)
+ // with "https": .16 vs .51 (second check)
+ // with "foo": .22 vs .31 (never found)
+ // with "mailto": .12 vs .51 (last check)
+ //
+ //
+ static bool IsPredefinedScheme(string scheme)
+ {
+ if (scheme == null || scheme.Length < 3)
+ return false;
+
+ char c = scheme[0];
+ if (c == 'h')
+ return (scheme == "http" || scheme == "https");
+ if (c == 'f')
+ return (scheme == "file" || scheme == "ftp");
+
+ if (c == 'n')
+ {
+ c = scheme[1];
+ if (c == 'e')
+ return (scheme == "news" || scheme == "net.pipe" || scheme == "net.tcp");
+ if (scheme == "nntp")
+ return true;
+ return false;
+ }
+ if ((c == 'g' && scheme == "gopher") || (c == 'm' && scheme == "mailto"))
+ return true;
+
+ return false;
+ }
+
+ internal static string Unquote(String str)
+ {
+ int start = str.IndexOf('\"');
+ int end = str.LastIndexOf('\"');
+ if (start >= 0 && end >= 0)
+ str = str.Substring(start + 1, end - 1);
+ return str.Trim();
+ }
+
+ internal void AddHeader(string header)
+ {
+ int colon = header.IndexOf(':');
+ if (colon == -1 || colon == 0)
+ {
+ context.ErrorMessage = "Bad Request";
+ context.ErrorStatus = 400;
+ return;
+ }
+
+ string name = header.Substring(0, colon).Trim();
+ string val = header.Substring(colon + 1).Trim();
+ string lower = name.ToLowerInvariant();
+ headers.SetInternal(name, val);
+ switch (lower)
+ {
+ case "accept-language":
+ user_languages = val.Split(','); // yes, only split with a ','
+ break;
+ case "accept":
+ accept_types = val.Split(','); // yes, only split with a ','
+ break;
+ case "content-length":
+ try
+ {
+ //TODO: max. content_length?
+ content_length = Int64.Parse(val.Trim());
+ if (content_length < 0)
+ context.ErrorMessage = "Invalid Content-Length.";
+ cl_set = true;
+ }
+ catch
+ {
+ context.ErrorMessage = "Invalid Content-Length.";
+ }
+
+ break;
+ case "content-type":
+ {
+ var contents = val.Split(';');
+ foreach (var content in contents)
+ {
+ var tmp = content.Trim();
+ if (tmp.StartsWith("charset"))
+ {
+ var charset = tmp.GetValue("=");
+ if (charset != null && charset.Length > 0)
+ {
+ try
+ {
+
+ // Support upnp/dlna devices - CONTENT-TYPE: text/xml ; charset="utf-8"\r\n
+ charset = charset.Trim('"');
+ var index = charset.IndexOf('"');
+ if (index != -1) charset = charset.Substring(0, index);
+
+ content_encoding = Encoding.GetEncoding(charset);
+ }
+ catch
+ {
+ context.ErrorMessage = "Invalid Content-Type header: " + charset;
+ }
+ }
+
+ break;
+ }
+ }
+ }
+ break;
+ case "referer":
+ try
+ {
+ referrer = new Uri(val);
+ }
+ catch
+ {
+ referrer = new Uri("http://someone.is.screwing.with.the.headers.com/");
+ }
+ break;
+ case "cookie":
+ if (cookies == null)
+ cookies = new CookieCollection();
+
+ string[] cookieStrings = val.Split(new char[] { ',', ';' });
+ Cookie current = null;
+ int version = 0;
+ foreach (string cookieString in cookieStrings)
+ {
+ string str = cookieString.Trim();
+ if (str.Length == 0)
+ continue;
+ if (str.StartsWith("$Version"))
+ {
+ version = Int32.Parse(Unquote(str.Substring(str.IndexOf('=') + 1)));
+ }
+ else if (str.StartsWith("$Path"))
+ {
+ if (current != null)
+ current.Path = str.Substring(str.IndexOf('=') + 1).Trim();
+ }
+ else if (str.StartsWith("$Domain"))
+ {
+ if (current != null)
+ current.Domain = str.Substring(str.IndexOf('=') + 1).Trim();
+ }
+ else if (str.StartsWith("$Port"))
+ {
+ if (current != null)
+ current.Port = str.Substring(str.IndexOf('=') + 1).Trim();
+ }
+ else
+ {
+ if (current != null)
+ {
+ cookies.Add(current);
+ }
+ current = new Cookie();
+ int idx = str.IndexOf('=');
+ if (idx > 0)
+ {
+ current.Name = str.Substring(0, idx).Trim();
+ current.Value = str.Substring(idx + 1).Trim();
+ }
+ else
+ {
+ current.Name = str.Trim();
+ current.Value = String.Empty;
+ }
+ current.Version = version;
+ }
+ }
+ if (current != null)
+ {
+ cookies.Add(current);
+ }
+ break;
+ }
+ }
+
+ // returns true is the stream could be reused.
+ internal bool FlushInput()
+ {
+ if (!HasEntityBody)
+ return true;
+
+ int length = 2048;
+ if (content_length > 0)
+ length = (int)Math.Min(content_length, (long)length);
+
+ byte[] bytes = new byte[length];
+ while (true)
+ {
+ // TODO: test if MS has a timeout when doing this
+ try
+ {
+ var task = InputStream.ReadAsync(bytes, 0, length);
+ var result = Task.WaitAll(new [] { task }, 1000);
+ if (!result)
+ {
+ return false;
+ }
+ if (task.Result <= 0)
+ {
+ return true;
+ }
+ }
+ catch (ObjectDisposedException e)
+ {
+ input_stream = null;
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ }
+
+ public string[] AcceptTypes
+ {
+ get { return accept_types; }
+ }
+
+ public int ClientCertificateError
+ {
+ get
+ {
+ HttpConnection cnc = context.Connection;
+ //if (cnc.ClientCertificate == null)
+ // throw new InvalidOperationException("No client certificate");
+ //int[] errors = cnc.ClientCertificateErrors;
+ //if (errors != null && errors.Length > 0)
+ // return errors[0];
+ return 0;
+ }
+ }
+
+ public Encoding ContentEncoding
+ {
+ get
+ {
+ if (content_encoding == null)
+ content_encoding = _textEncoding.GetDefaultEncoding();
+ return content_encoding;
+ }
+ }
+
+ public long ContentLength64
+ {
+ get { return is_chunked ? -1 : content_length; }
+ }
+
+ public string ContentType
+ {
+ get { return headers["content-type"]; }
+ }
+
+ public CookieCollection Cookies
+ {
+ get
+ {
+ // TODO: check if the collection is read-only
+ if (cookies == null)
+ cookies = new CookieCollection();
+ return cookies;
+ }
+ }
+
+ public bool HasEntityBody
+ {
+ get { return (content_length > 0 || is_chunked); }
+ }
+
+ public QueryParamCollection Headers
+ {
+ get { return headers; }
+ }
+
+ public string HttpMethod
+ {
+ get { return method; }
+ }
+
+ public Stream InputStream
+ {
+ get
+ {
+ if (input_stream == null)
+ {
+ if (is_chunked || content_length > 0)
+ input_stream = context.Connection.GetRequestStream(is_chunked, content_length);
+ else
+ input_stream = Stream.Null;
+ }
+
+ return input_stream;
+ }
+ }
+
+ public bool IsAuthenticated
+ {
+ get { return false; }
+ }
+
+ public bool IsLocal
+ {
+ get { return RemoteEndPoint.IpAddress.Equals(IpAddressInfo.Loopback) || RemoteEndPoint.IpAddress.Equals(IpAddressInfo.IPv6Loopback) || LocalEndPoint.IpAddress.Equals(RemoteEndPoint.IpAddress); }
+ }
+
+ public bool IsSecureConnection
+ {
+ get { return context.Connection.IsSecure; }
+ }
+
+ public bool KeepAlive
+ {
+ get
+ {
+ if (ka_set)
+ return keep_alive;
+
+ ka_set = true;
+ // 1. Connection header
+ // 2. Protocol (1.1 == keep-alive by default)
+ // 3. Keep-Alive header
+ string cnc = headers["Connection"];
+ if (!String.IsNullOrEmpty(cnc))
+ {
+ keep_alive = (0 == String.Compare(cnc, "keep-alive", StringComparison.OrdinalIgnoreCase));
+ }
+ else if (version == HttpVersion.Version11)
+ {
+ keep_alive = true;
+ }
+ else
+ {
+ cnc = headers["keep-alive"];
+ if (!String.IsNullOrEmpty(cnc))
+ keep_alive = (0 != String.Compare(cnc, "closed", StringComparison.OrdinalIgnoreCase));
+ }
+ return keep_alive;
+ }
+ }
+
+ public IpEndPointInfo LocalEndPoint
+ {
+ get { return context.Connection.LocalEndPoint; }
+ }
+
+ public Version ProtocolVersion
+ {
+ get { return version; }
+ }
+
+ public QueryParamCollection QueryString
+ {
+ get { return query_string; }
+ }
+
+ public string RawUrl
+ {
+ get { return raw_url; }
+ }
+
+ public IpEndPointInfo RemoteEndPoint
+ {
+ get { return context.Connection.RemoteEndPoint; }
+ }
+
+ public Guid RequestTraceIdentifier
+ {
+ get { return Guid.Empty; }
+ }
+
+ public Uri Url
+ {
+ get { return url; }
+ }
+
+ public Uri UrlReferrer
+ {
+ get { return referrer; }
+ }
+
+ public string UserAgent
+ {
+ get { return headers["user-agent"]; }
+ }
+
+ public string UserHostAddress
+ {
+ get { return LocalEndPoint.ToString(); }
+ }
+
+ public string UserHostName
+ {
+ get { return headers["host"]; }
+ }
+
+ public string[] UserLanguages
+ {
+ get { return user_languages; }
+ }
+
+ public string ServiceName
+ {
+ get
+ {
+ return null;
+ }
+ }
+
+ private bool _websocketRequestWasSet;
+ private bool _websocketRequest;
+
+ /// <summary>
+ /// Gets a value indicating whether the request is a WebSocket connection request.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if the request is a WebSocket connection request; otherwise, <c>false</c>.
+ /// </value>
+ public bool IsWebSocketRequest
+ {
+ get
+ {
+ if (!_websocketRequestWasSet)
+ {
+ _websocketRequest = method == "GET" &&
+ version > HttpVersion.Version10 &&
+ headers.Contains("Upgrade", "websocket") &&
+ headers.Contains("Connection", "Upgrade");
+
+ _websocketRequestWasSet = true;
+ }
+
+ return _websocketRequest;
+ }
+ }
+
+ public Task<ICertificate> GetClientCertificateAsync()
+ {
+ return Task.FromResult<ICertificate>(null);
+ }
+ }
+}
diff --git a/SocketHttpListener/Net/HttpListenerResponse.cs b/SocketHttpListener/Net/HttpListenerResponse.cs
new file mode 100644
index 000000000..3cb6a0d75
--- /dev/null
+++ b/SocketHttpListener/Net/HttpListenerResponse.cs
@@ -0,0 +1,525 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Text;
+using SocketHttpListener.Primitives;
+
+namespace SocketHttpListener.Net
+{
+ public sealed class HttpListenerResponse : IDisposable
+ {
+ bool disposed;
+ Encoding content_encoding;
+ long content_length;
+ bool cl_set;
+ string content_type;
+ CookieCollection cookies;
+ WebHeaderCollection headers = new WebHeaderCollection();
+ bool keep_alive = true;
+ Stream output_stream;
+ Version version = HttpVersion.Version11;
+ string location;
+ int status_code = 200;
+ string status_description = "OK";
+ bool chunked;
+ HttpListenerContext context;
+
+ internal bool HeadersSent;
+ internal object headers_lock = new object();
+
+ private readonly ILogger _logger;
+ private readonly ITextEncoding _textEncoding;
+ private readonly IFileSystem _fileSystem;
+
+ internal HttpListenerResponse(HttpListenerContext context, ILogger logger, ITextEncoding textEncoding, IFileSystem fileSystem)
+ {
+ this.context = context;
+ _logger = logger;
+ _textEncoding = textEncoding;
+ _fileSystem = fileSystem;
+ }
+
+ internal bool CloseConnection
+ {
+ get
+ {
+ return headers["Connection"] == "close";
+ }
+ }
+
+ public Encoding ContentEncoding
+ {
+ get
+ {
+ if (content_encoding == null)
+ content_encoding = _textEncoding.GetDefaultEncoding();
+ return content_encoding;
+ }
+ set
+ {
+ if (disposed)
+ throw new ObjectDisposedException(GetType().ToString());
+
+ content_encoding = value;
+ }
+ }
+
+ public long ContentLength64
+ {
+ get { return content_length; }
+ set
+ {
+ if (disposed)
+ throw new ObjectDisposedException(GetType().ToString());
+
+ if (HeadersSent)
+ throw new InvalidOperationException("Cannot be changed after headers are sent.");
+
+ if (value < 0)
+ throw new ArgumentOutOfRangeException("Must be >= 0", "value");
+
+ cl_set = true;
+ content_length = value;
+ }
+ }
+
+ public string ContentType
+ {
+ get { return content_type; }
+ set
+ {
+ // TODO: is null ok?
+ if (disposed)
+ throw new ObjectDisposedException(GetType().ToString());
+
+ content_type = value;
+ }
+ }
+
+ // RFC 2109, 2965 + the netscape specification at http://wp.netscape.com/newsref/std/cookie_spec.html
+ public CookieCollection Cookies
+ {
+ get
+ {
+ if (cookies == null)
+ cookies = new CookieCollection();
+ return cookies;
+ }
+ set { cookies = value; } // null allowed?
+ }
+
+ public WebHeaderCollection Headers
+ {
+ get { return headers; }
+ set
+ {
+ /**
+ * "If you attempt to set a Content-Length, Keep-Alive, Transfer-Encoding, or
+ * WWW-Authenticate header using the Headers property, an exception will be
+ * thrown. Use the KeepAlive or ContentLength64 properties to set these headers.
+ * You cannot set the Transfer-Encoding or WWW-Authenticate headers manually."
+ */
+ // TODO: check if this is marked readonly after headers are sent.
+ headers = value;
+ }
+ }
+
+ public bool KeepAlive
+ {
+ get { return keep_alive; }
+ set
+ {
+ if (disposed)
+ throw new ObjectDisposedException(GetType().ToString());
+
+ keep_alive = value;
+ }
+ }
+
+ public Stream OutputStream
+ {
+ get
+ {
+ if (output_stream == null)
+ output_stream = context.Connection.GetResponseStream();
+ return output_stream;
+ }
+ }
+
+ public Version ProtocolVersion
+ {
+ get { return version; }
+ set
+ {
+ if (disposed)
+ throw new ObjectDisposedException(GetType().ToString());
+
+ if (value == null)
+ throw new ArgumentNullException("value");
+
+ if (value.Major != 1 || (value.Minor != 0 && value.Minor != 1))
+ throw new ArgumentException("Must be 1.0 or 1.1", "value");
+
+ if (disposed)
+ throw new ObjectDisposedException(GetType().ToString());
+
+ version = value;
+ }
+ }
+
+ public string RedirectLocation
+ {
+ get { return location; }
+ set
+ {
+ if (disposed)
+ throw new ObjectDisposedException(GetType().ToString());
+
+ location = value;
+ }
+ }
+
+ public bool SendChunked
+ {
+ get { return chunked; }
+ set
+ {
+ if (disposed)
+ throw new ObjectDisposedException(GetType().ToString());
+
+ chunked = value;
+ }
+ }
+
+ public int StatusCode
+ {
+ get { return status_code; }
+ set
+ {
+ if (disposed)
+ throw new ObjectDisposedException(GetType().ToString());
+
+ if (value < 100 || value > 999)
+ throw new ProtocolViolationException("StatusCode must be between 100 and 999.");
+ status_code = value;
+ status_description = GetStatusDescription(value);
+ }
+ }
+
+ internal static string GetStatusDescription(int code)
+ {
+ switch (code)
+ {
+ case 100: return "Continue";
+ case 101: return "Switching Protocols";
+ case 102: return "Processing";
+ case 200: return "OK";
+ case 201: return "Created";
+ case 202: return "Accepted";
+ case 203: return "Non-Authoritative Information";
+ case 204: return "No Content";
+ case 205: return "Reset Content";
+ case 206: return "Partial Content";
+ case 207: return "Multi-Status";
+ case 300: return "Multiple Choices";
+ case 301: return "Moved Permanently";
+ case 302: return "Found";
+ case 303: return "See Other";
+ case 304: return "Not Modified";
+ case 305: return "Use Proxy";
+ case 307: return "Temporary Redirect";
+ case 400: return "Bad Request";
+ case 401: return "Unauthorized";
+ case 402: return "Payment Required";
+ case 403: return "Forbidden";
+ case 404: return "Not Found";
+ case 405: return "Method Not Allowed";
+ case 406: return "Not Acceptable";
+ case 407: return "Proxy Authentication Required";
+ case 408: return "Request Timeout";
+ case 409: return "Conflict";
+ case 410: return "Gone";
+ case 411: return "Length Required";
+ case 412: return "Precondition Failed";
+ case 413: return "Request Entity Too Large";
+ case 414: return "Request-Uri Too Long";
+ case 415: return "Unsupported Media Type";
+ case 416: return "Requested Range Not Satisfiable";
+ case 417: return "Expectation Failed";
+ case 422: return "Unprocessable Entity";
+ case 423: return "Locked";
+ case 424: return "Failed Dependency";
+ case 500: return "Internal Server Error";
+ case 501: return "Not Implemented";
+ case 502: return "Bad Gateway";
+ case 503: return "Service Unavailable";
+ case 504: return "Gateway Timeout";
+ case 505: return "Http Version Not Supported";
+ case 507: return "Insufficient Storage";
+ }
+ return "";
+ }
+
+ public string StatusDescription
+ {
+ get { return status_description; }
+ set
+ {
+ status_description = value;
+ }
+ }
+
+ void IDisposable.Dispose()
+ {
+ Close(true); //TODO: Abort or Close?
+ }
+
+ public void Abort()
+ {
+ if (disposed)
+ return;
+
+ Close(true);
+ }
+
+ public void AddHeader(string name, string value)
+ {
+ if (name == null)
+ throw new ArgumentNullException("name");
+
+ if (name == "")
+ throw new ArgumentException("'name' cannot be empty", "name");
+
+ //TODO: check for forbidden headers and invalid characters
+ if (value.Length > 65535)
+ throw new ArgumentOutOfRangeException("value");
+
+ headers.Set(name, value);
+ }
+
+ public void AppendCookie(Cookie cookie)
+ {
+ if (cookie == null)
+ throw new ArgumentNullException("cookie");
+
+ Cookies.Add(cookie);
+ }
+
+ public void AppendHeader(string name, string value)
+ {
+ if (name == null)
+ throw new ArgumentNullException("name");
+
+ if (name == "")
+ throw new ArgumentException("'name' cannot be empty", "name");
+
+ if (value.Length > 65535)
+ throw new ArgumentOutOfRangeException("value");
+
+ headers.Add(name, value);
+ }
+
+ private void Close(bool force)
+ {
+ if (force)
+ {
+ _logger.Debug("HttpListenerResponse force closing HttpConnection");
+ }
+ disposed = true;
+ context.Connection.Close(force);
+ }
+
+ public void Close()
+ {
+ if (disposed)
+ return;
+
+ Close(false);
+ }
+
+ public void Redirect(string url)
+ {
+ StatusCode = 302; // Found
+ location = url;
+ }
+
+ bool FindCookie(Cookie cookie)
+ {
+ string name = cookie.Name;
+ string domain = cookie.Domain;
+ string path = cookie.Path;
+ foreach (Cookie c in cookies)
+ {
+ if (name != c.Name)
+ continue;
+ if (domain != c.Domain)
+ continue;
+ if (path == c.Path)
+ return true;
+ }
+
+ return false;
+ }
+
+ public void DetermineIfChunked()
+ {
+ if (chunked)
+ {
+ return;
+ }
+
+ Version v = context.Request.ProtocolVersion;
+ if (!cl_set && !chunked && v >= HttpVersion.Version11)
+ chunked = true;
+ if (!chunked && string.Equals(headers["Transfer-Encoding"], "chunked"))
+ {
+ chunked = true;
+ }
+ }
+
+ internal void SendHeaders(bool closing, MemoryStream ms)
+ {
+ Encoding encoding = content_encoding;
+ if (encoding == null)
+ encoding = _textEncoding.GetDefaultEncoding();
+
+ if (content_type != null)
+ {
+ if (content_encoding != null && content_type.IndexOf("charset=", StringComparison.OrdinalIgnoreCase) == -1)
+ {
+ string enc_name = content_encoding.WebName;
+ headers.SetInternal("Content-Type", content_type + "; charset=" + enc_name);
+ }
+ else
+ {
+ headers.SetInternal("Content-Type", content_type);
+ }
+ }
+
+ if (headers["Server"] == null)
+ headers.SetInternal("Server", "Mono-HTTPAPI/1.0");
+
+ CultureInfo inv = CultureInfo.InvariantCulture;
+ if (headers["Date"] == null)
+ headers.SetInternal("Date", DateTime.UtcNow.ToString("r", inv));
+
+ if (!chunked)
+ {
+ if (!cl_set && closing)
+ {
+ cl_set = true;
+ content_length = 0;
+ }
+
+ if (cl_set)
+ headers.SetInternal("Content-Length", content_length.ToString(inv));
+ }
+
+ Version v = context.Request.ProtocolVersion;
+ if (!cl_set && !chunked && v >= HttpVersion.Version11)
+ chunked = true;
+
+ /* Apache forces closing the connection for these status codes:
+ * HttpStatusCode.BadRequest 400
+ * HttpStatusCode.RequestTimeout 408
+ * HttpStatusCode.LengthRequired 411
+ * HttpStatusCode.RequestEntityTooLarge 413
+ * HttpStatusCode.RequestUriTooLong 414
+ * HttpStatusCode.InternalServerError 500
+ * HttpStatusCode.ServiceUnavailable 503
+ */
+ bool conn_close = status_code == 400 || status_code == 408 || status_code == 411 ||
+ status_code == 413 || status_code == 414 ||
+ status_code == 500 ||
+ status_code == 503;
+
+ if (conn_close == false)
+ conn_close = !context.Request.KeepAlive;
+
+ // They sent both KeepAlive: true and Connection: close!?
+ if (!keep_alive || conn_close)
+ {
+ headers.SetInternal("Connection", "close");
+ conn_close = true;
+ }
+
+ if (chunked)
+ headers.SetInternal("Transfer-Encoding", "chunked");
+
+ //int reuses = context.Connection.Reuses;
+ //if (reuses >= 100)
+ //{
+ // _logger.Debug("HttpListenerResponse - keep alive has exceeded 100 uses and will be closed.");
+
+ // force_close_chunked = true;
+ // if (!conn_close)
+ // {
+ // headers.SetInternal("Connection", "close");
+ // conn_close = true;
+ // }
+ //}
+
+ if (!conn_close)
+ {
+ if (context.Request.ProtocolVersion <= HttpVersion.Version10)
+ headers.SetInternal("Connection", "keep-alive");
+ }
+
+ if (location != null)
+ headers.SetInternal("Location", location);
+
+ if (cookies != null)
+ {
+ foreach (Cookie cookie in cookies)
+ headers.SetInternal("Set-Cookie", cookie.ToString());
+ }
+
+ headers.SetInternal("Status", status_code.ToString(CultureInfo.InvariantCulture));
+
+ using (StreamWriter writer = new StreamWriter(ms, encoding, 256, true))
+ {
+ writer.Write("HTTP/{0} {1} {2}\r\n", version, status_code, status_description);
+ string headers_str = headers.ToStringMultiValue();
+ writer.Write(headers_str);
+ writer.Flush();
+ }
+
+ int preamble = encoding.GetPreamble().Length;
+ if (output_stream == null)
+ output_stream = context.Connection.GetResponseStream();
+
+ /* Assumes that the ms was at position 0 */
+ ms.Position = preamble;
+ HeadersSent = true;
+ }
+
+ public void SetCookie(Cookie cookie)
+ {
+ if (cookie == null)
+ throw new ArgumentNullException("cookie");
+
+ if (cookies != null)
+ {
+ if (FindCookie(cookie))
+ throw new ArgumentException("The cookie already exists.");
+ }
+ else
+ {
+ cookies = new CookieCollection();
+ }
+
+ cookies.Add(cookie);
+ }
+
+ public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken)
+ {
+ return ((ResponseStream)OutputStream).TransmitFile(path, offset, count, fileShareMode, cancellationToken);
+ }
+ }
+} \ No newline at end of file
diff --git a/SocketHttpListener/Net/HttpRequestStream.Managed.cs b/SocketHttpListener/Net/HttpRequestStream.Managed.cs
new file mode 100644
index 000000000..cb02a4d5a
--- /dev/null
+++ b/SocketHttpListener/Net/HttpRequestStream.Managed.cs
@@ -0,0 +1,196 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.ExceptionServices;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SocketHttpListener.Net
+{
+ // Licensed to the .NET Foundation under one or more agreements.
+ // See the LICENSE file in the project root for more information.
+ //
+ // System.Net.ResponseStream
+ //
+ // Author:
+ // Gonzalo Paniagua Javier (gonzalo@novell.com)
+ //
+ // Copyright (c) 2005 Novell, Inc. (http://www.novell.com)
+ //
+ // Permission is hereby granted, free of charge, to any person obtaining
+ // a copy of this software and associated documentation files (the
+ // "Software"), to deal in the Software without restriction, including
+ // without limitation the rights to use, copy, modify, merge, publish,
+ // distribute, sublicense, and/or sell copies of the Software, and to
+ // permit persons to whom the Software is furnished to do so, subject to
+ // the following conditions:
+ //
+ // The above copyright notice and this permission notice shall be
+ // included in all copies or substantial portions of the Software.
+ //
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ //
+
+ internal partial class HttpRequestStream : Stream
+ {
+ private byte[] _buffer;
+ private int _offset;
+ private int _length;
+ private long _remainingBody;
+ protected bool _closed;
+ private Stream _stream;
+
+ internal HttpRequestStream(Stream stream, byte[] buffer, int offset, int length)
+ : this(stream, buffer, offset, length, -1)
+ {
+ }
+
+ internal HttpRequestStream(Stream stream, byte[] buffer, int offset, int length, long contentlength)
+ {
+ _stream = stream;
+ _buffer = buffer;
+ _offset = offset;
+ _length = length;
+ _remainingBody = contentlength;
+ }
+
+ // Returns 0 if we can keep reading from the base stream,
+ // > 0 if we read something from the buffer.
+ // -1 if we had a content length set and we finished reading that many bytes.
+ private int FillFromBuffer(byte[] buffer, int offset, int count)
+ {
+ if (_remainingBody == 0)
+ return -1;
+
+ if (_length == 0)
+ return 0;
+
+ int size = Math.Min(_length, count);
+ if (_remainingBody > 0)
+ size = (int)Math.Min(size, _remainingBody);
+
+ if (_offset > _buffer.Length - size)
+ {
+ size = Math.Min(size, _buffer.Length - _offset);
+ }
+ if (size == 0)
+ return 0;
+
+ Buffer.BlockCopy(_buffer, _offset, buffer, offset, size);
+ _offset += size;
+ _length -= size;
+ if (_remainingBody > 0)
+ _remainingBody -= size;
+ return size;
+ }
+
+ protected virtual int ReadCore(byte[] buffer, int offset, int size)
+ {
+ // Call FillFromBuffer to check for buffer boundaries even when remaining_body is 0
+ int nread = FillFromBuffer(buffer, offset, size);
+ if (nread == -1)
+ { // No more bytes available (Content-Length)
+ return 0;
+ }
+ else if (nread > 0)
+ {
+ return nread;
+ }
+
+ nread = _stream.Read(buffer, offset, size);
+ if (nread > 0 && _remainingBody > 0)
+ _remainingBody -= nread;
+ return nread;
+ }
+
+ protected virtual IAsyncResult BeginReadCore(byte[] buffer, int offset, int size, AsyncCallback cback, object state)
+ {
+ if (size == 0 || _closed)
+ {
+ HttpStreamAsyncResult ares = new HttpStreamAsyncResult(this);
+ ares._callback = cback;
+ ares._state = state;
+ ares.Complete();
+ return ares;
+ }
+
+ int nread = FillFromBuffer(buffer, offset, size);
+ if (nread > 0 || nread == -1)
+ {
+ HttpStreamAsyncResult ares = new HttpStreamAsyncResult(this);
+ ares._buffer = buffer;
+ ares._offset = offset;
+ ares._count = size;
+ ares._callback = cback;
+ ares._state = state;
+ ares._synchRead = Math.Max(0, nread);
+ ares.Complete();
+ return ares;
+ }
+
+ // Avoid reading past the end of the request to allow
+ // for HTTP pipelining
+ if (_remainingBody >= 0 && size > _remainingBody)
+ {
+ size = (int)Math.Min(int.MaxValue, _remainingBody);
+ }
+
+ return _stream.BeginRead(buffer, offset, size, cback, state);
+ }
+
+ public override int EndRead(IAsyncResult asyncResult)
+ {
+ if (asyncResult == null)
+ throw new ArgumentNullException(nameof(asyncResult));
+
+ var r = asyncResult as HttpStreamAsyncResult;
+
+ if (r != null)
+ {
+ if (!ReferenceEquals(this, r._parent))
+ {
+ throw new ArgumentException("Invalid async result");
+ }
+ if (r._endCalled)
+ {
+ throw new InvalidOperationException("Invalid end call");
+ }
+ r._endCalled = true;
+
+ if (!asyncResult.IsCompleted)
+ {
+ asyncResult.AsyncWaitHandle.WaitOne();
+ }
+
+ return r._synchRead;
+ }
+
+ if (_closed)
+ return 0;
+
+ int nread = 0;
+ try
+ {
+ nread = _stream.EndRead(asyncResult);
+ }
+ catch (IOException e) when (e.InnerException is ArgumentException || e.InnerException is InvalidOperationException)
+ {
+ throw e.InnerException;
+ }
+
+ if (_remainingBody > 0 && nread > 0)
+ {
+ _remainingBody -= nread;
+ }
+
+ return nread;
+ }
+ }
+}
diff --git a/SocketHttpListener/Net/HttpRequestStream.cs b/SocketHttpListener/Net/HttpRequestStream.cs
new file mode 100644
index 000000000..c54da44a1
--- /dev/null
+++ b/SocketHttpListener/Net/HttpRequestStream.cs
@@ -0,0 +1,144 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SocketHttpListener.Net
+{
+ // Licensed to the .NET Foundation under one or more agreements.
+ // See the LICENSE file in the project root for more information.
+ //
+ // System.Net.ResponseStream
+ //
+ // Author:
+ // Gonzalo Paniagua Javier (gonzalo@novell.com)
+ //
+ // Copyright (c) 2005 Novell, Inc. (http://www.novell.com)
+ //
+ // Permission is hereby granted, free of charge, to any person obtaining
+ // a copy of this software and associated documentation files (the
+ // "Software"), to deal in the Software without restriction, including
+ // without limitation the rights to use, copy, modify, merge, publish,
+ // distribute, sublicense, and/or sell copies of the Software, and to
+ // permit persons to whom the Software is furnished to do so, subject to
+ // the following conditions:
+ //
+ // The above copyright notice and this permission notice shall be
+ // included in all copies or substantial portions of the Software.
+ //
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ //
+
+ internal partial class HttpRequestStream : Stream
+ {
+ public override bool CanSeek => false;
+ public override bool CanWrite => false;
+ public override bool CanRead => true;
+
+ public override int Read(byte[] buffer, int offset, int size)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException(nameof(buffer));
+ }
+ if (offset < 0 || offset > buffer.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset));
+ }
+ if (size < 0 || size > buffer.Length - offset)
+ {
+ throw new ArgumentOutOfRangeException(nameof(size));
+ }
+ if (size == 0 || _closed)
+ {
+ return 0;
+ }
+
+ return ReadCore(buffer, offset, size);
+ }
+
+ public override IAsyncResult BeginRead(byte[] buffer, int offset, int size, AsyncCallback callback, object state)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException(nameof(buffer));
+ }
+ if (offset < 0 || offset > buffer.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset));
+ }
+ if (size < 0 || size > buffer.Length - offset)
+ {
+ throw new ArgumentOutOfRangeException(nameof(size));
+ }
+
+ return BeginReadCore(buffer, offset, size, callback, state);
+ }
+
+ public override void Flush() { }
+ public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ public override long Length
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public override long Position
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+
+ set
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ {
+ return base.BeginWrite(buffer, offset, count, callback, state);
+ }
+
+ public override void EndWrite(IAsyncResult asyncResult)
+ {
+ base.EndWrite(asyncResult);
+ }
+
+ internal bool Closed => _closed;
+
+ protected override void Dispose(bool disposing)
+ {
+ _closed = true;
+ base.Dispose(disposing);
+ }
+ }
+}
diff --git a/SocketHttpListener/Net/HttpStatusCode.cs b/SocketHttpListener/Net/HttpStatusCode.cs
new file mode 100644
index 000000000..93da82ba0
--- /dev/null
+++ b/SocketHttpListener/Net/HttpStatusCode.cs
@@ -0,0 +1,321 @@
+namespace SocketHttpListener.Net
+{
+ /// <summary>
+ /// Contains the values of the HTTP status codes.
+ /// </summary>
+ /// <remarks>
+ /// The HttpStatusCode enumeration contains the values of the HTTP status codes defined in
+ /// <see href="http://tools.ietf.org/html/rfc2616#section-10">RFC 2616</see> for HTTP 1.1.
+ /// </remarks>
+ public enum HttpStatusCode
+ {
+ /// <summary>
+ /// Equivalent to status code 100.
+ /// Indicates that the client should continue with its request.
+ /// </summary>
+ Continue = 100,
+ /// <summary>
+ /// Equivalent to status code 101.
+ /// Indicates that the server is switching the HTTP version or protocol on the connection.
+ /// </summary>
+ SwitchingProtocols = 101,
+ /// <summary>
+ /// Equivalent to status code 200.
+ /// Indicates that the client's request has succeeded.
+ /// </summary>
+ OK = 200,
+ /// <summary>
+ /// Equivalent to status code 201.
+ /// Indicates that the client's request has been fulfilled and resulted in a new resource being
+ /// created.
+ /// </summary>
+ Created = 201,
+ /// <summary>
+ /// Equivalent to status code 202.
+ /// Indicates that the client's request has been accepted for processing, but the processing
+ /// hasn't been completed.
+ /// </summary>
+ Accepted = 202,
+ /// <summary>
+ /// Equivalent to status code 203.
+ /// Indicates that the returned metainformation is from a local or a third-party copy instead of
+ /// the origin server.
+ /// </summary>
+ NonAuthoritativeInformation = 203,
+ /// <summary>
+ /// Equivalent to status code 204.
+ /// Indicates that the server has fulfilled the client's request but doesn't need to return
+ /// an entity-body.
+ /// </summary>
+ NoContent = 204,
+ /// <summary>
+ /// Equivalent to status code 205.
+ /// Indicates that the server has fulfilled the client's request, and the user agent should
+ /// reset the document view which caused the request to be sent.
+ /// </summary>
+ ResetContent = 205,
+ /// <summary>
+ /// Equivalent to status code 206.
+ /// Indicates that the server has fulfilled the partial GET request for the resource.
+ /// </summary>
+ PartialContent = 206,
+ /// <summary>
+ /// <para>
+ /// Equivalent to status code 300.
+ /// Indicates that the requested resource corresponds to any of multiple representations.
+ /// </para>
+ /// <para>
+ /// MultipleChoices is a synonym for Ambiguous.
+ /// </para>
+ /// </summary>
+ MultipleChoices = 300,
+ /// <summary>
+ /// <para>
+ /// Equivalent to status code 300.
+ /// Indicates that the requested resource corresponds to any of multiple representations.
+ /// </para>
+ /// <para>
+ /// Ambiguous is a synonym for MultipleChoices.
+ /// </para>
+ /// </summary>
+ Ambiguous = 300,
+ /// <summary>
+ /// <para>
+ /// Equivalent to status code 301.
+ /// Indicates that the requested resource has been assigned a new permanent URI and
+ /// any future references to this resource should use one of the returned URIs.
+ /// </para>
+ /// <para>
+ /// MovedPermanently is a synonym for Moved.
+ /// </para>
+ /// </summary>
+ MovedPermanently = 301,
+ /// <summary>
+ /// <para>
+ /// Equivalent to status code 301.
+ /// Indicates that the requested resource has been assigned a new permanent URI and
+ /// any future references to this resource should use one of the returned URIs.
+ /// </para>
+ /// <para>
+ /// Moved is a synonym for MovedPermanently.
+ /// </para>
+ /// </summary>
+ Moved = 301,
+ /// <summary>
+ /// <para>
+ /// Equivalent to status code 302.
+ /// Indicates that the requested resource is located temporarily under a different URI.
+ /// </para>
+ /// <para>
+ /// Found is a synonym for Redirect.
+ /// </para>
+ /// </summary>
+ Found = 302,
+ /// <summary>
+ /// <para>
+ /// Equivalent to status code 302.
+ /// Indicates that the requested resource is located temporarily under a different URI.
+ /// </para>
+ /// <para>
+ /// Redirect is a synonym for Found.
+ /// </para>
+ /// </summary>
+ Redirect = 302,
+ /// <summary>
+ /// <para>
+ /// Equivalent to status code 303.
+ /// Indicates that the response to the request can be found under a different URI and
+ /// should be retrieved using a GET method on that resource.
+ /// </para>
+ /// <para>
+ /// SeeOther is a synonym for RedirectMethod.
+ /// </para>
+ /// </summary>
+ SeeOther = 303,
+ /// <summary>
+ /// <para>
+ /// Equivalent to status code 303.
+ /// Indicates that the response to the request can be found under a different URI and
+ /// should be retrieved using a GET method on that resource.
+ /// </para>
+ /// <para>
+ /// RedirectMethod is a synonym for SeeOther.
+ /// </para>
+ /// </summary>
+ RedirectMethod = 303,
+ /// <summary>
+ /// Equivalent to status code 304.
+ /// Indicates that the client has performed a conditional GET request and access is allowed,
+ /// but the document hasn't been modified.
+ /// </summary>
+ NotModified = 304,
+ /// <summary>
+ /// Equivalent to status code 305.
+ /// Indicates that the requested resource must be accessed through the proxy given by
+ /// the Location field.
+ /// </summary>
+ UseProxy = 305,
+ /// <summary>
+ /// Equivalent to status code 306.
+ /// This status code was used in a previous version of the specification, is no longer used,
+ /// and is reserved for future use.
+ /// </summary>
+ Unused = 306,
+ /// <summary>
+ /// <para>
+ /// Equivalent to status code 307.
+ /// Indicates that the requested resource is located temporarily under a different URI.
+ /// </para>
+ /// <para>
+ /// TemporaryRedirect is a synonym for RedirectKeepVerb.
+ /// </para>
+ /// </summary>
+ TemporaryRedirect = 307,
+ /// <summary>
+ /// <para>
+ /// Equivalent to status code 307.
+ /// Indicates that the requested resource is located temporarily under a different URI.
+ /// </para>
+ /// <para>
+ /// RedirectKeepVerb is a synonym for TemporaryRedirect.
+ /// </para>
+ /// </summary>
+ RedirectKeepVerb = 307,
+ /// <summary>
+ /// Equivalent to status code 400.
+ /// Indicates that the client's request couldn't be understood by the server due to
+ /// malformed syntax.
+ /// </summary>
+ BadRequest = 400,
+ /// <summary>
+ /// Equivalent to status code 401.
+ /// Indicates that the client's request requires user authentication.
+ /// </summary>
+ Unauthorized = 401,
+ /// <summary>
+ /// Equivalent to status code 402.
+ /// This status code is reserved for future use.
+ /// </summary>
+ PaymentRequired = 402,
+ /// <summary>
+ /// Equivalent to status code 403.
+ /// Indicates that the server understood the client's request but is refusing to fulfill it.
+ /// </summary>
+ Forbidden = 403,
+ /// <summary>
+ /// Equivalent to status code 404.
+ /// Indicates that the server hasn't found anything matching the request URI.
+ /// </summary>
+ NotFound = 404,
+ /// <summary>
+ /// Equivalent to status code 405.
+ /// Indicates that the method specified in the request line isn't allowed for the resource
+ /// identified by the request URI.
+ /// </summary>
+ MethodNotAllowed = 405,
+ /// <summary>
+ /// Equivalent to status code 406.
+ /// Indicates that the server doesn't have the appropriate resource to respond to the Accept
+ /// headers in the client's request.
+ /// </summary>
+ NotAcceptable = 406,
+ /// <summary>
+ /// Equivalent to status code 407.
+ /// Indicates that the client must first authenticate itself with the proxy.
+ /// </summary>
+ ProxyAuthenticationRequired = 407,
+ /// <summary>
+ /// Equivalent to status code 408.
+ /// Indicates that the client didn't produce a request within the time that the server was
+ /// prepared to wait.
+ /// </summary>
+ RequestTimeout = 408,
+ /// <summary>
+ /// Equivalent to status code 409.
+ /// Indicates that the client's request couldn't be completed due to a conflict on the server.
+ /// </summary>
+ Conflict = 409,
+ /// <summary>
+ /// Equivalent to status code 410.
+ /// Indicates that the requested resource is no longer available at the server and
+ /// no forwarding address is known.
+ /// </summary>
+ Gone = 410,
+ /// <summary>
+ /// Equivalent to status code 411.
+ /// Indicates that the server refuses to accept the client's request without a defined
+ /// Content-Length.
+ /// </summary>
+ LengthRequired = 411,
+ /// <summary>
+ /// Equivalent to status code 412.
+ /// Indicates that the precondition given in one or more of the request headers evaluated to
+ /// false when it was tested on the server.
+ /// </summary>
+ PreconditionFailed = 412,
+ /// <summary>
+ /// Equivalent to status code 413.
+ /// Indicates that the entity of the client's request is larger than the server is willing or
+ /// able to process.
+ /// </summary>
+ RequestEntityTooLarge = 413,
+ /// <summary>
+ /// Equivalent to status code 414.
+ /// Indicates that the request URI is longer than the server is willing to interpret.
+ /// </summary>
+ RequestUriTooLong = 414,
+ /// <summary>
+ /// Equivalent to status code 415.
+ /// Indicates that the entity of the client's request is in a format not supported by
+ /// the requested resource for the requested method.
+ /// </summary>
+ UnsupportedMediaType = 415,
+ /// <summary>
+ /// Equivalent to status code 416.
+ /// Indicates that none of the range specifier values in a Range request header overlap
+ /// the current extent of the selected resource.
+ /// </summary>
+ RequestedRangeNotSatisfiable = 416,
+ /// <summary>
+ /// Equivalent to status code 417.
+ /// Indicates that the expectation given in an Expect request header couldn't be met by
+ /// the server.
+ /// </summary>
+ ExpectationFailed = 417,
+ /// <summary>
+ /// Equivalent to status code 500.
+ /// Indicates that the server encountered an unexpected condition which prevented it from
+ /// fulfilling the client's request.
+ /// </summary>
+ InternalServerError = 500,
+ /// <summary>
+ /// Equivalent to status code 501.
+ /// Indicates that the server doesn't support the functionality required to fulfill the client's
+ /// request.
+ /// </summary>
+ NotImplemented = 501,
+ /// <summary>
+ /// Equivalent to status code 502.
+ /// Indicates that a gateway or proxy server received an invalid response from the upstream
+ /// server.
+ /// </summary>
+ BadGateway = 502,
+ /// <summary>
+ /// Equivalent to status code 503.
+ /// Indicates that the server is currently unable to handle the client's request due to
+ /// a temporary overloading or maintenance of the server.
+ /// </summary>
+ ServiceUnavailable = 503,
+ /// <summary>
+ /// Equivalent to status code 504.
+ /// Indicates that a gateway or proxy server didn't receive a timely response from the upstream
+ /// server or some other auxiliary server.
+ /// </summary>
+ GatewayTimeout = 504,
+ /// <summary>
+ /// Equivalent to status code 505.
+ /// Indicates that the server doesn't support the HTTP version used in the client's request.
+ /// </summary>
+ HttpVersionNotSupported = 505,
+ }
+}
diff --git a/SocketHttpListener/Net/HttpStreamAsyncResult.cs b/SocketHttpListener/Net/HttpStreamAsyncResult.cs
new file mode 100644
index 000000000..e7e516c6b
--- /dev/null
+++ b/SocketHttpListener/Net/HttpStreamAsyncResult.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SocketHttpListener.Net
+{
+ internal class HttpStreamAsyncResult : IAsyncResult
+ {
+ private object _locker = new object();
+ private ManualResetEvent _handle;
+ private bool _completed;
+
+ internal readonly object _parent;
+ internal byte[] _buffer;
+ internal int _offset;
+ internal int _count;
+ internal AsyncCallback _callback;
+ internal object _state;
+ internal int _synchRead;
+ internal Exception _error;
+ internal bool _endCalled;
+
+ internal HttpStreamAsyncResult(object parent)
+ {
+ _parent = parent;
+ }
+
+ public void Complete(Exception e)
+ {
+ _error = e;
+ Complete();
+ }
+
+ public void Complete()
+ {
+ lock (_locker)
+ {
+ if (_completed)
+ return;
+
+ _completed = true;
+ if (_handle != null)
+ _handle.Set();
+
+ if (_callback != null)
+ Task.Run(() => _callback(this));
+ }
+ }
+
+ public object AsyncState
+ {
+ get { return _state; }
+ }
+
+ public WaitHandle AsyncWaitHandle
+ {
+ get
+ {
+ lock (_locker)
+ {
+ if (_handle == null)
+ _handle = new ManualResetEvent(_completed);
+ }
+
+ return _handle;
+ }
+ }
+
+ public bool CompletedSynchronously
+ {
+ get { return (_synchRead == _count); }
+ }
+
+ public bool IsCompleted
+ {
+ get
+ {
+ lock (_locker)
+ {
+ return _completed;
+ }
+ }
+ }
+ }
+}
diff --git a/SocketHttpListener/Net/HttpVersion.cs b/SocketHttpListener/Net/HttpVersion.cs
new file mode 100644
index 000000000..c0839b46d
--- /dev/null
+++ b/SocketHttpListener/Net/HttpVersion.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace SocketHttpListener.Net
+{
+ // <remarks>
+ // </remarks>
+ public class HttpVersion
+ {
+
+ public static readonly Version Version10 = new Version(1, 0);
+ public static readonly Version Version11 = new Version(1, 1);
+
+ // pretty useless..
+ public HttpVersion() { }
+ }
+}
diff --git a/SocketHttpListener/Net/ListenerPrefix.cs b/SocketHttpListener/Net/ListenerPrefix.cs
new file mode 100644
index 000000000..2c314da50
--- /dev/null
+++ b/SocketHttpListener/Net/ListenerPrefix.cs
@@ -0,0 +1,148 @@
+using System;
+using System.Net;
+using MediaBrowser.Model.Net;
+
+namespace SocketHttpListener.Net
+{
+ sealed class ListenerPrefix
+ {
+ string original;
+ string host;
+ ushort port;
+ string path;
+ bool secure;
+ IpAddressInfo[] addresses;
+ public HttpListener Listener;
+
+ public ListenerPrefix(string prefix)
+ {
+ this.original = prefix;
+ Parse(prefix);
+ }
+
+ public override string ToString()
+ {
+ return original;
+ }
+
+ public IpAddressInfo[] Addresses
+ {
+ get { return addresses; }
+ set { addresses = value; }
+ }
+ public bool Secure
+ {
+ get { return secure; }
+ }
+
+ public string Host
+ {
+ get { return host; }
+ }
+
+ public int Port
+ {
+ get { return (int)port; }
+ }
+
+ public string Path
+ {
+ get { return path; }
+ }
+
+ // Equals and GetHashCode are required to detect duplicates in HttpListenerPrefixCollection.
+ public override bool Equals(object o)
+ {
+ ListenerPrefix other = o as ListenerPrefix;
+ if (other == null)
+ return false;
+
+ return (original == other.original);
+ }
+
+ public override int GetHashCode()
+ {
+ return original.GetHashCode();
+ }
+
+ void Parse(string uri)
+ {
+ ushort default_port = 80;
+ if (uri.StartsWith("https://"))
+ {
+ default_port = 443;
+ secure = true;
+ }
+
+ int length = uri.Length;
+ int start_host = uri.IndexOf(':') + 3;
+ if (start_host >= length)
+ throw new ArgumentException("No host specified.");
+
+ int colon = uri.IndexOf(':', start_host, length - start_host);
+ int root;
+ if (colon > 0)
+ {
+ host = uri.Substring(start_host, colon - start_host);
+ root = uri.IndexOf('/', colon, length - colon);
+ port = (ushort)Int32.Parse(uri.Substring(colon + 1, root - colon - 1));
+ path = uri.Substring(root);
+ }
+ else
+ {
+ root = uri.IndexOf('/', start_host, length - start_host);
+ host = uri.Substring(start_host, root - start_host);
+ port = default_port;
+ path = uri.Substring(root);
+ }
+ if (path.Length != 1)
+ path = path.Substring(0, path.Length - 1);
+ }
+
+ public static void CheckUri(string uri)
+ {
+ if (uri == null)
+ throw new ArgumentNullException("uriPrefix");
+
+ if (!uri.StartsWith("http://") && !uri.StartsWith("https://"))
+ throw new ArgumentException("Only 'http' and 'https' schemes are supported.");
+
+ int length = uri.Length;
+ int start_host = uri.IndexOf(':') + 3;
+ if (start_host >= length)
+ throw new ArgumentException("No host specified.");
+
+ int colon = uri.IndexOf(':', start_host, length - start_host);
+ if (start_host == colon)
+ throw new ArgumentException("No host specified.");
+
+ int root;
+ if (colon > 0)
+ {
+ root = uri.IndexOf('/', colon, length - colon);
+ if (root == -1)
+ throw new ArgumentException("No path specified.");
+
+ try
+ {
+ int p = Int32.Parse(uri.Substring(colon + 1, root - colon - 1));
+ if (p <= 0 || p >= 65536)
+ throw new Exception();
+ }
+ catch
+ {
+ throw new ArgumentException("Invalid port.");
+ }
+ }
+ else
+ {
+ root = uri.IndexOf('/', start_host, length - start_host);
+ if (root == -1)
+ throw new ArgumentException("No path specified.");
+ }
+
+ if (uri[uri.Length - 1] != '/')
+ throw new ArgumentException("The prefix must end with '/'");
+ }
+ }
+}
diff --git a/SocketHttpListener/Net/ResponseStream.cs b/SocketHttpListener/Net/ResponseStream.cs
new file mode 100644
index 000000000..5949e3817
--- /dev/null
+++ b/SocketHttpListener/Net/ResponseStream.cs
@@ -0,0 +1,400 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.System;
+using MediaBrowser.Model.Text;
+using SocketHttpListener.Primitives;
+
+namespace SocketHttpListener.Net
+{
+ // FIXME: Does this buffer the response until Close?
+ // Update: we send a single packet for the first non-chunked Write
+ // What happens when we set content-length to X and write X-1 bytes then close?
+ // what if we don't set content-length at all?
+ public class ResponseStream : Stream
+ {
+ HttpListenerResponse response;
+ bool disposed;
+ bool trailer_sent;
+ Stream stream;
+ private readonly IMemoryStreamFactory _memoryStreamFactory;
+ private readonly ITextEncoding _textEncoding;
+ private readonly IFileSystem _fileSystem;
+ private readonly IAcceptSocket _socket;
+ private readonly bool _supportsDirectSocketAccess;
+ private readonly ILogger _logger;
+ private readonly IEnvironmentInfo _environment;
+
+ internal ResponseStream(Stream stream, HttpListenerResponse response, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding, IFileSystem fileSystem, IAcceptSocket socket, bool supportsDirectSocketAccess, ILogger logger, IEnvironmentInfo environment)
+ {
+ this.response = response;
+ _memoryStreamFactory = memoryStreamFactory;
+ _textEncoding = textEncoding;
+ _fileSystem = fileSystem;
+ _socket = socket;
+ _supportsDirectSocketAccess = supportsDirectSocketAccess;
+ _logger = logger;
+ _environment = environment;
+ this.stream = stream;
+ }
+
+ public override bool CanRead
+ {
+ get { return false; }
+ }
+
+ public override bool CanSeek
+ {
+ get { return false; }
+ }
+
+ public override bool CanWrite
+ {
+ get { return true; }
+ }
+
+ public override long Length
+ {
+ get { throw new NotSupportedException(); }
+ }
+
+ public override long Position
+ {
+ get { throw new NotSupportedException(); }
+ set { throw new NotSupportedException(); }
+ }
+
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposed == false)
+ {
+ disposed = true;
+ using (var ms = GetHeaders(response, _memoryStreamFactory, false))
+ {
+ if (stream.CanWrite)
+ {
+ try
+ {
+ bool chunked = response.SendChunked;
+
+ if (ms != null)
+ {
+ var start = ms.Position;
+ if (chunked && !trailer_sent)
+ {
+ trailer_sent = true;
+ var bytes = GetChunkSizeBytes(0, true);
+ ms.Position = ms.Length;
+ ms.Write(bytes, 0, bytes.Length);
+ ms.Position = start;
+ }
+
+ ms.CopyTo(stream);
+ }
+ else if (chunked && !trailer_sent)
+ {
+ trailer_sent = true;
+
+ var bytes = GetChunkSizeBytes(0, true);
+ stream.Write(bytes, 0, bytes.Length);
+ }
+ }
+ catch (IOException ex)
+ {
+ // Ignore error due to connection reset by peer
+ }
+ }
+ response.Close();
+ }
+ }
+
+ base.Dispose(disposing);
+ }
+
+ internal static MemoryStream GetHeaders(HttpListenerResponse response, IMemoryStreamFactory memoryStreamFactory, bool closing)
+ {
+ // SendHeaders works on shared headers
+ lock (response.headers_lock)
+ {
+ if (response.HeadersSent)
+ return null;
+ var ms = memoryStreamFactory.CreateNew();
+ response.SendHeaders(closing, ms);
+ return ms;
+ }
+ }
+
+ public override void Flush()
+ {
+ }
+
+ static byte[] crlf = new byte[] { 13, 10 };
+ byte[] GetChunkSizeBytes(int size, bool final)
+ {
+ string str = String.Format("{0:x}\r\n{1}", size, final ? "\r\n" : "");
+ return _textEncoding.GetASCIIEncoding().GetBytes(str);
+ }
+
+ internal void InternalWrite(byte[] buffer, int offset, int count)
+ {
+ stream.Write(buffer, offset, count);
+ }
+
+ const int MsCopyBufferSize = 81920;
+ const int StreamCopyToBufferSize = 81920;
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ if (disposed)
+ throw new ObjectDisposedException(GetType().ToString());
+
+ if (count == 0)
+ {
+ return;
+ }
+
+ using (var ms = GetHeaders(response, _memoryStreamFactory, false))
+ {
+ bool chunked = response.SendChunked;
+ if (ms != null)
+ {
+ long start = ms.Position; // After the possible preamble for the encoding
+ ms.Position = ms.Length;
+ if (chunked)
+ {
+ var bytes = GetChunkSizeBytes(count, false);
+ ms.Write(bytes, 0, bytes.Length);
+ }
+
+ ms.Write(buffer, offset, count);
+
+ if (chunked)
+ {
+ ms.Write(crlf, 0, 2);
+ }
+
+ ms.Position = start;
+ ms.CopyTo(stream, MsCopyBufferSize);
+
+ return;
+ }
+
+ if (chunked)
+ {
+ var bytes = GetChunkSizeBytes(count, false);
+ stream.Write(bytes, 0, bytes.Length);
+ }
+
+ stream.Write(buffer, offset, count);
+
+ if (chunked)
+ stream.Write(crlf, 0, 2);
+ }
+ }
+
+ public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ if (disposed)
+ throw new ObjectDisposedException(GetType().ToString());
+
+ if (count == 0)
+ {
+ return;
+ }
+
+ using (var ms = GetHeaders(response, _memoryStreamFactory, false))
+ {
+ bool chunked = response.SendChunked;
+ if (ms != null)
+ {
+ long start = ms.Position; // After the possible preamble for the encoding
+ ms.Position = ms.Length;
+ if (chunked)
+ {
+ var bytes = GetChunkSizeBytes(count, false);
+ ms.Write(bytes, 0, bytes.Length);
+ }
+
+ ms.Write(buffer, offset, count);
+
+ if (chunked)
+ {
+ ms.Write(crlf, 0, 2);
+ }
+
+ ms.Position = start;
+ await ms.CopyToAsync(stream, MsCopyBufferSize, cancellationToken).ConfigureAwait(false);
+
+ return;
+ }
+
+ if (chunked)
+ {
+ var bytes = GetChunkSizeBytes(count, false);
+ stream.Write(bytes, 0, bytes.Length);
+ }
+
+ await stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false);
+
+ if (chunked)
+ stream.Write(crlf, 0, 2);
+ }
+ }
+
+ public override int Read([In, Out] byte[] buffer, int offset, int count)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotSupportedException();
+ }
+
+ private bool EnableSendFileWithSocket
+ {
+ get { return false; }
+ }
+
+ public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken)
+ {
+ if (_supportsDirectSocketAccess && offset == 0 && count == 0 && !response.SendChunked && response.ContentLength64 > 8192)
+ {
+ if (EnableSendFileWithSocket)
+ {
+ return TransmitFileOverSocket(path, offset, count, fileShareMode, cancellationToken);
+ }
+ }
+ return TransmitFileManaged(path, offset, count, fileShareMode, cancellationToken);
+ }
+
+ private readonly byte[] _emptyBuffer = new byte[] { };
+ private Task TransmitFileOverSocket(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken)
+ {
+ var ms = GetHeaders(response, _memoryStreamFactory, false);
+
+ byte[] preBuffer;
+ if (ms != null)
+ {
+ using (var msCopy = new MemoryStream())
+ {
+ ms.CopyTo(msCopy);
+ preBuffer = msCopy.ToArray();
+ }
+ }
+ else
+ {
+ return TransmitFileManaged(path, offset, count, fileShareMode, cancellationToken);
+ }
+
+ _logger.Info("Socket sending file {0} {1}", path, response.ContentLength64);
+ return _socket.SendFile(path, preBuffer, _emptyBuffer, cancellationToken);
+ }
+
+ private async Task TransmitFileManaged(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken)
+ {
+ var allowAsync = _environment.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.Windows;
+
+ var fileOpenOptions = offset > 0
+ ? FileOpenOptions.RandomAccess
+ : FileOpenOptions.SequentialScan;
+
+ if (allowAsync)
+ {
+ fileOpenOptions |= FileOpenOptions.Asynchronous;
+ }
+
+ // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
+
+ using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShareMode, fileOpenOptions))
+ {
+ if (offset > 0)
+ {
+ fs.Position = offset;
+ }
+
+ var targetStream = this;
+
+ if (count > 0)
+ {
+ if (allowAsync)
+ {
+ await CopyToInternalAsync(fs, targetStream, count, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ await CopyToInternalAsyncWithSyncRead(fs, targetStream, count, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ else
+ {
+ if (allowAsync)
+ {
+ await fs.CopyToAsync(targetStream, StreamCopyToBufferSize, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ fs.CopyTo(targetStream, StreamCopyToBufferSize);
+ }
+ }
+ }
+ }
+
+ private static async Task CopyToInternalAsyncWithSyncRead(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
+ {
+ var array = new byte[StreamCopyToBufferSize];
+ int bytesRead;
+
+ while ((bytesRead = source.Read(array, 0, array.Length)) != 0)
+ {
+ var bytesToWrite = Math.Min(bytesRead, copyLength);
+
+ if (bytesToWrite > 0)
+ {
+ await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
+ }
+
+ copyLength -= bytesToWrite;
+
+ if (copyLength <= 0)
+ {
+ break;
+ }
+ }
+ }
+
+ private static async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
+ {
+ var array = new byte[StreamCopyToBufferSize];
+ int bytesRead;
+
+ while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0)
+ {
+ var bytesToWrite = Math.Min(bytesRead, copyLength);
+
+ if (bytesToWrite > 0)
+ {
+ await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
+ }
+
+ copyLength -= bytesToWrite;
+
+ if (copyLength <= 0)
+ {
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/SocketHttpListener/Net/WebHeaderCollection.cs b/SocketHttpListener/Net/WebHeaderCollection.cs
new file mode 100644
index 000000000..d20f99b9b
--- /dev/null
+++ b/SocketHttpListener/Net/WebHeaderCollection.cs
@@ -0,0 +1,391 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Net;
+using System.Runtime.InteropServices;
+using System.Runtime.Serialization;
+using System.Text;
+using MediaBrowser.Model.Services;
+
+namespace SocketHttpListener.Net
+{
+ [ComVisible(true)]
+ public class WebHeaderCollection : QueryParamCollection
+ {
+ [Flags]
+ internal enum HeaderInfo
+ {
+ Request = 1,
+ Response = 1 << 1,
+ MultiValue = 1 << 10
+ }
+
+ static readonly bool[] allowed_chars = {
+ false, false, false, false, false, false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, true, false, true, true, true, true, false, false, false, true,
+ true, false, true, true, false, true, true, true, true, true, true, true, true, true, true, false,
+ false, false, false, false, false, false, true, true, true, true, true, true, true, true, true,
+ true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true,
+ false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true,
+ true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true,
+ false, true, false
+ };
+
+ static readonly Dictionary<string, HeaderInfo> headers;
+ HeaderInfo? headerRestriction;
+ HeaderInfo? headerConsistency;
+
+ static WebHeaderCollection()
+ {
+ headers = new Dictionary<string, HeaderInfo>(StringComparer.OrdinalIgnoreCase) {
+ { "Allow", HeaderInfo.MultiValue },
+ { "Accept", HeaderInfo.Request | HeaderInfo.MultiValue },
+ { "Accept-Charset", HeaderInfo.MultiValue },
+ { "Accept-Encoding", HeaderInfo.MultiValue },
+ { "Accept-Language", HeaderInfo.MultiValue },
+ { "Accept-Ranges", HeaderInfo.MultiValue },
+ { "Age", HeaderInfo.Response },
+ { "Authorization", HeaderInfo.MultiValue },
+ { "Cache-Control", HeaderInfo.MultiValue },
+ { "Cookie", HeaderInfo.MultiValue },
+ { "Connection", HeaderInfo.Request | HeaderInfo.MultiValue },
+ { "Content-Encoding", HeaderInfo.MultiValue },
+ { "Content-Length", HeaderInfo.Request | HeaderInfo.Response },
+ { "Content-Type", HeaderInfo.Request },
+ { "Content-Language", HeaderInfo.MultiValue },
+ { "Date", HeaderInfo.Request },
+ { "Expect", HeaderInfo.Request | HeaderInfo.MultiValue},
+ { "Host", HeaderInfo.Request },
+ { "If-Match", HeaderInfo.MultiValue },
+ { "If-Modified-Since", HeaderInfo.Request },
+ { "If-None-Match", HeaderInfo.MultiValue },
+ { "Keep-Alive", HeaderInfo.Response },
+ { "Pragma", HeaderInfo.MultiValue },
+ { "Proxy-Authenticate", HeaderInfo.MultiValue },
+ { "Proxy-Authorization", HeaderInfo.MultiValue },
+ { "Proxy-Connection", HeaderInfo.Request | HeaderInfo.MultiValue },
+ { "Range", HeaderInfo.Request | HeaderInfo.MultiValue },
+ { "Referer", HeaderInfo.Request },
+ { "Set-Cookie", HeaderInfo.MultiValue },
+ { "Set-Cookie2", HeaderInfo.MultiValue },
+ { "Server", HeaderInfo.Response },
+ { "TE", HeaderInfo.MultiValue },
+ { "Trailer", HeaderInfo.MultiValue },
+ { "Transfer-Encoding", HeaderInfo.Request | HeaderInfo.Response | HeaderInfo.MultiValue },
+ { "Translate", HeaderInfo.Request | HeaderInfo.Response },
+ { "Upgrade", HeaderInfo.MultiValue },
+ { "User-Agent", HeaderInfo.Request },
+ { "Vary", HeaderInfo.MultiValue },
+ { "Via", HeaderInfo.MultiValue },
+ { "Warning", HeaderInfo.MultiValue },
+ { "WWW-Authenticate", HeaderInfo.Response | HeaderInfo. MultiValue },
+ { "SecWebSocketAccept", HeaderInfo.Response },
+ { "SecWebSocketExtensions", HeaderInfo.Request | HeaderInfo.Response | HeaderInfo. MultiValue },
+ { "SecWebSocketKey", HeaderInfo.Request },
+ { "Sec-WebSocket-Protocol", HeaderInfo.Request | HeaderInfo.Response | HeaderInfo. MultiValue },
+ { "SecWebSocketVersion", HeaderInfo.Response | HeaderInfo. MultiValue }
+ };
+ }
+
+ // Methods
+
+ public void Add(string header)
+ {
+ if (header == null)
+ throw new ArgumentNullException("header");
+ int pos = header.IndexOf(':');
+ if (pos == -1)
+ throw new ArgumentException("no colon found", "header");
+
+ this.Add(header.Substring(0, pos), header.Substring(pos + 1));
+ }
+
+ public override void Add(string name, string value)
+ {
+ if (name == null)
+ throw new ArgumentNullException("name");
+
+ ThrowIfRestricted(name);
+ this.AddWithoutValidate(name, value);
+ }
+
+ protected void AddWithoutValidate(string headerName, string headerValue)
+ {
+ if (!IsHeaderName(headerName))
+ throw new ArgumentException("invalid header name: " + headerName, "headerName");
+ if (headerValue == null)
+ headerValue = String.Empty;
+ else
+ headerValue = headerValue.Trim();
+ if (!IsHeaderValue(headerValue))
+ throw new ArgumentException("invalid header value: " + headerValue, "headerValue");
+
+ AddValue(headerName, headerValue);
+ }
+
+ internal void AddValue(string headerName, string headerValue)
+ {
+ base.Add(headerName, headerValue);
+ }
+
+ internal string[] GetValues_internal(string header, bool split)
+ {
+ if (header == null)
+ throw new ArgumentNullException("header");
+
+ string[] values = base.GetValues(header);
+ if (values == null || values.Length == 0)
+ return null;
+
+ if (split && IsMultiValue(header))
+ {
+ List<string> separated = null;
+ foreach (var value in values)
+ {
+ if (value.IndexOf(',') < 0)
+ {
+ if (separated != null)
+ separated.Add(value);
+
+ continue;
+ }
+
+ if (separated == null)
+ {
+ separated = new List<string>(values.Length + 1);
+ foreach (var v in values)
+ {
+ if (v == value)
+ break;
+
+ separated.Add(v);
+ }
+ }
+
+ var slices = value.Split(',');
+ var slices_length = slices.Length;
+ if (value[value.Length - 1] == ',')
+ --slices_length;
+
+ for (int i = 0; i < slices_length; ++i)
+ {
+ separated.Add(slices[i].Trim());
+ }
+ }
+
+ if (separated != null)
+ return separated.ToArray();
+ }
+
+ return values;
+ }
+
+ public override string[] GetValues(string header)
+ {
+ return GetValues_internal(header, true);
+ }
+
+ public override string[] GetValues(int index)
+ {
+ string[] values = base.GetValues(index);
+
+ if (values == null || values.Length == 0)
+ {
+ return null;
+ }
+
+ return values;
+ }
+
+ public static bool IsRestricted(string headerName)
+ {
+ return IsRestricted(headerName, false);
+ }
+
+ public static bool IsRestricted(string headerName, bool response)
+ {
+ if (headerName == null)
+ throw new ArgumentNullException("headerName");
+
+ if (headerName.Length == 0)
+ throw new ArgumentException("empty string", "headerName");
+
+ if (!IsHeaderName(headerName))
+ throw new ArgumentException("Invalid character in header");
+
+ HeaderInfo info;
+ if (!headers.TryGetValue(headerName, out info))
+ return false;
+
+ var flag = response ? HeaderInfo.Response : HeaderInfo.Request;
+ return (info & flag) != 0;
+ }
+
+ public override void Set(string name, string value)
+ {
+ if (name == null)
+ throw new ArgumentNullException("name");
+ if (!IsHeaderName(name))
+ throw new ArgumentException("invalid header name");
+ if (value == null)
+ value = String.Empty;
+ else
+ value = value.Trim();
+ if (!IsHeaderValue(value))
+ throw new ArgumentException("invalid header value");
+
+ ThrowIfRestricted(name);
+ base.Set(name, value);
+ }
+
+ internal string ToStringMultiValue()
+ {
+ StringBuilder sb = new StringBuilder();
+
+ int count = base.Count;
+ for (int i = 0; i < count; i++)
+ {
+ string key = GetKey(i);
+ if (IsMultiValue(key))
+ {
+ foreach (string v in GetValues(i))
+ {
+ sb.Append(key)
+ .Append(": ")
+ .Append(v)
+ .Append("\r\n");
+ }
+ }
+ else
+ {
+ sb.Append(key)
+ .Append(": ")
+ .Append(Get(i))
+ .Append("\r\n");
+ }
+ }
+ return sb.Append("\r\n").ToString();
+ }
+
+ public override string ToString()
+ {
+ StringBuilder sb = new StringBuilder();
+
+ int count = base.Count;
+ for (int i = 0; i < count; i++)
+ sb.Append(GetKey(i))
+ .Append(": ")
+ .Append(Get(i))
+ .Append("\r\n");
+
+ return sb.Append("\r\n").ToString();
+ }
+
+
+ // Internal Methods
+
+ // With this we don't check for invalid characters in header. See bug #55994.
+ internal void SetInternal(string header)
+ {
+ int pos = header.IndexOf(':');
+ if (pos == -1)
+ throw new ArgumentException("no colon found", "header");
+
+ SetInternal(header.Substring(0, pos), header.Substring(pos + 1));
+ }
+
+ internal void SetInternal(string name, string value)
+ {
+ if (value == null)
+ value = String.Empty;
+ else
+ value = value.Trim();
+ if (!IsHeaderValue(value))
+ throw new ArgumentException("invalid header value");
+
+ if (IsMultiValue(name))
+ {
+ base.Add(name, value);
+ }
+ else
+ {
+ base.Remove(name);
+ base.Set(name, value);
+ }
+ }
+
+ // Private Methods
+
+ public override int Remove(string name)
+ {
+ ThrowIfRestricted(name);
+ return base.Remove(name);
+ }
+
+ protected void ThrowIfRestricted(string headerName)
+ {
+ if (!headerRestriction.HasValue)
+ return;
+
+ HeaderInfo info;
+ if (!headers.TryGetValue(headerName, out info))
+ return;
+
+ if ((info & headerRestriction.Value) != 0)
+ throw new ArgumentException("This header must be modified with the appropriate property.");
+ }
+
+ internal static bool IsMultiValue(string headerName)
+ {
+ if (headerName == null)
+ return false;
+
+ HeaderInfo info;
+ return headers.TryGetValue(headerName, out info) && (info & HeaderInfo.MultiValue) != 0;
+ }
+
+ internal static bool IsHeaderValue(string value)
+ {
+ // TEXT any 8 bit value except CTL's (0-31 and 127)
+ // but including \r\n space and \t
+ // after a newline at least one space or \t must follow
+ // certain header fields allow comments ()
+
+ int len = value.Length;
+ for (int i = 0; i < len; i++)
+ {
+ char c = value[i];
+ if (c == 127)
+ return false;
+ if (c < 0x20 && (c != '\r' && c != '\n' && c != '\t'))
+ return false;
+ if (c == '\n' && ++i < len)
+ {
+ c = value[i];
+ if (c != ' ' && c != '\t')
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ internal static bool IsHeaderName(string name)
+ {
+ if (name == null || name.Length == 0)
+ return false;
+
+ int len = name.Length;
+ for (int i = 0; i < len; i++)
+ {
+ char c = name[i];
+ if (c > 126 || !allowed_chars[c])
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/SocketHttpListener/Net/WebSockets/HttpListenerWebSocketContext.cs b/SocketHttpListener/Net/WebSockets/HttpListenerWebSocketContext.cs
new file mode 100644
index 000000000..034ac17d2
--- /dev/null
+++ b/SocketHttpListener/Net/WebSockets/HttpListenerWebSocketContext.cs
@@ -0,0 +1,348 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.IO;
+using System.Net;
+using System.Security.Principal;
+using MediaBrowser.Model.Cryptography;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Services;
+using SocketHttpListener.Primitives;
+
+namespace SocketHttpListener.Net.WebSockets
+{
+ /// <summary>
+ /// Provides the properties used to access the information in a WebSocket connection request
+ /// received by the <see cref="HttpListener"/>.
+ /// </summary>
+ /// <remarks>
+ /// </remarks>
+ public class HttpListenerWebSocketContext : WebSocketContext
+ {
+ #region Private Fields
+
+ private HttpListenerContext _context;
+ private WebSocket _websocket;
+
+ #endregion
+
+ #region Internal Constructors
+
+ internal HttpListenerWebSocketContext(
+ HttpListenerContext context, string protocol, ICryptoProvider cryptoProvider, IMemoryStreamFactory memoryStreamFactory)
+ {
+ _context = context;
+ _websocket = new WebSocket(this, protocol, cryptoProvider, memoryStreamFactory);
+ }
+
+ #endregion
+
+ #region Internal Properties
+
+ internal Stream Stream
+ {
+ get
+ {
+ return _context.Connection.Stream;
+ }
+ }
+
+ #endregion
+
+ #region Public Properties
+
+ /// <summary>
+ /// Gets the HTTP cookies included in the request.
+ /// </summary>
+ /// <value>
+ /// A <see cref="System.Net.CookieCollection"/> that contains the cookies.
+ /// </value>
+ public override CookieCollection CookieCollection
+ {
+ get
+ {
+ return _context.Request.Cookies;
+ }
+ }
+
+ /// <summary>
+ /// Gets the HTTP headers included in the request.
+ /// </summary>
+ /// <value>
+ /// A <see cref="QueryParamCollection"/> that contains the headers.
+ /// </value>
+ public override QueryParamCollection Headers
+ {
+ get
+ {
+ return _context.Request.Headers;
+ }
+ }
+
+ /// <summary>
+ /// Gets the value of the Host header included in the request.
+ /// </summary>
+ /// <value>
+ /// A <see cref="string"/> that represents the value of the Host header.
+ /// </value>
+ public override string Host
+ {
+ get
+ {
+ return _context.Request.Headers["Host"];
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the client is authenticated.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if the client is authenticated; otherwise, <c>false</c>.
+ /// </value>
+ public override bool IsAuthenticated
+ {
+ get
+ {
+ return _context.Request.IsAuthenticated;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the client connected from the local computer.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if the client connected from the local computer; otherwise, <c>false</c>.
+ /// </value>
+ public override bool IsLocal
+ {
+ get
+ {
+ return _context.Request.IsLocal;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the WebSocket connection is secured.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if the connection is secured; otherwise, <c>false</c>.
+ /// </value>
+ public override bool IsSecureConnection
+ {
+ get
+ {
+ return _context.Connection.IsSecure;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the request is a WebSocket connection request.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if the request is a WebSocket connection request; otherwise, <c>false</c>.
+ /// </value>
+ public override bool IsWebSocketRequest
+ {
+ get
+ {
+ return _context.Request.IsWebSocketRequest;
+ }
+ }
+
+ /// <summary>
+ /// Gets the value of the Origin header included in the request.
+ /// </summary>
+ /// <value>
+ /// A <see cref="string"/> that represents the value of the Origin header.
+ /// </value>
+ public override string Origin
+ {
+ get
+ {
+ return _context.Request.Headers["Origin"];
+ }
+ }
+
+ /// <summary>
+ /// Gets the query string included in the request.
+ /// </summary>
+ /// <value>
+ /// A <see cref="QueryParamCollection"/> that contains the query string parameters.
+ /// </value>
+ public override QueryParamCollection QueryString
+ {
+ get
+ {
+ return _context.Request.QueryString;
+ }
+ }
+
+ /// <summary>
+ /// Gets the URI requested by the client.
+ /// </summary>
+ /// <value>
+ /// A <see cref="Uri"/> that represents the requested URI.
+ /// </value>
+ public override Uri RequestUri
+ {
+ get
+ {
+ return _context.Request.Url;
+ }
+ }
+
+ /// <summary>
+ /// Gets the value of the Sec-WebSocket-Key header included in the request.
+ /// </summary>
+ /// <remarks>
+ /// This property provides a part of the information used by the server to prove that it
+ /// received a valid WebSocket connection request.
+ /// </remarks>
+ /// <value>
+ /// A <see cref="string"/> that represents the value of the Sec-WebSocket-Key header.
+ /// </value>
+ public override string SecWebSocketKey
+ {
+ get
+ {
+ return _context.Request.Headers["Sec-WebSocket-Key"];
+ }
+ }
+
+ /// <summary>
+ /// Gets the values of the Sec-WebSocket-Protocol header included in the request.
+ /// </summary>
+ /// <remarks>
+ /// This property represents the subprotocols requested by the client.
+ /// </remarks>
+ /// <value>
+ /// An <see cref="T:System.Collections.Generic.IEnumerable{string}"/> instance that provides
+ /// an enumerator which supports the iteration over the values of the Sec-WebSocket-Protocol
+ /// header.
+ /// </value>
+ public override IEnumerable<string> SecWebSocketProtocols
+ {
+ get
+ {
+ var protocols = _context.Request.Headers["Sec-WebSocket-Protocol"];
+ if (protocols != null)
+ foreach (var protocol in protocols.Split(','))
+ yield return protocol.Trim();
+ }
+ }
+
+ /// <summary>
+ /// Gets the value of the Sec-WebSocket-Version header included in the request.
+ /// </summary>
+ /// <remarks>
+ /// This property represents the WebSocket protocol version.
+ /// </remarks>
+ /// <value>
+ /// A <see cref="string"/> that represents the value of the Sec-WebSocket-Version header.
+ /// </value>
+ public override string SecWebSocketVersion
+ {
+ get
+ {
+ return _context.Request.Headers["Sec-WebSocket-Version"];
+ }
+ }
+
+ /// <summary>
+ /// Gets the server endpoint as an IP address and a port number.
+ /// </summary>
+ /// <value>
+ /// </value>
+ public override IpEndPointInfo ServerEndPoint
+ {
+ get
+ {
+ return _context.Connection.LocalEndPoint;
+ }
+ }
+
+ /// <summary>
+ /// Gets the client information (identity, authentication, and security roles).
+ /// </summary>
+ /// <value>
+ /// A <see cref="IPrincipal"/> that represents the client information.
+ /// </value>
+ public override IPrincipal User
+ {
+ get
+ {
+ return _context.User;
+ }
+ }
+
+ /// <summary>
+ /// Gets the client endpoint as an IP address and a port number.
+ /// </summary>
+ /// <value>
+ /// </value>
+ public override IpEndPointInfo UserEndPoint
+ {
+ get
+ {
+ return _context.Connection.RemoteEndPoint;
+ }
+ }
+
+ /// <summary>
+ /// Gets the <see cref="SocketHttpListener.WebSocket"/> instance used for two-way communication
+ /// between client and server.
+ /// </summary>
+ /// <value>
+ /// A <see cref="SocketHttpListener.WebSocket"/>.
+ /// </value>
+ public override WebSocket WebSocket
+ {
+ get
+ {
+ return _websocket;
+ }
+ }
+
+ #endregion
+
+ #region Internal Methods
+
+ internal void Close()
+ {
+ try
+ {
+ _context.Connection.Close(true);
+ }
+ catch
+ {
+ // catch errors sending the closing handshake
+ }
+ }
+
+ internal void Close(HttpStatusCode code)
+ {
+ _context.Response.StatusCode = (int)code;
+ _context.Response.OutputStream.Dispose();
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ /// <summary>
+ /// Returns a <see cref="string"/> that represents the current
+ /// <see cref="HttpListenerWebSocketContext"/>.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string"/> that represents the current
+ /// <see cref="HttpListenerWebSocketContext"/>.
+ /// </returns>
+ public override string ToString()
+ {
+ return _context.Request.ToString();
+ }
+
+ #endregion
+ }
+}
diff --git a/SocketHttpListener/Net/WebSockets/WebSocketContext.cs b/SocketHttpListener/Net/WebSockets/WebSocketContext.cs
new file mode 100644
index 000000000..3ffa6e639
--- /dev/null
+++ b/SocketHttpListener/Net/WebSockets/WebSocketContext.cs
@@ -0,0 +1,183 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Net;
+using System.Security.Principal;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Services;
+
+namespace SocketHttpListener.Net.WebSockets
+{
+ /// <summary>
+ /// Exposes the properties used to access the information in a WebSocket connection request.
+ /// </summary>
+ /// <remarks>
+ /// The WebSocketContext class is an abstract class.
+ /// </remarks>
+ public abstract class WebSocketContext
+ {
+ #region Protected Constructors
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="WebSocketContext"/> class.
+ /// </summary>
+ protected WebSocketContext()
+ {
+ }
+
+ #endregion
+
+ #region Public Properties
+
+ /// <summary>
+ /// Gets the HTTP cookies included in the request.
+ /// </summary>
+ /// <value>
+ /// A <see cref="System.Net.CookieCollection"/> that contains the cookies.
+ /// </value>
+ public abstract CookieCollection CookieCollection { get; }
+
+ /// <summary>
+ /// Gets the HTTP headers included in the request.
+ /// </summary>
+ /// <value>
+ /// A <see cref="QueryParamCollection"/> that contains the headers.
+ /// </value>
+ public abstract QueryParamCollection Headers { get; }
+
+ /// <summary>
+ /// Gets the value of the Host header included in the request.
+ /// </summary>
+ /// <value>
+ /// A <see cref="string"/> that represents the value of the Host header.
+ /// </value>
+ public abstract string Host { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether the client is authenticated.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if the client is authenticated; otherwise, <c>false</c>.
+ /// </value>
+ public abstract bool IsAuthenticated { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether the client connected from the local computer.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if the client connected from the local computer; otherwise, <c>false</c>.
+ /// </value>
+ public abstract bool IsLocal { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether the WebSocket connection is secured.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if the connection is secured; otherwise, <c>false</c>.
+ /// </value>
+ public abstract bool IsSecureConnection { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether the request is a WebSocket connection request.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if the request is a WebSocket connection request; otherwise, <c>false</c>.
+ /// </value>
+ public abstract bool IsWebSocketRequest { get; }
+
+ /// <summary>
+ /// Gets the value of the Origin header included in the request.
+ /// </summary>
+ /// <value>
+ /// A <see cref="string"/> that represents the value of the Origin header.
+ /// </value>
+ public abstract string Origin { get; }
+
+ /// <summary>
+ /// Gets the query string included in the request.
+ /// </summary>
+ /// <value>
+ /// A <see cref="QueryParamCollection"/> that contains the query string parameters.
+ /// </value>
+ public abstract QueryParamCollection QueryString { get; }
+
+ /// <summary>
+ /// Gets the URI requested by the client.
+ /// </summary>
+ /// <value>
+ /// A <see cref="Uri"/> that represents the requested URI.
+ /// </value>
+ public abstract Uri RequestUri { get; }
+
+ /// <summary>
+ /// Gets the value of the Sec-WebSocket-Key header included in the request.
+ /// </summary>
+ /// <remarks>
+ /// This property provides a part of the information used by the server to prove that it
+ /// received a valid WebSocket connection request.
+ /// </remarks>
+ /// <value>
+ /// A <see cref="string"/> that represents the value of the Sec-WebSocket-Key header.
+ /// </value>
+ public abstract string SecWebSocketKey { get; }
+
+ /// <summary>
+ /// Gets the values of the Sec-WebSocket-Protocol header included in the request.
+ /// </summary>
+ /// <remarks>
+ /// This property represents the subprotocols requested by the client.
+ /// </remarks>
+ /// <value>
+ /// An <see cref="T:System.Collections.Generic.IEnumerable{string}"/> instance that provides
+ /// an enumerator which supports the iteration over the values of the Sec-WebSocket-Protocol
+ /// header.
+ /// </value>
+ public abstract IEnumerable<string> SecWebSocketProtocols { get; }
+
+ /// <summary>
+ /// Gets the value of the Sec-WebSocket-Version header included in the request.
+ /// </summary>
+ /// <remarks>
+ /// This property represents the WebSocket protocol version.
+ /// </remarks>
+ /// <value>
+ /// A <see cref="string"/> that represents the value of the Sec-WebSocket-Version header.
+ /// </value>
+ public abstract string SecWebSocketVersion { get; }
+
+ /// <summary>
+ /// Gets the server endpoint as an IP address and a port number.
+ /// </summary>
+ /// <value>
+ /// A <see cref="System.Net.IPEndPoint"/> that represents the server endpoint.
+ /// </value>
+ public abstract IpEndPointInfo ServerEndPoint { get; }
+
+ /// <summary>
+ /// Gets the client information (identity, authentication, and security roles).
+ /// </summary>
+ /// <value>
+ /// A <see cref="IPrincipal"/> that represents the client information.
+ /// </value>
+ public abstract IPrincipal User { get; }
+
+ /// <summary>
+ /// Gets the client endpoint as an IP address and a port number.
+ /// </summary>
+ /// <value>
+ /// A <see cref="System.Net.IPEndPoint"/> that represents the client endpoint.
+ /// </value>
+ public abstract IpEndPointInfo UserEndPoint { get; }
+
+ /// <summary>
+ /// Gets the <see cref="SocketHttpListener.WebSocket"/> instance used for two-way communication
+ /// between client and server.
+ /// </summary>
+ /// <value>
+ /// A <see cref="SocketHttpListener.WebSocket"/>.
+ /// </value>
+ public abstract WebSocket WebSocket { get; }
+
+ #endregion
+ }
+}
diff --git a/SocketHttpListener/Opcode.cs b/SocketHttpListener/Opcode.cs
new file mode 100644
index 000000000..62b7d8585
--- /dev/null
+++ b/SocketHttpListener/Opcode.cs
@@ -0,0 +1,43 @@
+namespace SocketHttpListener
+{
+ /// <summary>
+ /// Contains the values of the opcode that indicates the type of a WebSocket frame.
+ /// </summary>
+ /// <remarks>
+ /// The values of the opcode are defined in
+ /// <see href="http://tools.ietf.org/html/rfc6455#section-5.2">Section 5.2</see> of RFC 6455.
+ /// </remarks>
+ public enum Opcode : byte
+ {
+ /// <summary>
+ /// Equivalent to numeric value 0.
+ /// Indicates a continuation frame.
+ /// </summary>
+ Cont = 0x0,
+ /// <summary>
+ /// Equivalent to numeric value 1.
+ /// Indicates a text frame.
+ /// </summary>
+ Text = 0x1,
+ /// <summary>
+ /// Equivalent to numeric value 2.
+ /// Indicates a binary frame.
+ /// </summary>
+ Binary = 0x2,
+ /// <summary>
+ /// Equivalent to numeric value 8.
+ /// Indicates a connection close frame.
+ /// </summary>
+ Close = 0x8,
+ /// <summary>
+ /// Equivalent to numeric value 9.
+ /// Indicates a ping frame.
+ /// </summary>
+ Ping = 0x9,
+ /// <summary>
+ /// Equivalent to numeric value 10.
+ /// Indicates a pong frame.
+ /// </summary>
+ Pong = 0xa
+ }
+}
diff --git a/SocketHttpListener/PayloadData.cs b/SocketHttpListener/PayloadData.cs
new file mode 100644
index 000000000..a6318da2b
--- /dev/null
+++ b/SocketHttpListener/PayloadData.cs
@@ -0,0 +1,149 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Text;
+
+namespace SocketHttpListener
+{
+ internal class PayloadData : IEnumerable<byte>
+ {
+ #region Private Fields
+
+ private byte [] _applicationData;
+ private byte [] _extensionData;
+ private bool _masked;
+
+ #endregion
+
+ #region Public Const Fields
+
+ public const ulong MaxLength = long.MaxValue;
+
+ #endregion
+
+ #region Public Constructors
+
+ public PayloadData ()
+ : this (new byte [0], new byte [0], false)
+ {
+ }
+
+ public PayloadData (byte [] applicationData)
+ : this (new byte [0], applicationData, false)
+ {
+ }
+
+ public PayloadData (string applicationData)
+ : this (new byte [0], Encoding.UTF8.GetBytes (applicationData), false)
+ {
+ }
+
+ public PayloadData (byte [] applicationData, bool masked)
+ : this (new byte [0], applicationData, masked)
+ {
+ }
+
+ public PayloadData (byte [] extensionData, byte [] applicationData, bool masked)
+ {
+ _extensionData = extensionData;
+ _applicationData = applicationData;
+ _masked = masked;
+ }
+
+ #endregion
+
+ #region Internal Properties
+
+ internal bool ContainsReservedCloseStatusCode {
+ get {
+ return _applicationData.Length > 1 &&
+ _applicationData.SubArray (0, 2).ToUInt16 (ByteOrder.Big).IsReserved ();
+ }
+ }
+
+ #endregion
+
+ #region Public Properties
+
+ public byte [] ApplicationData {
+ get {
+ return _applicationData;
+ }
+ }
+
+ public byte [] ExtensionData {
+ get {
+ return _extensionData;
+ }
+ }
+
+ public bool IsMasked {
+ get {
+ return _masked;
+ }
+ }
+
+ public ulong Length {
+ get {
+ return (ulong) (_extensionData.Length + _applicationData.Length);
+ }
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ private static void mask (byte [] src, byte [] key)
+ {
+ for (long i = 0; i < src.Length; i++)
+ src [i] = (byte) (src [i] ^ key [i % 4]);
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ public IEnumerator<byte> GetEnumerator ()
+ {
+ foreach (byte b in _extensionData)
+ yield return b;
+
+ foreach (byte b in _applicationData)
+ yield return b;
+ }
+
+ public void Mask (byte [] maskingKey)
+ {
+ if (_extensionData.Length > 0)
+ mask (_extensionData, maskingKey);
+
+ if (_applicationData.Length > 0)
+ mask (_applicationData, maskingKey);
+
+ _masked = !_masked;
+ }
+
+ public byte [] ToByteArray ()
+ {
+ return _extensionData.Length > 0
+ ? new List<byte> (this).ToArray ()
+ : _applicationData;
+ }
+
+ public override string ToString ()
+ {
+ return BitConverter.ToString (ToByteArray ());
+ }
+
+ #endregion
+
+ #region Explicitly Implemented Interface Members
+
+ IEnumerator IEnumerable.GetEnumerator ()
+ {
+ return GetEnumerator ();
+ }
+
+ #endregion
+ }
+}
diff --git a/SocketHttpListener/Primitives/ICertificate.cs b/SocketHttpListener/Primitives/ICertificate.cs
new file mode 100644
index 000000000..1289da13d
--- /dev/null
+++ b/SocketHttpListener/Primitives/ICertificate.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SocketHttpListener.Primitives
+{
+ public interface ICertificate
+ {
+ }
+}
diff --git a/SocketHttpListener/Primitives/IStreamFactory.cs b/SocketHttpListener/Primitives/IStreamFactory.cs
new file mode 100644
index 000000000..57e21e31b
--- /dev/null
+++ b/SocketHttpListener/Primitives/IStreamFactory.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Net;
+
+namespace SocketHttpListener.Primitives
+{
+ public interface IStreamFactory
+ {
+ Stream CreateNetworkStream(IAcceptSocket acceptSocket, bool ownsSocket);
+ Stream CreateSslStream(Stream innerStream, bool leaveInnerStreamOpen);
+
+ Task AuthenticateSslStreamAsServer(Stream stream, ICertificate certificate);
+ }
+}
diff --git a/SocketHttpListener/Primitives/ITextEncoding.cs b/SocketHttpListener/Primitives/ITextEncoding.cs
new file mode 100644
index 000000000..b10145687
--- /dev/null
+++ b/SocketHttpListener/Primitives/ITextEncoding.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Text;
+
+namespace SocketHttpListener.Primitives
+{
+ public static class TextEncodingExtensions
+ {
+ public static Encoding GetDefaultEncoding(this ITextEncoding encoding)
+ {
+ return Encoding.UTF8;
+ }
+ }
+}
diff --git a/SocketHttpListener/Properties/AssemblyInfo.cs b/SocketHttpListener/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..8876cea4f
--- /dev/null
+++ b/SocketHttpListener/Properties/AssemblyInfo.cs
@@ -0,0 +1,34 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("SocketHttpListener")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("SocketHttpListener")]
+[assembly: AssemblyCopyright("Copyright © 2017")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("1d74413b-e7cf-455b-b021-f52bdf881542")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
diff --git a/SocketHttpListener/Rsv.cs b/SocketHttpListener/Rsv.cs
new file mode 100644
index 000000000..668059b8a
--- /dev/null
+++ b/SocketHttpListener/Rsv.cs
@@ -0,0 +1,8 @@
+namespace SocketHttpListener
+{
+ internal enum Rsv : byte
+ {
+ Off = 0x0,
+ On = 0x1
+ }
+}
diff --git a/SocketHttpListener/SocketHttpListener.csproj b/SocketHttpListener/SocketHttpListener.csproj
new file mode 100644
index 000000000..dd2d2cf0f
--- /dev/null
+++ b/SocketHttpListener/SocketHttpListener.csproj
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProjectGuid>{1D74413B-E7CF-455B-B021-F52BDF881542}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>SocketHttpListener</RootNamespace>
+ <AssemblyName>SocketHttpListener</AssemblyName>
+ <TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ <TargetFrameworkProfile />
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Xml.Linq" />
+ <Reference Include="System.Data.DataSetExtensions" />
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.Data" />
+ <Reference Include="System.Net.Http" />
+ <Reference Include="System.Xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="..\SharedVersion.cs">
+ <Link>Properties\SharedVersion.cs</Link>
+ </Compile>
+ <Compile Include="ByteOrder.cs" />
+ <Compile Include="CloseEventArgs.cs" />
+ <Compile Include="CloseStatusCode.cs" />
+ <Compile Include="CompressionMethod.cs" />
+ <Compile Include="ErrorEventArgs.cs" />
+ <Compile Include="Ext.cs" />
+ <Compile Include="Fin.cs" />
+ <Compile Include="HttpBase.cs" />
+ <Compile Include="HttpResponse.cs" />
+ <Compile Include="Mask.cs" />
+ <Compile Include="MessageEventArgs.cs" />
+ <Compile Include="Net\AuthenticationSchemeSelector.cs" />
+ <Compile Include="Net\ChunkedInputStream.cs" />
+ <Compile Include="Net\ChunkStream.cs" />
+ <Compile Include="Net\CookieHelper.cs" />
+ <Compile Include="Net\EndPointListener.cs" />
+ <Compile Include="Net\EndPointManager.cs" />
+ <Compile Include="Net\HttpConnection.cs" />
+ <Compile Include="Net\HttpListener.cs" />
+ <Compile Include="Net\HttpListenerBasicIdentity.cs" />
+ <Compile Include="Net\HttpListenerContext.cs" />
+ <Compile Include="Net\HttpListenerPrefixCollection.cs" />
+ <Compile Include="Net\HttpListenerRequest.cs" />
+ <Compile Include="Net\HttpListenerResponse.cs" />
+ <Compile Include="Net\HttpRequestStream.cs" />
+ <Compile Include="Net\HttpRequestStream.Managed.cs" />
+ <Compile Include="Net\HttpStatusCode.cs" />
+ <Compile Include="Net\HttpStreamAsyncResult.cs" />
+ <Compile Include="Net\HttpVersion.cs" />
+ <Compile Include="Net\ListenerPrefix.cs" />
+ <Compile Include="Net\ResponseStream.cs" />
+ <Compile Include="Net\WebHeaderCollection.cs" />
+ <Compile Include="Net\WebSockets\HttpListenerWebSocketContext.cs" />
+ <Compile Include="Net\WebSockets\WebSocketContext.cs" />
+ <Compile Include="Opcode.cs" />
+ <Compile Include="PayloadData.cs" />
+ <Compile Include="Primitives\ICertificate.cs" />
+ <Compile Include="Primitives\IStreamFactory.cs" />
+ <Compile Include="Primitives\ITextEncoding.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Rsv.cs" />
+ <Compile Include="WebSocket.cs" />
+ <Compile Include="WebSocketException.cs" />
+ <Compile Include="WebSocketFrame.cs" />
+ <Compile Include="WebSocketState.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
+ <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project>
+ <Name>MediaBrowser.Common</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
+ <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
+ <Name>MediaBrowser.Model</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project> \ No newline at end of file
diff --git a/SocketHttpListener/WebSocket.cs b/SocketHttpListener/WebSocket.cs
new file mode 100644
index 000000000..9966d3fcf
--- /dev/null
+++ b/SocketHttpListener/WebSocket.cs
@@ -0,0 +1,887 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Cryptography;
+using MediaBrowser.Model.IO;
+using SocketHttpListener.Net.WebSockets;
+using SocketHttpListener.Primitives;
+using HttpStatusCode = SocketHttpListener.Net.HttpStatusCode;
+
+namespace SocketHttpListener
+{
+ /// <summary>
+ /// Implements the WebSocket interface.
+ /// </summary>
+ /// <remarks>
+ /// The WebSocket class provides a set of methods and properties for two-way communication using
+ /// the WebSocket protocol (<see href="http://tools.ietf.org/html/rfc6455">RFC 6455</see>).
+ /// </remarks>
+ public class WebSocket : IDisposable
+ {
+ #region Private Fields
+
+ private string _base64Key;
+ private Action _closeContext;
+ private CompressionMethod _compression;
+ private WebSocketContext _context;
+ private CookieCollection _cookies;
+ private string _extensions;
+ private AutoResetEvent _exitReceiving;
+ private object _forConn;
+ private object _forEvent;
+ private object _forMessageEventQueue;
+ private object _forSend;
+ private const string _guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+ private Func<WebSocketContext, string>
+ _handshakeRequestChecker;
+ private Queue<MessageEventArgs> _messageEventQueue;
+ private uint _nonceCount;
+ private string _origin;
+ private bool _preAuth;
+ private string _protocol;
+ private string[] _protocols;
+ private Uri _proxyUri;
+ private volatile WebSocketState _readyState;
+ private AutoResetEvent _receivePong;
+ private bool _secure;
+ private Stream _stream;
+ private Uri _uri;
+ private const string _version = "13";
+ private readonly IMemoryStreamFactory _memoryStreamFactory;
+
+ private readonly ICryptoProvider _cryptoProvider;
+
+ #endregion
+
+ #region Internal Fields
+
+ internal const int FragmentLength = 1016; // Max value is int.MaxValue - 14.
+
+ #endregion
+
+ #region Internal Constructors
+
+ // As server
+ internal WebSocket(HttpListenerWebSocketContext context, string protocol, ICryptoProvider cryptoProvider, IMemoryStreamFactory memoryStreamFactory)
+ {
+ _context = context;
+ _protocol = protocol;
+ _cryptoProvider = cryptoProvider;
+ _memoryStreamFactory = memoryStreamFactory;
+
+ _closeContext = context.Close;
+ _secure = context.IsSecureConnection;
+ _stream = context.Stream;
+
+ init();
+ }
+
+ #endregion
+
+ // As server
+ internal Func<WebSocketContext, string> CustomHandshakeRequestChecker
+ {
+ get
+ {
+ return _handshakeRequestChecker ?? (context => null);
+ }
+
+ set
+ {
+ _handshakeRequestChecker = value;
+ }
+ }
+
+ internal bool IsConnected
+ {
+ get
+ {
+ return _readyState == WebSocketState.Open || _readyState == WebSocketState.Closing;
+ }
+ }
+
+ /// <summary>
+ /// Gets the state of the WebSocket connection.
+ /// </summary>
+ /// <value>
+ /// One of the <see cref="WebSocketState"/> enum values, indicates the state of the WebSocket
+ /// connection. The default value is <see cref="WebSocketState.Connecting"/>.
+ /// </value>
+ public WebSocketState ReadyState
+ {
+ get
+ {
+ return _readyState;
+ }
+ }
+
+ #region Public Events
+
+ /// <summary>
+ /// Occurs when the WebSocket connection has been closed.
+ /// </summary>
+ public event EventHandler<CloseEventArgs> OnClose;
+
+ /// <summary>
+ /// Occurs when the <see cref="WebSocket"/> gets an error.
+ /// </summary>
+ public event EventHandler<ErrorEventArgs> OnError;
+
+ /// <summary>
+ /// Occurs when the <see cref="WebSocket"/> receives a message.
+ /// </summary>
+ public event EventHandler<MessageEventArgs> OnMessage;
+
+ /// <summary>
+ /// Occurs when the WebSocket connection has been established.
+ /// </summary>
+ public event EventHandler OnOpen;
+
+ #endregion
+
+ #region Private Methods
+
+ // As server
+ private bool acceptHandshake()
+ {
+ var msg = checkIfValidHandshakeRequest(_context);
+ if (msg != null)
+ {
+ error("An error has occurred while connecting: " + msg);
+ Close(HttpStatusCode.BadRequest);
+
+ return false;
+ }
+
+ if (_protocol != null &&
+ !_context.SecWebSocketProtocols.Contains(protocol => protocol == _protocol))
+ _protocol = null;
+
+ ////var extensions = _context.Headers["Sec-WebSocket-Extensions"];
+ ////if (extensions != null && extensions.Length > 0)
+ //// processSecWebSocketExtensionsHeader(extensions);
+
+ return sendHttpResponse(createHandshakeResponse());
+ }
+
+ // As server
+ private string checkIfValidHandshakeRequest(WebSocketContext context)
+ {
+ var headers = context.Headers;
+ return context.RequestUri == null
+ ? "Invalid request url."
+ : !context.IsWebSocketRequest
+ ? "Not WebSocket connection request."
+ : !validateSecWebSocketKeyHeader(headers["Sec-WebSocket-Key"])
+ ? "Invalid Sec-WebSocket-Key header."
+ : !validateSecWebSocketVersionClientHeader(headers["Sec-WebSocket-Version"])
+ ? "Invalid Sec-WebSocket-Version header."
+ : CustomHandshakeRequestChecker(context);
+ }
+
+ private void close(CloseStatusCode code, string reason, bool wait)
+ {
+ close(new PayloadData(((ushort)code).Append(reason)), !code.IsReserved(), wait);
+ }
+
+ private void close(PayloadData payload, bool send, bool wait)
+ {
+ lock (_forConn)
+ {
+ if (_readyState == WebSocketState.Closing || _readyState == WebSocketState.Closed)
+ {
+ return;
+ }
+
+ _readyState = WebSocketState.Closing;
+ }
+
+ var e = new CloseEventArgs(payload);
+ e.WasClean =
+ closeHandshake(
+ send ? WebSocketFrame.CreateCloseFrame(Mask.Unmask, payload).ToByteArray() : null,
+ wait ? 1000 : 0,
+ closeServerResources);
+
+ _readyState = WebSocketState.Closed;
+ try
+ {
+ OnClose.Emit(this, e);
+ }
+ catch (Exception ex)
+ {
+ error("An exception has occurred while OnClose.", ex);
+ }
+ }
+
+ private bool closeHandshake(byte[] frameAsBytes, int millisecondsTimeout, Action release)
+ {
+ var sent = frameAsBytes != null && writeBytes(frameAsBytes);
+ var received =
+ millisecondsTimeout == 0 ||
+ (sent && _exitReceiving != null && _exitReceiving.WaitOne(millisecondsTimeout));
+
+ release();
+ if (_receivePong != null)
+ {
+ _receivePong.Dispose();
+ _receivePong = null;
+ }
+
+ if (_exitReceiving != null)
+ {
+ _exitReceiving.Dispose();
+ _exitReceiving = null;
+ }
+
+ var result = sent && received;
+
+ return result;
+ }
+
+ // As server
+ private void closeServerResources()
+ {
+ if (_closeContext == null)
+ return;
+
+ _closeContext();
+ _closeContext = null;
+ _stream = null;
+ _context = null;
+ }
+
+ private bool concatenateFragmentsInto(Stream dest)
+ {
+ while (true)
+ {
+ var frame = WebSocketFrame.Read(_stream, true);
+ if (frame.IsFinal)
+ {
+ /* FINAL */
+
+ // CONT
+ if (frame.IsContinuation)
+ {
+ dest.WriteBytes(frame.PayloadData.ApplicationData);
+ break;
+ }
+
+ // PING
+ if (frame.IsPing)
+ {
+ processPingFrame(frame);
+ continue;
+ }
+
+ // PONG
+ if (frame.IsPong)
+ {
+ processPongFrame(frame);
+ continue;
+ }
+
+ // CLOSE
+ if (frame.IsClose)
+ return processCloseFrame(frame);
+ }
+ else
+ {
+ /* MORE */
+
+ // CONT
+ if (frame.IsContinuation)
+ {
+ dest.WriteBytes(frame.PayloadData.ApplicationData);
+ continue;
+ }
+ }
+
+ // ?
+ return processUnsupportedFrame(
+ frame,
+ CloseStatusCode.IncorrectData,
+ "An incorrect data has been received while receiving fragmented data.");
+ }
+
+ return true;
+ }
+
+ // As server
+ private HttpResponse createHandshakeCloseResponse(HttpStatusCode code)
+ {
+ var res = HttpResponse.CreateCloseResponse(code);
+ res.Headers["Sec-WebSocket-Version"] = _version;
+
+ return res;
+ }
+
+ // As server
+ private HttpResponse createHandshakeResponse()
+ {
+ var res = HttpResponse.CreateWebSocketResponse();
+
+ var headers = res.Headers;
+ headers["Sec-WebSocket-Accept"] = CreateResponseKey(_base64Key);
+
+ if (_protocol != null)
+ headers["Sec-WebSocket-Protocol"] = _protocol;
+
+ if (_extensions != null)
+ headers["Sec-WebSocket-Extensions"] = _extensions;
+
+ if (_cookies.Count > 0)
+ res.SetCookies(_cookies);
+
+ return res;
+ }
+
+ private MessageEventArgs dequeueFromMessageEventQueue()
+ {
+ lock (_forMessageEventQueue)
+ return _messageEventQueue.Count > 0
+ ? _messageEventQueue.Dequeue()
+ : null;
+ }
+
+ private void enqueueToMessageEventQueue(MessageEventArgs e)
+ {
+ lock (_forMessageEventQueue)
+ _messageEventQueue.Enqueue(e);
+ }
+
+ private void error(string message, Exception exception)
+ {
+ try
+ {
+ if (exception != null)
+ {
+ message += ". Exception.Message: " + exception.Message;
+ }
+ OnError.Emit(this, new ErrorEventArgs(message));
+ }
+ catch (Exception ex)
+ {
+ }
+ }
+
+ private void error(string message)
+ {
+ try
+ {
+ OnError.Emit(this, new ErrorEventArgs(message));
+ }
+ catch (Exception ex)
+ {
+ }
+ }
+
+ private void init()
+ {
+ _compression = CompressionMethod.None;
+ _cookies = new CookieCollection();
+ _forConn = new object();
+ _forEvent = new object();
+ _forSend = new object();
+ _messageEventQueue = new Queue<MessageEventArgs>();
+ _forMessageEventQueue = ((ICollection)_messageEventQueue).SyncRoot;
+ _readyState = WebSocketState.Connecting;
+ }
+
+ private void open()
+ {
+ try
+ {
+ startReceiving();
+
+ lock (_forEvent)
+ {
+ try
+ {
+ OnOpen.Emit(this, EventArgs.Empty);
+ }
+ catch (Exception ex)
+ {
+ processException(ex, "An exception has occurred while OnOpen.");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ processException(ex, "An exception has occurred while opening.");
+ }
+ }
+
+ private bool processCloseFrame(WebSocketFrame frame)
+ {
+ var payload = frame.PayloadData;
+ close(payload, !payload.ContainsReservedCloseStatusCode, false);
+
+ return false;
+ }
+
+ private bool processDataFrame(WebSocketFrame frame)
+ {
+ var e = frame.IsCompressed
+ ? new MessageEventArgs(
+ frame.Opcode, frame.PayloadData.ApplicationData.Decompress(_compression))
+ : new MessageEventArgs(frame.Opcode, frame.PayloadData);
+
+ enqueueToMessageEventQueue(e);
+ return true;
+ }
+
+ private void processException(Exception exception, string message)
+ {
+ var code = CloseStatusCode.Abnormal;
+ var reason = message;
+ if (exception is WebSocketException)
+ {
+ var wsex = (WebSocketException)exception;
+ code = wsex.Code;
+ reason = wsex.Message;
+ }
+
+ error(message ?? code.GetMessage(), exception);
+ if (_readyState == WebSocketState.Connecting)
+ Close(HttpStatusCode.BadRequest);
+ else
+ close(code, reason ?? code.GetMessage(), false);
+ }
+
+ private bool processFragmentedFrame(WebSocketFrame frame)
+ {
+ return frame.IsContinuation // Not first fragment
+ ? true
+ : processFragments(frame);
+ }
+
+ private bool processFragments(WebSocketFrame first)
+ {
+ using (var buff = _memoryStreamFactory.CreateNew())
+ {
+ buff.WriteBytes(first.PayloadData.ApplicationData);
+ if (!concatenateFragmentsInto(buff))
+ return false;
+
+ byte[] data;
+ if (_compression != CompressionMethod.None)
+ {
+ data = buff.DecompressToArray(_compression);
+ }
+ else
+ {
+ data = buff.ToArray();
+ }
+
+ enqueueToMessageEventQueue(new MessageEventArgs(first.Opcode, data));
+ return true;
+ }
+ }
+
+ private bool processPingFrame(WebSocketFrame frame)
+ {
+ var mask = Mask.Unmask;
+
+ return true;
+ }
+
+ private bool processPongFrame(WebSocketFrame frame)
+ {
+ _receivePong.Set();
+
+ return true;
+ }
+
+ private bool processUnsupportedFrame(WebSocketFrame frame, CloseStatusCode code, string reason)
+ {
+ processException(new WebSocketException(code, reason), null);
+
+ return false;
+ }
+
+ private bool processWebSocketFrame(WebSocketFrame frame)
+ {
+ return frame.IsCompressed && _compression == CompressionMethod.None
+ ? processUnsupportedFrame(
+ frame,
+ CloseStatusCode.IncorrectData,
+ "A compressed data has been received without available decompression method.")
+ : frame.IsFragmented
+ ? processFragmentedFrame(frame)
+ : frame.IsData
+ ? processDataFrame(frame)
+ : frame.IsPing
+ ? processPingFrame(frame)
+ : frame.IsPong
+ ? processPongFrame(frame)
+ : frame.IsClose
+ ? processCloseFrame(frame)
+ : processUnsupportedFrame(frame, CloseStatusCode.PolicyViolation, null);
+ }
+
+ private bool send(Opcode opcode, Stream stream)
+ {
+ lock (_forSend)
+ {
+ var src = stream;
+ var compressed = false;
+ var sent = false;
+ try
+ {
+ if (_compression != CompressionMethod.None)
+ {
+ stream = stream.Compress(_compression);
+ compressed = true;
+ }
+
+ sent = send(opcode, Mask.Unmask, stream, compressed);
+ if (!sent)
+ error("Sending a data has been interrupted.");
+ }
+ catch (Exception ex)
+ {
+ error("An exception has occurred while sending a data.", ex);
+ }
+ finally
+ {
+ if (compressed)
+ stream.Dispose();
+
+ src.Dispose();
+ }
+
+ return sent;
+ }
+ }
+
+ private bool send(Opcode opcode, Mask mask, Stream stream, bool compressed)
+ {
+ var len = stream.Length;
+
+ /* Not fragmented */
+
+ if (len == 0)
+ return send(Fin.Final, opcode, mask, new byte[0], compressed);
+
+ var quo = len / FragmentLength;
+ var rem = (int)(len % FragmentLength);
+
+ byte[] buff = null;
+ if (quo == 0)
+ {
+ buff = new byte[rem];
+ return stream.Read(buff, 0, rem) == rem &&
+ send(Fin.Final, opcode, mask, buff, compressed);
+ }
+
+ buff = new byte[FragmentLength];
+ if (quo == 1 && rem == 0)
+ return stream.Read(buff, 0, FragmentLength) == FragmentLength &&
+ send(Fin.Final, opcode, mask, buff, compressed);
+
+ /* Send fragmented */
+
+ // Begin
+ if (stream.Read(buff, 0, FragmentLength) != FragmentLength ||
+ !send(Fin.More, opcode, mask, buff, compressed))
+ return false;
+
+ var n = rem == 0 ? quo - 2 : quo - 1;
+ for (long i = 0; i < n; i++)
+ if (stream.Read(buff, 0, FragmentLength) != FragmentLength ||
+ !send(Fin.More, Opcode.Cont, mask, buff, compressed))
+ return false;
+
+ // End
+ if (rem == 0)
+ rem = FragmentLength;
+ else
+ buff = new byte[rem];
+
+ return stream.Read(buff, 0, rem) == rem &&
+ send(Fin.Final, Opcode.Cont, mask, buff, compressed);
+ }
+
+ private bool send(Fin fin, Opcode opcode, Mask mask, byte[] data, bool compressed)
+ {
+ lock (_forConn)
+ {
+ if (_readyState != WebSocketState.Open)
+ {
+ return false;
+ }
+
+ return writeBytes(
+ WebSocketFrame.CreateWebSocketFrame(fin, opcode, mask, data, compressed).ToByteArray());
+ }
+ }
+
+ private Task sendAsync(Opcode opcode, Stream stream)
+ {
+ var completionSource = new TaskCompletionSource<bool>();
+ Task.Run(() =>
+ {
+ try
+ {
+ send(opcode, stream);
+ completionSource.TrySetResult(true);
+ }
+ catch (Exception ex)
+ {
+ completionSource.TrySetException(ex);
+ }
+ });
+ return completionSource.Task;
+ }
+
+ // As server
+ private bool sendHttpResponse(HttpResponse response)
+ {
+ return writeBytes(response.ToByteArray());
+ }
+
+ private void startReceiving()
+ {
+ if (_messageEventQueue.Count > 0)
+ _messageEventQueue.Clear();
+
+ _exitReceiving = new AutoResetEvent(false);
+ _receivePong = new AutoResetEvent(false);
+
+ Action receive = null;
+ receive = () => WebSocketFrame.ReadAsync(
+ _stream,
+ true,
+ frame =>
+ {
+ if (processWebSocketFrame(frame) && _readyState != WebSocketState.Closed)
+ {
+ receive();
+
+ if (!frame.IsData)
+ return;
+
+ lock (_forEvent)
+ {
+ try
+ {
+ var e = dequeueFromMessageEventQueue();
+ if (e != null && _readyState == WebSocketState.Open)
+ OnMessage.Emit(this, e);
+ }
+ catch (Exception ex)
+ {
+ processException(ex, "An exception has occurred while OnMessage.");
+ }
+ }
+ }
+ else if (_exitReceiving != null)
+ {
+ _exitReceiving.Set();
+ }
+ },
+ ex => processException(ex, "An exception has occurred while receiving a message."));
+
+ receive();
+ }
+
+ // As server
+ private bool validateSecWebSocketKeyHeader(string value)
+ {
+ if (value == null || value.Length == 0)
+ return false;
+
+ _base64Key = value;
+ return true;
+ }
+
+ // As server
+ private bool validateSecWebSocketVersionClientHeader(string value)
+ {
+ return true;
+ //return value != null && value == _version;
+ }
+
+ private bool writeBytes(byte[] data)
+ {
+ try
+ {
+ _stream.Write(data, 0, data.Length);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ return false;
+ }
+ }
+
+ #endregion
+
+ #region Internal Methods
+
+ // As server
+ internal void Close(HttpResponse response)
+ {
+ _readyState = WebSocketState.Closing;
+
+ sendHttpResponse(response);
+ closeServerResources();
+
+ _readyState = WebSocketState.Closed;
+ }
+
+ // As server
+ internal void Close(HttpStatusCode code)
+ {
+ Close(createHandshakeCloseResponse(code));
+ }
+
+ // As server
+ public void ConnectAsServer()
+ {
+ try
+ {
+ if (acceptHandshake())
+ {
+ _readyState = WebSocketState.Open;
+ open();
+ }
+ }
+ catch (Exception ex)
+ {
+ processException(ex, "An exception has occurred while connecting.");
+ }
+ }
+
+ private string CreateResponseKey(string base64Key)
+ {
+ var buff = new StringBuilder(base64Key, 64);
+ buff.Append(_guid);
+ var src = _cryptoProvider.ComputeSHA1(Encoding.UTF8.GetBytes(buff.ToString()));
+
+ return Convert.ToBase64String(src);
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ /// <summary>
+ /// Closes the WebSocket connection, and releases all associated resources.
+ /// </summary>
+ public void Close()
+ {
+ var msg = _readyState.CheckIfClosable();
+ if (msg != null)
+ {
+ error(msg);
+
+ return;
+ }
+
+ var send = _readyState == WebSocketState.Open;
+ close(new PayloadData(), send, send);
+ }
+
+ /// <summary>
+ /// Closes the WebSocket connection with the specified <see cref="CloseStatusCode"/>
+ /// and <see cref="string"/>, and releases all associated resources.
+ /// </summary>
+ /// <remarks>
+ /// This method emits a <see cref="OnError"/> event if the size
+ /// of <paramref name="reason"/> is greater than 123 bytes.
+ /// </remarks>
+ /// <param name="code">
+ /// One of the <see cref="CloseStatusCode"/> enum values, represents the status code
+ /// indicating the reason for the close.
+ /// </param>
+ /// <param name="reason">
+ /// A <see cref="string"/> that represents the reason for the close.
+ /// </param>
+ public void Close(CloseStatusCode code, string reason)
+ {
+ byte[] data = null;
+ var msg = _readyState.CheckIfClosable() ??
+ (data = ((ushort)code).Append(reason)).CheckIfValidControlData("reason");
+
+ if (msg != null)
+ {
+ error(msg);
+
+ return;
+ }
+
+ var send = _readyState == WebSocketState.Open && !code.IsReserved();
+ close(new PayloadData(data), send, send);
+ }
+
+ /// <summary>
+ /// Sends a binary <paramref name="data"/> asynchronously using the WebSocket connection.
+ /// </summary>
+ /// <remarks>
+ /// This method doesn't wait for the send to be complete.
+ /// </remarks>
+ /// <param name="data">
+ /// An array of <see cref="byte"/> that represents the binary data to send.
+ /// </param>
+ /// An Action&lt;bool&gt; delegate that references the method(s) called when the send is
+ /// complete. A <see cref="bool"/> passed to this delegate is <c>true</c> if the send is
+ /// complete successfully; otherwise, <c>false</c>.
+ public Task SendAsync(byte[] data)
+ {
+ var msg = _readyState.CheckIfOpen() ?? data.CheckIfValidSendData();
+ if (msg != null)
+ {
+ throw new Exception(msg);
+ }
+
+ return sendAsync(Opcode.Binary, _memoryStreamFactory.CreateNew(data));
+ }
+
+ /// <summary>
+ /// Sends a text <paramref name="data"/> asynchronously using the WebSocket connection.
+ /// </summary>
+ /// <remarks>
+ /// This method doesn't wait for the send to be complete.
+ /// </remarks>
+ /// <param name="data">
+ /// A <see cref="string"/> that represents the text data to send.
+ /// </param>
+ /// An Action&lt;bool&gt; delegate that references the method(s) called when the send is
+ /// complete. A <see cref="bool"/> passed to this delegate is <c>true</c> if the send is
+ /// complete successfully; otherwise, <c>false</c>.
+ public Task SendAsync(string data)
+ {
+ var msg = _readyState.CheckIfOpen() ?? data.CheckIfValidSendData();
+ if (msg != null)
+ {
+ throw new Exception(msg);
+ }
+
+ return sendAsync(Opcode.Text, _memoryStreamFactory.CreateNew(Encoding.UTF8.GetBytes(data)));
+ }
+
+ #endregion
+
+ #region Explicit Interface Implementation
+
+ /// <summary>
+ /// Closes the WebSocket connection, and releases all associated resources.
+ /// </summary>
+ /// <remarks>
+ /// This method closes the WebSocket connection with <see cref="CloseStatusCode.Away"/>.
+ /// </remarks>
+ void IDisposable.Dispose()
+ {
+ Close(CloseStatusCode.Away, null);
+ }
+
+ #endregion
+ }
+} \ No newline at end of file
diff --git a/SocketHttpListener/WebSocketException.cs b/SocketHttpListener/WebSocketException.cs
new file mode 100644
index 000000000..260721317
--- /dev/null
+++ b/SocketHttpListener/WebSocketException.cs
@@ -0,0 +1,60 @@
+using System;
+
+namespace SocketHttpListener
+{
+ /// <summary>
+ /// The exception that is thrown when a <see cref="WebSocket"/> gets a fatal error.
+ /// </summary>
+ public class WebSocketException : Exception
+ {
+ #region Internal Constructors
+
+ internal WebSocketException ()
+ : this (CloseStatusCode.Abnormal, null, null)
+ {
+ }
+
+ internal WebSocketException (string message)
+ : this (CloseStatusCode.Abnormal, message, null)
+ {
+ }
+
+ internal WebSocketException (CloseStatusCode code)
+ : this (code, null, null)
+ {
+ }
+
+ internal WebSocketException (string message, Exception innerException)
+ : this (CloseStatusCode.Abnormal, message, innerException)
+ {
+ }
+
+ internal WebSocketException (CloseStatusCode code, string message)
+ : this (code, message, null)
+ {
+ }
+
+ internal WebSocketException (CloseStatusCode code, string message, Exception innerException)
+ : base (message ?? code.GetMessage (), innerException)
+ {
+ Code = code;
+ }
+
+ #endregion
+
+ #region Public Properties
+
+ /// <summary>
+ /// Gets the status code indicating the cause for the exception.
+ /// </summary>
+ /// <value>
+ /// One of the <see cref="CloseStatusCode"/> enum values, represents the status code indicating
+ /// the cause for the exception.
+ /// </value>
+ public CloseStatusCode Code {
+ get; private set;
+ }
+
+ #endregion
+ }
+}
diff --git a/SocketHttpListener/WebSocketFrame.cs b/SocketHttpListener/WebSocketFrame.cs
new file mode 100644
index 000000000..44fa4a5dc
--- /dev/null
+++ b/SocketHttpListener/WebSocketFrame.cs
@@ -0,0 +1,578 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace SocketHttpListener
+{
+ internal class WebSocketFrame : IEnumerable<byte>
+ {
+ #region Private Fields
+
+ private byte[] _extPayloadLength;
+ private Fin _fin;
+ private Mask _mask;
+ private byte[] _maskingKey;
+ private Opcode _opcode;
+ private PayloadData _payloadData;
+ private byte _payloadLength;
+ private Rsv _rsv1;
+ private Rsv _rsv2;
+ private Rsv _rsv3;
+
+ #endregion
+
+ #region Internal Fields
+
+ internal static readonly byte[] EmptyUnmaskPingData;
+
+ #endregion
+
+ #region Static Constructor
+
+ static WebSocketFrame()
+ {
+ EmptyUnmaskPingData = CreatePingFrame(Mask.Unmask).ToByteArray();
+ }
+
+ #endregion
+
+ #region Private Constructors
+
+ private WebSocketFrame()
+ {
+ }
+
+ #endregion
+
+ #region Internal Constructors
+
+ internal WebSocketFrame(Opcode opcode, PayloadData payload)
+ : this(Fin.Final, opcode, Mask.Mask, payload, false)
+ {
+ }
+
+ internal WebSocketFrame(Opcode opcode, Mask mask, PayloadData payload)
+ : this(Fin.Final, opcode, mask, payload, false)
+ {
+ }
+
+ internal WebSocketFrame(Fin fin, Opcode opcode, Mask mask, PayloadData payload)
+ : this(fin, opcode, mask, payload, false)
+ {
+ }
+
+ internal WebSocketFrame(
+ Fin fin, Opcode opcode, Mask mask, PayloadData payload, bool compressed)
+ {
+ _fin = fin;
+ _rsv1 = isData(opcode) && compressed ? Rsv.On : Rsv.Off;
+ _rsv2 = Rsv.Off;
+ _rsv3 = Rsv.Off;
+ _opcode = opcode;
+ _mask = mask;
+
+ var len = payload.Length;
+ if (len < 126)
+ {
+ _payloadLength = (byte)len;
+ _extPayloadLength = new byte[0];
+ }
+ else if (len < 0x010000)
+ {
+ _payloadLength = (byte)126;
+ _extPayloadLength = ((ushort)len).ToByteArrayInternally(ByteOrder.Big);
+ }
+ else
+ {
+ _payloadLength = (byte)127;
+ _extPayloadLength = len.ToByteArrayInternally(ByteOrder.Big);
+ }
+
+ if (mask == Mask.Mask)
+ {
+ _maskingKey = createMaskingKey();
+ payload.Mask(_maskingKey);
+ }
+ else
+ {
+ _maskingKey = new byte[0];
+ }
+
+ _payloadData = payload;
+ }
+
+ #endregion
+
+ #region Public Properties
+
+ public byte[] ExtendedPayloadLength
+ {
+ get
+ {
+ return _extPayloadLength;
+ }
+ }
+
+ public Fin Fin
+ {
+ get
+ {
+ return _fin;
+ }
+ }
+
+ public bool IsBinary
+ {
+ get
+ {
+ return _opcode == Opcode.Binary;
+ }
+ }
+
+ public bool IsClose
+ {
+ get
+ {
+ return _opcode == Opcode.Close;
+ }
+ }
+
+ public bool IsCompressed
+ {
+ get
+ {
+ return _rsv1 == Rsv.On;
+ }
+ }
+
+ public bool IsContinuation
+ {
+ get
+ {
+ return _opcode == Opcode.Cont;
+ }
+ }
+
+ public bool IsControl
+ {
+ get
+ {
+ return _opcode == Opcode.Close || _opcode == Opcode.Ping || _opcode == Opcode.Pong;
+ }
+ }
+
+ public bool IsData
+ {
+ get
+ {
+ return _opcode == Opcode.Binary || _opcode == Opcode.Text;
+ }
+ }
+
+ public bool IsFinal
+ {
+ get
+ {
+ return _fin == Fin.Final;
+ }
+ }
+
+ public bool IsFragmented
+ {
+ get
+ {
+ return _fin == Fin.More || _opcode == Opcode.Cont;
+ }
+ }
+
+ public bool IsMasked
+ {
+ get
+ {
+ return _mask == Mask.Mask;
+ }
+ }
+
+ public bool IsPerMessageCompressed
+ {
+ get
+ {
+ return (_opcode == Opcode.Binary || _opcode == Opcode.Text) && _rsv1 == Rsv.On;
+ }
+ }
+
+ public bool IsPing
+ {
+ get
+ {
+ return _opcode == Opcode.Ping;
+ }
+ }
+
+ public bool IsPong
+ {
+ get
+ {
+ return _opcode == Opcode.Pong;
+ }
+ }
+
+ public bool IsText
+ {
+ get
+ {
+ return _opcode == Opcode.Text;
+ }
+ }
+
+ public ulong Length
+ {
+ get
+ {
+ return 2 + (ulong)(_extPayloadLength.Length + _maskingKey.Length) + _payloadData.Length;
+ }
+ }
+
+ public Mask Mask
+ {
+ get
+ {
+ return _mask;
+ }
+ }
+
+ public byte[] MaskingKey
+ {
+ get
+ {
+ return _maskingKey;
+ }
+ }
+
+ public Opcode Opcode
+ {
+ get
+ {
+ return _opcode;
+ }
+ }
+
+ public PayloadData PayloadData
+ {
+ get
+ {
+ return _payloadData;
+ }
+ }
+
+ public byte PayloadLength
+ {
+ get
+ {
+ return _payloadLength;
+ }
+ }
+
+ public Rsv Rsv1
+ {
+ get
+ {
+ return _rsv1;
+ }
+ }
+
+ public Rsv Rsv2
+ {
+ get
+ {
+ return _rsv2;
+ }
+ }
+
+ public Rsv Rsv3
+ {
+ get
+ {
+ return _rsv3;
+ }
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ private byte[] createMaskingKey()
+ {
+ var key = new byte[4];
+ var rand = new Random();
+ rand.NextBytes(key);
+
+ return key;
+ }
+
+ private static bool isControl(Opcode opcode)
+ {
+ return opcode == Opcode.Close || opcode == Opcode.Ping || opcode == Opcode.Pong;
+ }
+
+ private static bool isData(Opcode opcode)
+ {
+ return opcode == Opcode.Text || opcode == Opcode.Binary;
+ }
+
+ private static WebSocketFrame read(byte[] header, Stream stream, bool unmask)
+ {
+ /* Header */
+
+ // FIN
+ var fin = (header[0] & 0x80) == 0x80 ? Fin.Final : Fin.More;
+ // RSV1
+ var rsv1 = (header[0] & 0x40) == 0x40 ? Rsv.On : Rsv.Off;
+ // RSV2
+ var rsv2 = (header[0] & 0x20) == 0x20 ? Rsv.On : Rsv.Off;
+ // RSV3
+ var rsv3 = (header[0] & 0x10) == 0x10 ? Rsv.On : Rsv.Off;
+ // Opcode
+ var opcode = (Opcode)(header[0] & 0x0f);
+ // MASK
+ var mask = (header[1] & 0x80) == 0x80 ? Mask.Mask : Mask.Unmask;
+ // Payload Length
+ var payloadLen = (byte)(header[1] & 0x7f);
+
+ // Check if correct frame.
+ var incorrect = isControl(opcode) && fin == Fin.More
+ ? "A control frame is fragmented."
+ : !isData(opcode) && rsv1 == Rsv.On
+ ? "A non data frame is compressed."
+ : null;
+
+ if (incorrect != null)
+ throw new WebSocketException(CloseStatusCode.IncorrectData, incorrect);
+
+ // Check if consistent frame.
+ if (isControl(opcode) && payloadLen > 125)
+ throw new WebSocketException(
+ CloseStatusCode.InconsistentData,
+ "The length of payload data of a control frame is greater than 125 bytes.");
+
+ var frame = new WebSocketFrame();
+ frame._fin = fin;
+ frame._rsv1 = rsv1;
+ frame._rsv2 = rsv2;
+ frame._rsv3 = rsv3;
+ frame._opcode = opcode;
+ frame._mask = mask;
+ frame._payloadLength = payloadLen;
+
+ /* Extended Payload Length */
+
+ var size = payloadLen < 126
+ ? 0
+ : payloadLen == 126
+ ? 2
+ : 8;
+
+ var extPayloadLen = size > 0 ? stream.ReadBytes(size) : new byte[0];
+ if (size > 0 && extPayloadLen.Length != size)
+ throw new WebSocketException(
+ "The 'Extended Payload Length' of a frame cannot be read from the data source.");
+
+ frame._extPayloadLength = extPayloadLen;
+
+ /* Masking Key */
+
+ var masked = mask == Mask.Mask;
+ var maskingKey = masked ? stream.ReadBytes(4) : new byte[0];
+ if (masked && maskingKey.Length != 4)
+ throw new WebSocketException(
+ "The 'Masking Key' of a frame cannot be read from the data source.");
+
+ frame._maskingKey = maskingKey;
+
+ /* Payload Data */
+
+ ulong len = payloadLen < 126
+ ? payloadLen
+ : payloadLen == 126
+ ? extPayloadLen.ToUInt16(ByteOrder.Big)
+ : extPayloadLen.ToUInt64(ByteOrder.Big);
+
+ byte[] data = null;
+ if (len > 0)
+ {
+ // Check if allowable payload data length.
+ if (payloadLen > 126 && len > PayloadData.MaxLength)
+ throw new WebSocketException(
+ CloseStatusCode.TooBig,
+ "The length of 'Payload Data' of a frame is greater than the allowable length.");
+
+ data = payloadLen > 126
+ ? stream.ReadBytes((long)len, 1024)
+ : stream.ReadBytes((int)len);
+
+ //if (data.LongLength != (long)len)
+ // throw new WebSocketException(
+ // "The 'Payload Data' of a frame cannot be read from the data source.");
+ }
+ else
+ {
+ data = new byte[0];
+ }
+
+ var payload = new PayloadData(data, masked);
+ if (masked && unmask)
+ {
+ payload.Mask(maskingKey);
+ frame._mask = Mask.Unmask;
+ frame._maskingKey = new byte[0];
+ }
+
+ frame._payloadData = payload;
+ return frame;
+ }
+
+ #endregion
+
+ #region Internal Methods
+
+ internal static WebSocketFrame CreateCloseFrame(Mask mask, byte[] data)
+ {
+ return new WebSocketFrame(Opcode.Close, mask, new PayloadData(data));
+ }
+
+ internal static WebSocketFrame CreateCloseFrame(Mask mask, PayloadData payload)
+ {
+ return new WebSocketFrame(Opcode.Close, mask, payload);
+ }
+
+ internal static WebSocketFrame CreateCloseFrame(Mask mask, CloseStatusCode code, string reason)
+ {
+ return new WebSocketFrame(
+ Opcode.Close, mask, new PayloadData(((ushort)code).Append(reason)));
+ }
+
+ internal static WebSocketFrame CreatePingFrame(Mask mask)
+ {
+ return new WebSocketFrame(Opcode.Ping, mask, new PayloadData());
+ }
+
+ internal static WebSocketFrame CreatePingFrame(Mask mask, byte[] data)
+ {
+ return new WebSocketFrame(Opcode.Ping, mask, new PayloadData(data));
+ }
+
+ internal static WebSocketFrame CreatePongFrame(Mask mask, PayloadData payload)
+ {
+ return new WebSocketFrame(Opcode.Pong, mask, payload);
+ }
+
+ internal static WebSocketFrame CreateWebSocketFrame(
+ Fin fin, Opcode opcode, Mask mask, byte[] data, bool compressed)
+ {
+ return new WebSocketFrame(fin, opcode, mask, new PayloadData(data), compressed);
+ }
+
+ internal static WebSocketFrame Read(Stream stream)
+ {
+ return Read(stream, true);
+ }
+
+ internal static WebSocketFrame Read(Stream stream, bool unmask)
+ {
+ var header = stream.ReadBytes(2);
+ if (header.Length != 2)
+ throw new WebSocketException(
+ "The header part of a frame cannot be read from the data source.");
+
+ return read(header, stream, unmask);
+ }
+
+ internal static async void ReadAsync(
+ Stream stream, bool unmask, Action<WebSocketFrame> completed, Action<Exception> error)
+ {
+ try
+ {
+ var header = await stream.ReadBytesAsync(2).ConfigureAwait(false);
+ if (header.Length != 2)
+ throw new WebSocketException(
+ "The header part of a frame cannot be read from the data source.");
+
+ var frame = read(header, stream, unmask);
+ if (completed != null)
+ completed(frame);
+ }
+ catch (Exception ex)
+ {
+ if (error != null)
+ {
+ error(ex);
+ }
+ }
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ public IEnumerator<byte> GetEnumerator()
+ {
+ foreach (var b in ToByteArray())
+ yield return b;
+ }
+
+ public void Print(bool dumped)
+ {
+ //Console.WriteLine(dumped ? dump(this) : print(this));
+ }
+
+ public byte[] ToByteArray()
+ {
+ using (var buff = new MemoryStream())
+ {
+ var header = (int)_fin;
+ header = (header << 1) + (int)_rsv1;
+ header = (header << 1) + (int)_rsv2;
+ header = (header << 1) + (int)_rsv3;
+ header = (header << 4) + (int)_opcode;
+ header = (header << 1) + (int)_mask;
+ header = (header << 7) + (int)_payloadLength;
+ buff.Write(((ushort)header).ToByteArrayInternally(ByteOrder.Big), 0, 2);
+
+ if (_payloadLength > 125)
+ buff.Write(_extPayloadLength, 0, _extPayloadLength.Length);
+
+ if (_mask == Mask.Mask)
+ buff.Write(_maskingKey, 0, _maskingKey.Length);
+
+ if (_payloadLength > 0)
+ {
+ var payload = _payloadData.ToByteArray();
+ if (_payloadLength < 127)
+ buff.Write(payload, 0, payload.Length);
+ else
+ buff.WriteBytes(payload);
+ }
+
+ return buff.ToArray();
+ }
+ }
+
+ public override string ToString()
+ {
+ return BitConverter.ToString(ToByteArray());
+ }
+
+ #endregion
+
+ #region Explicitly Implemented Interface Members
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ #endregion
+ }
+} \ No newline at end of file
diff --git a/SocketHttpListener/WebSocketState.cs b/SocketHttpListener/WebSocketState.cs
new file mode 100644
index 000000000..73b3a49dd
--- /dev/null
+++ b/SocketHttpListener/WebSocketState.cs
@@ -0,0 +1,35 @@
+namespace SocketHttpListener
+{
+ /// <summary>
+ /// Contains the values of the state of the WebSocket connection.
+ /// </summary>
+ /// <remarks>
+ /// The values of the state are defined in
+ /// <see href="http://www.w3.org/TR/websockets/#dom-websocket-readystate">The WebSocket
+ /// API</see>.
+ /// </remarks>
+ public enum WebSocketState : ushort
+ {
+ /// <summary>
+ /// Equivalent to numeric value 0.
+ /// Indicates that the connection has not yet been established.
+ /// </summary>
+ Connecting = 0,
+ /// <summary>
+ /// Equivalent to numeric value 1.
+ /// Indicates that the connection is established and the communication is possible.
+ /// </summary>
+ Open = 1,
+ /// <summary>
+ /// Equivalent to numeric value 2.
+ /// Indicates that the connection is going through the closing handshake or
+ /// the <c>WebSocket.Close</c> method has been invoked.
+ /// </summary>
+ Closing = 2,
+ /// <summary>
+ /// Equivalent to numeric value 3.
+ /// Indicates that the connection has been closed or couldn't be opened.
+ /// </summary>
+ Closed = 3
+ }
+}