From 08a2be32213132901930783634115d11589c2aee Mon Sep 17 00:00:00 2001 From: Jeroen Date: Fri, 17 Nov 2023 20:26:11 +0100 Subject: [PATCH] Updated --- .../Core/Authentication/AuthenticationBase.cs | 6 +- .../Authentication/AuthenticationChallenge.cs | 19 +- .../Authentication/AuthenticationResponse.cs | 220 +- .../Core/Authentication/NetworkCredential.cs | 2 +- .../Sockets/Web/Core/Chunks/ChunkStream.cs | 151 +- .../Web/Core/Chunks/ChunkedRequestStream.cs | 62 +- .../Core/Collections/WebHeaderCollection.cs | 580 ++- .../Web/Core/Context/HttpListenerWSContext.cs | 21 +- .../Web/Core/Context/TcpListenerWSContext.cs | 38 +- .../System/Sockets/Web/Core/Cookies/Cookie.cs | 291 +- .../Web/Core/Cookies/CookieCollection.cs | 362 +- .../Web/Core/Cookies/CookieException.cs | 17 +- .../Web/Core/Endpoints/EndPointListener.cs | 581 ++- .../Web/Core/Endpoints/EndPointManager.cs | 182 +- .../Sockets/Web/Core/Http/HttpConnection.cs | 745 ++-- .../Sockets/Web/Core/Http/HttpHeaderInfo.cs | 19 +- .../Sockets/Web/Core/Http/HttpListener.cs | 610 +-- .../Web/Core/Http/HttpListenerAsyncResult.cs | 83 +- .../Web/Core/Http/HttpListenerContext.cs | 79 +- .../Web/Core/Http/HttpListenerException.cs | 17 +- .../Web/Core/Http/HttpListenerPrefix.cs | 58 +- .../Core/Http/HttpListenerPrefixCollection.cs | 18 +- .../Web/Core/Http/HttpListenerRequest.cs | 41 +- .../Web/Core/Http/HttpListenerResponse.cs | 4 +- .../Web/Core/Http/HttpStreamAsyncResult.cs | 53 +- .../Sockets/Web/Core/Http/HttpUtility.cs | 1536 ++++--- .../System/Sockets/Web/Core/Logger.cs | 185 +- .../Web/Core/SSLConfig/SSLConfigClient.cs | 12 +- .../Web/Core/SSLConfig/SSLConfigServer.cs | 11 +- .../Sockets/Web/Core/Stream/RequestStream.cs | 125 +- .../Sockets/Web/Core/Stream/ResponseStream.cs | 238 +- .../Sockets/Web/EventArgs/CloseEventArgs.cs | 5 +- .../Sockets/Web/EventArgs/MessageEventArgs.cs | 9 +- .../System/Sockets/Web/Extensions.cs | 1184 ++--- .../System/Sockets/Web/OperationCode.cs | 2 +- .../System/Sockets/Web/PayloadData.cs | 15 + .../Web/Server/HttpRequestEventArgs.cs | 60 +- .../System/Sockets/Web/Server/HttpServer.cs | 386 +- .../System/Sockets/Web/Server/WSEndpoint.cs | 98 +- .../Sockets/Web/Server/WSEndpointHost.cs | 8 +- .../Sockets/Web/Server/WSEndpointManager.cs | 417 +- .../System/Sockets/Web/Server/WSServer.cs | 358 +- .../Sockets/Web/Server/WSSessionManager.cs | 565 ++- .../Web/Server/WebSocketEndpointHost.cs | 12 +- .../System/Sockets/Web/WSClient.cs | 3903 ++++++++--------- EonaCat.Network/System/Sockets/Web/WSFrame.cs | 382 +- EonaCat.Network/System/Sockets/Web/WebBase.cs | 112 +- .../System/Sockets/Web/WebRequest.cs | 135 +- .../System/Sockets/Web/WebResponse.cs | 85 +- 49 files changed, 7033 insertions(+), 7069 deletions(-) diff --git a/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationBase.cs b/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationBase.cs index 2ca1941..35c4d75 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationBase.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationBase.cs @@ -84,7 +84,7 @@ namespace EonaCat.Network /// A collection of authentication parameters. internal static NameValueCollection ParseParameters(string value) { - var res = new NameValueCollection(); + var result = new NameValueCollection(); foreach (var param in value.SplitHeaderValue(',')) { var i = param.IndexOf('='); @@ -95,10 +95,10 @@ namespace EonaCat.Network ? param.Substring(i + 1).Trim().Trim('"') : string.Empty; - res.Add(name, val); + result.Add(name, val); } - return res; + return result; } /// diff --git a/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationChallenge.cs b/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationChallenge.cs index 7b98248..4c4ba68 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationChallenge.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationChallenge.cs @@ -15,16 +15,6 @@ namespace EonaCat.Network private const string DIGEST = "digest"; private const int DIGEST_SIZE = 128; - /// - /// Initializes a new instance of the class. - /// - /// The authentication scheme. - /// The collection of authentication parameters. - private AuthenticationChallenge(AuthenticationSchemes scheme, NameValueCollection parameters) - : base(scheme, parameters) - { - } - /// /// Initializes a new instance of the class for Basic or Digest authentication. /// @@ -42,6 +32,15 @@ namespace EonaCat.Network } } + /// + /// Initializes a new instance of the class. + /// + /// The authentication scheme. + /// The collection of authentication parameters. + private AuthenticationChallenge(AuthenticationSchemes scheme, NameValueCollection parameters) + : base(scheme, parameters) + { + } /// /// Gets the domain for Digest authentication. /// diff --git a/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationResponse.cs b/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationResponse.cs index 6cc4b4f..10d14c6 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationResponse.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationResponse.cs @@ -18,16 +18,6 @@ namespace EonaCat.Network private const string DIGEST = "digest"; private uint _nonceCount; - /// - /// Initializes a new instance of the class. - /// - /// The authentication scheme. - /// The collection of authentication parameters. - private AuthenticationResponse(AuthenticationSchemes scheme, NameValueCollection parameters) - : base(scheme, parameters) - { - } - /// /// Initializes a new instance of the class for Basic authentication. /// @@ -74,12 +64,14 @@ namespace EonaCat.Network } /// - /// Gets the nonce count. + /// Initializes a new instance of the class. /// - internal uint NonceCount => _nonceCount < uint.MaxValue - ? _nonceCount - : 0; - + /// The authentication scheme. + /// The collection of authentication parameters. + private AuthenticationResponse(AuthenticationSchemes scheme, NameValueCollection parameters) + : base(scheme, parameters) + { + } /// /// Gets the cnonce value for Digest authentication. /// @@ -111,97 +103,23 @@ namespace EonaCat.Network public string UserName => Parameters["username"]; /// - /// Creates the A1 value for Digest authentication. + /// Gets the nonce count. /// - /// The username. - /// The password. - /// The realm. - /// The A1 value. - private static string CreateA1(string username, string password, string realm) - { - return $"{username}:{realm}:{password}"; - } - + internal uint NonceCount => _nonceCount < uint.MaxValue + ? _nonceCount + : 0; /// - /// Creates the A1 value for Digest authentication with cnonce and nonce. + /// Converts the authentication response to an identity. /// - /// The username. - /// The password. - /// The realm. - /// The nonce. - /// The cnonce. - /// The A1 value. - private static string CreateA1( - string username, string password, string realm, string nonce, string cnonce) + /// An instance of . + public IIdentity ToIdentity() { - return $"{Hash(CreateA1(username, password, realm))}:{nonce}:{cnonce}"; - } - - /// - /// Creates the A2 value for Digest authentication. - /// - /// The HTTP method. - /// The URI. - /// The A2 value. - private static string CreateA2(string method, string uri) - { - return $"{method}:{uri}"; - } - - /// - /// Creates the A2 value for Digest authentication with an entity. - /// - /// The HTTP method. - /// The URI. - /// The entity. - /// The A2 value. - private static string CreateA2(string method, string uri, string entity) - { - return $"{method}:{uri}:{Hash(entity)}"; - } - - /// - /// Computes the MD5 hash of the given value. - /// - /// The value to hash. - /// The MD5 hash. - private static string Hash(string value) - { - var source = Encoding.UTF8.GetBytes(value); - var md5 = MD5.Create(); - var hashed = md5.ComputeHash(source); - - var result = new StringBuilder(64); - foreach (var currentByte in hashed) - { - result.Append(currentByte.ToString("x2")); - } - - return result.ToString(); - } - - /// - /// Initializes the authentication as Digest. - /// - private void InitAsDigest() - { - var qops = Parameters["qop"]; - if (qops != null) - { - if (qops.Split(',').Contains(qop => qop.Trim().ToLower() == "auth")) - { - Parameters["qop"] = "auth"; - Parameters["cnonce"] = CreateNonceValue(); - Parameters["nc"] = string.Format("{0:x8}", ++_nonceCount); - } - else - { - Parameters["qop"] = null; - } - } - - Parameters["method"] = "GET"; - Parameters["response"] = CreateRequestDigest(Parameters); + var scheme = Scheme; + return scheme == AuthenticationSchemes.Basic + ? new HttpBasicIdentity(Parameters["username"], Parameters["password"]) + : scheme == AuthenticationSchemes.Digest + ? new HttpDigestIdentity(Parameters) + : null; } /// @@ -343,17 +261,97 @@ namespace EonaCat.Network } /// - /// Converts the authentication response to an identity. + /// Creates the A1 value for Digest authentication. /// - /// An instance of . - public IIdentity ToIdentity() + /// The username. + /// The password. + /// The realm. + /// The A1 value. + private static string CreateA1(string username, string password, string realm) { - var scheme = Scheme; - return scheme == AuthenticationSchemes.Basic - ? new HttpBasicIdentity(Parameters["username"], Parameters["password"]) - : scheme == AuthenticationSchemes.Digest - ? new HttpDigestIdentity(Parameters) - : null; + return $"{username}:{realm}:{password}"; + } + + /// + /// Creates the A1 value for Digest authentication with cnonce and nonce. + /// + /// The username. + /// The password. + /// The realm. + /// The nonce. + /// The cnonce. + /// The A1 value. + private static string CreateA1( + string username, string password, string realm, string nonce, string cnonce) + { + return $"{Hash(CreateA1(username, password, realm))}:{nonce}:{cnonce}"; + } + + /// + /// Creates the A2 value for Digest authentication. + /// + /// The HTTP method. + /// The URI. + /// The A2 value. + private static string CreateA2(string method, string uri) + { + return $"{method}:{uri}"; + } + + /// + /// Creates the A2 value for Digest authentication with an entity. + /// + /// The HTTP method. + /// The URI. + /// The entity. + /// The A2 value. + private static string CreateA2(string method, string uri, string entity) + { + return $"{method}:{uri}:{Hash(entity)}"; + } + + /// + /// Computes the MD5 hash of the given value. + /// + /// The value to hash. + /// The MD5 hash. + private static string Hash(string value) + { + var source = Encoding.UTF8.GetBytes(value); + var md5 = MD5.Create(); + var hashed = md5.ComputeHash(source); + + var result = new StringBuilder(64); + foreach (var currentByte in hashed) + { + result.Append(currentByte.ToString("x2")); + } + + return result.ToString(); + } + + /// + /// Initializes the authentication as Digest. + /// + private void InitAsDigest() + { + var qops = Parameters["qop"]; + if (qops != null) + { + if (qops.Split(',').Contains(qop => qop.Trim().ToLower() == "auth")) + { + Parameters["qop"] = "auth"; + Parameters["cnonce"] = CreateNonceValue(); + Parameters["nc"] = string.Format("{0:x8}", ++_nonceCount); + } + else + { + Parameters["qop"] = null; + } + } + + Parameters["method"] = "GET"; + Parameters["response"] = CreateRequestDigest(Parameters); } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Authentication/NetworkCredential.cs b/EonaCat.Network/System/Sockets/Web/Core/Authentication/NetworkCredential.cs index ded296b..ce6e467 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Authentication/NetworkCredential.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Authentication/NetworkCredential.cs @@ -10,8 +10,8 @@ namespace EonaCat.Network /// public class NetworkCredential { - private string _domain; private static readonly string[] _noRoles; + private string _domain; private string _password; private string[] _roles; diff --git a/EonaCat.Network/System/Sockets/Web/Core/Chunks/ChunkStream.cs b/EonaCat.Network/System/Sockets/Web/Core/Chunks/ChunkStream.cs index a7a9e4a..4fa478b 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Chunks/ChunkStream.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Chunks/ChunkStream.cs @@ -15,11 +15,11 @@ namespace EonaCat.Network /// internal class ChunkStream { + private readonly List _chunks; + private readonly StringBuilder _saved; private int _chunkRead; private int _chunkSize; - private readonly List _chunks; private bool _foundSPCode; - private readonly StringBuilder _saved; private bool _gotChunck; private InputChunkState _state; private int _trailerState; @@ -49,11 +49,6 @@ namespace EonaCat.Network Write(buffer, offset, count); } - /// - /// Gets the web headers associated with the chunk stream. - /// - internal WebHeaderCollection Headers { get; } - /// /// Gets the number of bytes left in the current chunk. /// @@ -64,6 +59,78 @@ namespace EonaCat.Network /// public bool WantMore => _state != InputChunkState.End; + /// + /// Gets the web headers associated with the chunk stream. + /// + internal WebHeaderCollection Headers { get; } + /// + /// Reads a specified amount of data from the chunk stream. + /// + /// The destination buffer. + /// The zero-based byte offset in the buffer at which to begin storing the data. + /// The maximum number of bytes to read. + /// The total number of bytes read into the buffer. + public int Read(byte[] buffer, int offset, int count) + { + if (count <= 0) + { + return 0; + } + + return read(buffer, offset, count); + } + + /// + /// Writes a specified amount of data to the chunk stream. + /// + /// The byte array containing data to be written to the chunk stream. + /// The offset in the buffer at which to begin writing. + /// The number of bytes to write to the chunk stream. + public void Write(byte[] buffer, int offset, int count) + { + if (count <= 0) + { + return; + } + + Write(buffer, ref offset, offset + count); + } + + /// + /// Resets the internal buffer and state of the chunk stream. + /// + internal void ResetBuffer() + { + _chunkRead = 0; + _chunkSize = -1; + _chunks.Clear(); + } + + /// + /// Writes a specified amount of data to the chunk stream and reads it back. + /// + /// The byte array containing data to be written to the chunk stream. + /// The offset in the buffer at which to begin writing. + /// The number of bytes to write to the chunk stream. + /// The number of bytes to read back from the chunk stream. + /// The number of bytes read from the chunk stream. + internal int WriteAndReadBack(byte[] buffer, int offset, int writeCount, int readCount) + { + Write(buffer, offset, writeCount); + return Read(buffer, offset, readCount); + } + + private static string RemoveChunkExtension(string value) + { + var index = value.IndexOf(';'); + return index > -1 ? value.Substring(0, index) : value; + } + + private static void ThrowProtocolViolation(string message) + { + throw new WebException($"EonaCat Network: {message}", null, WebExceptionStatus.ServerProtocolViolation, null); + } + private int read(byte[] buffer, int offset, int count) { var nread = 0; @@ -92,13 +159,6 @@ namespace EonaCat.Network return nread; } - - private static string RemoveChunkExtension(string value) - { - var index = value.IndexOf(';'); - return index > -1 ? value.Substring(0, index) : value; - } - private InputChunkState SeekCrLf(byte[] buffer, ref int offset, int length) { if (!_gotChunck) @@ -256,12 +316,6 @@ namespace EonaCat.Network return InputChunkState.End; } - - private static void ThrowProtocolViolation(string message) - { - throw new WebException($"EonaCat Network: {message}", null, WebExceptionStatus.ServerProtocolViolation, null); - } - private void Write(byte[] buffer, ref int offset, int length) { if (_state == InputChunkState.End) @@ -337,62 +391,5 @@ namespace EonaCat.Network return _chunkRead == _chunkSize ? InputChunkState.DataEnded : InputChunkState.Data; } - - /// - /// Resets the internal buffer and state of the chunk stream. - /// - internal void ResetBuffer() - { - _chunkRead = 0; - _chunkSize = -1; - _chunks.Clear(); - } - - /// - /// Writes a specified amount of data to the chunk stream and reads it back. - /// - /// The byte array containing data to be written to the chunk stream. - /// The offset in the buffer at which to begin writing. - /// The number of bytes to write to the chunk stream. - /// The number of bytes to read back from the chunk stream. - /// The number of bytes read from the chunk stream. - internal int WriteAndReadBack(byte[] buffer, int offset, int writeCount, int readCount) - { - Write(buffer, offset, writeCount); - return Read(buffer, offset, readCount); - } - - /// - /// Reads a specified amount of data from the chunk stream. - /// - /// The destination buffer. - /// The zero-based byte offset in the buffer at which to begin storing the data. - /// The maximum number of bytes to read. - /// The total number of bytes read into the buffer. - public int Read(byte[] buffer, int offset, int count) - { - if (count <= 0) - { - return 0; - } - - return read(buffer, offset, count); - } - - /// - /// Writes a specified amount of data to the chunk stream. - /// - /// The byte array containing data to be written to the chunk stream. - /// The offset in the buffer at which to begin writing. - /// The number of bytes to write to the chunk stream. - public void Write(byte[] buffer, int offset, int count) - { - if (count <= 0) - { - return; - } - - Write(buffer, ref offset, offset + count); - } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Chunks/ChunkedRequestStream.cs b/EonaCat.Network/System/Sockets/Web/Core/Chunks/ChunkedRequestStream.cs index e76eb43..d62b77d 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Chunks/ChunkedRequestStream.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Chunks/ChunkedRequestStream.cs @@ -36,37 +36,6 @@ namespace EonaCat.Network /// internal ChunkStream Decoder { get; set; } - private void OnRead(IAsyncResult asyncResult) - { - var readBufferState = (ReadBufferState)asyncResult.AsyncState; - var result = readBufferState.AsyncResult; - try - { - var nread = base.EndRead(asyncResult); - Decoder.Write(result.Buffer, result.Offset, nread); - nread = Decoder.Read(readBufferState.Buffer, readBufferState.Offset, readBufferState.Count); - readBufferState.Offset += nread; - readBufferState.Count -= nread; - if (readBufferState.Count == 0 || !Decoder.WantMore || nread == 0) - { - _noMoreData = !Decoder.WantMore && nread == 0; - result.Count = readBufferState.InitialCount - readBufferState.Count; - result.Complete(); - - return; - } - - result.Offset = 0; - result.Count = Math.Min(_bufferLength, Decoder.ChunkLeft + 6); - base.BeginRead(result.Buffer, result.Offset, result.Count, OnRead, readBufferState); - } - catch (Exception ex) - { - _context.Connection.SendError(ex.Message, 400); - result.Complete(ex); - } - } - /// /// Begins an asynchronous read operation from the stream. /// @@ -204,5 +173,36 @@ namespace EonaCat.Network var result = BeginRead(buffer, offset, count, null, null); return EndRead(result); } + + private void OnRead(IAsyncResult asyncResult) + { + var readBufferState = (ReadBufferState)asyncResult.AsyncState; + var result = readBufferState.AsyncResult; + try + { + var nread = base.EndRead(asyncResult); + Decoder.Write(result.Buffer, result.Offset, nread); + nread = Decoder.Read(readBufferState.Buffer, readBufferState.Offset, readBufferState.Count); + readBufferState.Offset += nread; + readBufferState.Count -= nread; + if (readBufferState.Count == 0 || !Decoder.WantMore || nread == 0) + { + _noMoreData = !Decoder.WantMore && nread == 0; + result.Count = readBufferState.InitialCount - readBufferState.Count; + result.Complete(); + + return; + } + + result.Offset = 0; + result.Count = Math.Min(_bufferLength, Decoder.ChunkLeft + 6); + base.BeginRead(result.Buffer, result.Offset, result.Count, OnRead, readBufferState); + } + catch (Exception ex) + { + _context.Connection.SendError(ex.Message, 400); + result.Complete(ex); + } + } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Collections/WebHeaderCollection.cs b/EonaCat.Network/System/Sockets/Web/Core/Collections/WebHeaderCollection.cs index 16416dd..0e0ca9b 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Collections/WebHeaderCollection.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Collections/WebHeaderCollection.cs @@ -90,6 +90,10 @@ namespace EonaCat.Network }; } + public WebHeaderCollection() + { + } + /// /// Initializes a new instance of the WebHeaderCollection class with the specified parameters. /// @@ -132,18 +136,10 @@ namespace EonaCat.Network throw new ArgumentException(ex.Message, nameof(serializationInfo), ex); } } - - - public WebHeaderCollection() - { - } - - internal HttpHeaderType State => _state; - public override string[] AllKeys => base.AllKeys; - public override int Count => base.Count; - + public override KeysCollection Keys => base.Keys; + internal HttpHeaderType State => _state; public string this[HttpRequestHeader header] { get @@ -169,272 +165,14 @@ namespace EonaCat.Network Add(header, value); } } - - public override KeysCollection Keys => base.Keys; - - private void add(string name, string value, bool ignoreRestricted) + public static bool IsRestricted(string headerName) { - var act = ignoreRestricted - ? (Action)addWithoutCheckingNameAndRestricted - : addWithoutCheckingName; - - DoWithCheckingState(act, CheckName(name), value, true); + return isRestricted(CheckName(headerName), false); } - private void addWithoutCheckingName(string name, string value) + public static bool IsRestricted(string headerName, bool response) { - DoWithoutCheckingName(base.Add, name, value); - } - - private void addWithoutCheckingNameAndRestricted(string name, string value) - { - base.Add(name, CheckValue(value)); - } - - private static int checkColonSeparated(string header) - { - var idx = header.IndexOf(':'); - if (idx == -1) - { - throw new ArgumentException("No colon could be found.", nameof(header)); - } - - return idx; - } - - private static HttpHeaderType CheckHeaderType(string name) - { - var info = GetHeaderInfo(name); - return info == null - ? HttpHeaderType.Unspecified - : info.IsRequest && !info.IsResponse - ? HttpHeaderType.Request - : !info.IsRequest && info.IsResponse - ? HttpHeaderType.Response - : HttpHeaderType.Unspecified; - } - - private static string CheckName(string name) - { - if (name == null || name.Length == 0) - { - throw new ArgumentNullException(nameof(name)); - } - - name = name.Trim(); - if (!IsHeaderName(name)) - { - throw new ArgumentException("Contains invalid characters.", nameof(name)); - } - - return name; - } - - private void CheckRestricted(string name) - { - if (!_internallyUsed && isRestricted(name, true)) - { - throw new ArgumentException("This header must be modified with the appropiate property."); - } - } - - private void CheckState(bool response) - { - if (_state == HttpHeaderType.Unspecified) - { - return; - } - - if (response && _state == HttpHeaderType.Request) - { - throw new InvalidOperationException( - "This collection has already been used to store the request headers."); - } - - if (!response && _state == HttpHeaderType.Response) - { - throw new InvalidOperationException( - "This collection has already been used to store the response headers."); - } - } - - private static string CheckValue(string value) - { - if (value == null || value.Length == 0) - { - return string.Empty; - } - - value = value.Trim(); - if (value.Length > 65535) - { - throw new ArgumentOutOfRangeException(nameof(value), "Greater than 65,535 characters."); - } - - if (!IsHeaderValue(value)) - { - throw new ArgumentException("Contains invalid characters.", nameof(value)); - } - - return value; - } - - private static string Convert(string key) - { - return _headers.TryGetValue(key, out HttpHeaderInfo info) ? info.Name : string.Empty; - } - - private void DoWithCheckingState( - Action action, string name, string value, bool setState) - { - var type = CheckHeaderType(name); - if (type == HttpHeaderType.Request) - { - DoWithCheckingState(action, name, value, false, setState); - } - else if (type == HttpHeaderType.Response) - { - DoWithCheckingState(action, name, value, true, setState); - } - else - { - action(name, value); - } - } - - private void DoWithCheckingState( - Action action, string name, string value, bool response, bool setState) - { - CheckState(response); - action(name, value); - if (setState && _state == HttpHeaderType.Unspecified) - { - _state = response ? HttpHeaderType.Response : HttpHeaderType.Request; - } - } - - private void DoWithoutCheckingName(Action action, string name, string value) - { - CheckRestricted(name); - action(name, CheckValue(value)); - } - - private static HttpHeaderInfo GetHeaderInfo(string name) - { - foreach (var info in _headers.Values) - { - if (info.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) - { - return info; - } - } - - return null; - } - - private static bool isRestricted(string name, bool response) - { - var info = GetHeaderInfo(name); - return info != null && info.IsRestricted(response); - } - - private void removeWithoutCheckingName(string name, string unuse) - { - CheckRestricted(name); - base.Remove(name); - } - - private void setWithoutCheckingName(string name, string value) - { - DoWithoutCheckingName(base.Set, name, value); - } - - /// - /// Converts the specified HttpRequestHeader to a string. - /// - /// The HttpRequestHeader to convert. - /// A string representing the converted HttpRequestHeader. - internal static string Convert(HttpRequestHeader header) - { - return Convert(header.ToString()); - } - - internal static string Convert(HttpResponseHeader header) - { - return Convert(header.ToString()); - } - - internal void InternalRemove(string name) - { - base.Remove(name); - } - - internal void InternalSet(string header, bool response) - { - var pos = checkColonSeparated(header); - InternalSet(header.Substring(0, pos), header.Substring(pos + 1), response); - } - - internal void InternalSet(string name, string value, bool response) - { - value = CheckValue(value); - if (IsMultiValue(name, response)) - { - base.Add(name, value); - } - else - { - base.Set(name, value); - } - } - - internal static bool IsHeaderName(string name) - { - return name != null && name.Length > 0 && name.IsToken(); - } - - internal static bool IsHeaderValue(string value) - { - return value.IsText(); - } - - internal static bool IsMultiValue(string headerName, bool response) - { - if (headerName == null || headerName.Length == 0) - { - return false; - } - - var info = GetHeaderInfo(headerName); - return info != null && info.IsMultiValue(response); - } - - internal string ToStringMultiValue(bool response) - { - var buff = new StringBuilder(); - Count.Times( - i => - { - var key = GetKey(i); - if (IsMultiValue(key, response)) - { - foreach (var val in GetValues(i)) - { - buff.AppendFormat($"{key}: {val}\r\n"); - } - } - else - { - buff.AppendFormat($"{key}: {Get(i)}\r\n"); - } - }); - - return buff.Append("\r\n").ToString(); - } - - protected void AddWithoutValidate(string headerName, string headerValue) - { - add(headerName, headerValue, true); + return isRestricted(CheckName(headerName), response); } public void Add(string header) @@ -489,18 +227,6 @@ namespace EonaCat.Network return base.GetKey(index); } - public override string[] GetValues(int index) - { - var vals = base.GetValues(index); - return vals != null && vals.Length > 0 ? vals : null; - } - - public override string[] GetValues(string header) - { - var vals = base.GetValues(header); - return vals != null && vals.Length > 0 ? vals : null; - } - [SecurityPermission( SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)] public override void GetObjectData( @@ -524,14 +250,26 @@ namespace EonaCat.Network }); } - public static bool IsRestricted(string headerName) + [SecurityPermission( + SecurityAction.LinkDemand, + Flags = SecurityPermissionFlag.SerializationFormatter, + SerializationFormatter = true)] + void ISerializable.GetObjectData( + SerializationInfo serializationInfo, StreamingContext streamingContext) { - return isRestricted(CheckName(headerName), false); + GetObjectData(serializationInfo, streamingContext); } - public static bool IsRestricted(string headerName, bool response) + public override string[] GetValues(int index) { - return isRestricted(CheckName(headerName), response); + var vals = base.GetValues(index); + return vals != null && vals.Length > 0 ? vals : null; + } + + public override string[] GetValues(string header) + { + var vals = base.GetValues(header); + return vals != null && vals.Length > 0 ? vals : null; } public override void OnDeserialization(object sender) @@ -589,14 +327,266 @@ namespace EonaCat.Network return buff.Append("\r\n").ToString(); } - [SecurityPermission( - SecurityAction.LinkDemand, - Flags = SecurityPermissionFlag.SerializationFormatter, - SerializationFormatter = true)] - void ISerializable.GetObjectData( - SerializationInfo serializationInfo, StreamingContext streamingContext) + /// + /// Converts the specified HttpRequestHeader to a string. + /// + /// The HttpRequestHeader to convert. + /// A string representing the converted HttpRequestHeader. + internal static string Convert(HttpRequestHeader header) { - GetObjectData(serializationInfo, streamingContext); + return Convert(header.ToString()); + } + + internal static string Convert(HttpResponseHeader header) + { + return Convert(header.ToString()); + } + + internal static bool IsHeaderName(string name) + { + return name != null && name.Length > 0 && name.IsToken(); + } + + internal static bool IsHeaderValue(string value) + { + return value.IsText(); + } + + internal static bool IsMultiValue(string headerName, bool response) + { + if (headerName == null || headerName.Length == 0) + { + return false; + } + + var info = GetHeaderInfo(headerName); + return info != null && info.IsMultiValue(response); + } + + internal void InternalRemove(string name) + { + base.Remove(name); + } + + internal void InternalSet(string header, bool response) + { + var pos = checkColonSeparated(header); + InternalSet(header.Substring(0, pos), header.Substring(pos + 1), response); + } + + internal void InternalSet(string name, string value, bool response) + { + value = CheckValue(value); + if (IsMultiValue(name, response)) + { + base.Add(name, value); + } + else + { + base.Set(name, value); + } + } + + internal string ToStringMultiValue(bool response) + { + var buff = new StringBuilder(); + Count.Times( + i => + { + var key = GetKey(i); + if (IsMultiValue(key, response)) + { + foreach (var val in GetValues(i)) + { + buff.AppendFormat($"{key}: {val}\r\n"); + } + } + else + { + buff.AppendFormat($"{key}: {Get(i)}\r\n"); + } + }); + + return buff.Append("\r\n").ToString(); + } + + protected void AddWithoutValidate(string headerName, string headerValue) + { + add(headerName, headerValue, true); + } + + private static int checkColonSeparated(string header) + { + var idx = header.IndexOf(':'); + if (idx == -1) + { + throw new ArgumentException("No colon could be found.", nameof(header)); + } + + return idx; + } + + private static HttpHeaderType CheckHeaderType(string name) + { + var info = GetHeaderInfo(name); + return info == null + ? HttpHeaderType.Unspecified + : info.IsRequest && !info.IsResponse + ? HttpHeaderType.Request + : !info.IsRequest && info.IsResponse + ? HttpHeaderType.Response + : HttpHeaderType.Unspecified; + } + + private static string CheckName(string name) + { + if (name == null || name.Length == 0) + { + throw new ArgumentNullException(nameof(name)); + } + + name = name.Trim(); + if (!IsHeaderName(name)) + { + throw new ArgumentException("Contains invalid characters.", nameof(name)); + } + + return name; + } + + private static string CheckValue(string value) + { + if (value == null || value.Length == 0) + { + return string.Empty; + } + + value = value.Trim(); + if (value.Length > 65535) + { + throw new ArgumentOutOfRangeException(nameof(value), "Greater than 65,535 characters."); + } + + if (!IsHeaderValue(value)) + { + throw new ArgumentException("Contains invalid characters.", nameof(value)); + } + + return value; + } + + private static string Convert(string key) + { + return _headers.TryGetValue(key, out HttpHeaderInfo info) ? info.Name : string.Empty; + } + + private static HttpHeaderInfo GetHeaderInfo(string name) + { + foreach (var info in _headers.Values) + { + if (info.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) + { + return info; + } + } + + return null; + } + + private static bool isRestricted(string name, bool response) + { + var info = GetHeaderInfo(name); + return info != null && info.IsRestricted(response); + } + + private void add(string name, string value, bool ignoreRestricted) + { + var act = ignoreRestricted + ? (Action)addWithoutCheckingNameAndRestricted + : addWithoutCheckingName; + + DoWithCheckingState(act, CheckName(name), value, true); + } + + private void addWithoutCheckingName(string name, string value) + { + DoWithoutCheckingName(base.Add, name, value); + } + + private void addWithoutCheckingNameAndRestricted(string name, string value) + { + base.Add(name, CheckValue(value)); + } + private void CheckRestricted(string name) + { + if (!_internallyUsed && isRestricted(name, true)) + { + throw new ArgumentException("This header must be modified with the appropiate property."); + } + } + + private void CheckState(bool response) + { + if (_state == HttpHeaderType.Unspecified) + { + return; + } + + if (response && _state == HttpHeaderType.Request) + { + throw new InvalidOperationException( + "This collection has already been used to store the request headers."); + } + + if (!response && _state == HttpHeaderType.Response) + { + throw new InvalidOperationException( + "This collection has already been used to store the response headers."); + } + } + private void DoWithCheckingState( + Action action, string name, string value, bool setState) + { + var type = CheckHeaderType(name); + if (type == HttpHeaderType.Request) + { + DoWithCheckingState(action, name, value, false, setState); + } + else if (type == HttpHeaderType.Response) + { + DoWithCheckingState(action, name, value, true, setState); + } + else + { + action(name, value); + } + } + + private void DoWithCheckingState( + Action action, string name, string value, bool response, bool setState) + { + CheckState(response); + action(name, value); + if (setState && _state == HttpHeaderType.Unspecified) + { + _state = response ? HttpHeaderType.Response : HttpHeaderType.Request; + } + } + + private void DoWithoutCheckingName(Action action, string name, string value) + { + CheckRestricted(name); + action(name, CheckValue(value)); + } + private void removeWithoutCheckingName(string name, string unuse) + { + CheckRestricted(name); + base.Remove(name); + } + + private void setWithoutCheckingName(string name, string value) + { + DoWithoutCheckingName(base.Set, name, value); } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Context/HttpListenerWSContext.cs b/EonaCat.Network/System/Sockets/Web/Core/Context/HttpListenerWSContext.cs index 32a851d..1375c36 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Context/HttpListenerWSContext.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Context/HttpListenerWSContext.cs @@ -33,11 +33,6 @@ namespace EonaCat.Network _websocket = new WSClient(this, protocol); } - /// - /// Gets the stream of the underlying TCP connection. - /// - internal Stream Stream => _context.Connection.Stream; - public override CookieCollection CookieCollection => _context.Request.Cookies; public override NameValueCollection Headers => _context.Request.Headers; @@ -85,6 +80,16 @@ namespace EonaCat.Network public override WSClient WebSocket => _websocket; + /// + /// Gets the stream of the underlying TCP connection. + /// + internal Stream Stream => _context.Connection.Stream; + /// + public override string ToString() + { + return _context.Request.ToString(); + } + /// /// Closes the WebSocket connection. /// @@ -101,11 +106,5 @@ namespace EonaCat.Network { _context.Response.Close(code); } - - /// - public override string ToString() - { - return _context.Request.ToString(); - } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Context/TcpListenerWSContext.cs b/EonaCat.Network/System/Sockets/Web/Core/Context/TcpListenerWSContext.cs index 30fee85..e359cc9 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Context/TcpListenerWSContext.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Context/TcpListenerWSContext.cs @@ -21,15 +21,14 @@ namespace EonaCat.Network /// internal class TcpListenerWSContext : WSContext { - private CookieCollection _cookies; - private NameValueCollection _queryString; - private WebRequest _request; private readonly bool _secure; private readonly TcpClient _tcpClient; private readonly Uri _uri; - private IPrincipal _user; private readonly WSClient _websocket; - + private CookieCollection _cookies; + private NameValueCollection _queryString; + private WebRequest _request; + private IPrincipal _user; /// /// Initializes a new instance of the class. /// @@ -76,11 +75,6 @@ namespace EonaCat.Network _websocket = new WSClient(this, protocol); } - /// - /// Gets the stream of the underlying TCP connection. - /// - internal Stream Stream { get; } - public override CookieCollection CookieCollection => _cookies ??= _request.Cookies; public override NameValueCollection Headers => _request.Headers; @@ -98,10 +92,10 @@ namespace EonaCat.Network public override string Origin => _request.Headers["Origin"]; public override NameValueCollection QueryString => _queryString ??= - HttpUtility.InternalParseQueryString( - _uri?.Query, Encoding.UTF8 - ) -; + HttpUtility.InternalParseQueryString( + _uri?.Query, Encoding.UTF8 + ) + ; public override Uri RequestUri => _uri; @@ -132,6 +126,16 @@ namespace EonaCat.Network public override WSClient WebSocket => _websocket; + /// + /// Gets the stream of the underlying TCP connection. + /// + internal Stream Stream { get; } + /// + public override string ToString() + { + return _request.ToString(); + } + /// /// Authenticates the WebSocket connection based on the specified authentication scheme. /// @@ -220,11 +224,5 @@ namespace EonaCat.Network Stream.Write(buff, 0, buff.Length); _request = WebRequest.Read(Stream, 15000); } - - /// - public override string ToString() - { - return _request.ToString(); - } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Cookies/Cookie.cs b/EonaCat.Network/System/Sockets/Web/Core/Cookies/Cookie.cs index f0557d5..0a513be 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Cookies/Cookie.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Cookies/Cookie.cs @@ -13,6 +13,9 @@ namespace EonaCat.Network [Serializable] public sealed class Cookie { + private static readonly char[] _reservedCharsForName; + private static readonly char[] _reservedCharsForValue; + private readonly DateTime _timestamp; private string _comment; private Uri _commentUri; private bool _discard; @@ -23,10 +26,7 @@ namespace EonaCat.Network private string _path; private string _port; private int[] _ports; - private static readonly char[] _reservedCharsForName; - private static readonly char[] _reservedCharsForValue; private bool _secure; - private readonly DateTime _timestamp; private string _value; private int _version; @@ -90,33 +90,6 @@ namespace EonaCat.Network Domain = domain; } - internal bool ExactDomain - { - get; set; - } - - internal int MaxAge - { - get - { - if (_expires == DateTime.MinValue) - { - return 0; - } - - var expires = _expires.Kind != DateTimeKind.Local - ? _expires.ToLocalTime() - : _expires; - - var span = expires - DateTime.Now; - return span > TimeSpan.Zero - ? (int)span.TotalSeconds - : 0; - } - } - - internal int[] Ports => _ports; - public string Comment { get @@ -333,6 +306,118 @@ namespace EonaCat.Network } } + internal bool ExactDomain + { + get; set; + } + + internal int MaxAge + { + get + { + if (_expires == DateTime.MinValue) + { + return 0; + } + + var expires = _expires.Kind != DateTimeKind.Local + ? _expires.ToLocalTime() + : _expires; + + var span = expires - DateTime.Now; + return span > TimeSpan.Zero + ? (int)span.TotalSeconds + : 0; + } + } + + internal int[] Ports => _ports; + /// + public override bool Equals(object comparand) + { + return comparand is Cookie cookie && + _name.Equals(cookie.Name, StringComparison.InvariantCultureIgnoreCase) && + _value.Equals(cookie.Value, StringComparison.InvariantCulture) && + _path.Equals(cookie.Path, StringComparison.InvariantCulture) && + _domain.Equals(cookie.Domain, StringComparison.InvariantCultureIgnoreCase) && + _version == cookie.Version; + } + + /// + public override int GetHashCode() + { + return hash( + StringComparer.InvariantCultureIgnoreCase.GetHashCode(_name), + _value.GetHashCode(), + _path.GetHashCode(), + StringComparer.InvariantCultureIgnoreCase.GetHashCode(_domain), + _version); + } + + /// + public override string ToString() + { + return ToRequestString(null); + } + + // From client to server + internal string ToRequestString(Uri uri) + { + if (_name.Length == 0) + { + return string.Empty; + } + + if (_version == 0) + { + return string.Format("{0}={1}", _name, _value); + } + + var output = new StringBuilder(64); + output.AppendFormat("$Version={0}; {1}={2}", _version, _name, _value); + + if (!_path.IsNullOrEmpty()) + { + output.AppendFormat("; $Path={0}", _path); + } + else if (uri != null) + { + output.AppendFormat("; $Path={0}", uri.GetAbsolutePath()); + } + else + { + output.Append("; $Path=/"); + } + + var appendDomain = uri == null || uri.Host != _domain; + if (appendDomain && !_domain.IsNullOrEmpty()) + { + output.AppendFormat("; $Domain={0}", _domain); + } + + if (!_port.IsNullOrEmpty()) + { + if (_port == "\"\"") + { + output.Append("; $Port"); + } + else + { + output.AppendFormat("; $Port={0}", _port); + } + } + + return output.ToString(); + } + + // From server to client + internal string ToResponseString() + { + return _name.Length > 0 + ? (_version == 0 ? toResponseStringVersion0() : toResponseStringVersion1()) + : string.Empty; + } + private static bool canSetName(string name, out string message) { if (name.IsNullOrEmpty()) @@ -378,6 +463,36 @@ namespace EonaCat.Network (m << 20 | m >> 12); } + private static bool tryCreatePorts(string value, out int[] result, out string parseError) + { + var ports = value.Trim('"').Split(','); + var len = ports.Length; + var res = new int[len]; + for (var i = 0; i < len; i++) + { + res[i] = int.MinValue; + + var port = ports[i].Trim(); + if (port.Length == 0) + { + continue; + } + + if (!int.TryParse(port, out res[i])) + { + result = new int[0]; + parseError = port; + + return false; + } + } + + result = res; + parseError = string.Empty; + + return true; + } + private string toResponseStringVersion0() { var output = new StringBuilder(64); @@ -470,121 +585,5 @@ namespace EonaCat.Network return output.ToString(); } - - private static bool tryCreatePorts(string value, out int[] result, out string parseError) - { - var ports = value.Trim('"').Split(','); - var len = ports.Length; - var res = new int[len]; - for (var i = 0; i < len; i++) - { - res[i] = int.MinValue; - - var port = ports[i].Trim(); - if (port.Length == 0) - { - continue; - } - - if (!int.TryParse(port, out res[i])) - { - result = new int[0]; - parseError = port; - - return false; - } - } - - result = res; - parseError = string.Empty; - - return true; - } - - // From client to server - internal string ToRequestString(Uri uri) - { - if (_name.Length == 0) - { - return string.Empty; - } - - if (_version == 0) - { - return string.Format("{0}={1}", _name, _value); - } - - var output = new StringBuilder(64); - output.AppendFormat("$Version={0}; {1}={2}", _version, _name, _value); - - if (!_path.IsNullOrEmpty()) - { - output.AppendFormat("; $Path={0}", _path); - } - else if (uri != null) - { - output.AppendFormat("; $Path={0}", uri.GetAbsolutePath()); - } - else - { - output.Append("; $Path=/"); - } - - var appendDomain = uri == null || uri.Host != _domain; - if (appendDomain && !_domain.IsNullOrEmpty()) - { - output.AppendFormat("; $Domain={0}", _domain); - } - - if (!_port.IsNullOrEmpty()) - { - if (_port == "\"\"") - { - output.Append("; $Port"); - } - else - { - output.AppendFormat("; $Port={0}", _port); - } - } - - return output.ToString(); - } - - // From server to client - internal string ToResponseString() - { - return _name.Length > 0 - ? (_version == 0 ? toResponseStringVersion0() : toResponseStringVersion1()) - : string.Empty; - } - - /// - public override bool Equals(object comparand) - { - return comparand is Cookie cookie && - _name.Equals(cookie.Name, StringComparison.InvariantCultureIgnoreCase) && - _value.Equals(cookie.Value, StringComparison.InvariantCulture) && - _path.Equals(cookie.Path, StringComparison.InvariantCulture) && - _domain.Equals(cookie.Domain, StringComparison.InvariantCultureIgnoreCase) && - _version == cookie.Version; - } - - /// - public override int GetHashCode() - { - return hash( - StringComparer.InvariantCultureIgnoreCase.GetHashCode(_name), - _value.GetHashCode(), - _path.GetHashCode(), - StringComparer.InvariantCultureIgnoreCase.GetHashCode(_domain), - _version); - } - - /// - public override string ToString() - { - return ToRequestString(null); - } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Cookies/CookieCollection.cs b/EonaCat.Network/System/Sockets/Web/Core/Cookies/CookieCollection.cs index 2d85554..6b5be1f 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Cookies/CookieCollection.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Cookies/CookieCollection.cs @@ -27,22 +27,6 @@ namespace EonaCat.Network } - internal IList List => _list; - - internal IEnumerable Sorted - { - get - { - var list = new List(_list); - if (list.Count > 1) - { - list.Sort(compareCookieWithinSorted); - } - - return list; - } - } - /// /// Gets the number of cookies in the collection. /// @@ -58,6 +42,26 @@ namespace EonaCat.Network /// public bool IsSynchronized => false; + /// + /// Gets an object that can be used to synchronize access to the collection. + /// + public object SyncRoot => _sync ??= ((ICollection)_list).SyncRoot; + + internal IList List => _list; + + internal IEnumerable Sorted + { + get + { + var list = new List(_list); + if (list.Count > 1) + { + list.Sort(compareCookieWithinSorted); + } + + return list; + } + } /// /// Gets or sets the cookie at the specified index. /// @@ -101,11 +105,164 @@ namespace EonaCat.Network return null; } } + /// + /// Adds the specified cookie to the collection, updating it if it already exists. + /// + /// The cookie to add or update. + public void Add(Cookie cookie) + { + if (cookie == null) + { + throw new ArgumentNullException(nameof(cookie)); + } + + var pos = searchCookie(cookie); + if (pos == -1) + { + _list.Add(cookie); + return; + } + + _list[pos] = cookie; + } /// - /// Gets an object that can be used to synchronize access to the collection. + /// Adds the cookies from the specified to this collection, updating existing cookies. /// - public object SyncRoot => _sync ??= ((ICollection)_list).SyncRoot; + /// The to add or update from. + public void Add(CookieCollection cookies) + { + if (cookies == null) + { + throw new ArgumentNullException(nameof(cookies)); + } + + foreach (Cookie cookie in cookies) + { + Add(cookie); + } + } + + /// + /// Copies the cookies in the collection to the specified array, starting at the specified index. + /// + /// The destination array. + /// The index in the destination array at which copying begins. + public void CopyTo(Array array, int index) + { + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + if (index < 0) + { + throw new ArgumentOutOfRangeException(nameof(index), "Less than zero."); + } + + if (array.Rank > 1) + { + throw new ArgumentException("Multidimensional.", nameof(array)); + } + + if (array.Length - index < _list.Count) + { + throw new ArgumentException( + "The number of elements in this collection is greater than the available space of the destination array."); + } + + if (!array.GetType().GetElementType().IsAssignableFrom(typeof(Cookie))) + { + throw new InvalidCastException( + "The elements in this collection cannot be cast automatically to the type of the destination array."); + } ((IList)_list).CopyTo(array, index); + } + + /// + /// Copies the cookies in the collection to the specified array, starting at the specified index. + /// + /// The destination array. + /// The index in the destination array at which copying begins. + public void CopyTo(Cookie[] array, int index) + { + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + if (index < 0) + { + throw new ArgumentOutOfRangeException(nameof(index), "Less than zero."); + } + + if (array.Length - index < _list.Count) + { + throw new ArgumentException( + "The number of elements in this collection is greater than the available space of the destination array."); + } + + _list.CopyTo(array, index); + } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// An enumerator for the collection. + public IEnumerator GetEnumerator() + { + return _list.GetEnumerator(); + } + + /// + /// Parses the specified cookie string, creating a . + /// + /// The cookie string to parse. + /// True if parsing a response header; otherwise, false. + /// A instance representing the parsed cookies. + internal static CookieCollection Parse(string value, bool response) + { + return response + ? parseResponse(value) + : parseRequest(value); + } + + internal void SetOrRemove(Cookie cookie) + { + var pos = searchCookie(cookie); + if (pos == -1) + { + if (!cookie.Expired) + { + _list.Add(cookie); + } + + return; + } + + if (!cookie.Expired) + { + _list[pos] = cookie; + return; + } + + _list.RemoveAt(pos); + } + + internal void SetOrRemove(CookieCollection cookies) + { + foreach (Cookie cookie in cookies) + { + SetOrRemove(cookie); + } + } + + internal void Sort() + { + if (_list.Count > 1) + { + _list.Sort(compareCookieWithinSort); + } + } private static int compareCookieWithinSort(Cookie x, Cookie y) { @@ -357,6 +514,11 @@ namespace EonaCat.Network return cookies; } + private static string[] splitCookieHeaderValue(string value) + { + return new List(value.SplitHeaderValue(',', ';')).ToArray(); + } + private int searchCookie(Cookie cookie) { var name = cookie.Name; @@ -378,169 +540,5 @@ namespace EonaCat.Network return -1; } - - private static string[] splitCookieHeaderValue(string value) - { - return new List(value.SplitHeaderValue(',', ';')).ToArray(); - } - - /// - /// Parses the specified cookie string, creating a . - /// - /// The cookie string to parse. - /// True if parsing a response header; otherwise, false. - /// A instance representing the parsed cookies. - internal static CookieCollection Parse(string value, bool response) - { - return response - ? parseResponse(value) - : parseRequest(value); - } - - internal void SetOrRemove(Cookie cookie) - { - var pos = searchCookie(cookie); - if (pos == -1) - { - if (!cookie.Expired) - { - _list.Add(cookie); - } - - return; - } - - if (!cookie.Expired) - { - _list[pos] = cookie; - return; - } - - _list.RemoveAt(pos); - } - - internal void SetOrRemove(CookieCollection cookies) - { - foreach (Cookie cookie in cookies) - { - SetOrRemove(cookie); - } - } - - internal void Sort() - { - if (_list.Count > 1) - { - _list.Sort(compareCookieWithinSort); - } - } - - /// - /// Adds the specified cookie to the collection, updating it if it already exists. - /// - /// The cookie to add or update. - public void Add(Cookie cookie) - { - if (cookie == null) - { - throw new ArgumentNullException(nameof(cookie)); - } - - var pos = searchCookie(cookie); - if (pos == -1) - { - _list.Add(cookie); - return; - } - - _list[pos] = cookie; - } - - /// - /// Adds the cookies from the specified to this collection, updating existing cookies. - /// - /// The to add or update from. - public void Add(CookieCollection cookies) - { - if (cookies == null) - { - throw new ArgumentNullException(nameof(cookies)); - } - - foreach (Cookie cookie in cookies) - { - Add(cookie); - } - } - - /// - /// Copies the cookies in the collection to the specified array, starting at the specified index. - /// - /// The destination array. - /// The index in the destination array at which copying begins. - public void CopyTo(Array array, int index) - { - if (array == null) - { - throw new ArgumentNullException(nameof(array)); - } - - if (index < 0) - { - throw new ArgumentOutOfRangeException(nameof(index), "Less than zero."); - } - - if (array.Rank > 1) - { - throw new ArgumentException("Multidimensional.", nameof(array)); - } - - if (array.Length - index < _list.Count) - { - throw new ArgumentException( - "The number of elements in this collection is greater than the available space of the destination array."); - } - - if (!array.GetType().GetElementType().IsAssignableFrom(typeof(Cookie))) - { - throw new InvalidCastException( - "The elements in this collection cannot be cast automatically to the type of the destination array."); - } ((IList)_list).CopyTo(array, index); - } - - /// - /// Copies the cookies in the collection to the specified array, starting at the specified index. - /// - /// The destination array. - /// The index in the destination array at which copying begins. - public void CopyTo(Cookie[] array, int index) - { - if (array == null) - { - throw new ArgumentNullException(nameof(array)); - } - - if (index < 0) - { - throw new ArgumentOutOfRangeException(nameof(index), "Less than zero."); - } - - if (array.Length - index < _list.Count) - { - throw new ArgumentException( - "The number of elements in this collection is greater than the available space of the destination array."); - } - - _list.CopyTo(array, index); - } - - /// - /// Returns an enumerator that iterates through the collection. - /// - /// An enumerator for the collection. - public IEnumerator GetEnumerator() - { - return _list.GetEnumerator(); - } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Cookies/CookieException.cs b/EonaCat.Network/System/Sockets/Web/Core/Cookies/CookieException.cs index 1fe2ae0..1ef28f4 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Cookies/CookieException.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Cookies/CookieException.cs @@ -13,6 +13,14 @@ namespace EonaCat.Network [Serializable] public class CookieException : FormatException, ISerializable { + /// + /// Initializes a new instance of the class. + /// + public CookieException() + : base() + { + } + /// /// Initializes a new instance of the class with a specified error message. /// @@ -41,15 +49,6 @@ namespace EonaCat.Network : base(serializationInfo, streamingContext) { } - - /// - /// Initializes a new instance of the class. - /// - public CookieException() - : base() - { - } - /// /// Populates a with the data needed to serialize the exception. /// diff --git a/EonaCat.Network/System/Sockets/Web/Core/Endpoints/EndPointListener.cs b/EonaCat.Network/System/Sockets/Web/Core/Endpoints/EndPointListener.cs index 181c274..84bab1c 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Endpoints/EndPointListener.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Endpoints/EndPointListener.cs @@ -18,15 +18,14 @@ namespace EonaCat.Network /// internal sealed class EndPointListener { - private List _all; // host == '+' private static readonly string _defaultCertFolderPath; private readonly IPEndPoint _endpoint; - private Dictionary _prefixes; private readonly Socket _socket; - private List _unhandled; // host == '*' private readonly Dictionary _unregistered; private readonly object _unregisteredSync; - + private List _all; // host == '+' + private Dictionary _prefixes; + private List _unhandled; // host == '*' static EndPointListener() { _defaultCertFolderPath = @@ -93,293 +92,6 @@ namespace EonaCat.Network /// public SSLConfigServer SSL { get; } - private static void addSpecial(List prefixes, HttpListenerPrefix prefix) - { - var path = prefix.Path; - foreach (var pref in prefixes) - { - if (pref.Path == path) - { - throw new HttpListenerException(87, "The prefix is already in use."); - } - } - - prefixes.Add(prefix); - } - - private static RSACryptoServiceProvider createRSAFromFile(string filename) - { - byte[] pvk = null; - using (var fs = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.Read)) - { - pvk = new byte[fs.Length]; - fs.Read(pvk, 0, pvk.Length); - } - - var rsa = new RSACryptoServiceProvider(); - rsa.ImportCspBlob(pvk); - - return rsa; - } - - private static X509Certificate2 getCertificate( - int port, string folderPath, X509Certificate2 defaultCertificate - ) - { - if (folderPath == null || folderPath.Length == 0) - { - folderPath = _defaultCertFolderPath; - } - - try - { - var cer = Path.Combine(folderPath, string.Format("{0}.cer", port)); - var key = Path.Combine(folderPath, string.Format("{0}.key", port)); - if (File.Exists(cer) && File.Exists(key)) - { - var cert = new X509Certificate2(cer); - cert.PrivateKey = createRSAFromFile(key); - - return cert; - } - } - catch - { - } - - return defaultCertificate; - } - - private void leaveIfNoPrefix() - { - if (_prefixes.Count > 0) - { - return; - } - - var prefs = _unhandled; - if (prefs != null && prefs.Count > 0) - { - return; - } - - prefs = _all; - if (prefs != null && prefs.Count > 0) - { - return; - } - - EndPointManager.RemoveEndPoint(_endpoint); - } - - private static void onAccept(IAsyncResult asyncResult) - { - var lsnr = (EndPointListener)asyncResult.AsyncState; - - Socket sock = null; - try - { - sock = lsnr._socket.EndAccept(asyncResult); - } - catch (SocketException) - { - // Do nothing - } - catch (ObjectDisposedException) - { - return; - } - - try - { - lsnr._socket.BeginAccept(onAccept, lsnr); - } - catch - { - sock?.Close(); - - return; - } - - if (sock == null) - { - return; - } - - processAccepted(sock, lsnr); - } - - private static void processAccepted(Socket socket, EndPointListener listener) - { - HttpConnection conn = null; - try - { - conn = new HttpConnection(socket, listener); - lock (listener._unregisteredSync) - { - listener._unregistered[conn] = conn; - } - - conn.BeginReadRequest(); - } - catch - { - if (conn != null) - { - conn.Close(true); - return; - } - - socket.Close(); - } - } - - private static bool removeSpecial(List prefixes, HttpListenerPrefix prefix) - { - var path = prefix.Path; - var cnt = prefixes.Count; - for (var i = 0; i < cnt; i++) - { - if (prefixes[i].Path == path) - { - prefixes.RemoveAt(i); - return true; - } - } - - return false; - } - - private static HttpListener searchHttpListenerFromSpecial( - string path, List prefixes - ) - { - if (prefixes == null) - { - return null; - } - - HttpListener bestMatch = null; - - var bestLen = -1; - foreach (var pref in prefixes) - { - var prefPath = pref.Path; - - var len = prefPath.Length; - if (len < bestLen) - { - continue; - } - - if (path.StartsWith(prefPath)) - { - bestLen = len; - bestMatch = pref.Listener; - } - } - - return bestMatch; - } - - internal static bool CertificateExists(int port, string folderPath) - { - if (folderPath == null || folderPath.Length == 0) - { - folderPath = _defaultCertFolderPath; - } - - var cer = Path.Combine(folderPath, string.Format("{0}.cer", port)); - var key = Path.Combine(folderPath, string.Format("{0}.key", port)); - - return File.Exists(cer) && File.Exists(key); - } - - internal void RemoveConnection(HttpConnection connection) - { - lock (_unregisteredSync) - { - _unregistered.Remove(connection); - } - } - - internal bool TrySearchHttpListener(Uri uri, out HttpListener listener) - { - listener = null; - - if (uri == null) - { - return false; - } - - var host = uri.Host; - var dns = Uri.CheckHostName(host) == UriHostNameType.Dns; - var port = uri.Port.ToString(); - var path = HttpUtility.UrlDecode(uri.AbsolutePath); - var pathSlash = path[path.Length - 1] != '/' ? path + "/" : path; - - if (host != null && host.Length > 0) - { - var bestLen = -1; - foreach (var pref in _prefixes.Keys) - { - if (dns) - { - var prefHost = pref.Host; - if (Uri.CheckHostName(prefHost) == UriHostNameType.Dns && prefHost != host) - { - continue; - } - } - - if (pref.Port != port) - { - continue; - } - - var prefPath = pref.Path; - - var len = prefPath.Length; - if (len < bestLen) - { - continue; - } - - if (path.StartsWith(prefPath) || pathSlash.StartsWith(prefPath)) - { - bestLen = len; - listener = _prefixes[pref]; - } - } - - if (bestLen != -1) - { - return true; - } - } - - var prefs = _unhandled; - listener = searchHttpListenerFromSpecial(path, prefs); - if (listener == null && pathSlash != path) - { - listener = searchHttpListenerFromSpecial(pathSlash, prefs); - } - - if (listener != null) - { - return true; - } - - prefs = _all; - listener = searchHttpListenerFromSpecial(path, prefs); - if (listener == null && pathSlash != path) - { - listener = searchHttpListenerFromSpecial(pathSlash, prefs); - } - - return listener != null; - } - public void AddPrefix(HttpListenerPrefix prefix, HttpListener listener) { List current, future; @@ -528,5 +240,292 @@ namespace EonaCat.Network leaveIfNoPrefix(); } + + internal static bool CertificateExists(int port, string folderPath) + { + if (folderPath == null || folderPath.Length == 0) + { + folderPath = _defaultCertFolderPath; + } + + var cer = Path.Combine(folderPath, string.Format("{0}.cer", port)); + var key = Path.Combine(folderPath, string.Format("{0}.key", port)); + + return File.Exists(cer) && File.Exists(key); + } + + internal void RemoveConnection(HttpConnection connection) + { + lock (_unregisteredSync) + { + _unregistered.Remove(connection); + } + } + + internal bool TrySearchHttpListener(Uri uri, out HttpListener listener) + { + listener = null; + + if (uri == null) + { + return false; + } + + var host = uri.Host; + var dns = Uri.CheckHostName(host) == UriHostNameType.Dns; + var port = uri.Port.ToString(); + var path = HttpUtility.UrlDecode(uri.AbsolutePath); + var pathSlash = path[path.Length - 1] != '/' ? path + "/" : path; + + if (host != null && host.Length > 0) + { + var bestLen = -1; + foreach (var pref in _prefixes.Keys) + { + if (dns) + { + var prefHost = pref.Host; + if (Uri.CheckHostName(prefHost) == UriHostNameType.Dns && prefHost != host) + { + continue; + } + } + + if (pref.Port != port && !_prefixes[pref].AllowForwardedRequest) + { + continue; + } + + var prefPath = pref.Path; + + var len = prefPath.Length; + if (len < bestLen) + { + continue; + } + + if (path.StartsWith(prefPath) || pathSlash.StartsWith(prefPath)) + { + bestLen = len; + listener = _prefixes[pref]; + } + } + + if (bestLen != -1) + { + return true; + } + } + + var prefs = _unhandled; + listener = searchHttpListenerFromSpecial(path, prefs); + if (listener == null && pathSlash != path) + { + listener = searchHttpListenerFromSpecial(pathSlash, prefs); + } + + if (listener != null) + { + return true; + } + + prefs = _all; + listener = searchHttpListenerFromSpecial(path, prefs); + if (listener == null && pathSlash != path) + { + listener = searchHttpListenerFromSpecial(pathSlash, prefs); + } + + return listener != null; + } + + private static void addSpecial(List prefixes, HttpListenerPrefix prefix) + { + var path = prefix.Path; + foreach (var pref in prefixes) + { + if (pref.Path == path) + { + throw new HttpListenerException(87, "The prefix is already in use."); + } + } + + prefixes.Add(prefix); + } + + private static RSACryptoServiceProvider createRSAFromFile(string filename) + { + byte[] pvk = null; + using (var fs = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + pvk = new byte[fs.Length]; + fs.Read(pvk, 0, pvk.Length); + } + + var rsa = new RSACryptoServiceProvider(); + rsa.ImportCspBlob(pvk); + + return rsa; + } + + private static X509Certificate2 getCertificate( + int port, string folderPath, X509Certificate2 defaultCertificate + ) + { + if (folderPath == null || folderPath.Length == 0) + { + folderPath = _defaultCertFolderPath; + } + + try + { + var cer = Path.Combine(folderPath, string.Format("{0}.cer", port)); + var key = Path.Combine(folderPath, string.Format("{0}.key", port)); + if (File.Exists(cer) && File.Exists(key)) + { + var cert = new X509Certificate2(cer); + cert.PrivateKey = createRSAFromFile(key); + + return cert; + } + } + catch + { + } + + return defaultCertificate; + } + + private static void onAccept(IAsyncResult asyncResult) + { + var lsnr = (EndPointListener)asyncResult.AsyncState; + + Socket sock = null; + try + { + sock = lsnr._socket.EndAccept(asyncResult); + } + catch (SocketException) + { + // Do nothing + } + catch (ObjectDisposedException) + { + return; + } + + try + { + lsnr._socket.BeginAccept(onAccept, lsnr); + } + catch + { + sock?.Close(); + + return; + } + + if (sock == null) + { + return; + } + + processAccepted(sock, lsnr); + } + + private static void processAccepted(Socket socket, EndPointListener listener) + { + HttpConnection conn = null; + try + { + conn = new HttpConnection(socket, listener); + lock (listener._unregisteredSync) + { + listener._unregistered[conn] = conn; + } + + conn.BeginReadRequest(); + } + catch + { + if (conn != null) + { + conn.Close(true); + return; + } + + socket.Close(); + } + } + + private static bool removeSpecial(List prefixes, HttpListenerPrefix prefix) + { + var path = prefix.Path; + var cnt = prefixes.Count; + for (var i = 0; i < cnt; i++) + { + if (prefixes[i].Path == path) + { + prefixes.RemoveAt(i); + return true; + } + } + + return false; + } + + private static HttpListener searchHttpListenerFromSpecial( + string path, List prefixes + ) + { + if (prefixes == null) + { + return null; + } + + HttpListener bestMatch = null; + + var bestLen = -1; + foreach (var pref in prefixes) + { + var prefPath = pref.Path; + + var len = prefPath.Length; + if (len < bestLen) + { + continue; + } + + if (path.StartsWith(prefPath)) + { + bestLen = len; + bestMatch = pref.Listener; + } + } + + return bestMatch; + } + + private void leaveIfNoPrefix() + { + if (_prefixes.Count > 0) + { + return; + } + + var prefs = _unhandled; + if (prefs != null && prefs.Count > 0) + { + return; + } + + prefs = _all; + if (prefs != null && prefs.Count > 0) + { + return; + } + + EndPointManager.RemoveEndPoint(_endpoint); + } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Endpoints/EndPointManager.cs b/EonaCat.Network/System/Sockets/Web/Core/Endpoints/EndPointManager.cs index e03d515..f2aa2a2 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Endpoints/EndPointManager.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Endpoints/EndPointManager.cs @@ -30,6 +30,97 @@ namespace EonaCat.Network { } + /// + /// Adds an HTTP listener and its associated prefixes. + /// + /// The HTTP listener to be added. + public static void AddListener(HttpListener listener) + { + var added = new List(); + lock (((ICollection)_endpoints).SyncRoot) + { + try + { + foreach (var pref in listener.Prefixes) + { + addPrefix(pref, listener); + added.Add(pref); + } + } + catch + { + foreach (var pref in added) + { + removePrefix(pref, listener); + } + + throw; + } + } + } + + /// + /// Adds an HTTP listener prefix. + /// + /// The URI prefix to be added. + /// The HTTP listener associated with the prefix. + public static void AddPrefix(string uriPrefix, HttpListener listener) + { + lock (((ICollection)_endpoints).SyncRoot) + { + addPrefix(uriPrefix, listener); + } + } + + /// + /// Removes an HTTP listener and its associated prefixes. + /// + /// The HTTP listener to be removed. + public static void RemoveListener(HttpListener listener) + { + lock (((ICollection)_endpoints).SyncRoot) + { + foreach (var pref in listener.Prefixes) + { + removePrefix(pref, listener); + } + } + } + + /// + /// Removes an HTTP listener prefix. + /// + /// The URI prefix to be removed. + /// The HTTP listener associated with the prefix. + public static void RemovePrefix(string uriPrefix, HttpListener listener) + { + lock (((ICollection)_endpoints).SyncRoot) + { + removePrefix(uriPrefix, listener); + } + } + + /// + /// Removes an endpoint and closes its associated listener. + /// + /// The endpoint to be removed. + /// true if the endpoint is successfully removed; otherwise, false. + internal static bool RemoveEndPoint(IPEndPoint endpoint) + { + lock (((ICollection)_endpoints).SyncRoot) + { + if (!_endpoints.TryGetValue(endpoint, out EndPointListener lsnr)) + { + return false; + } + + _endpoints.Remove(endpoint); + lsnr.Close(); + + return true; + } + } + private static void addPrefix(string uriPrefix, HttpListener listener) { var pref = new HttpListenerPrefix(uriPrefix); @@ -137,96 +228,5 @@ namespace EonaCat.Network lsnr.RemovePrefix(pref, listener); } - - /// - /// Removes an endpoint and closes its associated listener. - /// - /// The endpoint to be removed. - /// true if the endpoint is successfully removed; otherwise, false. - internal static bool RemoveEndPoint(IPEndPoint endpoint) - { - lock (((ICollection)_endpoints).SyncRoot) - { - if (!_endpoints.TryGetValue(endpoint, out EndPointListener lsnr)) - { - return false; - } - - _endpoints.Remove(endpoint); - lsnr.Close(); - - return true; - } - } - - /// - /// Adds an HTTP listener and its associated prefixes. - /// - /// The HTTP listener to be added. - public static void AddListener(HttpListener listener) - { - var added = new List(); - lock (((ICollection)_endpoints).SyncRoot) - { - try - { - foreach (var pref in listener.Prefixes) - { - addPrefix(pref, listener); - added.Add(pref); - } - } - catch - { - foreach (var pref in added) - { - removePrefix(pref, listener); - } - - throw; - } - } - } - - /// - /// Adds an HTTP listener prefix. - /// - /// The URI prefix to be added. - /// The HTTP listener associated with the prefix. - public static void AddPrefix(string uriPrefix, HttpListener listener) - { - lock (((ICollection)_endpoints).SyncRoot) - { - addPrefix(uriPrefix, listener); - } - } - - /// - /// Removes an HTTP listener and its associated prefixes. - /// - /// The HTTP listener to be removed. - public static void RemoveListener(HttpListener listener) - { - lock (((ICollection)_endpoints).SyncRoot) - { - foreach (var pref in listener.Prefixes) - { - removePrefix(pref, listener); - } - } - } - - /// - /// Removes an HTTP listener prefix. - /// - /// The URI prefix to be removed. - /// The HTTP listener associated with the prefix. - public static void RemovePrefix(string uriPrefix, HttpListener listener) - { - lock (((ICollection)_endpoints).SyncRoot) - { - removePrefix(uriPrefix, listener); - } - } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpConnection.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpConnection.cs index 025e54a..3487ed6 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpConnection.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpConnection.cs @@ -17,10 +17,13 @@ namespace EonaCat.Network /// internal sealed class HttpConnection { - private byte[] _buffer; private const int _bufferLength = 8192; private const int TIMEOUT_CONTINUE = 15000; private const int TIMEOUT_INITIAL = 90000; + private readonly EndPointListener _listener; + private readonly object _lock; + private readonly Dictionary _timeoutCanceled; + private byte[] _buffer; private HttpListenerContext _context; private bool _contextRegistered; private StringBuilder _currentLine; @@ -28,14 +31,11 @@ namespace EonaCat.Network private RequestStream _inputStream; private HttpListener _lastListener; private LineState _lineState; - private readonly EndPointListener _listener; private ResponseStream _outputStream; private int _position; private MemoryStream _requestBuffer; private Socket _socket; - private readonly object _lock; private int _timeout; - private readonly Dictionary _timeoutCanceled; private Timer _timer; /// @@ -106,375 +106,6 @@ namespace EonaCat.Network /// public Stream Stream { get; private set; } - private void close() - { - lock (_lock) - { - if (_socket == null) - { - return; - } - - DisposeTimer(); - DisposeRequestBuffer(); - DisposeStream(); - CloseSocket(); - } - - UnregisterContext(); - RemoveConnection(); - } - - private void CloseSocket() - { - try - { - _socket.Shutdown(SocketShutdown.Both); - } - catch - { - } - - _socket.Close(); - _socket = null; - } - - private void DisposeRequestBuffer() - { - if (_requestBuffer == null) - { - return; - } - - _requestBuffer.Dispose(); - _requestBuffer = null; - } - - private void DisposeStream() - { - if (Stream == null) - { - return; - } - - _inputStream = null; - _outputStream = null; - - Stream.Dispose(); - Stream = null; - } - - private void DisposeTimer() - { - if (_timer == null) - { - return; - } - - try - { - _timer.Change(Timeout.Infinite, Timeout.Infinite); - } - catch - { - } - - _timer.Dispose(); - _timer = null; - } - - private void Setup() - { - _context = new HttpListenerContext(this); - _inputState = InputState.RequestLine; - _inputStream = null; - _lineState = LineState.None; - _outputStream = null; - _position = 0; - _requestBuffer = new MemoryStream(); - } - - private static void OnRead(IAsyncResult asyncResult) - { - var httpConnection = (HttpConnection)asyncResult.AsyncState; - if (httpConnection._socket == null) - { - return; - } - - lock (httpConnection._lock) - { - if (httpConnection._socket == null) - { - return; - } - - var nread = -1; - var len = 0; - try - { - var current = httpConnection.Reuses; - if (!httpConnection._timeoutCanceled[current]) - { - httpConnection._timer.Change(Timeout.Infinite, Timeout.Infinite); - httpConnection._timeoutCanceled[current] = true; - } - - nread = httpConnection.Stream.EndRead(asyncResult); - httpConnection._requestBuffer.Write(httpConnection._buffer, 0, nread); - len = (int)httpConnection._requestBuffer.Length; - } - catch (Exception exception) - { - if (httpConnection._requestBuffer != null && httpConnection._requestBuffer.Length > 0) - { - httpConnection.SendError(exception.Message, 400); - return; - } - - httpConnection.close(); - return; - } - - if (nread <= 0) - { - httpConnection.close(); - return; - } - - if (httpConnection.ProcessInput(httpConnection._requestBuffer.GetBuffer(), len)) - { - if (!httpConnection._context.HasError) - { - httpConnection._context.Request.FinishInitialization(); - } - - if (httpConnection._context.HasError) - { - httpConnection.SendError(); - return; - } - - if (!httpConnection._listener.TrySearchHttpListener(httpConnection._context.Request.Url, out HttpListener httpListener)) - { - httpConnection.SendError(null, 404); - return; - } - - if (httpConnection._lastListener != httpListener) - { - httpConnection.RemoveConnection(); - if (!httpListener.AddConnection(httpConnection)) - { - httpConnection.close(); - return; - } - - httpConnection._lastListener = httpListener; - } - - httpConnection._context.Listener = httpListener; - if (!httpConnection._context.Authenticate()) - { - return; - } - - if (httpConnection._context.Register()) - { - httpConnection._contextRegistered = true; - } - - return; - } - - httpConnection.Stream.BeginRead(httpConnection._buffer, 0, _bufferLength, OnRead, httpConnection); - } - } - - private static void OnTimeout(object state) - { - var httpConnection = (HttpConnection)state; - var current = httpConnection.Reuses; - if (httpConnection._socket == null) - { - return; - } - - lock (httpConnection._lock) - { - if (httpConnection._socket == null) - { - return; - } - - if (httpConnection._timeoutCanceled[current]) - { - return; - } - - httpConnection.SendError(null, 408); - } - } - - private bool ProcessInput(byte[] data, int length) - { - _currentLine ??= new StringBuilder(64); - - var nread = 0; - try - { - string line; - while ((line = ReadLineFrom(data, _position, length, out nread)) != null) - { - _position += nread; - if (line.Length == 0) - { - if (_inputState == InputState.RequestLine) - { - continue; - } - - if (_position > 32768) - { - _context.ErrorMessage = "Headers too long"; - } - - _currentLine = null; - return true; - } - - if (_inputState == InputState.RequestLine) - { - _context.Request.SetRequestLine(line); - _inputState = InputState.Headers; - } - else - { - _context.Request.AddHeader(line); - } - - if (_context.HasError) - { - return true; - } - } - } - catch (Exception exception) - { - _context.ErrorMessage = exception.Message; - return true; - } - - _position += nread; - if (_position >= 32768) - { - _context.ErrorMessage = "Headers too long"; - return true; - } - - return false; - } - - private string ReadLineFrom(byte[] buffer, int offset, int length, out int read) - { - read = 0; - - for (var i = offset; i < length && _lineState != LineState.LineFeed; i++) - { - read++; - - var b = buffer[i]; - if (b == 13) - { - _lineState = LineState.CarriageReturn; - } - else if (b == 10) - { - _lineState = LineState.LineFeed; - } - else - { - _currentLine.Append((char)b); - } - } - - if (_lineState != LineState.LineFeed) - { - return null; - } - - var line = _currentLine.ToString(); - - _currentLine.Length = 0; - _lineState = LineState.None; - - return line; - } - - private void RemoveConnection() - { - if (_lastListener != null) - { - _lastListener.RemoveConnection(this); - } - else - { - _listener.RemoveConnection(this); - } - } - - private void UnregisterContext() - { - if (!_contextRegistered) - { - return; - } - - _context.Unregister(); - _contextRegistered = false; - } - - /// - /// Closes the connection. - /// - /// True to force close, false otherwise. - internal void Close(bool force) - { - if (_socket == null) - { - return; - } - - lock (_lock) - { - if (_socket == null) - { - return; - } - - if (!force) - { - GetResponseStream().Close(false); - if (!_context.Response.CloseConnection && _context.Request.FlushInput()) - { - // Don't close. Keep working. - Reuses++; - DisposeRequestBuffer(); - UnregisterContext(); - Setup(); - BeginReadRequest(); - return; - } - } - else - { - _outputStream?.Close(true); - } - - close(); - } - } - /// /// Initiates reading the request. /// @@ -629,5 +260,373 @@ namespace EonaCat.Network } } } + + /// + /// Closes the connection. + /// + /// True to force close, false otherwise. + internal void Close(bool force) + { + if (_socket == null) + { + return; + } + + lock (_lock) + { + if (_socket == null) + { + return; + } + + if (!force) + { + GetResponseStream().Close(false); + if (!_context.Response.CloseConnection && _context.Request.FlushInput()) + { + // Don't close. Keep working. + Reuses++; + DisposeRequestBuffer(); + UnregisterContext(); + Setup(); + BeginReadRequest(); + return; + } + } + else + { + _outputStream?.Close(true); + } + + close(); + } + } + + private static void OnRead(IAsyncResult asyncResult) + { + var httpConnection = (HttpConnection)asyncResult.AsyncState; + if (httpConnection._socket == null) + { + return; + } + + lock (httpConnection._lock) + { + if (httpConnection._socket == null) + { + return; + } + + var nread = -1; + var len = 0; + try + { + var current = httpConnection.Reuses; + if (!httpConnection._timeoutCanceled[current]) + { + httpConnection._timer.Change(Timeout.Infinite, Timeout.Infinite); + httpConnection._timeoutCanceled[current] = true; + } + + nread = httpConnection.Stream.EndRead(asyncResult); + httpConnection._requestBuffer.Write(httpConnection._buffer, 0, nread); + len = (int)httpConnection._requestBuffer.Length; + } + catch (Exception exception) + { + if (httpConnection._requestBuffer != null && httpConnection._requestBuffer.Length > 0) + { + httpConnection.SendError(exception.Message, 400); + return; + } + + httpConnection.close(); + return; + } + + if (nread <= 0) + { + httpConnection.close(); + return; + } + + if (httpConnection.ProcessInput(httpConnection._requestBuffer.GetBuffer(), len)) + { + if (!httpConnection._context.HasError) + { + httpConnection._context.Request.FinishInitialization(); + } + + if (httpConnection._context.HasError) + { + httpConnection.SendError(); + return; + } + + if (!httpConnection._listener.TrySearchHttpListener(httpConnection._context.Request.Url, out HttpListener httpListener)) + { + httpConnection.SendError(null, 404); + return; + } + + if (httpConnection._lastListener != httpListener) + { + httpConnection.RemoveConnection(); + if (!httpListener.AddConnection(httpConnection)) + { + httpConnection.close(); + return; + } + + httpConnection._lastListener = httpListener; + } + + httpConnection._context.Listener = httpListener; + if (!httpConnection._context.Authenticate()) + { + return; + } + + if (httpConnection._context.Register()) + { + httpConnection._contextRegistered = true; + } + + return; + } + + httpConnection.Stream.BeginRead(httpConnection._buffer, 0, _bufferLength, OnRead, httpConnection); + } + } + + private static void OnTimeout(object state) + { + var httpConnection = (HttpConnection)state; + var current = httpConnection.Reuses; + if (httpConnection._socket == null) + { + return; + } + + lock (httpConnection._lock) + { + if (httpConnection._socket == null) + { + return; + } + + if (httpConnection._timeoutCanceled[current]) + { + return; + } + + httpConnection.SendError(null, 408); + } + } + + private void close() + { + lock (_lock) + { + if (_socket == null) + { + return; + } + + DisposeTimer(); + DisposeRequestBuffer(); + DisposeStream(); + CloseSocket(); + } + + UnregisterContext(); + RemoveConnection(); + } + + private void CloseSocket() + { + try + { + _socket.Shutdown(SocketShutdown.Both); + } + catch + { + } + + _socket.Close(); + _socket = null; + } + + private void DisposeRequestBuffer() + { + if (_requestBuffer == null) + { + return; + } + + _requestBuffer.Dispose(); + _requestBuffer = null; + } + + private void DisposeStream() + { + if (Stream == null) + { + return; + } + + _inputStream = null; + _outputStream = null; + + Stream.Dispose(); + Stream = null; + } + + private void DisposeTimer() + { + if (_timer == null) + { + return; + } + + try + { + _timer.Change(Timeout.Infinite, Timeout.Infinite); + } + catch + { + } + + _timer.Dispose(); + _timer = null; + } + + private bool ProcessInput(byte[] data, int length) + { + _currentLine ??= new StringBuilder(64); + + var nread = 0; + try + { + string line; + while ((line = ReadLineFrom(data, _position, length, out nread)) != null) + { + _position += nread; + if (line.Length == 0) + { + if (_inputState == InputState.RequestLine) + { + continue; + } + + if (_position > 32768) + { + _context.ErrorMessage = "Headers too long"; + } + + _currentLine = null; + return true; + } + + if (_inputState == InputState.RequestLine) + { + _context.Request.SetRequestLine(line); + _inputState = InputState.Headers; + } + else + { + _context.Request.AddHeader(line); + } + + if (_context.HasError) + { + return true; + } + } + } + catch (Exception exception) + { + _context.ErrorMessage = exception.Message; + return true; + } + + _position += nread; + if (_position >= 32768) + { + _context.ErrorMessage = "Headers too long"; + return true; + } + + return false; + } + + private string ReadLineFrom(byte[] buffer, int offset, int length, out int read) + { + read = 0; + + for (var i = offset; i < length && _lineState != LineState.LineFeed; i++) + { + read++; + + var b = buffer[i]; + if (b == 13) + { + _lineState = LineState.CarriageReturn; + } + else if (b == 10) + { + _lineState = LineState.LineFeed; + } + else + { + _currentLine.Append((char)b); + } + } + + if (_lineState != LineState.LineFeed) + { + return null; + } + + var line = _currentLine.ToString(); + + _currentLine.Length = 0; + _lineState = LineState.None; + + return line; + } + + private void RemoveConnection() + { + if (_lastListener != null) + { + _lastListener.RemoveConnection(this); + } + else + { + _listener.RemoveConnection(this); + } + } + + private void Setup() + { + _context = new HttpListenerContext(this); + _inputState = InputState.RequestLine; + _inputStream = null; + _lineState = LineState.None; + _outputStream = null; + _position = 0; + _requestBuffer = new MemoryStream(); + } + private void UnregisterContext() + { + if (!_contextRegistered) + { + return; + } + + _context.Unregister(); + _contextRegistered = false; + } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpHeaderInfo.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpHeaderInfo.cs index 1473562..25c380d 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpHeaderInfo.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpHeaderInfo.cs @@ -19,16 +19,6 @@ namespace EonaCat.Network Type = type; } - /// - /// Gets a value indicating whether the header is multi-value in a request. - /// - internal bool IsMultiValueInRequest => (Type & HttpHeaderType.MultiValueInRequest) == HttpHeaderType.MultiValueInRequest; - - /// - /// Gets a value indicating whether the header is multi-value in a response. - /// - internal bool IsMultiValueInResponse => (Type & HttpHeaderType.MultiValueInResponse) == HttpHeaderType.MultiValueInResponse; - /// /// Gets a value indicating whether the header is for a request. /// @@ -49,6 +39,15 @@ namespace EonaCat.Network /// public HttpHeaderType Type { get; } + /// + /// Gets a value indicating whether the header is multi-value in a request. + /// + internal bool IsMultiValueInRequest => (Type & HttpHeaderType.MultiValueInRequest) == HttpHeaderType.MultiValueInRequest; + + /// + /// Gets a value indicating whether the header is multi-value in a response. + /// + internal bool IsMultiValueInResponse => (Type & HttpHeaderType.MultiValueInResponse) == HttpHeaderType.MultiValueInResponse; /// /// Gets a value indicating whether the header is multi-value. /// diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListener.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListener.cs index 54f0427..09d10ab 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListener.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListener.cs @@ -13,25 +13,25 @@ namespace EonaCat.Network /// public sealed class HttpListener : IDisposable { - private AuthenticationSchemes _authSchemes; - private Func _authSchemeSelector; - private string _certFolderPath; + private static readonly string _defaultRealm; private readonly Dictionary _connections; private readonly object _connectionsSync; - private readonly List contextQueue; private readonly object _contextQueueLock; private readonly Dictionary _contextRegistry; private readonly object _contextRegistryLock; - private static readonly string _defaultRealm; + private readonly HttpListenerPrefixCollection _prefixes; + private readonly List _waitQueue; + private readonly object _waitQueueLock; + private readonly List contextQueue; + private bool _allowForwardedRequest; + private AuthenticationSchemes _authSchemes; + private Func _authSchemeSelector; + private string _certFolderPath; private bool _ignoreWriteExceptions; private volatile bool _listening; - private readonly HttpListenerPrefixCollection _prefixes; private string _realm; private SSLConfigServer _sslConfig; private Func _userCredFinder; - private readonly List _waitQueue; - private readonly object _waitQueueLock; - static HttpListener() { _defaultRealm = "SECRET AREA"; @@ -59,9 +59,29 @@ namespace EonaCat.Network _waitQueueLock = ((ICollection)_waitQueue).SyncRoot; } - internal bool IsDisposed { get; private set; } - - internal bool ReuseAddress { get; set; } + public static bool IsSupported => true; + /// + /// Gets or sets a value indicating whether the server accepts every + /// handshake request without checking the request URI. + /// + /// + /// The set operation does nothing if the server has already started or + /// it is shutting down. + /// + /// + /// + /// true if the server accepts every handshake request without + /// checking the request URI; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool AllowForwardedRequest + { + get { return _allowForwardedRequest; } + set { _allowForwardedRequest = value; } + } /// /// Gets or sets the authentication schemes used by this listener. @@ -127,9 +147,6 @@ namespace EonaCat.Network } public bool IsListening => _listening; - - public static bool IsSupported => true; - public HttpListenerPrefixCollection Prefixes { get @@ -197,6 +214,289 @@ namespace EonaCat.Network } } + internal bool IsDisposed { get; private set; } + + internal bool ReuseAddress { get; set; } + /// + /// Aborts the listener and releases all resources associated with it. + /// + public void Abort() + { + if (IsDisposed) + { + return; + } + + close(true); + } + + /// + /// Begins asynchronously getting an HTTP context from the listener. + /// + /// The method to call when the operation completes. + /// A user-defined object that contains information about the asynchronous operation. + /// An that represents the asynchronous operation. + public IAsyncResult BeginGetContext(AsyncCallback callback, object state) + { + CheckDisposed(); + if (_prefixes.Count == 0) + { + throw new InvalidOperationException("The listener has no URI prefix on which listens."); + } + + if (!_listening) + { + throw new InvalidOperationException("The listener hasn't been started."); + } + + return BeginGetContext(new HttpListenerAsyncResult(callback, state)); + } + + /// + /// Closes the listener. + /// + public void Close() + { + if (IsDisposed) + { + return; + } + + close(false); + } + + /// + /// Disposes of the resources used by the . + /// + void IDisposable.Dispose() + { + if (IsDisposed) + { + return; + } + + close(true); + } + + /// + /// Ends an asynchronous operation to get an HTTP context from the listener. + /// + /// The reference to the pending asynchronous request to finish. + /// An that represents the context of the asynchronous operation. + public HttpListenerContext EndGetContext(IAsyncResult asyncResult) + { + CheckDisposed(); + if (asyncResult == null) + { + throw new ArgumentNullException(nameof(asyncResult)); + } + + if (asyncResult is not HttpListenerAsyncResult result) + { + throw new ArgumentException("A wrong IAsyncResult.", nameof(asyncResult)); + } + + if (result.EndCalled) + { + throw new InvalidOperationException("This IAsyncResult cannot be reused."); + } + + result.EndCalled = true; + if (!result.IsCompleted) + { + result.AsyncWaitHandle.WaitOne(); + } + + return result.GetContext(); + } + + /// + /// Gets the next available HTTP context from the listener. + /// + /// An that represents the context of the HTTP request. + public HttpListenerContext GetContext() + { + CheckDisposed(); + if (_prefixes.Count == 0) + { + throw new InvalidOperationException("The listener has no URI prefix on which listens."); + } + + if (!_listening) + { + throw new InvalidOperationException("The listener hasn't been started."); + } + + var result = BeginGetContext(new HttpListenerAsyncResult(null, null)); + result.InGet = true; + + return EndGetContext(result); + } + + /// + /// Starts listening for incoming requests. + /// + public void Start() + { + CheckDisposed(); + if (_listening) + { + return; + } + + EndPointManager.AddListener(this); + _listening = true; + } + + /// + /// Stops listening for incoming requests. + /// + public void Stop() + { + CheckDisposed(); + if (!_listening) + { + return; + } + + _listening = false; + EndPointManager.RemoveListener(this); + + lock (_contextRegistryLock) + { + CleanupContextQueue(true); + } + + CleanupContextRegistry(); + CleanupConnections(); + CleanupWaitQueue(new HttpListenerException(995, "The listener is closed.")); + } + + internal bool AddConnection(HttpConnection connection) + { + if (!_listening) + { + return false; + } + + lock (_connectionsSync) + { + if (!_listening) + { + return false; + } + + _connections[connection] = connection; + return true; + } + } + + internal HttpListenerAsyncResult BeginGetContext(HttpListenerAsyncResult asyncResult) + { + lock (_contextRegistryLock) + { + if (!_listening) + { + throw new HttpListenerException(995); + } + + var ctx = GetContextFromQueue(); + if (ctx == null) + { + _waitQueue.Add(asyncResult); + } + else + { + asyncResult.Complete(ctx, true); + } + + return asyncResult; + } + } + + internal void CheckDisposed() + { + if (IsDisposed) + { + throw new ObjectDisposedException(GetType().ToString()); + } + } + + internal string GetRealm() + { + var realm = _realm; + return realm != null && realm.Length > 0 ? realm : _defaultRealm; + } + + internal Func GetUserCredentialsFinder() + { + return _userCredFinder; + } + + internal bool RegisterContext(HttpListenerContext context) + { + if (!_listening) + { + return false; + } + + lock (_contextRegistryLock) + { + if (!_listening) + { + return false; + } + + _contextRegistry[context] = context; + + var result = GetAsyncResultFromQueue(); + if (result == null) + { + contextQueue.Add(context); + } + else + { + result.Complete(context); + } + + return true; + } + } + + internal void RemoveConnection(HttpConnection connection) + { + lock (_connectionsSync) + { + _connections.Remove(connection); + } + } + + internal AuthenticationSchemes SelectAuthenticationScheme(HttpListenerRequest request) + { + var selector = _authSchemeSelector; + if (selector == null) + { + return _authSchemes; + } + + try + { + return selector(request); + } + catch + { + return AuthenticationSchemes.None; + } + } + + internal void UnregisterContext(HttpListenerContext context) + { + lock (_contextRegistryLock) + { + _contextRegistry.Remove(context); + } + } + private void CleanupConnections() { HttpConnection[] httpConnections = null; @@ -335,285 +635,5 @@ namespace EonaCat.Network return ctx; } - - internal bool AddConnection(HttpConnection connection) - { - if (!_listening) - { - return false; - } - - lock (_connectionsSync) - { - if (!_listening) - { - return false; - } - - _connections[connection] = connection; - return true; - } - } - - internal HttpListenerAsyncResult BeginGetContext(HttpListenerAsyncResult asyncResult) - { - lock (_contextRegistryLock) - { - if (!_listening) - { - throw new HttpListenerException(995); - } - - var ctx = GetContextFromQueue(); - if (ctx == null) - { - _waitQueue.Add(asyncResult); - } - else - { - asyncResult.Complete(ctx, true); - } - - return asyncResult; - } - } - - internal void CheckDisposed() - { - if (IsDisposed) - { - throw new ObjectDisposedException(GetType().ToString()); - } - } - - internal string GetRealm() - { - var realm = _realm; - return realm != null && realm.Length > 0 ? realm : _defaultRealm; - } - - internal Func GetUserCredentialsFinder() - { - return _userCredFinder; - } - - internal bool RegisterContext(HttpListenerContext context) - { - if (!_listening) - { - return false; - } - - lock (_contextRegistryLock) - { - if (!_listening) - { - return false; - } - - _contextRegistry[context] = context; - - var result = GetAsyncResultFromQueue(); - if (result == null) - { - contextQueue.Add(context); - } - else - { - result.Complete(context); - } - - return true; - } - } - - internal void RemoveConnection(HttpConnection connection) - { - lock (_connectionsSync) - { - _connections.Remove(connection); - } - } - - internal AuthenticationSchemes SelectAuthenticationScheme(HttpListenerRequest request) - { - var selector = _authSchemeSelector; - if (selector == null) - { - return _authSchemes; - } - - try - { - return selector(request); - } - catch - { - return AuthenticationSchemes.None; - } - } - - internal void UnregisterContext(HttpListenerContext context) - { - lock (_contextRegistryLock) - { - _contextRegistry.Remove(context); - } - } - - /// - /// Aborts the listener and releases all resources associated with it. - /// - public void Abort() - { - if (IsDisposed) - { - return; - } - - close(true); - } - - /// - /// Begins asynchronously getting an HTTP context from the listener. - /// - /// The method to call when the operation completes. - /// A user-defined object that contains information about the asynchronous operation. - /// An that represents the asynchronous operation. - public IAsyncResult BeginGetContext(AsyncCallback callback, object state) - { - CheckDisposed(); - if (_prefixes.Count == 0) - { - throw new InvalidOperationException("The listener has no URI prefix on which listens."); - } - - if (!_listening) - { - throw new InvalidOperationException("The listener hasn't been started."); - } - - return BeginGetContext(new HttpListenerAsyncResult(callback, state)); - } - - /// - /// Closes the listener. - /// - public void Close() - { - if (IsDisposed) - { - return; - } - - close(false); - } - - /// - /// Ends an asynchronous operation to get an HTTP context from the listener. - /// - /// The reference to the pending asynchronous request to finish. - /// An that represents the context of the asynchronous operation. - public HttpListenerContext EndGetContext(IAsyncResult asyncResult) - { - CheckDisposed(); - if (asyncResult == null) - { - throw new ArgumentNullException(nameof(asyncResult)); - } - - if (asyncResult is not HttpListenerAsyncResult result) - { - throw new ArgumentException("A wrong IAsyncResult.", nameof(asyncResult)); - } - - if (result.EndCalled) - { - throw new InvalidOperationException("This IAsyncResult cannot be reused."); - } - - result.EndCalled = true; - if (!result.IsCompleted) - { - result.AsyncWaitHandle.WaitOne(); - } - - return result.GetContext(); - } - - /// - /// Gets the next available HTTP context from the listener. - /// - /// An that represents the context of the HTTP request. - public HttpListenerContext GetContext() - { - CheckDisposed(); - if (_prefixes.Count == 0) - { - throw new InvalidOperationException("The listener has no URI prefix on which listens."); - } - - if (!_listening) - { - throw new InvalidOperationException("The listener hasn't been started."); - } - - var result = BeginGetContext(new HttpListenerAsyncResult(null, null)); - result.InGet = true; - - return EndGetContext(result); - } - - /// - /// Starts listening for incoming requests. - /// - public void Start() - { - CheckDisposed(); - if (_listening) - { - return; - } - - EndPointManager.AddListener(this); - _listening = true; - } - - /// - /// Stops listening for incoming requests. - /// - public void Stop() - { - CheckDisposed(); - if (!_listening) - { - return; - } - - _listening = false; - EndPointManager.RemoveListener(this); - - lock (_contextRegistryLock) - { - CleanupContextQueue(true); - } - - CleanupContextRegistry(); - CleanupConnections(); - CleanupWaitQueue(new HttpListenerException(995, "The listener is closed.")); - } - - /// - /// Disposes of the resources used by the . - /// - void IDisposable.Dispose() - { - if (IsDisposed) - { - return; - } - - close(true); - } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerAsyncResult.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerAsyncResult.cs index 899cbfb..384e0b1 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerAsyncResult.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerAsyncResult.cs @@ -12,10 +12,10 @@ namespace EonaCat.Network internal class HttpListenerAsyncResult : IAsyncResult { private readonly AsyncCallback _callback; + private readonly object _sync; private bool _completed; private HttpListenerContext _context; private Exception _exception; - private readonly object _sync; private ManualResetEvent _waitHandle; /// @@ -30,16 +30,6 @@ namespace EonaCat.Network _sync = new object(); } - /// - /// Gets or sets a value indicating whether the method has been called. - /// - internal bool EndCalled { get; set; } - - /// - /// Gets or sets a value indicating whether the asynchronous operation is in progress. - /// - internal bool InGet { get; set; } - /// /// Gets the user-defined object that contains information about the asynchronous operation. /// @@ -78,38 +68,15 @@ namespace EonaCat.Network } } - // Private method to complete the asynchronous operation - private static void complete(HttpListenerAsyncResult asyncResult) - { - lock (asyncResult._sync) - { - asyncResult._completed = true; - - var waitHandle = asyncResult._waitHandle; - waitHandle?.Set(); - } - - var callback = asyncResult._callback; - if (callback == null) - { - return; - } - - ThreadPool.QueueUserWorkItem( - state => - { - try - { - callback(asyncResult); - } - catch - { - } - }, - null - ); - } + /// + /// Gets or sets a value indicating whether the method has been called. + /// + internal bool EndCalled { get; set; } + /// + /// Gets or sets a value indicating whether the asynchronous operation is in progress. + /// + internal bool InGet { get; set; } /// /// Completes the asynchronous operation with the specified exception. /// @@ -158,5 +125,37 @@ namespace EonaCat.Network return _context; } + + // Private method to complete the asynchronous operation + private static void complete(HttpListenerAsyncResult asyncResult) + { + lock (asyncResult._sync) + { + asyncResult._completed = true; + + var waitHandle = asyncResult._waitHandle; + waitHandle?.Set(); + } + + var callback = asyncResult._callback; + if (callback == null) + { + return; + } + + ThreadPool.QueueUserWorkItem( + state => + { + try + { + callback(asyncResult); + } + catch + { + } + }, + null + ); + } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerContext.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerContext.cs index 54b9a13..c8c0463 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerContext.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerContext.cs @@ -25,6 +25,21 @@ namespace EonaCat.Network Response = new HttpListenerResponse(this); } + /// + /// Gets the associated with the context. + /// + public HttpListenerRequest Request { get; } + + /// + /// Gets the associated with the context. + /// + public HttpListenerResponse Response { get; } + + /// + /// Gets or sets the associated with the user. + /// + public IPrincipal User { get; private set; } + /// /// Gets the underlying for the context. /// @@ -49,21 +64,34 @@ namespace EonaCat.Network /// Gets or sets the associated with the context. /// internal HttpListener Listener { get; set; } - /// - /// Gets the associated with the context. + /// Accepts a WebSocket connection with the specified protocol. /// - public HttpListenerRequest Request { get; } + /// The WebSocket subprotocol to negotiate. + /// The for the WebSocket connection. + public HttpListenerWSContext AcceptWebSocket(string protocol) + { + if (_websocketContext != null) + { + throw new InvalidOperationException("Accepting already in progress."); + } - /// - /// Gets the associated with the context. - /// - public HttpListenerResponse Response { get; } + if (protocol != null) + { + if (protocol.Length == 0) + { + throw new ArgumentException("Empty string.", nameof(protocol)); + } - /// - /// Gets or sets the associated with the user. - /// - public IPrincipal User { get; private set; } + if (!protocol.IsToken()) + { + throw new ArgumentException("Contains invalid characters", nameof(protocol)); + } + } + + _websocketContext = new HttpListenerWSContext(this, protocol); + return _websocketContext; + } /// /// Authenticates the user based on the specified authentication scheme. @@ -119,34 +147,5 @@ namespace EonaCat.Network { Listener.UnregisterContext(this); } - - /// - /// Accepts a WebSocket connection with the specified protocol. - /// - /// The WebSocket subprotocol to negotiate. - /// The for the WebSocket connection. - public HttpListenerWSContext AcceptWebSocket(string protocol) - { - if (_websocketContext != null) - { - throw new InvalidOperationException("Accepting already in progress."); - } - - if (protocol != null) - { - if (protocol.Length == 0) - { - throw new ArgumentException("Empty string.", nameof(protocol)); - } - - if (!protocol.IsToken()) - { - throw new ArgumentException("Contains invalid characters", nameof(protocol)); - } - } - - _websocketContext = new HttpListenerWSContext(this, protocol); - return _websocketContext; - } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerException.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerException.cs index 00d1e85..fe8610e 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerException.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerException.cs @@ -13,15 +13,6 @@ namespace EonaCat.Network [Serializable] public class HttpListenerException : Win32Exception { - /// - /// Initializes a new instance of the class. - /// - protected HttpListenerException( - SerializationInfo serializationInfo, StreamingContext streamingContext) - : base(serializationInfo, streamingContext) - { - } - /// /// Initializes a new instance of the class with no error message. /// @@ -48,6 +39,14 @@ namespace EonaCat.Network { } + /// + /// Initializes a new instance of the class. + /// + protected HttpListenerException( + SerializationInfo serializationInfo, StreamingContext streamingContext) + : base(serializationInfo, streamingContext) + { + } /// /// Gets the Win32 error code associated with this exception. /// diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerPrefix.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerPrefix.cs index 0db7458..1270378 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerPrefix.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerPrefix.cs @@ -52,35 +52,6 @@ namespace EonaCat.Network /// public string Port { get; private set; } - private void parse(string uriPrefix) - { - if (uriPrefix.StartsWith("https")) - { - IsSecure = true; - } - - var len = uriPrefix.Length; - var startHost = uriPrefix.IndexOf(':') + 3; - var root = uriPrefix.IndexOf('/', startHost + 1, len - startHost - 1); - - var colon = uriPrefix.LastIndexOf(':', root - 1, root - startHost - 1); - if (uriPrefix[root - 1] != ']' && colon > startHost) - { - Host = uriPrefix.Substring(startHost, colon - startHost); - Port = uriPrefix.Substring(colon + 1, root - colon - 1); - } - else - { - Host = uriPrefix.Substring(startHost, root - startHost); - Port = IsSecure ? "443" : "80"; - } - - Path = uriPrefix.Substring(root); - - _prefix = - string.Format("http{0}://{1}:{2}{3}", IsSecure ? "s" : "", Host, Port, Path); - } - /// /// Checks if the specified URI prefix is valid. /// @@ -163,5 +134,34 @@ namespace EonaCat.Network { return _prefix; } + + private void parse(string uriPrefix) + { + if (uriPrefix.StartsWith("https")) + { + IsSecure = true; + } + + var len = uriPrefix.Length; + var startHost = uriPrefix.IndexOf(':') + 3; + var root = uriPrefix.IndexOf('/', startHost + 1, len - startHost - 1); + + var colon = uriPrefix.LastIndexOf(':', root - 1, root - startHost - 1); + if (uriPrefix[root - 1] != ']' && colon > startHost) + { + Host = uriPrefix.Substring(startHost, colon - startHost); + Port = uriPrefix.Substring(colon + 1, root - colon - 1); + } + else + { + Host = uriPrefix.Substring(startHost, root - startHost); + Port = IsSecure ? "443" : "80"; + } + + Path = uriPrefix.Substring(root); + + _prefix = + string.Format("http{0}://{1}:{2}{3}", IsSecure ? "s" : "", Host, Port, Path); + } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerPrefixCollection.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerPrefixCollection.cs index 0749aa7..5204118 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerPrefixCollection.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerPrefixCollection.cs @@ -120,6 +120,15 @@ namespace EonaCat.Network return _prefixes.GetEnumerator(); } + /// + /// Returns an enumerator that iterates through the collection. + /// + /// An enumerator that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + return _prefixes.GetEnumerator(); + } + /// /// Removes the specified URI prefix from the collection. /// @@ -141,14 +150,5 @@ namespace EonaCat.Network return ret; } - - /// - /// Returns an enumerator that iterates through the collection. - /// - /// An enumerator that can be used to iterate through the collection. - IEnumerator IEnumerable.GetEnumerator() - { - return _prefixes.GetEnumerator(); - } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerRequest.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerRequest.cs index 50753c3..78f691d 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerRequest.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerRequest.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; using System.IO; -using System.Security.Cryptography.X509Certificates; using System.Text; namespace EonaCat.Network @@ -17,12 +16,12 @@ namespace EonaCat.Network public sealed class HttpListenerRequest { private static readonly byte[] _100continue; + private readonly HttpListenerContext _context; + private readonly WebHeaderCollection _headers; private bool _chunked; private Encoding _contentEncoding; private bool _contentLengthSet; - private readonly HttpListenerContext _context; private CookieCollection _cookies; - private readonly WebHeaderCollection _headers; private Stream _inputStream; private bool _keepAlive; private bool _keepAliveSet; @@ -162,7 +161,7 @@ namespace EonaCat.Network /// /// Gets the query string in the request. /// - public NameValueCollection QueryString => _queryString ??= HttpUtility.InternalParseQueryString(Url.Query, Encoding.UTF8); + public NameValueCollection QueryString => _queryString ??= HttpUtility.InternalParseQueryString(Url.Query, ContentEncoding); /// /// Gets the raw URL of the request. @@ -209,18 +208,13 @@ namespace EonaCat.Network /// public string[] UserLanguages { get; private set; } - private static bool tryCreateVersion(string version, out Version result) + public override string ToString() { - try - { - result = new Version(version); - return true; - } - catch - { - result = null; - return false; - } + var buff = new StringBuilder(64); + buff.AppendFormat("{0} {1} HTTP/{2}\r\n", HttpMethod, _uri, _version); + buff.Append(_headers.ToString()); + + return buff.ToString(); } internal void AddHeader(string header) @@ -404,13 +398,18 @@ namespace EonaCat.Network } } - public override string ToString() + private static bool tryCreateVersion(string version, out Version result) { - var buff = new StringBuilder(64); - buff.AppendFormat("{0} {1} HTTP/{2}\r\n", HttpMethod, _uri, _version); - buff.Append(_headers.ToString()); - - return buff.ToString(); + try + { + result = new Version(version); + return true; + } + catch + { + result = null; + return false; + } } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerResponse.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerResponse.cs index ef7372d..d99e046 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerResponse.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerResponse.cs @@ -1,12 +1,10 @@ // This file is part of the EonaCat project(s) which is released under the Apache License. // See the LICENSE file or go to https://EonaCat.com/License for full license details. -using EonaCat.Logger; using System; using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Reflection; using System.Text; namespace EonaCat.Network @@ -16,10 +14,10 @@ namespace EonaCat.Network /// public sealed class HttpListenerResponse : IDisposable { + private readonly HttpListenerContext _context; private Encoding _contentEncoding; private long _contentLength; private string _contentType; - private readonly HttpListenerContext _context; private CookieCollection _cookies; private bool _disposed; private WebHeaderCollection _headers; diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpStreamAsyncResult.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpStreamAsyncResult.cs index 4139de1..8f25045 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpStreamAsyncResult.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpStreamAsyncResult.cs @@ -9,8 +9,8 @@ namespace EonaCat.Network internal class HttpStreamAsyncResult : IAsyncResult { private readonly AsyncCallback _callback; - private bool _isCompleted; private readonly object _sync; + private bool _isCompleted; private ManualResetEvent _waitHandle; internal HttpStreamAsyncResult(AsyncCallback callback, object state) @@ -20,6 +20,30 @@ namespace EonaCat.Network _sync = new object(); } + public object AsyncState { get; } + public WaitHandle AsyncWaitHandle + { + get + { + lock (_sync) + { + return _waitHandle ??= new ManualResetEvent(_isCompleted); + } + } + } + + public bool CompletedSynchronously => SyncRead == Count; + public bool IsCompleted + { + get + { + lock (_sync) + { + return _isCompleted; + } + } + } + internal byte[] Buffer { get; set; } internal int Count { get; set; } @@ -31,33 +55,6 @@ namespace EonaCat.Network internal int Offset { get; set; } internal int SyncRead { get; set; } - - public object AsyncState { get; } - - public WaitHandle AsyncWaitHandle - { - get - { - lock (_sync) - { - return _waitHandle ??= new ManualResetEvent(_isCompleted); - } - } - } - - public bool CompletedSynchronously => SyncRead == Count; - - public bool IsCompleted - { - get - { - lock (_sync) - { - return _isCompleted; - } - } - } - internal void Complete() { lock (_sync) diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpUtility.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpUtility.cs index d3a1bf9..a37f4b5 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpUtility.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpUtility.cs @@ -14,777 +14,9 @@ namespace EonaCat.Network { internal sealed class HttpUtility { - private static Dictionary _entities; private static readonly char[] _hexChars = "0123456789abcdef".ToCharArray(); private static readonly object _sync = new object(); - - private static int getChar(byte[] bytes, int offset, int length) - { - var val = 0; - var end = length + offset; - for (var i = offset; i < end; i++) - { - var current = getInt(bytes[i]); - if (current == -1) - { - return -1; - } - - val = (val << 4) + current; - } - - return val; - } - - private static int getChar(string s, int offset, int length) - { - var val = 0; - var end = length + offset; - for (var i = offset; i < end; i++) - { - var c = s[i]; - if (c > 127) - { - return -1; - } - - var current = getInt((byte)c); - if (current == -1) - { - return -1; - } - - val = (val << 4) + current; - } - - return val; - } - - private static char[] getChars(MemoryStream buffer, Encoding encoding) - { - return encoding.GetChars(buffer.GetBuffer(), 0, (int)buffer.Length); - } - - private static Dictionary getEntities() - { - lock (_sync) - { - if (_entities == null) - { - GenerateHtmlEntityRefrencesDictionary(); - } - - return _entities; - } - } - - private static int getInt(byte b) - { - var c = (char)b; - return c >= '0' && c <= '9' - ? c - '0' - : c >= 'a' && c <= 'f' - ? c - 'a' + 10 - : c >= 'A' && c <= 'F' - ? c - 'A' + 10 - : -1; - } - - private static void GenerateHtmlEntityRefrencesDictionary() - { - // Build the dictionary of HTML entity references. - _entities = new Dictionary - { - { "nbsp", '\u00A0' }, - { "iexcl", '\u00A1' }, - { "cent", '\u00A2' }, - { "pound", '\u00A3' }, - { "curren", '\u00A4' }, - { "yen", '\u00A5' }, - { "brvbar", '\u00A6' }, - { "sect", '\u00A7' }, - { "uml", '\u00A8' }, - { "copy", '\u00A9' }, - { "ordf", '\u00AA' }, - { "laquo", '\u00AB' }, - { "not", '\u00AC' }, - { "shy", '\u00AD' }, - { "reg", '\u00AE' }, - { "macr", '\u00AF' }, - { "deg", '\u00B0' }, - { "plusmn", '\u00B1' }, - { "sup2", '\u00B2' }, - { "sup3", '\u00B3' }, - { "acute", '\u00B4' }, - { "micro", '\u00B5' }, - { "para", '\u00B6' }, - { "middot", '\u00B7' }, - { "cedil", '\u00B8' }, - { "sup1", '\u00B9' }, - { "ordm", '\u00BA' }, - { "raquo", '\u00BB' }, - { "frac14", '\u00BC' }, - { "frac12", '\u00BD' }, - { "frac34", '\u00BE' }, - { "iquest", '\u00BF' }, - { "Agrave", '\u00C0' }, - { "Aacute", '\u00C1' }, - { "Acirc", '\u00C2' }, - { "Atilde", '\u00C3' }, - { "Auml", '\u00C4' }, - { "Aring", '\u00C5' }, - { "AElig", '\u00C6' }, - { "Ccedil", '\u00C7' }, - { "Egrave", '\u00C8' }, - { "Eacute", '\u00C9' }, - { "Ecirc", '\u00CA' }, - { "Euml", '\u00CB' }, - { "Igrave", '\u00CC' }, - { "Iacute", '\u00CD' }, - { "Icirc", '\u00CE' }, - { "Iuml", '\u00CF' }, - { "ETH", '\u00D0' }, - { "Ntilde", '\u00D1' }, - { "Ograve", '\u00D2' }, - { "Oacute", '\u00D3' }, - { "Ocirc", '\u00D4' }, - { "Otilde", '\u00D5' }, - { "Ouml", '\u00D6' }, - { "times", '\u00D7' }, - { "Oslash", '\u00D8' }, - { "Ugrave", '\u00D9' }, - { "Uacute", '\u00DA' }, - { "Ucirc", '\u00DB' }, - { "Uuml", '\u00DC' }, - { "Yacute", '\u00DD' }, - { "THORN", '\u00DE' }, - { "szlig", '\u00DF' }, - { "agrave", '\u00E0' }, - { "aacute", '\u00E1' }, - { "acirc", '\u00E2' }, - { "atilde", '\u00E3' }, - { "auml", '\u00E4' }, - { "aring", '\u00E5' }, - { "aelig", '\u00E6' }, - { "ccedil", '\u00E7' }, - { "egrave", '\u00E8' }, - { "eacute", '\u00E9' }, - { "ecirc", '\u00EA' }, - { "euml", '\u00EB' }, - { "igrave", '\u00EC' }, - { "iacute", '\u00ED' }, - { "icirc", '\u00EE' }, - { "iuml", '\u00EF' }, - { "eth", '\u00F0' }, - { "ntilde", '\u00F1' }, - { "ograve", '\u00F2' }, - { "oacute", '\u00F3' }, - { "ocirc", '\u00F4' }, - { "otilde", '\u00F5' }, - { "ouml", '\u00F6' }, - { "divide", '\u00F7' }, - { "oslash", '\u00F8' }, - { "ugrave", '\u00F9' }, - { "uacute", '\u00FA' }, - { "ucirc", '\u00FB' }, - { "uuml", '\u00FC' }, - { "yacute", '\u00FD' }, - { "thorn", '\u00FE' }, - { "yuml", '\u00FF' }, - { "fnof", '\u0192' }, - { "Alpha", '\u0391' }, - { "Beta", '\u0392' }, - { "Gamma", '\u0393' }, - { "Delta", '\u0394' }, - { "Epsilon", '\u0395' }, - { "Zeta", '\u0396' }, - { "Eta", '\u0397' }, - { "Theta", '\u0398' }, - { "Iota", '\u0399' }, - { "Kappa", '\u039A' }, - { "Lambda", '\u039B' }, - { "Mu", '\u039C' }, - { "Nu", '\u039D' }, - { "Xi", '\u039E' }, - { "Omicron", '\u039F' }, - { "Pi", '\u03A0' }, - { "Rho", '\u03A1' }, - { "Sigma", '\u03A3' }, - { "Tau", '\u03A4' }, - { "Upsilon", '\u03A5' }, - { "Phi", '\u03A6' }, - { "Chi", '\u03A7' }, - { "Psi", '\u03A8' }, - { "Omega", '\u03A9' }, - { "alpha", '\u03B1' }, - { "beta", '\u03B2' }, - { "gamma", '\u03B3' }, - { "delta", '\u03B4' }, - { "epsilon", '\u03B5' }, - { "zeta", '\u03B6' }, - { "eta", '\u03B7' }, - { "theta", '\u03B8' }, - { "iota", '\u03B9' }, - { "kappa", '\u03BA' }, - { "lambda", '\u03BB' }, - { "mu", '\u03BC' }, - { "nu", '\u03BD' }, - { "xi", '\u03BE' }, - { "omicron", '\u03BF' }, - { "pi", '\u03C0' }, - { "rho", '\u03C1' }, - { "sigmaf", '\u03C2' }, - { "sigma", '\u03C3' }, - { "tau", '\u03C4' }, - { "upsilon", '\u03C5' }, - { "phi", '\u03C6' }, - { "chi", '\u03C7' }, - { "psi", '\u03C8' }, - { "omega", '\u03C9' }, - { "thetasym", '\u03D1' }, - { "upsih", '\u03D2' }, - { "piv", '\u03D6' }, - { "bull", '\u2022' }, - { "hellip", '\u2026' }, - { "prime", '\u2032' }, - { "Prime", '\u2033' }, - { "oline", '\u203E' }, - { "frasl", '\u2044' }, - { "weierp", '\u2118' }, - { "image", '\u2111' }, - { "real", '\u211C' }, - { "trade", '\u2122' }, - { "alefsym", '\u2135' }, - { "larr", '\u2190' }, - { "uarr", '\u2191' }, - { "rarr", '\u2192' }, - { "darr", '\u2193' }, - { "harr", '\u2194' }, - { "crarr", '\u21B5' }, - { "lArr", '\u21D0' }, - { "uArr", '\u21D1' }, - { "rArr", '\u21D2' }, - { "dArr", '\u21D3' }, - { "hArr", '\u21D4' }, - { "forall", '\u2200' }, - { "part", '\u2202' }, - { "exist", '\u2203' }, - { "empty", '\u2205' }, - { "nabla", '\u2207' }, - { "isin", '\u2208' }, - { "notin", '\u2209' }, - { "ni", '\u220B' }, - { "prod", '\u220F' }, - { "sum", '\u2211' }, - { "minus", '\u2212' }, - { "lowast", '\u2217' }, - { "radic", '\u221A' }, - { "prop", '\u221D' }, - { "infin", '\u221E' }, - { "ang", '\u2220' }, - { "and", '\u2227' }, - { "or", '\u2228' }, - { "cap", '\u2229' }, - { "cup", '\u222A' }, - { "int", '\u222B' }, - { "there4", '\u2234' }, - { "sim", '\u223C' }, - { "cong", '\u2245' }, - { "asymp", '\u2248' }, - { "ne", '\u2260' }, - { "equiv", '\u2261' }, - { "le", '\u2264' }, - { "ge", '\u2265' }, - { "sub", '\u2282' }, - { "sup", '\u2283' }, - { "nsub", '\u2284' }, - { "sube", '\u2286' }, - { "supe", '\u2287' }, - { "oplus", '\u2295' }, - { "otimes", '\u2297' }, - { "perp", '\u22A5' }, - { "sdot", '\u22C5' }, - { "lceil", '\u2308' }, - { "rceil", '\u2309' }, - { "lfloor", '\u230A' }, - { "rfloor", '\u230B' }, - { "lang", '\u2329' }, - { "rang", '\u232A' }, - { "loz", '\u25CA' }, - { "spades", '\u2660' }, - { "clubs", '\u2663' }, - { "hearts", '\u2665' }, - { "diams", '\u2666' }, - { "quot", '\u0022' }, - { "amp", '\u0026' }, - { "lt", '\u003C' }, - { "gt", '\u003E' }, - { "OElig", '\u0152' }, - { "oelig", '\u0153' }, - { "Scaron", '\u0160' }, - { "scaron", '\u0161' }, - { "Yuml", '\u0178' }, - { "circ", '\u02C6' }, - { "tilde", '\u02DC' }, - { "ensp", '\u2002' }, - { "emsp", '\u2003' }, - { "thinsp", '\u2009' }, - { "zwnj", '\u200C' }, - { "zwj", '\u200D' }, - { "lrm", '\u200E' }, - { "rlm", '\u200F' }, - { "ndash", '\u2013' }, - { "mdash", '\u2014' }, - { "lsquo", '\u2018' }, - { "rsquo", '\u2019' }, - { "sbquo", '\u201A' }, - { "ldquo", '\u201C' }, - { "rdquo", '\u201D' }, - { "bdquo", '\u201E' }, - { "dagger", '\u2020' }, - { "Dagger", '\u2021' }, - { "permil", '\u2030' }, - { "lsaquo", '\u2039' }, - { "rsaquo", '\u203A' }, - { "euro", '\u20AC' } - }; - } - - private static bool notEncoded(char c) - { - return c == '!' || - c == '\'' || - c == '(' || - c == ')' || - c == '*' || - c == '-' || - c == '.' || - c == '_'; - } - - private static void urlEncode(char c, Stream result, bool unicode) - { - if (c > 255) - { - result.WriteByte((byte)'%'); - result.WriteByte((byte)'u'); - - var i = (int)c; - var idx = i >> 12; - result.WriteByte((byte)_hexChars[idx]); - - idx = (i >> 8) & 0x0F; - result.WriteByte((byte)_hexChars[idx]); - - idx = (i >> 4) & 0x0F; - result.WriteByte((byte)_hexChars[idx]); - - idx = i & 0x0F; - result.WriteByte((byte)_hexChars[idx]); - - return; - } - - if (c > ' ' && notEncoded(c)) - { - result.WriteByte((byte)c); - return; - } - - if (c == ' ') - { - result.WriteByte((byte)'+'); - return; - } - - if ((c < '0') || - (c < 'A' && c > '9') || - (c > 'Z' && c < 'a') || - (c > 'z')) - { - if (unicode && c > 127) - { - result.WriteByte((byte)'%'); - result.WriteByte((byte)'u'); - result.WriteByte((byte)'0'); - result.WriteByte((byte)'0'); - } - else - { - result.WriteByte((byte)'%'); - } - - var i = (int)c; - var idx = i >> 4; - result.WriteByte((byte)_hexChars[idx]); - - idx = i & 0x0F; - result.WriteByte((byte)_hexChars[idx]); - - return; - } - - result.WriteByte((byte)c); - } - - private static void urlPathEncode(char c, Stream result) - { - if (c < 33 || c > 126) - { - var bytes = Encoding.UTF8.GetBytes(c.ToString()); - foreach (var b in bytes) - { - result.WriteByte((byte)'%'); - - var i = (int)b; - var idx = i >> 4; - result.WriteByte((byte)_hexChars[idx]); - - idx = i & 0x0F; - result.WriteByte((byte)_hexChars[idx]); - } - - return; - } - - if (c == ' ') - { - result.WriteByte((byte)'%'); - result.WriteByte((byte)'2'); - result.WriteByte((byte)'0'); - - return; - } - - result.WriteByte((byte)c); - } - - private static void writeCharBytes(char c, IList buffer, Encoding encoding) - { - if (c > 255) - { - foreach (var b in encoding.GetBytes(new[] { c })) - { - buffer.Add(b); - } - - return; - } - - buffer.Add((byte)c); - } - - internal static Uri CreateRequestUrl( - string requestUri, string host, bool websocketRequest, bool secure) - { - if (requestUri == null || requestUri.Length == 0 || host == null || host.Length == 0) - { - return null; - } - - string schm = null; - string path = null; - if (requestUri.StartsWith("/")) - { - path = requestUri; - } - else if (requestUri.MaybeUri()) - { - var valid = Uri.TryCreate(requestUri, UriKind.Absolute, out Uri uri) && - (((schm = uri.Scheme).StartsWith("http") && !websocketRequest) || - (schm.StartsWith("ws") && websocketRequest)); - - if (!valid) - { - return null; - } - - host = uri.Authority; - path = uri.PathAndQuery; - } - else if (requestUri == "*") - { - } - else - { - // As authority form - host = requestUri; - } - - schm ??= (websocketRequest ? "ws" : "http") + (secure ? "s" : string.Empty); - - var colon = host.IndexOf(':'); - if (colon == -1) - { - host = string.Format("{0}:{1}", host, schm == "http" || schm == "ws" ? 80 : 443); - } - - var url = string.Format("{0}://{1}{2}", schm, host, path); - - if (!Uri.TryCreate(url, UriKind.Absolute, out Uri res)) - { - return null; - } - - return res; - } - - internal static IPrincipal CreateUser( - string response, - AuthenticationSchemes scheme, - string realm, - string method, - Func credentialsFinder - ) - { - if (response == null || response.Length == 0) - { - return null; - } - - if (credentialsFinder == null) - { - return null; - } - - if (!(scheme == AuthenticationSchemes.Basic || scheme == AuthenticationSchemes.Digest)) - { - return null; - } - - if (scheme == AuthenticationSchemes.Digest) - { - if (realm == null || realm.Length == 0) - { - return null; - } - - if (method == null || method.Length == 0) - { - return null; - } - } - - if (!response.StartsWith(scheme.ToString(), StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - var res = AuthenticationResponse.Parse(response); - if (res == null) - { - return null; - } - - var id = res.ToIdentity(); - if (id == null) - { - return null; - } - - NetworkCredential cred = null; - try - { - cred = credentialsFinder(id); - } - catch - { - } - - if (cred == null) - { - return null; - } - - if (scheme == AuthenticationSchemes.Basic - && ((HttpBasicIdentity)id).Password != cred.Password - ) - { - return null; - } - - if (scheme == AuthenticationSchemes.Digest - && !((HttpDigestIdentity)id).IsValid(cred.Password, realm, method, null) - ) - { - return null; - } - - return new GenericPrincipal(id, cred.Roles); - } - - internal static Encoding GetEncoding(string contentType) - { - var parts = contentType.Split(';'); - foreach (var p in parts) - { - var part = p.Trim(); - if (part.StartsWith("charset", StringComparison.OrdinalIgnoreCase)) - { - return Encoding.GetEncoding(part.GetValue('=', true)); - } - } - - return null; - } - - internal static NameValueCollection InternalParseQueryString(string query, Encoding encoding) - { - int len; - if (query == null || (len = query.Length) == 0 || (len == 1 && query[0] == '?')) - { - return new NameValueCollection(1); - } - - if (query[0] == '?') - { - query = query.Substring(1); - } - - var res = new QueryStringCollection(); - var components = query.Split('&'); - foreach (var component in components) - { - var i = component.IndexOf('='); - if (i > -1) - { - var name = UrlDecode(component.Substring(0, i), encoding); - var val = component.Length > i + 1 - ? UrlDecode(component.Substring(i + 1), encoding) - : string.Empty; - - res.Add(name, val); - } - else - { - res.Add(null, UrlDecode(component, encoding)); - } - } - - return res; - } - - internal static string InternalUrlDecode( - byte[] bytes, int offset, int count, Encoding encoding) - { - var output = new StringBuilder(); - using (var acc = new MemoryStream()) - { - var end = count + offset; - for (var i = offset; i < end; i++) - { - if (bytes[i] == '%' && i + 2 < count && bytes[i + 1] != '%') - { - int xchar; - if (bytes[i + 1] == (byte)'u' && i + 5 < end) - { - if (acc.Length > 0) - { - output.Append(getChars(acc, encoding)); - acc.SetLength(0); - } - - xchar = getChar(bytes, i + 2, 4); - if (xchar != -1) - { - output.Append((char)xchar); - i += 5; - - continue; - } - } - else if ((xchar = getChar(bytes, i + 1, 2)) != -1) - { - acc.WriteByte((byte)xchar); - i += 2; - - continue; - } - } - - if (acc.Length > 0) - { - output.Append(getChars(acc, encoding)); - acc.SetLength(0); - } - - if (bytes[i] == '+') - { - output.Append(' '); - continue; - } - - output.Append((char)bytes[i]); - } - - if (acc.Length > 0) - { - output.Append(getChars(acc, encoding)); - } - } - - return output.ToString(); - } - - internal static byte[] InternalUrlDecodeToBytes(byte[] bytes, int offset, int count) - { - using (var res = new MemoryStream()) - { - var end = offset + count; - for (var i = offset; i < end; i++) - { - var c = (char)bytes[i]; - if (c == '+') - { - c = ' '; - } - else if (c == '%' && i < end - 2) - { - var xchar = getChar(bytes, i + 1, 2); - if (xchar != -1) - { - c = (char)xchar; - i += 2; - } - } - - res.WriteByte((byte)c); - } - - res.Close(); - return res.ToArray(); - } - } - - internal static byte[] InternalUrlEncodeToBytes(byte[] bytes, int offset, int count) - { - using (var res = new MemoryStream()) - { - var end = offset + count; - for (var i = offset; i < end; i++) - { - urlEncode((char)bytes[i], res, false); - } - - res.Close(); - return res.ToArray(); - } - } - - internal static byte[] InternalUrlEncodeUnicodeToBytes(string s) - { - using (var res = new MemoryStream()) - { - foreach (var c in s) - { - urlEncode(c, res, true); - } - - res.Close(); - return res.ToArray(); - } - } - + private static Dictionary _entities; public static string HtmlAttributeEncode(string s) { if (s == null || s.Length == 0 || !s.Contains('&', '"', '<', '>')) @@ -1359,5 +591,771 @@ namespace EonaCat.Network return Encoding.ASCII.GetString(res.ToArray()); } } + + internal static Uri CreateRequestUrl( + string requestUri, string host, bool websocketRequest, bool secure) + { + if (requestUri == null || requestUri.Length == 0 || host == null || host.Length == 0) + { + return null; + } + + string schm = null; + string path = null; + if (requestUri.StartsWith("/")) + { + path = requestUri; + } + else if (requestUri.MaybeUri()) + { + var valid = Uri.TryCreate(requestUri, UriKind.Absolute, out Uri uri) && + (((schm = uri.Scheme).StartsWith("http") && !websocketRequest) || + (schm.StartsWith("ws") && websocketRequest)); + + if (!valid) + { + return null; + } + + host = uri.Authority; + path = uri.PathAndQuery; + } + else if (requestUri == "*") + { + } + else + { + // As authority form + host = requestUri; + } + + schm ??= (websocketRequest ? "ws" : "http") + (secure ? "s" : string.Empty); + + var colon = host.IndexOf(':'); + if (colon == -1) + { + host = string.Format("{0}:{1}", host, schm == "http" || schm == "ws" ? 80 : 443); + } + + var url = string.Format("{0}://{1}{2}", schm, host, path); + + if (!Uri.TryCreate(url, UriKind.Absolute, out Uri res)) + { + return null; + } + + return res; + } + + internal static IPrincipal CreateUser( + string response, + AuthenticationSchemes scheme, + string realm, + string method, + Func credentialsFinder + ) + { + if (response == null || response.Length == 0) + { + return null; + } + + if (credentialsFinder == null) + { + return null; + } + + if (!(scheme == AuthenticationSchemes.Basic || scheme == AuthenticationSchemes.Digest)) + { + return null; + } + + if (scheme == AuthenticationSchemes.Digest) + { + if (realm == null || realm.Length == 0) + { + return null; + } + + if (method == null || method.Length == 0) + { + return null; + } + } + + if (!response.StartsWith(scheme.ToString(), StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + var res = AuthenticationResponse.Parse(response); + if (res == null) + { + return null; + } + + var id = res.ToIdentity(); + if (id == null) + { + return null; + } + + NetworkCredential cred = null; + try + { + cred = credentialsFinder(id); + } + catch + { + } + + if (cred == null) + { + return null; + } + + if (scheme == AuthenticationSchemes.Basic + && ((HttpBasicIdentity)id).Password != cred.Password + ) + { + return null; + } + + if (scheme == AuthenticationSchemes.Digest + && !((HttpDigestIdentity)id).IsValid(cred.Password, realm, method, null) + ) + { + return null; + } + + return new GenericPrincipal(id, cred.Roles); + } + + internal static Encoding GetEncoding(string contentType) + { + var parts = contentType.Split(';'); + foreach (var p in parts) + { + var part = p.Trim(); + if (part.StartsWith("charset", StringComparison.OrdinalIgnoreCase)) + { + return Encoding.GetEncoding(part.GetValue('=', true)); + } + } + + return null; + } + + internal static NameValueCollection InternalParseQueryString(string query, Encoding encoding) + { + int len; + if (query == null || (len = query.Length) == 0 || (len == 1 && query[0] == '?')) + { + return new NameValueCollection(1); + } + + if (query[0] == '?') + { + query = query.Substring(1); + } + + var res = new QueryStringCollection(); + var components = query.Split('&'); + foreach (var component in components) + { + var i = component.IndexOf('='); + if (i > -1) + { + var name = UrlDecode(component.Substring(0, i), encoding); + var val = component.Length > i + 1 + ? UrlDecode(component.Substring(i + 1), encoding) + : string.Empty; + + res.Add(name, val); + } + else + { + res.Add(null, UrlDecode(component, encoding)); + } + } + + return res; + } + + internal static string InternalUrlDecode( + byte[] bytes, int offset, int count, Encoding encoding) + { + var output = new StringBuilder(); + using (var acc = new MemoryStream()) + { + var end = count + offset; + for (var i = offset; i < end; i++) + { + if (bytes[i] == '%' && i + 2 < count && bytes[i + 1] != '%') + { + int xchar; + if (bytes[i + 1] == (byte)'u' && i + 5 < end) + { + if (acc.Length > 0) + { + output.Append(getChars(acc, encoding)); + acc.SetLength(0); + } + + xchar = getChar(bytes, i + 2, 4); + if (xchar != -1) + { + output.Append((char)xchar); + i += 5; + + continue; + } + } + else if ((xchar = getChar(bytes, i + 1, 2)) != -1) + { + acc.WriteByte((byte)xchar); + i += 2; + + continue; + } + } + + if (acc.Length > 0) + { + output.Append(getChars(acc, encoding)); + acc.SetLength(0); + } + + if (bytes[i] == '+') + { + output.Append(' '); + continue; + } + + output.Append((char)bytes[i]); + } + + if (acc.Length > 0) + { + output.Append(getChars(acc, encoding)); + } + } + + return output.ToString(); + } + + internal static byte[] InternalUrlDecodeToBytes(byte[] bytes, int offset, int count) + { + using (var res = new MemoryStream()) + { + var end = offset + count; + for (var i = offset; i < end; i++) + { + var c = (char)bytes[i]; + if (c == '+') + { + c = ' '; + } + else if (c == '%' && i < end - 2) + { + var xchar = getChar(bytes, i + 1, 2); + if (xchar != -1) + { + c = (char)xchar; + i += 2; + } + } + + res.WriteByte((byte)c); + } + + res.Close(); + return res.ToArray(); + } + } + + internal static byte[] InternalUrlEncodeToBytes(byte[] bytes, int offset, int count) + { + using (var res = new MemoryStream()) + { + var end = offset + count; + for (var i = offset; i < end; i++) + { + urlEncode((char)bytes[i], res, false); + } + + res.Close(); + return res.ToArray(); + } + } + + internal static byte[] InternalUrlEncodeUnicodeToBytes(string s) + { + using (var res = new MemoryStream()) + { + foreach (var c in s) + { + urlEncode(c, res, true); + } + + res.Close(); + return res.ToArray(); + } + } + + private static void GenerateHtmlEntityRefrencesDictionary() + { + // Build the dictionary of HTML entity references. + _entities = new Dictionary + { + { "nbsp", '\u00A0' }, + { "iexcl", '\u00A1' }, + { "cent", '\u00A2' }, + { "pound", '\u00A3' }, + { "curren", '\u00A4' }, + { "yen", '\u00A5' }, + { "brvbar", '\u00A6' }, + { "sect", '\u00A7' }, + { "uml", '\u00A8' }, + { "copy", '\u00A9' }, + { "ordf", '\u00AA' }, + { "laquo", '\u00AB' }, + { "not", '\u00AC' }, + { "shy", '\u00AD' }, + { "reg", '\u00AE' }, + { "macr", '\u00AF' }, + { "deg", '\u00B0' }, + { "plusmn", '\u00B1' }, + { "sup2", '\u00B2' }, + { "sup3", '\u00B3' }, + { "acute", '\u00B4' }, + { "micro", '\u00B5' }, + { "para", '\u00B6' }, + { "middot", '\u00B7' }, + { "cedil", '\u00B8' }, + { "sup1", '\u00B9' }, + { "ordm", '\u00BA' }, + { "raquo", '\u00BB' }, + { "frac14", '\u00BC' }, + { "frac12", '\u00BD' }, + { "frac34", '\u00BE' }, + { "iquest", '\u00BF' }, + { "Agrave", '\u00C0' }, + { "Aacute", '\u00C1' }, + { "Acirc", '\u00C2' }, + { "Atilde", '\u00C3' }, + { "Auml", '\u00C4' }, + { "Aring", '\u00C5' }, + { "AElig", '\u00C6' }, + { "Ccedil", '\u00C7' }, + { "Egrave", '\u00C8' }, + { "Eacute", '\u00C9' }, + { "Ecirc", '\u00CA' }, + { "Euml", '\u00CB' }, + { "Igrave", '\u00CC' }, + { "Iacute", '\u00CD' }, + { "Icirc", '\u00CE' }, + { "Iuml", '\u00CF' }, + { "ETH", '\u00D0' }, + { "Ntilde", '\u00D1' }, + { "Ograve", '\u00D2' }, + { "Oacute", '\u00D3' }, + { "Ocirc", '\u00D4' }, + { "Otilde", '\u00D5' }, + { "Ouml", '\u00D6' }, + { "times", '\u00D7' }, + { "Oslash", '\u00D8' }, + { "Ugrave", '\u00D9' }, + { "Uacute", '\u00DA' }, + { "Ucirc", '\u00DB' }, + { "Uuml", '\u00DC' }, + { "Yacute", '\u00DD' }, + { "THORN", '\u00DE' }, + { "szlig", '\u00DF' }, + { "agrave", '\u00E0' }, + { "aacute", '\u00E1' }, + { "acirc", '\u00E2' }, + { "atilde", '\u00E3' }, + { "auml", '\u00E4' }, + { "aring", '\u00E5' }, + { "aelig", '\u00E6' }, + { "ccedil", '\u00E7' }, + { "egrave", '\u00E8' }, + { "eacute", '\u00E9' }, + { "ecirc", '\u00EA' }, + { "euml", '\u00EB' }, + { "igrave", '\u00EC' }, + { "iacute", '\u00ED' }, + { "icirc", '\u00EE' }, + { "iuml", '\u00EF' }, + { "eth", '\u00F0' }, + { "ntilde", '\u00F1' }, + { "ograve", '\u00F2' }, + { "oacute", '\u00F3' }, + { "ocirc", '\u00F4' }, + { "otilde", '\u00F5' }, + { "ouml", '\u00F6' }, + { "divide", '\u00F7' }, + { "oslash", '\u00F8' }, + { "ugrave", '\u00F9' }, + { "uacute", '\u00FA' }, + { "ucirc", '\u00FB' }, + { "uuml", '\u00FC' }, + { "yacute", '\u00FD' }, + { "thorn", '\u00FE' }, + { "yuml", '\u00FF' }, + { "fnof", '\u0192' }, + { "Alpha", '\u0391' }, + { "Beta", '\u0392' }, + { "Gamma", '\u0393' }, + { "Delta", '\u0394' }, + { "Epsilon", '\u0395' }, + { "Zeta", '\u0396' }, + { "Eta", '\u0397' }, + { "Theta", '\u0398' }, + { "Iota", '\u0399' }, + { "Kappa", '\u039A' }, + { "Lambda", '\u039B' }, + { "Mu", '\u039C' }, + { "Nu", '\u039D' }, + { "Xi", '\u039E' }, + { "Omicron", '\u039F' }, + { "Pi", '\u03A0' }, + { "Rho", '\u03A1' }, + { "Sigma", '\u03A3' }, + { "Tau", '\u03A4' }, + { "Upsilon", '\u03A5' }, + { "Phi", '\u03A6' }, + { "Chi", '\u03A7' }, + { "Psi", '\u03A8' }, + { "Omega", '\u03A9' }, + { "alpha", '\u03B1' }, + { "beta", '\u03B2' }, + { "gamma", '\u03B3' }, + { "delta", '\u03B4' }, + { "epsilon", '\u03B5' }, + { "zeta", '\u03B6' }, + { "eta", '\u03B7' }, + { "theta", '\u03B8' }, + { "iota", '\u03B9' }, + { "kappa", '\u03BA' }, + { "lambda", '\u03BB' }, + { "mu", '\u03BC' }, + { "nu", '\u03BD' }, + { "xi", '\u03BE' }, + { "omicron", '\u03BF' }, + { "pi", '\u03C0' }, + { "rho", '\u03C1' }, + { "sigmaf", '\u03C2' }, + { "sigma", '\u03C3' }, + { "tau", '\u03C4' }, + { "upsilon", '\u03C5' }, + { "phi", '\u03C6' }, + { "chi", '\u03C7' }, + { "psi", '\u03C8' }, + { "omega", '\u03C9' }, + { "thetasym", '\u03D1' }, + { "upsih", '\u03D2' }, + { "piv", '\u03D6' }, + { "bull", '\u2022' }, + { "hellip", '\u2026' }, + { "prime", '\u2032' }, + { "Prime", '\u2033' }, + { "oline", '\u203E' }, + { "frasl", '\u2044' }, + { "weierp", '\u2118' }, + { "image", '\u2111' }, + { "real", '\u211C' }, + { "trade", '\u2122' }, + { "alefsym", '\u2135' }, + { "larr", '\u2190' }, + { "uarr", '\u2191' }, + { "rarr", '\u2192' }, + { "darr", '\u2193' }, + { "harr", '\u2194' }, + { "crarr", '\u21B5' }, + { "lArr", '\u21D0' }, + { "uArr", '\u21D1' }, + { "rArr", '\u21D2' }, + { "dArr", '\u21D3' }, + { "hArr", '\u21D4' }, + { "forall", '\u2200' }, + { "part", '\u2202' }, + { "exist", '\u2203' }, + { "empty", '\u2205' }, + { "nabla", '\u2207' }, + { "isin", '\u2208' }, + { "notin", '\u2209' }, + { "ni", '\u220B' }, + { "prod", '\u220F' }, + { "sum", '\u2211' }, + { "minus", '\u2212' }, + { "lowast", '\u2217' }, + { "radic", '\u221A' }, + { "prop", '\u221D' }, + { "infin", '\u221E' }, + { "ang", '\u2220' }, + { "and", '\u2227' }, + { "or", '\u2228' }, + { "cap", '\u2229' }, + { "cup", '\u222A' }, + { "int", '\u222B' }, + { "there4", '\u2234' }, + { "sim", '\u223C' }, + { "cong", '\u2245' }, + { "asymp", '\u2248' }, + { "ne", '\u2260' }, + { "equiv", '\u2261' }, + { "le", '\u2264' }, + { "ge", '\u2265' }, + { "sub", '\u2282' }, + { "sup", '\u2283' }, + { "nsub", '\u2284' }, + { "sube", '\u2286' }, + { "supe", '\u2287' }, + { "oplus", '\u2295' }, + { "otimes", '\u2297' }, + { "perp", '\u22A5' }, + { "sdot", '\u22C5' }, + { "lceil", '\u2308' }, + { "rceil", '\u2309' }, + { "lfloor", '\u230A' }, + { "rfloor", '\u230B' }, + { "lang", '\u2329' }, + { "rang", '\u232A' }, + { "loz", '\u25CA' }, + { "spades", '\u2660' }, + { "clubs", '\u2663' }, + { "hearts", '\u2665' }, + { "diams", '\u2666' }, + { "quot", '\u0022' }, + { "amp", '\u0026' }, + { "lt", '\u003C' }, + { "gt", '\u003E' }, + { "OElig", '\u0152' }, + { "oelig", '\u0153' }, + { "Scaron", '\u0160' }, + { "scaron", '\u0161' }, + { "Yuml", '\u0178' }, + { "circ", '\u02C6' }, + { "tilde", '\u02DC' }, + { "ensp", '\u2002' }, + { "emsp", '\u2003' }, + { "thinsp", '\u2009' }, + { "zwnj", '\u200C' }, + { "zwj", '\u200D' }, + { "lrm", '\u200E' }, + { "rlm", '\u200F' }, + { "ndash", '\u2013' }, + { "mdash", '\u2014' }, + { "lsquo", '\u2018' }, + { "rsquo", '\u2019' }, + { "sbquo", '\u201A' }, + { "ldquo", '\u201C' }, + { "rdquo", '\u201D' }, + { "bdquo", '\u201E' }, + { "dagger", '\u2020' }, + { "Dagger", '\u2021' }, + { "permil", '\u2030' }, + { "lsaquo", '\u2039' }, + { "rsaquo", '\u203A' }, + { "euro", '\u20AC' } + }; + } + + private static int getChar(byte[] bytes, int offset, int length) + { + var val = 0; + var end = length + offset; + for (var i = offset; i < end; i++) + { + var current = getInt(bytes[i]); + if (current == -1) + { + return -1; + } + + val = (val << 4) + current; + } + + return val; + } + + private static int getChar(string s, int offset, int length) + { + var val = 0; + var end = length + offset; + for (var i = offset; i < end; i++) + { + var c = s[i]; + if (c > 127) + { + return -1; + } + + var current = getInt((byte)c); + if (current == -1) + { + return -1; + } + + val = (val << 4) + current; + } + + return val; + } + + private static char[] getChars(MemoryStream buffer, Encoding encoding) + { + return encoding.GetChars(buffer.GetBuffer(), 0, (int)buffer.Length); + } + + private static Dictionary getEntities() + { + lock (_sync) + { + if (_entities == null) + { + GenerateHtmlEntityRefrencesDictionary(); + } + + return _entities; + } + } + + private static int getInt(byte b) + { + var c = (char)b; + return c >= '0' && c <= '9' + ? c - '0' + : c >= 'a' && c <= 'f' + ? c - 'a' + 10 + : c >= 'A' && c <= 'F' + ? c - 'A' + 10 + : -1; + } + private static bool notEncoded(char c) + { + return c == '!' || + c == '\'' || + c == '(' || + c == ')' || + c == '*' || + c == '-' || + c == '.' || + c == '_'; + } + + private static void urlEncode(char c, Stream result, bool unicode) + { + if (c > 255) + { + result.WriteByte((byte)'%'); + result.WriteByte((byte)'u'); + + var i = (int)c; + var idx = i >> 12; + result.WriteByte((byte)_hexChars[idx]); + + idx = (i >> 8) & 0x0F; + result.WriteByte((byte)_hexChars[idx]); + + idx = (i >> 4) & 0x0F; + result.WriteByte((byte)_hexChars[idx]); + + idx = i & 0x0F; + result.WriteByte((byte)_hexChars[idx]); + + return; + } + + if (c > ' ' && notEncoded(c)) + { + result.WriteByte((byte)c); + return; + } + + if (c == ' ') + { + result.WriteByte((byte)'+'); + return; + } + + if ((c < '0') || + (c < 'A' && c > '9') || + (c > 'Z' && c < 'a') || + (c > 'z')) + { + if (unicode && c > 127) + { + result.WriteByte((byte)'%'); + result.WriteByte((byte)'u'); + result.WriteByte((byte)'0'); + result.WriteByte((byte)'0'); + } + else + { + result.WriteByte((byte)'%'); + } + + var i = (int)c; + var idx = i >> 4; + result.WriteByte((byte)_hexChars[idx]); + + idx = i & 0x0F; + result.WriteByte((byte)_hexChars[idx]); + + return; + } + + result.WriteByte((byte)c); + } + + private static void urlPathEncode(char c, Stream result) + { + if (c < 33 || c > 126) + { + var bytes = Encoding.UTF8.GetBytes(c.ToString()); + foreach (var b in bytes) + { + result.WriteByte((byte)'%'); + + var i = (int)b; + var idx = i >> 4; + result.WriteByte((byte)_hexChars[idx]); + + idx = i & 0x0F; + result.WriteByte((byte)_hexChars[idx]); + } + + return; + } + + if (c == ' ') + { + result.WriteByte((byte)'%'); + result.WriteByte((byte)'2'); + result.WriteByte((byte)'0'); + + return; + } + + result.WriteByte((byte)c); + } + + private static void writeCharBytes(char c, IList buffer, Encoding encoding) + { + if (c > 255) + { + foreach (var b in encoding.GetBytes(new[] { c })) + { + buffer.Add(b); + } + + return; + } + + buffer.Add((byte)c); + } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Logger.cs b/EonaCat.Network/System/Sockets/Web/Core/Logger.cs index b44eb20..d53ce58 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Logger.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Logger.cs @@ -25,62 +25,23 @@ namespace EonaCat.Network internal class Logger { - internal static string LoggingDirectory { get; private set; } private static readonly LogManager _logManager; - - internal static bool IsLoggingDirectorySet => !string.IsNullOrWhiteSpace(LoggingDirectory); - private static bool HasBeenSetup { get; set; } - internal static bool DisableConsole { get; set; } - internal static bool IsLoggingEnabled { get; set; } - static Logger() { _logManager = new LogManager(new LoggerSettings { RemoveMessagePrefix = true }); _logManager.OnException += _logManager_OnException; } - internal static void Setup(string loggingDirectory = null) - { - LoggingDirectory = loggingDirectory; - _logManager.Settings.FileLoggerOptions.FileNamePrefix = "EonaCat.Network"; - - if (IsLoggingDirectorySet) - { - _logManager.Settings.FileLoggerOptions.LogDirectory = LoggingDirectory; - } - _logManager.Settings.UseLocalTime = true; - _logManager.Settings.FileLoggerOptions.UseLocalTime = true; - _logManager.Settings.SysLogServers = new List(); - _logManager.Settings.SplunkServers = new List(); - _logManager.Settings.GrayLogServers = new List(); - _logManager.StartNewLogAsync(); - HasBeenSetup = true; - } - - private static void _logManager_OnException(object? sender, ErrorMessage e) - { - Console.WriteLine(e.Message); - if (e.Exception != null) - { - Console.WriteLine(e.Exception); - } - } - + internal static bool DisableConsole { get; set; } + internal static bool IsLoggingDirectorySet => !string.IsNullOrWhiteSpace(LoggingDirectory); + internal static bool IsLoggingEnabled { get; set; } + internal static string LoggingDirectory { get; private set; } + private static bool HasBeenSetup { get; set; } internal static void AddGrayLogServer(string hostname, int port) { _logManager.Settings.GrayLogServers.Add(new GrayLogServer(hostname, port)); } - internal static bool RemoveGrayLogServer(GrayLogServer grayLogServer) - { - return _logManager.Settings.GrayLogServers.Remove(grayLogServer); - } - - internal static void GrayLogState(bool state) - { - _logManager.Settings.SendToGrayLogServers = state; - } - internal static void AddSplunkServer(string splunkHecUrl, string splunkHecToken, bool disableSSL = false) { var splunkServer = new SplunkServer(splunkHecUrl, splunkHecToken); @@ -91,56 +52,14 @@ namespace EonaCat.Network _logManager.Settings.SplunkServers.Add(splunkServer); } - internal static bool RemoveSplunkServer(SplunkServer splunkServer) - { - return _logManager.Settings.SplunkServers.Remove(splunkServer); - } - - internal static void SplunkState(bool state) - { - _logManager.Settings.SendToSplunkServers = state; - } - internal static void AddSyslogServer(string ipAddress, int port) { _logManager.Settings.SysLogServers.Add(new SyslogServer(ipAddress, port)); } - internal static bool RemoveSyslogServer(SyslogServer syslogServer) + internal static void Critical(string message, bool? writeToConsole = null, bool? sendToSysLogServers = null, bool? sendToSplunkServers = null, string? customSplunkSourceType = null, bool? sendToGrayLogServers = null, GrayLogSettings grayLogSettings = null) { - return _logManager.Settings.SysLogServers.Remove(syslogServer); - } - - internal static void SysLogState(bool state) - { - _logManager.Settings.SendToSyslogServers = state; - } - - internal static void Info(string message, bool? writeToConsole = null, bool? sendToSysLogServers = null, bool? sendToSplunkServers = null, string? customSplunkSourceType = null, bool? sendToGrayLogServers = null, GrayLogSettings grayLogSettings = null) - { - Write(message, ELogType.INFO, writeToConsole, sendToSysLogServers, sendToSplunkServers, customSplunkSourceType, sendToGrayLogServers, grayLogSettings); - } - - private static void Write(string message, ELogType logType, bool? writeToConsole = null, bool? sendToSysLogServers = null, bool? sendToSplunkServers = null, string? customSplunkSourceType = null, bool? sendToGrayLogServers = null, GrayLogSettings grayLogSettings = null) - { - if (!HasBeenSetup) - { - Setup(); - } - - if (DisableConsole) - { - writeToConsole = false; - } - - if (grayLogSettings != null) - { - _logManager.Write(message, logType, writeToConsole, sendToSysLogServers, sendToSplunkServers, customSplunkSourceType, sendToGrayLogServers, grayLogSettings.Facility, grayLogSettings.Source, grayLogSettings.Version); - } - else - { - _logManager.Write(message, logType, writeToConsole, sendToSysLogServers, sendToSplunkServers, customSplunkSourceType, sendToGrayLogServers); - } + Write(message, ELogType.CRITICAL, writeToConsole, sendToSysLogServers, sendToSplunkServers, customSplunkSourceType, sendToGrayLogServers, grayLogSettings); } internal static void Debug(string message, bool? writeToConsole = null, bool? sendToSysLogServers = null, bool? sendToSplunkServers = null, string? customSplunkSourceType = null, bool? sendToGrayLogServers = null, GrayLogSettings grayLogSettings = null) @@ -182,14 +101,62 @@ namespace EonaCat.Network } } - internal static void Warning(string message, bool? writeToConsole = null, bool? sendToSysLogServers = null, bool? sendToSplunkServers = null, string? customSplunkSourceType = null, bool? sendToGrayLogServers = null, GrayLogSettings grayLogSettings = null) + internal static void GrayLogState(bool state) { - Write(message, ELogType.WARNING, writeToConsole, sendToSysLogServers, sendToSplunkServers, customSplunkSourceType, sendToGrayLogServers, grayLogSettings); + _logManager.Settings.SendToGrayLogServers = state; } - internal static void Critical(string message, bool? writeToConsole = null, bool? sendToSysLogServers = null, bool? sendToSplunkServers = null, string? customSplunkSourceType = null, bool? sendToGrayLogServers = null, GrayLogSettings grayLogSettings = null) + internal static void Info(string message, bool? writeToConsole = null, bool? sendToSysLogServers = null, bool? sendToSplunkServers = null, string? customSplunkSourceType = null, bool? sendToGrayLogServers = null, GrayLogSettings grayLogSettings = null) { - Write(message, ELogType.CRITICAL, writeToConsole, sendToSysLogServers, sendToSplunkServers, customSplunkSourceType, sendToGrayLogServers, grayLogSettings); + Write(message, ELogType.INFO, writeToConsole, sendToSysLogServers, sendToSplunkServers, customSplunkSourceType, sendToGrayLogServers, grayLogSettings); + } + + internal static bool RemoveGrayLogServer(GrayLogServer grayLogServer) + { + return _logManager.Settings.GrayLogServers.Remove(grayLogServer); + } + + internal static bool RemoveSplunkServer(SplunkServer splunkServer) + { + return _logManager.Settings.SplunkServers.Remove(splunkServer); + } + + internal static bool RemoveSyslogServer(SyslogServer syslogServer) + { + return _logManager.Settings.SysLogServers.Remove(syslogServer); + } + + internal static void Setup(string loggingDirectory = null) + { + LoggingDirectory = loggingDirectory; + _logManager.Settings.FileLoggerOptions.FileNamePrefix = "EonaCat.Network"; + + if (IsLoggingDirectorySet) + { + _logManager.Settings.FileLoggerOptions.LogDirectory = LoggingDirectory; + } + _logManager.Settings.UseLocalTime = true; + _logManager.Settings.FileLoggerOptions.UseLocalTime = true; + _logManager.Settings.SysLogServers = new List(); + _logManager.Settings.SplunkServers = new List(); + _logManager.Settings.GrayLogServers = new List(); + _logManager.StartNewLogAsync(); + HasBeenSetup = true; + } + + internal static void SplunkState(bool state) + { + _logManager.Settings.SendToSplunkServers = state; + } + + internal static void SysLogState(bool state) + { + _logManager.Settings.SendToSyslogServers = state; + } + + internal static void Trace(string message, bool? writeToConsole = null, bool? sendToSysLogServers = null, bool? sendToSplunkServers = null, string? customSplunkSourceType = null, bool? sendToGrayLogServers = null, GrayLogSettings grayLogSettings = null) + { + Write(message, ELogType.TRACE, writeToConsole, sendToSysLogServers, sendToSplunkServers, customSplunkSourceType, sendToGrayLogServers, grayLogSettings); } internal static void Traffic(string message, bool? writeToConsole = null, bool? sendToSysLogServers = null, bool? sendToSplunkServers = null, string? customSplunkSourceType = null, bool? sendToGrayLogServers = null, GrayLogSettings grayLogSettings = null) @@ -197,9 +164,39 @@ namespace EonaCat.Network Write(message, ELogType.TRAFFIC, writeToConsole, sendToSysLogServers, sendToSplunkServers, customSplunkSourceType, sendToGrayLogServers, grayLogSettings); } - internal static void Trace(string message, bool? writeToConsole = null, bool? sendToSysLogServers = null, bool? sendToSplunkServers = null, string? customSplunkSourceType = null, bool? sendToGrayLogServers = null, GrayLogSettings grayLogSettings = null) + internal static void Warning(string message, bool? writeToConsole = null, bool? sendToSysLogServers = null, bool? sendToSplunkServers = null, string? customSplunkSourceType = null, bool? sendToGrayLogServers = null, GrayLogSettings grayLogSettings = null) { - Write(message, ELogType.TRACE, writeToConsole, sendToSysLogServers, sendToSplunkServers, customSplunkSourceType, sendToGrayLogServers, grayLogSettings); + Write(message, ELogType.WARNING, writeToConsole, sendToSysLogServers, sendToSplunkServers, customSplunkSourceType, sendToGrayLogServers, grayLogSettings); + } + + private static void _logManager_OnException(object? sender, ErrorMessage e) + { + Console.WriteLine(e.Message); + if (e.Exception != null) + { + Console.WriteLine(e.Exception); + } + } + private static void Write(string message, ELogType logType, bool? writeToConsole = null, bool? sendToSysLogServers = null, bool? sendToSplunkServers = null, string? customSplunkSourceType = null, bool? sendToGrayLogServers = null, GrayLogSettings grayLogSettings = null) + { + if (!HasBeenSetup) + { + Setup(); + } + + if (DisableConsole) + { + writeToConsole = false; + } + + if (grayLogSettings != null) + { + _logManager.Write(message, logType, writeToConsole, sendToSysLogServers, sendToSplunkServers, customSplunkSourceType, sendToGrayLogServers, grayLogSettings.Facility, grayLogSettings.Source, grayLogSettings.Version); + } + else + { + _logManager.Write(message, logType, writeToConsole, sendToSysLogServers, sendToSplunkServers, customSplunkSourceType, sendToGrayLogServers); + } } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/SSLConfig/SSLConfigClient.cs b/EonaCat.Network/System/Sockets/Web/Core/SSLConfig/SSLConfigClient.cs index 93fb22f..240c5e6 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/SSLConfig/SSLConfigClient.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/SSLConfig/SSLConfigClient.cs @@ -10,19 +10,19 @@ namespace EonaCat.Network { public class SSLConfigClient { - private LocalCertificateSelectionCallback _clientCertSelectionCallback; private X509CertificateCollection _clientCertificates; + private LocalCertificateSelectionCallback _clientCertSelectionCallback; private RemoteCertificateValidationCallback _serverCertValidationCallback; public SSLConfigClient() { - SslProtocols = SslProtocols.Tls12; + SslProtocols = SslProtocols.None; } public SSLConfigClient(string targetHost) { TargetHost = targetHost; - SslProtocols = SslProtocols.Tls12; + SslProtocols = SslProtocols.None; } public SSLConfigClient(SSLConfigClient sslConfig) @@ -40,8 +40,6 @@ namespace EonaCat.Network TargetHost = sslConfig.TargetHost; } - public bool CheckForCertificateRevocation { get; set; } - public X509CertificateCollection Certificates { get @@ -56,6 +54,7 @@ namespace EonaCat.Network } } + public bool CheckForCertificateRevocation { get; set; } public LocalCertificateSelectionCallback ClientCertificateSelectionCallback { get @@ -71,8 +70,6 @@ namespace EonaCat.Network } } - public SslProtocols SslProtocols { get; set; } - public RemoteCertificateValidationCallback ServerCertificateValidationCallback { get @@ -88,6 +85,7 @@ namespace EonaCat.Network } } + public SslProtocols SslProtocols { get; set; } public string TargetHost { get; set; } private static X509Certificate SelectClientCertificate( diff --git a/EonaCat.Network/System/Sockets/Web/Core/SSLConfig/SSLConfigServer.cs b/EonaCat.Network/System/Sockets/Web/Core/SSLConfig/SSLConfigServer.cs index e57621f..d4aa157 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/SSLConfig/SSLConfigServer.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/SSLConfig/SSLConfigServer.cs @@ -14,13 +14,13 @@ namespace EonaCat.Network public SSLConfigServer() { - SslProtocols = SslProtocols.Tls12; + SslProtocols = SslProtocols.None; } public SSLConfigServer(X509Certificate2 certificate) { Certificate = certificate; - SslProtocols = SslProtocols.Tls12; + SslProtocols = SslProtocols.None; } public SSLConfigServer(SSLConfigServer sslConfig) @@ -37,10 +37,9 @@ namespace EonaCat.Network Certificate = sslConfig.Certificate; } + public X509Certificate2 Certificate { get; set; } public bool CheckForCertificateRevocation { get; set; } - public bool IsClientCertificateRequired { get; set; } - public RemoteCertificateValidationCallback ClientCertificateValidationCallback { get @@ -56,10 +55,8 @@ namespace EonaCat.Network } } + public bool IsClientCertificateRequired { get; set; } public SslProtocols SslProtocols { get; set; } - - public X509Certificate2 Certificate { get; set; } - private static bool ValidateClientCertificate( object sender, X509Certificate certificate, diff --git a/EonaCat.Network/System/Sockets/Web/Core/Stream/RequestStream.cs b/EonaCat.Network/System/Sockets/Web/Core/Stream/RequestStream.cs index a89f31f..081176d 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Stream/RequestStream.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Stream/RequestStream.cs @@ -8,13 +8,12 @@ namespace EonaCat.Network { internal class RequestStream : Stream { - private long _bodyLeft; private readonly byte[] _buffer; + private readonly Stream _stream; + private long _bodyLeft; private int _count; private bool _disposed; private int _offset; - private readonly Stream _stream; - internal RequestStream(Stream stream, byte[] buffer, int offset, int count) : this(stream, buffer, offset, count, -1) { @@ -51,66 +50,8 @@ namespace EonaCat.Network } } - // 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 (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - - if (offset < 0) - { - throw new ArgumentOutOfRangeException(nameof(offset), "A negative value."); - } - - if (count < 0) - { - throw new ArgumentOutOfRangeException(nameof(count), "A negative value."); - } - - var bufferLength = buffer.Length; - if (offset + count > bufferLength) - { - throw new ArgumentException( - "The sum of 'offset' and 'count' is greater than 'buffer' length."); - } - - if (_bodyLeft == 0) - { - return -1; - } - - if (_count == 0 || count == 0) - { - return 0; - } - - if (count > _count) - { - count = _count; - } - - if (_bodyLeft > 0 && count > _bodyLeft) - { - count = (int)_bodyLeft; - } - - Buffer.BlockCopy(_buffer, _offset, buffer, offset, count); - _offset += count; - _count -= count; - if (_bodyLeft > 0) - { - _bodyLeft -= count; - } - - return count; - } - public override IAsyncResult BeginRead( - byte[] buffer, int offset, int count, AsyncCallback callback, object state) + byte[] buffer, int offset, int count, AsyncCallback callback, object state) { if (_disposed) { @@ -139,7 +80,7 @@ namespace EonaCat.Network } public override IAsyncResult BeginWrite( - byte[] buffer, int offset, int count, AsyncCallback callback, object state) + byte[] buffer, int offset, int count, AsyncCallback callback, object state) { throw new NotSupportedException(); } @@ -233,5 +174,63 @@ namespace EonaCat.Network { throw new NotSupportedException(); } + + // 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 (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset), "A negative value."); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "A negative value."); + } + + var bufferLength = buffer.Length; + if (offset + count > bufferLength) + { + throw new ArgumentException( + "The sum of 'offset' and 'count' is greater than 'buffer' length."); + } + + if (_bodyLeft == 0) + { + return -1; + } + + if (_count == 0 || count == 0) + { + return 0; + } + + if (count > _count) + { + count = _count; + } + + if (_bodyLeft > 0 && count > _bodyLeft) + { + count = (int)_bodyLeft; + } + + Buffer.BlockCopy(_buffer, _offset, buffer, offset, count); + _offset += count; + _count -= count; + if (_bodyLeft > 0) + { + _bodyLeft -= count; + } + + return count; + } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Stream/ResponseStream.cs b/EonaCat.Network/System/Sockets/Web/Core/Stream/ResponseStream.cs index c04f56a..83018e9 100644 --- a/EonaCat.Network/System/Sockets/Web/Core/Stream/ResponseStream.cs +++ b/EonaCat.Network/System/Sockets/Web/Core/Stream/ResponseStream.cs @@ -9,16 +9,15 @@ namespace EonaCat.Network { internal class ResponseStream : Stream { - private MemoryStream _body; private static readonly byte[] _crlf = new byte[] { 13, 10 }; + private readonly Action _write; + private readonly Action _writeChunked; + private MemoryStream _body; private bool _disposed; private HttpListenerResponse _response; private bool _sendChunked; private Stream _stream; - private readonly Action _write; private Action _writeBody; - private readonly Action _writeChunked; - internal ResponseStream( Stream stream, HttpListenerResponse response, bool ignoreWriteExceptions) { @@ -60,6 +59,121 @@ namespace EonaCat.Network } } + public override IAsyncResult BeginRead( + byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + throw new NotSupportedException(); + } + + public override IAsyncResult BeginWrite( + byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().ToString()); + } + + return _body.BeginWrite(buffer, offset, count, callback, state); + } + + public override void Close() + { + Close(false); + } + + public override int EndRead(IAsyncResult asyncResult) + { + throw new NotSupportedException(); + } + + public override void EndWrite(IAsyncResult asyncResult) + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().ToString()); + } + + _body.EndWrite(asyncResult); + } + + public override void Flush() + { + if (!_disposed && (_sendChunked || _response.SendInChunks)) + { + flush(false); + } + } + + public override int Read(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(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().ToString()); + } + + _body.Write(buffer, offset, count); + } + + internal void Close(bool force) + { + if (_disposed) + { + return; + } + + _disposed = true; + if (!force && flush(true)) + { + _response.Close(); + } + else + { + if (_sendChunked) + { + var last = getChunkSizeBytes(0, true); + _write(last, 0, last.Length); + } + + _body.Dispose(); + _body = null; + + _response.Abort(); + } + + _response = null; + _stream = null; + } + + internal void InternalWrite(byte[] buffer, int offset, int count) + { + _write(buffer, offset, count); + } + + protected override void Dispose(bool disposing) + { + Close(!disposing); + } + + private static byte[] getChunkSizeBytes(int size, bool final) + { + return Encoding.ASCII.GetBytes(string.Format("{0:x}\r\n{1}", size, final ? "\r\n" : "")); + } + private bool flush(bool closing) { if (!_response.HeadersSent) @@ -137,12 +251,6 @@ namespace EonaCat.Network return true; } - - private static byte[] getChunkSizeBytes(int size, bool final) - { - return Encoding.ASCII.GetBytes(string.Format("{0:x}\r\n{1}", size, final ? "\r\n" : "")); - } - private void writeChunked(byte[] buffer, int offset, int count) { var size = getChunkSizeBytes(count, false); @@ -172,115 +280,5 @@ namespace EonaCat.Network { } } - - internal void Close(bool force) - { - if (_disposed) - { - return; - } - - _disposed = true; - if (!force && flush(true)) - { - _response.Close(); - } - else - { - if (_sendChunked) - { - var last = getChunkSizeBytes(0, true); - _write(last, 0, last.Length); - } - - _body.Dispose(); - _body = null; - - _response.Abort(); - } - - _response = null; - _stream = null; - } - - internal void InternalWrite(byte[] buffer, int offset, int count) - { - _write(buffer, offset, count); - } - - public override IAsyncResult BeginRead( - byte[] buffer, int offset, int count, AsyncCallback callback, object state) - { - throw new NotSupportedException(); - } - - public override IAsyncResult BeginWrite( - byte[] buffer, int offset, int count, AsyncCallback callback, object state) - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().ToString()); - } - - return _body.BeginWrite(buffer, offset, count, callback, state); - } - - public override void Close() - { - Close(false); - } - - protected override void Dispose(bool disposing) - { - Close(!disposing); - } - - public override int EndRead(IAsyncResult asyncResult) - { - throw new NotSupportedException(); - } - - public override void EndWrite(IAsyncResult asyncResult) - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().ToString()); - } - - _body.EndWrite(asyncResult); - } - - public override void Flush() - { - if (!_disposed && (_sendChunked || _response.SendInChunks)) - { - flush(false); - } - } - - public override int Read(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(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().ToString()); - } - - _body.Write(buffer, offset, count); - } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/EventArgs/CloseEventArgs.cs b/EonaCat.Network/System/Sockets/Web/EventArgs/CloseEventArgs.cs index 907fc06..d18797b 100644 --- a/EonaCat.Network/System/Sockets/Web/EventArgs/CloseEventArgs.cs +++ b/EonaCat.Network/System/Sockets/Web/EventArgs/CloseEventArgs.cs @@ -37,12 +37,9 @@ namespace EonaCat.Network { } - internal PayloadData PayloadData { get; } - public ushort Code => PayloadData.Code; - public string Reason => PayloadData.Reason ?? string.Empty; - public bool WasClean { get; internal set; } + internal PayloadData PayloadData { get; } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/EventArgs/MessageEventArgs.cs b/EonaCat.Network/System/Sockets/Web/EventArgs/MessageEventArgs.cs index 56b31b3..34114a8 100644 --- a/EonaCat.Network/System/Sockets/Web/EventArgs/MessageEventArgs.cs +++ b/EonaCat.Network/System/Sockets/Web/EventArgs/MessageEventArgs.cs @@ -7,10 +7,9 @@ namespace EonaCat.Network { public class MessageEventArgs : EventArgs { + private readonly byte[] _rawData; private string _data; private bool _dataSet; - private readonly byte[] _rawData; - internal MessageEventArgs(WSFrame frame) { Opcode = frame.Opcode; @@ -28,8 +27,6 @@ namespace EonaCat.Network _rawData = rawData; } - internal OperationCode Opcode { get; } - public string Data { get @@ -40,11 +37,8 @@ namespace EonaCat.Network } public bool IsBinary => Opcode == OperationCode.Binary; - public bool IsPing => Opcode == OperationCode.Ping; - public bool IsText => Opcode == OperationCode.Text; - public byte[] RawData { get @@ -54,6 +48,7 @@ namespace EonaCat.Network } } + internal OperationCode Opcode { get; } private void setData() { if (_dataSet) diff --git a/EonaCat.Network/System/Sockets/Web/Extensions.cs b/EonaCat.Network/System/Sockets/Web/Extensions.cs index 2961e3c..06faeab 100644 --- a/EonaCat.Network/System/Sockets/Web/Extensions.cs +++ b/EonaCat.Network/System/Sockets/Web/Extensions.cs @@ -13,101 +13,601 @@ namespace EonaCat.Network { public static class Ext { - private static readonly byte[] _last = new byte[] { 0x00 }; - private static readonly int _retry = 5; private const string _tspecials = "()<>@,;:\\\"/[]?={} \t"; private const int BUFFER_SIZE = 1024; - - private static byte[] compress(this byte[] data) + private static readonly byte[] _last = new byte[] { 0x00 }; + private static readonly int _retry = 5; + public static bool Contains(this string value, params char[] chars) { - if (data.LongLength == 0) + return chars == null || chars.Length == 0 +|| value != null && value.Length != 0 +&& value.IndexOfAny(chars) > -1; + } + + public static bool Contains(this NameValueCollection collection, string name) + { + return collection != null && collection.Count > 0 && collection[name] != null; + } + + public static bool Contains(this NameValueCollection collection, string name, string value) + { + if (collection == null || collection.Count == 0) { - return data; + return false; } - using (var input = new MemoryStream(data)) + var vals = collection[name]; + if (vals == null) { - return input.compressToArray(); + return false; + } + + foreach (var val in vals.Split(',')) + { + if (val.Trim().Equals(value, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + public static void Emit(this EventHandler eventHandler, object sender, EventArgs e) + { + if (eventHandler != null) + { + eventHandler(sender, e); } } - private static MemoryStream compress(this Stream stream) + public static void Emit( + this EventHandler eventHandler, object sender, TEventArgs e) + where TEventArgs : EventArgs { - var output = new MemoryStream(); - if (stream.Length == 0) + if (eventHandler != null) { - return output; - } - - stream.Position = 0; - using (var ds = new DeflateStream(output, CompressionMode.Compress, true)) - { - stream.CopyTo(ds, BUFFER_SIZE); - ds.Close(); // BFINAL set to 1. - output.Write(_last, 0, 1); - output.Position = 0; - - return output; + eventHandler(sender, e); } } - private static byte[] compressToArray(this Stream stream) + public static CookieCollection GetCookies(this NameValueCollection headers, bool response) { - using (var output = stream.compress()) + var name = response ? "Set-Cookie" : "Cookie"; + return headers != null && headers.Contains(name) + ? CookieCollection.Parse(headers[name], response) + : new CookieCollection(); + } + + public static string GetDescription(this HttpStatusCode code) + { + return ((int)code).GetStatusDescription(); + } + + public static string GetStatusDescription(this int code) + { + switch (code) { - output.Close(); - return output.ToArray(); + 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; + } + + public static bool IsCloseStatusCode(this ushort value) + { + return value > 999 && value < 5000; + } + + public static bool IsEnclosedIn(this string value, char c) + { + return value != null + && value.Length > 1 + && value[0] == c + && value[value.Length - 1] == c; + } + + 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)); + } + + /// + /// An extension method to determine if an IP address is internal + /// Class A Private IP Range: 10.0.0.0 ? 10.255.255.255 + /// Class B Private IP Range: 172.16.0.0 ? 172.31.255.255 + /// Class C Private IP Range: 192.168.0.0 ? 192.168.255.25 + /// + /// The IP address that will be tested + /// Returns true if the IP is internal, false if it is external + public static bool IsInternal(this System.Net.IPAddress address) + { + byte[] bytes = address.GetAddressBytes(); + switch (bytes[0]) + { + case 10: + return true; + case 172: + return bytes[1] < 32 && bytes[1] >= 16; + case 192: + return bytes[1] == 168; + default: + return false; } } - private static byte[] decompress(this byte[] data) + public static bool IsLocal(this System.Net.IPAddress address) { - if (data.LongLength == 0) + if (address == null) { - return data; + return false; } - using (var input = new MemoryStream(data)) + if (address.Equals(System.Net.IPAddress.Any)) { - return input.decompressToArray(); + return true; + } + + if (address.Equals(System.Net.IPAddress.Loopback)) + { + return true; + } + + if (Socket.OSSupportsIPv6) + { + if (address.Equals(System.Net.IPAddress.IPv6Any)) + { + return true; + } + + if (address.Equals(System.Net.IPAddress.IPv6Loopback)) + { + return true; + } + } + + if (address.IsInternal()) + return true; + + var host = System.Net.Dns.GetHostName(); + var addrs = System.Net.Dns.GetHostAddresses(host); + foreach (var addr in addrs) + { + if (address.Equals(addr)) + { + return true; + } + } + + return false; + } + + public static bool IsNullOrEmpty(this string value) + { + return value == null || value.Length == 0; + } + + 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 == 'g') + { + return value == "gopher"; + } + + if (c == 'm') + { + return value == "mailto"; + } + + if (c == 'n') + { + c = value[1]; + return c == 'e' + ? value == "news" || value == "net.pipe" || value == "net.tcp" + : value == "nntp"; + } + + return false; + } + + public static bool IsUpgradeTo(this HttpListenerRequest request, string protocol) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (protocol == null) + { + throw new ArgumentNullException(nameof(protocol)); + } + + if (protocol.Length == 0) + { + throw new ArgumentException("An empty string.", nameof(protocol)); + } + + return request.Headers.Contains("Upgrade", protocol) && + request.Headers.Contains("Connection", "Upgrade"); + } + + public static bool MaybeUri(this string value) + { + if (value == null || value.Length == 0) + { + return false; + } + + var idx = value.IndexOf(':'); + if (idx == -1) + { + return false; + } + + if (idx >= 10) + { + return false; + } + + var schm = value.Substring(0, idx); + return schm.IsPredefinedScheme(); + } + + public static T[] SubArray(this T[] array, int startIndex, int length) + { + int len; + if (array == null || (len = array.Length) == 0) + { + return new T[0]; + } + + if (startIndex < 0 || length <= 0 || startIndex + length > len) + { + return new T[0]; + } + + if (startIndex == 0 && length == len) + { + return array; + } + + var subArray = new T[length]; + Array.Copy(array, startIndex, subArray, 0, length); + + return subArray; + } + + public static T[] SubArray(this T[] array, long startIndex, long length) + { + long len; + if (array == null || (len = array.LongLength) == 0) + { + return new T[0]; + } + + if (startIndex < 0 || length <= 0 || startIndex + length > len) + { + return new T[0]; + } + + if (startIndex == 0 && length == len) + { + return array; + } + + var subArray = new T[length]; + Array.Copy(array, startIndex, subArray, 0, length); + + return subArray; + } + + public static void Times(this int n, Action action) + { + if (n > 0 && action != null) + { + ((ulong)n).times(action); } } - private static MemoryStream decompress(this Stream stream) + public static void Times(this long n, Action action) { - var output = new MemoryStream(); - if (stream.Length == 0) + if (n > 0 && action != null) { - return output; - } - - stream.Position = 0; - using (var ds = new DeflateStream(stream, CompressionMode.Decompress, true)) - { - ds.CopyTo(output, BUFFER_SIZE); - output.Position = 0; - - return output; + ((ulong)n).times(action); } } - private static byte[] decompressToArray(this Stream stream) + public static void Times(this uint n, Action action) { - using (var output = stream.decompress()) + if (n > 0 && action != null) { - output.Close(); - return output.ToArray(); + ((ulong)n).times(action); } } - private static void times(this ulong n, Action action) + public static void Times(this ulong n, Action action) { - for (ulong i = 0; i < n; i++) + if (n > 0 && action != null) { - action(); + n.times(action); } } + public static void Times(this int n, Action action) + { + if (n > 0 && action != null) + { + for (int i = 0; i < n; i++) + { + action(i); + } + } + } + + public static void Times(this long n, Action action) + { + if (n > 0 && action != null) + { + for (long i = 0; i < n; i++) + { + action(i); + } + } + } + + public static void Times(this uint n, Action action) + { + if (n > 0 && action != null) + { + for (uint i = 0; i < n; i++) + { + action(i); + } + } + } + + public static void Times(this ulong n, Action action) + { + if (n > 0 && action != null) + { + for (ulong i = 0; i < n; i++) + { + action(i); + } + } + } + + public static T To(this byte[] source, ByteOrder sourceOrder) + where T : struct + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (source.Length == 0) + { + return default(T); + } + + var type = typeof(T); + var buff = source.ToHostOrder(sourceOrder); + + return type == typeof(bool) + ? (T)(object)BitConverter.ToBoolean(buff, 0) + : type == typeof(char) + ? (T)(object)BitConverter.ToChar(buff, 0) + : type == typeof(double) + ? (T)(object)BitConverter.ToDouble(buff, 0) + : type == typeof(short) + ? (T)(object)BitConverter.ToInt16(buff, 0) + : type == typeof(int) + ? (T)(object)BitConverter.ToInt32(buff, 0) + : type == typeof(long) + ? (T)(object)BitConverter.ToInt64(buff, 0) + : type == typeof(float) + ? (T)(object)BitConverter.ToSingle(buff, 0) + : type == typeof(ushort) + ? (T)(object)BitConverter.ToUInt16(buff, 0) + : type == typeof(uint) + ? (T)(object)BitConverter.ToUInt32(buff, 0) + : type == typeof(ulong) + ? (T)(object)BitConverter.ToUInt64(buff, 0) + : default(T); + } + + public static byte[] ToByteArray(this T value, ByteOrder order) + where T : struct + { + var type = typeof(T); + var bytes = type == typeof(bool) + ? BitConverter.GetBytes((bool)(object)value) + : type == typeof(byte) + ? new byte[] { (byte)(object)value } + : type == typeof(char) + ? BitConverter.GetBytes((char)(object)value) + : type == typeof(double) + ? BitConverter.GetBytes((double)(object)value) + : type == typeof(short) + ? BitConverter.GetBytes((short)(object)value) + : type == typeof(int) + ? BitConverter.GetBytes((int)(object)value) + : type == typeof(long) + ? BitConverter.GetBytes((long)(object)value) + : type == typeof(float) + ? BitConverter.GetBytes((float)(object)value) + : type == typeof(ushort) + ? BitConverter.GetBytes((ushort)(object)value) + : type == typeof(uint) + ? BitConverter.GetBytes((uint)(object)value) + : type == typeof(ulong) + ? BitConverter.GetBytes((ulong)(object)value) + : WSClient.EmptyBytes; + + if (bytes.Length > 1 && !order.IsHostOrder()) + { + Array.Reverse(bytes); + } + + return bytes; + } + + public static byte[] ToHostOrder(this byte[] source, ByteOrder sourceOrder) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + return source.Length > 1 && !sourceOrder.IsHostOrder() ? source.Reverse() : source; + } + + 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; + } + + 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(); + } + + public static Uri ToUri(this string value) + { + Uri.TryCreate( + value, value.MaybeUri() ? UriKind.Absolute : UriKind.Relative, out Uri ret + ); + + return ret; + } + + public static string UrlDecode(this string value) + { + return value != null && value.Length > 0 + ? HttpUtility.UrlDecode(value) + : value; + } + + public static string UrlEncode(this string value) + { + return value != null && value.Length > 0 + ? HttpUtility.UrlEncode(value) + : value; + } + + public static void WriteContent(this HttpListenerResponse response, byte[] content) + { + if (response == null) + { + throw new ArgumentNullException(nameof(response)); + } + + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + var len = content.LongLength; + if (len == 0) + { + response.Close(); + return; + } + + response.ContentLength64 = len; + var output = response.OutputStream; + if (len <= int.MaxValue) + { + output.Write(content, 0, (int)len); + } + else + { + output.WriteBytes(content, BUFFER_SIZE); + } + + output.Close(); + } + internal static byte[] Append(this ushort code, string reason) { var ret = code.InternalToByteArray(ByteOrder.Big); @@ -599,7 +1099,7 @@ namespace EonaCat.Network return; } - if (nread == 0 || nread == length) + if (nread == length) { if (completed != null) { @@ -1075,568 +1575,94 @@ namespace EonaCat.Network }); } - public static bool Contains(this string value, params char[] chars) + private static byte[] compress(this byte[] data) { - return chars == null || chars.Length == 0 -|| value != null && value.Length != 0 -&& value.IndexOfAny(chars) > -1; - } - - public static bool Contains(this NameValueCollection collection, string name) - { - return collection != null && collection.Count > 0 && collection[name] != null; - } - - public static bool Contains(this NameValueCollection collection, string name, string value) - { - if (collection == null || collection.Count == 0) + if (data.LongLength == 0) { - return false; + return data; } - var vals = collection[name]; - if (vals == null) + using (var input = new MemoryStream(data)) { - return false; - } - - foreach (var val in vals.Split(',')) - { - if (val.Trim().Equals(value, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; - } - - public static void Emit(this EventHandler eventHandler, object sender, EventArgs e) - { - if (eventHandler != null) - { - eventHandler(sender, e); + return input.compressToArray(); } } - public static void Emit( - this EventHandler eventHandler, object sender, TEventArgs e) - where TEventArgs : EventArgs + private static MemoryStream compress(this Stream stream) { - if (eventHandler != null) + var output = new MemoryStream(); + if (stream.Length == 0) { - eventHandler(sender, e); + return output; + } + + stream.Position = 0; + using (var ds = new DeflateStream(output, CompressionMode.Compress, true)) + { + stream.CopyTo(ds, BUFFER_SIZE); + ds.Close(); // BFINAL set to 1. + output.Write(_last, 0, 1); + output.Position = 0; + + return output; } } - public static CookieCollection GetCookies(this NameValueCollection headers, bool response) + private static byte[] compressToArray(this Stream stream) { - var name = response ? "Set-Cookie" : "Cookie"; - return headers != null && headers.Contains(name) - ? CookieCollection.Parse(headers[name], response) - : new CookieCollection(); - } - - public static string GetDescription(this HttpStatusCode code) - { - return ((int)code).GetStatusDescription(); - } - - public static string GetStatusDescription(this int code) - { - switch (code) + using (var output = stream.compress()) { - 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; - } - - public static bool IsCloseStatusCode(this ushort value) - { - return value > 999 && value < 5000; - } - - public static bool IsEnclosedIn(this string value, char c) - { - return value != null - && value.Length > 1 - && value[0] == c - && value[value.Length - 1] == c; - } - - 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)); - } - - public static bool IsLocal(this System.Net.IPAddress address) - { - if (address == null) - { - return false; - } - - if (address.Equals(System.Net.IPAddress.Any)) - { - return true; - } - - if (address.Equals(System.Net.IPAddress.Loopback)) - { - return true; - } - - if (Socket.OSSupportsIPv6) - { - if (address.Equals(System.Net.IPAddress.IPv6Any)) - { - return true; - } - - if (address.Equals(System.Net.IPAddress.IPv6Loopback)) - { - return true; - } - } - - var host = System.Net.Dns.GetHostName(); - var addrs = System.Net.Dns.GetHostAddresses(host); - foreach (var addr in addrs) - { - if (address.Equals(addr)) - { - return true; - } - } - - return false; - } - - public static bool IsNullOrEmpty(this string value) - { - return value == null || value.Length == 0; - } - - 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 == 'g') - { - return value == "gopher"; - } - - if (c == 'm') - { - return value == "mailto"; - } - - if (c == 'n') - { - c = value[1]; - return c == 'e' - ? value == "news" || value == "net.pipe" || value == "net.tcp" - : value == "nntp"; - } - - return false; - } - - public static bool IsUpgradeTo(this HttpListenerRequest request, string protocol) - { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } - - if (protocol == null) - { - throw new ArgumentNullException(nameof(protocol)); - } - - if (protocol.Length == 0) - { - throw new ArgumentException("An empty string.", nameof(protocol)); - } - - return request.Headers.Contains("Upgrade", protocol) && - request.Headers.Contains("Connection", "Upgrade"); - } - - public static bool MaybeUri(this string value) - { - if (value == null || value.Length == 0) - { - return false; - } - - var idx = value.IndexOf(':'); - if (idx == -1) - { - return false; - } - - if (idx >= 10) - { - return false; - } - - var schm = value.Substring(0, idx); - return schm.IsPredefinedScheme(); - } - - public static T[] SubArray(this T[] array, int startIndex, int length) - { - int len; - if (array == null || (len = array.Length) == 0) - { - return new T[0]; - } - - if (startIndex < 0 || length <= 0 || startIndex + length > len) - { - return new T[0]; - } - - if (startIndex == 0 && length == len) - { - return array; - } - - var subArray = new T[length]; - Array.Copy(array, startIndex, subArray, 0, length); - - return subArray; - } - - public static T[] SubArray(this T[] array, long startIndex, long length) - { - long len; - if (array == null || (len = array.LongLength) == 0) - { - return new T[0]; - } - - if (startIndex < 0 || length <= 0 || startIndex + length > len) - { - return new T[0]; - } - - if (startIndex == 0 && length == len) - { - return array; - } - - var subArray = new T[length]; - Array.Copy(array, startIndex, subArray, 0, length); - - return subArray; - } - - public static void Times(this int n, Action action) - { - if (n > 0 && action != null) - { - ((ulong)n).times(action); + output.Close(); + return output.ToArray(); } } - public static void Times(this long n, Action action) + private static byte[] decompress(this byte[] data) { - if (n > 0 && action != null) + if (data.LongLength == 0) { - ((ulong)n).times(action); + return data; + } + + using (var input = new MemoryStream(data)) + { + return input.decompressToArray(); } } - public static void Times(this uint n, Action action) + private static MemoryStream decompress(this Stream stream) { - if (n > 0 && action != null) + var output = new MemoryStream(); + if (stream.Length == 0) { - ((ulong)n).times(action); + return output; + } + + stream.Position = 0; + using (var ds = new DeflateStream(stream, CompressionMode.Decompress, true)) + { + ds.CopyTo(output, BUFFER_SIZE); + output.Position = 0; + + return output; } } - public static void Times(this ulong n, Action action) + private static byte[] decompressToArray(this Stream stream) { - if (n > 0 && action != null) + using (var output = stream.decompress()) { - n.times(action); + output.Close(); + return output.ToArray(); } } - public static void Times(this int n, Action action) + private static void times(this ulong n, Action action) { - if (n > 0 && action != null) + for (ulong i = 0; i < n; i++) { - for (int i = 0; i < n; i++) - { - action(i); - } + action(); } } - - public static void Times(this long n, Action action) - { - if (n > 0 && action != null) - { - for (long i = 0; i < n; i++) - { - action(i); - } - } - } - - public static void Times(this uint n, Action action) - { - if (n > 0 && action != null) - { - for (uint i = 0; i < n; i++) - { - action(i); - } - } - } - - public static void Times(this ulong n, Action action) - { - if (n > 0 && action != null) - { - for (ulong i = 0; i < n; i++) - { - action(i); - } - } - } - - public static T To(this byte[] source, ByteOrder sourceOrder) - where T : struct - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (source.Length == 0) - { - return default(T); - } - - var type = typeof(T); - var buff = source.ToHostOrder(sourceOrder); - - return type == typeof(bool) - ? (T)(object)BitConverter.ToBoolean(buff, 0) - : type == typeof(char) - ? (T)(object)BitConverter.ToChar(buff, 0) - : type == typeof(double) - ? (T)(object)BitConverter.ToDouble(buff, 0) - : type == typeof(short) - ? (T)(object)BitConverter.ToInt16(buff, 0) - : type == typeof(int) - ? (T)(object)BitConverter.ToInt32(buff, 0) - : type == typeof(long) - ? (T)(object)BitConverter.ToInt64(buff, 0) - : type == typeof(float) - ? (T)(object)BitConverter.ToSingle(buff, 0) - : type == typeof(ushort) - ? (T)(object)BitConverter.ToUInt16(buff, 0) - : type == typeof(uint) - ? (T)(object)BitConverter.ToUInt32(buff, 0) - : type == typeof(ulong) - ? (T)(object)BitConverter.ToUInt64(buff, 0) - : default(T); - } - - public static byte[] ToByteArray(this T value, ByteOrder order) - where T : struct - { - var type = typeof(T); - var bytes = type == typeof(bool) - ? BitConverter.GetBytes((bool)(object)value) - : type == typeof(byte) - ? new byte[] { (byte)(object)value } - : type == typeof(char) - ? BitConverter.GetBytes((char)(object)value) - : type == typeof(double) - ? BitConverter.GetBytes((double)(object)value) - : type == typeof(short) - ? BitConverter.GetBytes((short)(object)value) - : type == typeof(int) - ? BitConverter.GetBytes((int)(object)value) - : type == typeof(long) - ? BitConverter.GetBytes((long)(object)value) - : type == typeof(float) - ? BitConverter.GetBytes((float)(object)value) - : type == typeof(ushort) - ? BitConverter.GetBytes((ushort)(object)value) - : type == typeof(uint) - ? BitConverter.GetBytes((uint)(object)value) - : type == typeof(ulong) - ? BitConverter.GetBytes((ulong)(object)value) - : WSClient.EmptyBytes; - - if (bytes.Length > 1 && !order.IsHostOrder()) - { - Array.Reverse(bytes); - } - - return bytes; - } - - public static byte[] ToHostOrder(this byte[] source, ByteOrder sourceOrder) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - return source.Length > 1 && !sourceOrder.IsHostOrder() ? source.Reverse() : source; - } - - 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; - } - - 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(); - } - - public static Uri ToUri(this string value) - { - Uri.TryCreate( - value, value.MaybeUri() ? UriKind.Absolute : UriKind.Relative, out Uri ret - ); - - return ret; - } - - public static string UrlDecode(this string value) - { - return value != null && value.Length > 0 - ? HttpUtility.UrlDecode(value) - : value; - } - - public static string UrlEncode(this string value) - { - return value != null && value.Length > 0 - ? HttpUtility.UrlEncode(value) - : value; - } - - public static void WriteContent(this HttpListenerResponse response, byte[] content) - { - if (response == null) - { - throw new ArgumentNullException(nameof(response)); - } - - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - - var len = content.LongLength; - if (len == 0) - { - response.Close(); - return; - } - - response.ContentLength64 = len; - var output = response.OutputStream; - if (len <= int.MaxValue) - { - output.Write(content, 0, (int)len); - } - else - { - output.WriteBytes(content, BUFFER_SIZE); - } - - output.Close(); - } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/OperationCode.cs b/EonaCat.Network/System/Sockets/Web/OperationCode.cs index d99cc1e..cdcb124 100644 --- a/EonaCat.Network/System/Sockets/Web/OperationCode.cs +++ b/EonaCat.Network/System/Sockets/Web/OperationCode.cs @@ -5,7 +5,7 @@ namespace EonaCat.Network { internal enum OperationCode : byte { - Cont = 0x0, + Continue = 0x0, Text = 0x1, diff --git a/EonaCat.Network/System/Sockets/Web/PayloadData.cs b/EonaCat.Network/System/Sockets/Web/PayloadData.cs index a0acd60..dc0386e 100644 --- a/EonaCat.Network/System/Sockets/Web/PayloadData.cs +++ b/EonaCat.Network/System/Sockets/Web/PayloadData.cs @@ -37,6 +37,21 @@ namespace EonaCat.Network _reasonSet = true; } + internal PayloadData(PayloadData original) + { + _code = original._code; + _codeSet = original._codeSet; + ExtensionDataLength = original.ExtensionDataLength; + _length = original._length; + _reason = original._reason; + _reasonSet = original._reasonSet; + + _data = new byte[_length]; + original._data.CopyTo(_data, 0); + + + } + internal PayloadData(byte[] data) : this(data, data.LongLength) { diff --git a/EonaCat.Network/System/Sockets/Web/Server/HttpRequestEventArgs.cs b/EonaCat.Network/System/Sockets/Web/Server/HttpRequestEventArgs.cs index 1ca206d..8679885 100644 --- a/EonaCat.Network/System/Sockets/Web/Server/HttpRequestEventArgs.cs +++ b/EonaCat.Network/System/Sockets/Web/Server/HttpRequestEventArgs.cs @@ -27,36 +27,6 @@ namespace EonaCat.Network public IPrincipal User => _context.User; - private string createFilePath(string childPath) - { - childPath = childPath.TrimStart('/', '\\'); - return new StringBuilder(_docRootPath, 32) - .AppendFormat("/{0}", childPath) - .ToString() - .Replace('\\', '/'); - } - - private static bool tryReadFile(string path, out byte[] contents) - { - contents = null; - - if (!File.Exists(path)) - { - return false; - } - - try - { - contents = File.ReadAllBytes(path); - } - catch - { - return false; - } - - return true; - } - public byte[] ReadFile(string path) { if (path == null) @@ -98,5 +68,35 @@ namespace EonaCat.Network return tryReadFile(createFilePath(path), out contents); } + + private static bool tryReadFile(string path, out byte[] contents) + { + contents = null; + + if (!File.Exists(path)) + { + return false; + } + + try + { + contents = File.ReadAllBytes(path); + } + catch + { + return false; + } + + return true; + } + + private string createFilePath(string childPath) + { + childPath = childPath.TrimStart('/', '\\'); + return new StringBuilder(_docRootPath, 32) + .AppendFormat("/{0}", childPath) + .ToString() + .Replace('\\', '/'); + } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Server/HttpServer.cs b/EonaCat.Network/System/Sockets/Web/Server/HttpServer.cs index 42eb020..902b4a7 100644 --- a/EonaCat.Network/System/Sockets/Web/Server/HttpServer.cs +++ b/EonaCat.Network/System/Sockets/Web/Server/HttpServer.cs @@ -11,13 +11,13 @@ namespace EonaCat.Network { public class HttpServer { + private bool _allowForwardedRequest; private string _docRootPath; private string _hostname; private HttpListener _listener; private Thread _receiveThread; private volatile ServerState _state; private object _sync; - public HttpServer() { init("*", System.Net.IPAddress.Any, 80, false); @@ -100,8 +100,49 @@ namespace EonaCat.Network init(address.ToString(true), address, port, secure); } + public event EventHandler OnConnect; + + public event EventHandler OnDelete; + + public event EventHandler OnGet; + + public event EventHandler OnHead; + + public event EventHandler OnOptions; + + public event EventHandler OnPatch; + + public event EventHandler OnPost; + + public event EventHandler OnPut; + + public event EventHandler OnTrace; + public System.Net.IPAddress Address { get; private set; } + /// + /// Gets or sets a value indicating whether the server accepts every + /// handshake request without checking the request URI. + /// + /// + /// The set operation does nothing if the server has already started or + /// it is shutting down. + /// + /// + /// + /// true if the server accepts every handshake request without + /// checking the request URI; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool AllowForwardedRequest + { + get { return _allowForwardedRequest; } + set { _allowForwardedRequest = value; } + } + public AuthenticationSchemes AuthenticationSchemes { get @@ -208,6 +249,7 @@ namespace EonaCat.Network public bool IsListening => _state == ServerState.Start; + public bool IsLoggingEnabled { get; private set; } public bool IsSecure { get; private set; } public bool KeepClean @@ -222,11 +264,7 @@ namespace EonaCat.Network WebSocketServices.AutoCleanSessions = value; } } - - public bool IsLoggingEnabled { get; private set; } - public int Port { get; private set; } - public string Realm { get @@ -339,24 +377,174 @@ namespace EonaCat.Network } public WSEndpointManager WebSocketServices { get; private set; } + public void AddWebSocketService(string path) + where TEndpoint : WSEndpoint, new() + { + WebSocketServices.AddService(path, null); + } - public event EventHandler OnConnect; + public void AddWebSocketService( + string path, Action initializer + ) + where TEndpoint : WSEndpoint, new() + { + WebSocketServices.AddService(path, initializer); + } - public event EventHandler OnDelete; + public bool RemoveWebSocketService(string path) + { + return WebSocketServices.RemoveService(path); + } - public event EventHandler OnGet; + public void Start() + { + if (IsSecure) + { + if (!checkCertificate(out string message)) + { + throw new InvalidOperationException(message); + } + } - public event EventHandler OnHead; + start(); + } - public event EventHandler OnOptions; + public void Stop() + { + stop((ushort)CloseStatusCode.NoStatus, string.Empty); + } - public event EventHandler OnPatch; + public void Stop(ushort code, string reason) + { + if (!code.IsCloseStatusCode()) + { + var message = "Less than 1000 or greater than 4999."; + throw new ArgumentOutOfRangeException(nameof(code), message); + } - public event EventHandler OnPost; + if (code == (ushort)CloseStatusCode.MandatoryExtension) + { + var message = $"{(ushort)CloseStatusCode.MandatoryExtension} cannot be used."; + throw new ArgumentException(message, nameof(code)); + } - public event EventHandler OnPut; + if (!reason.IsNullOrEmpty()) + { + if (code == (ushort)CloseStatusCode.NoStatus) + { + var message = $"{(ushort)CloseStatusCode.NoStatus} cannot be used."; + throw new ArgumentException(message, nameof(code)); + } - public event EventHandler OnTrace; + if (!reason.TryGetUTF8EncodedBytes(out byte[] bytes)) + { + var message = "It could not be UTF-8-encoded."; + throw new ArgumentException(message, nameof(reason)); + } + + if (bytes.Length > 123) + { + var message = "Its size is greater than 123 bytes."; + throw new ArgumentOutOfRangeException(nameof(reason), message); + } + } + + stop(code, reason); + } + + public void Stop(CloseStatusCode code, string reason) + { + if (code == CloseStatusCode.MandatoryExtension) + { + var message = "MandatoryExtension cannot be used."; + throw new ArgumentException(message, nameof(code)); + } + + if (!reason.IsNullOrEmpty()) + { + if (code == CloseStatusCode.NoStatus) + { + var message = "NoStatus cannot be used."; + throw new ArgumentException(message, nameof(code)); + } + + if (!reason.TryGetUTF8EncodedBytes(out byte[] bytes)) + { + var message = "It could not be UTF-8-encoded."; + throw new ArgumentException(message, nameof(reason)); + } + + if (bytes.Length > 123) + { + var message = "Its size is greater than 123 bytes."; + throw new ArgumentOutOfRangeException(nameof(reason), message); + } + } + + stop((ushort)code, reason); + } + + private static HttpListener createListener( + string hostname, int port, bool secure + ) + { + var lsnr = new HttpListener(); + + var schm = secure ? "https" : "http"; + var pref = string.Format("{0}://{1}:{2}/", schm, hostname, port); + lsnr.Prefixes.Add(pref); + + return lsnr; + } + + private static bool tryCreateUri( + string uriString, out Uri result, out string message + ) + { + result = null; + message = null; + + var uri = uriString.ToUri(); + if (uri == null) + { + message = "An invalid URI string."; + return false; + } + + if (!uri.IsAbsoluteUri) + { + message = "A relative URI."; + return false; + } + + var schm = uri.Scheme; + if (!(schm == "http" || schm == "https")) + { + message = "The scheme part is not 'http' or 'https'."; + return false; + } + + if (uri.PathAndQuery != "/") + { + message = "It includes either or both path and query components."; + return false; + } + + if (uri.Fragment.Length > 0) + { + message = "It includes the fragment component."; + return false; + } + + if (uri.Port == 0) + { + message = "The port part is zero."; + return false; + } + + result = uri; + return true; + } private void abort() { @@ -432,20 +620,6 @@ namespace EonaCat.Network return true; } - - private static HttpListener createListener( - string hostname, int port, bool secure - ) - { - var lsnr = new HttpListener(); - - var schm = secure ? "https" : "http"; - var pref = string.Format("{0}://{1}:{2}/", schm, hostname, port); - lsnr.Prefixes.Add(pref); - - return lsnr; - } - private void init( string hostname, System.Net.IPAddress address, int port, bool secure ) @@ -703,161 +877,5 @@ namespace EonaCat.Network _listener.Stop(); _receiveThread.Join(millisecondsTimeout); } - - private static bool tryCreateUri( - string uriString, out Uri result, out string message - ) - { - result = null; - message = null; - - var uri = uriString.ToUri(); - if (uri == null) - { - message = "An invalid URI string."; - return false; - } - - if (!uri.IsAbsoluteUri) - { - message = "A relative URI."; - return false; - } - - var schm = uri.Scheme; - if (!(schm == "http" || schm == "https")) - { - message = "The scheme part is not 'http' or 'https'."; - return false; - } - - if (uri.PathAndQuery != "/") - { - message = "It includes either or both path and query components."; - return false; - } - - if (uri.Fragment.Length > 0) - { - message = "It includes the fragment component."; - return false; - } - - if (uri.Port == 0) - { - message = "The port part is zero."; - return false; - } - - result = uri; - return true; - } - - public void AddWebSocketService(string path) - where TEndpoint : WSEndpoint, new() - { - WebSocketServices.AddService(path, null); - } - - public void AddWebSocketService( - string path, Action initializer - ) - where TEndpoint : WSEndpoint, new() - { - WebSocketServices.AddService(path, initializer); - } - - public bool RemoveWebSocketService(string path) - { - return WebSocketServices.RemoveService(path); - } - - public void Start() - { - if (IsSecure) - { - if (!checkCertificate(out string message)) - { - throw new InvalidOperationException(message); - } - } - - start(); - } - - public void Stop() - { - stop((ushort)CloseStatusCode.NoStatus, string.Empty); - } - - public void Stop(ushort code, string reason) - { - if (!code.IsCloseStatusCode()) - { - var message = "Less than 1000 or greater than 4999."; - throw new ArgumentOutOfRangeException(nameof(code), message); - } - - if (code == (ushort)CloseStatusCode.MandatoryExtension) - { - var message = $"{(ushort)CloseStatusCode.MandatoryExtension} cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!reason.IsNullOrEmpty()) - { - if (code == (ushort)CloseStatusCode.NoStatus) - { - var message = $"{(ushort)CloseStatusCode.NoStatus} cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!reason.TryGetUTF8EncodedBytes(out byte[] bytes)) - { - var message = "It could not be UTF-8-encoded."; - throw new ArgumentException(message, nameof(reason)); - } - - if (bytes.Length > 123) - { - var message = "Its size is greater than 123 bytes."; - throw new ArgumentOutOfRangeException(nameof(reason), message); - } - } - - stop(code, reason); - } - - public void Stop(CloseStatusCode code, string reason) - { - if (code == CloseStatusCode.MandatoryExtension) - { - var message = "MandatoryExtension cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!reason.IsNullOrEmpty()) - { - if (code == CloseStatusCode.NoStatus) - { - var message = "NoStatus cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!reason.TryGetUTF8EncodedBytes(out byte[] bytes)) - { - var message = "It could not be UTF-8-encoded."; - throw new ArgumentException(message, nameof(reason)); - } - - if (bytes.Length > 123) - { - var message = "Its size is greater than 123 bytes."; - throw new ArgumentOutOfRangeException(nameof(reason), message); - } - } - - stop((ushort)code, reason); - } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Server/WSEndpoint.cs b/EonaCat.Network/System/Sockets/Web/Server/WSEndpoint.cs index da7b67c..6f60b6e 100644 --- a/EonaCat.Network/System/Sockets/Web/Server/WSEndpoint.cs +++ b/EonaCat.Network/System/Sockets/Web/Server/WSEndpoint.cs @@ -17,12 +17,8 @@ namespace EonaCat.Network StartTime = DateTime.MaxValue; } - protected WSSessionManager Sessions { get; private set; } - public WSContext Context { get; private set; } - public Func CookiesValidator { get; set; } - public bool EmitOnPing { get @@ -43,11 +39,8 @@ namespace EonaCat.Network } public string ID { get; private set; } - public bool IgnoreExtensions { get; set; } - public Func OriginValidator { get; set; } - public string Protocol { get @@ -72,53 +65,8 @@ namespace EonaCat.Network } public DateTime StartTime { get; private set; } - public WSState State => _websocket != null ? _websocket.ReadyState : WSState.Connecting; - - private string checkHandshakeRequest(WSContext context) - { - return OriginValidator != null && !OriginValidator(context.Origin) - ? "Includes no Origin header, or it has an invalid value." - : CookiesValidator != null - && !CookiesValidator(context.CookieCollection, context.WebSocket.CookieCollection) - ? "Includes no cookie, or an invalid cookie exists." - : null; - } - - private void onClose(object sender, CloseEventArgs e) - { - if (ID == null) - { - return; - } - - Sessions.Remove(ID); - OnClose(e); - } - - private void onError(object sender, ErrorEventArgs e) - { - OnError(e); - } - - private void onMessage(object sender, MessageEventArgs e) - { - OnMessage(e); - } - - private void onOpen(object sender, EventArgs e) - { - ID = Sessions.Add(this); - if (ID == null) - { - _websocket.Close(CloseStatusCode.Away); - return; - } - - StartTime = DateTime.Now; - OnOpen(); - } - + protected WSSessionManager Sessions { get; private set; } internal void Start(WSContext context, WSSessionManager sessions) { if (_websocket != null) @@ -210,5 +158,49 @@ namespace EonaCat.Network { _websocket?.SendAsync(stream, length, completed); } + + private string checkHandshakeRequest(WSContext context) + { + return OriginValidator != null && !OriginValidator(context.Origin) + ? "Includes no Origin header, or it has an invalid value." + : CookiesValidator != null + && !CookiesValidator(context.CookieCollection, context.WebSocket.CookieCollection) + ? "Includes no cookie, or an invalid cookie exists." + : null; + } + + private void onClose(object sender, CloseEventArgs e) + { + if (ID == null) + { + return; + } + + Sessions.Remove(ID); + OnClose(e); + } + + private void onError(object sender, ErrorEventArgs e) + { + OnError(e); + } + + private void onMessage(object sender, MessageEventArgs e) + { + OnMessage(e); + } + + private void onOpen(object sender, EventArgs e) + { + ID = Sessions.Add(this); + if (ID == null) + { + _websocket.Close(CloseStatusCode.Away); + return; + } + + StartTime = DateTime.Now; + OnOpen(); + } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Server/WSEndpointHost.cs b/EonaCat.Network/System/Sockets/Web/Server/WSEndpointHost.cs index fd1590b..d711343 100644 --- a/EonaCat.Network/System/Sockets/Web/Server/WSEndpointHost.cs +++ b/EonaCat.Network/System/Sockets/Web/Server/WSEndpointHost.cs @@ -13,8 +13,7 @@ namespace EonaCat.Network Sessions = new WSSessionManager(); } - internal ServerState State => Sessions.State; - + public abstract Type EndpointType { get; } public bool KeepClean { get @@ -29,11 +28,7 @@ namespace EonaCat.Network } public string Path { get; } - public WSSessionManager Sessions { get; } - - public abstract Type EndpointType { get; } - public TimeSpan WaitTime { get @@ -47,6 +42,7 @@ namespace EonaCat.Network } } + internal ServerState State => Sessions.State; internal void Start() { Sessions.Start(); diff --git a/EonaCat.Network/System/Sockets/Web/Server/WSEndpointManager.cs b/EonaCat.Network/System/Sockets/Web/Server/WSEndpointManager.cs index 173d44b..663166e 100644 --- a/EonaCat.Network/System/Sockets/Web/Server/WSEndpointManager.cs +++ b/EonaCat.Network/System/Sockets/Web/Server/WSEndpointManager.cs @@ -11,10 +11,10 @@ namespace EonaCat.Network { public class WSEndpointManager { - private volatile bool _clean; private readonly Dictionary _hosts; - private volatile ServerState _state; private readonly object _sync; + private volatile bool _clean; + private volatile ServerState _state; private TimeSpan _waitTime; internal WSEndpointManager() @@ -26,59 +26,6 @@ namespace EonaCat.Network _waitTime = TimeSpan.FromSeconds(1); } - public int Count - { - get - { - lock (_sync) - { - return _hosts.Count; - } - } - } - - public IEnumerable Hosts - { - get - { - lock (_sync) - { - return _hosts.Values.ToList(); - } - } - } - - public WSEndpointHost this[string path] - { - get - { - if (path == null) - { - throw new ArgumentNullException(nameof(path)); - } - - if (path.Length == 0) - { - throw new ArgumentException("An empty string.", nameof(path)); - } - - if (path[0] != '/') - { - throw new ArgumentException("Not an absolute path.", nameof(path)); - } - - if (path.IndexOfAny(new[] { '?', '#' }) > -1) - { - var message = "It includes either or both query and fragment components."; - throw new ArgumentException(message, nameof(path)); - } - - InternalTryGetServiceHost(path, out WSEndpointHost host); - - return host; - } - } - public bool AutoCleanSessions { get @@ -112,6 +59,28 @@ namespace EonaCat.Network } } + public int Count + { + get + { + lock (_sync) + { + return _hosts.Count; + } + } + } + + public IEnumerable Hosts + { + get + { + lock (_sync) + { + return _hosts.Values.ToList(); + } + } + } + public IEnumerable Paths { get @@ -161,169 +130,36 @@ namespace EonaCat.Network } } - private void broadcast(OperationCode opcode, byte[] data, Action completed) + public WSEndpointHost this[string path] { - var cache = new Dictionary(); - - try + get { - foreach (var host in Hosts) + if (path == null) { - if (_state != ServerState.Start) - { - Logger.Error("The server is shutting down."); - break; - } - - host.Sessions.Broadcast(opcode, data, cache); + throw new ArgumentNullException(nameof(path)); } - if (completed != null) + if (path.Length == 0) { - completed(); + throw new ArgumentException("An empty string.", nameof(path)); } - } - catch (Exception ex) - { - Logger.Error(ex, "Could not broadcast"); - } - finally - { - cache.Clear(); + + if (path[0] != '/') + { + throw new ArgumentException("Not an absolute path.", nameof(path)); + } + + if (path.IndexOfAny(new[] { '?', '#' }) > -1) + { + var message = "It includes either or both query and fragment components."; + throw new ArgumentException(message, nameof(path)); + } + + InternalTryGetServiceHost(path, out WSEndpointHost host); + + return host; } } - - private void broadcast(OperationCode opcode, Stream stream, Action completed) - { - var cache = new Dictionary(); - - try - { - foreach (var host in Hosts) - { - if (_state != ServerState.Start) - { - Logger.Error("The server is shutting down."); - break; - } - - host.Sessions.Broadcast(opcode, stream, cache); - } - - if (completed != null) - { - completed(); - } - } - catch (Exception ex) - { - Logger.Error(ex, "Could not broadcast"); - } - finally - { - foreach (var cached in cache.Values) - { - cached.Dispose(); - } - - cache.Clear(); - } - } - - private bool canSet(out string message) - { - message = null; - - if (_state == ServerState.Start) - { - message = "The server has already started."; - return false; - } - - if (_state == ServerState.ShuttingDown) - { - message = "The server is shutting down."; - return false; - } - - return true; - } - - internal void Add(string path, Func creator) - where TEndpoint : WSEndpoint - { - path = HttpUtility.UrlDecode(path).TrimSlashFromEnd(); - - lock (_sync) - { - if (_hosts.TryGetValue(path, out WSEndpointHost host)) - { - throw new ArgumentException("Already in use.", nameof(path)); - } - - host = new WebSocketEndpointHost( - path, creator, null - ); - - if (!_clean) - { - host.KeepClean = false; - } - - if (_waitTime != host.WaitTime) - { - host.WaitTime = _waitTime; - } - - if (_state == ServerState.Start) - { - host.Start(); - } - - _hosts.Add(path, host); - } - } - - internal bool InternalTryGetServiceHost( - string path, out WSEndpointHost host - ) - { - path = HttpUtility.UrlDecode(path).TrimSlashFromEnd(); - - lock (_sync) - { - return _hosts.TryGetValue(path, out host); - } - } - - internal void Start() - { - lock (_sync) - { - foreach (var host in _hosts.Values) - { - host.Start(); - } - - _state = ServerState.Start; - } - } - - internal void Stop(ushort code, string reason) - { - lock (_sync) - { - _state = ServerState.ShuttingDown; - - foreach (var host in _hosts.Values) - { - host.Stop(code, reason); - } - - _state = ServerState.Stop; - } - } - public void AddService( string path, Action initializer ) @@ -470,5 +306,168 @@ namespace EonaCat.Network return InternalTryGetServiceHost(path, out host); } + + internal void Add(string path, Func creator) + where TEndpoint : WSEndpoint + { + path = HttpUtility.UrlDecode(path).TrimSlashFromEnd(); + + lock (_sync) + { + if (_hosts.TryGetValue(path, out WSEndpointHost host)) + { + throw new ArgumentException("Already in use.", nameof(path)); + } + + host = new WebSocketEndpointHost( + path, creator, null + ); + + if (!_clean) + { + host.KeepClean = false; + } + + if (_waitTime != host.WaitTime) + { + host.WaitTime = _waitTime; + } + + if (_state == ServerState.Start) + { + host.Start(); + } + + _hosts.Add(path, host); + } + } + + internal bool InternalTryGetServiceHost( + string path, out WSEndpointHost host + ) + { + path = HttpUtility.UrlDecode(path).TrimSlashFromEnd(); + + lock (_sync) + { + return _hosts.TryGetValue(path, out host); + } + } + + internal void Start() + { + lock (_sync) + { + foreach (var host in _hosts.Values) + { + host.Start(); + } + + _state = ServerState.Start; + } + } + + internal void Stop(ushort code, string reason) + { + lock (_sync) + { + _state = ServerState.ShuttingDown; + + foreach (var host in _hosts.Values) + { + host.Stop(code, reason); + } + + _state = ServerState.Stop; + } + } + + private void broadcast(OperationCode opcode, byte[] data, Action completed) + { + var cache = new Dictionary(); + + try + { + foreach (var host in Hosts) + { + if (_state != ServerState.Start) + { + Logger.Error("The server is shutting down."); + break; + } + + host.Sessions.Broadcast(opcode, data, cache); + } + + if (completed != null) + { + completed(); + } + } + catch (Exception ex) + { + Logger.Error(ex, "Could not broadcast"); + } + finally + { + cache.Clear(); + } + } + + private void broadcast(OperationCode opcode, Stream stream, Action completed) + { + var cache = new Dictionary(); + + try + { + foreach (var host in Hosts) + { + if (_state != ServerState.Start) + { + Logger.Error("The server is shutting down."); + break; + } + + host.Sessions.Broadcast(opcode, stream, cache); + } + + if (completed != null) + { + completed(); + } + } + catch (Exception ex) + { + Logger.Error(ex, "Could not broadcast"); + } + finally + { + foreach (var cached in cache.Values) + { + cached.Dispose(); + } + + cache.Clear(); + } + } + + private bool canSet(out string message) + { + message = null; + + if (_state == ServerState.Start) + { + message = "The server has already started."; + return false; + } + + if (_state == ServerState.ShuttingDown) + { + message = "The server is shutting down."; + return false; + } + + return true; + } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Server/WSServer.cs b/EonaCat.Network/System/Sockets/Web/Server/WSServer.cs index 9088118..ef74e8e 100644 --- a/EonaCat.Network/System/Sockets/Web/Server/WSServer.cs +++ b/EonaCat.Network/System/Sockets/Web/Server/WSServer.cs @@ -10,11 +10,9 @@ namespace EonaCat.Network { public class WSServer { - public bool IsConsoleLoggingEnabled { get; set; } - public bool IsLoggingEnabled { get; set; } + private static readonly string _defaultRealm; private bool _allowForwardedRequest; private AuthenticationSchemes _authSchemes; - private static readonly string _defaultRealm; private bool _dnsStyle; private string _hostname; private TcpListener _listener; @@ -27,7 +25,6 @@ namespace EonaCat.Network private volatile ServerState _state; private object _sync; private Func _userCredentialsFinder; - static WSServer() { _defaultRealm = "SECRET AREA"; @@ -35,8 +32,8 @@ namespace EonaCat.Network public WSServer() { - var addr = System.Net.IPAddress.Any; - init(addr.ToString(), addr, 80, false); + var address = System.Net.IPAddress.Any; + init(address.ToString(), address, 80, false); } public WSServer(int port) @@ -118,7 +115,6 @@ namespace EonaCat.Network } public System.Net.IPAddress Address { get; private set; } - public bool AllowForwardedRequest { get @@ -175,10 +171,6 @@ namespace EonaCat.Network } } - public bool IsListening => _state == ServerState.Start; - - public bool IsSecure { get; private set; } - /// /// Determines if sessions need to be removed automatically /// @@ -195,6 +187,39 @@ namespace EonaCat.Network } } + public WSEndpointManager Endpoints { get; private set; } + public Func FindCredentials + { + get + { + return _userCredentialsFinder; + } + + set + { + if (!CanSet(out string message)) + { + Logger.Warning(message); + return; + } + + lock (_sync) + { + if (!CanSet(out message)) + { + Logger.Warning(message); + return; + } + + _userCredentialsFinder = value; + } + } + } + + public bool IsConsoleLoggingEnabled { get; set; } + public bool IsListening => _state == ServerState.Start; + public bool IsLoggingEnabled { get; set; } + public bool IsSecure { get; private set; } public int Port { get; private set; } public string Realm @@ -266,35 +291,6 @@ namespace EonaCat.Network return GetSSLConfig(); } } - - public Func FindCredentials - { - get - { - return _userCredentialsFinder; - } - - set - { - if (!CanSet(out string message)) - { - Logger.Warning(message); - return; - } - - lock (_sync) - { - if (!CanSet(out message)) - { - Logger.Warning(message); - return; - } - - _userCredentialsFinder = value; - } - } - } - public TimeSpan WaitTime { get @@ -307,8 +303,146 @@ namespace EonaCat.Network Endpoints.WaitTime = value; } } + public void AddEndpoint(string path) where TEndpoint : WSEndpoint, new() + { + Endpoints.AddService(path, null); + } - public WSEndpointManager Endpoints { get; private set; } + public void AddEndpoint(string path, Action initializer) where TEndpoint : WSEndpoint, new() + { + Endpoints.AddService(path, initializer); + } + + public bool RemoveEndpoint(string path) + { + return Endpoints.RemoveService(path); + } + + public void Start() + { + SSLConfigServer sslConfig = null; + + if (IsSecure) + { + sslConfig = new SSLConfigServer(GetSSLConfig()); + + if (!CheckSslConfig(sslConfig, out string message)) + { + throw new InvalidOperationException(message); + } + } + + start(sslConfig); + } + + public void Stop() + { + stop((ushort)CloseStatusCode.NoStatus, string.Empty); + } + + public void Stop(ushort code, string reason) + { + if (!code.IsCloseStatusCode()) + { + var message = "Less than 1000 or greater than 4999."; + throw new ArgumentOutOfRangeException(nameof(code), message); + } + + if (code == (ushort)CloseStatusCode.MandatoryExtension) + { + var message = $"{(ushort)CloseStatusCode.MandatoryExtension} cannot be used."; + throw new ArgumentException(message, nameof(code)); + } + + if (!reason.IsNullOrEmpty()) + { + if (code == (ushort)CloseStatusCode.NoStatus) + { + var message = $"{(ushort)CloseStatusCode.NoStatus} cannot be used."; + throw new ArgumentException(message, nameof(code)); + } + + if (!reason.TryGetUTF8EncodedBytes(out byte[] bytes)) + { + var message = "It could not be UTF-8-encoded."; + throw new ArgumentException(message, nameof(reason)); + } + + if (bytes.Length > 123) + { + var message = "Its size is greater than 123 bytes."; + throw new ArgumentOutOfRangeException(nameof(reason), message); + } + } + + stop(code, reason); + } + + public void Stop(CloseStatusCode code, string reason) + { + if (code == CloseStatusCode.MandatoryExtension) + { + var message = "MandatoryExtension cannot be used."; + throw new ArgumentException(message, nameof(code)); + } + + if (!reason.IsNullOrEmpty()) + { + if (code == CloseStatusCode.NoStatus) + { + var message = "NoStatus cannot be used."; + throw new ArgumentException(message, nameof(code)); + } + + if (!reason.TryGetUTF8EncodedBytes(out byte[] bytes)) + { + var message = "It could not be UTF-8-encoded."; + throw new ArgumentException(message, nameof(reason)); + } + + if (bytes.Length > 123) + { + var message = "Its size is greater than 123 bytes."; + throw new ArgumentOutOfRangeException(nameof(reason), message); + } + } + + stop((ushort)code, reason); + } + + private static bool CheckSslConfig(SSLConfigServer sslConfig, out string message + ) + { + message = null; + + if (sslConfig.Certificate == null) + { + message = "There is no server certificate for secure connections."; + return false; + } + + return true; + } + + private static bool tryCreateUri( + string uriString, out Uri result, out string message + ) + { + if (!uriString.TryCreateWebSocketUri(out result, out message)) + { + return false; + } + + if (result.PathAndQuery != "/") + { + result = null; + message = "It includes either or both path and query components."; + + return false; + } + + return true; + } private void abort() { @@ -365,21 +499,6 @@ namespace EonaCat.Network || Uri.CheckHostName(name) != UriHostNameType.Dns || name == _hostname; } - - private static bool CheckSslConfig(SSLConfigServer sslConfig, out string message - ) - { - message = null; - - if (sslConfig.Certificate == null) - { - message = "There is no server certificate for secure connections."; - return false; - } - - return true; - } - private string GetRealm() { var realm = _realm; @@ -662,132 +781,5 @@ namespace EonaCat.Network _receiveThread.Join(millisecondsTimeout); } - - private static bool tryCreateUri( - string uriString, out Uri result, out string message - ) - { - if (!uriString.TryCreateWebSocketUri(out result, out message)) - { - return false; - } - - if (result.PathAndQuery != "/") - { - result = null; - message = "It includes either or both path and query components."; - - return false; - } - - return true; - } - - public void AddEndpoint(string path) where TEndpoint : WSEndpoint, new() - { - Endpoints.AddService(path, null); - } - - public void AddEndpoint(string path, Action initializer) where TEndpoint : WSEndpoint, new() - { - Endpoints.AddService(path, initializer); - } - - public bool RemoveEndpoint(string path) - { - return Endpoints.RemoveService(path); - } - - public void Start() - { - SSLConfigServer sslConfig = null; - - if (IsSecure) - { - sslConfig = new SSLConfigServer(GetSSLConfig()); - - if (!CheckSslConfig(sslConfig, out string message)) - { - throw new InvalidOperationException(message); - } - } - - start(sslConfig); - } - - public void Stop() - { - stop((ushort)CloseStatusCode.NoStatus, string.Empty); - } - - public void Stop(ushort code, string reason) - { - if (!code.IsCloseStatusCode()) - { - var message = "Less than 1000 or greater than 4999."; - throw new ArgumentOutOfRangeException(nameof(code), message); - } - - if (code == (ushort)CloseStatusCode.MandatoryExtension) - { - var message = $"{(ushort)CloseStatusCode.MandatoryExtension} cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!reason.IsNullOrEmpty()) - { - if (code == (ushort)CloseStatusCode.NoStatus) - { - var message = $"{(ushort)CloseStatusCode.NoStatus} cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!reason.TryGetUTF8EncodedBytes(out byte[] bytes)) - { - var message = "It could not be UTF-8-encoded."; - throw new ArgumentException(message, nameof(reason)); - } - - if (bytes.Length > 123) - { - var message = "Its size is greater than 123 bytes."; - throw new ArgumentOutOfRangeException(nameof(reason), message); - } - } - - stop(code, reason); - } - - public void Stop(CloseStatusCode code, string reason) - { - if (code == CloseStatusCode.MandatoryExtension) - { - var message = "MandatoryExtension cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!reason.IsNullOrEmpty()) - { - if (code == CloseStatusCode.NoStatus) - { - var message = "NoStatus cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!reason.TryGetUTF8EncodedBytes(out byte[] bytes)) - { - var message = "It could not be UTF-8-encoded."; - throw new ArgumentException(message, nameof(reason)); - } - - if (bytes.Length > 123) - { - var message = "Its size is greater than 123 bytes."; - throw new ArgumentOutOfRangeException(nameof(reason), message); - } - } - - stop((ushort)code, reason); - } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Server/WSSessionManager.cs b/EonaCat.Network/System/Sockets/Web/Server/WSSessionManager.cs index eb76222..edb6a2b 100644 --- a/EonaCat.Network/System/Sockets/Web/Server/WSSessionManager.cs +++ b/EonaCat.Network/System/Sockets/Web/Server/WSSessionManager.cs @@ -12,13 +12,13 @@ namespace EonaCat.Network { public class WSSessionManager { - private volatile bool _clean; private readonly object _forSweep; private readonly Dictionary _sessions; + private readonly object _sync; + private volatile bool _clean; private volatile ServerState _state; private volatile bool _sweeping; private System.Timers.Timer _sweepTimer; - private readonly object _sync; private TimeSpan _waitTime; internal WSSessionManager() @@ -33,8 +33,6 @@ namespace EonaCat.Network setSweepTimer(60000); } - internal ServerState State => _state; - public IEnumerable ActiveIDs { get @@ -95,26 +93,6 @@ namespace EonaCat.Network } } - public IWSSession this[string id] - { - get - { - if (id == null) - { - throw new ArgumentNullException(nameof(id)); - } - - if (id.Length == 0) - { - throw new ArgumentException("An empty string.", nameof(id)); - } - - tryGetSession(id, out IWSSession session); - - return session; - } - } - public bool KeepClean { get @@ -197,277 +175,26 @@ namespace EonaCat.Network } } - private void broadcast(OperationCode opcode, byte[] data, Action completed) + internal ServerState State => _state; + public IWSSession this[string id] { - var cache = new Dictionary(); - - try + get { - foreach (var session in Sessions) + if (id == null) { - if (_state != ServerState.Start) - { - Logger.Error("The service is shutting down."); - break; - } - - session.Context.WebSocket.Send(opcode, data, cache); + throw new ArgumentNullException(nameof(id)); } - if (completed != null) + if (id.Length == 0) { - completed(); - } - } - catch (Exception ex) - { - Logger.Error(ex.Message); - Logger.Debug(ex.ToString()); - } - finally - { - cache.Clear(); - } - } - - private void broadcast(OperationCode opcode, Stream stream, Action completed) - { - var cache = new Dictionary(); - - try - { - foreach (var session in Sessions) - { - if (_state != ServerState.Start) - { - Logger.Error("The service is shutting down."); - break; - } - - session.Context.WebSocket.Send(opcode, stream, cache); + throw new ArgumentException("An empty string.", nameof(id)); } - if (completed != null) - { - completed(); - } - } - catch (Exception ex) - { - Logger.Error(ex.Message); - Logger.Debug(ex.ToString()); - } - finally - { - foreach (var cached in cache.Values) - { - cached.Dispose(); - } + tryGetSession(id, out IWSSession session); - cache.Clear(); + return session; } } - - private void broadcastAsync(OperationCode opcode, byte[] data, Action completed) - { - ThreadPool.QueueUserWorkItem( - state => broadcast(opcode, data, completed) - ); - } - - private void broadcastAsync(OperationCode opcode, Stream stream, Action completed) - { - ThreadPool.QueueUserWorkItem( - state => broadcast(opcode, stream, completed) - ); - } - - private Dictionary broadping(byte[] frameAsBytes) - { - var ret = new Dictionary(); - - foreach (var session in Sessions) - { - if (_state != ServerState.Start) - { - Logger.Error("The service is shutting down."); - break; - } - - var res = session.Context.WebSocket.Ping(frameAsBytes, _waitTime); - ret.Add(session.ID, res); - } - - return ret; - } - - private bool canSet(out string message) - { - message = null; - - if (_state == ServerState.Start) - { - message = "The service has already started."; - return false; - } - - if (_state == ServerState.ShuttingDown) - { - message = "The service is shutting down."; - return false; - } - - return true; - } - - private static string createID() - { - return Guid.NewGuid().ToString("N"); - } - - private void setSweepTimer(double interval) - { - _sweepTimer = new System.Timers.Timer(interval); - _sweepTimer.Elapsed += (sender, e) => Sweep(); - } - - private void stop(PayloadData payloadData, bool send) - { - var bytes = send - ? WSFrame.CreateCloseFrame(payloadData, false).ToArray() - : null; - - lock (_sync) - { - _state = ServerState.ShuttingDown; - - _sweepTimer.Enabled = false; - foreach (var session in _sessions.Values.ToList()) - { - session.Context.WebSocket.Close(payloadData, bytes); - } - - _state = ServerState.Stop; - } - } - - private bool tryGetSession(string id, out IWSSession session) - { - session = null; - - if (_state != ServerState.Start) - { - return false; - } - - lock (_sync) - { - if (_state != ServerState.Start) - { - return false; - } - - return _sessions.TryGetValue(id, out session); - } - } - - internal string Add(IWSSession session) - { - lock (_sync) - { - if (_state != ServerState.Start) - { - return null; - } - - var id = createID(); - _sessions.Add(id, session); - - return id; - } - } - - internal void Broadcast( - OperationCode opcode, byte[] data, Dictionary cache - ) - { - foreach (var session in Sessions) - { - if (_state != ServerState.Start) - { - Logger.Error("The service is shutting down."); - break; - } - - session.Context.WebSocket.Send(opcode, data, cache); - } - } - - internal void Broadcast( - OperationCode opcode, Stream stream, Dictionary cache - ) - { - foreach (var session in Sessions) - { - if (_state != ServerState.Start) - { - Logger.Error("The service is shutting down."); - break; - } - - session.Context.WebSocket.Send(opcode, stream, cache); - } - } - - internal Dictionary Broadping( - byte[] frameAsBytes, TimeSpan timeout - ) - { - var ret = new Dictionary(); - - foreach (var session in Sessions) - { - if (_state != ServerState.Start) - { - Logger.Error("The service is shutting down."); - break; - } - - var res = session.Context.WebSocket.Ping(frameAsBytes, timeout); - ret.Add(session.ID, res); - } - - return ret; - } - - internal bool Remove(string id) - { - lock (_sync) - { - return _sessions.Remove(id); - } - } - - internal void Start() - { - lock (_sync) - { - _sweepTimer.Enabled = _clean; - _state = ServerState.Start; - } - } - - internal void Stop(ushort code, string reason) - { - if (code == (ushort)CloseStatusCode.NoStatus) - { // == no status - stop(PayloadData.Empty, true); - return; - } - - stop(new PayloadData(code, reason), !code.IsReserved()); - } - public void Broadcast(byte[] data) { if (_state != ServerState.Start) @@ -872,5 +599,275 @@ namespace EonaCat.Network return tryGetSession(id, out session); } + + internal string Add(IWSSession session) + { + lock (_sync) + { + if (_state != ServerState.Start) + { + return null; + } + + var id = createID(); + _sessions.Add(id, session); + + return id; + } + } + + internal void Broadcast( + OperationCode opcode, byte[] data, Dictionary cache + ) + { + foreach (var session in Sessions) + { + if (_state != ServerState.Start) + { + Logger.Error("The service is shutting down."); + break; + } + + session.Context.WebSocket.Send(opcode, data, cache); + } + } + + internal void Broadcast( + OperationCode opcode, Stream stream, Dictionary cache + ) + { + foreach (var session in Sessions) + { + if (_state != ServerState.Start) + { + Logger.Error("The service is shutting down."); + break; + } + + session.Context.WebSocket.Send(opcode, stream, cache); + } + } + + internal Dictionary Broadping( + byte[] frameAsBytes, TimeSpan timeout + ) + { + var ret = new Dictionary(); + + foreach (var session in Sessions) + { + if (_state != ServerState.Start) + { + Logger.Error("The service is shutting down."); + break; + } + + var res = session.Context.WebSocket.Ping(frameAsBytes, timeout); + ret.Add(session.ID, res); + } + + return ret; + } + + internal bool Remove(string id) + { + lock (_sync) + { + return _sessions.Remove(id); + } + } + + internal void Start() + { + lock (_sync) + { + _sweepTimer.Enabled = _clean; + _state = ServerState.Start; + } + } + + internal void Stop(ushort code, string reason) + { + if (code == (ushort)CloseStatusCode.NoStatus) + { // == no status + stop(PayloadData.Empty, true); + return; + } + + stop(new PayloadData(code, reason), !code.IsReserved()); + } + + private static string createID() + { + return Guid.NewGuid().ToString("N"); + } + + private void broadcast(OperationCode opcode, byte[] data, Action completed) + { + var cache = new Dictionary(); + + try + { + foreach (var session in Sessions) + { + if (_state != ServerState.Start) + { + Logger.Error("The service is shutting down."); + break; + } + + session.Context.WebSocket.Send(opcode, data, cache); + } + + if (completed != null) + { + completed(); + } + } + catch (Exception ex) + { + Logger.Error(ex.Message); + Logger.Debug(ex.ToString()); + } + finally + { + cache.Clear(); + } + } + + private void broadcast(OperationCode opcode, Stream stream, Action completed) + { + var cache = new Dictionary(); + + try + { + foreach (var session in Sessions) + { + if (_state != ServerState.Start) + { + Logger.Error("The service is shutting down."); + break; + } + + session.Context.WebSocket.Send(opcode, stream, cache); + } + + if (completed != null) + { + completed(); + } + } + catch (Exception ex) + { + Logger.Error(ex.Message); + Logger.Debug(ex.ToString()); + } + finally + { + foreach (var cached in cache.Values) + { + cached.Dispose(); + } + + cache.Clear(); + } + } + + private void broadcastAsync(OperationCode opcode, byte[] data, Action completed) + { + ThreadPool.QueueUserWorkItem( + state => broadcast(opcode, data, completed) + ); + } + + private void broadcastAsync(OperationCode opcode, Stream stream, Action completed) + { + ThreadPool.QueueUserWorkItem( + state => broadcast(opcode, stream, completed) + ); + } + + private Dictionary broadping(byte[] frameAsBytes) + { + var ret = new Dictionary(); + + foreach (var session in Sessions) + { + if (_state != ServerState.Start) + { + Logger.Error("The service is shutting down."); + break; + } + + var res = session.Context.WebSocket.Ping(frameAsBytes, _waitTime); + ret.Add(session.ID, res); + } + + return ret; + } + + private bool canSet(out string message) + { + message = null; + + if (_state == ServerState.Start) + { + message = "The service has already started."; + return false; + } + + if (_state == ServerState.ShuttingDown) + { + message = "The service is shutting down."; + return false; + } + + return true; + } + private void setSweepTimer(double interval) + { + _sweepTimer = new System.Timers.Timer(interval); + _sweepTimer.Elapsed += (sender, e) => Sweep(); + } + + private void stop(PayloadData payloadData, bool send) + { + var bytes = send + ? WSFrame.CreateCloseFrame(payloadData, false).ToArray() + : null; + + lock (_sync) + { + _state = ServerState.ShuttingDown; + + _sweepTimer.Enabled = false; + foreach (var session in _sessions.Values.ToList()) + { + session.Context.WebSocket.Close(payloadData, bytes); + } + + _state = ServerState.Stop; + } + } + + private bool tryGetSession(string id, out IWSSession session) + { + session = null; + + if (_state != ServerState.Start) + { + return false; + } + + lock (_sync) + { + if (_state != ServerState.Start) + { + return false; + } + + return _sessions.TryGetValue(id, out session); + } + } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Server/WebSocketEndpointHost.cs b/EonaCat.Network/System/Sockets/Web/Server/WebSocketEndpointHost.cs index ec50ed6..fe211f8 100644 --- a/EonaCat.Network/System/Sockets/Web/Server/WebSocketEndpointHost.cs +++ b/EonaCat.Network/System/Sockets/Web/Server/WebSocketEndpointHost.cs @@ -29,8 +29,13 @@ namespace EonaCat.Network public override Type EndpointType => typeof(TEndpoint); + protected override WSEndpoint CreateSession() + { + return _creator(); + } + private Func createCreator( - Func creator, Action initializer + Func creator, Action initializer ) { if (initializer == null) @@ -46,10 +51,5 @@ namespace EonaCat.Network return ret; }; } - - protected override WSEndpoint CreateSession() - { - return _creator(); - } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/WSClient.cs b/EonaCat.Network/System/Sockets/Web/WSClient.cs index 62657ca..eda7872 100644 --- a/EonaCat.Network/System/Sockets/Web/WSClient.cs +++ b/EonaCat.Network/System/Sockets/Web/WSClient.cs @@ -16,13 +16,20 @@ namespace EonaCat.Network { public class WSClient : IDisposable { + internal static readonly byte[] EmptyBytes; + internal static readonly RandomNumberGenerator RandomNumber; + private const string _version = "13"; + private const string _webSocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + private const int FRAGMENT_LENGTH = 1016; + private static readonly int _maxRetryCountForConnect; + private readonly bool _client; + private readonly Action _message; + private readonly string[] _protocols; private AuthenticationChallenge _authChallenge; private string _base64Key; - private readonly bool _client; private Action _closeContext; private CompressionMethod _compression; private WSContext _context; - private bool _isRedirectionEnabled; private string _extensions; private bool _extensionsRequested; private object _forMessageEventQueue; @@ -32,18 +39,15 @@ namespace EonaCat.Network private MemoryStream _fragmentsBuffer; private bool _fragmentsCompressed; private OperationCode _fragmentsOpcode; - private const string _guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; private bool _inContinuation; private volatile bool _inMessage; - private static readonly int _maxRetryCountForConnect; - private readonly Action _message; + private bool _isRedirectionEnabled; private Queue _messageEventQueue; private uint _nonceCount; private string _origin; private ManualResetEvent _pongReceived; private bool _preAuth; private string _protocol; - private readonly string[] _protocols; private bool _protocolsRequested; private NetworkCredential _proxyCredentials; private Uri _proxyUri; @@ -54,16 +58,7 @@ namespace EonaCat.Network private Stream _stream; private TcpClient _tcpClient; private Uri _uri; - private const string _version = "13"; - private const int FRAGMENT_LENGTH = 1016; private TimeSpan _waitTime; - - internal static readonly byte[] EmptyBytes; - - public static int FragmentLength; - - internal static readonly RandomNumberGenerator RandomNumber; - static WSClient() { _maxRetryCountForConnect = 10; @@ -72,36 +67,6 @@ namespace EonaCat.Network RandomNumber = new RNGCryptoServiceProvider(); } - // As server - internal WSClient(HttpListenerWSContext context, string protocol) - { - _context = context; - _protocol = protocol; - - _closeContext = context.Close; - _message = messages; - IsSecure = context.IsSecureConnection; - _stream = context.Stream; - _waitTime = TimeSpan.FromSeconds(1); - - Setup(); - } - - // As server - internal WSClient(TcpListenerWSContext context, string protocol) - { - _context = context; - _protocol = protocol; - - _closeContext = context.Close; - _message = messages; - IsSecure = context.IsSecureConnection; - _stream = context.Stream; - _waitTime = TimeSpan.FromSeconds(1); - - Setup(); - } - public WSClient(string url, params string[] protocols) { if (url == null) @@ -134,28 +99,47 @@ namespace EonaCat.Network Setup(); } - - internal CookieCollection CookieCollection { get; private set; } - - // As server - internal Func CustomHandshakeRequestChecker { get; set; } - - internal bool HasMessage + internal WSClient(HttpListenerWSContext context, string protocol) { - get - { - lock (_forMessageEventQueue) - { - return _messageEventQueue.Count > 0; - } - } + _context = context; + _protocol = protocol; + + _closeContext = context.Close; + _message = messages; + IsSecure = context.IsSecureConnection; + _stream = context.Stream; + _waitTime = TimeSpan.FromSeconds(1); + + Setup(); } - // As server - internal bool IgnoreExtensions { get; set; } + internal WSClient(TcpListenerWSContext context, string protocol) + { + _context = context; + _protocol = protocol; - internal bool IsConnected => _readyState == WSState.Open || _readyState == WSState.Closing; + _closeContext = context.Close; + _message = messages; + IsSecure = context.IsSecureConnection; + _stream = context.Stream; + _waitTime = TimeSpan.FromSeconds(1); + Setup(); + } + + public event EventHandler OnConnect; + + public event EventHandler OnDisconnect; + + public event EventHandler OnError; + + public event EventHandler OnMessageReceived; + + /// + /// Fragment length + /// + public static int FragmentLength { get; set; } + public bool CallMessageOnPing { get; set; } public CompressionMethod Compression { get @@ -207,9 +191,13 @@ namespace EonaCat.Network } public NetworkCredential Credentials { get; private set; } + /// + /// Gets or sets the custom headers + /// + public IEnumerable> CustomHeaders { get; set; } - public bool CallMessageOnPing { get; set; } - + public string Extensions => _extensions ?? string.Empty; + public bool IsAlive => ping(EmptyBytes); public bool IsRedirectionEnabled { get @@ -246,12 +234,7 @@ namespace EonaCat.Network } } - public string Extensions => _extensions ?? string.Empty; - - public bool IsAlive => ping(EmptyBytes); - public bool IsSecure { get; private set; } - public string Origin { get @@ -317,7 +300,6 @@ namespace EonaCat.Network } public WSState ReadyState => _readyState; - public SSLConfigClient SSL { get @@ -340,8 +322,18 @@ namespace EonaCat.Network } } - public Uri Url => _client ? _uri : _context.RequestUri; + /// + /// Gets the TcpClient used for the WebSocket. + /// + public TcpClient TcpClient + { + get + { + return _tcpClient; + } + } + public Uri Url => _client ? _uri : _context.RequestUri; public TimeSpan WaitTime { get @@ -375,1896 +367,23 @@ namespace EonaCat.Network } } - public event EventHandler OnDisconnect; + internal CookieCollection CookieCollection { get; private set; } + internal Func CustomHandshakeRequestChecker { get; set; } - public event EventHandler OnError; - - public event EventHandler OnMessageReceived; - - public event EventHandler OnConnect; - - // As server - private bool accept() + internal bool HasMessage { - lock (_forState) + get { - if (!checkIfAvailable(true, false, false, false, out string message)) - { - Logger.Error(message); - Error("An error has occurred in accepting.", null); - - return false; - } - - try - { - if (!acceptHandshake()) - { - return false; - } - - _readyState = WSState.Open; - } - catch (Exception ex) - { - Logger.Error(ex.ToString()); - fatal("An exception has occurred while accepting.", ex); - - return false; - } - - return true; - } - } - - // As server - private bool acceptHandshake() - { - Logger.Debug($"A request from {_context.UserEndPoint}:\n{_context}"); - - if (!checkHandshakeRequest(_context, out string message)) - { - sendHttpResponse(createHandshakeFailureResponse(HttpStatusCode.BadRequest)); - - Logger.Error(message); - fatal("An error has occurred while accepting.", CloseStatusCode.ProtocolError); - - return false; - } - - if (!customCheckHandshakeRequest(_context, out message)) - { - sendHttpResponse(createHandshakeFailureResponse(HttpStatusCode.BadRequest)); - - Logger.Error(message); - fatal("An error has occurred while accepting.", CloseStatusCode.PolicyViolation); - - return false; - } - - _base64Key = _context.Headers["Sec-WebSocket-Key"]; - - if (_protocol != null) - { - processSecWebSocketProtocolHeader(_context.SecWebSocketProtocols); - } - - if (!IgnoreExtensions) - { - processSecWebSocketExtensionsClientHeader(_context.Headers["Sec-WebSocket-Extensions"]); - } - - return sendHttpResponse(createHandshakeResponse()); - } - - private bool canSet(out string message) - { - message = null; - - if (_readyState == WSState.Open) - { - message = "The connection has already been established."; - return false; - } - - if (_readyState == WSState.Closing) - { - message = "The connection is closing."; - return false; - } - - return true; - } - - // As server - private bool checkHandshakeRequest(WSContext context, out string message) - { - message = null; - - if (context.RequestUri == null) - { - message = "Specifies an invalid Request-URI."; - return false; - } - - if (!context.IsWebSocketRequest) - { - message = "Not a WebSocket handshake request."; - return false; - } - - var headers = context.Headers; - if (!validateSecWebSocketKeyHeader(headers["Sec-WebSocket-Key"])) - { - message = "Includes no Sec-WebSocket-Key header, or it has an invalid value."; - return false; - } - - if (!validateSecWebSocketVersionClientHeader(headers["Sec-WebSocket-Version"])) - { - message = "Includes no Sec-WebSocket-Version header, or it has an invalid value."; - return false; - } - - if (!validateSecWebSocketProtocolClientHeader(headers["Sec-WebSocket-Protocol"])) - { - message = "Includes an invalid Sec-WebSocket-Protocol header."; - return false; - } - - if (!IgnoreExtensions - && !validateSecWebSocketExtensionsClientHeader(headers["Sec-WebSocket-Extensions"]) - ) - { - message = "Includes an invalid Sec-WebSocket-Extensions header."; - return false; - } - - return true; - } - - // As client - private bool checkHandshakeResponse(WebResponse response, out string message) - { - message = null; - - if (response.IsRedirect) - { - message = "Indicates the redirection."; - return false; - } - - if (response.IsUnauthorized) - { - message = "Requires the authentication."; - return false; - } - - if (!response.IsWebSocketResponse) - { - message = "Not a WebSocket handshake response."; - return false; - } - - var headers = response.Headers; - if (!validateSecWebSocketAcceptHeader(headers["Sec-WebSocket-Accept"])) - { - message = "Includes no Sec-WebSocket-Accept header, or it has an invalid value."; - return false; - } - - if (!validateSecWebSocketProtocolServerHeader(headers["Sec-WebSocket-Protocol"])) - { - message = "Includes no Sec-WebSocket-Protocol header, or it has an invalid value."; - return false; - } - - if (!validateSecWebSocketExtensionsServerHeader(headers["Sec-WebSocket-Extensions"])) - { - message = "Includes an invalid Sec-WebSocket-Extensions header."; - return false; - } - - if (!validateSecWebSocketVersionServerHeader(headers["Sec-WebSocket-Version"])) - { - message = "Includes an invalid Sec-WebSocket-Version header."; - return false; - } - - return true; - } - - private bool checkIfAvailable( - bool connecting, bool open, bool closing, bool closed, out string message - ) - { - message = null; - - if (!connecting && _readyState == WSState.Connecting) - { - message = "This operation is not available in: connecting"; - return false; - } - - if (!open && _readyState == WSState.Open) - { - message = "This operation is not available in: open"; - return false; - } - - if (!closing && _readyState == WSState.Closing) - { - message = "This operation is not available in: closing"; - return false; - } - - if (!closed && _readyState == WSState.Closed) - { - message = "This operation is not available in: closed"; - return false; - } - - return true; - } - - private bool checkIfAvailable( - bool client, - bool server, - bool connecting, - bool open, - bool closing, - bool closed, - out string message - ) - { - message = null; - - if (!client && _client) - { - message = "This operation is not available in: client"; - return false; - } - - if (!server && !_client) - { - message = "This operation is not available in: server"; - return false; - } - - return checkIfAvailable(connecting, open, closing, closed, out message); - } - - private static bool checkParametersForSetCredentials( - string username, string password, out string message - ) - { - message = null; - - if (username.IsNullOrEmpty()) - { - return true; - } - - if (username.Contains(':') || !username.IsText()) - { - message = "'username' contains an invalid character."; - return false; - } - - if (password.IsNullOrEmpty()) - { - return true; - } - - if (!password.IsText()) - { - message = "'password' contains an invalid character."; - return false; - } - - return true; - } - - private static bool checkParametersForSetProxy( - string url, string username, string password, out string message - ) - { - message = null; - - if (url.IsNullOrEmpty()) - { - return true; - } - - if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri) - || uri.Scheme != "http" - || uri.Segments.Length > 1 - ) - { - message = "'url' is an invalid URL."; - return false; - } - - if (username.IsNullOrEmpty()) - { - return true; - } - - if (username.Contains(':') || !username.IsText()) - { - message = "'username' contains an invalid character."; - return false; - } - - if (password.IsNullOrEmpty()) - { - return true; - } - - if (!password.IsText()) - { - message = "'password' contains an invalid character."; - return false; - } - - return true; - } - - private static bool checkProtocols(string[] protocols, out string message) - { - message = null; - - Func cond = protocol => protocol.IsNullOrEmpty() - || !protocol.IsToken(); - - if (protocols.Contains(cond)) - { - message = "It contains a value that is not a token."; - return false; - } - - if (protocols.ContainsTwice()) - { - message = "It contains a value twice."; - return false; - } - - return true; - } - - private bool checkReceivedFrame(WSFrame frame, out string message) - { - message = null; - - var masked = frame.IsMasked; - if (_client && masked) - { - message = "A frame from the server is masked."; - return false; - } - - if (!_client && !masked) - { - message = "A frame from a client is not masked."; - return false; - } - - if (_inContinuation && frame.IsData) - { - message = "A data frame has been received while receiving continuation frames."; - return false; - } - - if (frame.IsCompressed && _compression == CompressionMethod.None) - { - message = "A compressed frame has been received without any agreement for it."; - return false; - } - - if (frame.Rsv2 == Rsv.On) - { - message = "The RSV2 of a frame is non-zero without any negotiation for it."; - return false; - } - - if (frame.Rsv3 == Rsv.On) - { - message = "The RSV3 of a frame is non-zero without any negotiation for it."; - return false; - } - - return true; - } - - private void close(ushort code, string reason) - { - if (_readyState == WSState.Closing) - { - Logger.Info("The closing is already in progress."); - return; - } - - if (_readyState == WSState.Closed) - { - Logger.Info("The connection has already been closed."); - return; - } - - if (code == (ushort)CloseStatusCode.NoStatus) - { // == no status - close(PayloadData.Empty, true, true, false); - return; - } - - var send = !code.IsReserved(); - close(new PayloadData(code, reason), send, send, false); - } - - private void close( - PayloadData payloadData, bool send, bool receive, bool received - ) - { - lock (_forState) - { - if (_readyState == WSState.Closing) - { - Logger.Info("The closing is already in progress."); - return; - } - - if (_readyState == WSState.Closed) - { - Logger.Info("The connection has already been closed."); - return; - } - - send = send && _readyState == WSState.Open; - receive = send && receive; - - _readyState = WSState.Closing; - } - - Logger.Trace("Begin closing the connection."); - - var res = closeHandshake(payloadData, send, receive, received); - releaseResources(); - - Logger.Trace("End closing the connection."); - - _readyState = WSState.Closed; - - var e = new CloseEventArgs(payloadData); - e.WasClean = res; - - try - { - OnDisconnect.Emit(this, e); - } - catch (Exception ex) - { - Logger.Error(ex.ToString()); - Error("An error has occurred during the OnClose event.", ex); - } - } - - private void closeAsync(ushort code, string reason) - { - if (_readyState == WSState.Closing) - { - Logger.Info("The closing is already in progress."); - return; - } - - if (_readyState == WSState.Closed) - { - Logger.Info("The connection has already been closed."); - return; - } - - if (code == (ushort)CloseStatusCode.NoStatus) - { // == no status - closeAsync(PayloadData.Empty, true, true, false); - return; - } - - var send = !code.IsReserved(); - closeAsync(new PayloadData(code, reason), send, send, false); - } - - private async Task closeAsync(PayloadData payloadData, bool send, bool receive, bool received) - { - await Task.Run(() => close(payloadData, send, receive, received)); - } - - private bool closeHandshake( - PayloadData payloadData, bool send, bool receive, bool received - ) - { - var sent = false; - if (send) - { - var frame = WSFrame.CreateCloseFrame(payloadData, _client); - sent = sendBytes(frame.ToArray()); - - if (_client) - { - frame.Unmask(); - } - } - - var wait = !received && sent && receive && _receivingExited != null; - if (wait) - { - received = _receivingExited.WaitOne(_waitTime); - } - - var ret = sent && received; - - Logger.Debug( - string.Format( - "Was clean?: {0}\n sent: {1}\n received: {2}", ret, sent, received - ) - ); - - return ret; - } - - // As client - private bool connect() - { - lock (_forState) - { - if (!checkIfAvailable(true, false, false, true, out string message)) - { - Logger.Error(message); - Error("An error has occurred in connecting.", null); - - return false; - } - - if (_retryCountForConnect > _maxRetryCountForConnect) - { - _retryCountForConnect = 0; - Logger.Error("A series of reconnecting has failed."); - - return false; - } - - _readyState = WSState.Connecting; - - try - { - Handshake(); - } - catch (Exception ex) - { - _retryCountForConnect++; - Logger.Error(ex.ToString()); - fatal("An exception has occurred while connecting.", ex); - - return false; - } - - _retryCountForConnect = 1; - _readyState = WSState.Open; - - return true; - } - } - - // As client - private string createExtensions() - { - var buff = new StringBuilder(80); - - if (_compression != CompressionMethod.None) - { - var str = _compression.ToExtensionString( - "server_no_context_takeover", "client_no_context_takeover"); - - buff.AppendFormat("{0}, ", str); - } - - var len = buff.Length; - if (len > 2) - { - buff.Length = len - 2; - return buff.ToString(); - } - - return null; - } - - // As server - private WebResponse createHandshakeFailureResponse(HttpStatusCode code) - { - var ret = WebResponse.CreateCloseResponse(code); - ret.Headers["Sec-WebSocket-Version"] = _version; - - return ret; - } - - // As client - private WebRequest createHandshakeRequest() - { - var ret = WebRequest.CreateWebSocketRequest(_uri); - - var headers = ret.Headers; - if (!_origin.IsNullOrEmpty()) - { - headers["Origin"] = _origin; - } - - headers["Sec-WebSocket-Key"] = _base64Key; - - _protocolsRequested = _protocols != null && _protocols.Length > 0; - if (_protocolsRequested) - { - headers["Sec-WebSocket-Protocol"] = _protocols.ToString(", "); - } - - _extensionsRequested = _compression != CompressionMethod.None; - if (_extensionsRequested) - { - headers["Sec-WebSocket-Extensions"] = createExtensions(); - } - - headers["Sec-WebSocket-Version"] = _version; - - AuthenticationResponse authRes = null; - if (_authChallenge != null && Credentials != null) - { - authRes = new AuthenticationResponse(_authChallenge, Credentials, _nonceCount); - _nonceCount = authRes.NonceCount; - } - else if (_preAuth) - { - authRes = new AuthenticationResponse(Credentials); - } - - if (authRes != null) - { - headers["Authorization"] = authRes.ToString(); - } - - if (CookieCollection.Count > 0) - { - ret.SetCookies(CookieCollection); - } - - return ret; - } - - // As server - private WebResponse createHandshakeResponse() - { - var ret = WebResponse.CreateWebSocketResponse(); - - var headers = ret.Headers; - headers["Sec-WebSocket-Accept"] = CreateResponseKey(_base64Key); - - if (_protocol != null) - { - headers["Sec-WebSocket-Protocol"] = _protocol; - } - - if (_extensions != null) - { - headers["Sec-WebSocket-Extensions"] = _extensions; - } - - if (CookieCollection.Count > 0) - { - ret.SetCookies(CookieCollection); - } - - return ret; - } - - // As server - private bool customCheckHandshakeRequest(WSContext context, out string message) - { - message = null; - return CustomHandshakeRequestChecker == null - || (message = CustomHandshakeRequestChecker(context)) == null; - } - - // As client - private void Handshake() - { - SetClientStream(); - - var response = sendHandshakeRequest(); - if (!checkHandshakeResponse(response, out string message)) - { - throw new WSException(CloseStatusCode.ProtocolError, message); - } - - if (_protocolsRequested) - { - _protocol = response.Headers["Sec-WebSocket-Protocol"]; - } - - if (_extensionsRequested) - { - processSecWebSocketExtensionsServerHeader(response.Headers["Sec-WebSocket-Extensions"]); - } - - processCookies(response.Cookies); - } - - private void EnqueueToMessageEventQueue(MessageEventArgs e) - { - lock (_forMessageEventQueue) - { - _messageEventQueue.Enqueue(e); - } - } - - private void Error(string message, Exception exception) - { - try - { - OnError.Emit(this, new ErrorEventArgs(message, exception)); - } - catch (Exception ex) - { - Logger.Error(ex.ToString()); - } - } - - private void fatal(string message, Exception exception) - { - var code = exception is WSException - ? ((WSException)exception).Code - : CloseStatusCode.Abnormal; - - fatal(message, (ushort)code); - } - - private void fatal(string message, ushort code) - { - var payload = new PayloadData(code, message); - close(payload, !code.IsReserved(), false, false); - } - - private void fatal(string message, CloseStatusCode code) - { - fatal(message, (ushort)code); - } - - private void Setup() - { - _compression = CompressionMethod.None; - CookieCollection = new CookieCollection(); - _forPing = new object(); - _forSend = new object(); - _forState = new object(); - _messageEventQueue = new Queue(); - _forMessageEventQueue = ((ICollection)_messageEventQueue).SyncRoot; - _readyState = WSState.Connecting; - } - - private void message() - { - MessageEventArgs e = null; - lock (_forMessageEventQueue) - { - if (_inMessage || _messageEventQueue.Count == 0 || _readyState != WSState.Open) - { - return; - } - - _inMessage = true; - e = _messageEventQueue.Dequeue(); - } - - _message(e); - } - - private void messagec(MessageEventArgs e) - { - do - { - try - { - OnMessageReceived.Emit(this, e); - } - catch (Exception ex) - { - Logger.Error(ex.ToString()); - Error("An error has occurred during an OnMessage event.", ex); - } - lock (_forMessageEventQueue) { - if (_messageEventQueue.Count == 0 || _readyState != WSState.Open) - { - _inMessage = false; - break; - } - - e = _messageEventQueue.Dequeue(); - } - } - while (true); - } - - private void messages(MessageEventArgs e) - { - try - { - OnMessageReceived.Emit(this, e); - } - catch (Exception ex) - { - Logger.Error(ex.ToString()); - Error("An error has occurred during an OnMessage event.", ex); - } - - lock (_forMessageEventQueue) - { - if (_messageEventQueue.Count == 0 || _readyState != WSState.Open) - { - _inMessage = false; - return; - } - - e = _messageEventQueue.Dequeue(); - } - - ThreadPool.QueueUserWorkItem(state => messages(e)); - } - - private void open() - { - _inMessage = true; - startReceiving(); - try - { - OnConnect.Emit(this, EventArgs.Empty); - } - catch (Exception ex) - { - Logger.Error(ex.ToString()); - Error("An error has occurred during the OnOpen event.", ex); - } - - MessageEventArgs e = null; - lock (_forMessageEventQueue) - { - if (_messageEventQueue.Count == 0 || _readyState != WSState.Open) - { - _inMessage = false; - return; - } - - e = _messageEventQueue.Dequeue(); - } - - Task.Run(() => _message(e)); - } - - private bool ping(byte[] data) - { - if (_readyState != WSState.Open) - { - return false; - } - - var pongReceived = _pongReceived; - if (pongReceived == null) - { - return false; - } - - lock (_forPing) - { - try - { - pongReceived.Reset(); - if (!send(FinalFrame.Final, OperationCode.Ping, data, false)) - { - return false; - } - - return pongReceived.WaitOne(_waitTime); - } - catch (ObjectDisposedException) - { - return false; + return _messageEventQueue.Count > 0; } } } - private bool processCloseFrame(WSFrame frame) - { - var payload = frame.PayloadData; - close(payload, !payload.HasReservedCode, false, true); - - return false; - } - - // As client - private void processCookies(CookieCollection cookies) - { - if (cookies.Count == 0) - { - return; - } - - CookieCollection.SetOrRemove(cookies); - } - - private bool processDataFrame(WSFrame frame) - { - EnqueueToMessageEventQueue( - frame.IsCompressed - ? new MessageEventArgs( - frame.Opcode, frame.PayloadData.ApplicationData.Decompress(_compression)) - : new MessageEventArgs(frame)); - - return true; - } - - private bool processFragmentFrame(WSFrame frame) - { - if (!_inContinuation) - { - // Must process first fragment. - if (frame.IsContinuation) - { - return true; - } - - _fragmentsOpcode = frame.Opcode; - _fragmentsCompressed = frame.IsCompressed; - _fragmentsBuffer = new MemoryStream(); - _inContinuation = true; - } - - _fragmentsBuffer.WriteBytes(frame.PayloadData.ApplicationData, 1024); - if (frame.IsFinal) - { - using (_fragmentsBuffer) - { - var data = _fragmentsCompressed - ? _fragmentsBuffer.DecompressToArray(_compression) - : _fragmentsBuffer.ToArray(); - - EnqueueToMessageEventQueue(new MessageEventArgs(_fragmentsOpcode, data)); - } - - _fragmentsBuffer = null; - _inContinuation = false; - } - - return true; - } - - private bool processPingFrame(WSFrame frame) - { - Logger.Trace("A ping was received."); - - var pong = WSFrame.CreatePongFrame(frame.PayloadData, _client); - - lock (_forState) - { - if (_readyState != WSState.Open) - { - Logger.Error("The connection is closing."); - return true; - } - - if (!sendBytes(pong.ToArray())) - { - return false; - } - } - - Logger.Trace("A pong to this ping has been sent."); - - if (CallMessageOnPing) - { - if (_client) - { - pong.Unmask(); - } - - EnqueueToMessageEventQueue(new MessageEventArgs(frame)); - } - - return true; - } - - private bool processPongFrame(WSFrame frame) - { - Logger.Trace("A pong was received."); - - try - { - _pongReceived.Set(); - } - catch (NullReferenceException ex) - { - Logger.Error(ex.Message); - Logger.Debug(ex.ToString()); - - return false; - } - catch (ObjectDisposedException ex) - { - Logger.Error(ex.Message); - Logger.Debug(ex.ToString()); - - return false; - } - - Logger.Trace("It has been signaled."); - - return true; - } - - private bool processReceivedFrame(WSFrame frame) - { - if (!checkReceivedFrame(frame, out string message)) - { - throw new WSException(CloseStatusCode.ProtocolError, message); - } - - frame.Unmask(); - return frame.IsFragment - ? processFragmentFrame(frame) - : frame.IsData - ? processDataFrame(frame) - : frame.IsPing - ? processPingFrame(frame) - : frame.IsPong - ? processPongFrame(frame) - : frame.IsClose - ? processCloseFrame(frame) - : processUnsupportedFrame(frame); - } - - // As server - private void processSecWebSocketExtensionsClientHeader(string value) - { - if (value == null) - { - return; - } - - var buff = new StringBuilder(80); - - var comp = false; - foreach (var e in value.SplitHeaderValue(',')) - { - var ext = e.Trim(); - if (!comp && ext.IsCompressionExtension(CompressionMethod.Deflate)) - { - _compression = CompressionMethod.Deflate; - buff.AppendFormat( - "{0}, ", - _compression.ToExtensionString( - "client_no_context_takeover", "server_no_context_takeover" - ) - ); - - comp = true; - } - } - - var len = buff.Length; - if (len > 2) - { - buff.Length = len - 2; - _extensions = buff.ToString(); - } - } - - // As client - private void processSecWebSocketExtensionsServerHeader(string value) - { - if (value == null) - { - _compression = CompressionMethod.None; - return; - } - - _extensions = value; - } - - // As server - private void processSecWebSocketProtocolHeader(IEnumerable values) - { - if (values.Contains(p => p == _protocol)) - { - return; - } - - _protocol = null; - } - - private bool processUnsupportedFrame(WSFrame frame) - { - Logger.Error("An unsupported frame:" + frame.PrintToString(false)); - fatal("There is no way to handle it.", CloseStatusCode.PolicyViolation); - - return false; - } - - // As client - private void releaseClientResources() - { - if (_stream != null) - { - _stream.Dispose(); - _stream = null; - } - - if (_tcpClient != null) - { - _tcpClient.Close(); - _tcpClient = null; - } - } - - private void releaseCommonResources() - { - if (_fragmentsBuffer != null) - { - _fragmentsBuffer.Dispose(); - _fragmentsBuffer = null; - _inContinuation = false; - } - - if (_pongReceived != null) - { - _pongReceived.Close(); - _pongReceived = null; - } - - if (_receivingExited != null) - { - _receivingExited.Close(); - _receivingExited = null; - } - } - - private void releaseResources() - { - if (_client) - { - releaseClientResources(); - } - else - { - releaseServerResources(); - } - - releaseCommonResources(); - } - - // As server - private void releaseServerResources() - { - if (_closeContext == null) - { - return; - } - - _closeContext(); - _closeContext = null; - _stream = null; - _context = null; - } - - private bool send(OperationCode 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, stream, compressed); - if (!sent) - { - Error("A send has been interrupted.", null); - } - } - catch (Exception ex) - { - Logger.Error(ex.ToString()); - Error("An error has occurred during a send.", ex); - } - finally - { - if (compressed) - { - stream.Dispose(); - } - - src.Dispose(); - } - - return sent; - } - } - - private bool send(OperationCode opcode, Stream stream, bool compressed) - { - var len = stream.Length; - if (len == 0) - { - return send(FinalFrame.Final, opcode, EmptyBytes, false); - } - - 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(FinalFrame.Final, opcode, buff, compressed); - } - - if (quo == 1 && rem == 0) - { - buff = new byte[FragmentLength]; - return stream.Read(buff, 0, FragmentLength) == FragmentLength - && send(FinalFrame.Final, opcode, buff, compressed); - } - - /* Send fragments */ - - // Begin - buff = new byte[FragmentLength]; - var sent = stream.Read(buff, 0, FragmentLength) == FragmentLength - && send(FinalFrame.More, opcode, buff, compressed); - - if (!sent) - { - return false; - } - - var n = rem == 0 ? quo - 2 : quo - 1; - for (long i = 0; i < n; i++) - { - sent = stream.Read(buff, 0, FragmentLength) == FragmentLength - && send(FinalFrame.More, OperationCode.Cont, buff, false); - - if (!sent) - { - return false; - } - } - - // End - if (rem == 0) - { - rem = FragmentLength; - } - else - { - buff = new byte[rem]; - } - - return stream.Read(buff, 0, rem) == rem - && send(FinalFrame.Final, OperationCode.Cont, buff, false); - } - - private bool send(FinalFrame fin, OperationCode opcode, byte[] data, bool compressed) - { - lock (_forState) - { - if (_readyState != WSState.Open) - { - Logger.Error("The connection is closing."); - return false; - } - - var frame = new WSFrame(fin, opcode, data, compressed, _client); - return sendBytes(frame.ToArray()); - } - } - - private void sendAsync(OperationCode opcode, Stream stream, Action completed) - { - Func sender = send; - Task.Run(() => - { - try - { - var sent = sender(opcode, stream); - completed?.Invoke(sent); - } - catch (Exception ex) - { - Logger.Error(ex.ToString()); - Error("An error has occurred during the callback for an async send.", ex); - } - }); - } - - private bool sendBytes(byte[] bytes) - { - try - { - _stream.Write(bytes, 0, bytes.Length); - } - catch (Exception ex) - { - Logger.Error(ex.Message); - Logger.Debug(ex.ToString()); - - return false; - } - - return true; - } - - // As client - private WebResponse sendHandshakeRequest() - { - var req = createHandshakeRequest(); - var res = sendHttpRequest(req, 90000); - if (res.IsUnauthorized) - { - var chal = res.Headers["WWW-Authenticate"]; - Logger.Warning(string.Format("Received an authentication requirement for '{0}'.", chal)); - if (chal.IsNullOrEmpty()) - { - Logger.Error("No authentication challenge is specified."); - return res; - } - - _authChallenge = AuthenticationChallenge.Parse(chal); - if (_authChallenge == null) - { - Logger.Error("An invalid authentication challenge is specified."); - return res; - } - - if (Credentials != null && - (!_preAuth || _authChallenge.Scheme == AuthenticationSchemes.Digest)) - { - if (res.HasConnectionClose) - { - releaseClientResources(); - SetClientStream(); - } - - var authRes = new AuthenticationResponse(_authChallenge, Credentials, _nonceCount); - _nonceCount = authRes.NonceCount; - req.Headers["Authorization"] = authRes.ToString(); - res = sendHttpRequest(req, 15000); - } - } - - if (res.IsRedirect) - { - var url = res.Headers["Location"]; - Logger.Warning(string.Format("Received a redirection to '{0}'.", url)); - if (_isRedirectionEnabled) - { - if (url.IsNullOrEmpty()) - { - Logger.Error("No url to redirect is located."); - return res; - } - - if (!url.TryCreateWebSocketUri(out Uri uri, out string message)) - { - Logger.Error("An invalid url to redirect is located: " + message); - return res; - } - - releaseClientResources(); - - _uri = uri; - IsSecure = uri.Scheme == "wss"; - - SetClientStream(); - return sendHandshakeRequest(); - } - } - - return res; - } - - // As client - private WebResponse sendHttpRequest(WebRequest request, int millisecondsTimeout) - { - Logger.Debug("Request to server:\n" + request.ToString()); - var res = request.GetResponse(_stream, millisecondsTimeout); - Logger.Debug("Response to request:\n" + res.ToString()); - - return res; - } - - // As server - private bool sendHttpResponse(WebResponse response) - { - Logger.Debug("Response to request:\n" + response.ToString()); - return sendBytes(response.ToByteArray()); - } - - // As client - private void sendProxyConnectRequest() - { - var req = WebRequest.CreateConnectRequest(_uri); - var res = sendHttpRequest(req, 90000); - if (res.IsProxyAuthenticationRequired) - { - var chal = res.Headers["Proxy-Authenticate"]; - Logger.Warning( - string.Format("Received a proxy authentication requirement for '{0}'.", chal)); - - if (chal.IsNullOrEmpty()) - { - throw new WSException("No proxy authentication challenge is specified."); - } - - var authChal = AuthenticationChallenge.Parse(chal); - if (authChal == null) - { - throw new WSException("An invalid proxy authentication challenge is specified."); - } - - if (_proxyCredentials != null) - { - if (res.HasConnectionClose) - { - releaseClientResources(); - _tcpClient = new TcpClient(_proxyUri.DnsSafeHost, _proxyUri.Port); - _stream = _tcpClient.GetStream(); - } - - var authRes = new AuthenticationResponse(authChal, _proxyCredentials, 0); - req.Headers["Proxy-Authorization"] = authRes.ToString(); - res = sendHttpRequest(req, 15000); - } - - if (res.IsProxyAuthenticationRequired) - { - throw new WSException("A proxy authentication is required."); - } - } - - if (res.StatusCode[0] != '2') - { - throw new WSException( - "The proxy has failed a connection to the requested host and port."); - } - } - - // As client - private void SetClientStream() - { - if (_proxyUri != null) - { - _tcpClient = new TcpClient(_proxyUri.DnsSafeHost, _proxyUri.Port); - _stream = _tcpClient.GetStream(); - sendProxyConnectRequest(); - } - else - { - _tcpClient = new TcpClient(_uri.DnsSafeHost, _uri.Port); - _stream = _tcpClient.GetStream(); - } - - if (IsSecure) - { - var conf = SSL; - var host = conf.TargetHost; - if (host != _uri.DnsSafeHost) - { - throw new WSException( - CloseStatusCode.TlsHandshakeFailure, "An invalid host name is specified."); - } - - try - { - var sslStream = new SslStream( - _stream, - false, - conf.ServerCertificateValidationCallback, - conf.ClientCertificateSelectionCallback); - - sslStream.AuthenticateAsClient( - host, - conf.Certificates, - conf.SslProtocols, - conf.CheckForCertificateRevocation); - - _stream = sslStream; - } - catch (Exception ex) - { - throw new WSException(CloseStatusCode.TlsHandshakeFailure, ex); - } - } - } - - private void startReceiving() - { - if (_messageEventQueue.Count > 0) - { - _messageEventQueue.Clear(); - } - - _pongReceived = new ManualResetEvent(false); - _receivingExited = new ManualResetEvent(false); - - Action receive = null; - receive = - () => - WSFrame.ReadFrameAsync( - _stream, - false, - frame => - { - if (!processReceivedFrame(frame) || _readyState == WSState.Closed) - { - var exited = _receivingExited; - exited?.Set(); - - return; - } - - // Receive next asap because the Ping or Close needs a response to it. - receive(); - - if (_inMessage || !HasMessage || _readyState != WSState.Open) - { - return; - } - - message(); - }, - ex => - { - Logger.Error(ex.ToString()); - fatal("An exception has occurred while receiving.", ex); - } - ); - - receive(); - } - - // As client - private bool validateSecWebSocketAcceptHeader(string value) - { - return value != null && value == CreateResponseKey(_base64Key); - } - - // As server - private bool validateSecWebSocketExtensionsClientHeader(string value) - { - return value == null || value.Length > 0; - } - - // As client - private bool validateSecWebSocketExtensionsServerHeader(string value) - { - if (value == null) - { - return true; - } - - if (value.Length == 0) - { - return false; - } - - if (!_extensionsRequested) - { - return false; - } - - var comp = _compression != CompressionMethod.None; - foreach (var e in value.SplitHeaderValue(',')) - { - var ext = e.Trim(); - if (comp && ext.IsCompressionExtension(_compression)) - { - if (!ext.Contains("server_no_context_takeover")) - { - Logger.Error("The server hasn't sent back 'server_no_context_takeover'."); - return false; - } - - if (!ext.Contains("client_no_context_takeover")) - { - Logger.Warning("The server hasn't sent back 'client_no_context_takeover'."); - } - - var method = _compression.ToExtensionString(); - var invalid = - ext.SplitHeaderValue(';').Contains( - t => - { - t = t.Trim(); - return t != method - && t != "server_no_context_takeover" - && t != "client_no_context_takeover"; - } - ); - - if (invalid) - { - return false; - } - } - else - { - return false; - } - } - - return true; - } - - // As server - private bool validateSecWebSocketKeyHeader(string value) - { - return value != null && value.Length > 0; - } - - // As server - private bool validateSecWebSocketProtocolClientHeader(string value) - { - return value == null || value.Length > 0; - } - - // As client - private bool validateSecWebSocketProtocolServerHeader(string value) - { - if (value == null) - { - return !_protocolsRequested; - } - - if (value.Length == 0) - { - return false; - } - - return _protocolsRequested && _protocols.Contains(p => p == value); - } - - // As server - private bool validateSecWebSocketVersionClientHeader(string value) - { - return value != null && value == _version; - } - - // As client - private bool validateSecWebSocketVersionServerHeader(string value) - { - return value == null || value == _version; - } - - // As server - internal void Close(WebResponse response) - { - _readyState = WSState.Closing; - - sendHttpResponse(response); - releaseServerResources(); - - _readyState = WSState.Closed; - } - - // As server - internal void Close(HttpStatusCode code) - { - Close(createHandshakeFailureResponse(code)); - } - - // As server - internal void Close(PayloadData payloadData, byte[] frameAsBytes) - { - lock (_forState) - { - if (_readyState == WSState.Closing) - { - Logger.Info("The closing is already in progress."); - return; - } - - if (_readyState == WSState.Closed) - { - Logger.Info("The connection has already been closed."); - return; - } - - _readyState = WSState.Closing; - } - - Logger.Trace("Begin closing the connection."); - - var sent = frameAsBytes != null && sendBytes(frameAsBytes); - var received = sent && _receivingExited != null -&& _receivingExited.WaitOne(_waitTime); - - var res = sent && received; - - Logger.Debug( - string.Format( - "Was clean?: {0}\n sent: {1}\n received: {2}", res, sent, received - ) - ); - - releaseServerResources(); - releaseCommonResources(); - - Logger.Trace("End closing the connection."); - - _readyState = WSState.Closed; - - var e = new CloseEventArgs(payloadData); - e.WasClean = res; - - try - { - OnDisconnect.Emit(this, e); - } - catch (Exception ex) - { - Logger.Error(ex.ToString()); - } - } - - // As client - internal static string CreateBase64Key() - { - var src = new byte[16]; - RandomNumber.GetBytes(src); - - return Convert.ToBase64String(src); - } - - internal static string CreateResponseKey(string base64Key) - { - var buff = new StringBuilder(base64Key, 64); - buff.Append(_guid); - SHA1 sha1 = new SHA1CryptoServiceProvider(); - var src = sha1.ComputeHash(buff.ToString().UTF8Encode()); - - return Convert.ToBase64String(src); - } - - // As server - internal void InternalAccept() - { - try - { - if (!acceptHandshake()) - { - return; - } - - _readyState = WSState.Open; - } - catch (Exception ex) - { - Logger.Error(ex.ToString()); - fatal("An exception has occurred while accepting.", ex); - - return; - } - - open(); - } - - // As server - internal bool Ping(byte[] frameAsBytes, TimeSpan timeout) - { - if (_readyState != WSState.Open) - { - return false; - } - - var pongReceived = _pongReceived; - if (pongReceived == null) - { - return false; - } - - lock (_forPing) - { - try - { - pongReceived.Reset(); - - lock (_forState) - { - if (_readyState != WSState.Open) - { - return false; - } - - if (!sendBytes(frameAsBytes)) - { - return false; - } - } - - return pongReceived.WaitOne(timeout); - } - catch (ObjectDisposedException) - { - return false; - } - } - } - - // As server - internal void Send( - OperationCode opcode, byte[] data, Dictionary cache - ) - { - lock (_forSend) - { - lock (_forState) - { - if (_readyState != WSState.Open) - { - Logger.Error("The connection is closing."); - return; - } - - if (!cache.TryGetValue(_compression, out byte[] found)) - { - found = new WSFrame( - FinalFrame.Final, - opcode, - data.Compress(_compression), - _compression != CompressionMethod.None, - false - ) - .ToArray(); - - cache.Add(_compression, found); - } - - sendBytes(found); - } - } - } - - // As server - internal void Send( - OperationCode opcode, Stream stream, Dictionary cache - ) - { - lock (_forSend) - { - if (!cache.TryGetValue(_compression, out Stream found)) - { - found = stream.Compress(_compression); - cache.Add(_compression, found); - } - else - { - found.Position = 0; - } - - send(opcode, found, _compression != CompressionMethod.None); - } - } + internal bool IgnoreExtensions { get; set; } + internal bool IsConnected => _readyState == WSState.Open || _readyState == WSState.Closing; public void Accept() { if (!checkIfAvailable(false, true, true, false, false, false, out string message)) @@ -2601,6 +720,11 @@ namespace EonaCat.Network }); } + void IDisposable.Dispose() + { + close((ushort)CloseStatusCode.Away, string.Empty); + } + public bool Ping() { return ping(EmptyBytes); @@ -2628,6 +752,18 @@ namespace EonaCat.Network return ping(bytes); } + /// + /// Sends a ping using the WebSocket connection. + /// + /// + /// true if the send has done with no error and a pong has been + /// received within a time; otherwise, false. + /// + public void PingAsync(Action completed) + { + pingAsync(EmptyBytes, completed); + } + public void Send(byte[] data) { if (_readyState != WSState.Open) @@ -2985,9 +1121,1870 @@ namespace EonaCat.Network } } - void IDisposable.Dispose() + internal static string CreateBase64Key() { - close((ushort)CloseStatusCode.Away, string.Empty); + var src = new byte[16]; + RandomNumber.GetBytes(src); + + return Convert.ToBase64String(src); + } + + internal static string CreateResponseKey(string base64Key) + { + var buff = new StringBuilder(base64Key, 64); + buff.Append(_webSocketGuid); + SHA1 sha1 = new SHA1CryptoServiceProvider(); + var src = sha1.ComputeHash(buff.ToString().UTF8Encode()); + + return Convert.ToBase64String(src); + } + + internal void Close(WebResponse response) + { + _readyState = WSState.Closing; + + sendHttpResponse(response); + releaseServerResources(); + + _readyState = WSState.Closed; + } + + internal void Close(HttpStatusCode code) + { + Close(createHandshakeFailureResponse(code)); + } + + internal void Close(PayloadData payloadData, byte[] frameAsBytes) + { + lock (_forState) + { + if (_readyState == WSState.Closing) + { + Logger.Info("The closing is already in progress."); + return; + } + + if (_readyState == WSState.Closed) + { + Logger.Info("The connection has already been closed."); + return; + } + + _readyState = WSState.Closing; + } + + Logger.Trace("Begin closing the connection."); + + var sent = frameAsBytes != null && sendBytes(frameAsBytes); + var received = sent && _receivingExited != null +&& _receivingExited.WaitOne(_waitTime); + + var res = sent && received; + + Logger.Debug( + string.Format( + "Was clean?: {0}\n sent: {1}\n received: {2}", res, sent, received + ) + ); + + releaseServerResources(); + releaseCommonResources(); + + Logger.Trace("End closing the connection."); + + _readyState = WSState.Closed; + + var e = new CloseEventArgs(payloadData); + e.WasClean = res; + + try + { + OnDisconnect.Emit(this, e); + } + catch (Exception ex) + { + Logger.Error(ex.ToString()); + } + } + + internal void InternalAccept() + { + try + { + if (!acceptHandshake()) + { + return; + } + + _readyState = WSState.Open; + } + catch (Exception ex) + { + Logger.Error(ex.ToString()); + fatal("An exception has occurred while accepting.", ex); + + return; + } + + open(); + } + + internal bool Ping(byte[] frameAsBytes, TimeSpan timeout) + { + if (_readyState != WSState.Open) + { + return false; + } + + var pongReceived = _pongReceived; + if (pongReceived == null) + { + return false; + } + + lock (_forPing) + { + try + { + pongReceived.Reset(); + + lock (_forState) + { + if (_readyState != WSState.Open) + { + return false; + } + + if (!sendBytes(frameAsBytes)) + { + return false; + } + } + + return pongReceived.WaitOne(timeout); + } + catch (ObjectDisposedException) + { + return false; + } + } + } + + internal void Send( + OperationCode opcode, byte[] data, Dictionary cache + ) + { + lock (_forSend) + { + lock (_forState) + { + if (_readyState != WSState.Open) + { + Logger.Error("The connection is closing."); + return; + } + + if (!cache.TryGetValue(_compression, out byte[] found)) + { + found = new WSFrame( + FinalFrame.Final, + opcode, + data.Compress(_compression), + _compression != CompressionMethod.None, + false + ) + .ToArray(); + + cache.Add(_compression, found); + } + + sendBytes(found); + } + } + } + + internal void Send( + OperationCode opcode, Stream stream, Dictionary cache + ) + { + lock (_forSend) + { + if (!cache.TryGetValue(_compression, out Stream found)) + { + found = stream.Compress(_compression); + cache.Add(_compression, found); + } + else + { + found.Position = 0; + } + + send(opcode, found, _compression != CompressionMethod.None); + } + } + + private static bool checkParametersForSetCredentials( + string username, string password, out string message + ) + { + message = null; + + if (username.IsNullOrEmpty()) + { + return true; + } + + if (username.Contains(':') || !username.IsText()) + { + message = "'username' contains an invalid character."; + return false; + } + + if (password.IsNullOrEmpty()) + { + return true; + } + + if (!password.IsText()) + { + message = "'password' contains an invalid character."; + return false; + } + + return true; + } + + private static bool checkParametersForSetProxy( + string url, string username, string password, out string message + ) + { + message = null; + + if (url.IsNullOrEmpty()) + { + return true; + } + + if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri) + || uri.Scheme != "http" + || uri.Segments.Length > 1 + ) + { + message = "'url' is an invalid URL."; + return false; + } + + if (username.IsNullOrEmpty()) + { + return true; + } + + if (username.Contains(':') || !username.IsText()) + { + message = "'username' contains an invalid character."; + return false; + } + + if (password.IsNullOrEmpty()) + { + return true; + } + + if (!password.IsText()) + { + message = "'password' contains an invalid character."; + return false; + } + + return true; + } + + private static bool checkProtocols(string[] protocols, out string message) + { + message = null; + + Func cond = protocol => protocol.IsNullOrEmpty() + || !protocol.IsToken(); + + if (protocols.Contains(cond)) + { + message = "It contains a value that is not a token."; + return false; + } + + if (protocols.ContainsTwice()) + { + message = "It contains a value twice."; + return false; + } + + return true; + } + + private bool accept() + { + lock (_forState) + { + if (!checkIfAvailable(true, false, false, false, out string message)) + { + Logger.Error(message); + Error("An error has occurred in accepting.", null); + + return false; + } + + try + { + if (!acceptHandshake()) + { + return false; + } + + _readyState = WSState.Open; + } + catch (Exception ex) + { + Logger.Error(ex.ToString()); + fatal("An exception has occurred while accepting.", ex); + + return false; + } + + return true; + } + } + + private bool acceptHandshake() + { + Logger.Debug($"A request from {_context.UserEndPoint}:\n{_context}"); + + if (!checkHandshakeRequest(_context, out string message)) + { + sendHttpResponse(createHandshakeFailureResponse(HttpStatusCode.BadRequest)); + + Logger.Error(message); + fatal("An error has occurred while accepting.", CloseStatusCode.ProtocolError); + + return false; + } + + if (!customCheckHandshakeRequest(_context, out message)) + { + sendHttpResponse(createHandshakeFailureResponse(HttpStatusCode.BadRequest)); + + Logger.Error(message); + fatal("An error has occurred while accepting.", CloseStatusCode.PolicyViolation); + + return false; + } + + _base64Key = _context.Headers["Sec-WebSocket-Key"]; + + if (_protocol != null) + { + processSecWebSocketProtocolHeader(_context.SecWebSocketProtocols); + } + + if (!IgnoreExtensions) + { + processSecWebSocketExtensionsClientHeader(_context.Headers["Sec-WebSocket-Extensions"]); + } + + return sendHttpResponse(createHandshakeResponse()); + } + + private bool canSet(out string message) + { + message = null; + + if (_readyState == WSState.Open) + { + message = "The connection has already been established."; + return false; + } + + if (_readyState == WSState.Closing) + { + message = "The connection is closing."; + return false; + } + + return true; + } + + private bool checkHandshakeRequest(WSContext context, out string message) + { + message = null; + + if (context.RequestUri == null) + { + message = "Specifies an invalid Request-URI."; + return false; + } + + if (!context.IsWebSocketRequest) + { + message = "Not a WebSocket handshake request."; + return false; + } + + var headers = context.Headers; + if (!validateSecWebSocketKeyHeader(headers["Sec-WebSocket-Key"])) + { + message = "Includes no Sec-WebSocket-Key header, or it has an invalid value."; + return false; + } + + if (!validateSecWebSocketVersionClientHeader(headers["Sec-WebSocket-Version"])) + { + message = "Includes no Sec-WebSocket-Version header, or it has an invalid value."; + return false; + } + + if (!validateSecWebSocketProtocolClientHeader(headers["Sec-WebSocket-Protocol"])) + { + message = "Includes an invalid Sec-WebSocket-Protocol header."; + return false; + } + + if (!IgnoreExtensions + && !validateSecWebSocketExtensionsClientHeader(headers["Sec-WebSocket-Extensions"]) + ) + { + message = "Includes an invalid Sec-WebSocket-Extensions header."; + return false; + } + + return true; + } + + private bool checkHandshakeResponse(WebResponse response, out string message) + { + message = null; + + if (response.IsRedirect) + { + message = "Indicates the redirection."; + return false; + } + + if (response.IsUnauthorized) + { + message = "Requires the authentication."; + return false; + } + + if (!response.IsWebSocketResponse) + { + message = "Not a WebSocket handshake response."; + return false; + } + + var headers = response.Headers; + if (!validateSecWebSocketAcceptHeader(headers["Sec-WebSocket-Accept"])) + { + message = "Includes no Sec-WebSocket-Accept header, or it has an invalid value."; + return false; + } + + if (!validateSecWebSocketProtocolServerHeader(headers["Sec-WebSocket-Protocol"])) + { + message = "Includes no Sec-WebSocket-Protocol header, or it has an invalid value."; + return false; + } + + if (!validateSecWebSocketExtensionsServerHeader(headers["Sec-WebSocket-Extensions"])) + { + message = "Includes an invalid Sec-WebSocket-Extensions header."; + return false; + } + + if (!validateSecWebSocketVersionServerHeader(headers["Sec-WebSocket-Version"])) + { + message = "Includes an invalid Sec-WebSocket-Version header."; + return false; + } + + return true; + } + + private bool checkIfAvailable( + bool connecting, bool open, bool closing, bool closed, out string message + ) + { + message = null; + + if (!connecting && _readyState == WSState.Connecting) + { + message = "This operation is not available in: connecting"; + return false; + } + + if (!open && _readyState == WSState.Open) + { + message = "This operation is not available in: open"; + return false; + } + + if (!closing && _readyState == WSState.Closing) + { + message = "This operation is not available in: closing"; + return false; + } + + if (!closed && _readyState == WSState.Closed) + { + message = "This operation is not available in: closed"; + return false; + } + + return true; + } + + private bool checkIfAvailable( + bool client, + bool server, + bool connecting, + bool open, + bool closing, + bool closed, + out string message + ) + { + message = null; + + if (!client && _client) + { + message = "This operation is not available in: client"; + return false; + } + + if (!server && !_client) + { + message = "This operation is not available in: server"; + return false; + } + + return checkIfAvailable(connecting, open, closing, closed, out message); + } + private bool checkReceivedFrame(WSFrame frame, out string message) + { + message = null; + + var masked = frame.IsMasked; + if (_client && masked) + { + message = "A frame from the server is masked."; + return false; + } + + if (!_client && !masked) + { + message = "A frame from a client is not masked."; + return false; + } + + if (_inContinuation && frame.IsData) + { + message = "A data frame has been received while receiving continuation frames."; + return false; + } + + if (frame.IsCompressed && _compression == CompressionMethod.None) + { + message = "A compressed frame has been received without any agreement for it."; + return false; + } + + if (frame.Opcode == OperationCode.Continue && !_inContinuation) + { + message = "A continuation frame has been received but there is nothing to continue."; + return false; + } + + if (frame.Rsv2 == Rsv.On) + { + message = "The RSV2 of a frame is non-zero without any negotiation for it."; + return false; + } + + if (frame.Rsv3 == Rsv.On) + { + message = "The RSV3 of a frame is non-zero without any negotiation for it."; + return false; + } + + return true; + } + + private void close(ushort code, string reason) + { + if (_readyState == WSState.Closing) + { + Logger.Info("The closing is already in progress."); + return; + } + + if (_readyState == WSState.Closed) + { + Logger.Info("The connection has already been closed."); + return; + } + + if (code == (ushort)CloseStatusCode.NoStatus) + { // == no status + close(PayloadData.Empty, true, true, false); + return; + } + + var send = !code.IsReserved(); + close(new PayloadData(code, reason), send, send, false); + } + + private void close( + PayloadData payloadData, bool send, bool receive, bool received + ) + { + lock (_forState) + { + if (_readyState == WSState.Closing) + { + Logger.Info("The closing is already in progress."); + return; + } + + if (_readyState == WSState.Closed) + { + Logger.Info("The connection has already been closed."); + return; + } + + send = send && _readyState == WSState.Open; + receive = send && receive; + + _readyState = WSState.Closing; + } + + Logger.Trace("Begin closing the connection."); + + var res = closeHandshake(payloadData, send, receive, received); + releaseResources(); + + Logger.Trace("End closing the connection."); + + _readyState = WSState.Closed; + + var e = new CloseEventArgs(payloadData); + e.WasClean = res; + + try + { + OnDisconnect.Emit(this, e); + } + catch (Exception ex) + { + Logger.Error(ex.ToString()); + Error("An error has occurred during the OnClose event.", ex); + } + } + + private void closeAsync(ushort code, string reason) + { + if (_readyState == WSState.Closing) + { + Logger.Info("The closing is already in progress."); + return; + } + + if (_readyState == WSState.Closed) + { + Logger.Info("The connection has already been closed."); + return; + } + + if (code == (ushort)CloseStatusCode.NoStatus) + { // == no status + closeAsync(PayloadData.Empty, true, true, false); + return; + } + + var send = !code.IsReserved(); + closeAsync(new PayloadData(code, reason), send, send, false); + } + + private async Task closeAsync(PayloadData payloadData, bool send, bool receive, bool received) + { + await Task.Run(() => close(payloadData, send, receive, received)); + } + + private bool closeHandshake( + PayloadData payloadData, bool send, bool receive, bool received + ) + { + var sent = false; + if (send) + { + var frame = WSFrame.CreateCloseFrame(payloadData, _client); + sent = sendBytes(frame.ToArray()); + + if (_client) + { + frame.Unmask(); + } + } + + var wait = !received && sent && receive && _receivingExited != null; + if (wait) + { + received = _receivingExited.WaitOne(_waitTime); + } + + var ret = sent && received; + + Logger.Debug( + string.Format( + "Was clean?: {0}\n sent: {1}\n received: {2}", ret, sent, received + ) + ); + + return ret; + } + + private bool connect() + { + lock (_forState) + { + if (!checkIfAvailable(true, false, false, true, out string message)) + { + Logger.Error(message); + Error("An error has occurred in connecting.", null); + + return false; + } + + if (_retryCountForConnect > _maxRetryCountForConnect) + { + _retryCountForConnect = 0; + Logger.Error("A series of reconnecting has failed."); + + return false; + } + + _readyState = WSState.Connecting; + + try + { + Handshake(); + } + catch (Exception ex) + { + _retryCountForConnect++; + Logger.Error(ex.ToString()); + fatal("An exception has occurred while connecting.", ex); + + return false; + } + + _retryCountForConnect = 1; + _readyState = WSState.Open; + + return true; + } + } + + private string createExtensions() + { + var buff = new StringBuilder(80); + + if (_compression != CompressionMethod.None) + { + var str = _compression.ToExtensionString( + "server_no_context_takeover", "client_no_context_takeover"); + + buff.AppendFormat("{0}, ", str); + } + + var len = buff.Length; + if (len > 2) + { + buff.Length = len - 2; + return buff.ToString(); + } + + return null; + } + + private WebResponse createHandshakeFailureResponse(HttpStatusCode code) + { + var ret = WebResponse.CreateCloseResponse(code); + ret.Headers["Sec-WebSocket-Version"] = _version; + + return ret; + } + + private WebRequest createHandshakeRequest() + { + var ret = WebRequest.CreateWebSocketRequest(_uri); + + var headers = ret.Headers; + if (!_origin.IsNullOrEmpty()) + { + headers["Origin"] = _origin; + } + + headers["Sec-WebSocket-Key"] = _base64Key; + + _protocolsRequested = _protocols != null && _protocols.Length > 0; + if (_protocolsRequested) + { + headers["Sec-WebSocket-Protocol"] = _protocols.ToString(", "); + } + + _extensionsRequested = _compression != CompressionMethod.None; + if (_extensionsRequested) + { + headers["Sec-WebSocket-Extensions"] = createExtensions(); + } + + headers["Sec-WebSocket-Version"] = _version; + + AuthenticationResponse authRes = null; + if (_authChallenge != null && Credentials != null) + { + authRes = new AuthenticationResponse(_authChallenge, Credentials, _nonceCount); + _nonceCount = authRes.NonceCount; + } + else if (_preAuth) + { + authRes = new AuthenticationResponse(Credentials); + } + + if (authRes != null) + { + headers["Authorization"] = authRes.ToString(); + } + + if (CookieCollection.Count > 0) + { + ret.SetCookies(CookieCollection); + } + + if (CustomHeaders != null) + foreach (var header in CustomHeaders) + if (!headers.Contains(header.Key)) + ret.Headers[header.Key] = header.Value; + + return ret; + } + + private WebResponse createHandshakeResponse() + { + var ret = WebResponse.CreateWebSocketResponse(); + + var headers = ret.Headers; + headers["Sec-WebSocket-Accept"] = CreateResponseKey(_base64Key); + + if (_protocol != null) + { + headers["Sec-WebSocket-Protocol"] = _protocol; + } + + if (_extensions != null) + { + headers["Sec-WebSocket-Extensions"] = _extensions; + } + + if (CookieCollection.Count > 0) + { + ret.SetCookies(CookieCollection); + } + + return ret; + } + + private bool customCheckHandshakeRequest(WSContext context, out string message) + { + message = null; + return CustomHandshakeRequestChecker == null + || (message = CustomHandshakeRequestChecker(context)) == null; + } + + private void EnqueueToMessageEventQueue(MessageEventArgs e) + { + lock (_forMessageEventQueue) + { + _messageEventQueue.Enqueue(e); + } + } + + private void Error(string message, Exception exception) + { + try + { + OnError.Emit(this, new ErrorEventArgs(message, exception)); + } + catch (Exception ex) + { + Logger.Error(ex.ToString()); + } + } + + private void fatal(string message, Exception exception) + { + var code = exception is WSException + ? ((WSException)exception).Code + : CloseStatusCode.Abnormal; + + fatal(message, (ushort)code); + } + + private void fatal(string message, ushort code) + { + var payload = new PayloadData(code, message); + close(payload, !code.IsReserved(), false, false); + } + + private void fatal(string message, CloseStatusCode code) + { + fatal(message, (ushort)code); + } + + private void Handshake() + { + SetClientStream(); + + var response = sendHandshakeRequest(); + if (!checkHandshakeResponse(response, out string message)) + { + throw new WSException(CloseStatusCode.ProtocolError, message); + } + + if (_protocolsRequested) + { + _protocol = response.Headers["Sec-WebSocket-Protocol"]; + } + + if (_extensionsRequested) + { + processSecWebSocketExtensionsServerHeader(response.Headers["Sec-WebSocket-Extensions"]); + } + + processCookies(response.Cookies); + } + private void message() + { + MessageEventArgs e = null; + lock (_forMessageEventQueue) + { + if (_inMessage || _messageEventQueue.Count == 0 || _readyState != WSState.Open) + { + return; + } + + _inMessage = true; + e = _messageEventQueue.Dequeue(); + } + + _message(e); + } + + private void messagec(MessageEventArgs e) + { + do + { + try + { + OnMessageReceived.Emit(this, e); + } + catch (Exception ex) + { + Logger.Error(ex.ToString()); + Error("An error has occurred during an OnMessage event.", ex); + } + + lock (_forMessageEventQueue) + { + if (_messageEventQueue.Count == 0 || _readyState != WSState.Open) + { + _inMessage = false; + break; + } + + e = _messageEventQueue.Dequeue(); + } + } + while (true); + } + + private void messages(MessageEventArgs e) + { + try + { + OnMessageReceived.Emit(this, e); + } + catch (Exception ex) + { + Logger.Error(ex.ToString()); + Error("An error has occurred during an OnMessage event.", ex); + } + + lock (_forMessageEventQueue) + { + if (_messageEventQueue.Count == 0 || _readyState != WSState.Open) + { + _inMessage = false; + return; + } + + e = _messageEventQueue.Dequeue(); + } + + ThreadPool.QueueUserWorkItem(state => messages(e)); + } + + private void open() + { + _inMessage = true; + startReceiving(); + try + { + OnConnect.Emit(this, EventArgs.Empty); + } + catch (Exception ex) + { + Logger.Error(ex.ToString()); + Error("An error has occurred during the OnOpen event.", ex); + } + + MessageEventArgs e = null; + lock (_forMessageEventQueue) + { + if (_messageEventQueue.Count == 0 || _readyState != WSState.Open) + { + _inMessage = false; + return; + } + + e = _messageEventQueue.Dequeue(); + } + + Task.Run(() => _message(e)); + } + + private bool ping(byte[] data) + { + if (_readyState != WSState.Open) + { + return false; + } + + var pongReceived = _pongReceived; + if (pongReceived == null) + { + return false; + } + + lock (_forPing) + { + try + { + pongReceived.Reset(); + if (!send(FinalFrame.Final, OperationCode.Ping, data, false)) + { + return false; + } + + return pongReceived.WaitOne(_waitTime); + } + catch (ObjectDisposedException) + { + return false; + } + } + } + + private void pingAsync(byte[] data, Action completed) + { + if (_readyState != WSState.Open) + { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException(msg); + } + + sendAsync(OperationCode.Ping, new MemoryStream(data), completed); + } + + private bool processCloseFrame(WSFrame frame) + { + var payload = frame.PayloadData; + close(payload, !payload.HasReservedCode, false, true); + + return ping(EmptyBytes); + } + + private void processCookies(CookieCollection cookies) + { + if (cookies.Count == 0) + { + return; + } + + CookieCollection.SetOrRemove(cookies); + } + + private bool processDataFrame(WSFrame frame) + { + EnqueueToMessageEventQueue( + frame.IsCompressed + ? new MessageEventArgs( + frame.Opcode, frame.PayloadData.ApplicationData.Decompress(_compression)) + : new MessageEventArgs(frame)); + + return true; + } + + private bool processFragmentFrame(WSFrame frame) + { + if (!_inContinuation) + { + // Must process first fragment. + if (frame.IsContinuation) + { + return true; + } + + _fragmentsOpcode = frame.Opcode; + _fragmentsCompressed = frame.IsCompressed; + _fragmentsBuffer = new MemoryStream(); + _inContinuation = true; + } + + _fragmentsBuffer.WriteBytes(frame.PayloadData.ApplicationData, 1024); + if (frame.IsFinal) + { + using (_fragmentsBuffer) + { + var data = _fragmentsCompressed + ? _fragmentsBuffer.DecompressToArray(_compression) + : _fragmentsBuffer.ToArray(); + + EnqueueToMessageEventQueue(new MessageEventArgs(_fragmentsOpcode, data)); + } + + _fragmentsBuffer = null; + _inContinuation = false; + } + + return true; + } + + private bool processPingFrame(WSFrame frame) + { + Logger.Trace("A ping was received."); + + var pong = WSFrame.CreatePongFrame(frame.PayloadData, _client); + + lock (_forState) + { + if (_readyState != WSState.Open) + { + Logger.Error("The connection is closing."); + return true; + } + + if (!sendBytes(pong.ToArray())) + { + return false; + } + } + + Logger.Trace("A pong to this ping has been sent."); + + if (CallMessageOnPing) + { + if (_client) + { + pong.Unmask(); + } + + EnqueueToMessageEventQueue(new MessageEventArgs(frame)); + } + + return true; + } + + private bool processPongFrame(WSFrame frame) + { + Logger.Trace("A pong was received."); + + try + { + _pongReceived.Set(); + } + catch (NullReferenceException ex) + { + Logger.Error(ex.Message); + Logger.Debug(ex.ToString()); + + return false; + } + catch (ObjectDisposedException ex) + { + Logger.Error(ex.Message); + Logger.Debug(ex.ToString()); + + return false; + } + + Logger.Trace("It has been signaled."); + + return true; + } + + private bool processReceivedFrame(WSFrame frame) + { + if (!checkReceivedFrame(frame, out string message)) + { + throw new WSException(CloseStatusCode.ProtocolError, message); + } + + frame.Unmask(); + return frame.IsFragment + ? processFragmentFrame(frame) + : frame.IsData + ? processDataFrame(frame) + : frame.IsPing + ? processPingFrame(frame) + : frame.IsPong + ? processPongFrame(frame) + : frame.IsClose + ? processCloseFrame(frame) + : processUnsupportedFrame(frame); + } + + private void processSecWebSocketExtensionsClientHeader(string value) + { + if (value == null) + { + return; + } + + var buff = new StringBuilder(80); + + var comp = false; + foreach (var e in value.SplitHeaderValue(',')) + { + var ext = e.Trim(); + if (!comp && ext.IsCompressionExtension(CompressionMethod.Deflate)) + { + _compression = CompressionMethod.Deflate; + buff.AppendFormat( + "{0}, ", + _compression.ToExtensionString( + "client_no_context_takeover", "server_no_context_takeover" + ) + ); + + comp = true; + } + } + + var len = buff.Length; + if (len > 2) + { + buff.Length = len - 2; + _extensions = buff.ToString(); + } + } + + private void processSecWebSocketExtensionsServerHeader(string value) + { + if (value == null) + { + _compression = CompressionMethod.None; + return; + } + + _extensions = value; + } + + private void processSecWebSocketProtocolHeader(IEnumerable values) + { + if (values.Contains(p => p == _protocol)) + { + return; + } + + _protocol = null; + } + + private bool processUnsupportedFrame(WSFrame frame) + { + Logger.Error("An unsupported frame:" + frame.PrintToString(false)); + fatal("There is no way to handle it.", CloseStatusCode.PolicyViolation); + + return false; + } + + private void releaseClientResources() + { + if (_stream != null) + { + _stream.Dispose(); + _stream = null; + } + + if (_tcpClient != null) + { + _tcpClient.Close(); + _tcpClient = null; + } + } + + private void releaseCommonResources() + { + if (_fragmentsBuffer != null) + { + _fragmentsBuffer.Dispose(); + _fragmentsBuffer = null; + _inContinuation = false; + } + + if (_pongReceived != null) + { + _pongReceived.Close(); + _pongReceived = null; + } + + if (_receivingExited != null) + { + _receivingExited.Close(); + _receivingExited = null; + } + } + + private void releaseResources() + { + if (_client) + { + releaseClientResources(); + } + else + { + releaseServerResources(); + } + + releaseCommonResources(); + } + + private void releaseServerResources() + { + if (_closeContext == null) + { + return; + } + + _closeContext(); + _closeContext = null; + _stream = null; + _context = null; + } + + private bool send(OperationCode 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, stream, compressed); + if (!sent) + { + Error("A send has been interrupted.", null); + } + } + catch (Exception ex) + { + Logger.Error(ex.ToString()); + Error("An error has occurred during a send.", ex); + } + finally + { + if (compressed) + { + stream.Dispose(); + } + + src.Dispose(); + } + + return sent; + } + } + + private bool send(OperationCode opcode, Stream stream, bool compressed) + { + var len = stream.Length; + if (len == 0) + { + return send(FinalFrame.Final, opcode, EmptyBytes, false); + } + + 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(FinalFrame.Final, opcode, buff, compressed); + } + + if (quo == 1 && rem == 0) + { + buff = new byte[FragmentLength]; + return stream.Read(buff, 0, FragmentLength) == FragmentLength + && send(FinalFrame.Final, opcode, buff, compressed); + } + + /* Send fragments */ + + // Begin + buff = new byte[FragmentLength]; + var sent = stream.Read(buff, 0, FragmentLength) == FragmentLength + && send(FinalFrame.More, opcode, buff, compressed); + + if (!sent) + { + return false; + } + + var n = rem == 0 ? quo - 2 : quo - 1; + for (long i = 0; i < n; i++) + { + sent = stream.Read(buff, 0, FragmentLength) == FragmentLength + && send(FinalFrame.More, OperationCode.Continue, buff, false); + + if (!sent) + { + return false; + } + } + + // End + if (rem == 0) + { + rem = FragmentLength; + } + else + { + buff = new byte[rem]; + } + + return stream.Read(buff, 0, rem) == rem + && send(FinalFrame.Final, OperationCode.Continue, buff, false); + } + + private bool send(FinalFrame fin, OperationCode opcode, byte[] data, bool compressed) + { + lock (_forState) + { + if (_readyState != WSState.Open) + { + Logger.Error("The connection is closing."); + return false; + } + + var frame = new WSFrame(fin, opcode, data, compressed, _client); + return sendBytes(frame.ToArray()); + } + } + + private void sendAsync(OperationCode opcode, Stream stream, Action completed) + { + Func sender = send; + Task.Run(() => + { + try + { + var sent = sender(opcode, stream); + completed?.Invoke(sent); + } + catch (Exception ex) + { + Logger.Error(ex.ToString()); + Error("An error has occurred during the callback for an async send.", ex); + } + }); + } + + private bool sendBytes(byte[] bytes) + { + try + { + _stream.Write(bytes, 0, bytes.Length); + } + catch (Exception ex) + { + Logger.Error(ex.Message); + Logger.Debug(ex.ToString()); + + return false; + } + + return true; + } + + private WebResponse sendHandshakeRequest() + { + var req = createHandshakeRequest(); + var res = sendHttpRequest(req, 90000); + if (res.IsUnauthorized) + { + var chal = res.Headers["WWW-Authenticate"]; + Logger.Warning(string.Format("Received an authentication requirement for '{0}'.", chal)); + if (chal.IsNullOrEmpty()) + { + Logger.Error("No authentication challenge is specified."); + return res; + } + + _authChallenge = AuthenticationChallenge.Parse(chal); + if (_authChallenge == null) + { + Logger.Error("An invalid authentication challenge is specified."); + return res; + } + + if (Credentials != null && + (!_preAuth || _authChallenge.Scheme == AuthenticationSchemes.Digest)) + { + if (res.HasConnectionClose) + { + releaseClientResources(); + SetClientStream(); + } + + var authRes = new AuthenticationResponse(_authChallenge, Credentials, _nonceCount); + _nonceCount = authRes.NonceCount; + req.Headers["Authorization"] = authRes.ToString(); + res = sendHttpRequest(req, 15000); + } + } + + if (res.IsRedirect) + { + var url = res.Headers["Location"]; + Logger.Warning(string.Format("Received a redirection to '{0}'.", url)); + if (_isRedirectionEnabled) + { + if (url.IsNullOrEmpty()) + { + Logger.Error("No url to redirect is located."); + return res; + } + + if (!url.TryCreateWebSocketUri(out Uri uri, out string message)) + { + Logger.Error("An invalid url to redirect is located: " + message); + return res; + } + + releaseClientResources(); + + _uri = uri; + IsSecure = uri.Scheme == "wss"; + + SetClientStream(); + return sendHandshakeRequest(); + } + } + + return res; + } + + private WebResponse sendHttpRequest(WebRequest request, int millisecondsTimeout) + { + Logger.Debug("Request to server:\n" + request.ToString()); + var res = request.GetResponse(_stream, millisecondsTimeout); + Logger.Debug("Response to request:\n" + res.ToString()); + + return res; + } + + private bool sendHttpResponse(WebResponse response) + { + Logger.Debug("Response to request:\n" + response.ToString()); + return sendBytes(response.ToByteArray()); + } + + private void sendProxyConnectRequest() + { + var req = WebRequest.CreateConnectRequest(_uri); + var res = sendHttpRequest(req, 90000); + if (res.IsProxyAuthenticationRequired) + { + var chal = res.Headers["Proxy-Authenticate"]; + Logger.Warning( + string.Format("Received a proxy authentication requirement for '{0}'.", chal)); + + if (chal.IsNullOrEmpty()) + { + throw new WSException("No proxy authentication challenge is specified."); + } + + var authChal = AuthenticationChallenge.Parse(chal); + if (authChal == null) + { + throw new WSException("An invalid proxy authentication challenge is specified."); + } + + if (_proxyCredentials != null) + { + if (res.HasConnectionClose) + { + releaseClientResources(); + _tcpClient = new TcpClient(_proxyUri.DnsSafeHost, _proxyUri.Port); + _stream = _tcpClient.GetStream(); + } + + var authRes = new AuthenticationResponse(authChal, _proxyCredentials, 0); + req.Headers["Proxy-Authorization"] = authRes.ToString(); + res = sendHttpRequest(req, 15000); + } + + if (res.IsProxyAuthenticationRequired) + { + throw new WSException("A proxy authentication is required."); + } + } + + if (res.StatusCode[0] != '2') + { + throw new WSException( + "The proxy has failed a connection to the requested host and port."); + } + } + + private void SetClientStream() + { + if (_proxyUri != null) + { + _tcpClient = new TcpClient(_proxyUri.DnsSafeHost, _proxyUri.Port); + _stream = _tcpClient.GetStream(); + sendProxyConnectRequest(); + } + else + { + _tcpClient = new TcpClient(_uri.DnsSafeHost, _uri.Port); + _stream = _tcpClient.GetStream(); + } + + if (IsSecure) + { + var conf = SSL; + var host = conf.TargetHost; + if (host != _uri.DnsSafeHost) + { + throw new WSException( + CloseStatusCode.TlsHandshakeFailure, "An invalid host name is specified."); + } + + try + { + var sslStream = new SslStream( + _stream, + false, + conf.ServerCertificateValidationCallback, + conf.ClientCertificateSelectionCallback); + + sslStream.ReadTimeout = (int)WaitTime.TotalMilliseconds; + sslStream.WriteTimeout = (int)WaitTime.TotalMilliseconds; + + sslStream.AuthenticateAsClient( + host, + conf.Certificates, + conf.SslProtocols, + conf.CheckForCertificateRevocation); + + _stream = sslStream; + } + catch (Exception ex) + { + throw new WSException(CloseStatusCode.TlsHandshakeFailure, ex); + } + } + } + + private void Setup() + { + _compression = CompressionMethod.None; + CookieCollection = new CookieCollection(); + _forPing = new object(); + _forSend = new object(); + _forState = new object(); + _messageEventQueue = new Queue(); + _forMessageEventQueue = ((ICollection)_messageEventQueue).SyncRoot; + _readyState = WSState.Connecting; + } + private void startReceiving() + { + if (_messageEventQueue.Count > 0) + { + _messageEventQueue.Clear(); + } + + _pongReceived = new ManualResetEvent(false); + _receivingExited = new ManualResetEvent(false); + + Action receive = null; + receive = + () => + WSFrame.ReadFrameAsync( + _stream, + false, + frame => + { + if (!processReceivedFrame(frame) || _readyState == WSState.Closed) + { + var exited = _receivingExited; + exited?.Set(); + + return; + } + + // Receive next asap because the Ping or Close needs a response to it. + receive(); + + if (_inMessage || !HasMessage || _readyState != WSState.Open) + { + return; + } + + message(); + }, + ex => + { + Logger.Error(ex.ToString()); + fatal("An exception has occurred while receiving.", ex); + } + ); + + receive(); + } + + private bool validateSecWebSocketAcceptHeader(string value) + { + return value != null && value == CreateResponseKey(_base64Key); + } + + private bool validateSecWebSocketExtensionsClientHeader(string value) + { + return value == null || value.Length > 0; + } + + private bool validateSecWebSocketExtensionsServerHeader(string value) + { + if (value == null) + { + return true; + } + + if (value.Length == 0) + { + return false; + } + + if (!_extensionsRequested) + { + return false; + } + + var comp = _compression != CompressionMethod.None; + foreach (var e in value.SplitHeaderValue(',')) + { + var ext = e.Trim(); + if (comp && ext.IsCompressionExtension(_compression)) + { + if (!ext.Contains("server_no_context_takeover")) + { + Logger.Error("The server hasn't sent back 'server_no_context_takeover'."); + return false; + } + + if (!ext.Contains("client_no_context_takeover")) + { + Logger.Warning("The server hasn't sent back 'client_no_context_takeover'."); + } + + var method = _compression.ToExtensionString(); + var invalid = + ext.SplitHeaderValue(';').Contains( + t => + { + t = t.Trim(); + return t != method + && t != "server_no_context_takeover" + && t != "client_no_context_takeover"; + } + ); + + if (invalid) + { + return false; + } + } + else + { + return false; + } + } + + return true; + } + + private bool validateSecWebSocketKeyHeader(string value) + { + return value != null && value.Length > 0; + } + + private bool validateSecWebSocketProtocolClientHeader(string value) + { + return value == null || value.Length > 0; + } + + private bool validateSecWebSocketProtocolServerHeader(string value) + { + if (value == null) + { + return !_protocolsRequested; + } + + if (value.Length == 0) + { + return false; + } + + return _protocolsRequested && _protocols.Contains(p => p == value); + } + + private bool validateSecWebSocketVersionClientHeader(string value) + { + return value != null && value == _version; + } + + private bool validateSecWebSocketVersionServerHeader(string value) + { + return value == null || value == _version; } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/WSFrame.cs b/EonaCat.Network/System/Sockets/Web/WSFrame.cs index 886e386..7cb6250 100644 --- a/EonaCat.Network/System/Sockets/Web/WSFrame.cs +++ b/EonaCat.Network/System/Sockets/Web/WSFrame.cs @@ -11,19 +11,13 @@ namespace EonaCat.Network { internal class WSFrame : IEnumerable { - private const int BUFFER_SIZE = 1024; - internal static readonly byte[] EmptyPingBytes; - + private const int BUFFER_SIZE = 1024; static WSFrame() { EmptyPingBytes = CreatePingFrame(false).ToArray(); } - private WSFrame() - { - } - internal WSFrame(OperationCode opcode, PayloadData payloadData, bool mask) : this(FinalFrame.Final, opcode, payloadData, false, mask) { @@ -42,8 +36,9 @@ namespace EonaCat.Network Rsv2 = Rsv.Off; Rsv3 = Rsv.Off; Opcode = opcode; + PayloadData = new PayloadData(payloadData); - var len = payloadData.Length; + var len = PayloadData.Length; if (len < 126) { PayloadLength = (byte)len; @@ -64,17 +59,41 @@ namespace EonaCat.Network { Mask = Mask.On; MaskingKey = createMaskingKey(); - payloadData.Mask(MaskingKey); + PayloadData.Mask(MaskingKey); } else { Mask = Mask.Off; MaskingKey = WSClient.EmptyBytes; } - - PayloadData = payloadData; } + private WSFrame() + { + } + public byte[] ExtendedPayloadLength { get; private set; } + public FinalFrame Fin { get; private set; } + public bool IsBinary => Opcode == OperationCode.Binary; + public bool IsClose => Opcode == OperationCode.Close; + public bool IsCompressed => Rsv1 == Rsv.On; + public bool IsContinuation => Opcode == OperationCode.Continue; + public bool IsControl => Opcode >= OperationCode.Close; + public bool IsData => Opcode == OperationCode.Text || Opcode == OperationCode.Binary; + public bool IsFinal => Fin == FinalFrame.Final; + public bool IsFragment => Fin == FinalFrame.More || Opcode == OperationCode.Continue; + public bool IsMasked => Mask == Mask.On; + public bool IsPing => Opcode == OperationCode.Ping; + public bool IsPong => Opcode == OperationCode.Pong; + public bool IsText => Opcode == OperationCode.Text; + public ulong Length => 2 + (ulong)(ExtendedPayloadLength.Length + MaskingKey.Length) + PayloadData.Length; + public Mask Mask { get; private set; } + public byte[] MaskingKey { get; private set; } + public OperationCode Opcode { get; private set; } + public PayloadData PayloadData { get; private set; } + public byte PayloadLength { get; private set; } + public Rsv Rsv1 { get; private set; } + public Rsv Rsv2 { get; private set; } + public Rsv Rsv3 { get; private set; } internal int ExtendedPayloadLengthCount => PayloadLength < 126 ? 0 : (PayloadLength == 126 ? 2 : 8); internal ulong FullPayloadLength => PayloadLength < 126 @@ -82,52 +101,173 @@ namespace EonaCat.Network : PayloadLength == 126 ? ExtendedPayloadLength.ToUInt16(ByteOrder.Big) : ExtendedPayloadLength.ToUInt64(ByteOrder.Big); + public IEnumerator GetEnumerator() + { + foreach (var b in ToArray()) + { + yield return b; + } + } - public byte[] ExtendedPayloadLength { get; private set; } + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } - public FinalFrame Fin { get; private set; } + public void Print(bool dumped) + { + Console.WriteLine(dumped ? dump(this) : print(this)); + } - public bool IsBinary => Opcode == OperationCode.Binary; + public string PrintToString(bool dumped) + { + return dumped ? dump(this) : print(this); + } - public bool IsClose => Opcode == OperationCode.Close; + public byte[] ToArray() + { + 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) + PayloadLength; + buff.Write(((ushort)header).InternalToByteArray(ByteOrder.Big), 0, 2); - public bool IsCompressed => Rsv1 == Rsv.On; + if (PayloadLength > 125) + { + buff.Write(ExtendedPayloadLength, 0, PayloadLength == 126 ? 2 : 8); + } - public bool IsContinuation => Opcode == OperationCode.Cont; + if (Mask == Mask.On) + { + buff.Write(MaskingKey, 0, 4); + } - public bool IsControl => Opcode >= OperationCode.Close; + if (PayloadLength > 0) + { + var bytes = PayloadData.ToArray(); + if (PayloadLength < 127) + { + buff.Write(bytes, 0, bytes.Length); + } + else + { + buff.WriteBytes(bytes, BUFFER_SIZE); + } + } - public bool IsData => Opcode == OperationCode.Text || Opcode == OperationCode.Binary; + buff.Close(); + return buff.ToArray(); + } + } - public bool IsFinal => Fin == FinalFrame.Final; + public override string ToString() + { + return BitConverter.ToString(ToArray()); + } - public bool IsFragment => Fin == FinalFrame.More || Opcode == OperationCode.Cont; + internal static WSFrame CreateCloseFrame( + PayloadData payloadData, bool mask + ) + { + return new WSFrame( + FinalFrame.Final, OperationCode.Close, payloadData, false, mask + ); + } - public bool IsMasked => Mask == Mask.On; + internal static WSFrame CreatePingFrame(bool mask) + { + return new WSFrame( + FinalFrame.Final, OperationCode.Ping, PayloadData.Empty, false, mask + ); + } - public bool IsPing => Opcode == OperationCode.Ping; + internal static WSFrame CreatePingFrame(byte[] data, bool mask) + { + return new WSFrame( + FinalFrame.Final, OperationCode.Ping, new PayloadData(data), false, mask + ); + } - public bool IsPong => Opcode == OperationCode.Pong; + internal static WSFrame CreatePongFrame( + PayloadData payloadData, bool mask + ) + { + return new WSFrame( + FinalFrame.Final, OperationCode.Pong, payloadData, false, mask + ); + } - public bool IsText => Opcode == OperationCode.Text; + internal static WSFrame ReadFrame(Stream stream, bool unmask) + { + var frame = readHeader(stream); + readExtendedPayloadLength(stream, frame); + readMaskingKey(stream, frame); + readPayloadData(stream, frame); - public ulong Length => 2 + (ulong)(ExtendedPayloadLength.Length + MaskingKey.Length) + PayloadData.Length; + if (unmask) + { + frame.Unmask(); + } - public Mask Mask { get; private set; } + return frame; + } - public byte[] MaskingKey { get; private set; } + internal static void ReadFrameAsync( + Stream stream, + bool unmask, + Action completed, + Action error + ) + { + readHeaderAsync( + stream, + frame => + readExtendedPayloadLengthAsync( + stream, + frame, + frame1 => + readMaskingKeyAsync( + stream, + frame1, + frame2 => + readPayloadDataAsync( + stream, + frame2, + frame3 => + { + if (unmask) + { + frame3.Unmask(); + } - public OperationCode Opcode { get; private set; } + completed(frame3); + }, + error + ), + error + ), + error + ), + error + ); + } - public PayloadData PayloadData { get; private set; } + internal void Unmask() + { + if (Mask == Mask.Off) + { + return; + } - public byte PayloadLength { get; private set; } - - public Rsv Rsv1 { get; private set; } - - public Rsv Rsv2 { get; private set; } - - public Rsv Rsv3 { get; private set; } + Mask = Mask.Off; + PayloadData.Mask(MaskingKey); + MaskingKey = WSClient.EmptyBytes; + } private static byte[] createMaskingKey() { @@ -496,173 +636,5 @@ Extended Payload Length: {7} stream.ReadBytesAsync(llen, BUFFER_SIZE, compl, error); } - - internal static WSFrame CreateCloseFrame( - PayloadData payloadData, bool mask - ) - { - return new WSFrame( - FinalFrame.Final, OperationCode.Close, payloadData, false, mask - ); - } - - internal static WSFrame CreatePingFrame(bool mask) - { - return new WSFrame( - FinalFrame.Final, OperationCode.Ping, PayloadData.Empty, false, mask - ); - } - - internal static WSFrame CreatePingFrame(byte[] data, bool mask) - { - return new WSFrame( - FinalFrame.Final, OperationCode.Ping, new PayloadData(data), false, mask - ); - } - - internal static WSFrame CreatePongFrame( - PayloadData payloadData, bool mask - ) - { - return new WSFrame( - FinalFrame.Final, OperationCode.Pong, payloadData, false, mask - ); - } - - internal static WSFrame ReadFrame(Stream stream, bool unmask) - { - var frame = readHeader(stream); - readExtendedPayloadLength(stream, frame); - readMaskingKey(stream, frame); - readPayloadData(stream, frame); - - if (unmask) - { - frame.Unmask(); - } - - return frame; - } - - internal static void ReadFrameAsync( - Stream stream, - bool unmask, - Action completed, - Action error - ) - { - readHeaderAsync( - stream, - frame => - readExtendedPayloadLengthAsync( - stream, - frame, - frame1 => - readMaskingKeyAsync( - stream, - frame1, - frame2 => - readPayloadDataAsync( - stream, - frame2, - frame3 => - { - if (unmask) - { - frame3.Unmask(); - } - - completed(frame3); - }, - error - ), - error - ), - error - ), - error - ); - } - - internal void Unmask() - { - if (Mask == Mask.Off) - { - return; - } - - Mask = Mask.Off; - PayloadData.Mask(MaskingKey); - MaskingKey = WSClient.EmptyBytes; - } - - public IEnumerator GetEnumerator() - { - foreach (var b in ToArray()) - { - yield return b; - } - } - - public void Print(bool dumped) - { - Console.WriteLine(dumped ? dump(this) : print(this)); - } - - public string PrintToString(bool dumped) - { - return dumped ? dump(this) : print(this); - } - - public byte[] ToArray() - { - 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) + PayloadLength; - buff.Write(((ushort)header).InternalToByteArray(ByteOrder.Big), 0, 2); - - if (PayloadLength > 125) - { - buff.Write(ExtendedPayloadLength, 0, PayloadLength == 126 ? 2 : 8); - } - - if (Mask == Mask.On) - { - buff.Write(MaskingKey, 0, 4); - } - - if (PayloadLength > 0) - { - var bytes = PayloadData.ToArray(); - if (PayloadLength < 127) - { - buff.Write(bytes, 0, bytes.Length); - } - else - { - buff.WriteBytes(bytes, BUFFER_SIZE); - } - } - - buff.Close(); - return buff.ToArray(); - } - } - - public override string ToString() - { - return BitConverter.ToString(ToArray()); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/WebBase.cs b/EonaCat.Network/System/Sockets/Web/WebBase.cs index 940df0b..8f372e3 100644 --- a/EonaCat.Network/System/Sockets/Web/WebBase.cs +++ b/EonaCat.Network/System/Sockets/Web/WebBase.cs @@ -12,11 +12,9 @@ namespace EonaCat.Network { internal abstract class WebBase { - private const int _headersMaxLength = 8192; internal byte[] EntityBodyData; - protected const string CrLf = "\r\n"; - + private const int _headersMaxLength = 8192; protected WebBase(Version version, NameValueCollection headers) { ProtocolVersion = version; @@ -48,6 +46,60 @@ namespace EonaCat.Network public Version ProtocolVersion { get; } + public byte[] ToByteArray() + { + return Encoding.UTF8.GetBytes(ToString()); + } + + protected static T Read(Stream stream, Func parser, int millisecondsTimeout) + where T : WebBase + { + var timeout = false; + var timer = new Timer( + state => + { + timeout = true; + stream.Close(); + }, + null, + millisecondsTimeout, + -1); + + T http = null; + Exception exception = null; + try + { + http = parser(readHeaders(stream, _headersMaxLength)); + var contentLen = http.Headers["Content-Length"]; + if (contentLen != null && contentLen.Length > 0) + { + http.EntityBodyData = readEntityBody(stream, contentLen); + } + } + catch (Exception ex) + { + exception = ex; + } + finally + { + timer.Change(-1, -1); + timer.Dispose(); + } + + var message = timeout + ? "A timeout has occurred while reading an HTTP request/response." + : exception != null + ? "An exception has occurred while reading an HTTP request/response." + : null; + + if (message != null) + { + throw new WSException(message, exception); + } + + return http; + } + private static byte[] readEntityBody(Stream stream, string length) { if (!long.TryParse(length, out long len)) @@ -105,59 +157,5 @@ namespace EonaCat.Network .Replace(CrLf + "\t", " ") .Split(new[] { CrLf }, StringSplitOptions.RemoveEmptyEntries); } - - protected static T Read(Stream stream, Func parser, int millisecondsTimeout) - where T : WebBase - { - var timeout = false; - var timer = new Timer( - state => - { - timeout = true; - stream.Close(); - }, - null, - millisecondsTimeout, - -1); - - T http = null; - Exception exception = null; - try - { - http = parser(readHeaders(stream, _headersMaxLength)); - var contentLen = http.Headers["Content-Length"]; - if (contentLen != null && contentLen.Length > 0) - { - http.EntityBodyData = readEntityBody(stream, contentLen); - } - } - catch (Exception ex) - { - exception = ex; - } - finally - { - timer.Change(-1, -1); - timer.Dispose(); - } - - var message = timeout - ? "A timeout has occurred while reading an HTTP request/response." - : exception != null - ? "An exception has occurred while reading an HTTP request/response." - : null; - - if (message != null) - { - throw new WSException(message, exception); - } - - return http; - } - - public byte[] ToByteArray() - { - return Encoding.UTF8.GetBytes(ToString()); - } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/WebRequest.cs b/EonaCat.Network/System/Sockets/Web/WebRequest.cs index ced217f..2b14982 100644 --- a/EonaCat.Network/System/Sockets/Web/WebRequest.cs +++ b/EonaCat.Network/System/Sockets/Web/WebRequest.cs @@ -14,19 +14,18 @@ namespace EonaCat.Network private bool _websocketRequest; private bool _websocketRequestSet; - private WebRequest(string method, string uri, Version version, NameValueCollection headers) - : base(version, headers) - { - HttpMethod = method; - RequestUri = uri; - } - internal WebRequest(string method, string uri) : this(method, uri, HttpVersion.Version11, new NameValueCollection()) { Headers["User-Agent"] = $"EonaCat.Network/{Constants.Version}"; } + private WebRequest(string method, string uri, Version version, NameValueCollection headers) + : base(version, headers) + { + HttpMethod = method; + RequestUri = uri; + } public AuthenticationResponse AuthenticationResponse { get @@ -63,67 +62,6 @@ namespace EonaCat.Network public string RequestUri { get; } - internal static WebRequest CreateConnectRequest(Uri uri) - { - var host = uri.DnsSafeHost; - var port = uri.Port; - var authority = string.Format("{0}:{1}", host, port); - var req = new WebRequest("CONNECT", authority); - req.Headers["Host"] = port == 80 ? host : authority; - - return req; - } - - internal static WebRequest CreateWebSocketRequest(Uri uri) - { - var req = new WebRequest("GET", uri.PathAndQuery); - var headers = req.Headers; - - // Only includes a port number in the Host header value if it's non-default. - // See: https://tools.ietf.org/html/rfc6455#page-17 - var port = uri.Port; - var schm = uri.Scheme; - headers["Host"] = (port == 80 && schm == "ws") || (port == 443 && schm == "wss") - ? uri.DnsSafeHost - : uri.Authority; - - headers["Upgrade"] = "websocket"; - headers["Connection"] = "Upgrade"; - - return req; - } - - internal WebResponse GetResponse(Stream stream, int millisecondsTimeout) - { - var buff = ToByteArray(); - stream.Write(buff, 0, buff.Length); - - return Read(stream, WebResponse.Parse, millisecondsTimeout); - } - - internal static WebRequest Parse(string[] headerParts) - { - var requestLine = headerParts[0].Split(new[] { ' ' }, 3); - if (requestLine.Length != 3) - { - throw new ArgumentException("Invalid request line: " + headerParts[0]); - } - - var headers = new WebHeaderCollection(); - for (int i = 1; i < headerParts.Length; i++) - { - headers.InternalSet(headerParts[i], false); - } - - return new WebRequest( - requestLine[0], requestLine[1], new Version(requestLine[2].Substring(5)), headers); - } - - internal static WebRequest Read(Stream stream, int millisecondsTimeout) - { - return Read(stream, Parse, millisecondsTimeout); - } - public void SetCookies(CookieCollection cookies) { if (cookies == null || cookies.Count == 0) @@ -169,5 +107,66 @@ namespace EonaCat.Network return output.ToString(); } + + internal static WebRequest CreateConnectRequest(Uri uri) + { + var host = uri.DnsSafeHost; + var port = uri.Port; + var authority = string.Format("{0}:{1}", host, port); + var req = new WebRequest("CONNECT", authority); + req.Headers["Host"] = port == 80 ? host : authority; + + return req; + } + + internal static WebRequest CreateWebSocketRequest(Uri uri) + { + var req = new WebRequest("GET", uri.PathAndQuery); + var headers = req.Headers; + + // Only includes a port number in the Host header value if it's non-default. + // See: https://tools.ietf.org/html/rfc6455#page-17 + var port = uri.Port; + var schm = uri.Scheme; + headers["Host"] = (port == 80 && schm == "ws") || (port == 443 && schm == "wss") + ? uri.DnsSafeHost + : uri.Authority; + + headers["Upgrade"] = "websocket"; + headers["Connection"] = "Upgrade"; + + return req; + } + + internal static WebRequest Parse(string[] headerParts) + { + var requestLine = headerParts[0].Split(new[] { ' ' }, 3); + if (requestLine.Length != 3) + { + throw new ArgumentException("Invalid request line: " + headerParts[0]); + } + + var headers = new WebHeaderCollection(); + for (int i = 1; i < headerParts.Length; i++) + { + headers.InternalSet(headerParts[i], false); + } + + return new WebRequest( + requestLine[0], requestLine[1], new Version(requestLine[2].Substring(5)), headers); + } + + internal static WebRequest Read(Stream stream, int millisecondsTimeout) + { + return Read(stream, Parse, millisecondsTimeout); + } + + internal WebResponse GetResponse(Stream stream, int millisecondsTimeout) + { + var buff = ToByteArray(); + stream.Write(buff, 0, buff.Length); + + return Read(stream, WebResponse.Parse, millisecondsTimeout); + } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/WebResponse.cs b/EonaCat.Network/System/Sockets/Web/WebResponse.cs index 3353bdf..e5b0e1a 100644 --- a/EonaCat.Network/System/Sockets/Web/WebResponse.cs +++ b/EonaCat.Network/System/Sockets/Web/WebResponse.cs @@ -11,13 +11,6 @@ namespace EonaCat.Network { internal class WebResponse : WebBase { - private WebResponse(string code, string reason, Version version, NameValueCollection headers) - : base(version, headers) - { - StatusCode = code; - Reason = reason; - } - internal WebResponse(HttpStatusCode code) : this(code, code.GetDescription()) { @@ -29,6 +22,12 @@ namespace EonaCat.Network Headers["Server"] = $"EonaCat.Network/{Constants.Version}"; } + private WebResponse(string code, string reason, Version version, NameValueCollection headers) + : base(version, headers) + { + StatusCode = code; + Reason = reason; + } public CookieCollection Cookies => Headers.GetCookies(true); public bool HasConnectionClose => Headers.Contains("Connection", "close"); @@ -55,6 +54,42 @@ namespace EonaCat.Network public string StatusCode { get; } + public void SetCookies(CookieCollection cookies) + { + if (cookies == null || cookies.Count == 0) + { + return; + } + + var headers = Headers; + foreach (var cookie in cookies.Sorted) + { + headers.Add("Set-Cookie", cookie.ToResponseString()); + } + } + + public override string ToString() + { + var output = new StringBuilder(64); + output.AppendFormat("HTTP/{0} {1} {2}{3}", ProtocolVersion, StatusCode, Reason, CrLf); + + var headers = Headers; + foreach (var key in headers.AllKeys) + { + 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(); + } + internal static WebResponse CreateCloseResponse(HttpStatusCode code) { var res = new WebResponse(code); @@ -104,41 +139,5 @@ namespace EonaCat.Network { return Read(stream, Parse, millisecondsTimeout); } - - public void SetCookies(CookieCollection cookies) - { - if (cookies == null || cookies.Count == 0) - { - return; - } - - var headers = Headers; - foreach (var cookie in cookies.Sorted) - { - headers.Add("Set-Cookie", cookie.ToResponseString()); - } - } - - public override string ToString() - { - var output = new StringBuilder(64); - output.AppendFormat("HTTP/{0} {1} {2}{3}", ProtocolVersion, StatusCode, Reason, CrLf); - - var headers = Headers; - foreach (var key in headers.AllKeys) - { - 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(); - } } } \ No newline at end of file