From d6f0c7b5af52f470536a49ed77cc4f5eef6a69e6 Mon Sep 17 00:00:00 2001 From: Jeroen Date: Sat, 11 Nov 2023 10:46:31 +0100 Subject: [PATCH] Added Secure WebSockets --- EonaCat.Network/EonaCat.Network.csproj | 13 +- EonaCat.Network/System/Sockets/RemoteInfo.cs | 3 + .../Sockets/Web/WebSocketSecureClient.cs | 133 ++++++++++++ .../Sockets/Web/WebSocketSecureServer.cs | 190 ++++++++++++++++++ README.md | 3 +- 5 files changed, 333 insertions(+), 9 deletions(-) create mode 100644 EonaCat.Network/System/Sockets/Web/WebSocketSecureClient.cs create mode 100644 EonaCat.Network/System/Sockets/Web/WebSocketSecureServer.cs diff --git a/EonaCat.Network/EonaCat.Network.csproj b/EonaCat.Network/EonaCat.Network.csproj index 698e847..f0177ef 100644 --- a/EonaCat.Network/EonaCat.Network.csproj +++ b/EonaCat.Network/EonaCat.Network.csproj @@ -15,10 +15,10 @@ https://www.nuget.org/packages/EonaCat.Network/ EonaCat, Network, .NET Standard, EonaCatHelpers, Jeroen, Saey, Protocol, Quic, UDP, TCP, Web, Server - EonaCat Networking library with Quic, TCP, UDP and a Webserver - 1.0.9 - 1.0.0.9 - 1.0.0.9 + EonaCat Networking library with Quic, TCP, UDP, WebSockets and a Webserver + 1.1.0 + 1.1.0.0 + 1.1.0.0 icon.png @@ -52,9 +52,6 @@ - - - - + diff --git a/EonaCat.Network/System/Sockets/RemoteInfo.cs b/EonaCat.Network/System/Sockets/RemoteInfo.cs index 347f1db..8cbf966 100644 --- a/EonaCat.Network/System/Sockets/RemoteInfo.cs +++ b/EonaCat.Network/System/Sockets/RemoteInfo.cs @@ -13,5 +13,8 @@ namespace EonaCat.Network public int Port => HasEndpoint ? ((IPEndPoint)EndPoint).Port : 0; public Socket Socket { 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; } } } diff --git a/EonaCat.Network/System/Sockets/Web/WebSocketSecureClient.cs b/EonaCat.Network/System/Sockets/Web/WebSocketSecureClient.cs new file mode 100644 index 0000000..c90eb5d --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/WebSocketSecureClient.cs @@ -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; + + /// + /// OnConnect event + /// + public event Action OnConnect; + + /// + /// OnReceive event + /// + public event Action OnReceive; + + /// + /// OnDisconnect event + /// + public event Action OnDisconnect; + + /// + /// OnError event + /// + public event Action OnError; + + private ClientWebSocket _webSocket; + + /// + /// The client name to be sent when connecting (optional). + /// + public string ClientName { get; set; } + + /// + /// Create secure WebSocket client with certificate support + /// + /// + /// The client certificate for the connection + /// The password for the connection + /// + 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(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 }); + } + + /// + /// Send data + /// + /// + /// + public Task SendAsync(byte[] data) + { + return _webSocket.SendAsync(new ArraySegment(data), WebSocketMessageType.Binary, true, CancellationToken.None); + } + + /// + /// Disconnect + /// + public void Disconnect() + { + _webSocket?.CloseAsync(WebSocketCloseStatus.NormalClosure, "Disconnecting", CancellationToken.None); + } + } +} diff --git a/EonaCat.Network/System/Sockets/Web/WebSocketSecureServer.cs b/EonaCat.Network/System/Sockets/Web/WebSocketSecureServer.cs new file mode 100644 index 0000000..6f5351d --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/WebSocketSecureServer.cs @@ -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; + + /// + /// OnConnect event + /// + public event Action OnConnect; + + /// + /// OnReceive event + /// + public event Action OnReceive; + + /// + /// OnSend event + /// + public event Action OnSend; + + /// + /// OnDisconnect event + /// + public event Action OnDisconnect; + + /// + /// OnError event + /// + public event Action 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 _connectedClients = new Dictionary(); + private readonly Dictionary _clientNames = new Dictionary(); + + /// + /// Create secure WebSocket server with certificate support + /// + /// Array of URI prefixes to listen on, e.g., "https://localhost:8443/" + /// The server certificate for HTTPS + /// The password required from clients for the connection (optional) + public WebSocketSecureServer(List 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); + } + } + + /// + /// Start secure WebSocket server + /// + /// + /// + 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(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); + } + + /// + /// Stop secure WebSocket server + /// + /// + public async Task StopAsync() + { + _cancellationTokenSource.Cancel(); + _listener.Stop(); + if (_acceptTask != null) + { + try + { + await _acceptTask.ConfigureAwait(false); + } + catch (AggregateException) { } + } + } + } +} diff --git a/README.md b/README.md index b1ea2f3..de5e416 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,5 @@ ------------ -EonaCat Network Library \ No newline at end of file +EonaCat Network Library +