Added Secure WebSockets
This commit is contained in:
parent
04fd2329f1
commit
d6f0c7b5af
|
@ -15,10 +15,10 @@
|
||||||
<PackageProjectUrl>https://www.nuget.org/packages/EonaCat.Network/</PackageProjectUrl>
|
<PackageProjectUrl>https://www.nuget.org/packages/EonaCat.Network/</PackageProjectUrl>
|
||||||
<PackageTags>EonaCat, Network, .NET Standard, EonaCatHelpers, Jeroen, Saey, Protocol, Quic, UDP, TCP, Web, Server</PackageTags>
|
<PackageTags>EonaCat, Network, .NET Standard, EonaCatHelpers, Jeroen, Saey, Protocol, Quic, UDP, TCP, Web, Server</PackageTags>
|
||||||
<PackageReleaseNotes></PackageReleaseNotes>
|
<PackageReleaseNotes></PackageReleaseNotes>
|
||||||
<Description>EonaCat Networking library with Quic, TCP, UDP and a Webserver</Description>
|
<Description>EonaCat Networking library with Quic, TCP, UDP, WebSockets and a Webserver</Description>
|
||||||
<Version>1.0.9</Version>
|
<Version>1.1.0</Version>
|
||||||
<AssemblyVersion>1.0.0.9</AssemblyVersion>
|
<AssemblyVersion>1.1.0.0</AssemblyVersion>
|
||||||
<FileVersion>1.0.0.9</FileVersion>
|
<FileVersion>1.1.0.0</FileVersion>
|
||||||
<PackageIcon>icon.png</PackageIcon>
|
<PackageIcon>icon.png</PackageIcon>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
@ -52,9 +52,6 @@
|
||||||
<PackageReference Include="EonaCat.Json" Version="1.0.3" />
|
<PackageReference Include="EonaCat.Json" Version="1.0.3" />
|
||||||
<PackageReference Include="EonaCat.LogSystem" Version="1.0.0" />
|
<PackageReference Include="EonaCat.LogSystem" Version="1.0.0" />
|
||||||
<PackageReference Include="EonaCat.Matchers" Version="1.0.0" />
|
<PackageReference Include="EonaCat.Matchers" Version="1.0.0" />
|
||||||
</ItemGroup>
|
<PackageReference Include="System.Net.WebSockets" Version="4.3.0" />
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="System\Sockets\" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -13,5 +13,8 @@ namespace EonaCat.Network
|
||||||
public int Port => HasEndpoint ? ((IPEndPoint)EndPoint).Port : 0;
|
public int Port => HasEndpoint ? ((IPEndPoint)EndPoint).Port : 0;
|
||||||
public Socket Socket { get; set; }
|
public Socket Socket { get; set; }
|
||||||
public bool IsIpv6 { get; set; }
|
public bool IsIpv6 { get; set; }
|
||||||
|
public bool IsWebSocket { get; internal set; }
|
||||||
|
public string ClientId { get; internal set; }
|
||||||
|
public string ClientName { get; internal set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,133 @@
|
||||||
|
using System;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace EonaCat.Network
|
||||||
|
{
|
||||||
|
public class WebSocketSecureClient
|
||||||
|
{
|
||||||
|
private const int BUFFER_SIZE = 4096;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// OnConnect event
|
||||||
|
/// </summary>
|
||||||
|
public event Action<RemoteInfo> OnConnect;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// OnReceive event
|
||||||
|
/// </summary>
|
||||||
|
public event Action<RemoteInfo> OnReceive;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// OnDisconnect event
|
||||||
|
/// </summary>
|
||||||
|
public event Action<RemoteInfo> OnDisconnect;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// OnError event
|
||||||
|
/// </summary>
|
||||||
|
public event Action<Exception, string> OnError;
|
||||||
|
|
||||||
|
private ClientWebSocket _webSocket;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The client name to be sent when connecting (optional).
|
||||||
|
/// </summary>
|
||||||
|
public string ClientName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create secure WebSocket client with certificate support
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uri"></param>
|
||||||
|
/// <param name="clientCertificate">The client certificate for the connection</param>
|
||||||
|
/// <param name="password">The password for the connection</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task ConnectAsync(string uri, X509Certificate2 clientCertificate = null, string password = null)
|
||||||
|
{
|
||||||
|
return CreateWebSocketClientAsync(uri, clientCertificate, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateWebSocketClientAsync(string uri, X509Certificate2 clientCertificate, string password)
|
||||||
|
{
|
||||||
|
_webSocket = new ClientWebSocket();
|
||||||
|
|
||||||
|
if (clientCertificate != null)
|
||||||
|
{
|
||||||
|
_webSocket.Options.ClientCertificates.Add(clientCertificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(password))
|
||||||
|
{
|
||||||
|
var passwordBytes = Encoding.UTF8.GetBytes(password);
|
||||||
|
_webSocket.Options.SetRequestHeader("Password", Convert.ToBase64String(passwordBytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(ClientName))
|
||||||
|
{
|
||||||
|
_webSocket.Options.SetRequestHeader("ClientName", ClientName);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Uri serverUri = new Uri(uri);
|
||||||
|
await _webSocket.ConnectAsync(serverUri, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
OnConnect?.Invoke(new RemoteInfo { IsWebSocket = true });
|
||||||
|
_ = StartReceivingAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
OnError?.Invoke(ex, $"Exception: {ex.Message}");
|
||||||
|
Disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartReceivingAsync()
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[BUFFER_SIZE];
|
||||||
|
while (_webSocket.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None).ConfigureAwait(false);
|
||||||
|
if (result.Count > 0)
|
||||||
|
{
|
||||||
|
byte[] data = new byte[result.Count];
|
||||||
|
Buffer.BlockCopy(buffer, 0, data, 0, result.Count);
|
||||||
|
OnReceive?.Invoke(new RemoteInfo { IsWebSocket = true, Data = data });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
OnError?.Invoke(ex, $"Exception: {ex.Message}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OnDisconnect?.Invoke(new RemoteInfo { IsWebSocket = true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send data
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task SendAsync(byte[] data)
|
||||||
|
{
|
||||||
|
return _webSocket.SendAsync(new ArraySegment<byte>(data), WebSocketMessageType.Binary, true, CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disconnect
|
||||||
|
/// </summary>
|
||||||
|
public void Disconnect()
|
||||||
|
{
|
||||||
|
_webSocket?.CloseAsync(WebSocketCloseStatus.NormalClosure, "Disconnecting", CancellationToken.None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,190 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace EonaCat.Network
|
||||||
|
{
|
||||||
|
public class WebSocketSecureServer
|
||||||
|
{
|
||||||
|
private const int BUFFER_SIZE = 4096;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// OnConnect event
|
||||||
|
/// </summary>
|
||||||
|
public event Action<RemoteInfo> OnConnect;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// OnReceive event
|
||||||
|
/// </summary>
|
||||||
|
public event Action<RemoteInfo> OnReceive;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// OnSend event
|
||||||
|
/// </summary>
|
||||||
|
public event Action<RemoteInfo> OnSend;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// OnDisconnect event
|
||||||
|
/// </summary>
|
||||||
|
public event Action<RemoteInfo> OnDisconnect;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// OnError event
|
||||||
|
/// </summary>
|
||||||
|
public event Action<Exception, string> OnError;
|
||||||
|
|
||||||
|
private readonly HttpListener _listener;
|
||||||
|
private CancellationTokenSource _cancellationTokenSource;
|
||||||
|
private Task _acceptTask;
|
||||||
|
private readonly X509Certificate2 _serverCertificate;
|
||||||
|
private readonly string _requiredPassword;
|
||||||
|
private bool _passwordProtectionEnabled;
|
||||||
|
|
||||||
|
private readonly Dictionary<string, WebSocket> _connectedClients = new Dictionary<string, WebSocket>();
|
||||||
|
private readonly Dictionary<string, string> _clientNames = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create secure WebSocket server with certificate support
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uriPrefixes">Array of URI prefixes to listen on, e.g., "https://localhost:8443/"</param>
|
||||||
|
/// <param name="serverCertificate">The server certificate for HTTPS</param>
|
||||||
|
/// <param name="requiredPassword">The password required from clients for the connection (optional)</param>
|
||||||
|
public WebSocketSecureServer(List<string> uriPrefixes, X509Certificate2 serverCertificate = null, string requiredPassword = null)
|
||||||
|
{
|
||||||
|
_listener = new HttpListener();
|
||||||
|
_serverCertificate = serverCertificate;
|
||||||
|
_requiredPassword = requiredPassword;
|
||||||
|
_passwordProtectionEnabled = !string.IsNullOrEmpty(requiredPassword);
|
||||||
|
|
||||||
|
foreach (var uriPrefix in uriPrefixes)
|
||||||
|
{
|
||||||
|
_listener.Prefixes.Add(uriPrefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start secure WebSocket server
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_listener.Start();
|
||||||
|
_cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
|
_acceptTask = AcceptConnectionsAsync(_cancellationTokenSource.Token);
|
||||||
|
return _acceptTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AcceptConnectionsAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var context = await _listener.GetContextAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (context.Request.IsWebSocketRequest)
|
||||||
|
{
|
||||||
|
if (_passwordProtectionEnabled)
|
||||||
|
{
|
||||||
|
var password = context.Request.Headers["Password"];
|
||||||
|
|
||||||
|
if (password != _requiredPassword)
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 401; // Unauthorized
|
||||||
|
context.Response.Close();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string clientName = null;
|
||||||
|
foreach (var key in context.Request.Headers.AllKeys)
|
||||||
|
{
|
||||||
|
if (key == "ClientName")
|
||||||
|
{
|
||||||
|
clientName = context.Request.Headers["ClientName"];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var webSocketContext = await context.AcceptWebSocketAsync(subProtocol: null).ConfigureAwait(false);
|
||||||
|
_ = HandleWebSocketConnectionAsync(webSocketContext.WebSocket, clientName, cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 400;
|
||||||
|
context.Response.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
OnError?.Invoke(ex, $"Exception: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleWebSocketConnectionAsync(WebSocket webSocket, string clientName, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
string clientId = Guid.NewGuid().ToString();
|
||||||
|
_connectedClients.Add(clientId, webSocket);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(clientName))
|
||||||
|
{
|
||||||
|
_clientNames.TryAdd(clientId, clientName);
|
||||||
|
}
|
||||||
|
|
||||||
|
OnConnect?.Invoke(new RemoteInfo { IsWebSocket = true, ClientId = clientId, ClientName = clientName });
|
||||||
|
|
||||||
|
byte[] buffer = new byte[BUFFER_SIZE];
|
||||||
|
while (webSocket.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken).ConfigureAwait(false);
|
||||||
|
if (result.Count > 0)
|
||||||
|
{
|
||||||
|
byte[] data = new byte[result.Count];
|
||||||
|
Buffer.BlockCopy(buffer, 0, data, 0, result.Count);
|
||||||
|
OnReceive?.Invoke(new RemoteInfo { IsWebSocket = true, ClientId = clientId, ClientName = clientName, Data = data });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
OnError?.Invoke(ex, $"Exception: {ex.Message}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OnDisconnect?.Invoke(new RemoteInfo { IsWebSocket = true, ClientId = clientId, ClientName = clientName });
|
||||||
|
_connectedClients.Remove(clientId);
|
||||||
|
_clientNames.Remove(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stop secure WebSocket server
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task StopAsync()
|
||||||
|
{
|
||||||
|
_cancellationTokenSource.Cancel();
|
||||||
|
_listener.Stop();
|
||||||
|
if (_acceptTask != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _acceptTask.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (AggregateException) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue