diff options
| author | Luke Pulverenti <luke.pulverenti@gmail.com> | 2017-05-24 15:12:55 -0400 |
|---|---|---|
| committer | Luke Pulverenti <luke.pulverenti@gmail.com> | 2017-05-24 15:12:55 -0400 |
| commit | f07af448fa11330db93dd7ddcabac37ef9e014c7 (patch) | |
| tree | 1b52a4f73d674a48258c2f14c94117b96ca4a678 /SocketHttpListener | |
| parent | 27c3acb2bfde9025c33f584c759a4038020cb702 (diff) | |
update main projects
Diffstat (limited to 'SocketHttpListener')
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<int> 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<int> 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<TEventArgs></c> delegate + /// if it isn't <see langword="null"/>. + /// </summary> + /// <param name="eventHandler"> + /// An <c>EventHandler<TEventArgs></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<string, string></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<int></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<int></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<bool> 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<bool> 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 + } +} |
