using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Services; using HttpStatusCode = SocketHttpListener.Net.HttpStatusCode; using WebSocketState = System.Net.WebSockets.WebSocketState; namespace SocketHttpListener { /// /// Provides a set of static methods for the websocket-sharp. /// 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 async Task ReadBytesAsync(this Stream stream, byte[] buffer, int offset, int length) { var len = await stream.ReadAsync(buffer, offset, length).ConfigureAwait(false); if (len < 1) return buffer.SubArray(0, offset); var tmp = 0; while (len < length) { tmp = await stream.ReadAsync(buffer, offset + len, length - len).ConfigureAwait(false); 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; } private static async Task ReadBytesAsync(this Stream stream, byte[] buffer, int offset, int length, Stream dest) { var bytes = await stream.ReadBytesAsync(buffer, offset, length).ConfigureAwait(false); 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.CloseSent ? "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.CloseSent ? "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 Stream Compress(this Stream stream, CompressionMethod method) { return method == CompressionMethod.Deflate ? stream.compress() : stream; } internal static bool Contains(this IEnumerable source, Func 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(); } /// /// Determines whether the specified equals the specified , /// and invokes the specified Action<int> delegate at the same time. /// /// /// true if equals ; /// otherwise, false. /// /// /// An to compare. /// /// /// A to compare. /// /// /// An Action<int> delegate that references the method(s) called at /// the same time as comparing. An parameter to pass to /// the method(s) is . /// /// /// isn't between 0 and 255. /// internal static bool EqualsWith(this int value, char c, Action action) { if (value < 0 || value > 255) throw new ArgumentOutOfRangeException(nameof(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 Task ReadBytesAsync(this Stream stream, int length) => stream.ReadBytesAsync(new byte[length], 0, length); internal static async Task ReadBytesAsync(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 (!await stream.ReadBytesAsync(buffer, 0, bufferLength, result).ConfigureAwait(false)) { end = true; break; } } if (!end && rem > 0) { await stream.ReadBytesAsync(new byte[rem], 0, rem, result).ConfigureAwait(false); } return result.ToArray(); } } 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(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 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().ToLowerInvariant()); if (parameters == null || parameters.Length == 0) return m; return string.Format("{0}; {1}", m, parameters.ToString("; ")); } internal static ushort ToUInt16(this byte[] src, ByteOrder srcOrder) { src.ToHostOrder(srcOrder); return BitConverter.ToUInt16(src, 0); } internal static ulong ToUInt64(this byte[] src, ByteOrder srcOrder) { src.ToHostOrder(srcOrder); return BitConverter.ToUInt64(src, 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 /// /// Determines whether the specified contains any of characters /// in the specified array of . /// /// /// true if contains any of ; /// otherwise, false. /// /// /// A to test. /// /// /// An array of that contains characters to find. /// 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; } /// /// Determines whether the specified contains the entry /// with the specified . /// /// /// true if contains the entry /// with ; otherwise, false. /// /// /// A to test. /// /// /// A that represents the key of the entry to find. /// public static bool Contains(this QueryParamCollection collection, string name) { return collection == null || collection.Count == 0 ? false : collection[name] != null; } /// /// Determines whether the specified contains the entry /// with the specified both and . /// /// /// true if contains the entry /// with both and ; /// otherwise, false. /// /// /// A to test. /// /// /// A that represents the key of the entry to find. /// /// /// A that represents the value of the entry to find. /// 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; } /// /// Emits the specified EventHandler<TEventArgs> delegate /// if it isn't . /// /// /// An EventHandler<TEventArgs> to emit. /// /// /// An from which emits this . /// /// /// A TEventArgs that represents the event data. /// /// /// The type of the event data generated by the event. /// public static void Emit( this EventHandler eventHandler, object sender, TEventArgs e) where TEventArgs : EventArgs { if (eventHandler != null) eventHandler(sender, e); } /// /// Gets the description of the specified HTTP status . /// /// /// A that represents the description of the HTTP status code. /// /// /// One of enum values, indicates the HTTP status codes. /// public static string GetDescription(this HttpStatusCode code) { return ((int)code).GetStatusDescription(); } /// /// Gets the description of the specified HTTP status . /// /// /// A that represents the description of the HTTP status code. /// /// /// An that represents the HTTP status code. /// 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; } /// /// Determines whether the specified is host /// (this computer architecture) byte order. /// /// /// true if is host byte order; /// otherwise, false. /// /// /// One of the enum values, to test. /// 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)); } /// /// Determines whether the specified is a predefined scheme. /// /// /// true if is a predefined scheme; otherwise, false. /// /// /// A to test. /// 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"); } /// /// Determines whether the specified is a URI string. /// /// /// true if may be a URI string; otherwise, false. /// /// /// A to test. /// 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(); } /// /// Retrieves a sub-array from the specified . /// A sub-array starts at the specified element position. /// /// /// An array of T that receives a sub-array, or an empty array of T if any problems /// with the parameters. /// /// /// An array of T that contains the data to retrieve a sub-array. /// /// /// An that contains the zero-based starting position of a sub-array /// in . /// /// /// An that contains the number of elements to retrieve a sub-array. /// /// /// The type of elements in the . /// public static T[] SubArray(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; } /// /// Converts the order of the specified array of to the host byte order. /// /// /// An array of converted from . /// /// /// An array of to convert. /// /// /// One of the enum values, indicates the byte order of /// . /// /// /// is . /// public static void ToHostOrder(this byte[] src, ByteOrder srcOrder) { if (src == null) { throw new ArgumentNullException(nameof(src)); } if (src.Length > 1 && !srcOrder.IsHostOrder()) { Array.Reverse(src); } } /// /// Converts the specified to a that /// concatenates the each element of across the specified /// . /// /// /// A converted from , /// or if is empty. /// /// /// An array of T to convert. /// /// /// A that represents the separator string. /// /// /// The type of elements in . /// /// /// is . /// public static string ToString(this T[] array, string separator) { if (array == null) throw new ArgumentNullException(nameof(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(); } /// /// Executes the specified Action<int> delegate times. /// /// /// An is the number of times to execute. /// /// /// An Action<int> delegate that references the method(s) to execute. /// An parameter to pass to the method(s) is the zero-based count of /// iteration. /// public static void Times(this int n, Action action) { if (n > 0 && action != null) for (int i = 0; i < n; i++) action(i); } /// /// Converts the specified to a . /// /// /// A converted from , or /// if isn't successfully converted. /// /// /// A to convert. /// public static Uri ToUri(this string uriString) { return Uri.TryCreate( uriString, uriString.MaybeUri() ? UriKind.Absolute : UriKind.Relative, out var res) ? res : null; } /// /// URL-decodes the specified . /// /// /// A that receives the decoded string, or the /// if it's or empty. /// /// /// A to decode. /// public static string UrlDecode(this string value) { return value == null || value.Length == 0 ? value : WebUtility.UrlDecode(value); } #endregion } }