Updated
This commit is contained in:
parent
d86433db2a
commit
1b11821ebc
|
@ -22,21 +22,6 @@ namespace EonaCat.Connections.Client.Example
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var jsonUrl = "https://microsoftedge.github.io/Demos/json-dummy-data/5MB-min.json";
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var httpClient = new HttpClient();
|
|
||||||
var jsonContent = await httpClient.GetStringAsync(jsonUrl);
|
|
||||||
var jsonSize = Encoding.UTF8.GetByteCount(jsonContent);
|
|
||||||
Console.WriteLine($"Using large JSON file (size: {jsonSize / 1024 / 1024} MB)");
|
|
||||||
message = jsonContent;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Failed to download large JSON file: {ex.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(message))
|
if (!string.IsNullOrEmpty(message))
|
||||||
{
|
{
|
||||||
await _client.SendAsync(message).ConfigureAwait(false);
|
await _client.SendAsync(message).ConfigureAwait(false);
|
||||||
|
@ -53,8 +38,8 @@ namespace EonaCat.Connections.Client.Example
|
||||||
Port = 1111,
|
Port = 1111,
|
||||||
UseSsl = false,
|
UseSsl = false,
|
||||||
UseAesEncryption = true,
|
UseAesEncryption = true,
|
||||||
AesPassword = "p@ss",
|
AesPassword = "EonaCat.Connections.Password",
|
||||||
//ServerCertificate = new System.Security.Cryptography.X509Certificates.X509Certificate2("client.pfx", "p@ss"),
|
Certificate = new System.Security.Cryptography.X509Certificates.X509Certificate2("client.pfx", "p@ss"),
|
||||||
};
|
};
|
||||||
|
|
||||||
_client = new NetworkClient(config);
|
_client = new NetworkClient(config);
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
using EonaCat.Connections;
|
using EonaCat.Connections.Models;
|
||||||
using EonaCat.Connections.Models;
|
|
||||||
|
|
||||||
namespace EonaCat.Connections.Server.Example
|
namespace EonaCat.Connections.Server.Example
|
||||||
{
|
{
|
||||||
|
@ -39,8 +38,8 @@ namespace EonaCat.Connections.Server.Example
|
||||||
UseSsl = false,
|
UseSsl = false,
|
||||||
UseAesEncryption = true,
|
UseAesEncryption = true,
|
||||||
MaxConnections = 100000,
|
MaxConnections = 100000,
|
||||||
AesPassword = "p@ss",
|
AesPassword = "EonaCat.Connections.Password",
|
||||||
//ServerCertificate = new System.Security.Cryptography.X509Certificates.X509Certificate2("server.pfx", "p@ss"),
|
Certificate = new System.Security.Cryptography.X509Certificates.X509Certificate2("server.pfx", "p@ss")
|
||||||
};
|
};
|
||||||
|
|
||||||
_server = new NetworkServer(config);
|
_server = new NetworkServer(config);
|
||||||
|
|
|
@ -9,19 +9,14 @@
|
||||||
<Title>EonaCat.Connections</Title>
|
<Title>EonaCat.Connections</Title>
|
||||||
<Company>EonaCat (Jeroen Saey)</Company>
|
<Company>EonaCat (Jeroen Saey)</Company>
|
||||||
<Copyright>EonaCat (Jeroen Saey)</Copyright>
|
<Copyright>EonaCat (Jeroen Saey)</Copyright>
|
||||||
<PackageIcon>EonaCat.png</PackageIcon>
|
|
||||||
<PackageReadmeFile>readme.md</PackageReadmeFile>
|
<PackageReadmeFile>readme.md</PackageReadmeFile>
|
||||||
<PackageId>EonaCat.Connections</PackageId>
|
<PackageId>EonaCat.Connections</PackageId>
|
||||||
<Version>1.0.2</Version>
|
<Version>1.0.4</Version>
|
||||||
<Authors>EonaCat (Jeroen Saey)</Authors>
|
<Authors>EonaCat (Jeroen Saey)</Authors>
|
||||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="..\EonaCat.png">
|
|
||||||
<Pack>True</Pack>
|
|
||||||
<PackagePath>\</PackagePath>
|
|
||||||
</None>
|
|
||||||
<None Include="..\LICENSE">
|
<None Include="..\LICENSE">
|
||||||
<Pack>True</Pack>
|
<Pack>True</Pack>
|
||||||
<PackagePath>\</PackagePath>
|
<PackagePath>\</PackagePath>
|
||||||
|
@ -32,4 +27,8 @@
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="EonaCat.Json" Version="1.1.9" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
namespace EonaCat.Connections
|
using System.Net;
|
||||||
|
|
||||||
|
namespace EonaCat.Connections
|
||||||
{
|
{
|
||||||
public class DataReceivedEventArgs : EventArgs
|
public class DataReceivedEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public string ClientId { get; set; }
|
public string ClientId { get; internal set; }
|
||||||
public byte[] Data { get; set; }
|
public byte[] Data { get; internal set; }
|
||||||
public string StringData { get; set; }
|
public string StringData { get; internal set; }
|
||||||
public bool IsBinary { get; set; }
|
public bool IsBinary { get; internal set; }
|
||||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
public DateTime Timestamp { get; internal set; } = DateTime.UtcNow;
|
||||||
|
public IPEndPoint RemoteEndPoint { get; internal set; }
|
||||||
|
public string Nickname { get; internal set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -77,7 +77,10 @@ namespace EonaCat.Connections.Helpers
|
||||||
for (int block = 1; block <= keyBlocks; block++)
|
for (int block = 1; block <= keyBlocks; block++)
|
||||||
{
|
{
|
||||||
byte[] intBlock = BitConverter.GetBytes(block);
|
byte[] intBlock = BitConverter.GetBytes(block);
|
||||||
if (BitConverter.IsLittleEndian) Array.Reverse(intBlock);
|
if (BitConverter.IsLittleEndian)
|
||||||
|
{
|
||||||
|
Array.Reverse(intBlock);
|
||||||
|
}
|
||||||
|
|
||||||
hmac.Initialize();
|
hmac.Initialize();
|
||||||
hmac.TransformBlock(salt, 0, salt.Length, salt, 0);
|
hmac.TransformBlock(salt, 0, salt.Length, salt, 0);
|
||||||
|
@ -89,7 +92,9 @@ namespace EonaCat.Connections.Helpers
|
||||||
{
|
{
|
||||||
temp = hmac.ComputeHash(temp);
|
temp = hmac.ComputeHash(temp);
|
||||||
for (int j = 0; j < hashLength; j++)
|
for (int j = 0; j < hashLength; j++)
|
||||||
|
{
|
||||||
buffer[j] ^= temp[j];
|
buffer[j] ^= temp[j];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int offset = (block - 1) * hashLength;
|
int offset = (block - 1) * hashLength;
|
||||||
|
@ -118,7 +123,11 @@ namespace EonaCat.Connections.Helpers
|
||||||
while (totalRead < count)
|
while (totalRead < count)
|
||||||
{
|
{
|
||||||
int read = await stream.ReadAsync(buffer, offset + totalRead, count - totalRead);
|
int read = await stream.ReadAsync(buffer, offset + totalRead, count - totalRead);
|
||||||
if (read == 0) throw new EndOfStreamException("Stream ended prematurely");
|
if (read == 0)
|
||||||
|
{
|
||||||
|
throw new EndOfStreamException("Stream ended prematurely");
|
||||||
|
}
|
||||||
|
|
||||||
totalRead += read;
|
totalRead += read;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
namespace EonaCat.Connections.Helpers
|
||||||
|
{
|
||||||
|
internal class StringHelper
|
||||||
|
{
|
||||||
|
public static string GetTextBetweenTags(string message, string startTag, string endTag)
|
||||||
|
{
|
||||||
|
int startIndex = message.IndexOf(startTag);
|
||||||
|
if (startIndex == -1)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
int endIndex = message.IndexOf(endTag, startIndex + startTag.Length);
|
||||||
|
if (endIndex == -1)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
int length = endIndex - startIndex - startTag.Length;
|
||||||
|
if (length < 0)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return message.Substring(startIndex + startTag.Length, length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Diagnostics;
|
||||||
|
using System.Net.Security;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
namespace EonaCat.Connections.Models
|
namespace EonaCat.Connections.Models
|
||||||
{
|
{
|
||||||
// Configuration class
|
|
||||||
public class Configuration
|
public class Configuration
|
||||||
{
|
{
|
||||||
public bool EnableAutoReconnect { get; set; } = true;
|
public bool EnableAutoReconnect { get; set; } = true;
|
||||||
|
@ -13,7 +14,7 @@ namespace EonaCat.Connections.Models
|
||||||
public int Port { get; set; } = 8080;
|
public int Port { get; set; } = 8080;
|
||||||
public string Host { get; set; } = "127.0.0.1";
|
public string Host { get; set; } = "127.0.0.1";
|
||||||
public bool UseSsl { get; set; } = false;
|
public bool UseSsl { get; set; } = false;
|
||||||
public X509Certificate2 ServerCertificate { get; set; }
|
public X509Certificate2 Certificate { get; set; }
|
||||||
public bool UseAesEncryption { get; set; } = false;
|
public bool UseAesEncryption { get; set; } = false;
|
||||||
public int BufferSize { get; set; } = 8192;
|
public int BufferSize { get; set; } = 8192;
|
||||||
public int MaxConnections { get; set; } = 100000;
|
public int MaxConnections { get; set; } = 100000;
|
||||||
|
@ -24,5 +25,55 @@ namespace EonaCat.Connections.Models
|
||||||
// For testing purposes, allow self-signed certificates
|
// For testing purposes, allow self-signed certificates
|
||||||
public bool IsSelfSignedEnabled { get; set; } = true;
|
public bool IsSelfSignedEnabled { get; set; } = true;
|
||||||
public string AesPassword { get; set; }
|
public string AesPassword { get; set; }
|
||||||
|
public bool CheckCertificateRevocation { get; set; }
|
||||||
|
public bool MutuallyAuthenticate { get; set; } = true;
|
||||||
|
|
||||||
|
internal RemoteCertificateValidationCallback GetRemoteCertificateValidationCallback()
|
||||||
|
{
|
||||||
|
return CertificateValidation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CertificateValidation(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
|
||||||
|
{
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (IsSelfSignedEnabled)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sslPolicyErrors == SslPolicyErrors.None)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateChainErrors) && chain != null)
|
||||||
|
{
|
||||||
|
foreach (var status in chain.ChainStatus)
|
||||||
|
{
|
||||||
|
if (status.Status == X509ChainStatusFlags.RevocationStatusUnknown ||
|
||||||
|
status.Status == X509ChainStatusFlags.OfflineRevocation)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.Status == X509ChainStatusFlags.Revoked)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
sw.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -11,8 +11,34 @@ namespace EonaCat.Connections.Models
|
||||||
public UdpClient UdpClient { get; set; }
|
public UdpClient UdpClient { get; set; }
|
||||||
public IPEndPoint RemoteEndPoint { get; set; }
|
public IPEndPoint RemoteEndPoint { get; set; }
|
||||||
public Stream Stream { get; set; }
|
public Stream Stream { get; set; }
|
||||||
public string Nickname { get; set; }
|
|
||||||
|
private string _nickName;
|
||||||
|
public string Nickname
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_nickName))
|
||||||
|
{
|
||||||
|
_nickName = Id;
|
||||||
|
}
|
||||||
|
return _nickName;
|
||||||
|
}
|
||||||
|
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
_nickName = Id;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_nickName = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public DateTime ConnectedAt { get; set; }
|
public DateTime ConnectedAt { get; set; }
|
||||||
|
public DateTime LastActive { get; set; }
|
||||||
public bool IsSecure { get; set; }
|
public bool IsSecure { get; set; }
|
||||||
public bool IsEncrypted { get; set; }
|
public bool IsEncrypted { get; set; }
|
||||||
public Aes AesEncryption { get; set; }
|
public Aes AesEncryption { get; set; }
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
using EonaCat.Connections.EventArguments;
|
using EonaCat.Connections.EventArguments;
|
||||||
using EonaCat.Connections.Helpers;
|
using EonaCat.Connections.Helpers;
|
||||||
using EonaCat.Connections.Models;
|
using EonaCat.Connections.Models;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Security;
|
using System.Net.Security;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Security.Authentication;
|
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
@ -63,8 +61,15 @@ namespace EonaCat.Connections
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sslStream = new SslStream(stream, false, userCertificateValidationCallback:RemoteCertificateValidationCallback);
|
var sslStream = new SslStream(stream, false, userCertificateValidationCallback:_config.GetRemoteCertificateValidationCallback());
|
||||||
await sslStream.AuthenticateAsClientAsync(_config.Host);
|
if (_config.Certificate != null)
|
||||||
|
{
|
||||||
|
sslStream.AuthenticateAsClient(_config.Host, new X509CertificateCollection { _config.Certificate }, _config.CheckCertificateRevocation);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sslStream.AuthenticateAsClient(_config.Host);
|
||||||
|
}
|
||||||
stream = sslStream;
|
stream = sslStream;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -102,27 +107,8 @@ namespace EonaCat.Connections
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool RemoteCertificateValidationCallback(object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors)
|
public string IpAddress => _config != null ? _config.Host : string.Empty;
|
||||||
{
|
public int Port => _config != null ? _config.Port : 0;
|
||||||
if (_config.IsSelfSignedEnabled)
|
|
||||||
{
|
|
||||||
return true; // Accept self-signed certificates
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sslPolicyErrors == SslPolicyErrors.None)
|
|
||||||
{
|
|
||||||
return true; // Certificate is valid
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log or handle the SSL error as needed
|
|
||||||
OnSslError?.Invoke(this, new ErrorEventArgs
|
|
||||||
{
|
|
||||||
Exception = new AuthenticationException("SSL certificate validation failed"),
|
|
||||||
Message = $"SSL Policy Errors: {sslPolicyErrors}"
|
|
||||||
});
|
|
||||||
return false; // Reject the certificate
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ConnectUdp()
|
private async Task ConnectUdp()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
@ -156,23 +142,31 @@ namespace EonaCat.Connections
|
||||||
// Read 4-byte length prefix
|
// Read 4-byte length prefix
|
||||||
var lengthBuffer = new byte[4];
|
var lengthBuffer = new byte[4];
|
||||||
int read = await ReadExactAsync(_stream, lengthBuffer, 4, _cancellation.Token);
|
int read = await ReadExactAsync(_stream, lengthBuffer, 4, _cancellation.Token);
|
||||||
if (read == 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
|
// 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, _cancellation.Token);
|
||||||
|
|
||||||
// **Decrypt once here**
|
|
||||||
data = await DecryptDataAsync(encrypted, _aesEncryption);
|
data = await 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, _cancellation.Token);
|
||||||
if (bytesRead == 0) break;
|
if (bytesRead == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (bytesRead < data.Length)
|
if (bytesRead < data.Length)
|
||||||
{
|
{
|
||||||
|
@ -204,7 +198,11 @@ 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) return 0;
|
if (read == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
offset += read;
|
offset += read;
|
||||||
}
|
}
|
||||||
return offset;
|
return offset;
|
||||||
|
@ -246,7 +244,9 @@ namespace EonaCat.Connections
|
||||||
{
|
{
|
||||||
stringData = Encoding.UTF8.GetString(data);
|
stringData = Encoding.UTF8.GetString(data);
|
||||||
if (Encoding.UTF8.GetBytes(stringData).Length == data.Length)
|
if (Encoding.UTF8.GetBytes(stringData).Length == data.Length)
|
||||||
|
{
|
||||||
isBinary = false;
|
isBinary = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
@ -264,16 +264,23 @@ namespace EonaCat.Connections
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
if (_config.UseAesEncryption)
|
if (_config.UseAesEncryption)
|
||||||
|
{
|
||||||
OnEncryptionError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error processing data" });
|
OnEncryptionError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error processing data" });
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error processing data" });
|
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error processing data" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task SendAsync(byte[] data)
|
public async Task SendAsync(byte[] data)
|
||||||
{
|
{
|
||||||
if (!_isConnected) return;
|
if (!_isConnected)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -284,7 +291,10 @@ namespace EonaCat.Connections
|
||||||
|
|
||||||
// Prepend 4-byte length for framing
|
// Prepend 4-byte length for framing
|
||||||
var lengthPrefix = BitConverter.GetBytes(data.Length);
|
var lengthPrefix = BitConverter.GetBytes(data.Length);
|
||||||
if (BitConverter.IsLittleEndian) Array.Reverse(lengthPrefix);
|
if (BitConverter.IsLittleEndian)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
@ -306,9 +316,13 @@ namespace EonaCat.Connections
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
if (_config.UseAesEncryption)
|
if (_config.UseAesEncryption)
|
||||||
|
{
|
||||||
OnEncryptionError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error encrypting/sending data" });
|
OnEncryptionError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error encrypting/sending data" });
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error sending data" });
|
OnGeneralError?.Invoke(this, new ErrorEventArgs { Exception = ex, Message = "Error sending data" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -372,7 +386,7 @@ namespace EonaCat.Connections
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// Ignore exceptions, we'll retry
|
// Do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.Delay(_config.ReconnectDelayMs);
|
await Task.Delay(_config.ReconnectDelayMs);
|
||||||
|
|
|
@ -47,6 +47,9 @@ namespace EonaCat.Connections
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string IpAddress => _config != null ? _config.Host : string.Empty;
|
||||||
|
public int Port => _config != null ? _config.Port : 0;
|
||||||
|
|
||||||
public async Task StartAsync()
|
public async Task StartAsync()
|
||||||
{
|
{
|
||||||
_serverCancellation = new CancellationTokenSource();
|
_serverCancellation = new CancellationTokenSource();
|
||||||
|
@ -86,6 +89,11 @@ 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);
|
||||||
|
@ -118,6 +126,7 @@ namespace EonaCat.Connections
|
||||||
TcpClient = tcpClient,
|
TcpClient = tcpClient,
|
||||||
RemoteEndPoint = (IPEndPoint)tcpClient.Client.RemoteEndPoint,
|
RemoteEndPoint = (IPEndPoint)tcpClient.Client.RemoteEndPoint,
|
||||||
ConnectedAt = DateTime.UtcNow,
|
ConnectedAt = DateTime.UtcNow,
|
||||||
|
LastActive = DateTime.UtcNow,
|
||||||
CancellationToken = new CancellationTokenSource()
|
CancellationToken = new CancellationTokenSource()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -137,8 +146,8 @@ namespace EonaCat.Connections
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sslStream = new SslStream(stream, false, userCertificateValidationCallback:RemoteCertificateValidationCallback);
|
var sslStream = new SslStream(stream, false, userCertificateValidationCallback: _config.GetRemoteCertificateValidationCallback());
|
||||||
await sslStream.AuthenticateAsServerAsync(_config.ServerCertificate, false, SslProtocols.Tls12, false);
|
await sslStream.AuthenticateAsServerAsync(_config.Certificate, _config.MutuallyAuthenticate, SslProtocols.Tls12 | SslProtocols.Tls13, _config.CheckCertificateRevocation);
|
||||||
stream = sslStream;
|
stream = sslStream;
|
||||||
client.IsSecure = true;
|
client.IsSecure = true;
|
||||||
}
|
}
|
||||||
|
@ -159,7 +168,7 @@ namespace EonaCat.Connections
|
||||||
client.AesEncryption.GenerateIV();
|
client.AesEncryption.GenerateIV();
|
||||||
client.IsEncrypted = true;
|
client.IsEncrypted = true;
|
||||||
|
|
||||||
// Securely send raw AES key + IV + salt
|
// Securely send raw AES key + IV + salt + password
|
||||||
await AesKeyExchange.SendAesKeyAsync(stream, client.AesEncryption, _config.AesPassword);
|
await AesKeyExchange.SendAesKeyAsync(stream, client.AesEncryption, _config.AesPassword);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -197,27 +206,6 @@ namespace EonaCat.Connections
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool RemoteCertificateValidationCallback(object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors)
|
|
||||||
{
|
|
||||||
if (_config.IsSelfSignedEnabled)
|
|
||||||
{
|
|
||||||
return true; // Accept self-signed certificates
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sslPolicyErrors == SslPolicyErrors.None)
|
|
||||||
{
|
|
||||||
return true; // Certificate is valid
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log or handle the SSL error as needed
|
|
||||||
OnSslError?.Invoke(this, new ErrorEventArgs
|
|
||||||
{
|
|
||||||
Exception = new AuthenticationException("SSL certificate validation failed"),
|
|
||||||
Message = $"SSL Policy Errors: {sslPolicyErrors}"
|
|
||||||
});
|
|
||||||
return false; // Reject the certificate
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleUdpDataAsync(UdpReceiveResult result)
|
private async Task HandleUdpDataAsync(UdpReceiveResult result)
|
||||||
{
|
{
|
||||||
var clientKey = result.RemoteEndPoint.ToString();
|
var clientKey = result.RemoteEndPoint.ToString();
|
||||||
|
@ -257,10 +245,15 @@ namespace EonaCat.Connections
|
||||||
{
|
{
|
||||||
// Read 4-byte length first
|
// Read 4-byte length first
|
||||||
int read = await ReadExactAsync(client.Stream, lengthBuffer, 4, client.CancellationToken.Token);
|
int read = await ReadExactAsync(client.Stream, lengthBuffer, 4, client.CancellationToken.Token);
|
||||||
if (read == 0) break;
|
if (read == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (BitConverter.IsLittleEndian)
|
if (BitConverter.IsLittleEndian)
|
||||||
|
{
|
||||||
Array.Reverse(lengthBuffer);
|
Array.Reverse(lengthBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
int length = BitConverter.ToInt32(lengthBuffer, 0);
|
int length = BitConverter.ToInt32(lengthBuffer, 0);
|
||||||
|
|
||||||
|
@ -276,7 +269,10 @@ namespace EonaCat.Connections
|
||||||
// Non-encrypted: just read raw bytes
|
// 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) break;
|
if (bytesRead == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (bytesRead < data.Length)
|
if (bytesRead < data.Length)
|
||||||
{
|
{
|
||||||
|
@ -307,7 +303,11 @@ 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) return 0; // disconnected
|
if (read == 0)
|
||||||
|
{
|
||||||
|
return 0; // disconnected
|
||||||
|
}
|
||||||
|
|
||||||
offset += read;
|
offset += read;
|
||||||
}
|
}
|
||||||
return offset;
|
return offset;
|
||||||
|
@ -333,27 +333,59 @@ namespace EonaCat.Connections
|
||||||
{
|
{
|
||||||
stringData = Encoding.UTF8.GetString(data);
|
stringData = Encoding.UTF8.GetString(data);
|
||||||
if (Encoding.UTF8.GetBytes(stringData).Length == data.Length)
|
if (Encoding.UTF8.GetBytes(stringData).Length == data.Length)
|
||||||
|
{
|
||||||
isBinary = false;
|
isBinary = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
|
|
||||||
// Handle special commands
|
// Handle special commands
|
||||||
if (!isBinary && stringData.StartsWith("NICKNAME:"))
|
if (!isBinary && stringData != null)
|
||||||
{
|
{
|
||||||
var nickname = stringData.Substring(9);
|
if (stringData.StartsWith("NICKNAME:"))
|
||||||
client.Nickname = nickname;
|
|
||||||
OnConnectedWithNickname?.Invoke(this, new NicknameConnectionEventArgs
|
|
||||||
{
|
{
|
||||||
ClientId = client.Id,
|
var nickname = stringData.Substring(9);
|
||||||
RemoteEndPoint = client.RemoteEndPoint,
|
client.Nickname = nickname;
|
||||||
Nickname = nickname
|
OnConnectedWithNickname?.Invoke(this, new NicknameConnectionEventArgs
|
||||||
});
|
{
|
||||||
return;
|
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 NicknameConnectionEventArgs
|
||||||
|
{
|
||||||
|
ClientId = client.Id,
|
||||||
|
RemoteEndPoint = client.RemoteEndPoint,
|
||||||
|
Nickname = nickname
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (stringData.Equals("DISCONNECT", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await DisconnectClientAsync(client.Id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client.LastActive = DateTime.UtcNow;
|
||||||
OnDataReceived?.Invoke(this, new DataReceivedEventArgs
|
OnDataReceived?.Invoke(this, new DataReceivedEventArgs
|
||||||
{
|
{
|
||||||
ClientId = client.Id,
|
ClientId = client.Id,
|
||||||
|
Nickname = client.Nickname,
|
||||||
|
RemoteEndPoint = client.RemoteEndPoint,
|
||||||
Data = data,
|
Data = data,
|
||||||
StringData = stringData,
|
StringData = stringData,
|
||||||
IsBinary = isBinary
|
IsBinary = isBinary
|
||||||
|
@ -362,18 +394,55 @@ namespace EonaCat.Connections
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
if (client.IsEncrypted)
|
if (client.IsEncrypted)
|
||||||
|
{
|
||||||
OnEncryptionError?.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
|
else
|
||||||
|
{
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error processing data" });
|
OnGeneralError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error processing data" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task SendToClientAsync(string clientId, byte[] data)
|
public async Task SendToClientAsync(string clientId, byte[] data)
|
||||||
{
|
{
|
||||||
if (_clients.TryGetValue(clientId, out var client))
|
// Check if clientId is a guid
|
||||||
|
if (Guid.TryParse(clientId, out _))
|
||||||
{
|
{
|
||||||
await SendDataAsync(client, data);
|
if (_clients.TryGetValue(clientId, out var client))
|
||||||
|
{
|
||||||
|
await SendDataAsync(client, data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if clientId is an IP:Port format
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if (kvp.Value.Nickname != null && kvp.Value.Nickname.Equals(clientId, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await SendDataAsync(kvp.Value, data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -409,7 +478,9 @@ namespace EonaCat.Connections
|
||||||
// Prepend length for safe framing
|
// Prepend length for safe 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);
|
||||||
|
@ -438,9 +509,13 @@ namespace EonaCat.Connections
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
if (client.IsEncrypted)
|
if (client.IsEncrypted)
|
||||||
|
{
|
||||||
OnEncryptionError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error encrypting/sending data" });
|
OnEncryptionError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error encrypting/sending data" });
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
OnGeneralError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error sending data" });
|
OnGeneralError?.Invoke(this, new ErrorEventArgs { ClientId = client.Id, Exception = ex, Message = "Error sending data" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,357 @@
|
||||||
|
using EonaCat.Json;
|
||||||
|
using EonaCat.Json.Linq;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text;
|
||||||
|
using System.Timers;
|
||||||
|
using Timer = System.Timers.Timer;
|
||||||
|
|
||||||
|
namespace EonaCat.Connections.Processors
|
||||||
|
{
|
||||||
|
public class JsonDataProcessor<TMessage> : IDisposable
|
||||||
|
{
|
||||||
|
public int MaxAllowedBufferSize = 20 * 1024 * 1024;
|
||||||
|
public int MaxMessagesPerBatch = 200;
|
||||||
|
private readonly ConcurrentDictionary<string, BufferEntry> _buffers = new();
|
||||||
|
private readonly Timer _cleanupTimer;
|
||||||
|
private readonly TimeSpan _clientBufferTimeout = TimeSpan.FromMinutes(5);
|
||||||
|
private bool _isDisposed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This clientName will be used for the buffer (if not set in the DataReceivedEventArgs).
|
||||||
|
/// </summary>
|
||||||
|
public string ClientName { get; set; } = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
private class BufferEntry
|
||||||
|
{
|
||||||
|
public readonly StringBuilder Buffer = new();
|
||||||
|
public DateTime LastUsed = DateTime.UtcNow;
|
||||||
|
public readonly object SyncRoot = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Action<TMessage, string, string>? ProcessMessage;
|
||||||
|
public Action<string, string>? ProcessTextMessage;
|
||||||
|
|
||||||
|
public event EventHandler<Exception>? OnMessageError;
|
||||||
|
public event EventHandler<Exception>? OnError;
|
||||||
|
|
||||||
|
public JsonDataProcessor()
|
||||||
|
{
|
||||||
|
_cleanupTimer = new Timer(_clientBufferTimeout.TotalMilliseconds / 5);
|
||||||
|
_cleanupTimer.Elapsed += CleanupInactiveClients;
|
||||||
|
_cleanupTimer.AutoReset = true;
|
||||||
|
_cleanupTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Process(DataReceivedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
{
|
||||||
|
throw new ObjectDisposedException(nameof(JsonDataProcessor<TMessage>));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.IsBinary)
|
||||||
|
{
|
||||||
|
e.StringData = Encoding.UTF8.GetString(e.Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(e.StringData))
|
||||||
|
{
|
||||||
|
OnError?.Invoke(this, new Exception("Received empty data."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string clientName = !string.IsNullOrWhiteSpace(e.Nickname) ? e.Nickname : ClientName;
|
||||||
|
string incomingText = e.StringData.Trim();
|
||||||
|
if (incomingText.Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bufferEntry = _buffers.GetOrAdd(clientName, _ => new BufferEntry());
|
||||||
|
List<string>? jsonChunksToProcess = null;
|
||||||
|
string? textMessageToProcess = null;
|
||||||
|
|
||||||
|
lock (bufferEntry.SyncRoot)
|
||||||
|
{
|
||||||
|
// Prevent growth before appending
|
||||||
|
if (bufferEntry.Buffer.Length > MaxAllowedBufferSize)
|
||||||
|
{
|
||||||
|
bufferEntry.Buffer.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
bufferEntry.Buffer.Append(incomingText);
|
||||||
|
bufferEntry.LastUsed = DateTime.UtcNow;
|
||||||
|
|
||||||
|
int processedCount = 0;
|
||||||
|
|
||||||
|
while (processedCount < MaxMessagesPerBatch && ExtractNextJson(bufferEntry.Buffer, out var jsonChunk))
|
||||||
|
{
|
||||||
|
ProcessDataReceived(jsonChunk, clientName);
|
||||||
|
processedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bufferEntry.Buffer.Length > 0 && !ContainsJsonStructure(bufferEntry.Buffer))
|
||||||
|
{
|
||||||
|
var leftover = bufferEntry.Buffer.ToString();
|
||||||
|
bufferEntry.Buffer.Clear();
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
{
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool looksLikeJson = data.Length > 1 &&
|
||||||
|
((data[0] == '{' && data[data.Length - 1] == '}') || (data[0] == '[' && data[data.Length - 1] == ']'));
|
||||||
|
|
||||||
|
if (!looksLikeJson)
|
||||||
|
{
|
||||||
|
ProcessTextMessage?.Invoke(data, clientName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (data.Contains("Exception") || data.Contains("Error"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
if (messages != null)
|
||||||
|
{
|
||||||
|
foreach (var message in messages)
|
||||||
|
{
|
||||||
|
ProcessMessage?.Invoke(message, clientName, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
OnError?.Invoke(this, new Exception("Failed to process JSON message.", ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static bool ExtractNextJson(StringBuilder buffer, out string? json)
|
||||||
|
{
|
||||||
|
json = null;
|
||||||
|
if (buffer.Length == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int depth = 0;
|
||||||
|
bool inString = false;
|
||||||
|
bool escape = false;
|
||||||
|
int startIndex = -1;
|
||||||
|
|
||||||
|
for (int i = 0; i < buffer.Length; i++)
|
||||||
|
{
|
||||||
|
char currentCharacter = buffer[i];
|
||||||
|
|
||||||
|
if (inString)
|
||||||
|
{
|
||||||
|
if (escape)
|
||||||
|
{
|
||||||
|
escape = false;
|
||||||
|
}
|
||||||
|
else if (currentCharacter == '\\')
|
||||||
|
{
|
||||||
|
escape = true;
|
||||||
|
}
|
||||||
|
else if (currentCharacter == '"')
|
||||||
|
{
|
||||||
|
inString = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (currentCharacter == '"')
|
||||||
|
{
|
||||||
|
inString = true;
|
||||||
|
if (depth == 0 && startIndex == -1)
|
||||||
|
{
|
||||||
|
startIndex = i; // string-only JSON
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (currentCharacter == '{' || currentCharacter == '[')
|
||||||
|
{
|
||||||
|
if (depth == 0)
|
||||||
|
{
|
||||||
|
startIndex = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
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
|
||||||
|
int tokenEnd = FindPrimitiveEnd(buffer, i);
|
||||||
|
json = buffer.ToString(startIndex, tokenEnd - startIndex);
|
||||||
|
buffer.Remove(0, tokenEnd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int FindPrimitiveEnd(StringBuilder buffer, int startIndex)
|
||||||
|
{
|
||||||
|
for (int i = startIndex; i < buffer.Length; i++)
|
||||||
|
{
|
||||||
|
char c = buffer[i];
|
||||||
|
if (char.IsWhiteSpace(c) || c == ',' || c == ']' || c == '}')
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buffer.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ContainsJsonStructure(StringBuilder buffer)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < buffer.Length; i++)
|
||||||
|
{
|
||||||
|
char c = buffer[i];
|
||||||
|
if (c == '{' || c == '[' || c == '"' ||
|
||||||
|
c == 't' || c == 'f' || c == 'n' ||
|
||||||
|
c == '-' || char.IsDigit(c))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CleanupInactiveClients(object? sender, ElapsedEventArgs e)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
foreach (var kvp in _buffers)
|
||||||
|
{
|
||||||
|
var bufferEntry = kvp.Value;
|
||||||
|
if (now - bufferEntry.LastUsed > _clientBufferTimeout && _buffers.TryRemove(kvp.Key, out var removed))
|
||||||
|
{
|
||||||
|
lock (removed.SyncRoot)
|
||||||
|
{
|
||||||
|
removed.Buffer.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveClient(string clientName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(clientName))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_buffers.TryRemove(clientName, out var removed))
|
||||||
|
{
|
||||||
|
lock (removed.SyncRoot)
|
||||||
|
{
|
||||||
|
removed.Buffer.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isDisposed = true;
|
||||||
|
|
||||||
|
_cleanupTimer.Stop();
|
||||||
|
_cleanupTimer.Elapsed -= CleanupInactiveClients;
|
||||||
|
_cleanupTimer.Dispose();
|
||||||
|
|
||||||
|
foreach (var bufferEntry in _buffers.Values)
|
||||||
|
{
|
||||||
|
lock (bufferEntry.SyncRoot)
|
||||||
|
{
|
||||||
|
bufferEntry.Buffer.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_buffers.Clear();
|
||||||
|
|
||||||
|
ProcessMessage = null;
|
||||||
|
ProcessTextMessage = null;
|
||||||
|
OnMessageError = null;
|
||||||
|
OnError = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue