Made it more secure
This commit is contained in:
parent
0e299d0d27
commit
0c659e9d34
|
@ -24,6 +24,9 @@ namespace EonaCat.Connections.Client.Example
|
||||||
Console.Write("Enter message to send (or 'exit' to quit): ");
|
Console.Write("Enter message to send (or 'exit' to quit): ");
|
||||||
var message = Console.ReadLine();
|
var message = Console.ReadLine();
|
||||||
|
|
||||||
|
HttpClient httpClient = new HttpClient();
|
||||||
|
message = await httpClient.GetStringAsync("https://samples.json-format.com/employees/10-level/employees-10-level_100MB.json");
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(message) && message.Equals("exit", StringComparison.OrdinalIgnoreCase))
|
if (!string.IsNullOrEmpty(message) && message.Equals("exit", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
await _client.DisconnectAsync().ConfigureAwait(false);
|
await _client.DisconnectAsync().ConfigureAwait(false);
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace EonaCat.Connections.Helpers
|
||||||
|
{
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
public static class AesCryptoHelpers
|
||||||
|
{
|
||||||
|
private static readonly byte[] HmacInfo = Encoding.UTF8.GetBytes("EonaCat.Connections.HMAC");
|
||||||
|
|
||||||
|
public static async Task<byte[]> EncryptDataAsync(byte[] plaintext, Aes aes)
|
||||||
|
{
|
||||||
|
byte[] iv = new byte[aes.BlockSize / 8];
|
||||||
|
using (var rng = RandomNumberGenerator.Create())
|
||||||
|
{
|
||||||
|
rng.GetBytes(iv);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] ciphertext;
|
||||||
|
using (var encryptor = aes.CreateEncryptor(aes.Key, iv))
|
||||||
|
using (var ms = new MemoryStream())
|
||||||
|
using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
|
||||||
|
{
|
||||||
|
await cs.WriteAsync(plaintext, 0, plaintext.Length);
|
||||||
|
cs.FlushFinalBlock();
|
||||||
|
ciphertext = ms.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] hmacKey = DeriveHmacKey(aes.Key);
|
||||||
|
byte[] toAuth = iv.Concat(ciphertext).ToArray();
|
||||||
|
byte[] hmac;
|
||||||
|
using (var h = new HMACSHA256(hmacKey))
|
||||||
|
{
|
||||||
|
hmac = h.ComputeHash(toAuth);
|
||||||
|
}
|
||||||
|
|
||||||
|
return toAuth.Concat(hmac).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<byte[]> DecryptDataAsync(byte[] payload, Aes aes)
|
||||||
|
{
|
||||||
|
int ivLen = aes.BlockSize / 8;
|
||||||
|
int hmacLen = 32;
|
||||||
|
|
||||||
|
if (payload.Length < ivLen + hmacLen)
|
||||||
|
{
|
||||||
|
throw new CryptographicException("Payload too short");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] iv = payload.Take(ivLen).ToArray();
|
||||||
|
byte[] ciphertext = payload.Skip(ivLen).Take(payload.Length - ivLen - hmacLen).ToArray();
|
||||||
|
byte[] receivedHmac = payload.Skip(payload.Length - hmacLen).ToArray();
|
||||||
|
|
||||||
|
byte[] hmacKey = DeriveHmacKey(aes.Key);
|
||||||
|
byte[] toAuth = iv.Concat(ciphertext).ToArray();
|
||||||
|
byte[] computed;
|
||||||
|
using (var h = new HMACSHA256(hmacKey))
|
||||||
|
{
|
||||||
|
computed = h.ComputeHash(toAuth);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!FixedTimeEquals(computed, receivedHmac))
|
||||||
|
{
|
||||||
|
throw new CryptographicException("HMAC validation failed: message tampered or wrong key");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] plaintext;
|
||||||
|
using (var decryptor = aes.CreateDecryptor(aes.Key, iv))
|
||||||
|
using (var ms = new MemoryStream(ciphertext))
|
||||||
|
using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
|
||||||
|
using (var result = new MemoryStream())
|
||||||
|
{
|
||||||
|
await cs.CopyToAsync(result);
|
||||||
|
plaintext = result.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] DeriveHmacKey(byte[] aesKey)
|
||||||
|
{
|
||||||
|
using var h = new HMACSHA256(aesKey);
|
||||||
|
return h.ComputeHash(HmacInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool FixedTimeEquals(byte[] a, byte[] b)
|
||||||
|
{
|
||||||
|
if (a.Length != b.Length)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int diff = 0;
|
||||||
|
for (int i = 0; i < a.Length; i++)
|
||||||
|
{
|
||||||
|
diff |= a[i] ^ b[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return diff == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace EonaCat.Connections.Helpers
|
namespace EonaCat.Connections.Helpers
|
||||||
{
|
{
|
||||||
|
@ -8,146 +7,68 @@ namespace EonaCat.Connections.Helpers
|
||||||
|
|
||||||
public static class AesKeyExchange
|
public static class AesKeyExchange
|
||||||
{
|
{
|
||||||
private static readonly string Pepper = "EonaCat.Connections.Salt";
|
private const int SaltSize = 16;
|
||||||
|
private const int KeySize = 32;
|
||||||
|
private const int IvSize = 16;
|
||||||
|
private const int HmacSize = 32;
|
||||||
|
private const int Pbkdf2Iterations = 100_000;
|
||||||
|
|
||||||
/// <summary>
|
// Returns an AES object derived from the password and salt
|
||||||
/// Send AES key, IV, and salt to the stream.
|
public static async Task<Aes> ReceiveAesKeyAsync(Stream stream, string password)
|
||||||
/// </summary>
|
|
||||||
/// <param name="stream"></param>
|
|
||||||
/// <param name="aes"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static async Task<Aes> SendAesKeyAsync(Stream stream, Aes aes, string password = null)
|
|
||||||
{
|
{
|
||||||
var rawKey = aes.Key;
|
// Read salt
|
||||||
var iv = aes.IV;
|
byte[] salt = new byte[SaltSize];
|
||||||
var salt = new byte[32];
|
await stream.ReadExactlyAsync(salt, 0, SaltSize);
|
||||||
|
|
||||||
|
// Derive key
|
||||||
|
byte[] key;
|
||||||
|
using (var kdf = new Rfc2898DeriveBytes(password, salt, Pbkdf2Iterations, HashAlgorithmName.SHA256))
|
||||||
|
{
|
||||||
|
key = kdf.GetBytes(KeySize);
|
||||||
|
}
|
||||||
|
|
||||||
|
var aes = Aes.Create();
|
||||||
|
aes.KeySize = 256;
|
||||||
|
aes.BlockSize = 128;
|
||||||
|
aes.Mode = CipherMode.CBC;
|
||||||
|
aes.Padding = PaddingMode.PKCS7;
|
||||||
|
aes.Key = key;
|
||||||
|
|
||||||
|
return aes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends salt (no key) to the other side
|
||||||
|
public static async Task SendAesKeyAsync(Stream stream, Aes aes, string password)
|
||||||
|
{
|
||||||
|
// Generate random salt
|
||||||
|
byte[] salt = new byte[SaltSize];
|
||||||
using (var rng = RandomNumberGenerator.Create())
|
using (var rng = RandomNumberGenerator.Create())
|
||||||
{
|
{
|
||||||
rng.GetBytes(salt);
|
rng.GetBytes(salt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send raw key, IV, and salt
|
// Derive AES key
|
||||||
await WriteBytesWithLengthAsync(stream, rawKey);
|
byte[] key;
|
||||||
await WriteBytesWithLengthAsync(stream, iv);
|
using (var kdf = new Rfc2898DeriveBytes(password, salt, Pbkdf2Iterations, HashAlgorithmName.SHA256))
|
||||||
await WriteBytesWithLengthAsync(stream, salt);
|
{
|
||||||
|
key = kdf.GetBytes(KeySize);
|
||||||
|
}
|
||||||
|
aes.Key = key;
|
||||||
|
|
||||||
|
// Send salt only
|
||||||
|
await stream.WriteAsync(salt, 0, salt.Length);
|
||||||
await stream.FlushAsync();
|
await stream.FlushAsync();
|
||||||
|
}
|
||||||
|
|
||||||
// Derive key using PBKDF2-SHA256 + salt + password + pepper
|
public static async Task ReadExactlyAsync(this Stream stream, byte[] buffer, int offset, int count)
|
||||||
if (string.IsNullOrEmpty(password))
|
{
|
||||||
|
int read = 0;
|
||||||
|
while (read < count)
|
||||||
{
|
{
|
||||||
password = "EonaCat.Connections";
|
int readBytes = await stream.ReadAsync(buffer, offset + read, count - read);
|
||||||
|
if (readBytes == 0) throw new EndOfStreamException();
|
||||||
|
read += readBytes;
|
||||||
}
|
}
|
||||||
var derivedKey = PBKDF2_SHA256(Combine(Combine(rawKey, Encoding.UTF8.GetBytes(password)), Encoding.UTF8.GetBytes(Pepper)), salt, 100_000, 32);
|
|
||||||
aes.Key = derivedKey;
|
|
||||||
|
|
||||||
return aes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Receive AES key, IV, and salt from the stream and derive the AES key.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="stream"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static async Task<Aes> ReceiveAesKeyAsync(Stream stream, string password = null)
|
|
||||||
{
|
|
||||||
var rawKey = await ReadBytesWithLengthAsync(stream);
|
|
||||||
var iv = await ReadBytesWithLengthAsync(stream);
|
|
||||||
var salt = await ReadBytesWithLengthAsync(stream);
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(password))
|
|
||||||
{
|
|
||||||
password = "EonaCat.Connections";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Derived key using PBKDF2-SHA256 + salt + password + pepper
|
|
||||||
var derivedKey = PBKDF2_SHA256(Combine(Combine(rawKey, Encoding.UTF8.GetBytes(password)), Encoding.UTF8.GetBytes(Pepper)), salt, 100_000, 32);
|
|
||||||
|
|
||||||
Aes _aesEncryption = Aes.Create();
|
|
||||||
_aesEncryption.Key = derivedKey;
|
|
||||||
_aesEncryption.IV = iv;
|
|
||||||
return _aesEncryption;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] PBKDF2_SHA256(byte[] password, byte[] salt, int iterations, int outputBytes)
|
|
||||||
{
|
|
||||||
using (var hmac = new HMACSHA256(password))
|
|
||||||
{
|
|
||||||
int hashLength = hmac.HashSize / 8;
|
|
||||||
int keyBlocks = (int)Math.Ceiling((double)outputBytes / hashLength);
|
|
||||||
byte[] output = new byte[outputBytes];
|
|
||||||
byte[] buffer = new byte[hashLength];
|
|
||||||
|
|
||||||
for (int block = 1; block <= keyBlocks; block++)
|
|
||||||
{
|
|
||||||
byte[] intBlock = BitConverter.GetBytes(block);
|
|
||||||
if (BitConverter.IsLittleEndian)
|
|
||||||
{
|
|
||||||
Array.Reverse(intBlock);
|
|
||||||
}
|
|
||||||
|
|
||||||
hmac.Initialize();
|
|
||||||
hmac.TransformBlock(salt, 0, salt.Length, salt, 0);
|
|
||||||
hmac.TransformFinalBlock(intBlock, 0, intBlock.Length);
|
|
||||||
Array.Copy(hmac.Hash, buffer, hashLength);
|
|
||||||
|
|
||||||
byte[] temp = (byte[])buffer.Clone();
|
|
||||||
for (int i = 1; i < iterations; i++)
|
|
||||||
{
|
|
||||||
temp = hmac.ComputeHash(temp);
|
|
||||||
for (int j = 0; j < hashLength; j++)
|
|
||||||
{
|
|
||||||
buffer[j] ^= temp[j];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int offset = (block - 1) * hashLength;
|
|
||||||
int remaining = Math.Min(hashLength, outputBytes - offset);
|
|
||||||
Array.Copy(buffer, 0, output, offset, remaining);
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<byte[]> ReadBytesWithLengthAsync(Stream stream)
|
|
||||||
{
|
|
||||||
var lengthBytes = new byte[4];
|
|
||||||
await ReadExactlyAsync(stream, lengthBytes, 0, 4);
|
|
||||||
int length = BitConverter.ToInt32(lengthBytes, 0);
|
|
||||||
|
|
||||||
var data = new byte[length];
|
|
||||||
await ReadExactlyAsync(stream, data, 0, length);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task ReadExactlyAsync(Stream stream, byte[] buffer, int offset, int count)
|
|
||||||
{
|
|
||||||
int totalRead = 0;
|
|
||||||
while (totalRead < count)
|
|
||||||
{
|
|
||||||
int read = await stream.ReadAsync(buffer, offset + totalRead, count - totalRead);
|
|
||||||
if (read == 0)
|
|
||||||
{
|
|
||||||
throw new EndOfStreamException("Stream ended prematurely");
|
|
||||||
}
|
|
||||||
|
|
||||||
totalRead += read;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task WriteBytesWithLengthAsync(Stream stream, byte[] data)
|
|
||||||
{
|
|
||||||
var lengthBytes = BitConverter.GetBytes(data.Length);
|
|
||||||
await stream.WriteAsync(lengthBytes, 0, 4);
|
|
||||||
await stream.WriteAsync(data, 0, data.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] Combine(byte[] a, byte[] b)
|
|
||||||
{
|
|
||||||
var c = new byte[a.Length + b.Length];
|
|
||||||
Buffer.BlockCopy(a, 0, c, 0, a.Length);
|
|
||||||
Buffer.BlockCopy(b, 0, c, a.Length, b.Length);
|
|
||||||
return c;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ namespace EonaCat.Connections
|
||||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
// 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.
|
// See the LICENSE file or go to https://EonaCat.com/license for full license details.
|
||||||
|
|
||||||
public class NetworkClient
|
public class NetworkClient : IDisposable
|
||||||
{
|
{
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
private TcpClient _tcpClient;
|
private TcpClient _tcpClient;
|
||||||
|
@ -25,6 +25,7 @@ namespace EonaCat.Connections
|
||||||
private bool _isConnected;
|
private bool _isConnected;
|
||||||
|
|
||||||
public bool IsConnected => _isConnected;
|
public bool IsConnected => _isConnected;
|
||||||
|
public bool IsAutoReconnecting { get; private set; }
|
||||||
|
|
||||||
public event EventHandler<ConnectionEventArgs> OnConnected;
|
public event EventHandler<ConnectionEventArgs> OnConnected;
|
||||||
public event EventHandler<DataReceivedEventArgs> OnDataReceived;
|
public event EventHandler<DataReceivedEventArgs> OnDataReceived;
|
||||||
|
@ -33,23 +34,20 @@ namespace EonaCat.Connections
|
||||||
public event EventHandler<ErrorEventArgs> OnEncryptionError;
|
public event EventHandler<ErrorEventArgs> OnEncryptionError;
|
||||||
public event EventHandler<ErrorEventArgs> OnGeneralError;
|
public event EventHandler<ErrorEventArgs> OnGeneralError;
|
||||||
|
|
||||||
public NetworkClient(Configuration config)
|
public string IpAddress => _config?.Host ?? string.Empty;
|
||||||
{
|
public int Port => _config?.Port ?? 0;
|
||||||
_config = config;
|
|
||||||
}
|
public NetworkClient(Configuration config) => _config = config;
|
||||||
|
|
||||||
public async Task ConnectAsync()
|
public async Task ConnectAsync()
|
||||||
{
|
{
|
||||||
|
_cancellation?.Cancel();
|
||||||
_cancellation = new CancellationTokenSource();
|
_cancellation = new CancellationTokenSource();
|
||||||
|
|
||||||
if (_config.Protocol == ProtocolType.TCP)
|
if (_config.Protocol == ProtocolType.TCP)
|
||||||
{
|
|
||||||
await ConnectTcpAsync();
|
await ConnectTcpAsync();
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
await ConnectUdpAsync();
|
||||||
await ConnectUdp();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ConnectTcpAsync()
|
private async Task ConnectTcpAsync()
|
||||||
|
@ -61,20 +59,16 @@ namespace EonaCat.Connections
|
||||||
|
|
||||||
Stream stream = _tcpClient.GetStream();
|
Stream stream = _tcpClient.GetStream();
|
||||||
|
|
||||||
// Setup SSL if required
|
|
||||||
if (_config.UseSsl)
|
if (_config.UseSsl)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sslStream = new SslStream(stream, false, userCertificateValidationCallback: _config.GetRemoteCertificateValidationCallback());
|
var sslStream = new SslStream(stream, false, _config.GetRemoteCertificateValidationCallback());
|
||||||
if (_config.Certificate != null)
|
if (_config.Certificate != null)
|
||||||
{
|
|
||||||
sslStream.AuthenticateAsClient(_config.Host, new X509CertificateCollection { _config.Certificate }, _config.CheckCertificateRevocation);
|
sslStream.AuthenticateAsClient(_config.Host, new X509CertificateCollection { _config.Certificate }, _config.CheckCertificateRevocation);
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
sslStream.AuthenticateAsClient(_config.Host);
|
sslStream.AuthenticateAsClient(_config.Host);
|
||||||
}
|
|
||||||
stream = sslStream;
|
stream = sslStream;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -84,7 +78,6 @@ namespace EonaCat.Connections
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup AES encryption if required
|
|
||||||
if (_config.UseAesEncryption)
|
if (_config.UseAesEncryption)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
@ -103,49 +96,38 @@ namespace EonaCat.Connections
|
||||||
|
|
||||||
OnConnected?.Invoke(this, new ConnectionEventArgs { ClientId = "self", RemoteEndPoint = new IPEndPoint(IPAddress.Parse(_config.Host), _config.Port) });
|
OnConnected?.Invoke(this, new ConnectionEventArgs { ClientId = "self", RemoteEndPoint = new IPEndPoint(IPAddress.Parse(_config.Host), _config.Port) });
|
||||||
|
|
||||||
// Start receiving data
|
_ = Task.Run(() => ReceiveDataAsync(_cancellation.Token), _cancellation.Token);
|
||||||
_ = Task.Run(() => ReceiveDataAsync(), _cancellation.Token);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_isConnected = false;
|
_isConnected = false;
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Failed to connect" });
|
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Failed to connect" });
|
||||||
await Task.Run(() => AutoReconnectAsync());
|
_ = Task.Run(() => AutoReconnectAsync());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string IpAddress => _config != null ? _config.Host : string.Empty;
|
private async Task ConnectUdpAsync()
|
||||||
public int Port => _config != null ? _config.Port : 0;
|
|
||||||
|
|
||||||
public bool IsAutoReconnecting { get; private set; }
|
|
||||||
|
|
||||||
private async Task ConnectUdp()
|
|
||||||
{
|
{
|
||||||
await Task.Run(() =>
|
try
|
||||||
{
|
{
|
||||||
try
|
_udpClient = new UdpClient();
|
||||||
{
|
_udpClient.Connect(_config.Host, _config.Port);
|
||||||
_udpClient = new UdpClient();
|
_isConnected = true;
|
||||||
_udpClient.Connect(_config.Host, _config.Port);
|
|
||||||
_isConnected = true;
|
|
||||||
|
|
||||||
OnConnected?.Invoke(this, new ConnectionEventArgs { ClientId = "self", RemoteEndPoint = new IPEndPoint(IPAddress.Parse(_config.Host), _config.Port) });
|
OnConnected?.Invoke(this, new ConnectionEventArgs { ClientId = "self", RemoteEndPoint = new IPEndPoint(IPAddress.Parse(_config.Host), _config.Port) });
|
||||||
|
|
||||||
// Start receiving data
|
_ = Task.Run(() => ReceiveUdpDataAsync(_cancellation.Token), _cancellation.Token);
|
||||||
_ = Task.Run(() => ReceiveUdpDataAsync(), _cancellation.Token);
|
}
|
||||||
}
|
catch (Exception ex)
|
||||||
catch (Exception ex)
|
{
|
||||||
{
|
_isConnected = false;
|
||||||
_isConnected = false;
|
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Failed to connect UDP" });
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Failed to connect UDP" });
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ReceiveDataAsync(CancellationToken ct)
|
||||||
private async Task ReceiveDataAsync()
|
|
||||||
{
|
{
|
||||||
while (!_cancellation.Token.IsCancellationRequested && _isConnected)
|
while (!ct.IsCancellationRequested && _isConnected)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -153,34 +135,22 @@ namespace EonaCat.Connections
|
||||||
|
|
||||||
if (_config.UseAesEncryption && _aesEncryption != null)
|
if (_config.UseAesEncryption && _aesEncryption != null)
|
||||||
{
|
{
|
||||||
// Read 4-byte length prefix
|
|
||||||
var lengthBuffer = new byte[4];
|
var lengthBuffer = new byte[4];
|
||||||
int read = await ReadExactAsync(_stream, lengthBuffer, 4, _cancellation.Token);
|
if (await ReadExactAsync(_stream, lengthBuffer, 4, ct) == 0) break;
|
||||||
if (read == 0)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (BitConverter.IsLittleEndian)
|
|
||||||
{
|
|
||||||
Array.Reverse(lengthBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (BitConverter.IsLittleEndian) Array.Reverse(lengthBuffer);
|
||||||
int length = BitConverter.ToInt32(lengthBuffer, 0);
|
int length = BitConverter.ToInt32(lengthBuffer, 0);
|
||||||
|
|
||||||
// Read encrypted payload
|
|
||||||
var encrypted = new byte[length];
|
var encrypted = new byte[length];
|
||||||
await ReadExactAsync(_stream, encrypted, length, _cancellation.Token);
|
await ReadExactAsync(_stream, encrypted, length, ct);
|
||||||
data = await DecryptDataAsync(encrypted, _aesEncryption);
|
|
||||||
|
data = await AesCryptoHelpers.DecryptDataAsync(encrypted, _aesEncryption);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
data = new byte[_config.BufferSize];
|
data = new byte[_config.BufferSize];
|
||||||
int bytesRead = await _stream.ReadAsync(data, 0, data.Length, _cancellation.Token);
|
int bytesRead = await _stream.ReadAsync(data, 0, data.Length, ct);
|
||||||
if (bytesRead == 0)
|
if (bytesRead == 0) break;
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bytesRead < data.Length)
|
if (bytesRead < data.Length)
|
||||||
{
|
{
|
||||||
|
@ -196,7 +166,6 @@ namespace EonaCat.Connections
|
||||||
{
|
{
|
||||||
_isConnected = false;
|
_isConnected = false;
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error receiving data" });
|
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error receiving data" });
|
||||||
|
|
||||||
_ = Task.Run(() => AutoReconnectAsync());
|
_ = Task.Run(() => AutoReconnectAsync());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -205,27 +174,21 @@ namespace EonaCat.Connections
|
||||||
await DisconnectAsync();
|
await DisconnectAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task<int> ReadExactAsync(Stream stream, byte[] buffer, int length, CancellationToken ct)
|
private async Task<int> ReadExactAsync(Stream stream, byte[] buffer, int length, CancellationToken ct)
|
||||||
{
|
{
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
while (offset < length)
|
while (offset < length)
|
||||||
{
|
{
|
||||||
int read = await stream.ReadAsync(buffer, offset, length - offset, ct);
|
int read = await stream.ReadAsync(buffer, offset, length - offset, ct);
|
||||||
if (read == 0)
|
if (read == 0) return 0;
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
offset += read;
|
offset += read;
|
||||||
}
|
}
|
||||||
return offset;
|
return offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ReceiveUdpDataAsync(CancellationToken ct)
|
||||||
private async Task ReceiveUdpDataAsync()
|
|
||||||
{
|
{
|
||||||
while (!_cancellation.Token.IsCancellationRequested && _isConnected)
|
while (!ct.IsCancellationRequested && _isConnected)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -234,10 +197,8 @@ namespace EonaCat.Connections
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error receiving data" });
|
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error receiving UDP data" });
|
||||||
_isConnected = false;
|
_isConnected = false;
|
||||||
|
|
||||||
// Start reconnect
|
|
||||||
_ = Task.Run(() => AutoReconnectAsync());
|
_ = Task.Run(() => AutoReconnectAsync());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -246,77 +207,49 @@ namespace EonaCat.Connections
|
||||||
|
|
||||||
private async Task ProcessReceivedDataAsync(byte[] data)
|
private async Task ProcessReceivedDataAsync(byte[] data)
|
||||||
{
|
{
|
||||||
await Task.Run(() =>
|
try
|
||||||
{
|
{
|
||||||
|
string stringData = null;
|
||||||
|
bool isBinary = true;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Data is already decrypted if AES is enabled
|
stringData = Encoding.UTF8.GetString(data);
|
||||||
// Just update stats / handle string conversion
|
isBinary = Encoding.UTF8.GetBytes(stringData).Length != data.Length;
|
||||||
|
|
||||||
bool isBinary = true;
|
|
||||||
string stringData = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
stringData = Encoding.UTF8.GetString(data);
|
|
||||||
if (Encoding.UTF8.GetBytes(stringData).Length == data.Length)
|
|
||||||
{
|
|
||||||
isBinary = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Keep as binary
|
|
||||||
}
|
|
||||||
|
|
||||||
OnDataReceived?.Invoke(this, new DataReceivedEventArgs
|
|
||||||
{
|
|
||||||
ClientId = "server",
|
|
||||||
Data = data,
|
|
||||||
StringData = stringData,
|
|
||||||
IsBinary = isBinary
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch { }
|
||||||
|
|
||||||
|
OnDataReceived?.Invoke(this, new DataReceivedEventArgs
|
||||||
{
|
{
|
||||||
if (_config.UseAesEncryption)
|
ClientId = "server",
|
||||||
{
|
Data = data,
|
||||||
OnEncryptionError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error processing data" });
|
StringData = stringData,
|
||||||
}
|
IsBinary = isBinary
|
||||||
else
|
});
|
||||||
{
|
}
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error processing data" });
|
catch (Exception ex)
|
||||||
}
|
{
|
||||||
}
|
var handler = _config.UseAesEncryption ? OnEncryptionError : OnGeneralError;
|
||||||
});
|
handler?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error processing data" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task SendAsync(byte[] data)
|
public async Task SendAsync(byte[] data)
|
||||||
{
|
{
|
||||||
if (!_isConnected)
|
if (!_isConnected) return;
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_config.UseAesEncryption && _aesEncryption != null)
|
if (_config.UseAesEncryption && _aesEncryption != null)
|
||||||
{
|
{
|
||||||
// Encrypt payload
|
data = await AesCryptoHelpers.EncryptDataAsync(data, _aesEncryption);
|
||||||
data = await EncryptDataAsync(data, _aesEncryption);
|
|
||||||
|
|
||||||
// Prepend 4-byte length for framing
|
|
||||||
var lengthPrefix = BitConverter.GetBytes(data.Length);
|
var lengthPrefix = BitConverter.GetBytes(data.Length);
|
||||||
if (BitConverter.IsLittleEndian)
|
if (BitConverter.IsLittleEndian) Array.Reverse(lengthPrefix);
|
||||||
{
|
|
||||||
Array.Reverse(lengthPrefix);
|
|
||||||
}
|
|
||||||
|
|
||||||
var framed = new byte[lengthPrefix.Length + data.Length];
|
var framed = new byte[lengthPrefix.Length + data.Length];
|
||||||
Buffer.BlockCopy(lengthPrefix, 0, framed, 0, lengthPrefix.Length);
|
Buffer.BlockCopy(lengthPrefix, 0, framed, 0, lengthPrefix.Length);
|
||||||
Buffer.BlockCopy(data, 0, framed, lengthPrefix.Length, data.Length);
|
Buffer.BlockCopy(data, 0, framed, lengthPrefix.Length, data.Length);
|
||||||
|
|
||||||
data = framed;
|
data = framed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -332,63 +265,17 @@ namespace EonaCat.Connections
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
if (_config.UseAesEncryption)
|
var handler = _config.UseAesEncryption ? OnEncryptionError : OnGeneralError;
|
||||||
{
|
handler?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error sending data" });
|
||||||
OnEncryptionError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error encrypting/sending data" });
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error sending data" });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SendAsync(string message) => await SendAsync(Encoding.UTF8.GetBytes(message));
|
||||||
public async Task SendAsync(string message)
|
public async Task SendNicknameAsync(string nickname) => await SendAsync($"NICKNAME:{nickname}");
|
||||||
{
|
|
||||||
await SendAsync(Encoding.UTF8.GetBytes(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SendNicknameAsync(string nickname)
|
|
||||||
{
|
|
||||||
await SendAsync($"NICKNAME:{nickname}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<byte[]> EncryptDataAsync(byte[] data, Aes aes)
|
|
||||||
{
|
|
||||||
using (var encryptor = aes.CreateEncryptor())
|
|
||||||
using (var ms = new MemoryStream())
|
|
||||||
using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
|
|
||||||
{
|
|
||||||
await cs.WriteAsync(data, 0, data.Length);
|
|
||||||
cs.FlushFinalBlock();
|
|
||||||
return ms.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<byte[]> DecryptDataAsync(byte[] data, Aes aes)
|
|
||||||
{
|
|
||||||
using (var decryptor = aes.CreateDecryptor())
|
|
||||||
using (var ms = new MemoryStream(data))
|
|
||||||
using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
|
|
||||||
using (var result = new MemoryStream())
|
|
||||||
{
|
|
||||||
await cs.CopyToAsync(result);
|
|
||||||
return result.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AutoReconnectAsync()
|
private async Task AutoReconnectAsync()
|
||||||
{
|
{
|
||||||
if (!_config.EnableAutoReconnect)
|
if (!_config.EnableAutoReconnect || IsAutoReconnecting) return;
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsAutoReconnecting)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int attempt = 0;
|
int attempt = 0;
|
||||||
|
|
||||||
|
@ -397,53 +284,43 @@ namespace EonaCat.Connections
|
||||||
attempt++;
|
attempt++;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { Message = $"Attempting to reconnect (Attempt {attempt})" });
|
|
||||||
IsAutoReconnecting = true;
|
IsAutoReconnecting = true;
|
||||||
|
OnGeneralError?.Invoke(this, new ErrorEventArgs { Message = $"Reconnecting attempt {attempt}" });
|
||||||
await ConnectAsync();
|
await ConnectAsync();
|
||||||
|
|
||||||
if (_isConnected)
|
if (_isConnected)
|
||||||
{
|
{
|
||||||
IsAutoReconnecting = false;
|
IsAutoReconnecting = false;
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { Message = $"Reconnected successfully after {attempt} attempt(s)" });
|
OnGeneralError?.Invoke(this, new ErrorEventArgs { Message = $"Reconnected after {attempt} attempt(s)" });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch { }
|
||||||
{
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(_config.ReconnectDelayMs);
|
await Task.Delay(_config.ReconnectDelayMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_isConnected)
|
if (!_isConnected)
|
||||||
{
|
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { Message = "Failed to reconnect" });
|
OnGeneralError?.Invoke(this, new ErrorEventArgs { Message = "Failed to reconnect" });
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task DisconnectAsync()
|
public async Task DisconnectAsync()
|
||||||
{
|
{
|
||||||
await Task.Run(() =>
|
_isConnected = false;
|
||||||
{
|
_cancellation?.Cancel();
|
||||||
_isConnected = false;
|
|
||||||
_cancellation?.Cancel();
|
|
||||||
_tcpClient?.Close();
|
|
||||||
_udpClient?.Close();
|
|
||||||
_stream?.Dispose();
|
|
||||||
_aesEncryption?.Dispose();
|
|
||||||
|
|
||||||
OnDisconnected?.Invoke(this, new ConnectionEventArgs { ClientId = "self" });
|
_tcpClient?.Close();
|
||||||
|
_udpClient?.Close();
|
||||||
|
_stream?.Dispose();
|
||||||
|
_aesEncryption?.Dispose();
|
||||||
|
|
||||||
_ = Task.Run(() => AutoReconnectAsync());
|
OnDisconnected?.Invoke(this, new ConnectionEventArgs { ClientId = "self" });
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
_cancellation?.Cancel();
|
||||||
DisconnectAsync().Wait();
|
DisconnectAsync().Wait();
|
||||||
_cancellation?.Dispose();
|
_cancellation?.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ using System.Net.Security;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Security.Authentication;
|
using System.Security.Authentication;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using ErrorEventArgs = EonaCat.Connections.EventArguments.ErrorEventArgs;
|
using ErrorEventArgs = EonaCat.Connections.EventArguments.ErrorEventArgs;
|
||||||
|
|
||||||
|
@ -16,7 +15,7 @@ namespace EonaCat.Connections
|
||||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
// 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.
|
// See the LICENSE file or go to https://EonaCat.com/license for full license details.
|
||||||
|
|
||||||
public class NetworkServer
|
public class NetworkServer : IDisposable
|
||||||
{
|
{
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
private readonly Stats _stats;
|
private readonly Stats _stats;
|
||||||
|
@ -50,28 +49,24 @@ namespace EonaCat.Connections
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string IpAddress => _config != null ? _config.Host : string.Empty;
|
public string IpAddress => _config?.Host ?? string.Empty;
|
||||||
public int Port => _config != null ? _config.Port : 0;
|
public int Port => _config?.Port ?? 0;
|
||||||
|
|
||||||
public async Task StartAsync()
|
public async Task StartAsync()
|
||||||
{
|
{
|
||||||
|
_serverCancellation?.Cancel();
|
||||||
_serverCancellation = new CancellationTokenSource();
|
_serverCancellation = new CancellationTokenSource();
|
||||||
|
|
||||||
if (_config.Protocol == ProtocolType.TCP)
|
if (_config.Protocol == ProtocolType.TCP)
|
||||||
{
|
|
||||||
await StartTcpServerAsync();
|
await StartTcpServerAsync();
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
await StartUdpServerAsync();
|
await StartUdpServerAsync();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task StartTcpServerAsync()
|
private async Task StartTcpServerAsync()
|
||||||
{
|
{
|
||||||
_tcpListener = new TcpListener(IPAddress.Parse(_config.Host), _config.Port);
|
_tcpListener = new TcpListener(IPAddress.Parse(_config.Host), _config.Port);
|
||||||
_tcpListener.Start();
|
_tcpListener.Start();
|
||||||
|
|
||||||
Console.WriteLine($"TCP Server started on {_config.Host}:{_config.Port}");
|
Console.WriteLine($"TCP Server started on {_config.Host}:{_config.Port}");
|
||||||
|
|
||||||
while (!_serverCancellation.Token.IsCancellationRequested)
|
while (!_serverCancellation.Token.IsCancellationRequested)
|
||||||
|
@ -81,10 +76,7 @@ namespace EonaCat.Connections
|
||||||
var tcpClient = await _tcpListener.AcceptTcpClientAsync();
|
var tcpClient = await _tcpListener.AcceptTcpClientAsync();
|
||||||
_ = Task.Run(() => HandleTcpClientAsync(tcpClient), _serverCancellation.Token);
|
_ = Task.Run(() => HandleTcpClientAsync(tcpClient), _serverCancellation.Token);
|
||||||
}
|
}
|
||||||
catch (ObjectDisposedException)
|
catch (ObjectDisposedException) { break; }
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error accepting TCP client" });
|
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error accepting TCP client" });
|
||||||
|
@ -92,11 +84,6 @@ namespace EonaCat.Connections
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dictionary<string, Connection> GetClients()
|
|
||||||
{
|
|
||||||
return _clients.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task StartUdpServerAsync()
|
private async Task StartUdpServerAsync()
|
||||||
{
|
{
|
||||||
_udpListener = new UdpClient(_config.Port);
|
_udpListener = new UdpClient(_config.Port);
|
||||||
|
@ -109,10 +96,7 @@ namespace EonaCat.Connections
|
||||||
var result = await _udpListener.ReceiveAsync();
|
var result = await _udpListener.ReceiveAsync();
|
||||||
_ = Task.Run(() => HandleUdpDataAsync(result), _serverCancellation.Token);
|
_ = Task.Run(() => HandleUdpDataAsync(result), _serverCancellation.Token);
|
||||||
}
|
}
|
||||||
catch (ObjectDisposedException)
|
catch (ObjectDisposedException) { break; }
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error receiving UDP data" });
|
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error receiving UDP data" });
|
||||||
|
@ -135,22 +119,23 @@ namespace EonaCat.Connections
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Configure TCP client
|
|
||||||
tcpClient.NoDelay = !_config.EnableNagle;
|
tcpClient.NoDelay = !_config.EnableNagle;
|
||||||
if (_config.EnableKeepAlive)
|
if (_config.EnableKeepAlive)
|
||||||
{
|
|
||||||
tcpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
|
tcpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
|
||||||
}
|
|
||||||
|
|
||||||
Stream stream = tcpClient.GetStream();
|
Stream stream = tcpClient.GetStream();
|
||||||
|
|
||||||
// Setup SSL if required
|
|
||||||
if (_config.UseSsl)
|
if (_config.UseSsl)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sslStream = new SslStream(stream, false, userCertificateValidationCallback: _config.GetRemoteCertificateValidationCallback());
|
var sslStream = new SslStream(stream, false, _config.GetRemoteCertificateValidationCallback());
|
||||||
await sslStream.AuthenticateAsServerAsync(_config.Certificate, _config.MutuallyAuthenticate, SslProtocols.Tls12 | SslProtocols.Tls13, _config.CheckCertificateRevocation);
|
await sslStream.AuthenticateAsServerAsync(
|
||||||
|
_config.Certificate,
|
||||||
|
_config.MutuallyAuthenticate,
|
||||||
|
SslProtocols.Tls12 | SslProtocols.Tls13,
|
||||||
|
_config.CheckCertificateRevocation
|
||||||
|
);
|
||||||
stream = sslStream;
|
stream = sslStream;
|
||||||
client.IsSecure = true;
|
client.IsSecure = true;
|
||||||
}
|
}
|
||||||
|
@ -161,17 +146,19 @@ namespace EonaCat.Connections
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup AES encryption if required
|
|
||||||
if (_config.UseAesEncryption)
|
if (_config.UseAesEncryption)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Create AES object
|
||||||
client.AesEncryption = Aes.Create();
|
client.AesEncryption = Aes.Create();
|
||||||
client.AesEncryption.GenerateKey();
|
client.AesEncryption.KeySize = 256;
|
||||||
client.AesEncryption.GenerateIV();
|
client.AesEncryption.BlockSize = 128;
|
||||||
|
client.AesEncryption.Mode = CipherMode.CBC;
|
||||||
|
client.AesEncryption.Padding = PaddingMode.PKCS7;
|
||||||
client.IsEncrypted = true;
|
client.IsEncrypted = true;
|
||||||
|
|
||||||
// Securely send raw AES key + IV + salt + password
|
// Send salt to client to derive key
|
||||||
await AesKeyExchange.SendAesKeyAsync(stream, client.AesEncryption, _config.AesPassword);
|
await AesKeyExchange.SendAesKeyAsync(stream, client.AesEncryption, _config.AesPassword);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -189,14 +176,10 @@ namespace EonaCat.Connections
|
||||||
client.Stream = stream;
|
client.Stream = stream;
|
||||||
_clients[clientId] = client;
|
_clients[clientId] = client;
|
||||||
|
|
||||||
lock (_statsLock)
|
lock (_statsLock) { _stats.TotalConnections++; }
|
||||||
{
|
|
||||||
_stats.TotalConnections++;
|
|
||||||
}
|
|
||||||
|
|
||||||
OnConnected?.Invoke(this, new ConnectionEventArgs { ClientId = clientId, RemoteEndPoint = client.RemoteEndPoint });
|
OnConnected?.Invoke(this, new ConnectionEventArgs { ClientId = clientId, RemoteEndPoint = client.RemoteEndPoint });
|
||||||
|
|
||||||
// Handle client communication
|
|
||||||
await HandleClientCommunicationAsync(client);
|
await HandleClientCommunicationAsync(client);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -205,14 +188,15 @@ namespace EonaCat.Connections
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
await DisconnectClientAsync(clientId);
|
DisconnectClient(clientId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private async Task HandleUdpDataAsync(UdpReceiveResult result)
|
private async Task HandleUdpDataAsync(UdpReceiveResult result)
|
||||||
{
|
{
|
||||||
var clientKey = result.RemoteEndPoint.ToString();
|
var clientKey = result.RemoteEndPoint.ToString();
|
||||||
|
|
||||||
if (!_clients.TryGetValue(clientKey, out var client))
|
if (!_clients.TryGetValue(clientKey, out var client))
|
||||||
{
|
{
|
||||||
client = new Connection
|
client = new Connection
|
||||||
|
@ -222,12 +206,7 @@ namespace EonaCat.Connections
|
||||||
ConnectedAt = DateTime.UtcNow
|
ConnectedAt = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
_clients[clientKey] = client;
|
_clients[clientKey] = client;
|
||||||
|
lock (_statsLock) { _stats.TotalConnections++; }
|
||||||
lock (_statsLock)
|
|
||||||
{
|
|
||||||
_stats.TotalConnections++;
|
|
||||||
}
|
|
||||||
|
|
||||||
OnConnected?.Invoke(this, new ConnectionEventArgs { ClientId = clientKey, RemoteEndPoint = result.RemoteEndPoint });
|
OnConnected?.Invoke(this, new ConnectionEventArgs { ClientId = clientKey, RemoteEndPoint = result.RemoteEndPoint });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,7 +215,7 @@ namespace EonaCat.Connections
|
||||||
|
|
||||||
private async Task HandleClientCommunicationAsync(Connection client)
|
private async Task HandleClientCommunicationAsync(Connection client)
|
||||||
{
|
{
|
||||||
var lengthBuffer = new byte[4]; // length prefix
|
var lengthBuffer = new byte[4];
|
||||||
|
|
||||||
while (!client.CancellationToken.Token.IsCancellationRequested && client.TcpClient.Connected)
|
while (!client.CancellationToken.Token.IsCancellationRequested && client.TcpClient.Connected)
|
||||||
{
|
{
|
||||||
|
@ -246,37 +225,20 @@ namespace EonaCat.Connections
|
||||||
|
|
||||||
if (client.IsEncrypted && client.AesEncryption != null)
|
if (client.IsEncrypted && client.AesEncryption != null)
|
||||||
{
|
{
|
||||||
// Read 4-byte length first
|
if (await ReadExactAsync(client.Stream, lengthBuffer, 4, client.CancellationToken.Token) == 0) break;
|
||||||
int read = await ReadExactAsync(client.Stream, lengthBuffer, 4, client.CancellationToken.Token);
|
if (BitConverter.IsLittleEndian) Array.Reverse(lengthBuffer);
|
||||||
if (read == 0)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (BitConverter.IsLittleEndian)
|
|
||||||
{
|
|
||||||
Array.Reverse(lengthBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
int length = BitConverter.ToInt32(lengthBuffer, 0);
|
int length = BitConverter.ToInt32(lengthBuffer, 0);
|
||||||
|
|
||||||
// Read full encrypted message
|
|
||||||
var encrypted = new byte[length];
|
var encrypted = new byte[length];
|
||||||
await ReadExactAsync(client.Stream, encrypted, length, client.CancellationToken.Token);
|
await ReadExactAsync(client.Stream, encrypted, length, client.CancellationToken.Token);
|
||||||
|
|
||||||
// **Decrypt once here**
|
data = await AesCryptoHelpers.DecryptDataAsync(encrypted, client.AesEncryption);
|
||||||
data = await DecryptDataAsync(encrypted, client.AesEncryption);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Non-encrypted: just read raw bytes
|
|
||||||
data = new byte[_config.BufferSize];
|
data = new byte[_config.BufferSize];
|
||||||
int bytesRead = await client.Stream.ReadAsync(data, 0, data.Length, client.CancellationToken.Token);
|
int bytesRead = await client.Stream.ReadAsync(data, 0, data.Length, client.CancellationToken.Token);
|
||||||
if (bytesRead == 0)
|
if (bytesRead == 0) break;
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bytesRead < data.Length)
|
if (bytesRead < data.Length)
|
||||||
{
|
{
|
||||||
var tmp = new byte[bytesRead];
|
var tmp = new byte[bytesRead];
|
||||||
|
@ -289,12 +251,7 @@ namespace EonaCat.Connections
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs
|
OnGeneralError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error reading from client" });
|
||||||
{
|
|
||||||
ClientId = client.Id,
|
|
||||||
Exception = ex,
|
|
||||||
Message = "Error reading from client"
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -306,17 +263,12 @@ namespace EonaCat.Connections
|
||||||
while (offset < length)
|
while (offset < length)
|
||||||
{
|
{
|
||||||
int read = await stream.ReadAsync(buffer, offset, length - offset, ct);
|
int read = await stream.ReadAsync(buffer, offset, length - offset, ct);
|
||||||
if (read == 0)
|
if (read == 0) return 0;
|
||||||
{
|
|
||||||
return 0; // disconnected
|
|
||||||
}
|
|
||||||
|
|
||||||
offset += read;
|
offset += read;
|
||||||
}
|
}
|
||||||
return offset;
|
return offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task ProcessReceivedDataAsync(Connection client, byte[] data)
|
private async Task ProcessReceivedDataAsync(Connection client, byte[] data)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
@ -328,57 +280,26 @@ namespace EonaCat.Connections
|
||||||
_stats.MessagesReceived++;
|
_stats.MessagesReceived++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to decode as string, fallback to binary
|
|
||||||
bool isBinary = true;
|
bool isBinary = true;
|
||||||
string stringData = null;
|
string stringData = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
stringData = Encoding.UTF8.GetString(data);
|
stringData = Encoding.UTF8.GetString(data);
|
||||||
if (Encoding.UTF8.GetBytes(stringData).Length == data.Length)
|
isBinary = Encoding.UTF8.GetBytes(stringData).Length != data.Length;
|
||||||
{
|
|
||||||
isBinary = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
|
|
||||||
// Handle special commands
|
|
||||||
if (!isBinary && stringData != null)
|
if (!isBinary && stringData != null)
|
||||||
{
|
{
|
||||||
if (stringData.StartsWith("NICKNAME:"))
|
if (stringData.StartsWith("NICKNAME:"))
|
||||||
{
|
{
|
||||||
var nickname = stringData.Substring(9);
|
client.Nickname = stringData.Substring(9);
|
||||||
client.Nickname = nickname;
|
OnConnectedWithNickname?.Invoke(this, new ConnectionEventArgs { ClientId = client.Id, RemoteEndPoint = client.RemoteEndPoint, Nickname = client.Nickname });
|
||||||
OnConnectedWithNickname?.Invoke(this, new ConnectionEventArgs
|
|
||||||
{
|
|
||||||
ClientId = client.Id,
|
|
||||||
RemoteEndPoint = client.RemoteEndPoint,
|
|
||||||
Nickname = nickname
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (stringData.StartsWith("[NICKNAME]", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var nickname = StringHelper.GetTextBetweenTags(stringData, "[NICKNAME]", "[/NICKNAME]");
|
|
||||||
if (string.IsNullOrWhiteSpace(nickname))
|
|
||||||
{
|
|
||||||
nickname = client.Id; // fallback to client ID if no valid nickname was provided
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
client.Nickname = nickname;
|
|
||||||
}
|
|
||||||
OnConnectedWithNickname?.Invoke(this, new ConnectionEventArgs
|
|
||||||
{
|
|
||||||
ClientId = client.Id,
|
|
||||||
RemoteEndPoint = client.RemoteEndPoint,
|
|
||||||
Nickname = nickname
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (stringData.Equals("DISCONNECT", StringComparison.OrdinalIgnoreCase))
|
else if (stringData.Equals("DISCONNECT", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
await DisconnectClientAsync(client.Id);
|
DisconnectClient(client.Id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -396,49 +317,71 @@ namespace EonaCat.Connections
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
if (client.IsEncrypted)
|
var handler = client.IsEncrypted ? OnEncryptionError : OnGeneralError;
|
||||||
{
|
handler?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error processing data" });
|
||||||
OnEncryptionError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error processing data" });
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error processing data" });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SendDataAsync(Connection client, byte[] data)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Encrypt if AES is enabled
|
||||||
|
if (client.IsEncrypted && client.AesEncryption != null)
|
||||||
|
{
|
||||||
|
data = await AesCryptoHelpers.EncryptDataAsync(data, client.AesEncryption);
|
||||||
|
|
||||||
|
// Prepend 4-byte length (big-endian) for framing
|
||||||
|
var lengthPrefix = BitConverter.GetBytes(data.Length);
|
||||||
|
if (BitConverter.IsLittleEndian)
|
||||||
|
Array.Reverse(lengthPrefix);
|
||||||
|
|
||||||
|
var framed = new byte[lengthPrefix.Length + data.Length];
|
||||||
|
Buffer.BlockCopy(lengthPrefix, 0, framed, 0, lengthPrefix.Length);
|
||||||
|
Buffer.BlockCopy(data, 0, framed, lengthPrefix.Length, data.Length);
|
||||||
|
|
||||||
|
data = framed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_config.Protocol == ProtocolType.TCP)
|
||||||
|
{
|
||||||
|
await client.Stream.WriteAsync(data, 0, data.Length);
|
||||||
|
await client.Stream.FlushAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _udpListener.SendAsync(data, data.Length, client.RemoteEndPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update stats
|
||||||
|
client.BytesSent += data.Length;
|
||||||
|
lock (_statsLock)
|
||||||
|
{
|
||||||
|
_stats.BytesSent += data.Length;
|
||||||
|
_stats.MessagesSent++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var handler = client.IsEncrypted ? OnEncryptionError : OnGeneralError;
|
||||||
|
handler?.Invoke(this, new ErrorEventArgs
|
||||||
|
{
|
||||||
|
ClientId = client.Id,
|
||||||
|
Exception = ex,
|
||||||
|
Message = "Error sending data"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task SendToClientAsync(string clientId, byte[] data)
|
public async Task SendToClientAsync(string clientId, byte[] data)
|
||||||
{
|
{
|
||||||
// Check if clientId is a guid
|
if (_clients.TryGetValue(clientId, out var client))
|
||||||
if (Guid.TryParse(clientId, out _))
|
|
||||||
{
|
{
|
||||||
if (_clients.TryGetValue(clientId, out var client))
|
await SendDataAsync(client, data);
|
||||||
{
|
return;
|
||||||
await SendDataAsync(client, data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if clientId is an IP:Port format
|
// Fallback: try nickname
|
||||||
string[] parts = clientId.Split(':');
|
|
||||||
if (parts.Length == 2)
|
|
||||||
{
|
|
||||||
if (IPAddress.TryParse(parts[0], out IPAddress ip) && int.TryParse(parts[1], out int port))
|
|
||||||
{
|
|
||||||
IPEndPoint endPoint = new IPEndPoint(ip, port);
|
|
||||||
string clientKey = endPoint.ToString();
|
|
||||||
|
|
||||||
if (_clients.TryGetValue(clientKey, out var client))
|
|
||||||
{
|
|
||||||
// If inside async method, you can use await
|
|
||||||
await SendDataAsync(client, data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the client is a nickname
|
|
||||||
foreach (var kvp in _clients)
|
foreach (var kvp in _clients)
|
||||||
{
|
{
|
||||||
if (kvp.Value.Nickname != null && kvp.Value.Nickname.Equals(clientId, StringComparison.OrdinalIgnoreCase))
|
if (kvp.Value.Nickname != null && kvp.Value.Nickname.Equals(clientId, StringComparison.OrdinalIgnoreCase))
|
||||||
|
@ -456,11 +399,7 @@ namespace EonaCat.Connections
|
||||||
|
|
||||||
public async Task BroadcastAsync(byte[] data)
|
public async Task BroadcastAsync(byte[] data)
|
||||||
{
|
{
|
||||||
var tasks = new List<Task>();
|
var tasks = _clients.Values.Select(c => SendDataAsync(c, data)).ToArray();
|
||||||
foreach (var client in _clients.Values)
|
|
||||||
{
|
|
||||||
tasks.Add(SendDataAsync(client, data));
|
|
||||||
}
|
|
||||||
await Task.WhenAll(tasks);
|
await Task.WhenAll(tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -469,105 +408,24 @@ namespace EonaCat.Connections
|
||||||
await BroadcastAsync(Encoding.UTF8.GetBytes(message));
|
await BroadcastAsync(Encoding.UTF8.GetBytes(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SendDataAsync(Connection client, byte[] data)
|
private void DisconnectClient(string clientId)
|
||||||
{
|
{
|
||||||
try
|
if (_clients.TryRemove(clientId, out var client))
|
||||||
{
|
{
|
||||||
if (client.IsEncrypted && client.AesEncryption != null)
|
try
|
||||||
{
|
{
|
||||||
// Encrypt payload
|
client.CancellationToken?.Cancel();
|
||||||
data = await EncryptDataAsync(data, client.AesEncryption);
|
client.TcpClient?.Close();
|
||||||
|
client.Stream?.Dispose();
|
||||||
|
client.AesEncryption?.Dispose();
|
||||||
|
|
||||||
// Prepend length for safe framing
|
OnDisconnected?.Invoke(this, new ConnectionEventArgs { ClientId = client.Id, RemoteEndPoint = client.RemoteEndPoint, Nickname = client.Nickname });
|
||||||
var lengthPrefix = BitConverter.GetBytes(data.Length);
|
|
||||||
if (BitConverter.IsLittleEndian)
|
|
||||||
{
|
|
||||||
Array.Reverse(lengthPrefix);
|
|
||||||
}
|
|
||||||
|
|
||||||
var framed = new byte[lengthPrefix.Length + data.Length];
|
|
||||||
Buffer.BlockCopy(lengthPrefix, 0, framed, 0, lengthPrefix.Length);
|
|
||||||
Buffer.BlockCopy(data, 0, framed, lengthPrefix.Length, data.Length);
|
|
||||||
|
|
||||||
data = framed; // replace the data with framed payload
|
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
if (_config.Protocol == ProtocolType.TCP)
|
|
||||||
{
|
{
|
||||||
await client.Stream.WriteAsync(data, 0, data.Length);
|
OnGeneralError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error disconnecting client" });
|
||||||
await client.Stream.FlushAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _udpListener.SendAsync(data, data.Length, client.RemoteEndPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
client.BytesSent += data.Length;
|
|
||||||
lock (_statsLock)
|
|
||||||
{
|
|
||||||
_stats.BytesSent += data.Length;
|
|
||||||
_stats.MessagesSent++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
if (client.IsEncrypted)
|
|
||||||
{
|
|
||||||
OnEncryptionError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error encrypting/sending data" });
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error sending data" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private async Task<byte[]> EncryptDataAsync(byte[] data, Aes aes)
|
|
||||||
{
|
|
||||||
using (var encryptor = aes.CreateEncryptor())
|
|
||||||
using (var ms = new MemoryStream())
|
|
||||||
using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
|
|
||||||
{
|
|
||||||
await cs.WriteAsync(data, 0, data.Length);
|
|
||||||
cs.FlushFinalBlock();
|
|
||||||
return ms.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<byte[]> DecryptDataAsync(byte[] data, Aes aes)
|
|
||||||
{
|
|
||||||
using (var decryptor = aes.CreateDecryptor())
|
|
||||||
using (var ms = new MemoryStream(data))
|
|
||||||
using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
|
|
||||||
using (var result = new MemoryStream())
|
|
||||||
{
|
|
||||||
await cs.CopyToAsync(result);
|
|
||||||
return result.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DisconnectClientAsync(string clientId)
|
|
||||||
{
|
|
||||||
await Task.Run(() =>
|
|
||||||
{
|
|
||||||
if (_clients.TryRemove(clientId, out var client))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
client.CancellationToken?.Cancel();
|
|
||||||
client.TcpClient?.Close();
|
|
||||||
client.Stream?.Dispose();
|
|
||||||
client.AesEncryption?.Dispose();
|
|
||||||
|
|
||||||
OnDisconnected?.Invoke(this, new ConnectionEventArgs { ClientId = clientId, RemoteEndPoint = client.RemoteEndPoint, Nickname = client.Nickname });
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { ClientId = clientId, Exception = ex, Message = "Error disconnecting client" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Stop()
|
public void Stop()
|
||||||
|
@ -576,19 +434,12 @@ namespace EonaCat.Connections
|
||||||
_tcpListener?.Stop();
|
_tcpListener?.Stop();
|
||||||
_udpListener?.Close();
|
_udpListener?.Close();
|
||||||
|
|
||||||
// Disconnect all clients
|
|
||||||
var disconnectTasks = new List<Task>();
|
|
||||||
foreach (var clientId in _clients.Keys.ToArray())
|
foreach (var clientId in _clients.Keys.ToArray())
|
||||||
{
|
{
|
||||||
disconnectTasks.Add(DisconnectClientAsync(clientId));
|
DisconnectClient(clientId);
|
||||||
}
|
}
|
||||||
Task.WaitAll(disconnectTasks.ToArray());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose() => Stop();
|
||||||
{
|
|
||||||
Stop();
|
|
||||||
_serverCancellation?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,63 +7,74 @@ using Timer = System.Timers.Timer;
|
||||||
|
|
||||||
namespace EonaCat.Connections.Processors
|
namespace EonaCat.Connections.Processors
|
||||||
{
|
{
|
||||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
/// <summary>
|
||||||
// See the LICENSE file or go to https://EonaCat.com/license for full license details.
|
/// Processes incoming data streams into JSON or text messages per client buffer.
|
||||||
|
/// </summary>
|
||||||
public class JsonDataProcessor<TMessage> : IDisposable
|
public class JsonDataProcessor<TMessage> : IDisposable
|
||||||
{
|
{
|
||||||
public int MaxAllowedBufferSize = 20 * 1024 * 1024;
|
private const int DefaultMaxBufferSize = 20 * 1024 * 1024; // 20 MB
|
||||||
public int MaxMessagesPerBatch = 200;
|
private const int DefaultMaxMessagesPerBatch = 200;
|
||||||
private readonly ConcurrentDictionary<string, BufferEntry> _buffers = new();
|
private static readonly TimeSpan DefaultClientBufferTimeout = TimeSpan.FromMinutes(5);
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<string, BufferEntry> _buffers = new ConcurrentDictionary<string, BufferEntry>();
|
||||||
private readonly Timer _cleanupTimer;
|
private readonly Timer _cleanupTimer;
|
||||||
private readonly TimeSpan _clientBufferTimeout = TimeSpan.FromMinutes(5);
|
|
||||||
private bool _isDisposed;
|
private bool _isDisposed;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This clientName will be used for the buffer (if not set in the DataReceivedEventArgs).
|
/// Maximum allowed buffer size in bytes (default: 20 MB).
|
||||||
|
/// </summary>
|
||||||
|
public int MaxAllowedBufferSize { get; set; } = DefaultMaxBufferSize;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of messages processed per batch (default: 200).
|
||||||
|
/// </summary>
|
||||||
|
public int MaxMessagesPerBatch { get; set; } = DefaultMaxMessagesPerBatch;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default client name when one is not provided in <see cref="DataReceivedEventArgs"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ClientName { get; set; } = Guid.NewGuid().ToString();
|
public string ClientName { get; set; } = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
public Action<TMessage, string, string> ProcessMessage { get; set; }
|
||||||
|
public Action<string, string> ProcessTextMessage { get; set; }
|
||||||
|
|
||||||
|
public event EventHandler<Exception> OnMessageError;
|
||||||
|
public event EventHandler<Exception> OnError;
|
||||||
|
|
||||||
private class BufferEntry
|
private class BufferEntry
|
||||||
{
|
{
|
||||||
public readonly StringBuilder Buffer = new();
|
public readonly StringBuilder Buffer = new StringBuilder();
|
||||||
public DateTime LastUsed = DateTime.UtcNow;
|
public DateTime LastUsed = DateTime.UtcNow;
|
||||||
public readonly object SyncRoot = new();
|
public readonly object SyncRoot = new object();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Action<TMessage, string, string>? ProcessMessage;
|
|
||||||
public Action<string, string>? ProcessTextMessage;
|
|
||||||
|
|
||||||
public event EventHandler<Exception>? OnMessageError;
|
|
||||||
public event EventHandler<Exception>? OnError;
|
|
||||||
|
|
||||||
public JsonDataProcessor()
|
public JsonDataProcessor()
|
||||||
{
|
{
|
||||||
_cleanupTimer = new Timer(_clientBufferTimeout.TotalMilliseconds / 5);
|
_cleanupTimer = new Timer(DefaultClientBufferTimeout.TotalMilliseconds / 5);
|
||||||
_cleanupTimer.Elapsed += CleanupInactiveClients;
|
|
||||||
_cleanupTimer.AutoReset = true;
|
_cleanupTimer.AutoReset = true;
|
||||||
|
_cleanupTimer.Elapsed += CleanupInactiveClients;
|
||||||
_cleanupTimer.Start();
|
_cleanupTimer.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process incoming raw data.
|
||||||
|
/// </summary>
|
||||||
public void Process(DataReceivedEventArgs e)
|
public void Process(DataReceivedEventArgs e)
|
||||||
{
|
{
|
||||||
if (_isDisposed)
|
EnsureNotDisposed();
|
||||||
{
|
|
||||||
throw new ObjectDisposedException(nameof(JsonDataProcessor<TMessage>));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.IsBinary)
|
if (e.IsBinary)
|
||||||
{
|
{
|
||||||
e.StringData = Encoding.UTF8.GetString(e.Data);
|
e.StringData = Encoding.UTF8.GetString(e.Data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(e.StringData))
|
if (string.IsNullOrWhiteSpace(e.StringData))
|
||||||
{
|
{
|
||||||
OnError?.Invoke(this, new Exception("Received empty data."));
|
OnError?.Invoke(this, new Exception("Received empty data."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string clientName = !string.IsNullOrWhiteSpace(e.Nickname) ? e.Nickname : ClientName;
|
string clientName = string.IsNullOrWhiteSpace(e.Nickname) ? ClientName : e.Nickname;
|
||||||
string incomingText = e.StringData.Trim();
|
string incomingText = e.StringData.Trim();
|
||||||
if (incomingText.Length == 0)
|
if (incomingText.Length == 0)
|
||||||
{
|
{
|
||||||
|
@ -71,12 +82,9 @@ namespace EonaCat.Connections.Processors
|
||||||
}
|
}
|
||||||
|
|
||||||
var bufferEntry = _buffers.GetOrAdd(clientName, _ => new BufferEntry());
|
var bufferEntry = _buffers.GetOrAdd(clientName, _ => new BufferEntry());
|
||||||
List<string>? jsonChunksToProcess = null;
|
|
||||||
string? textMessageToProcess = null;
|
|
||||||
|
|
||||||
lock (bufferEntry.SyncRoot)
|
lock (bufferEntry.SyncRoot)
|
||||||
{
|
{
|
||||||
// Prevent growth before appending
|
|
||||||
if (bufferEntry.Buffer.Length > MaxAllowedBufferSize)
|
if (bufferEntry.Buffer.Length > MaxAllowedBufferSize)
|
||||||
{
|
{
|
||||||
bufferEntry.Buffer.Clear();
|
bufferEntry.Buffer.Clear();
|
||||||
|
@ -87,12 +95,14 @@ namespace EonaCat.Connections.Processors
|
||||||
|
|
||||||
int processedCount = 0;
|
int processedCount = 0;
|
||||||
|
|
||||||
while (processedCount < MaxMessagesPerBatch && ExtractNextJson(bufferEntry.Buffer, out var jsonChunk))
|
while (processedCount < MaxMessagesPerBatch &&
|
||||||
|
ExtractNextJson(bufferEntry.Buffer, out var jsonChunk))
|
||||||
{
|
{
|
||||||
ProcessDataReceived(jsonChunk, clientName);
|
ProcessDataReceived(jsonChunk, clientName);
|
||||||
processedCount++;
|
processedCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle leftover non-JSON text
|
||||||
if (bufferEntry.Buffer.Length > 0 && !ContainsJsonStructure(bufferEntry.Buffer))
|
if (bufferEntry.Buffer.Length > 0 && !ContainsJsonStructure(bufferEntry.Buffer))
|
||||||
{
|
{
|
||||||
var leftover = bufferEntry.Buffer.ToString();
|
var leftover = bufferEntry.Buffer.ToString();
|
||||||
|
@ -100,50 +110,30 @@ namespace EonaCat.Connections.Processors
|
||||||
ProcessTextMessage?.Invoke(leftover, clientName);
|
ProcessTextMessage?.Invoke(leftover, clientName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (textMessageToProcess != null)
|
|
||||||
{
|
|
||||||
ProcessTextMessage?.Invoke(textMessageToProcess, clientName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jsonChunksToProcess != null)
|
|
||||||
{
|
|
||||||
foreach (var jsonChunk in jsonChunksToProcess)
|
|
||||||
{
|
|
||||||
ProcessDataReceived(jsonChunk, clientName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessDataReceived(string? data, string clientName)
|
private void ProcessDataReceived(string data, string clientName)
|
||||||
{
|
{
|
||||||
if (_isDisposed)
|
EnsureNotDisposed();
|
||||||
{
|
|
||||||
throw new ObjectDisposedException(nameof(JsonDataProcessor<TMessage>));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(clientName))
|
|
||||||
{
|
|
||||||
clientName = ClientName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(data) || data.Length == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(data))
|
if (string.IsNullOrWhiteSpace(data))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(clientName))
|
||||||
|
{
|
||||||
|
clientName = ClientName;
|
||||||
|
}
|
||||||
|
|
||||||
bool looksLikeJson = data.Length > 1 &&
|
bool looksLikeJson = data.Length > 1 &&
|
||||||
((data[0] == '{' && data[data.Length - 1] == '}') || (data[0] == '[' && data[data.Length - 1] == ']'));
|
((data[0] == '{' && data[data.Length - 1] == '}') ||
|
||||||
|
(data[0] == '[' && data[data.Length - 1] == ']') ||
|
||||||
|
data[0] == '"' || // string
|
||||||
|
char.IsDigit(data[0]) || data[0] == '-' || // numbers
|
||||||
|
data.StartsWith("true") ||
|
||||||
|
data.StartsWith("false") ||
|
||||||
|
data.StartsWith("null"));
|
||||||
|
|
||||||
if (!looksLikeJson)
|
if (!looksLikeJson)
|
||||||
{
|
{
|
||||||
|
@ -153,34 +143,19 @@ namespace EonaCat.Connections.Processors
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (data.Contains("Exception") || data.Contains("Error"))
|
// Try to detect JSON-encoded exceptions
|
||||||
|
if (data.IndexOf("Exception", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||||
|
data.IndexOf("Error", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
{
|
{
|
||||||
try
|
TryHandleJsonException(data);
|
||||||
{
|
|
||||||
var jsonObject = JObject.Parse(data);
|
|
||||||
var exceptionToken = jsonObject.SelectToken("Exception");
|
|
||||||
if (exceptionToken is { Type: not JTokenType.Null })
|
|
||||||
{
|
|
||||||
var exception = JsonHelper.ExtractException(data);
|
|
||||||
if (exception != null)
|
|
||||||
{
|
|
||||||
var currentException = new Exception(exception.Message);
|
|
||||||
OnMessageError?.Invoke(this, currentException);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var messages = JsonHelper.ToObjects<TMessage>(data);
|
var messages = JsonHelper.ToObjects<TMessage>(data);
|
||||||
if (messages != null)
|
if (messages != null && ProcessMessage != null)
|
||||||
{
|
{
|
||||||
foreach (var message in messages)
|
foreach (var message in messages)
|
||||||
{
|
{
|
||||||
ProcessMessage?.Invoke(message, clientName, data);
|
ProcessMessage(message, clientName, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -190,8 +165,28 @@ namespace EonaCat.Connections.Processors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void TryHandleJsonException(string data)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var jsonObject = JObject.Parse(data);
|
||||||
|
var exceptionToken = jsonObject.SelectToken("Exception");
|
||||||
|
if (exceptionToken != null && exceptionToken.Type != JTokenType.Null)
|
||||||
|
{
|
||||||
|
var exception = JsonHelper.ExtractException(data);
|
||||||
|
if (exception != null && OnMessageError != null)
|
||||||
|
{
|
||||||
|
OnMessageError(this, new Exception(exception.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore malformed exception JSON
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static bool ExtractNextJson(StringBuilder buffer, out string? json)
|
private static bool ExtractNextJson(StringBuilder buffer, out string json)
|
||||||
{
|
{
|
||||||
json = null;
|
json = null;
|
||||||
if (buffer.Length == 0)
|
if (buffer.Length == 0)
|
||||||
|
@ -200,13 +195,12 @@ namespace EonaCat.Connections.Processors
|
||||||
}
|
}
|
||||||
|
|
||||||
int depth = 0;
|
int depth = 0;
|
||||||
bool inString = false;
|
bool inString = false, escape = false;
|
||||||
bool escape = false;
|
|
||||||
int startIndex = -1;
|
int startIndex = -1;
|
||||||
|
|
||||||
for (int i = 0; i < buffer.Length; i++)
|
for (int i = 0; i < buffer.Length; i++)
|
||||||
{
|
{
|
||||||
char currentCharacter = buffer[i];
|
char c = buffer[i];
|
||||||
|
|
||||||
if (inString)
|
if (inString)
|
||||||
{
|
{
|
||||||
|
@ -214,71 +208,99 @@ namespace EonaCat.Connections.Processors
|
||||||
{
|
{
|
||||||
escape = false;
|
escape = false;
|
||||||
}
|
}
|
||||||
else if (currentCharacter == '\\')
|
else if (c == '\\')
|
||||||
{
|
{
|
||||||
escape = true;
|
escape = true;
|
||||||
}
|
}
|
||||||
else if (currentCharacter == '"')
|
else if (c == '"')
|
||||||
{
|
{
|
||||||
inString = false;
|
inString = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (currentCharacter == '"')
|
switch (c)
|
||||||
{
|
{
|
||||||
inString = true;
|
case '"':
|
||||||
if (depth == 0 && startIndex == -1)
|
inString = true;
|
||||||
{
|
if (depth == 0 && startIndex == -1)
|
||||||
startIndex = i; // string-only JSON
|
{
|
||||||
}
|
startIndex = i; // string-only JSON
|
||||||
}
|
}
|
||||||
else if (currentCharacter == '{' || currentCharacter == '[')
|
|
||||||
{
|
|
||||||
if (depth == 0)
|
|
||||||
{
|
|
||||||
startIndex = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
depth++;
|
break;
|
||||||
}
|
|
||||||
else if (currentCharacter == '}' || currentCharacter == ']')
|
|
||||||
{
|
|
||||||
depth--;
|
|
||||||
if (depth == 0 && startIndex != -1)
|
|
||||||
{
|
|
||||||
json = buffer.ToString(startIndex, i - startIndex + 1);
|
|
||||||
buffer.Remove(0, i + 1);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (depth == 0 && startIndex == -1 &&
|
|
||||||
(char.IsDigit(currentCharacter) || currentCharacter == '-' || currentCharacter == 't' || currentCharacter == 'f' || currentCharacter == 'n'))
|
|
||||||
{
|
|
||||||
startIndex = i;
|
|
||||||
|
|
||||||
// Find token end
|
case '{':
|
||||||
int tokenEnd = FindPrimitiveEnd(buffer, i);
|
case '[':
|
||||||
json = buffer.ToString(startIndex, tokenEnd - startIndex);
|
if (depth == 0)
|
||||||
buffer.Remove(0, tokenEnd);
|
{
|
||||||
return true;
|
startIndex = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
depth++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '}':
|
||||||
|
case ']':
|
||||||
|
depth--;
|
||||||
|
if (depth == 0 && startIndex != -1)
|
||||||
|
{
|
||||||
|
int length = i - startIndex + 1;
|
||||||
|
json = buffer.ToString(startIndex, length);
|
||||||
|
buffer.Remove(0, i + 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (depth == 0 && startIndex == -1 &&
|
||||||
|
(char.IsDigit(c) || c == '-' || c == 't' || c == 'f' || c == 'n'))
|
||||||
|
{
|
||||||
|
startIndex = i;
|
||||||
|
int tokenEnd = FindPrimitiveEnd(buffer, i);
|
||||||
|
json = buffer.ToString(startIndex, tokenEnd - startIndex);
|
||||||
|
buffer.Remove(0, tokenEnd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int FindPrimitiveEnd(StringBuilder buffer, int startIndex)
|
private static int FindPrimitiveEnd(StringBuilder buffer, int startIndex)
|
||||||
{
|
{
|
||||||
for (int i = startIndex; i < buffer.Length; i++)
|
// Keywords: true/false/null
|
||||||
|
if (buffer.Length >= startIndex + 4 && buffer.ToString(startIndex, 4) == "true")
|
||||||
|
{
|
||||||
|
return startIndex + 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffer.Length >= startIndex + 5 && buffer.ToString(startIndex, 5) == "false")
|
||||||
|
{
|
||||||
|
return startIndex + 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffer.Length >= startIndex + 4 && buffer.ToString(startIndex, 4) == "null")
|
||||||
|
{
|
||||||
|
return startIndex + 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Numbers: scan until non-number/decimal/exponent
|
||||||
|
int i = startIndex;
|
||||||
|
while (i < buffer.Length)
|
||||||
{
|
{
|
||||||
char c = buffer[i];
|
char c = buffer[i];
|
||||||
if (char.IsWhiteSpace(c) || c == ',' || c == ']' || c == '}')
|
if (!(char.IsDigit(c) || c == '-' || c == '+' || c == '.' || c == 'e' || c == 'E'))
|
||||||
{
|
{
|
||||||
return i;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
return buffer.Length;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool ContainsJsonStructure(StringBuilder buffer)
|
private static bool ContainsJsonStructure(StringBuilder buffer)
|
||||||
|
@ -286,9 +308,7 @@ namespace EonaCat.Connections.Processors
|
||||||
for (int i = 0; i < buffer.Length; i++)
|
for (int i = 0; i < buffer.Length; i++)
|
||||||
{
|
{
|
||||||
char c = buffer[i];
|
char c = buffer[i];
|
||||||
if (c == '{' || c == '[' || c == '"' ||
|
if (c == '{' || c == '[' || c == '"' || c == 't' || c == 'f' || c == 'n' || c == '-' || char.IsDigit(c))
|
||||||
c == 't' || c == 'f' || c == 'n' ||
|
|
||||||
c == '-' || char.IsDigit(c))
|
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -296,18 +316,22 @@ namespace EonaCat.Connections.Processors
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CleanupInactiveClients(object? sender, ElapsedEventArgs e)
|
private void CleanupInactiveClients(object sender, ElapsedEventArgs e)
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
foreach (var kvp in _buffers)
|
foreach (var kvp in _buffers)
|
||||||
{
|
{
|
||||||
var bufferEntry = kvp.Value;
|
var bufferEntry = kvp.Value;
|
||||||
if (now - bufferEntry.LastUsed > _clientBufferTimeout && _buffers.TryRemove(kvp.Key, out var removed))
|
if (now - bufferEntry.LastUsed > DefaultClientBufferTimeout)
|
||||||
{
|
{
|
||||||
lock (removed.SyncRoot)
|
BufferEntry removed;
|
||||||
|
if (_buffers.TryRemove(kvp.Key, out removed))
|
||||||
{
|
{
|
||||||
removed.Buffer.Clear();
|
lock (removed.SyncRoot)
|
||||||
|
{
|
||||||
|
removed.Buffer.Clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -320,7 +344,8 @@ namespace EonaCat.Connections.Processors
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_buffers.TryRemove(clientName, out var removed))
|
BufferEntry removed;
|
||||||
|
if (_buffers.TryRemove(clientName, out removed))
|
||||||
{
|
{
|
||||||
lock (removed.SyncRoot)
|
lock (removed.SyncRoot)
|
||||||
{
|
{
|
||||||
|
@ -329,6 +354,14 @@ namespace EonaCat.Connections.Processors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void EnsureNotDisposed()
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
{
|
||||||
|
throw new ObjectDisposedException(nameof(JsonDataProcessor<TMessage>));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_isDisposed)
|
if (_isDisposed)
|
||||||
|
@ -336,25 +369,30 @@ namespace EonaCat.Connections.Processors
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_isDisposed = true;
|
try
|
||||||
|
|
||||||
_cleanupTimer.Stop();
|
|
||||||
_cleanupTimer.Elapsed -= CleanupInactiveClients;
|
|
||||||
_cleanupTimer.Dispose();
|
|
||||||
|
|
||||||
foreach (var bufferEntry in _buffers.Values)
|
|
||||||
{
|
{
|
||||||
lock (bufferEntry.SyncRoot)
|
_cleanupTimer.Stop();
|
||||||
{
|
_cleanupTimer.Elapsed -= CleanupInactiveClients;
|
||||||
bufferEntry.Buffer.Clear();
|
_cleanupTimer.Dispose();
|
||||||
}
|
|
||||||
}
|
|
||||||
_buffers.Clear();
|
|
||||||
|
|
||||||
ProcessMessage = null;
|
foreach (var bufferEntry in _buffers.Values)
|
||||||
ProcessTextMessage = null;
|
{
|
||||||
OnMessageError = null;
|
lock (bufferEntry.SyncRoot)
|
||||||
OnError = null;
|
{
|
||||||
|
bufferEntry.Buffer.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_buffers.Clear();
|
||||||
|
|
||||||
|
ProcessMessage = null;
|
||||||
|
ProcessTextMessage = null;
|
||||||
|
OnMessageError = null;
|
||||||
|
OnError = null;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isDisposed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue