From ddea0ed3fa4274deaecd03b32796d8c51c207353 Mon Sep 17 00:00:00 2001 From: Christoph Jung Date: Sat, 21 Sep 2024 12:11:49 +0200 Subject: [PATCH 01/16] Add async event for handling a clients upgrade request Improve WebConext to handle the StatusCode Improve WebSocketUpgradeHandler to handle the status code properly --- .../Contracts/IWebSocketServer.cs | 6 ++ .../Delegates/AsyncEventHandler.cs | 14 ++++ .../ClientUpgradeRequestReceivedArgs.cs | 27 +++++++ Jung.SimpleWebSocket/Models/WebContext.cs | 71 +++++++++++++++++-- .../Models/WebSocketServerClient.cs | 8 +++ Jung.SimpleWebSocket/SimpleWebSocketServer.cs | 17 ++++- .../Utility/AsyncEventRaiser.cs | 51 +++++++++++++ .../Wrappers/WebSocketUpgradeHandler.cs | 22 +++++- .../SimpleWebSocketTest.cs | 37 +++++++++- 9 files changed, 242 insertions(+), 11 deletions(-) create mode 100644 Jung.SimpleWebSocket/Delegates/AsyncEventHandler.cs create mode 100644 Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs create mode 100644 Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs diff --git a/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs b/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs index 4e9e79b..25e5918 100644 --- a/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs +++ b/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs @@ -4,6 +4,7 @@ using Jung.SimpleWebSocket.Delegates; using Jung.SimpleWebSocket.Models; using Jung.SimpleWebSocket.Models.EventArguments; +using Jung.SimpleWebSocket.Utility; using System.Net; namespace Jung.SimpleWebSocket.Contracts; @@ -58,6 +59,11 @@ public interface IWebSocketServer : IDisposable /// event ClientBinaryMessageReceivedEventHandler? BinaryMessageReceived; + /// + /// Async Event that is raised when a client upgrade request is received. + /// + event AsyncEventHandler? ClientUpgradeRequestReceivedAsync; + /// /// Gets a client by its id. /// diff --git a/Jung.SimpleWebSocket/Delegates/AsyncEventHandler.cs b/Jung.SimpleWebSocket/Delegates/AsyncEventHandler.cs new file mode 100644 index 0000000..ba551fe --- /dev/null +++ b/Jung.SimpleWebSocket/Delegates/AsyncEventHandler.cs @@ -0,0 +1,14 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +namespace Jung.SimpleWebSocket.Delegates; + +/// +/// Represents an asynchronous event handler. +/// +/// The type of the event arguments. +/// The sender of the event. +/// The event arguments. +/// The cancellation token. +/// A task that represents the asynchronous operation. +public delegate Task AsyncEventHandler(object sender, TEventArgs e, CancellationToken cancellationToken) where TEventArgs : class; \ No newline at end of file diff --git a/Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs b/Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs new file mode 100644 index 0000000..0f238b0 --- /dev/null +++ b/Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs @@ -0,0 +1,27 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using Microsoft.Extensions.Logging; + +namespace Jung.SimpleWebSocket.Models.EventArguments; + +/// +/// Represents the arguments of the event when a upgrade request is received from a client. +/// +/// The client that is sending the upgrade request. +/// The context of the request. +/// The current Logger. +public record ClientUpgradeRequestReceivedArgs(WebSocketServerClient Client, WebContext WebContext, ILogger? Logger) +{ + private WebContext? _responseContext; + + /// + /// Gets or sets a value indicating whether the upgrade request should be handled. + /// + public bool Handle { get; set; } = true; + + /// + /// The context that is being use to response to the client. + /// + public WebContext ResponseContext { get => _responseContext ??= new WebContext(); } +} diff --git a/Jung.SimpleWebSocket/Models/WebContext.cs b/Jung.SimpleWebSocket/Models/WebContext.cs index c2603f6..cf73bde 100644 --- a/Jung.SimpleWebSocket/Models/WebContext.cs +++ b/Jung.SimpleWebSocket/Models/WebContext.cs @@ -3,7 +3,8 @@ using Jung.SimpleWebSocket.Exceptions; using System.Collections.Specialized; -using System.Net.Http.Headers; +using System.Net; +using System.Text.RegularExpressions; namespace Jung.SimpleWebSocket.Models; @@ -15,7 +16,7 @@ namespace Jung.SimpleWebSocket.Models; /// Initializes a new instance of the class. /// /// The content of the web request. -internal class WebContext(string? content = null) +public partial class WebContext(string? content = null) { /// @@ -43,6 +44,17 @@ internal class WebContext(string? content = null) /// private string? _requestPath = null; + /// + /// The status code of the context. + /// + private HttpStatusCode? _statusCode; + + + // A Regular Expression to split a string by uppercase letters. + [GeneratedRegex(@"(? /// Gets the headers of the web request. /// @@ -140,6 +152,33 @@ private set } } + /// + /// Gets or sets the status code of the context + /// + public HttpStatusCode StatusCode + { + get + { + if (_statusCode == null) + { + var parts = StatusLine.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (parts.Length >= 2) + { + _statusCode = (HttpStatusCode)Enum.Parse(typeof(HttpStatusCode), parts[1]); + } + else + { + throw new WebSocketUpgradeException("Status code is missing"); + } + } + return _statusCode.Value; + } + set + { + _statusCode = value; + } + } + /// /// Parses the headers of the web request. /// @@ -185,12 +224,12 @@ internal static WebContext CreateRequest(string hostName, int port, string reque /// Checks if the web request contains a specific header with a specific value. /// /// The name of the header. - /// The value of the header. + /// The value of the header. If null, only the header name is checked. /// true if the web request contains the specified header with the specified value; otherwise, false. - internal bool ContainsHeader(string name, string value) + internal bool ContainsHeader(string name, string? value = null) { string? headerValue = Headers[name]; - return headerValue != null && headerValue.Contains(value, StringComparison.OrdinalIgnoreCase); + return headerValue != null && (value == null || headerValue.Contains(value, StringComparison.OrdinalIgnoreCase)); } /// @@ -300,6 +339,28 @@ public string RequestLine } } + /// + /// Gets a value indicating whether the Content is empty. + /// + public bool IsEmpty => string.IsNullOrWhiteSpace(_content); + + /// + /// Gets the status description of the web request. + /// + public string StatusDescription => GetStatusDescription(StatusCode); + + + /// + /// Gets the status description for the given status code. + /// + /// The status code. + /// A string containing the status description. + public static string GetStatusDescription(HttpStatusCode statusCode) + { + var enumName = Enum.GetName(statusCode) ?? throw new WebSocketUpgradeException("Status code is not a valid HttpStatusCode"); + return string.Join(" ", _splitByUppercaseRegex.Split(enumName)); + } + /// /// Gets the content lines of the web request. /// diff --git a/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs b/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs index d737b17..c138a3c 100644 --- a/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs +++ b/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs @@ -16,6 +16,14 @@ public class WebSocketServerClient : IDisposable /// public string Id { get; private set; } = Guid.NewGuid().ToString(); + /// + /// Gets the properties of the WebSocket client. + /// + /// + /// The properties can be used to store additional information about the client. + /// + public Dictionary Properties { get; } = []; + /// /// Gets the connection of the WebSocket client. /// diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index 943ca4a..ef56a5e 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -6,6 +6,7 @@ using Jung.SimpleWebSocket.Exceptions; using Jung.SimpleWebSocket.Models; using Jung.SimpleWebSocket.Models.EventArguments; +using Jung.SimpleWebSocket.Utility; using Jung.SimpleWebSocket.Wrappers; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; @@ -34,6 +35,9 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable /// public event ClientBinaryMessageReceivedEventHandler? BinaryMessageReceived; + /// + public event AsyncEventHandler? ClientUpgradeRequestReceivedAsync; + /// /// A dictionary of active clients. /// @@ -227,7 +231,18 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT using var stream = client.ClientConnection.GetStream(); var socketWrapper = new WebSocketUpgradeHandler(stream); var request = await socketWrapper.AwaitContextAsync(cancellationToken); - await socketWrapper.AcceptWebSocketAsync(request, cancellationToken); + + // raise async client upgrade request received event + var eventArgs = new ClientUpgradeRequestReceivedArgs(client, request, _logger); + await AsyncEventRaiser.RaiseAsync(ClientUpgradeRequestReceivedAsync, this, eventArgs, cancellationToken); + if (!eventArgs.Handle) + { + _logger?.LogDebug("Client upgrade request rejected by ClientUpgradeRequestReceivedAsync event."); + // send rejection response + return; + } + + await socketWrapper.AcceptWebSocketAsync(request, eventArgs.WebContext, cancellationToken); client.UpdateWebSocket(socketWrapper.CreateWebSocket(isServer: true)); _ = Task.Run(() => ClientConnected?.Invoke(this, new ClientConnectedArgs(client.Id)), cancellationToken); diff --git a/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs b/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs new file mode 100644 index 0000000..b81a851 --- /dev/null +++ b/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs @@ -0,0 +1,51 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using Jung.SimpleWebSocket.Delegates; + +namespace Jung.SimpleWebSocket.Utility +{ + /// + /// Helper class to raise an async event. + /// + internal class AsyncEventRaiser + { + /// + /// Helper method to raise an async event. + /// + /// The type of the event arguments. + /// The async event handler + /// The sender of the event. + /// The event arguments. + /// The cancellation token. + /// A task that represents the asynchronous operation. + internal static async Task RaiseAsync(AsyncEventHandler? asyncEvent, object sender, TEventArgs e, CancellationToken cancellationToken) where TEventArgs : class + { + var syncContext = SynchronizationContext.Current; // Capture the current synchronization context + + if (asyncEvent != null) + { + var invocationList = asyncEvent.GetInvocationList(); + + foreach (var handler in invocationList) + { + var asyncHandler = (AsyncEventHandler)handler; + + if (syncContext != null) + { + // Post back to the captured context if it's not null + syncContext.Post(async _ => + { + await asyncHandler(sender, e, cancellationToken); + }, null); + } + else + { + // Execute directly if there's no synchronization context + await asyncHandler(sender, e, cancellationToken); + } + } + } + } + } +} diff --git a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs index 6da2ffc..2823c4e 100644 --- a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs +++ b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs @@ -9,6 +9,7 @@ using Jung.SimpleWebSocket.Exceptions; using Jung.SimpleWebSocket.Helpers; using Jung.SimpleWebSocket.Models; +using System.Net; using System.Net.Sockets; using System.Net.WebSockets; using System.Security.Cryptography; @@ -63,14 +64,23 @@ public async Task AwaitContextAsync(CancellationToken cancellationTo public async Task AcceptWebSocketAsync(WebContext request, CancellationToken cancellationToken) { - await AcceptWebSocketAsync(request, null, cancellationToken); + await AcceptWebSocketAsync(request, new WebContext(), null, cancellationToken); + } + + public async Task AcceptWebSocketAsync(WebContext request, WebContext response, CancellationToken cancellationToken) + { + await AcceptWebSocketAsync(request, response, null, cancellationToken); } public async Task AcceptWebSocketAsync(WebContext request, string? subProtocol, CancellationToken cancellationToken) + { + await AcceptWebSocketAsync(request, new WebContext(), subProtocol, cancellationToken); + } + + public async Task AcceptWebSocketAsync(WebContext request, WebContext response, string? subProtocol, CancellationToken cancellationToken) { try { - var response = new WebContext(); ValidateWebSocketHeaders(request); var protocol = request.GetConcatenatedHeaders("Sec-WebSocket-Protocol"); if (ProcessWebSocketProtocolHeader(protocol, subProtocol, out var acceptProtocol)) @@ -82,6 +92,7 @@ public async Task AcceptWebSocketAsync(WebContext request, string? subProtocol, response.Headers.Add("Connection", "upgrade"); response.Headers.Add("Upgrade", "websocket"); response.Headers.Add("Sec-WebSocket-Accept", secWebSocketAcceptString); + response.StatusCode = HttpStatusCode.SwitchingProtocols; await SendWebSocketResponseHeaders(response, cancellationToken); _acceptedProtocol = subProtocol; } @@ -98,7 +109,7 @@ public async Task AcceptWebSocketAsync(WebContext request, string? subProtocol, private async Task SendWebSocketResponseHeaders(WebContext context, CancellationToken cancellationToken) { var sb = new StringBuilder( - $"HTTP/1.1 101 Switching Protocols\r\n"); + $"HTTP/1.1 {(int)context.StatusCode} {context.StatusDescription}\r\n"); AddHeaders(context, sb); FinishMessage(sb); @@ -219,6 +230,11 @@ private static void ValidateRequestPath(string requestPath) internal static void ValidateUpgradeResponse(WebContext response, WebContext requestContext) { + if (response.IsEmpty) + { + throw new WebSocketUpgradeException("Empty response received"); + } + // Check if the response contains '101 Switching Protocols' if (!response.StatusLine.Contains("101 Switching Protocols")) { diff --git a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs index 0b058ec..1d56d99 100644 --- a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs +++ b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs @@ -17,7 +17,7 @@ namespace Jung.SimpleWebSocketTest [TestFixture] public class SimpleWebSocketTest { - private List _logMessages = []; + private readonly List _logMessages = []; private Mock> _serverLogger; private Mock> _clientLogger; @@ -60,7 +60,7 @@ private void SetUpLogger(Mock> mock, string loggerName) var formatter = invocation.Arguments[4]; var invokeMethod = formatter.GetType().GetMethod("Invoke"); - var logMessage = invokeMethod!.Invoke(formatter, new[] { state, exception }); + var logMessage = invokeMethod!.Invoke(formatter, [state, exception]); _logMessages.Add($"{loggerName}({logLevel}): {logMessage}"); })); } @@ -106,6 +106,27 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() messageResetEvent.Set(); }; + server.ClientUpgradeRequestReceivedAsync += async (sender, args, cancellationToken) => + { + var IpAddress = (args.Client.RemoteEndPoint as IPEndPoint)?.Address.ToString(); + if (IpAddress == null) + { + args.Handle = false; + return; + } + + // Check the IpAddress against the database + var isWhitelistedEndPoint = await DbContext_IpAddresses_Contains(IpAddress, cancellationToken); + if (!isWhitelistedEndPoint) + { + args.ResponseContext.StatusCode = HttpStatusCode.Forbidden; + // not yet implemented + // args.ResponseContext.Body = ""; + args.Handle = false; + } + args.Client.Properties["test"] = "test"; + }; + // Act server.Start(); await client.ConnectAsync(); @@ -129,6 +150,18 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() }); } + /// + /// Fake Async method to simulate a database call to check if the IP address is in the database. + /// + /// The IP address to check. + /// The cancellation token. + /// A task that represents the asynchronous operation. The task result contains a value indicating whether the IP address is in the database. + private static async Task DbContext_IpAddresses_Contains(string ipAddress, CancellationToken cancellationToken) + { + await Task.Delay(100, cancellationToken); + return ipAddress == IPAddress.Loopback.ToString(); + } + [Test] [Platform("Windows7,Windows8,Windows8.1,Windows10", Reason = "This test establishes a TCP client-server connection using SimpleWebSocket, which relies on specific networking features and behaviors that are only available and consistent on Windows platforms. Running this test on non-Windows platforms could lead to inconsistent results or failures due to differences in networking stack implementations.")] public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld2() From 1ead9c3fb6f84db731068bea81caef194ba17d7b Mon Sep 17 00:00:00 2001 From: Christoph Date: Sat, 28 Sep 2024 21:18:01 +0200 Subject: [PATCH 02/16] Add user id header handling Add BodyContent --- .../Contracts/IWebSocketClient.cs | 5 ++ Jung.SimpleWebSocket/Models/WebContext.cs | 58 +++++++++++++- .../Models/WebSocketServerClient.cs | 22 +++-- Jung.SimpleWebSocket/SimpleWebSocketClient.cs | 13 ++- Jung.SimpleWebSocket/SimpleWebSocketServer.cs | 80 +++++++++++++++---- .../Wrappers/WebSocketUpgradeHandler.cs | 52 ++++++++++-- .../SimpleWebSocketTest.cs | 25 +++++- .../WebSocketUpgradeHandlerTests.cs | 4 +- 8 files changed, 216 insertions(+), 43 deletions(-) diff --git a/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs b/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs index 1a73240..5bad1b4 100644 --- a/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs +++ b/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs @@ -30,6 +30,11 @@ public interface IWebSocketClient : IDisposable /// bool IsConnected { get; } + /// + /// The user id of the client. If not set, the server did not sent a user id at websocket upgrade. + /// + string? UserId { get; } + /// /// Event that is raised when a message is received from a client. /// diff --git a/Jung.SimpleWebSocket/Models/WebContext.cs b/Jung.SimpleWebSocket/Models/WebContext.cs index c2603f6..fa97341 100644 --- a/Jung.SimpleWebSocket/Models/WebContext.cs +++ b/Jung.SimpleWebSocket/Models/WebContext.cs @@ -3,7 +3,6 @@ using Jung.SimpleWebSocket.Exceptions; using System.Collections.Specialized; -using System.Net.Http.Headers; namespace Jung.SimpleWebSocket.Models; @@ -43,6 +42,11 @@ internal class WebContext(string? content = null) /// private string? _requestPath = null; + /// + /// The body content. + /// + private string? _bodyContent = null; + /// /// Gets the headers of the web request. /// @@ -55,6 +59,27 @@ public NameValueCollection Headers } } + public string BodyContent + { + get + { + if (_bodyContent == null) + { + var parts = _content.Split("\r\n\r\n", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + _bodyContent = parts.Length > 1 ? parts[1] : string.Empty; + } + return _bodyContent; + } + set + { + if (BodyContent != string.Empty) + { + throw new WebSocketUpgradeException("Body content is already set"); + } + _bodyContent = value; + } + } + /// /// Gets the host name of the web request. /// @@ -170,15 +195,23 @@ private NameValueCollection ParseHeaders() /// The host name of the web request. /// The port of the web request. /// The request path of the web request. + /// The user id of the web request. /// The created web request context. - internal static WebContext CreateRequest(string hostName, int port, string requestPath) + internal static WebContext CreateRequest(string hostName, int port, string requestPath, string? userId = null) { - return new WebContext() + var context = new WebContext() { HostName = hostName, Port = port, - RequestPath = requestPath + RequestPath = requestPath, }; + + if (userId != null) + { + context.Headers.Add("x-user-id", userId); + } + + return context; } /// @@ -300,6 +333,23 @@ public string RequestLine } } + /// + /// Gets the user id of the web request. + /// + public string UserId + { + get + { + var userId = Headers["x-user-id"] ?? throw new WebSocketUpgradeException("UserId header is missing"); + return userId; + } + } + + /// + /// Gets a value indicating whether the web request contains a user id. + /// + public bool ContainsUserId => Headers["x-user-id"] != null; + /// /// Gets the content lines of the web request. /// diff --git a/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs b/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs index d737b17..a30b924 100644 --- a/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs +++ b/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs @@ -19,7 +19,7 @@ public class WebSocketServerClient : IDisposable /// /// Gets the connection of the WebSocket client. /// - internal ITcpClient ClientConnection { get; private set; } + internal ITcpClient? ClientConnection { get; private set; } /// /// Gets the timestamp when the WebSocket client was last seen. @@ -34,7 +34,7 @@ public class WebSocketServerClient : IDisposable /// /// Gets the remote endpoint of the WebSocket client. /// - public EndPoint? RemoteEndPoint => ClientConnection.RemoteEndPoint; + public EndPoint? RemoteEndPoint => ClientConnection?.RemoteEndPoint; /// /// Gets or sets the WebSocket of the client. @@ -62,6 +62,16 @@ internal void UpdateClient(ITcpClient client) ClientConnection = client; } + /// + /// Updates the client with a new WebSocket. + /// + /// The websocket that the client should use. + internal void UseWebSocket(IWebSocket? webSocket) + { + ArgumentNullException.ThrowIfNull(webSocket); + WebSocket = webSocket; + } + /// /// Updates the WebSocket client with a new identifier. /// @@ -82,17 +92,13 @@ internal void UpdateId(string id) Id = id; } - internal void UpdateWebSocket(IWebSocket? webSocket) - { - ArgumentNullException.ThrowIfNull(webSocket); - WebSocket = webSocket; - } - /// public void Dispose() { WebSocket?.Dispose(); + WebSocket = null; ClientConnection?.Dispose(); + ClientConnection = null; GC.SuppressFinalize(this); } } diff --git a/Jung.SimpleWebSocket/SimpleWebSocketClient.cs b/Jung.SimpleWebSocket/SimpleWebSocketClient.cs index b690ab7..7b362cf 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketClient.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketClient.cs @@ -23,7 +23,7 @@ namespace Jung.SimpleWebSocket /// The port to connect to /// The web socket request path /// A logger to write internal log messages - public class SimpleWebSocketClient(string hostName, int port, string requestPath, ILogger? logger = null) : IWebSocketClient, IDisposable + public class SimpleWebSocketClient(string hostName, int port, string requestPath, string? userId = null, ILogger? logger = null) : IWebSocketClient, IDisposable { /// public string HostName { get; } = hostName; @@ -32,6 +32,9 @@ public class SimpleWebSocketClient(string hostName, int port, string requestPath /// public string RequestPath { get; } = requestPath; + /// + public string? UserId { get; private set; } + /// public bool IsConnected => _client?.Connected ?? false; @@ -150,10 +153,16 @@ private async Task HandleWebSocketInitiation(TcpClientWrapper client, Cancellati _stream = client.GetStream(); var socketWrapper = new WebSocketUpgradeHandler(_stream); - var requestContext = WebContext.CreateRequest(HostName, Port, RequestPath); + var requestContext = WebContext.CreateRequest(HostName, Port, RequestPath, userId); await socketWrapper.SendUpgradeRequestAsync(requestContext, cancellationToken); var response = await socketWrapper.AwaitContextAsync(cancellationToken); WebSocketUpgradeHandler.ValidateUpgradeResponse(response, requestContext); + + if (response.ContainsUserId) + { + UserId = response.UserId; + } + _webSocket = socketWrapper.CreateWebSocket(isServer: false); } diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index 943ca4a..e8f3f15 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -129,13 +129,7 @@ public void Start(CancellationToken? cancellationToken = null) // Accept the client var client = await _server.AcceptTcpClientAsync(linkedTokenSource.Token); - _logger?.LogDebug("Client connected from {endpoint}", client.ClientConnection.RemoteEndPoint); - var clientAdded = ActiveClients.TryAdd(client.Id, client); - if (!clientAdded) - { - _logger?.LogError("Error while adding client to active clients, rejecting client."); - continue; - } + _logger?.LogDebug("Client connected from {endpoint}", client.ClientConnection!.RemoteEndPoint); _ = HandleClientAsync(client, linkedTokenSource.Token); } @@ -221,20 +215,64 @@ public WebSocketServerClient GetClientById(string clientId) /// A asynchronous task private async Task HandleClientAsync(WebSocketServerClient client, CancellationToken cancellationToken) { + bool clientAdded = false; try { // Upgrade the connection to a WebSocket - using var stream = client.ClientConnection.GetStream(); + using var stream = client.ClientConnection!.GetStream(); var socketWrapper = new WebSocketUpgradeHandler(stream); var request = await socketWrapper.AwaitContextAsync(cancellationToken); - await socketWrapper.AcceptWebSocketAsync(request, cancellationToken); - client.UpdateWebSocket(socketWrapper.CreateWebSocket(isServer: true)); - _ = Task.Run(() => ClientConnected?.Invoke(this, new ClientConnectedArgs(client.Id)), cancellationToken); + // Check if the request contains a user id + if (request.ContainsUserId) + { + _logger?.LogDebug("User id found in request: {userId}", request.UserId); + // Check if the client is an existing passive client + var clientExists = PassiveClients.ContainsKey(request.UserId); + if (clientExists) + { + _logger?.LogDebug("Passive client found for user id {userId} - reactivating user.", request.UserId); + + // Use the existing client + // Update the client with the new connection + // Remove the client from the passive clients + var passiveClient = PassiveClients[request.UserId]; + passiveClient.UpdateClient(client.ClientConnection); + client = passiveClient; + PassiveClients.TryRemove(request.UserId, out _); + } + else + { + // No passive client found, checking for active clients with the same id + if (ActiveClients.ContainsKey(request.UserId)) + { + _logger?.LogDebug("Active client found for user id {userId} - rejecting connection.", request.UserId); + // Reject the connection + await socketWrapper.RejectWebSocketAsync(cancellationToken); + return; + } + else + { + // Update the client with the new id + client.UpdateId(request.UserId); + } + } + } + + await socketWrapper.AcceptWebSocketAsync(request, client.Id, cancellationToken); + + // Update the client with the new WebSocket + client.UseWebSocket(socketWrapper.CreateWebSocket(isServer: true)); + + clientAdded = ActiveClients.TryAdd(client.Id, client); + if (clientAdded) + { + _ = Task.Run(() => ClientConnected?.Invoke(this, new ClientConnectedArgs(client.Id)), cancellationToken); + // Start listening for messages + _logger?.LogDebug("Connection upgraded, now listening on client {clientId}", client.Id); + await ProcessWebSocketMessagesAsync(client, cancellationToken); + } - // Start listening for messages - _logger?.LogDebug("Connection upgraded, now listening on client {clientId}", client.Id); - await ProcessWebSocketMessagesAsync(client, cancellationToken); } catch (OperationCanceledException) { @@ -246,14 +284,22 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT } finally { - if (!_serverShuttingDown) + // If the client was added and the server is not shutting down, handle the disconnected client + // The client is not added if the connection was rejected + if (clientAdded && !_serverShuttingDown) { - ActiveClients.TryRemove(client.Id, out _); - client?.Dispose(); + HandleDisconnectedClient(client); } } } + private void HandleDisconnectedClient(WebSocketServerClient client) + { + ActiveClients.TryRemove(client.Id, out _); + client.Dispose(); + PassiveClients.TryAdd(client.Id, client); + } + /// /// Processes the WebSocket messages. /// diff --git a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs index 6da2ffc..3b8cd9f 100644 --- a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs +++ b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs @@ -21,6 +21,7 @@ internal partial class WebSocketUpgradeHandler { private const string _supportedVersion = "13"; private const string _webSocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + private const string _userIdHeaderName = "x-user-id"; private string? _acceptedProtocol; private readonly INetworkStream _networkStream; @@ -61,12 +62,12 @@ public async Task AwaitContextAsync(CancellationToken cancellationTo return context; } - public async Task AcceptWebSocketAsync(WebContext request, CancellationToken cancellationToken) + public async Task AcceptWebSocketAsync(WebContext request, string userId, CancellationToken cancellationToken) { - await AcceptWebSocketAsync(request, null, cancellationToken); + await AcceptWebSocketAsync(request, userId, null, cancellationToken); } - public async Task AcceptWebSocketAsync(WebContext request, string? subProtocol, CancellationToken cancellationToken) + public async Task AcceptWebSocketAsync(WebContext request, string userId, string? subProtocol, CancellationToken cancellationToken) { try { @@ -82,6 +83,7 @@ public async Task AcceptWebSocketAsync(WebContext request, string? subProtocol, response.Headers.Add("Connection", "upgrade"); response.Headers.Add("Upgrade", "websocket"); response.Headers.Add("Sec-WebSocket-Accept", secWebSocketAcceptString); + response.Headers.Add(_userIdHeaderName, userId); await SendWebSocketResponseHeaders(response, cancellationToken); _acceptedProtocol = subProtocol; } @@ -100,7 +102,19 @@ private async Task SendWebSocketResponseHeaders(WebContext context, Cancellation var sb = new StringBuilder( $"HTTP/1.1 101 Switching Protocols\r\n"); AddHeaders(context, sb); - FinishMessage(sb); + CompleteHeaderSection(sb); + + byte[] responseBytes = Encoding.UTF8.GetBytes(sb.ToString()); + await _networkStream.WriteAsync(responseBytes, cancellationToken); + } + + private async Task SendWebSocketRejectResponse(WebContext context, CancellationToken cancellationToken) + { + var sb = new StringBuilder( + $"HTTP/1.1 409 Conflict\r\n"); + AddHeaders(context, sb); + CompleteHeaderSection(sb); + AddBody(context, sb); byte[] responseBytes = Encoding.UTF8.GetBytes(sb.ToString()); await _networkStream.WriteAsync(responseBytes, cancellationToken); @@ -112,7 +126,7 @@ private async Task SendWebSocketRequestHeaders(WebContext context, CancellationT $"GET {context.RequestPath} HTTP/1.1\r\n" + $"Host: {context.HostName}:{context.Port}\r\n"); AddHeaders(context, sb); - FinishMessage(sb); + CompleteHeaderSection(sb); byte[] responseBytes = Encoding.UTF8.GetBytes(sb.ToString()); await _networkStream.WriteAsync(responseBytes, cancellationToken); @@ -126,11 +140,16 @@ private static void AddHeaders(WebContext response, StringBuilder sb) } } - private static void FinishMessage(StringBuilder sb) + private static void CompleteHeaderSection(StringBuilder sb) { sb.Append("\r\n"); } + private void AddBody(WebContext context, StringBuilder sb) + { + sb.Append(context.BodyContent); + } + private static void ValidateWebSocketHeaders(WebContext context) { if (!context.IsWebSocketRequest) @@ -222,12 +241,16 @@ internal static void ValidateUpgradeResponse(WebContext response, WebContext req // Check if the response contains '101 Switching Protocols' if (!response.StatusLine.Contains("101 Switching Protocols")) { + if (!string.IsNullOrEmpty(response.BodyContent)) + { + throw new WebSocketUpgradeException($"Connection not upgraded. The server returned: {response.BodyContent}"); + } throw new WebSocketUpgradeException("Invalid status code, expected '101 Switching Protocols'."); } - // Check for required headers 'Upgrade: websocket' and 'Connection: Upgrade' + // Check for required headers 'Upgrade: websocket' and 'Connection: upgrade' if (!response.ContainsHeader("Upgrade", "websocket") || - !response.ContainsHeader("Connection", "Upgrade")) + !response.ContainsHeader("Connection", "upgrade")) { throw new WebSocketUpgradeException("Invalid 'Upgrade' or 'Connection' header."); } @@ -274,4 +297,17 @@ internal IWebSocket CreateWebSocket(bool isServer, TimeSpan? keepAliveInterval = keepAliveInterval ??= TimeSpan.FromSeconds(30); return _websocketHelper.CreateFromStream(_networkStream.Stream, isServer, _acceptedProtocol, keepAliveInterval.Value); } + + internal async Task RejectWebSocketAsync(CancellationToken cancellationToken) + { + var response = new WebContext + { + BodyContent = "User ID already connected" + }; + + response.Headers.Add("Connection", "close"); + response.Headers.Add("Content-Type", "text/plain"); + response.Headers.Add("Content-Length", response.BodyContent.Length.ToString()); + await SendWebSocketRejectResponse(response, cancellationToken); + } } \ No newline at end of file diff --git a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs index 0b058ec..bfad460 100644 --- a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs +++ b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs @@ -71,13 +71,14 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() { // Arrange using var server = new SimpleWebSocketServer(IPAddress.Any, 8010, _serverLogger.Object); - using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", _clientLogger.Object); + using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", logger: _clientLogger.Object); const string Message = "Hello World"; const string ClosingStatusDescription = "closing status test description"; string receivedMessage = string.Empty; string receivedClosingDescription = string.Empty; + string exceptionMessage = string.Empty; var messageResetEvent = new ManualResetEvent(false); var disconnectResetEvent = new ManualResetEvent(false); @@ -118,6 +119,25 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() await client.DisconnectAsync(ClosingStatusDescription); WaitForManualResetEventOrThrow(disconnectResetEvent, 100000); + var client2 = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", client.UserId, logger: _clientLogger.Object); + await client2.ConnectAsync(); + + await Task.Delay(1000); + try + { + var client3 = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", client.UserId, logger: _clientLogger.Object); + await client3.ConnectAsync(); + } + catch (Exception exception) + { + exceptionMessage = exception.InnerException!.Message; + } + + + await client2.SendMessageAsync("Hello World"); + await Task.Delay(1000); + + await server.ShutdownServer(CancellationToken.None); _logMessages.ForEach(m => Trace.WriteLine(m)); @@ -126,6 +146,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() { Assert.That(receivedMessage, Is.EqualTo(Message)); Assert.That(receivedClosingDescription, Is.EqualTo(ClosingStatusDescription)); + Assert.That(exceptionMessage, Does.Contain("User ID already connected")); }); } @@ -135,7 +156,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld2() { // Arrange using var server = new SimpleWebSocketServer(IPAddress.Any, 8010, _serverLogger.Object); - using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", _clientLogger.Object); + using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", logger: _clientLogger.Object); const string Message = "Hello World"; diff --git a/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs b/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs index f8ba27e..254ddeb 100644 --- a/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs +++ b/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs @@ -64,7 +64,7 @@ public async Task AcceptWebSocketAsync_ShouldSendUpgradeResponse(string hostname _mockNetworkStream.Setup(ns => ns.WriteAsync(It.IsAny(), It.IsAny())).Callback((buffer, ct) => { response = Encoding.UTF8.GetString(buffer); }); // Act - await _socketWrapper.AcceptWebSocketAsync(request, cancellationToken); + await _socketWrapper.AcceptWebSocketAsync(request, Guid.NewGuid().ToString(), cancellationToken); // Assert Assert.That(response, Does.Contain("HTTP/1.1 101 Switching Protocols")); @@ -90,7 +90,7 @@ public async Task AcceptWebSocketAsync_ShouldSendUpgradeResponseWithCorrectProto _mockNetworkStream.Setup(ns => ns.WriteAsync(It.IsAny(), It.IsAny())).Callback((buffer, ct) => { response = Encoding.UTF8.GetString(buffer); }); // Act - await _socketWrapper.AcceptWebSocketAsync(request, serverSubprotocol, cancellationToken); + await _socketWrapper.AcceptWebSocketAsync(request, Guid.NewGuid().ToString(), serverSubprotocol, cancellationToken); // Assert Assert.Multiple(() => From 985cb525bc42945d170283c0e722943495ab469f Mon Sep 17 00:00:00 2001 From: Christoph Jung Date: Sun, 29 Sep 2024 01:54:18 +0200 Subject: [PATCH 03/16] Fix merge issues --- Jung.SimpleWebSocket/Models/WebContext.cs | 11 +++++++---- Jung.SimpleWebSocket/SimpleWebSocketClient.cs | 1 + .../Wrappers/WebSocketUpgradeHandler.cs | 12 +----------- Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs | 16 +++++++--------- .../WebSocketUpgradeHandlerTests.cs | 4 ++-- 5 files changed, 18 insertions(+), 26 deletions(-) diff --git a/Jung.SimpleWebSocket/Models/WebContext.cs b/Jung.SimpleWebSocket/Models/WebContext.cs index ffa6f8c..04173cf 100644 --- a/Jung.SimpleWebSocket/Models/WebContext.cs +++ b/Jung.SimpleWebSocket/Models/WebContext.cs @@ -61,7 +61,7 @@ public partial class WebContext(string? content = null) private string? _bodyContent = null; /// - /// Gets the headers of the web request. + /// Gets the headers. /// public NameValueCollection Headers { @@ -72,6 +72,9 @@ public NameValueCollection Headers } } + /// + /// Gets or Sets the body content. + /// public string BodyContent { get @@ -374,7 +377,6 @@ public string RequestLine } /// -<<<<<<< HEAD /// Gets a value indicating whether the Content is empty. /// public bool IsEmpty => string.IsNullOrWhiteSpace(_content); @@ -395,7 +397,9 @@ public static string GetStatusDescription(HttpStatusCode statusCode) var enumName = Enum.GetName(statusCode) ?? throw new WebSocketUpgradeException("Status code is not a valid HttpStatusCode"); return string.Join(" ", _splitByUppercaseRegex.Split(enumName)); } -======= + + + /// /// Gets the user id of the web request. /// public string UserId @@ -411,7 +415,6 @@ public string UserId /// Gets a value indicating whether the web request contains a user id. /// public bool ContainsUserId => Headers["x-user-id"] != null; ->>>>>>> origin/handling-user-id-header /// /// Gets the content lines of the web request. diff --git a/Jung.SimpleWebSocket/SimpleWebSocketClient.cs b/Jung.SimpleWebSocket/SimpleWebSocketClient.cs index 7b362cf..2138cd6 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketClient.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketClient.cs @@ -22,6 +22,7 @@ namespace Jung.SimpleWebSocket /// The host name to connect to /// The port to connect to /// The web socket request path + /// The user id of the client. This is normally created by the server and sent back to the client /// A logger to write internal log messages public class SimpleWebSocketClient(string hostName, int port, string requestPath, string? userId = null, ILogger? logger = null) : IWebSocketClient, IDisposable { diff --git a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs index 80c907b..38f44f6 100644 --- a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs +++ b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs @@ -63,17 +63,7 @@ public async Task AwaitContextAsync(CancellationToken cancellationTo return context; } - public async Task AcceptWebSocketAsync(WebContext request, string userId, CancellationToken cancellationToken) - { - await AcceptWebSocketAsync(request, userId, null, cancellationToken); - } - - public async Task AcceptWebSocketAsync(WebContext request, string userId, string? subProtocol, CancellationToken cancellationToken) - { - await AcceptWebSocketAsync(request, new WebContext(), subProtocol, cancellationToken); - } - - public async Task AcceptWebSocketAsync(WebContext request, WebContext response, string? subProtocol, CancellationToken cancellationToken) + public async Task AcceptWebSocketAsync(WebContext request, WebContext response, string userId, string? subProtocol, CancellationToken cancellationToken) { try { diff --git a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs index 982f0d9..bdda98a 100644 --- a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs +++ b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs @@ -61,7 +61,7 @@ private void SetUpLogger(Mock> mock, string loggerName) var invokeMethod = formatter.GetType().GetMethod("Invoke"); var logMessage = invokeMethod!.Invoke(formatter, [state, exception]); - _logMessages.Add($"{loggerName}({logLevel}): {logMessage}"); + _logMessages.Add($"[{DateTime.Now:HH:mm:ss:fff}] {loggerName} ({logLevel}): {logMessage}"); })); } @@ -109,6 +109,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() server.ClientUpgradeRequestReceivedAsync += async (sender, args, cancellationToken) => { + // Get the IP address of the client var IpAddress = (args.Client.RemoteEndPoint as IPEndPoint)?.Address.ToString(); if (IpAddress == null) { @@ -121,8 +122,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() if (!isWhitelistedEndPoint) { args.ResponseContext.StatusCode = HttpStatusCode.Forbidden; - // not yet implemented - // args.ResponseContext.Body = ""; + args.ResponseContext.BodyContent = "Connection only possible via local network."; args.Handle = false; } args.Client.Properties["test"] = "test"; @@ -131,21 +131,22 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() // Act server.Start(); await client.ConnectAsync(); - WaitForManualResetEventOrThrow(connectResetEvent); await client.SendMessageAsync(Message); WaitForManualResetEventOrThrow(messageResetEvent); await client.DisconnectAsync(ClosingStatusDescription); - WaitForManualResetEventOrThrow(disconnectResetEvent, 100000); + WaitForManualResetEventOrThrow(disconnectResetEvent); + // test if the server accepts the client again var client2 = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", client.UserId, logger: _clientLogger.Object); await client2.ConnectAsync(); - await Task.Delay(1000); + await Task.Delay(100); try { + // test if two clients with the same user id can connect var client3 = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", client.UserId, logger: _clientLogger.Object); await client3.ConnectAsync(); } @@ -153,11 +154,8 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() { exceptionMessage = exception.InnerException!.Message; } - await client2.SendMessageAsync("Hello World"); - await Task.Delay(1000); - await server.ShutdownServer(CancellationToken.None); _logMessages.ForEach(m => Trace.WriteLine(m)); diff --git a/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs b/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs index 254ddeb..aa69078 100644 --- a/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs +++ b/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs @@ -64,7 +64,7 @@ public async Task AcceptWebSocketAsync_ShouldSendUpgradeResponse(string hostname _mockNetworkStream.Setup(ns => ns.WriteAsync(It.IsAny(), It.IsAny())).Callback((buffer, ct) => { response = Encoding.UTF8.GetString(buffer); }); // Act - await _socketWrapper.AcceptWebSocketAsync(request, Guid.NewGuid().ToString(), cancellationToken); + await _socketWrapper.AcceptWebSocketAsync(request,new WebContext(), Guid.NewGuid().ToString(), null, cancellationToken); // Assert Assert.That(response, Does.Contain("HTTP/1.1 101 Switching Protocols")); @@ -90,7 +90,7 @@ public async Task AcceptWebSocketAsync_ShouldSendUpgradeResponseWithCorrectProto _mockNetworkStream.Setup(ns => ns.WriteAsync(It.IsAny(), It.IsAny())).Callback((buffer, ct) => { response = Encoding.UTF8.GetString(buffer); }); // Act - await _socketWrapper.AcceptWebSocketAsync(request, Guid.NewGuid().ToString(), serverSubprotocol, cancellationToken); + await _socketWrapper.AcceptWebSocketAsync(request, new WebContext(), Guid.NewGuid().ToString(), serverSubprotocol, cancellationToken); // Assert Assert.Multiple(() => From 1ad02faac2f41a93dc468d92f7f3ce99a6518658 Mon Sep 17 00:00:00 2001 From: Christoph Jung Date: Sun, 29 Sep 2024 01:55:28 +0200 Subject: [PATCH 04/16] Add Expiring dictionary Add event for expired passive user --- .../Contracts/IWebSocketServer.cs | 5 + .../PassiveUserExpiredEventHandler.cs | 13 ++ .../Models/EventArguments/ItemExpiredArgs.cs | 10 + .../PassiveUserExpiredEventArgs.cs | 10 + .../Models/SimpleWebSocketServerOptions.cs | 31 +++ Jung.SimpleWebSocket/SimpleWebSocketServer.cs | 97 +++++---- .../Utility/ExpiringDictionary.cs | 191 ++++++++++++++++++ .../SimpleWebSocketTest.cs | 72 ++++++- 8 files changed, 388 insertions(+), 41 deletions(-) create mode 100644 Jung.SimpleWebSocket/Delegates/PassiveUserExpiredEventHandler.cs create mode 100644 Jung.SimpleWebSocket/Models/EventArguments/ItemExpiredArgs.cs create mode 100644 Jung.SimpleWebSocket/Models/EventArguments/PassiveUserExpiredEventArgs.cs create mode 100644 Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs create mode 100644 Jung.SimpleWebSocket/Utility/ExpiringDictionary.cs diff --git a/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs b/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs index 25e5918..464198f 100644 --- a/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs +++ b/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs @@ -59,6 +59,11 @@ public interface IWebSocketServer : IDisposable /// event ClientBinaryMessageReceivedEventHandler? BinaryMessageReceived; + /// + /// Occurs when an passive user expired. + /// + event PassiveUserExpiredEventHandler? PassiveUserExpiredEvent; + /// /// Async Event that is raised when a client upgrade request is received. /// diff --git a/Jung.SimpleWebSocket/Delegates/PassiveUserExpiredEventHandler.cs b/Jung.SimpleWebSocket/Delegates/PassiveUserExpiredEventHandler.cs new file mode 100644 index 0000000..de46577 --- /dev/null +++ b/Jung.SimpleWebSocket/Delegates/PassiveUserExpiredEventHandler.cs @@ -0,0 +1,13 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using Jung.SimpleWebSocket.Models.EventArguments; + +namespace Jung.SimpleWebSocket.Delegates; + +/// +/// The event handler for the passive user expired event. +/// +/// The sender of the event. +/// The arguments of the event. +public delegate void PassiveUserExpiredEventHandler(object sender, PassiveUserExpiredArgs e); \ No newline at end of file diff --git a/Jung.SimpleWebSocket/Models/EventArguments/ItemExpiredArgs.cs b/Jung.SimpleWebSocket/Models/EventArguments/ItemExpiredArgs.cs new file mode 100644 index 0000000..f2b78be --- /dev/null +++ b/Jung.SimpleWebSocket/Models/EventArguments/ItemExpiredArgs.cs @@ -0,0 +1,10 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +namespace Jung.SimpleWebSocket.Models.EventArguments; + +/// +/// Represents the arguments of the event when an item is expired. +/// +/// The item that is expired. +public record ItemExpiredArgs(TValue Item); \ No newline at end of file diff --git a/Jung.SimpleWebSocket/Models/EventArguments/PassiveUserExpiredEventArgs.cs b/Jung.SimpleWebSocket/Models/EventArguments/PassiveUserExpiredEventArgs.cs new file mode 100644 index 0000000..19cd99a --- /dev/null +++ b/Jung.SimpleWebSocket/Models/EventArguments/PassiveUserExpiredEventArgs.cs @@ -0,0 +1,10 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +namespace Jung.SimpleWebSocket.Models.EventArguments; + +/// +/// Represents the arguments of the event when a passive user expired. +/// +/// The identifier of the user that expired. +public record PassiveUserExpiredArgs(string ClientId); \ No newline at end of file diff --git a/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs b/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs new file mode 100644 index 0000000..2feacc8 --- /dev/null +++ b/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs @@ -0,0 +1,31 @@ +using System.Net; + +namespace Jung.SimpleWebSocket.Models +{ + /// + /// Represents the options for the SimpleWebSocketServer. + /// + public class SimpleWebSocketServerOptions + { + /// + /// Gets or sets the local IP address of the server. + /// + public IPAddress LocalIpAddress { get; set; } = IPAddress.Any; + + + /// + /// Gets or sets the port of the server. + /// + public int Port { get; set; } + + /// + /// Gets or sets the state of the user handling. + /// + public bool ActivateUserHandling { get; set; } = false; + + /// + /// The time after which a passive client is removed from the passive client list. + /// + public TimeSpan PassiveClientLifetime { get; set; } = TimeSpan.FromMinutes(1); + } +} diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index bf5c27a..5e7e44e 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -34,6 +34,8 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable public event ClientMessageReceivedEventHandler? MessageReceived; /// public event ClientBinaryMessageReceivedEventHandler? BinaryMessageReceived; + /// + public event PassiveUserExpiredEventHandler? PassiveUserExpiredEvent; /// public event AsyncEventHandler? ClientUpgradeRequestReceivedAsync; @@ -43,17 +45,20 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable /// private ConcurrentDictionary ActiveClients { get; } = []; + /// + /// A dictionary of passive clients. + /// + /// + /// The passive clients get removed after a certain time of inactivity. + /// + private ExpiringDictionary PassiveClients { get; } + /// public string[] ClientIds => [.. ActiveClients.Keys]; /// public int ClientCount => ActiveClients.Count; - /// - /// Future: Handle passive (disconnected) clients, delete them after a period of time, configurate this behavior in the WebSocketServerOptions - /// - private ConcurrentDictionary PassiveClients { get; } = []; - /// public bool IsListening => _server?.IsListening ?? false; @@ -86,29 +91,27 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable /// Initializes a new instance of the class that listens /// for incoming connection attempts on the specified local IP address and port number. /// - /// A local ip address - /// A port on which to listen for incoming connection attempts + /// The options for the server /// A logger to write internal log messages - public SimpleWebSocketServer(IPAddress localIpAddress, int port, ILogger? logger = null) + public SimpleWebSocketServer(SimpleWebSocketServerOptions options, ILogger? logger = null) { - LocalIpAddress = localIpAddress; - Port = port; + LocalIpAddress = options.LocalIpAddress; + Port = options.Port; _logger = logger; + PassiveClients = new(options.PassiveClientLifetime, _logger); + PassiveClients.ItemExpired += PassiveClients_ItemExpired; } /// /// Constructor for dependency injection (used in tests) /// - /// A local ip address - /// A port on which to listen for incoming connection attempts + /// The options for the server /// A wrapped tcp listener /// >A logger to write internal log messages - internal SimpleWebSocketServer(IPAddress localIpAddress, int port, ITcpListener tcpListener, ILogger? logger = null) + internal SimpleWebSocketServer(SimpleWebSocketServerOptions options, ITcpListener tcpListener, ILogger? logger = null) + : this(options, logger) { - LocalIpAddress = localIpAddress; - Port = port; _server = tcpListener; - _logger = logger; } /// @@ -224,8 +227,8 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT { // Upgrade the connection to a WebSocket using var stream = client.ClientConnection!.GetStream(); - var socketWrapper = new WebSocketUpgradeHandler(stream); - var request = await socketWrapper.AwaitContextAsync(cancellationToken); + var upgradeHandler = new WebSocketUpgradeHandler(stream); + var request = await upgradeHandler.AwaitContextAsync(cancellationToken); // Check if the request contains a user id if (request.ContainsUserId) @@ -243,7 +246,7 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT var passiveClient = PassiveClients[request.UserId]; passiveClient.UpdateClient(client.ClientConnection); client = passiveClient; - PassiveClients.TryRemove(request.UserId, out _); + PassiveClients.Remove(request.UserId); } else { @@ -252,7 +255,7 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT { _logger?.LogDebug("Active client found for user id {userId} - rejecting connection.", request.UserId); // Reject the connection - await socketWrapper.RejectWebSocketAsync(cancellationToken); + await upgradeHandler.RejectWebSocketAsync(cancellationToken); return; } else @@ -263,30 +266,32 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT } } - // raise async client upgrade request received event + // raise async client upgrade request received event var eventArgs = new ClientUpgradeRequestReceivedArgs(client, request, _logger); await AsyncEventRaiser.RaiseAsync(ClientUpgradeRequestReceivedAsync, this, eventArgs, cancellationToken); - if (!eventArgs.Handle) + if (eventArgs.Handle) { - _logger?.LogDebug("Client upgrade request rejected by ClientUpgradeRequestReceivedAsync event."); - // send rejection response - return; - } - - await socketWrapper.AcceptWebSocketAsync(request, client.Id, cancellationToken); + // The client is accepted + await upgradeHandler.AcceptWebSocketAsync(request, eventArgs.ResponseContext, client.Id, null, cancellationToken); - // Update the client with the new WebSocket - client.UseWebSocket(socketWrapper.CreateWebSocket(isServer: true)); + // Update the client with the new WebSocket + client.UseWebSocket(upgradeHandler.CreateWebSocket(isServer: true)); - clientAdded = ActiveClients.TryAdd(client.Id, client); - if (clientAdded) + clientAdded = ActiveClients.TryAdd(client.Id, client); + if (clientAdded) + { + _ = Task.Run(() => ClientConnected?.Invoke(this, new ClientConnectedArgs(client.Id)), cancellationToken); + // Start listening for messages + _logger?.LogDebug("Connection upgraded, now listening on client {clientId}", client.Id); + await ProcessWebSocketMessagesAsync(client, cancellationToken); + } + } + else { - _ = Task.Run(() => ClientConnected?.Invoke(this, new ClientConnectedArgs(client.Id)), cancellationToken); - // Start listening for messages - _logger?.LogDebug("Connection upgraded, now listening on client {clientId}", client.Id); - await ProcessWebSocketMessagesAsync(client, cancellationToken); + // The client is rejected + _logger?.LogDebug("Client upgrade request rejected by ClientUpgradeRequestReceivedAsync event."); + await upgradeHandler.RejectWebSocketAsync(cancellationToken); } - } catch (OperationCanceledException) { @@ -311,7 +316,9 @@ private void HandleDisconnectedClient(WebSocketServerClient client) { ActiveClients.TryRemove(client.Id, out _); client.Dispose(); - PassiveClients.TryAdd(client.Id, client); + + _logger?.LogDebug("Client {clientId} is now a passive user.", client.Id); + PassiveClients.Add(client.Id, client); } /// @@ -362,6 +369,20 @@ private async Task ProcessWebSocketMessagesAsync(WebSocketServerClient client, C } } + /// + /// Handles the event when a passive user expired. + /// + /// The sender of the event () + /// The arguments of the event + private void PassiveClients_ItemExpired(object? sender, ItemExpiredArgs e) + { + _logger?.LogDebug("Passive client expired: {clientId}", e.Item.Id); + + // Raise the event asynchronously + // We don't want to block the cleanup process + Task.Run(() => PassiveUserExpiredEvent?.Invoke(this, new PassiveUserExpiredArgs(e.Item.Id))); + } + /// public void Dispose() { diff --git a/Jung.SimpleWebSocket/Utility/ExpiringDictionary.cs b/Jung.SimpleWebSocket/Utility/ExpiringDictionary.cs new file mode 100644 index 0000000..b21f4ac --- /dev/null +++ b/Jung.SimpleWebSocket/Utility/ExpiringDictionary.cs @@ -0,0 +1,191 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using Jung.SimpleWebSocket.Models.EventArguments; +using Microsoft.Extensions.Logging; + +namespace Jung.SimpleWebSocket.Utility; + +/// +/// Creates a dictionary with expiration time for each item. +/// +/// The type of the keys in the dictionary. +/// The type of the values in the dictionary. +/// The expiration time for each item. +/// The logger to log exceptions. +public class ExpiringDictionary(TimeSpan expiration, ILogger? logger = null) where TKey : class +{ + + /// + /// Occurs when an item is expired. + /// + public event EventHandler>? ItemExpired; + + private readonly SortedList _expirationQueue = []; + private readonly Dictionary _dictionary = []; + + private bool _cleanupInProgress = false; + + /// + /// Add the specified key and value to the dictionary. + /// + /// + /// + public void Add(TKey key, TValue value) + { + lock (_dictionary) + { + // Add the item to the dictionary + _dictionary[key] = value; + + // Add item with expiration time to the queue + var expirationTime = DateTime.Now.Add(expiration); + + lock (_expirationQueue) + { + _expirationQueue.Add(expirationTime, key); + } + } + + // Trigger cleanup after the Add operation is done + lock (_expirationQueue) + { + if (!_cleanupInProgress) + { + _cleanupInProgress = true; + + // Run cleanup asynchronously + CleanupExpiredItems().ContinueWith(t => + { + if (t.IsFaulted) + { + // Handle exceptions here + logger?.LogError(t.Exception, "An Exception occurred during cleanup expired items."); + } + }); + } + } + } + + /// + /// Determines whether the dictionary contains the specified key. + /// + /// + /// + public bool ContainsKey(TKey key) + { + lock (_dictionary) + { + return _dictionary.ContainsKey(key); + } + } + /// + /// Removes the value with the specified key from the dictionary. + /// + /// The key of the value to remove. + /// Returns true if the element is successfully found and removed; otherwise, false. + public bool Remove(TKey key) + { + lock (_dictionary) + { + if (_dictionary.Remove(key)) + { + lock (_expirationQueue) + { + // Find and remove the expiration time entry for this key + var expirationTime = _expirationQueue.FirstOrDefault(x => x.Value.Equals(key)).Key; + if (expirationTime != default) + { + _expirationQueue.Remove(expirationTime); + } + } + return true; + } + } + return false; + } + + /// + /// Get or set the value associated with the specified key. + /// + /// The key of the value to get or set. + /// The value associated with the specified key. + public TValue this[TKey key] + { + get + { + lock (_dictionary) + { + return _dictionary[key]; + } + } + set + { + lock (_dictionary) + { + _dictionary[key] = value; + var expirationTime = DateTime.Now.Add(expiration); + lock (_expirationQueue) + { + _expirationQueue[expirationTime] = key; + } + } + } + } + + /// + /// Cleans up expired items. + /// + /// A task that represents the asynchronous cleanup operation. + private async Task CleanupExpiredItems() + { + while (true) + { + DateTime nearestExpiration; + TKey expiredKey; + + // Safely lock and retrieve the first item to expire + lock (_expirationQueue) + { + // If there are no items, stop the cleanup process + if (_expirationQueue.Count == 0) + { + _cleanupInProgress = false; + return; + } + + // Get the first key in expiration queue (FIFO order) + var firstItem = _expirationQueue.First(); + nearestExpiration = firstItem.Key; + expiredKey = firstItem.Value; + } + + // Calculate the delay based on the expiration time + TimeSpan delay = nearestExpiration - DateTime.Now; + + if (delay > TimeSpan.Zero) + { + await Task.Delay(delay); + } + + // Remove the expired item from the dictionary + lock (_dictionary) + { + if (_dictionary.TryGetValue(expiredKey, out var expiredItem)) + { + ItemExpired?.Invoke(this, new ItemExpiredArgs(expiredItem)); + _dictionary.Remove(expiredKey); + } + } + + // Remove from expiration queue, but only if it's still the correct key + lock (_expirationQueue) + { + if (_expirationQueue.Count > 0 && _expirationQueue.First().Value.Equals(expiredKey)) + { + _expirationQueue.RemoveAt(0); // Safely remove the correct item + } + } + } + } +} \ No newline at end of file diff --git a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs index bdda98a..13920cf 100644 --- a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs +++ b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs @@ -2,6 +2,7 @@ // The project is licensed under the MIT license. using Jung.SimpleWebSocket; +using Jung.SimpleWebSocket.Models; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; @@ -70,7 +71,15 @@ private void SetUpLogger(Mock> mock, string loggerName) public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() { // Arrange - using var server = new SimpleWebSocketServer(IPAddress.Any, 8010, _serverLogger.Object); + var serverOptions = new SimpleWebSocketServerOptions + { + LocalIpAddress = IPAddress.Any, + Port = 8010, + ActivateUserHandling = true, + PassiveClientLifetime = TimeSpan.FromSeconds(5) + }; + + using var server = new SimpleWebSocketServer(serverOptions, _serverLogger.Object); using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", logger: _clientLogger.Object); @@ -169,6 +178,47 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() }); } + [Test] + [Platform("Windows7,Windows8,Windows8.1,Windows10", Reason = "This test establishes a TCP client-server connection using SimpleWebSocket, which relies on specific networking features and behaviors that are only available and consistent on Windows platforms. Running this test on non-Windows platforms could lead to inconsistent results or failures due to differences in networking stack implementations.")] + public async Task TestClientServerConnection_ShouldRemoveClientFromPassiveClients() + { + // Arrange + string userId = Guid.NewGuid().ToString(); + var serverOptions = new SimpleWebSocketServerOptions + { + LocalIpAddress = IPAddress.Any, + Port = 8010, + ActivateUserHandling = true, + PassiveClientLifetime = TimeSpan.FromSeconds(1) + }; + + using var server = new SimpleWebSocketServer(serverOptions, _serverLogger.Object); + using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", userId, _clientLogger.Object); + + var expiredClientId = string.Empty; + var expiredClientResetEvent = new ManualResetEvent(false); + + server.PassiveUserExpiredEvent += (sender, args) => + { + expiredClientId = args.ClientId; + expiredClientResetEvent.Set(); + }; + + // Act + server.Start(); + await client.ConnectAsync(); + await Task.Delay(100); + await client.DisconnectAsync(); + + WaitForManualResetEventOrThrow(expiredClientResetEvent, 2000); + + await server.ShutdownServer(CancellationToken.None); + _logMessages.ForEach(m => Trace.WriteLine(m)); + + // Assert + Assert.That(expiredClientId, Is.EqualTo(userId)); + } + /// /// Fake Async method to simulate a database call to check if the IP address is in the database. /// @@ -186,7 +236,15 @@ private static async Task DbContext_IpAddresses_Contains(string ipAddress, public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld2() { // Arrange - using var server = new SimpleWebSocketServer(IPAddress.Any, 8010, _serverLogger.Object); + var serverOptions = new SimpleWebSocketServerOptions + { + LocalIpAddress = IPAddress.Any, + Port = 8010, + ActivateUserHandling = true, + PassiveClientLifetime = TimeSpan.FromSeconds(1) + }; + + using var server = new SimpleWebSocketServer(serverOptions, _serverLogger.Object); using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", logger: _clientLogger.Object); @@ -249,7 +307,15 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld2() public async Task TestMultipleClientServerConnection_ShouldSendAndReceiveHelloWorld() { // Arrange - using var server = new SimpleWebSocketServer(IPAddress.Any, 8010); + var serverOptions = new SimpleWebSocketServerOptions + { + LocalIpAddress = IPAddress.Any, + Port = 8010, + ActivateUserHandling = true, + PassiveClientLifetime = TimeSpan.FromSeconds(1) + }; + + using var server = new SimpleWebSocketServer(serverOptions); List clients = []; var message = "Hello World"; const int clientsCount = 200; From 411e4992a285815cfcb62c4bc4f226fdaba246da Mon Sep 17 00:00:00 2001 From: Christoph Jung Date: Sun, 29 Sep 2024 02:09:32 +0200 Subject: [PATCH 05/16] Use individual messages when rejecting an upgrade request --- Jung.SimpleWebSocket/SimpleWebSocketServer.cs | 10 +++++-- .../Wrappers/WebSocketUpgradeHandler.cs | 7 +---- .../SimpleWebSocketTest.cs | 28 ++++++++++--------- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index 5e7e44e..d95d39a 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -255,7 +255,13 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT { _logger?.LogDebug("Active client found for user id {userId} - rejecting connection.", request.UserId); // Reject the connection - await upgradeHandler.RejectWebSocketAsync(cancellationToken); + + var responseContext = new WebContext + { + StatusCode = HttpStatusCode.Conflict, + BodyContent = "User id already in use" + }; + await upgradeHandler.RejectWebSocketAsync(responseContext, cancellationToken); return; } else @@ -290,7 +296,7 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT { // The client is rejected _logger?.LogDebug("Client upgrade request rejected by ClientUpgradeRequestReceivedAsync event."); - await upgradeHandler.RejectWebSocketAsync(cancellationToken); + await upgradeHandler.RejectWebSocketAsync(eventArgs.ResponseContext, cancellationToken); } } catch (OperationCanceledException) diff --git a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs index 38f44f6..3c064ef 100644 --- a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs +++ b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs @@ -299,13 +299,8 @@ internal IWebSocket CreateWebSocket(bool isServer, TimeSpan? keepAliveInterval = return _websocketHelper.CreateFromStream(_networkStream.Stream, isServer, _acceptedProtocol, keepAliveInterval.Value); } - internal async Task RejectWebSocketAsync(CancellationToken cancellationToken) + internal async Task RejectWebSocketAsync(WebContext response, CancellationToken cancellationToken) { - var response = new WebContext - { - BodyContent = "User ID already connected" - }; - response.Headers.Add("Connection", "close"); response.Headers.Add("Content-Type", "text/plain"); response.Headers.Add("Content-Length", response.BodyContent.Length.ToString()); diff --git a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs index 13920cf..5e0fb4d 100644 --- a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs +++ b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs @@ -174,10 +174,24 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() { Assert.That(receivedMessage, Is.EqualTo(Message)); Assert.That(receivedClosingDescription, Is.EqualTo(ClosingStatusDescription)); - Assert.That(exceptionMessage, Does.Contain("User ID already connected")); + Assert.That(exceptionMessage, Does.Contain("User id already in use")); }); } + + /// + /// Fake Async method to simulate a database call to check if the IP address is in the database. + /// + /// The IP address to check. + /// The cancellation token. + /// A task that represents the asynchronous operation. The task result contains a value indicating whether the IP address is in the database. + private static async Task DbContext_IpAddresses_Contains(string ipAddress, CancellationToken cancellationToken) + { + await Task.Delay(100, cancellationToken); + return ipAddress == IPAddress.Loopback.ToString(); + } + + [Test] [Platform("Windows7,Windows8,Windows8.1,Windows10", Reason = "This test establishes a TCP client-server connection using SimpleWebSocket, which relies on specific networking features and behaviors that are only available and consistent on Windows platforms. Running this test on non-Windows platforms could lead to inconsistent results or failures due to differences in networking stack implementations.")] public async Task TestClientServerConnection_ShouldRemoveClientFromPassiveClients() @@ -219,18 +233,6 @@ public async Task TestClientServerConnection_ShouldRemoveClientFromPassiveClient Assert.That(expiredClientId, Is.EqualTo(userId)); } - /// - /// Fake Async method to simulate a database call to check if the IP address is in the database. - /// - /// The IP address to check. - /// The cancellation token. - /// A task that represents the asynchronous operation. The task result contains a value indicating whether the IP address is in the database. - private static async Task DbContext_IpAddresses_Contains(string ipAddress, CancellationToken cancellationToken) - { - await Task.Delay(100, cancellationToken); - return ipAddress == IPAddress.Loopback.ToString(); - } - [Test] [Platform("Windows7,Windows8,Windows8.1,Windows10", Reason = "This test establishes a TCP client-server connection using SimpleWebSocket, which relies on specific networking features and behaviors that are only available and consistent on Windows platforms. Running this test on non-Windows platforms could lead to inconsistent results or failures due to differences in networking stack implementations.")] public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld2() From 2e3f3a727ef5fdcfd0d9b2339251f66b9181f47b Mon Sep 17 00:00:00 2001 From: Christoph Jung Date: Mon, 30 Sep 2024 19:52:52 +0200 Subject: [PATCH 06/16] Use IDictionary for passive users Add switch for remembering disconnected clients Add switch for removing passive users after the end of their lifetime --- .../Models/SimpleWebSocketServerOptions.cs | 21 ++- Jung.SimpleWebSocket/SimpleWebSocketServer.cs | 141 +++++++++++++----- .../Utility/ExpiringDictionary.cs | 94 +++++++++++- .../SimpleWebSocketTest.cs | 20 +-- 4 files changed, 217 insertions(+), 59 deletions(-) diff --git a/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs b/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs index 2feacc8..7e5a486 100644 --- a/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs +++ b/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs @@ -12,20 +12,33 @@ public class SimpleWebSocketServerOptions /// public IPAddress LocalIpAddress { get; set; } = IPAddress.Any; - /// /// Gets or sets the port of the server. /// public int Port { get; set; } /// - /// Gets or sets the state of the user handling. + /// Switch for remembering disconnected clients. + /// + /// + /// If true the server will put disconnected clients into a passive client list. + /// This clients can reidentify themselves with their user id. + /// + public bool RememberDisconnectedClients { get; set; } = false; + + /// + /// Switch for removing passive clients after the end of the . + /// + public bool RemovePassiveClientsAfterClientExpirationTime { get; set; } = false; + + /// + /// Switch for sending the user id to the client. /// - public bool ActivateUserHandling { get; set; } = false; + public bool SendUserIdToClient { get; set; } = false; /// /// The time after which a passive client is removed from the passive client list. /// public TimeSpan PassiveClientLifetime { get; set; } = TimeSpan.FromMinutes(1); } -} +} \ No newline at end of file diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index d95d39a..5861ea9 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -9,6 +9,7 @@ using Jung.SimpleWebSocket.Utility; using Jung.SimpleWebSocket.Wrappers; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using System.Collections.Concurrent; using System.Net; using System.Net.WebSockets; @@ -48,10 +49,7 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable /// /// A dictionary of passive clients. /// - /// - /// The passive clients get removed after a certain time of inactivity. - /// - private ExpiringDictionary PassiveClients { get; } + private IDictionary PassiveClients { get; set; } = null!; /// public string[] ClientIds => [.. ActiveClients.Keys]; @@ -87,6 +85,11 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable /// private readonly ILogger? _logger; + /// + /// The options for the server. + /// + private readonly SimpleWebSocketServerOptions _options; + /// /// Initializes a new instance of the class that listens /// for incoming connection attempts on the specified local IP address and port number. @@ -98,8 +101,46 @@ public SimpleWebSocketServer(SimpleWebSocketServerOptions options, ILogger? logg LocalIpAddress = options.LocalIpAddress; Port = options.Port; _logger = logger; - PassiveClients = new(options.PassiveClientLifetime, _logger); - PassiveClients.ItemExpired += PassiveClients_ItemExpired; + _options = options; + InitializePassiveClientDictionary(options); + } + + /// + /// Initializes the passive clients dictionary. + /// + /// + private void InitializePassiveClientDictionary(SimpleWebSocketServerOptions options) + { + if (options.RememberDisconnectedClients) + { + // Initialize the passive clients dictionary + if (options.RemovePassiveClientsAfterClientExpirationTime) + { + var passiveClients = new ExpiringDictionary(options.PassiveClientLifetime, _logger); + passiveClients.ItemExpired += PassiveClients_ItemExpired; + PassiveClients = passiveClients; + } + else + { + PassiveClients = new Dictionary(); + } + } + else + { + // If user handling is not activated, the passive clients are not needed + PassiveClients = null!; + } + } + + /// + /// Initializes a new instance of the class that listens + /// for incoming connection attempts on the specified local IP address and port number. + /// + /// The options for the server + /// A logger to write internal log messages + public SimpleWebSocketServer(IOptions options, ILogger? logger = null) + : this(options.Value, logger) + { } /// @@ -230,44 +271,48 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT var upgradeHandler = new WebSocketUpgradeHandler(stream); var request = await upgradeHandler.AwaitContextAsync(cancellationToken); - // Check if the request contains a user id - if (request.ContainsUserId) + // Check if disconnected clients are remembered and can be reactivated + if (_options.RememberDisconnectedClients) { - _logger?.LogDebug("User id found in request: {userId}", request.UserId); - // Check if the client is an existing passive client - var clientExists = PassiveClients.ContainsKey(request.UserId); - if (clientExists) - { - _logger?.LogDebug("Passive client found for user id {userId} - reactivating user.", request.UserId); - - // Use the existing client - // Update the client with the new connection - // Remove the client from the passive clients - var passiveClient = PassiveClients[request.UserId]; - passiveClient.UpdateClient(client.ClientConnection); - client = passiveClient; - PassiveClients.Remove(request.UserId); - } - else + // Check if the request contains a user id + if (request.ContainsUserId) { - // No passive client found, checking for active clients with the same id - if (ActiveClients.ContainsKey(request.UserId)) + _logger?.LogDebug("User id found in request: {userId}", request.UserId); + // Check if the client is an existing passive client + var clientExists = PassiveClients.ContainsKey(request.UserId); + if (clientExists) { - _logger?.LogDebug("Active client found for user id {userId} - rejecting connection.", request.UserId); - // Reject the connection - - var responseContext = new WebContext - { - StatusCode = HttpStatusCode.Conflict, - BodyContent = "User id already in use" - }; - await upgradeHandler.RejectWebSocketAsync(responseContext, cancellationToken); - return; + _logger?.LogDebug("Passive client found for user id {userId} - reactivating user.", request.UserId); + + // Use the existing client + // Update the client with the new connection + // Remove the client from the passive clients + var passiveClient = PassiveClients[request.UserId]; + passiveClient.UpdateClient(client.ClientConnection); + client = passiveClient; + PassiveClients.Remove(request.UserId); } else { - // Update the client with the new id - client.UpdateId(request.UserId); + // No passive client found, checking for active clients with the same id + if (ActiveClients.ContainsKey(request.UserId)) + { + _logger?.LogDebug("Active client found for user id {userId} - rejecting connection.", request.UserId); + // Reject the connection + + var responseContext = new WebContext + { + StatusCode = HttpStatusCode.Conflict, + BodyContent = "User id already in use" + }; + await upgradeHandler.RejectWebSocketAsync(responseContext, cancellationToken); + return; + } + else + { + // Update the client with the new id + client.UpdateId(request.UserId); + } } } } @@ -280,7 +325,7 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT // The client is accepted await upgradeHandler.AcceptWebSocketAsync(request, eventArgs.ResponseContext, client.Id, null, cancellationToken); - // Update the client with the new WebSocket + // Use the websocket for the client client.UseWebSocket(upgradeHandler.CreateWebSocket(isServer: true)); clientAdded = ActiveClients.TryAdd(client.Id, client); @@ -318,13 +363,24 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT } } + /// + /// Handles the disconnected client. + /// + /// private void HandleDisconnectedClient(WebSocketServerClient client) { ActiveClients.TryRemove(client.Id, out _); client.Dispose(); - _logger?.LogDebug("Client {clientId} is now a passive user.", client.Id); - PassiveClients.Add(client.Id, client); + if (_options.RememberDisconnectedClients) + { + _logger?.LogDebug("Client {clientId} is now a passive user.", client.Id); + PassiveClients.Add(client.Id, client); + } + else + { + _logger?.LogDebug("Client {clientId} is removed.", client.Id); + } } /// @@ -378,6 +434,9 @@ private async Task ProcessWebSocketMessagesAsync(WebSocketServerClient client, C /// /// Handles the event when a passive user expired. /// + /// + /// Condition: is set to true. + /// /// The sender of the event () /// The arguments of the event private void PassiveClients_ItemExpired(object? sender, ItemExpiredArgs e) diff --git a/Jung.SimpleWebSocket/Utility/ExpiringDictionary.cs b/Jung.SimpleWebSocket/Utility/ExpiringDictionary.cs index b21f4ac..52d3131 100644 --- a/Jung.SimpleWebSocket/Utility/ExpiringDictionary.cs +++ b/Jung.SimpleWebSocket/Utility/ExpiringDictionary.cs @@ -3,6 +3,8 @@ using Jung.SimpleWebSocket.Models.EventArguments; using Microsoft.Extensions.Logging; +using System.Collections; +using System.Diagnostics.CodeAnalysis; namespace Jung.SimpleWebSocket.Utility; @@ -13,7 +15,7 @@ namespace Jung.SimpleWebSocket.Utility; /// The type of the values in the dictionary. /// The expiration time for each item. /// The logger to log exceptions. -public class ExpiringDictionary(TimeSpan expiration, ILogger? logger = null) where TKey : class +public class ExpiringDictionary(TimeSpan expiration, ILogger? logger = null): IDictionary where TKey : class { /// @@ -24,7 +26,7 @@ public class ExpiringDictionary(TimeSpan expiration, ILogger? logg private readonly SortedList _expirationQueue = []; private readonly Dictionary _dictionary = []; - private bool _cleanupInProgress = false; + private bool _cleanupInProgress = false; /// /// Add the specified key and value to the dictionary. @@ -188,4 +190,92 @@ private async Task CleanupExpiredItems() } } } + + #region NotImplemented + + /// + /// Not implemented. + /// + public ICollection Keys => throw new NotImplementedException(); + + /// + /// Not implemented. + /// + public ICollection Values => throw new NotImplementedException(); + + /// + /// Not implemented. + /// + public int Count => throw new NotImplementedException(); + + /// + /// Not implemented. + /// + public bool IsReadOnly => throw new NotImplementedException(); + + /// + /// Not implemented. + /// + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + throw new NotImplementedException(); + } + + /// + /// Not implemented. + /// + public void Add(KeyValuePair item) + { + throw new NotImplementedException(); + } + + /// + /// Not implemented. + /// + public void Clear() + { + throw new NotImplementedException(); + } + + /// + /// Not implemented. + /// + public bool Contains(KeyValuePair item) + { + throw new NotImplementedException(); + } + + /// + /// Not implemented. + /// + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + /// + /// Not implemented. + /// + public bool Remove(KeyValuePair item) + { + throw new NotImplementedException(); + } + + /// + /// Not implemented. + /// + public IEnumerator> GetEnumerator() + { + throw new NotImplementedException(); + } + + /// + /// Not implemented. + /// + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } + + #endregion } \ No newline at end of file diff --git a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs index 5e0fb4d..a4209dc 100644 --- a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs +++ b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs @@ -75,8 +75,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() { LocalIpAddress = IPAddress.Any, Port = 8010, - ActivateUserHandling = true, - PassiveClientLifetime = TimeSpan.FromSeconds(5) + RememberDisconnectedClients = true, }; using var server = new SimpleWebSocketServer(serverOptions, _serverLogger.Object); @@ -119,7 +118,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() server.ClientUpgradeRequestReceivedAsync += async (sender, args, cancellationToken) => { // Get the IP address of the client - var IpAddress = (args.Client.RemoteEndPoint as IPEndPoint)?.Address.ToString(); + var IpAddress = (args.Client.RemoteEndPoint as IPEndPoint)?.Address; if (IpAddress == null) { args.Handle = false; @@ -185,10 +184,10 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() /// The IP address to check. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains a value indicating whether the IP address is in the database. - private static async Task DbContext_IpAddresses_Contains(string ipAddress, CancellationToken cancellationToken) + private static async Task DbContext_IpAddresses_Contains(IPAddress ipAddress, CancellationToken cancellationToken) { await Task.Delay(100, cancellationToken); - return ipAddress == IPAddress.Loopback.ToString(); + return ipAddress.Equals(IPAddress.Loopback); } @@ -202,7 +201,8 @@ public async Task TestClientServerConnection_ShouldRemoveClientFromPassiveClient { LocalIpAddress = IPAddress.Any, Port = 8010, - ActivateUserHandling = true, + RememberDisconnectedClients = true, + RemovePassiveClientsAfterClientExpirationTime = true, PassiveClientLifetime = TimeSpan.FromSeconds(1) }; @@ -241,9 +241,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld2() var serverOptions = new SimpleWebSocketServerOptions { LocalIpAddress = IPAddress.Any, - Port = 8010, - ActivateUserHandling = true, - PassiveClientLifetime = TimeSpan.FromSeconds(1) + Port = 8010 }; using var server = new SimpleWebSocketServer(serverOptions, _serverLogger.Object); @@ -312,9 +310,7 @@ public async Task TestMultipleClientServerConnection_ShouldSendAndReceiveHelloWo var serverOptions = new SimpleWebSocketServerOptions { LocalIpAddress = IPAddress.Any, - Port = 8010, - ActivateUserHandling = true, - PassiveClientLifetime = TimeSpan.FromSeconds(1) + Port = 8010 }; using var server = new SimpleWebSocketServer(serverOptions); From ba4809dea53e724a5d9ff9110e935b2a2c9ac37a Mon Sep 17 00:00:00 2001 From: Christoph Jung Date: Mon, 30 Sep 2024 21:40:33 +0200 Subject: [PATCH 07/16] Add client handling flow --- Jung.SimpleWebSocket/ClientHandlingFlow.cs | 184 ++++++++++++++++++ Jung.SimpleWebSocket/SimpleWebSocketServer.cs | 162 +++++---------- .../UserNotHandledException.cs | 13 ++ .../Wrappers/WebSocketUpgradeHandler.cs | 4 +- 4 files changed, 249 insertions(+), 114 deletions(-) create mode 100644 Jung.SimpleWebSocket/ClientHandlingFlow.cs create mode 100644 Jung.SimpleWebSocket/UserNotHandledException.cs diff --git a/Jung.SimpleWebSocket/ClientHandlingFlow.cs b/Jung.SimpleWebSocket/ClientHandlingFlow.cs new file mode 100644 index 0000000..221e78a --- /dev/null +++ b/Jung.SimpleWebSocket/ClientHandlingFlow.cs @@ -0,0 +1,184 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using Jung.SimpleWebSocket.Models; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; +using System.Net; + +namespace Jung.SimpleWebSocket +{ + /// + /// A flow that handles the client connection. + /// + /// + /// Creates a new instance of the class. + /// + /// The client to handle. + /// The server that handles the client. + /// The cancellation token of the server. + internal class ClientHandlingFlow(SimpleWebSocketServer server, WebSocketServerClient client, CancellationToken cancellationToken) + { + /// + /// Gets the client associated with the flow. + /// + internal WebSocketServerClient Client { get; set; } = client; + + /// + /// Gets the request context of the client. + /// + internal WebContext Request { get; set; } = null!; + + /// + /// Gets the upgrade handler for the client. + /// + private WebSocketUpgradeHandler _upgradeHandler = null!; + + /// + /// Gets the options of the server. + /// + private readonly SimpleWebSocketServerOptions _options = server.Options; + + /// + /// Gets the active clients of the server. + /// + private readonly ConcurrentDictionary _activeClients = server.ActiveClients; + + /// + /// Gets the passive clients of the server. + /// + private readonly IDictionary _passiveClients = server.PassiveClients; + + /// + /// Gets the logger of the server. + /// + private readonly ILogger? _logger = server.Logger; + + /// + /// Gets the cancellation token of the server. + /// + private readonly CancellationToken _cancellationToken = cancellationToken; + + /// + /// The lock object for the client dictionaries. + /// + private static readonly object _clientLock = new(); + + /// + /// Loads the request context. + /// + internal async Task LoadRequestContext() + { + var stream = Client.ClientConnection!.GetStream(); + _upgradeHandler = new WebSocketUpgradeHandler(stream); + Request = await _upgradeHandler.AwaitContextAsync(_cancellationToken); + } + + /// + /// Handles the client identification. + /// + internal void HandleClientIdentification() + { + // Check if disconnected clients are remembered and can be reactivated + if (_options.RememberDisconnectedClients) + { + // Check if the request contains a user id + if (Request.ContainsUserId) + { + _logger?.LogDebug("User id found in request: {userId}", Request.UserId); + + lock (_clientLock) + { + ThrowForUserAlreadyConnected(); + + // Check if the client is an existing passive client + var clientExists = _passiveClients.ContainsKey(Request.UserId); + if (clientExists) + { + _logger?.LogDebug("Passive Client found for user id {userId} - reactivating user.", Request.UserId); + + // Use the existing client + // Update the client with the new connection + // Remove the client from the passive clients + var passiveClient = _passiveClients[Request.UserId]; + passiveClient.UpdateClient(Client.ClientConnection!); + Client = passiveClient; + _passiveClients.Remove(Request.UserId); + } + else + { + // Client is not a passive client + // Update the clients user id + Client.UpdateId(Request.UserId); + } + } + } + } + } + + /// + /// Throws an exception if the user is already connected. + /// + /// + private void ThrowForUserAlreadyConnected() + { + // No passive client found, checking for active clients with the same id + if (_activeClients.ContainsKey(Request.UserId)) + { + _logger?.LogDebug("Active Client found for user id {userId} - rejecting connection.", Request.UserId); + // Reject the connection + + var responseContext = new WebContext + { + StatusCode = HttpStatusCode.Conflict, + BodyContent = "User id already in use" + }; + throw new UserNotHandledException(responseContext); + } + } + + /// + /// Accepts the websocket connection. + /// + /// The response context to send to the client. + internal async Task AcceptWebSocketAsync(WebContext responseContext) + { + // The client is accepted + await _upgradeHandler.AcceptWebSocketAsync(Request, responseContext, Client.Id, null, _cancellationToken); + + // Use the websocket for the client + Client.UseWebSocket(_upgradeHandler.CreateWebSocket(isServer: true)); + } + + /// + /// Rejects the websocket connection. + /// + /// The response context to send to the client. + internal async Task RejectWebSocketAsync(WebContext responseContext) + { + await _upgradeHandler.RejectWebSocketAsync(responseContext, _cancellationToken); + } + + /// + /// Handles the disconnected client. + /// + internal void HandleDisconnectedClient() + { + lock (_clientLock) + { + _activeClients.TryRemove(Client.Id, out _); + Client.Dispose(); + + if (_options.RememberDisconnectedClients) + { + _logger?.LogDebug("Client {clientId} is now a passive user.", Client.Id); + _passiveClients.Add(Client.Id, Client); + } + else + { + _logger?.LogDebug("Client {clientId} is removed.", Client.Id); + } + } + } + } +} \ No newline at end of file diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index 5861ea9..2634e70 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -14,6 +14,7 @@ using System.Net; using System.Net.WebSockets; using System.Text; +using System.Threading; namespace Jung.SimpleWebSocket { @@ -44,12 +45,12 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable /// /// A dictionary of active clients. /// - private ConcurrentDictionary ActiveClients { get; } = []; + internal ConcurrentDictionary ActiveClients { get; } = []; /// /// A dictionary of passive clients. /// - private IDictionary PassiveClients { get; set; } = null!; + internal IDictionary PassiveClients { get; set; } = null!; /// public string[] ClientIds => [.. ActiveClients.Keys]; @@ -60,6 +61,16 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable /// public bool IsListening => _server?.IsListening ?? false; + /// + /// A logger to write internal log messages. + /// + internal ILogger? Logger { get; } + + /// + /// The options for the server. + /// + internal SimpleWebSocketServerOptions Options { get; } + /// /// A flag indicating whether the server is started. /// @@ -80,16 +91,6 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable /// private ITcpListener? _server; - /// - /// A logger to write internal log messages. - /// - private readonly ILogger? _logger; - - /// - /// The options for the server. - /// - private readonly SimpleWebSocketServerOptions _options; - /// /// Initializes a new instance of the class that listens /// for incoming connection attempts on the specified local IP address and port number. @@ -100,8 +101,8 @@ public SimpleWebSocketServer(SimpleWebSocketServerOptions options, ILogger? logg { LocalIpAddress = options.LocalIpAddress; Port = options.Port; - _logger = logger; - _options = options; + Logger = logger; + Options = options; InitializePassiveClientDictionary(options); } @@ -116,7 +117,7 @@ private void InitializePassiveClientDictionary(SimpleWebSocketServerOptions opti // Initialize the passive clients dictionary if (options.RemovePassiveClientsAfterClientExpirationTime) { - var passiveClients = new ExpiringDictionary(options.PassiveClientLifetime, _logger); + var passiveClients = new ExpiringDictionary(options.PassiveClientLifetime, Logger); passiveClients.ItemExpired += PassiveClients_ItemExpired; PassiveClients = passiveClients; } @@ -169,7 +170,7 @@ public void Start(CancellationToken? cancellationToken = null) _server.Start(); _ = Task.Run(async delegate { - _logger?.LogInformation("Server started at {LocalIpAddress}:{Port}", LocalIpAddress, Port); + Logger?.LogInformation("Server started at {LocalIpAddress}:{Port}", LocalIpAddress, Port); while (!linkedTokenSource.IsCancellationRequested) { try @@ -177,7 +178,7 @@ public void Start(CancellationToken? cancellationToken = null) // Accept the client var client = await _server.AcceptTcpClientAsync(linkedTokenSource.Token); - _logger?.LogDebug("Client connected from {endpoint}", client.ClientConnection!.RemoteEndPoint); + Logger?.LogDebug("Client connected from {endpoint}", client.ClientConnection!.RemoteEndPoint); _ = HandleClientAsync(client, linkedTokenSource.Token); } @@ -187,7 +188,7 @@ public void Start(CancellationToken? cancellationToken = null) } catch (Exception exception) { - _logger?.LogError(exception, "Error while accepting client."); + Logger?.LogError(exception, "Error while accepting Client."); } } }, linkedTokenSource.Token); @@ -203,7 +204,7 @@ public async Task ShutdownServer(CancellationToken? cancellationToken = null) cancellationToken ??= CancellationToken.None; var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken.Value, _cancellationTokenSource.Token); - _logger?.LogInformation("Stopping server..."); + Logger?.LogInformation("Stopping server..."); // copying the active clients to avoid a collection modified exception var activeClients = ActiveClients.Values.ToArray(); @@ -220,7 +221,7 @@ public async Task ShutdownServer(CancellationToken? cancellationToken = null) _cancellationTokenSource?.Cancel(); _server?.Dispose(); _server = null; - _logger?.LogInformation("Server stopped"); + Logger?.LogInformation("Server stopped"); } /// @@ -238,11 +239,11 @@ public async Task SendMessageAsync(string clientId, string message, Cancellation // Send the message var buffer = Encoding.UTF8.GetBytes(message); await client.WebSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, linkedTokenSource.Token); - _logger?.LogDebug("Message sent: {message}.", message); + Logger?.LogDebug("Message sent: {message}.", message); } catch (Exception exception) { - _logger?.LogError(exception, "Error while sending a message."); + Logger?.LogError(exception, "Error while sending a message."); throw new WebSocketServerException(message: "An Error occurred sending a message.", innerException: exception); } } @@ -263,126 +264,63 @@ public WebSocketServerClient GetClientById(string clientId) /// A asynchronous task private async Task HandleClientAsync(WebSocketServerClient client, CancellationToken cancellationToken) { - bool clientAdded = false; + var flow = new ClientHandlingFlow(this, client, cancellationToken); + var clientHandled = false; try { - // Upgrade the connection to a WebSocket - using var stream = client.ClientConnection!.GetStream(); - var upgradeHandler = new WebSocketUpgradeHandler(stream); - var request = await upgradeHandler.AwaitContextAsync(cancellationToken); + // Load the request context + await flow.LoadRequestContext(); - // Check if disconnected clients are remembered and can be reactivated - if (_options.RememberDisconnectedClients) - { - // Check if the request contains a user id - if (request.ContainsUserId) - { - _logger?.LogDebug("User id found in request: {userId}", request.UserId); - // Check if the client is an existing passive client - var clientExists = PassiveClients.ContainsKey(request.UserId); - if (clientExists) - { - _logger?.LogDebug("Passive client found for user id {userId} - reactivating user.", request.UserId); - - // Use the existing client - // Update the client with the new connection - // Remove the client from the passive clients - var passiveClient = PassiveClients[request.UserId]; - passiveClient.UpdateClient(client.ClientConnection); - client = passiveClient; - PassiveClients.Remove(request.UserId); - } - else - { - // No passive client found, checking for active clients with the same id - if (ActiveClients.ContainsKey(request.UserId)) - { - _logger?.LogDebug("Active client found for user id {userId} - rejecting connection.", request.UserId); - // Reject the connection - - var responseContext = new WebContext - { - StatusCode = HttpStatusCode.Conflict, - BodyContent = "User id already in use" - }; - await upgradeHandler.RejectWebSocketAsync(responseContext, cancellationToken); - return; - } - else - { - // Update the client with the new id - client.UpdateId(request.UserId); - } - } - } - } + // Handle the client user identification if activated + flow.HandleClientIdentification(); // raise async client upgrade request received event - var eventArgs = new ClientUpgradeRequestReceivedArgs(client, request, _logger); + var eventArgs = new ClientUpgradeRequestReceivedArgs(flow.Client, flow.Request, Logger); await AsyncEventRaiser.RaiseAsync(ClientUpgradeRequestReceivedAsync, this, eventArgs, cancellationToken); if (eventArgs.Handle) { - // The client is accepted - await upgradeHandler.AcceptWebSocketAsync(request, eventArgs.ResponseContext, client.Id, null, cancellationToken); + // The event consumer accepted the client + await flow.AcceptWebSocketAsync(eventArgs.ResponseContext); - // Use the websocket for the client - client.UseWebSocket(upgradeHandler.CreateWebSocket(isServer: true)); - - clientAdded = ActiveClients.TryAdd(client.Id, client); - if (clientAdded) + if (ActiveClients.TryAdd(flow.Client.Id, flow.Client)) { - _ = Task.Run(() => ClientConnected?.Invoke(this, new ClientConnectedArgs(client.Id)), cancellationToken); + _ = Task.Run(() => ClientConnected?.Invoke(this, new ClientConnectedArgs(flow.Client.Id)), cancellationToken); // Start listening for messages - _logger?.LogDebug("Connection upgraded, now listening on client {clientId}", client.Id); - await ProcessWebSocketMessagesAsync(client, cancellationToken); + Logger?.LogDebug("Connection upgraded, now listening on Client {clientId}", flow.Client.Id); + await ProcessWebSocketMessagesAsync(flow.Client, cancellationToken); + clientHandled = true; } } else { // The client is rejected - _logger?.LogDebug("Client upgrade request rejected by ClientUpgradeRequestReceivedAsync event."); - await upgradeHandler.RejectWebSocketAsync(eventArgs.ResponseContext, cancellationToken); + Logger?.LogDebug("Client upgrade request rejected by ClientUpgradeRequestReceivedAsync event."); + await flow.RejectWebSocketAsync(eventArgs.ResponseContext); } } catch (OperationCanceledException) { // Ignore the exception, because it is thrown when cancellation is requested } + catch (UserNotHandledException userNotHandledException) + { + await flow.RejectWebSocketAsync(userNotHandledException.ResponseContext); + } catch (Exception exception) { - _logger?.LogError(exception, "Error while handling the client {clientId}", client.Id); + Logger?.LogError(exception, "Error while handling the Client {clientId}", flow.Client.Id); } finally { // If the client was added and the server is not shutting down, handle the disconnected client // The client is not added if the connection was rejected - if (clientAdded && !_serverShuttingDown) + if (clientHandled && !_serverShuttingDown) { - HandleDisconnectedClient(client); + flow.HandleDisconnectedClient(); } } } - /// - /// Handles the disconnected client. - /// - /// - private void HandleDisconnectedClient(WebSocketServerClient client) - { - ActiveClients.TryRemove(client.Id, out _); - client.Dispose(); - - if (_options.RememberDisconnectedClients) - { - _logger?.LogDebug("Client {clientId} is now a passive user.", client.Id); - PassiveClients.Add(client.Id, client); - } - else - { - _logger?.LogDebug("Client {clientId} is removed.", client.Id); - } - } - /// /// Processes the WebSocket messages. /// @@ -410,20 +348,20 @@ private async Task ProcessWebSocketMessagesAsync(WebSocketServerClient client, C { // Handle the text message string receivedMessage = Encoding.UTF8.GetString(buffer, 0, result.Count); - _logger?.LogDebug("Message received: {message}", receivedMessage); + Logger?.LogDebug("Message received: {message}", receivedMessage); _ = Task.Run(() => MessageReceived?.Invoke(this, new ClientMessageReceivedArgs(receivedMessage, client.Id)), cancellationToken); } else if (result.MessageType == WebSocketMessageType.Binary) { // Handle the binary message - _logger?.LogDebug("Binary message received, length: {length} bytes", result.Count); + Logger?.LogDebug("Binary message received, length: {length} bytes", result.Count); _ = Task.Run(() => BinaryMessageReceived?.Invoke(this, new ClientBinaryMessageReceivedArgs(buffer[..result.Count], client.Id)), cancellationToken); } // We have to check if the is shutting down here, // because then we already sent the close message and we don't want to send another one else if (result.MessageType == WebSocketMessageType.Close && !_serverShuttingDown) { - _logger?.LogInformation("Received close message from client"); + Logger?.LogInformation("Received close message from Client"); _ = Task.Run(() => ClientDisconnected?.Invoke(this, new ClientDisconnectedArgs(result.CloseStatusDescription ?? string.Empty, client.Id)), cancellationToken); await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None); break; @@ -441,7 +379,7 @@ private async Task ProcessWebSocketMessagesAsync(WebSocketServerClient client, C /// The arguments of the event private void PassiveClients_ItemExpired(object? sender, ItemExpiredArgs e) { - _logger?.LogDebug("Passive client expired: {clientId}", e.Item.Id); + Logger?.LogDebug("Passive Client expired: {clientId}", e.Item.Id); // Raise the event asynchronously // We don't want to block the cleanup process diff --git a/Jung.SimpleWebSocket/UserNotHandledException.cs b/Jung.SimpleWebSocket/UserNotHandledException.cs new file mode 100644 index 0000000..2e510a2 --- /dev/null +++ b/Jung.SimpleWebSocket/UserNotHandledException.cs @@ -0,0 +1,13 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using Jung.SimpleWebSocket.Models; + +namespace Jung.SimpleWebSocket +{ + [Serializable] + internal class UserNotHandledException(WebContext responseContext) : Exception + { + public WebContext ResponseContext { get; set; } = responseContext; + } +} \ No newline at end of file diff --git a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs index 3c064ef..416a4aa 100644 --- a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs +++ b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs @@ -141,7 +141,7 @@ private static void CompleteHeaderSection(StringBuilder sb) sb.Append("\r\n"); } - private void AddBody(WebContext context, StringBuilder sb) + private static void AddBody(WebContext context, StringBuilder sb) { sb.Append(context.BodyContent); } @@ -201,7 +201,7 @@ internal static bool ProcessWebSocketProtocolHeader(string? clientSecWebSocketPr return true; } } - throw new WebSocketUpgradeException($"The WebSocket _client requested the following protocols: '{clientSecWebSocketProtocol}', but the server accepted '{subProtocol}' protocol(s)."); + throw new WebSocketUpgradeException($"The WebSocket Client requested the following protocols: '{clientSecWebSocketProtocol}', but the server accepted '{subProtocol}' protocol(s)."); } internal async Task SendUpgradeRequestAsync(WebContext requestContext, CancellationToken token) From aab5c8ab81a35f7341ed01718e6cb0a2e54438a4 Mon Sep 17 00:00:00 2001 From: Christoph Jung Date: Tue, 1 Oct 2024 23:10:04 +0200 Subject: [PATCH 08/16] Improve ClientHandlingFlow Add helper method for raising events in a new task Change some event signatures --- Jung.SimpleWebSocket/ClientHandlingFlow.cs | 82 +++++++++++++++---- .../Contracts/IWebSocketServer.cs | 10 +-- Jung.SimpleWebSocket/SimpleWebSocketServer.cs | 44 +++++----- .../Utility/AsyncEventRaiser.cs | 27 +++++- 4 files changed, 121 insertions(+), 42 deletions(-) diff --git a/Jung.SimpleWebSocket/ClientHandlingFlow.cs b/Jung.SimpleWebSocket/ClientHandlingFlow.cs index 221e78a..11cde12 100644 --- a/Jung.SimpleWebSocket/ClientHandlingFlow.cs +++ b/Jung.SimpleWebSocket/ClientHandlingFlow.cs @@ -1,8 +1,12 @@ // This file is part of the Jung SimpleWebSocket project. // The project is licensed under the MIT license. +using Jung.SimpleWebSocket.Delegates; using Jung.SimpleWebSocket.Models; +using Jung.SimpleWebSocket.Models.EventArguments; +using Jung.SimpleWebSocket.Utility; using Microsoft.Extensions.Logging; +using System; using System.Collections.Concurrent; using System.Net; @@ -34,6 +38,21 @@ internal class ClientHandlingFlow(SimpleWebSocketServer server, WebSocketServerC /// private WebSocketUpgradeHandler _upgradeHandler = null!; + /// + /// Gets the response context that is being use to response to the client. + /// + private WebContext _responseContext = null!; + + /// + /// Gets a value indicating whether the client was accepted. + /// + private bool _clientAccepted; + + /// + /// Gets a value indicating whether the client was a passive client. + /// + private bool _clientWasPassiveClient; + /// /// Gets the options of the server. /// @@ -103,7 +122,12 @@ internal void HandleClientIdentification() var passiveClient = _passiveClients[Request.UserId]; passiveClient.UpdateClient(Client.ClientConnection!); Client = passiveClient; - _passiveClients.Remove(Request.UserId); + var clientRemoved = _passiveClients.Remove(Request.UserId); + + // Set the flag that the client was a passive client + // This should only be set if the client was removed in this specific flow + // Otherwise its possible that the client is handled twice + _clientWasPassiveClient = clientRemoved; } else { @@ -140,14 +164,17 @@ private void ThrowForUserAlreadyConnected() /// /// Accepts the websocket connection. /// - /// The response context to send to the client. - internal async Task AcceptWebSocketAsync(WebContext responseContext) + internal async Task AcceptWebSocketAsync() { // The client is accepted - await _upgradeHandler.AcceptWebSocketAsync(Request, responseContext, Client.Id, null, _cancellationToken); + await _upgradeHandler.AcceptWebSocketAsync(Request, _responseContext, Client.Id, null, _cancellationToken); // Use the websocket for the client Client.UseWebSocket(_upgradeHandler.CreateWebSocket(isServer: true)); + + // Set the flag that the client was accepted + // This is used to determine if the client should be added to the passive clients after disconnect + _clientAccepted = true; } /// @@ -164,21 +191,46 @@ internal async Task RejectWebSocketAsync(WebContext responseContext) /// internal void HandleDisconnectedClient() { - lock (_clientLock) + if (_clientWasPassiveClient || _clientAccepted) { - _activeClients.TryRemove(Client.Id, out _); - Client.Dispose(); - - if (_options.RememberDisconnectedClients) - { - _logger?.LogDebug("Client {clientId} is now a passive user.", Client.Id); - _passiveClients.Add(Client.Id, Client); - } - else + lock (_clientLock) { - _logger?.LogDebug("Client {clientId} is removed.", Client.Id); + _activeClients.TryRemove(Client.Id, out _); + Client.Dispose(); + + if (_options.RememberDisconnectedClients) + { + _logger?.LogDebug("Client {clientId} is now a passive user.", Client.Id); + _passiveClients.Add(Client.Id, Client); + } + else + { + _logger?.LogDebug("Client {clientId} is removed.", Client.Id); + } } } } + + /// + /// Raises the upgrade event. + /// + /// The event handler for the upgrade request. + /// The event arguments of the upgrade request. + internal async Task RaiseUpgradeEventAsync(AsyncEventHandler? clientUpgradeRequestReceivedAsync) + { + var eventArgs = new ClientUpgradeRequestReceivedArgs(Client, Request, _logger); + await AsyncEventRaiser.RaiseAsync(clientUpgradeRequestReceivedAsync, server, eventArgs, _cancellationToken); + _responseContext = eventArgs.ResponseContext; + return eventArgs; + } + + /// + /// Tries to add the client to the active user list. + /// + /// True if the client was added to the active user list. False if the client is already connected. + internal bool TryAddClientToActiveUserList() + { + return _activeClients.TryAdd(Client.Id, Client); + } } } \ No newline at end of file diff --git a/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs b/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs index 464198f..5add6a8 100644 --- a/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs +++ b/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs @@ -42,27 +42,27 @@ public interface IWebSocketServer : IDisposable /// /// Event that is raised when a client is connected. /// - event ClientConnectedEventHandler? ClientConnected; + event EventHandler? ClientConnected; /// /// Event that is raised when a client is disconnected. /// - event ClientDisconnectedEventHandler ClientDisconnected; + event EventHandler? ClientDisconnected; /// /// Event that is raised when a message is received from a client. /// - event ClientMessageReceivedEventHandler? MessageReceived; + event EventHandler? MessageReceived; /// /// Event that is raised when a binary message is received from a client. /// - event ClientBinaryMessageReceivedEventHandler? BinaryMessageReceived; + event EventHandler? BinaryMessageReceived; /// /// Occurs when an passive user expired. /// - event PassiveUserExpiredEventHandler? PassiveUserExpiredEvent; + event EventHandler? PassiveUserExpiredEvent; /// /// Async Event that is raised when a client upgrade request is received. diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index 2634e70..7e1d652 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -29,15 +29,15 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable public int Port { get; } /// - public event ClientConnectedEventHandler? ClientConnected; + public event EventHandler? ClientConnected; /// - public event ClientDisconnectedEventHandler? ClientDisconnected; + public event EventHandler? ClientDisconnected; /// - public event ClientMessageReceivedEventHandler? MessageReceived; + public event EventHandler? MessageReceived; /// - public event ClientBinaryMessageReceivedEventHandler? BinaryMessageReceived; + public event EventHandler? BinaryMessageReceived; /// - public event PassiveUserExpiredEventHandler? PassiveUserExpiredEvent; + public event EventHandler? PassiveUserExpiredEvent; /// public event AsyncEventHandler? ClientUpgradeRequestReceivedAsync; @@ -265,7 +265,6 @@ public WebSocketServerClient GetClientById(string clientId) private async Task HandleClientAsync(WebSocketServerClient client, CancellationToken cancellationToken) { var flow = new ClientHandlingFlow(this, client, cancellationToken); - var clientHandled = false; try { // Load the request context @@ -275,25 +274,28 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT flow.HandleClientIdentification(); // raise async client upgrade request received event - var eventArgs = new ClientUpgradeRequestReceivedArgs(flow.Client, flow.Request, Logger); - await AsyncEventRaiser.RaiseAsync(ClientUpgradeRequestReceivedAsync, this, eventArgs, cancellationToken); + var eventArgs = await flow.RaiseUpgradeEventAsync(ClientUpgradeRequestReceivedAsync); + + // Respond to the upgrade request if (eventArgs.Handle) { - // The event consumer accepted the client - await flow.AcceptWebSocketAsync(eventArgs.ResponseContext); - - if (ActiveClients.TryAdd(flow.Client.Id, flow.Client)) + // Accept the WebSocket connection + await flow.AcceptWebSocketAsync(); + if (flow.TryAddClientToActiveUserList()) { - _ = Task.Run(() => ClientConnected?.Invoke(this, new ClientConnectedArgs(flow.Client.Id)), cancellationToken); - // Start listening for messages Logger?.LogDebug("Connection upgraded, now listening on Client {clientId}", flow.Client.Id); + AsyncEventRaiser.RaiseAsyncInNewTask(ClientConnected, this, new ClientConnectedArgs(flow.Client.Id), cancellationToken); + // Start listening for messages await ProcessWebSocketMessagesAsync(flow.Client, cancellationToken); - clientHandled = true; + } + else + { + Logger?.LogDebug("Connection upgraded, now listening on Client {clientId}", flow.Client.Id); } } else { - // The client is rejected + // Reject the WebSocket connection Logger?.LogDebug("Client upgrade request rejected by ClientUpgradeRequestReceivedAsync event."); await flow.RejectWebSocketAsync(eventArgs.ResponseContext); } @@ -314,7 +316,7 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT { // If the client was added and the server is not shutting down, handle the disconnected client // The client is not added if the connection was rejected - if (clientHandled && !_serverShuttingDown) + if (!_serverShuttingDown) { flow.HandleDisconnectedClient(); } @@ -349,20 +351,20 @@ private async Task ProcessWebSocketMessagesAsync(WebSocketServerClient client, C // Handle the text message string receivedMessage = Encoding.UTF8.GetString(buffer, 0, result.Count); Logger?.LogDebug("Message received: {message}", receivedMessage); - _ = Task.Run(() => MessageReceived?.Invoke(this, new ClientMessageReceivedArgs(receivedMessage, client.Id)), cancellationToken); + AsyncEventRaiser.RaiseAsyncInNewTask(MessageReceived, this, new ClientMessageReceivedArgs(receivedMessage, client.Id), cancellationToken); } else if (result.MessageType == WebSocketMessageType.Binary) { // Handle the binary message Logger?.LogDebug("Binary message received, length: {length} bytes", result.Count); - _ = Task.Run(() => BinaryMessageReceived?.Invoke(this, new ClientBinaryMessageReceivedArgs(buffer[..result.Count], client.Id)), cancellationToken); + AsyncEventRaiser.RaiseAsyncInNewTask(BinaryMessageReceived, this, new ClientBinaryMessageReceivedArgs(buffer[..result.Count], client.Id), cancellationToken); } // We have to check if the is shutting down here, // because then we already sent the close message and we don't want to send another one else if (result.MessageType == WebSocketMessageType.Close && !_serverShuttingDown) { Logger?.LogInformation("Received close message from Client"); - _ = Task.Run(() => ClientDisconnected?.Invoke(this, new ClientDisconnectedArgs(result.CloseStatusDescription ?? string.Empty, client.Id)), cancellationToken); + AsyncEventRaiser.RaiseAsyncInNewTask(ClientDisconnected, this, new ClientDisconnectedArgs(result.CloseStatusDescription ?? string.Empty, client.Id), cancellationToken); await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None); break; } @@ -383,7 +385,7 @@ private void PassiveClients_ItemExpired(object? sender, ItemExpiredArgs PassiveUserExpiredEvent?.Invoke(this, new PassiveUserExpiredArgs(e.Item.Id))); + AsyncEventRaiser.RaiseAsyncInNewTask(PassiveUserExpiredEvent, this, new PassiveUserExpiredArgs(e.Item.Id), _cancellationTokenSource.Token); } /// diff --git a/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs b/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs index b81a851..91e76ae 100644 --- a/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs +++ b/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs @@ -47,5 +47,30 @@ internal static async Task RaiseAsync(AsyncEventHandler? } } } + + /// + /// Helper method to raise an async event. + /// + /// The type of the event arguments. + /// The event handler + /// The sender of the event. + /// The event arguments. + /// The cancellation token. + /// A task that represents the asynchronous operation. + internal static void RaiseAsyncInNewTask(EventHandler? @event, object sender, TEventArgs e, CancellationToken cancellationToken) where TEventArgs : class + { + if (@event != null) + { + var invocationList = @event.GetInvocationList(); + + foreach (var handler in invocationList) + { + var asyncHandler = (EventHandler)handler; + + // Execute directly if there's no synchronization context + _ = Task.Run(() => asyncHandler(sender, e), cancellationToken); + } + } + } } -} +} \ No newline at end of file From 3fa7413ca4f16347eddbe883b6244679ac11a858 Mon Sep 17 00:00:00 2001 From: Christoph Jung Date: Sun, 6 Oct 2024 20:01:18 +0200 Subject: [PATCH 09/16] Change the RaiseAsyncInNewTask method to call the handlers from the invoker list one after the other and not in parallel --- Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs b/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs index 91e76ae..a084e00 100644 --- a/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs +++ b/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs @@ -49,7 +49,7 @@ internal static async Task RaiseAsync(AsyncEventHandler? } /// - /// Helper method to raise an async event. + /// Helper method to raise an Event in a new Task. /// /// The type of the event arguments. /// The event handler @@ -62,14 +62,14 @@ internal static void RaiseAsyncInNewTask(EventHandler? @ if (@event != null) { var invocationList = @event.GetInvocationList(); - - foreach (var handler in invocationList) + Task.Run(() => { - var asyncHandler = (EventHandler)handler; - - // Execute directly if there's no synchronization context - _ = Task.Run(() => asyncHandler(sender, e), cancellationToken); - } + foreach (var handler in invocationList) + { + var handle = (EventHandler)handler; + handle(sender, e); + } + }, cancellationToken); } } } From e9c065e2def7c6ad23bbbb4e51925544200286ce Mon Sep 17 00:00:00 2001 From: Christoph Date: Tue, 28 Jan 2025 21:44:41 +0100 Subject: [PATCH 10/16] Add client handling flow tests --- .../UserNotHandledException.cs | 2 +- .../{ => Flows}/ClientHandlingFlow.cs | 1 + Jung.SimpleWebSocket/Models/WebContext.cs | 1 - .../Models/WebSocketServerClient.cs | 9 ++ Jung.SimpleWebSocket/docs/README.md | 9 +- .../ClientHandlingFlowTest.cs | 103 ++++++++++++++++++ .../Mock/ILoggerMockHelper.cs | 39 +++++++ .../Mock/LoggerMessages.cs | 25 +++++ .../SimpleWebSocketTest.cs | 59 +++------- 9 files changed, 200 insertions(+), 48 deletions(-) rename Jung.SimpleWebSocket/{ => Exceptions}/UserNotHandledException.cs (89%) rename Jung.SimpleWebSocket/{ => Flows}/ClientHandlingFlow.cs (99%) create mode 100644 Jung.SimpleWebSocketTest/ClientHandlingFlowTest.cs create mode 100644 Jung.SimpleWebSocketTest/Mock/ILoggerMockHelper.cs create mode 100644 Jung.SimpleWebSocketTest/Mock/LoggerMessages.cs diff --git a/Jung.SimpleWebSocket/UserNotHandledException.cs b/Jung.SimpleWebSocket/Exceptions/UserNotHandledException.cs similarity index 89% rename from Jung.SimpleWebSocket/UserNotHandledException.cs rename to Jung.SimpleWebSocket/Exceptions/UserNotHandledException.cs index 2e510a2..148c6f7 100644 --- a/Jung.SimpleWebSocket/UserNotHandledException.cs +++ b/Jung.SimpleWebSocket/Exceptions/UserNotHandledException.cs @@ -3,7 +3,7 @@ using Jung.SimpleWebSocket.Models; -namespace Jung.SimpleWebSocket +namespace Jung.SimpleWebSocket.Exceptions { [Serializable] internal class UserNotHandledException(WebContext responseContext) : Exception diff --git a/Jung.SimpleWebSocket/ClientHandlingFlow.cs b/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs similarity index 99% rename from Jung.SimpleWebSocket/ClientHandlingFlow.cs rename to Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs index 11cde12..c688226 100644 --- a/Jung.SimpleWebSocket/ClientHandlingFlow.cs +++ b/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs @@ -2,6 +2,7 @@ // The project is licensed under the MIT license. using Jung.SimpleWebSocket.Delegates; +using Jung.SimpleWebSocket.Exceptions; using Jung.SimpleWebSocket.Models; using Jung.SimpleWebSocket.Models.EventArguments; using Jung.SimpleWebSocket.Utility; diff --git a/Jung.SimpleWebSocket/Models/WebContext.cs b/Jung.SimpleWebSocket/Models/WebContext.cs index 04173cf..3b7626c 100644 --- a/Jung.SimpleWebSocket/Models/WebContext.cs +++ b/Jung.SimpleWebSocket/Models/WebContext.cs @@ -331,7 +331,6 @@ internal IEnumerable GetAllHeaderValues(string headerName) } } - /// /// Gets the concatenated headers of the web request. /// diff --git a/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs b/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs index 438a62f..e3a40d9 100644 --- a/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs +++ b/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs @@ -60,6 +60,15 @@ internal WebSocketServerClient(ITcpClient clientConnection) ClientConnection = clientConnection; } + /// + /// Constructor for unit tests + /// + /// The time the user was first seen. + internal WebSocketServerClient(DateTime firstSeen) + { + FirstSeen = LastConnectionTimestamp = firstSeen; + } + /// /// Updates the WebSocket client with a new connection. /// diff --git a/Jung.SimpleWebSocket/docs/README.md b/Jung.SimpleWebSocket/docs/README.md index 7d712e8..a0c5979 100644 --- a/Jung.SimpleWebSocket/docs/README.md +++ b/Jung.SimpleWebSocket/docs/README.md @@ -1,18 +1,19 @@ # Jung.SimpleWebSocket -Jung.SimpleWebSocket is a lightweight and easy-to-use library for working with WebSocket connections in .NET. +Jung.SimpleWebSocket is library for working with WebSocket connections in .NET. It is built on top of the `System.Net.WebSockets` namespace and provides a simple API for creating WebSocket clients and servers. -By using a TcpListener and TcpClient, Jung.SimpleWebSocket is able to handle WebSocket connections without the need for a full-fledged HTTP server. -You also don't need admin rights to run the server. +The library is designed to give you full access to the WebSocket connection process. +You also don't need admin rights to bind the server to a port, as the library uses the `HttpListener` class to listen for incoming WebSocket connections. ## Installation -You can install Jung.SimpleWebSocket via NuGet package manager or by manually downloading the library. +You can install Jung.SimpleWebSocket via NuGet package manager or by manually downloading and building the library. ### NuGet Package Manager 1. Open the NuGet Package Manager Console in Visual Studio. 2. Run the following command to install the package: `Install-Package Jung.SimpleWebSocket`. + ### Manual Download 1. Go to the [Jung.SimpleWebSocket GitHub repository](https://github.com/cjung95/SimpleWebSocket). diff --git a/Jung.SimpleWebSocketTest/ClientHandlingFlowTest.cs b/Jung.SimpleWebSocketTest/ClientHandlingFlowTest.cs new file mode 100644 index 0000000..e70360a --- /dev/null +++ b/Jung.SimpleWebSocketTest/ClientHandlingFlowTest.cs @@ -0,0 +1,103 @@ +using Jung.SimpleWebSocket; +using Jung.SimpleWebSocket.Contracts; +using Jung.SimpleWebSocket.Exceptions; +using Jung.SimpleWebSocket.Models; +using Jung.SimpleWebSocketTest.Mock; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using System.Text; + +namespace Jung.SimpleWebSocketTest +{ + [TestFixture] + internal class ClientHandlingFlowTest + { + private ILogger _logger; + + [SetUp] + public void SetUp() + { + var loggerHelper = new ILoggerMockHelper("Server"); + _logger = loggerHelper.Logger; + } + + private ClientHandlingFlow SetupClientHandlingFlow(object serverOptions, List? activeClients = null) + { + var tcpListener = new Mock(); + var serverMoq = new Mock(serverOptions, tcpListener.Object, _logger); + if (activeClients != null) + { + foreach (var client in activeClients) + { + serverMoq.Object.ActiveClients.TryAdd(client.Id, client); + } + } + + var tcpClientMoq = new Mock(); + var serverClientMoq = new WebSocketServerClient(tcpClientMoq.Object); + + return new ClientHandlingFlow(serverMoq.Object, serverClientMoq, CancellationToken.None); + } + + private string CreateUpgradeRequest(string? userId) + { + var sb = new StringBuilder(); + sb.Append("GET /chat HTTP/1.1\r\n" + + "Host: localhost:8080\r\n" + + "Upgrade: websocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n" + + "Sec-WebSocket-Version: 13\r\n"); + + if (!string.IsNullOrEmpty(userId)) + { + sb.Append("x-user-id: 6C8D0844-D84F-4AD9-B28D-23B3940887B7"); + } + + sb.Append("\r\n\r\n"); + return sb.ToString(); + } + + + [Test] + public void HandleClientIdentification_NoNewUser_UserIdIsUpdated() + { + var userId = "6C8D0844-D84F-4AD9-B28D-23B3940887B7"; + var requestText = CreateUpgradeRequest(userId); + var serverOptions = new SimpleWebSocketServerOptions + { + RememberDisconnectedClients = true, + }; + + var clientHandlingFlow = SetupClientHandlingFlow(serverOptions); + + clientHandlingFlow.Request = new WebContext(requestText); + clientHandlingFlow.HandleClientIdentification(); + + Assert.That(clientHandlingFlow.Client.Id, Is.EqualTo(userId)); + } + + [Test] + public void HandleClientIdentification_NoNewUser_UserAlreadyConnected() + { + // setup + var userId = "6C8D0844-D84F-4AD9-B28D-23B3940887B7"; + var activeUsers = new List(); + var client = new WebSocketServerClient(DateTime.Now); + client.UpdateId(userId); + activeUsers.Add(client); + var requestText = CreateUpgradeRequest(userId); + var serverOptions = new SimpleWebSocketServerOptions + { + RememberDisconnectedClients = true, + }; + + // act and assert + var clientHandlingFlow = SetupClientHandlingFlow(serverOptions, activeUsers); + + clientHandlingFlow.Request = new WebContext(requestText); + Assert.That(() => clientHandlingFlow.HandleClientIdentification(), Throws.Exception.TypeOf()); + } + } +} diff --git a/Jung.SimpleWebSocketTest/Mock/ILoggerMockHelper.cs b/Jung.SimpleWebSocketTest/Mock/ILoggerMockHelper.cs new file mode 100644 index 0000000..e72d2fa --- /dev/null +++ b/Jung.SimpleWebSocketTest/Mock/ILoggerMockHelper.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.Logging; +using Moq; + +namespace Jung.SimpleWebSocketTest.Mock +{ + internal class ILoggerMockHelper where T : class + { + internal Mock> LoggerMock { get; } + internal ILogger Logger => LoggerMock.Object; + + public ILoggerMockHelper(string name) + { + LoggerMock = new Mock>(); + ILoggerMockHelper.SetUpLogger(LoggerMock, name); + } + + private static void SetUpLogger(Mock> mock, string loggerName) + { + mock.Setup(m => m.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>()! + )).Callback(new InvocationAction(invocation => + { + var logLevel = (LogLevel)invocation.Arguments[0]; + var eventId = (EventId)invocation.Arguments[1]; + var state = invocation.Arguments[2]; + var exception = (Exception)invocation.Arguments[3]; + var formatter = invocation.Arguments[4]; + + var invokeMethod = formatter.GetType().GetMethod("Invoke"); + var logMessage = invokeMethod!.Invoke(formatter, [state, exception]); + LoggerMessages.AddMessage($"[{DateTime.Now:HH:mm:ss:fff}] {loggerName} ({logLevel}): {logMessage}"); + })); + } + } +} diff --git a/Jung.SimpleWebSocketTest/Mock/LoggerMessages.cs b/Jung.SimpleWebSocketTest/Mock/LoggerMessages.cs new file mode 100644 index 0000000..b5cc9b3 --- /dev/null +++ b/Jung.SimpleWebSocketTest/Mock/LoggerMessages.cs @@ -0,0 +1,25 @@ + +namespace Jung.SimpleWebSocketTest.Mock +{ + internal static class LoggerMessages + { + private static readonly object _lock = new object(); + internal static List Messages { get; } = []; + + internal static void AddMessage(string message) + { + lock (_lock) + { + Messages.Add(message); + } + } + + internal static string[] GetMessages() + { + lock (_lock) + { + return [.. Messages]; + } + } + } +} diff --git a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs index a4209dc..e86696c 100644 --- a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs +++ b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs @@ -3,8 +3,7 @@ using Jung.SimpleWebSocket; using Jung.SimpleWebSocket.Models; -using Microsoft.Extensions.Logging; -using Moq; +using Jung.SimpleWebSocketTest.Mock; using NUnit.Framework; using System.Diagnostics; using System.Net; @@ -18,9 +17,8 @@ namespace Jung.SimpleWebSocketTest [TestFixture] public class SimpleWebSocketTest { - private readonly List _logMessages = []; - private Mock> _serverLogger; - private Mock> _clientLogger; + private ILoggerMockHelper _serverLoggerMockHelper; + private ILoggerMockHelper _clientLoggerMockHelper; [OneTimeSetUp] public void SetUpOnce() @@ -31,11 +29,8 @@ public void SetUpOnce() [SetUp] public void SetUp() { - _logMessages.Clear(); - _serverLogger = new Mock>(); - _clientLogger = new Mock>(); - SetUpLogger(_serverLogger, "Server"); - SetUpLogger(_clientLogger, "Client"); + _serverLoggerMockHelper = new("Server"); + _clientLoggerMockHelper = new("Client"); } [OneTimeTearDown] @@ -44,27 +39,7 @@ public void EndTest() Trace.Flush(); } - private void SetUpLogger(Mock> mock, string loggerName) - { - mock.Setup(m => m.Log( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>()! - )).Callback(new InvocationAction(invocation => - { - var logLevel = (LogLevel)invocation.Arguments[0]; - var eventId = (EventId)invocation.Arguments[1]; - var state = invocation.Arguments[2]; - var exception = (Exception)invocation.Arguments[3]; - var formatter = invocation.Arguments[4]; - - var invokeMethod = formatter.GetType().GetMethod("Invoke"); - var logMessage = invokeMethod!.Invoke(formatter, [state, exception]); - _logMessages.Add($"[{DateTime.Now:HH:mm:ss:fff}] {loggerName} ({logLevel}): {logMessage}"); - })); - } + [Test] [Platform("Windows7,Windows8,Windows8.1,Windows10", Reason = "This test establishes a TCP client-server connection using SimpleWebSocket, which relies on specific networking features and behaviors that are only available and consistent on Windows platforms. Running this test on non-Windows platforms could lead to inconsistent results or failures due to differences in networking stack implementations.")] @@ -78,8 +53,8 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() RememberDisconnectedClients = true, }; - using var server = new SimpleWebSocketServer(serverOptions, _serverLogger.Object); - using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", logger: _clientLogger.Object); + using var server = new SimpleWebSocketServer(serverOptions, _serverLoggerMockHelper.Logger); + using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", logger: _clientLoggerMockHelper.Logger); const string Message = "Hello World"; @@ -148,14 +123,14 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() WaitForManualResetEventOrThrow(disconnectResetEvent); // test if the server accepts the client again - var client2 = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", client.UserId, logger: _clientLogger.Object); + var client2 = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", client.UserId, logger: _clientLoggerMockHelper.Logger); await client2.ConnectAsync(); await Task.Delay(100); try { // test if two clients with the same user id can connect - var client3 = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", client.UserId, logger: _clientLogger.Object); + var client3 = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", client.UserId, logger: _clientLoggerMockHelper.Logger); await client3.ConnectAsync(); } catch (Exception exception) @@ -166,7 +141,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() await client2.SendMessageAsync("Hello World"); await server.ShutdownServer(CancellationToken.None); - _logMessages.ForEach(m => Trace.WriteLine(m)); + Array.ForEach(LoggerMessages.GetMessages(), m => Trace.WriteLine(m)); // Assert Assert.Multiple(() => @@ -206,8 +181,8 @@ public async Task TestClientServerConnection_ShouldRemoveClientFromPassiveClient PassiveClientLifetime = TimeSpan.FromSeconds(1) }; - using var server = new SimpleWebSocketServer(serverOptions, _serverLogger.Object); - using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", userId, _clientLogger.Object); + using var server = new SimpleWebSocketServer(serverOptions, _serverLoggerMockHelper.Logger); + using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", userId, _clientLoggerMockHelper.Logger); var expiredClientId = string.Empty; var expiredClientResetEvent = new ManualResetEvent(false); @@ -227,7 +202,7 @@ public async Task TestClientServerConnection_ShouldRemoveClientFromPassiveClient WaitForManualResetEventOrThrow(expiredClientResetEvent, 2000); await server.ShutdownServer(CancellationToken.None); - _logMessages.ForEach(m => Trace.WriteLine(m)); + Array.ForEach(LoggerMessages.GetMessages(), m => Trace.WriteLine(m)); // Assert Assert.That(expiredClientId, Is.EqualTo(userId)); @@ -244,8 +219,8 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld2() Port = 8010 }; - using var server = new SimpleWebSocketServer(serverOptions, _serverLogger.Object); - using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", logger: _clientLogger.Object); + using var server = new SimpleWebSocketServer(serverOptions, _serverLoggerMockHelper.Logger); + using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", logger: _clientLoggerMockHelper.Logger); const string Message = "Hello World"; @@ -292,7 +267,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld2() await server.ShutdownServer(CancellationToken.None); WaitForManualResetEventOrThrow(disconnectResetEvent, 100); - _logMessages.ForEach(m => Trace.WriteLine(m)); + Array.ForEach(LoggerMessages.GetMessages(), m => Trace.WriteLine(m)); // Assert Assert.Multiple(() => From 4d97a19cb41c7504e5f4482f4bd19065915f750d Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 29 Jan 2025 17:38:27 +0100 Subject: [PATCH 11/16] Rename tcp listener field from _server to _tcpListener for better unterstading of its purpose Remove unnecessary parameter from WebSocketServerClient consturctor Improve client handling flow tests --- .../Flows/ClientHandlingFlow.cs | 8 +++---- .../Models/WebSocketServerClient.cs | 13 ++++++------ Jung.SimpleWebSocket/SimpleWebSocketServer.cs | 21 ++++++++++--------- .../ClientHandlingFlowTest.cs | 15 ++++++++----- 4 files changed, 31 insertions(+), 26 deletions(-) diff --git a/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs b/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs index c688226..ad5e3ab 100644 --- a/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs +++ b/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs @@ -11,7 +11,7 @@ using System.Collections.Concurrent; using System.Net; -namespace Jung.SimpleWebSocket +namespace Jung.SimpleWebSocket.Flows { /// /// A flow that handles the client connection. @@ -163,14 +163,14 @@ private void ThrowForUserAlreadyConnected() } /// - /// Accepts the websocket connection. + /// Accepts the web socket connection. /// internal async Task AcceptWebSocketAsync() { // The client is accepted await _upgradeHandler.AcceptWebSocketAsync(Request, _responseContext, Client.Id, null, _cancellationToken); - // Use the websocket for the client + // Use the web socket for the client Client.UseWebSocket(_upgradeHandler.CreateWebSocket(isServer: true)); // Set the flag that the client was accepted @@ -179,7 +179,7 @@ internal async Task AcceptWebSocketAsync() } /// - /// Rejects the websocket connection. + /// Rejects the web socket connection. /// /// The response context to send to the client. internal async Task RejectWebSocketAsync(WebContext responseContext) diff --git a/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs b/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs index e3a40d9..f48c7c3 100644 --- a/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs +++ b/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs @@ -54,19 +54,18 @@ public class WebSocketServerClient : IDisposable /// /// The connection of the client. internal WebSocketServerClient(ITcpClient clientConnection) + : base() { - FirstSeen = DateTime.UtcNow; - LastConnectionTimestamp = FirstSeen; ClientConnection = clientConnection; } /// - /// Constructor for unit tests + /// Initializes a new instance of the class without a client connection. /// - /// The time the user was first seen. - internal WebSocketServerClient(DateTime firstSeen) + internal WebSocketServerClient() { - FirstSeen = LastConnectionTimestamp = firstSeen; + FirstSeen = DateTime.UtcNow; + LastConnectionTimestamp = FirstSeen; } /// @@ -82,7 +81,7 @@ internal void UpdateClient(ITcpClient client) /// /// Updates the client with a new WebSocket. /// - /// The websocket that the client should use. + /// The web socket that the client should use. internal void UseWebSocket(IWebSocket? webSocket) { ArgumentNullException.ThrowIfNull(webSocket); diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index 7e1d652..13d28b9 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -4,6 +4,7 @@ using Jung.SimpleWebSocket.Contracts; using Jung.SimpleWebSocket.Delegates; using Jung.SimpleWebSocket.Exceptions; +using Jung.SimpleWebSocket.Flows; using Jung.SimpleWebSocket.Models; using Jung.SimpleWebSocket.Models.EventArguments; using Jung.SimpleWebSocket.Utility; @@ -59,7 +60,7 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable public int ClientCount => ActiveClients.Count; /// - public bool IsListening => _server?.IsListening ?? false; + public bool IsListening => _tcpListener?.IsListening ?? false; /// /// A logger to write internal log messages. @@ -89,7 +90,7 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable /// /// The server that listens for incoming connection attempts. /// - private ITcpListener? _server; + private ITcpListener? _tcpListener; /// /// Initializes a new instance of the class that listens @@ -153,7 +154,7 @@ public SimpleWebSocketServer(IOptions options, ILo internal SimpleWebSocketServer(SimpleWebSocketServerOptions options, ITcpListener tcpListener, ILogger? logger = null) : this(options, logger) { - _server = tcpListener; + _tcpListener = tcpListener; } /// @@ -166,8 +167,8 @@ public void Start(CancellationToken? cancellationToken = null) _cancellationTokenSource = new CancellationTokenSource(); var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken.Value, _cancellationTokenSource.Token); - _server ??= new TcpListenerWrapper(LocalIpAddress, Port); - _server.Start(); + _tcpListener ??= new TcpListenerWrapper(LocalIpAddress, Port); + _tcpListener.Start(); _ = Task.Run(async delegate { Logger?.LogInformation("Server started at {LocalIpAddress}:{Port}", LocalIpAddress, Port); @@ -176,7 +177,7 @@ public void Start(CancellationToken? cancellationToken = null) try { // Accept the client - var client = await _server.AcceptTcpClientAsync(linkedTokenSource.Token); + var client = await _tcpListener.AcceptTcpClientAsync(linkedTokenSource.Token); Logger?.LogDebug("Client connected from {endpoint}", client.ClientConnection!.RemoteEndPoint); @@ -219,8 +220,8 @@ public async Task ShutdownServer(CancellationToken? cancellationToken = null) } _cancellationTokenSource?.Cancel(); - _server?.Dispose(); - _server = null; + _tcpListener?.Dispose(); + _tcpListener = null; Logger?.LogInformation("Server stopped"); } @@ -392,8 +393,8 @@ private void PassiveClients_ItemExpired(object? sender, ItemExpiredArgs(); - var client = new WebSocketServerClient(DateTime.Now); + + var client = new WebSocketServerClient(); client.UpdateId(userId); activeUsers.Add(client); + var requestText = CreateUpgradeRequest(userId); var serverOptions = new SimpleWebSocketServerOptions { From 8902f123587030da8467bfac884a2f73852e16aa Mon Sep 17 00:00:00 2001 From: Christoph Date: Sat, 1 Feb 2025 21:25:28 +0100 Subject: [PATCH 12/16] Remove code that handles user remembering --- .../Contracts/IWebSocketClient.cs | 5 - .../Contracts/IWebSocketServer.cs | 6 - .../PassiveUserExpiredEventHandler.cs | 13 - .../Flows/ClientHandlingFlow.cs | 118 +------- .../PassiveUserExpiredEventArgs.cs | 10 - .../Models/SimpleWebSocketServerOptions.cs | 31 +- Jung.SimpleWebSocket/Models/WebContext.cs | 28 +- Jung.SimpleWebSocket/SimpleWebSocketClient.cs | 13 +- Jung.SimpleWebSocket/SimpleWebSocketServer.cs | 86 +----- .../Utility/ExpiringDictionary.cs | 281 ------------------ .../Wrappers/WebSocketUpgradeHandler.cs | 3 +- .../ClientHandlingFlowTest.cs | 53 +--- .../SimpleWebSocketTest.cs | 60 +--- .../WebSocketUpgradeHandlerTests.cs | 4 +- 14 files changed, 32 insertions(+), 679 deletions(-) delete mode 100644 Jung.SimpleWebSocket/Delegates/PassiveUserExpiredEventHandler.cs delete mode 100644 Jung.SimpleWebSocket/Models/EventArguments/PassiveUserExpiredEventArgs.cs delete mode 100644 Jung.SimpleWebSocket/Utility/ExpiringDictionary.cs diff --git a/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs b/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs index 5bad1b4..1a73240 100644 --- a/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs +++ b/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs @@ -30,11 +30,6 @@ public interface IWebSocketClient : IDisposable /// bool IsConnected { get; } - /// - /// The user id of the client. If not set, the server did not sent a user id at websocket upgrade. - /// - string? UserId { get; } - /// /// Event that is raised when a message is received from a client. /// diff --git a/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs b/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs index 5add6a8..f4973a2 100644 --- a/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs +++ b/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs @@ -4,7 +4,6 @@ using Jung.SimpleWebSocket.Delegates; using Jung.SimpleWebSocket.Models; using Jung.SimpleWebSocket.Models.EventArguments; -using Jung.SimpleWebSocket.Utility; using System.Net; namespace Jung.SimpleWebSocket.Contracts; @@ -59,11 +58,6 @@ public interface IWebSocketServer : IDisposable /// event EventHandler? BinaryMessageReceived; - /// - /// Occurs when an passive user expired. - /// - event EventHandler? PassiveUserExpiredEvent; - /// /// Async Event that is raised when a client upgrade request is received. /// diff --git a/Jung.SimpleWebSocket/Delegates/PassiveUserExpiredEventHandler.cs b/Jung.SimpleWebSocket/Delegates/PassiveUserExpiredEventHandler.cs deleted file mode 100644 index de46577..0000000 --- a/Jung.SimpleWebSocket/Delegates/PassiveUserExpiredEventHandler.cs +++ /dev/null @@ -1,13 +0,0 @@ -// This file is part of the Jung SimpleWebSocket project. -// The project is licensed under the MIT license. - -using Jung.SimpleWebSocket.Models.EventArguments; - -namespace Jung.SimpleWebSocket.Delegates; - -/// -/// The event handler for the passive user expired event. -/// -/// The sender of the event. -/// The arguments of the event. -public delegate void PassiveUserExpiredEventHandler(object sender, PassiveUserExpiredArgs e); \ No newline at end of file diff --git a/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs b/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs index ad5e3ab..e41e93e 100644 --- a/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs +++ b/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs @@ -2,14 +2,11 @@ // The project is licensed under the MIT license. using Jung.SimpleWebSocket.Delegates; -using Jung.SimpleWebSocket.Exceptions; using Jung.SimpleWebSocket.Models; using Jung.SimpleWebSocket.Models.EventArguments; using Jung.SimpleWebSocket.Utility; using Microsoft.Extensions.Logging; -using System; using System.Collections.Concurrent; -using System.Net; namespace Jung.SimpleWebSocket.Flows { @@ -44,31 +41,11 @@ internal class ClientHandlingFlow(SimpleWebSocketServer server, WebSocketServerC /// private WebContext _responseContext = null!; - /// - /// Gets a value indicating whether the client was accepted. - /// - private bool _clientAccepted; - - /// - /// Gets a value indicating whether the client was a passive client. - /// - private bool _clientWasPassiveClient; - - /// - /// Gets the options of the server. - /// - private readonly SimpleWebSocketServerOptions _options = server.Options; - /// /// Gets the active clients of the server. /// private readonly ConcurrentDictionary _activeClients = server.ActiveClients; - /// - /// Gets the passive clients of the server. - /// - private readonly IDictionary _passiveClients = server.PassiveClients; - /// /// Gets the logger of the server. /// @@ -94,88 +71,16 @@ internal async Task LoadRequestContext() Request = await _upgradeHandler.AwaitContextAsync(_cancellationToken); } - /// - /// Handles the client identification. - /// - internal void HandleClientIdentification() - { - // Check if disconnected clients are remembered and can be reactivated - if (_options.RememberDisconnectedClients) - { - // Check if the request contains a user id - if (Request.ContainsUserId) - { - _logger?.LogDebug("User id found in request: {userId}", Request.UserId); - - lock (_clientLock) - { - ThrowForUserAlreadyConnected(); - - // Check if the client is an existing passive client - var clientExists = _passiveClients.ContainsKey(Request.UserId); - if (clientExists) - { - _logger?.LogDebug("Passive Client found for user id {userId} - reactivating user.", Request.UserId); - - // Use the existing client - // Update the client with the new connection - // Remove the client from the passive clients - var passiveClient = _passiveClients[Request.UserId]; - passiveClient.UpdateClient(Client.ClientConnection!); - Client = passiveClient; - var clientRemoved = _passiveClients.Remove(Request.UserId); - - // Set the flag that the client was a passive client - // This should only be set if the client was removed in this specific flow - // Otherwise its possible that the client is handled twice - _clientWasPassiveClient = clientRemoved; - } - else - { - // Client is not a passive client - // Update the clients user id - Client.UpdateId(Request.UserId); - } - } - } - } - } - - /// - /// Throws an exception if the user is already connected. - /// - /// - private void ThrowForUserAlreadyConnected() - { - // No passive client found, checking for active clients with the same id - if (_activeClients.ContainsKey(Request.UserId)) - { - _logger?.LogDebug("Active Client found for user id {userId} - rejecting connection.", Request.UserId); - // Reject the connection - - var responseContext = new WebContext - { - StatusCode = HttpStatusCode.Conflict, - BodyContent = "User id already in use" - }; - throw new UserNotHandledException(responseContext); - } - } - /// /// Accepts the web socket connection. /// internal async Task AcceptWebSocketAsync() { // The client is accepted - await _upgradeHandler.AcceptWebSocketAsync(Request, _responseContext, Client.Id, null, _cancellationToken); + await _upgradeHandler.AcceptWebSocketAsync(Request, _responseContext, null, _cancellationToken); // Use the web socket for the client Client.UseWebSocket(_upgradeHandler.CreateWebSocket(isServer: true)); - - // Set the flag that the client was accepted - // This is used to determine if the client should be added to the passive clients after disconnect - _clientAccepted = true; } /// @@ -192,23 +97,12 @@ internal async Task RejectWebSocketAsync(WebContext responseContext) /// internal void HandleDisconnectedClient() { - if (_clientWasPassiveClient || _clientAccepted) + lock (_clientLock) { - lock (_clientLock) - { - _activeClients.TryRemove(Client.Id, out _); - Client.Dispose(); - - if (_options.RememberDisconnectedClients) - { - _logger?.LogDebug("Client {clientId} is now a passive user.", Client.Id); - _passiveClients.Add(Client.Id, Client); - } - else - { - _logger?.LogDebug("Client {clientId} is removed.", Client.Id); - } - } + _activeClients.TryRemove(Client.Id, out _); + Client.Dispose(); + + _logger?.LogDebug("Client {clientId} is removed.", Client.Id); } } diff --git a/Jung.SimpleWebSocket/Models/EventArguments/PassiveUserExpiredEventArgs.cs b/Jung.SimpleWebSocket/Models/EventArguments/PassiveUserExpiredEventArgs.cs deleted file mode 100644 index 19cd99a..0000000 --- a/Jung.SimpleWebSocket/Models/EventArguments/PassiveUserExpiredEventArgs.cs +++ /dev/null @@ -1,10 +0,0 @@ -// This file is part of the Jung SimpleWebSocket project. -// The project is licensed under the MIT license. - -namespace Jung.SimpleWebSocket.Models.EventArguments; - -/// -/// Represents the arguments of the event when a passive user expired. -/// -/// The identifier of the user that expired. -public record PassiveUserExpiredArgs(string ClientId); \ No newline at end of file diff --git a/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs b/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs index 7e5a486..ea58fde 100644 --- a/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs +++ b/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs @@ -1,4 +1,7 @@ -using System.Net; +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using System.Net; namespace Jung.SimpleWebSocket.Models { @@ -16,29 +19,5 @@ public class SimpleWebSocketServerOptions /// Gets or sets the port of the server. /// public int Port { get; set; } - - /// - /// Switch for remembering disconnected clients. - /// - /// - /// If true the server will put disconnected clients into a passive client list. - /// This clients can reidentify themselves with their user id. - /// - public bool RememberDisconnectedClients { get; set; } = false; - - /// - /// Switch for removing passive clients after the end of the . - /// - public bool RemovePassiveClientsAfterClientExpirationTime { get; set; } = false; - - /// - /// Switch for sending the user id to the client. - /// - public bool SendUserIdToClient { get; set; } = false; - - /// - /// The time after which a passive client is removed from the passive client list. - /// - public TimeSpan PassiveClientLifetime { get; set; } = TimeSpan.FromMinutes(1); } -} \ No newline at end of file +} diff --git a/Jung.SimpleWebSocket/Models/WebContext.cs b/Jung.SimpleWebSocket/Models/WebContext.cs index 3b7626c..589ab61 100644 --- a/Jung.SimpleWebSocket/Models/WebContext.cs +++ b/Jung.SimpleWebSocket/Models/WebContext.cs @@ -238,9 +238,8 @@ private NameValueCollection ParseHeaders() /// The host name of the web request. /// The port of the web request. /// The request path of the web request. - /// The user id of the web request. /// The created web request context. - internal static WebContext CreateRequest(string hostName, int port, string requestPath, string? userId = null) + internal static WebContext CreateRequest(string hostName, int port, string requestPath) { var context = new WebContext() { @@ -249,12 +248,7 @@ internal static WebContext CreateRequest(string hostName, int port, string reque RequestPath = requestPath, }; - if (userId != null) - { - context.Headers.Add("x-user-id", userId); - } - - return context; + return context; } /// @@ -397,24 +391,6 @@ public static string GetStatusDescription(HttpStatusCode statusCode) return string.Join(" ", _splitByUppercaseRegex.Split(enumName)); } - - /// - /// Gets the user id of the web request. - /// - public string UserId - { - get - { - var userId = Headers["x-user-id"] ?? throw new WebSocketUpgradeException("UserId header is missing"); - return userId; - } - } - - /// - /// Gets a value indicating whether the web request contains a user id. - /// - public bool ContainsUserId => Headers["x-user-id"] != null; - /// /// Gets the content lines of the web request. /// diff --git a/Jung.SimpleWebSocket/SimpleWebSocketClient.cs b/Jung.SimpleWebSocket/SimpleWebSocketClient.cs index 2138cd6..6793e3d 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketClient.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketClient.cs @@ -22,9 +22,8 @@ namespace Jung.SimpleWebSocket /// The host name to connect to /// The port to connect to /// The web socket request path - /// The user id of the client. This is normally created by the server and sent back to the client /// A logger to write internal log messages - public class SimpleWebSocketClient(string hostName, int port, string requestPath, string? userId = null, ILogger? logger = null) : IWebSocketClient, IDisposable + public class SimpleWebSocketClient(string hostName, int port, string requestPath, ILogger? logger = null) : IWebSocketClient, IDisposable { /// public string HostName { get; } = hostName; @@ -33,9 +32,6 @@ public class SimpleWebSocketClient(string hostName, int port, string requestPath /// public string RequestPath { get; } = requestPath; - /// - public string? UserId { get; private set; } - /// public bool IsConnected => _client?.Connected ?? false; @@ -154,16 +150,11 @@ private async Task HandleWebSocketInitiation(TcpClientWrapper client, Cancellati _stream = client.GetStream(); var socketWrapper = new WebSocketUpgradeHandler(_stream); - var requestContext = WebContext.CreateRequest(HostName, Port, RequestPath, userId); + var requestContext = WebContext.CreateRequest(HostName, Port, RequestPath); await socketWrapper.SendUpgradeRequestAsync(requestContext, cancellationToken); var response = await socketWrapper.AwaitContextAsync(cancellationToken); WebSocketUpgradeHandler.ValidateUpgradeResponse(response, requestContext); - if (response.ContainsUserId) - { - UserId = response.UserId; - } - _webSocket = socketWrapper.CreateWebSocket(isServer: false); } diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index 13d28b9..04f6d1a 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -15,19 +15,24 @@ using System.Net; using System.Net.WebSockets; using System.Text; -using System.Threading; namespace Jung.SimpleWebSocket { /// /// A simple WebSocket server. /// - public class SimpleWebSocketServer : IWebSocketServer, IDisposable + /// + /// Initializes a new instance of the class that listens + /// for incoming connection attempts on the specified local IP address and port number. + /// + /// The options for the server + /// A logger to write internal log messages + public class SimpleWebSocketServer(SimpleWebSocketServerOptions options, ILogger? logger = null) : IWebSocketServer, IDisposable { /// - public IPAddress LocalIpAddress { get; } + public IPAddress LocalIpAddress { get; } = options.LocalIpAddress; /// - public int Port { get; } + public int Port { get; } = options.Port; /// public event EventHandler? ClientConnected; @@ -37,8 +42,6 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable public event EventHandler? MessageReceived; /// public event EventHandler? BinaryMessageReceived; - /// - public event EventHandler? PassiveUserExpiredEvent; /// public event AsyncEventHandler? ClientUpgradeRequestReceivedAsync; @@ -48,11 +51,6 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable /// internal ConcurrentDictionary ActiveClients { get; } = []; - /// - /// A dictionary of passive clients. - /// - internal IDictionary PassiveClients { get; set; } = null!; - /// public string[] ClientIds => [.. ActiveClients.Keys]; @@ -65,12 +63,12 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable /// /// A logger to write internal log messages. /// - internal ILogger? Logger { get; } + internal ILogger? Logger { get; } = logger; /// /// The options for the server. /// - internal SimpleWebSocketServerOptions Options { get; } + internal SimpleWebSocketServerOptions Options { get; } = options; /// /// A flag indicating whether the server is started. @@ -92,48 +90,6 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable /// private ITcpListener? _tcpListener; - /// - /// Initializes a new instance of the class that listens - /// for incoming connection attempts on the specified local IP address and port number. - /// - /// The options for the server - /// A logger to write internal log messages - public SimpleWebSocketServer(SimpleWebSocketServerOptions options, ILogger? logger = null) - { - LocalIpAddress = options.LocalIpAddress; - Port = options.Port; - Logger = logger; - Options = options; - InitializePassiveClientDictionary(options); - } - - /// - /// Initializes the passive clients dictionary. - /// - /// - private void InitializePassiveClientDictionary(SimpleWebSocketServerOptions options) - { - if (options.RememberDisconnectedClients) - { - // Initialize the passive clients dictionary - if (options.RemovePassiveClientsAfterClientExpirationTime) - { - var passiveClients = new ExpiringDictionary(options.PassiveClientLifetime, Logger); - passiveClients.ItemExpired += PassiveClients_ItemExpired; - PassiveClients = passiveClients; - } - else - { - PassiveClients = new Dictionary(); - } - } - else - { - // If user handling is not activated, the passive clients are not needed - PassiveClients = null!; - } - } - /// /// Initializes a new instance of the class that listens /// for incoming connection attempts on the specified local IP address and port number. @@ -271,9 +227,6 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT // Load the request context await flow.LoadRequestContext(); - // Handle the client user identification if activated - flow.HandleClientIdentification(); - // raise async client upgrade request received event var eventArgs = await flow.RaiseUpgradeEventAsync(ClientUpgradeRequestReceivedAsync); @@ -372,23 +325,6 @@ private async Task ProcessWebSocketMessagesAsync(WebSocketServerClient client, C } } - /// - /// Handles the event when a passive user expired. - /// - /// - /// Condition: is set to true. - /// - /// The sender of the event () - /// The arguments of the event - private void PassiveClients_ItemExpired(object? sender, ItemExpiredArgs e) - { - Logger?.LogDebug("Passive Client expired: {clientId}", e.Item.Id); - - // Raise the event asynchronously - // We don't want to block the cleanup process - AsyncEventRaiser.RaiseAsyncInNewTask(PassiveUserExpiredEvent, this, new PassiveUserExpiredArgs(e.Item.Id), _cancellationTokenSource.Token); - } - /// public void Dispose() { diff --git a/Jung.SimpleWebSocket/Utility/ExpiringDictionary.cs b/Jung.SimpleWebSocket/Utility/ExpiringDictionary.cs deleted file mode 100644 index 52d3131..0000000 --- a/Jung.SimpleWebSocket/Utility/ExpiringDictionary.cs +++ /dev/null @@ -1,281 +0,0 @@ -// This file is part of the Jung SimpleWebSocket project. -// The project is licensed under the MIT license. - -using Jung.SimpleWebSocket.Models.EventArguments; -using Microsoft.Extensions.Logging; -using System.Collections; -using System.Diagnostics.CodeAnalysis; - -namespace Jung.SimpleWebSocket.Utility; - -/// -/// Creates a dictionary with expiration time for each item. -/// -/// The type of the keys in the dictionary. -/// The type of the values in the dictionary. -/// The expiration time for each item. -/// The logger to log exceptions. -public class ExpiringDictionary(TimeSpan expiration, ILogger? logger = null): IDictionary where TKey : class -{ - - /// - /// Occurs when an item is expired. - /// - public event EventHandler>? ItemExpired; - - private readonly SortedList _expirationQueue = []; - private readonly Dictionary _dictionary = []; - - private bool _cleanupInProgress = false; - - /// - /// Add the specified key and value to the dictionary. - /// - /// - /// - public void Add(TKey key, TValue value) - { - lock (_dictionary) - { - // Add the item to the dictionary - _dictionary[key] = value; - - // Add item with expiration time to the queue - var expirationTime = DateTime.Now.Add(expiration); - - lock (_expirationQueue) - { - _expirationQueue.Add(expirationTime, key); - } - } - - // Trigger cleanup after the Add operation is done - lock (_expirationQueue) - { - if (!_cleanupInProgress) - { - _cleanupInProgress = true; - - // Run cleanup asynchronously - CleanupExpiredItems().ContinueWith(t => - { - if (t.IsFaulted) - { - // Handle exceptions here - logger?.LogError(t.Exception, "An Exception occurred during cleanup expired items."); - } - }); - } - } - } - - /// - /// Determines whether the dictionary contains the specified key. - /// - /// - /// - public bool ContainsKey(TKey key) - { - lock (_dictionary) - { - return _dictionary.ContainsKey(key); - } - } - /// - /// Removes the value with the specified key from the dictionary. - /// - /// The key of the value to remove. - /// Returns true if the element is successfully found and removed; otherwise, false. - public bool Remove(TKey key) - { - lock (_dictionary) - { - if (_dictionary.Remove(key)) - { - lock (_expirationQueue) - { - // Find and remove the expiration time entry for this key - var expirationTime = _expirationQueue.FirstOrDefault(x => x.Value.Equals(key)).Key; - if (expirationTime != default) - { - _expirationQueue.Remove(expirationTime); - } - } - return true; - } - } - return false; - } - - /// - /// Get or set the value associated with the specified key. - /// - /// The key of the value to get or set. - /// The value associated with the specified key. - public TValue this[TKey key] - { - get - { - lock (_dictionary) - { - return _dictionary[key]; - } - } - set - { - lock (_dictionary) - { - _dictionary[key] = value; - var expirationTime = DateTime.Now.Add(expiration); - lock (_expirationQueue) - { - _expirationQueue[expirationTime] = key; - } - } - } - } - - /// - /// Cleans up expired items. - /// - /// A task that represents the asynchronous cleanup operation. - private async Task CleanupExpiredItems() - { - while (true) - { - DateTime nearestExpiration; - TKey expiredKey; - - // Safely lock and retrieve the first item to expire - lock (_expirationQueue) - { - // If there are no items, stop the cleanup process - if (_expirationQueue.Count == 0) - { - _cleanupInProgress = false; - return; - } - - // Get the first key in expiration queue (FIFO order) - var firstItem = _expirationQueue.First(); - nearestExpiration = firstItem.Key; - expiredKey = firstItem.Value; - } - - // Calculate the delay based on the expiration time - TimeSpan delay = nearestExpiration - DateTime.Now; - - if (delay > TimeSpan.Zero) - { - await Task.Delay(delay); - } - - // Remove the expired item from the dictionary - lock (_dictionary) - { - if (_dictionary.TryGetValue(expiredKey, out var expiredItem)) - { - ItemExpired?.Invoke(this, new ItemExpiredArgs(expiredItem)); - _dictionary.Remove(expiredKey); - } - } - - // Remove from expiration queue, but only if it's still the correct key - lock (_expirationQueue) - { - if (_expirationQueue.Count > 0 && _expirationQueue.First().Value.Equals(expiredKey)) - { - _expirationQueue.RemoveAt(0); // Safely remove the correct item - } - } - } - } - - #region NotImplemented - - /// - /// Not implemented. - /// - public ICollection Keys => throw new NotImplementedException(); - - /// - /// Not implemented. - /// - public ICollection Values => throw new NotImplementedException(); - - /// - /// Not implemented. - /// - public int Count => throw new NotImplementedException(); - - /// - /// Not implemented. - /// - public bool IsReadOnly => throw new NotImplementedException(); - - /// - /// Not implemented. - /// - public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) - { - throw new NotImplementedException(); - } - - /// - /// Not implemented. - /// - public void Add(KeyValuePair item) - { - throw new NotImplementedException(); - } - - /// - /// Not implemented. - /// - public void Clear() - { - throw new NotImplementedException(); - } - - /// - /// Not implemented. - /// - public bool Contains(KeyValuePair item) - { - throw new NotImplementedException(); - } - - /// - /// Not implemented. - /// - public void CopyTo(KeyValuePair[] array, int arrayIndex) - { - throw new NotImplementedException(); - } - - /// - /// Not implemented. - /// - public bool Remove(KeyValuePair item) - { - throw new NotImplementedException(); - } - - /// - /// Not implemented. - /// - public IEnumerator> GetEnumerator() - { - throw new NotImplementedException(); - } - - /// - /// Not implemented. - /// - IEnumerator IEnumerable.GetEnumerator() - { - throw new NotImplementedException(); - } - - #endregion -} \ No newline at end of file diff --git a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs index 416a4aa..8a8e122 100644 --- a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs +++ b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs @@ -63,7 +63,7 @@ public async Task AwaitContextAsync(CancellationToken cancellationTo return context; } - public async Task AcceptWebSocketAsync(WebContext request, WebContext response, string userId, string? subProtocol, CancellationToken cancellationToken) + public async Task AcceptWebSocketAsync(WebContext request, WebContext response, string? subProtocol, CancellationToken cancellationToken) { try { @@ -79,7 +79,6 @@ public async Task AcceptWebSocketAsync(WebContext request, WebContext response, response.Headers.Add("Upgrade", "websocket"); response.Headers.Add("Sec-WebSocket-Accept", secWebSocketAcceptString); response.StatusCode = HttpStatusCode.SwitchingProtocols; - response.Headers.Add(_userIdHeaderName, userId); await SendWebSocketResponseHeaders(response, cancellationToken); _acceptedProtocol = subProtocol; } diff --git a/Jung.SimpleWebSocketTest/ClientHandlingFlowTest.cs b/Jung.SimpleWebSocketTest/ClientHandlingFlowTest.cs index 0e8916e..c9ba4eb 100644 --- a/Jung.SimpleWebSocketTest/ClientHandlingFlowTest.cs +++ b/Jung.SimpleWebSocketTest/ClientHandlingFlowTest.cs @@ -1,6 +1,5 @@ using Jung.SimpleWebSocket; using Jung.SimpleWebSocket.Contracts; -using Jung.SimpleWebSocket.Exceptions; using Jung.SimpleWebSocket.Flows; using Jung.SimpleWebSocket.Models; using Jung.SimpleWebSocketTest.Mock; @@ -41,7 +40,7 @@ private ClientHandlingFlow SetupClientHandlingFlow(object serverOptions, List(); - - var client = new WebSocketServerClient(); - client.UpdateId(userId); - activeUsers.Add(client); - - var requestText = CreateUpgradeRequest(userId); - var serverOptions = new SimpleWebSocketServerOptions - { - RememberDisconnectedClients = true, - }; - - // act and assert - var clientHandlingFlow = SetupClientHandlingFlow(serverOptions, activeUsers); - - clientHandlingFlow.Request = new WebContext(requestText); - Assert.That(() => clientHandlingFlow.HandleClientIdentification(), Throws.Exception.TypeOf()); - } } } diff --git a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs index e86696c..dc061e7 100644 --- a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs +++ b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs @@ -50,7 +50,6 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() { LocalIpAddress = IPAddress.Any, Port = 8010, - RememberDisconnectedClients = true, }; using var server = new SimpleWebSocketServer(serverOptions, _serverLoggerMockHelper.Logger); @@ -61,7 +60,6 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() const string ClosingStatusDescription = "closing status test description"; string receivedMessage = string.Empty; string receivedClosingDescription = string.Empty; - string exceptionMessage = string.Empty; var messageResetEvent = new ManualResetEvent(false); var disconnectResetEvent = new ManualResetEvent(false); @@ -123,21 +121,11 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() WaitForManualResetEventOrThrow(disconnectResetEvent); // test if the server accepts the client again - var client2 = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", client.UserId, logger: _clientLoggerMockHelper.Logger); + var client2 = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", logger: _clientLoggerMockHelper.Logger); await client2.ConnectAsync(); await Task.Delay(100); - try - { - // test if two clients with the same user id can connect - var client3 = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", client.UserId, logger: _clientLoggerMockHelper.Logger); - await client3.ConnectAsync(); - } - catch (Exception exception) - { - exceptionMessage = exception.InnerException!.Message; - } - + await client2.SendMessageAsync("Hello World"); await server.ShutdownServer(CancellationToken.None); @@ -148,7 +136,6 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() { Assert.That(receivedMessage, Is.EqualTo(Message)); Assert.That(receivedClosingDescription, Is.EqualTo(ClosingStatusDescription)); - Assert.That(exceptionMessage, Does.Contain("User id already in use")); }); } @@ -165,49 +152,6 @@ private static async Task DbContext_IpAddresses_Contains(IPAddress ipAddre return ipAddress.Equals(IPAddress.Loopback); } - - [Test] - [Platform("Windows7,Windows8,Windows8.1,Windows10", Reason = "This test establishes a TCP client-server connection using SimpleWebSocket, which relies on specific networking features and behaviors that are only available and consistent on Windows platforms. Running this test on non-Windows platforms could lead to inconsistent results or failures due to differences in networking stack implementations.")] - public async Task TestClientServerConnection_ShouldRemoveClientFromPassiveClients() - { - // Arrange - string userId = Guid.NewGuid().ToString(); - var serverOptions = new SimpleWebSocketServerOptions - { - LocalIpAddress = IPAddress.Any, - Port = 8010, - RememberDisconnectedClients = true, - RemovePassiveClientsAfterClientExpirationTime = true, - PassiveClientLifetime = TimeSpan.FromSeconds(1) - }; - - using var server = new SimpleWebSocketServer(serverOptions, _serverLoggerMockHelper.Logger); - using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", userId, _clientLoggerMockHelper.Logger); - - var expiredClientId = string.Empty; - var expiredClientResetEvent = new ManualResetEvent(false); - - server.PassiveUserExpiredEvent += (sender, args) => - { - expiredClientId = args.ClientId; - expiredClientResetEvent.Set(); - }; - - // Act - server.Start(); - await client.ConnectAsync(); - await Task.Delay(100); - await client.DisconnectAsync(); - - WaitForManualResetEventOrThrow(expiredClientResetEvent, 2000); - - await server.ShutdownServer(CancellationToken.None); - Array.ForEach(LoggerMessages.GetMessages(), m => Trace.WriteLine(m)); - - // Assert - Assert.That(expiredClientId, Is.EqualTo(userId)); - } - [Test] [Platform("Windows7,Windows8,Windows8.1,Windows10", Reason = "This test establishes a TCP client-server connection using SimpleWebSocket, which relies on specific networking features and behaviors that are only available and consistent on Windows platforms. Running this test on non-Windows platforms could lead to inconsistent results or failures due to differences in networking stack implementations.")] public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld2() diff --git a/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs b/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs index aa69078..158ce22 100644 --- a/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs +++ b/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs @@ -64,7 +64,7 @@ public async Task AcceptWebSocketAsync_ShouldSendUpgradeResponse(string hostname _mockNetworkStream.Setup(ns => ns.WriteAsync(It.IsAny(), It.IsAny())).Callback((buffer, ct) => { response = Encoding.UTF8.GetString(buffer); }); // Act - await _socketWrapper.AcceptWebSocketAsync(request,new WebContext(), Guid.NewGuid().ToString(), null, cancellationToken); + await _socketWrapper.AcceptWebSocketAsync(request, new WebContext(), null, cancellationToken); // Assert Assert.That(response, Does.Contain("HTTP/1.1 101 Switching Protocols")); @@ -90,7 +90,7 @@ public async Task AcceptWebSocketAsync_ShouldSendUpgradeResponseWithCorrectProto _mockNetworkStream.Setup(ns => ns.WriteAsync(It.IsAny(), It.IsAny())).Callback((buffer, ct) => { response = Encoding.UTF8.GetString(buffer); }); // Act - await _socketWrapper.AcceptWebSocketAsync(request, new WebContext(), Guid.NewGuid().ToString(), serverSubprotocol, cancellationToken); + await _socketWrapper.AcceptWebSocketAsync(request, new WebContext(), serverSubprotocol, cancellationToken); // Assert Assert.Multiple(() => From 57c4fb80fd9fb911b40c3c3cb9c0b7da0f73e7c6 Mon Sep 17 00:00:00 2001 From: Christoph Date: Sun, 2 Feb 2025 09:47:59 +0100 Subject: [PATCH 13/16] Fix some spelling issues --- .../Wrappers/WebSocketUpgradeHandler.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs index 8a8e122..5d9f87b 100644 --- a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs +++ b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs @@ -22,11 +22,10 @@ internal partial class WebSocketUpgradeHandler { private const string _supportedVersion = "13"; private const string _webSocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - private const string _userIdHeaderName = "x-user-id"; private string? _acceptedProtocol; private readonly INetworkStream _networkStream; - private readonly WebSocketHelper _websocketHelper; + private readonly WebSocketHelper _webSocketHelper; // Regex for a valid request path: must start with a `/` and can include valid path characters. [GeneratedRegex(@"^\/[a-zA-Z0-9\-._~\/]*$", RegexOptions.Compiled)] @@ -36,13 +35,13 @@ internal partial class WebSocketUpgradeHandler public WebSocketUpgradeHandler(INetworkStream networkStream) { _networkStream = networkStream; - _websocketHelper = new WebSocketHelper(); + _webSocketHelper = new WebSocketHelper(); } - internal WebSocketUpgradeHandler(INetworkStream networkStream, WebSocketHelper websocketHelper) + internal WebSocketUpgradeHandler(INetworkStream networkStream, WebSocketHelper webSocketHelper) { _networkStream = networkStream; - _websocketHelper = websocketHelper; + _webSocketHelper = webSocketHelper; } public async Task AwaitContextAsync(CancellationToken cancellationToken) @@ -88,7 +87,7 @@ public async Task AcceptWebSocketAsync(WebContext request, WebContext response, } catch (Exception message) { - throw new WebSocketException("Error while accepting the websocket", message); + throw new WebSocketException("Error while accepting the web socket", message); } } @@ -295,7 +294,7 @@ private static string ComputeWebSocketAccept(string secWebSocketKey) internal IWebSocket CreateWebSocket(bool isServer, TimeSpan? keepAliveInterval = null) { keepAliveInterval ??= TimeSpan.FromSeconds(30); - return _websocketHelper.CreateFromStream(_networkStream.Stream, isServer, _acceptedProtocol, keepAliveInterval.Value); + return _webSocketHelper.CreateFromStream(_networkStream.Stream, isServer, _acceptedProtocol, keepAliveInterval.Value); } internal async Task RejectWebSocketAsync(WebContext response, CancellationToken cancellationToken) From 8998849e144d58fc9f8785e301abed8192a35b42 Mon Sep 17 00:00:00 2001 From: Christoph Date: Sun, 2 Feb 2025 09:54:49 +0100 Subject: [PATCH 14/16] Add method to change the id of a user to the web socket server --- .../Contracts/IWebSocketServer.cs | 10 +++ .../ClientIdAlreadyExistsException.cs | 13 ++++ .../Exceptions/ClientNotFoundException.cs | 13 ++++ Jung.SimpleWebSocket/SimpleWebSocketServer.cs | 14 ++++ .../SimpleWebSocketTest.cs | 76 ++++++++++++++++++- 5 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 Jung.SimpleWebSocket/Exceptions/ClientIdAlreadyExistsException.cs create mode 100644 Jung.SimpleWebSocket/Exceptions/ClientNotFoundException.cs diff --git a/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs b/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs index f4973a2..a12093e 100644 --- a/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs +++ b/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs @@ -2,6 +2,7 @@ // The project is licensed under the MIT license. using Jung.SimpleWebSocket.Delegates; +using Jung.SimpleWebSocket.Exceptions; using Jung.SimpleWebSocket.Models; using Jung.SimpleWebSocket.Models.EventArguments; using System.Net; @@ -92,4 +93,13 @@ public interface IWebSocketServer : IDisposable /// The cancellation token. /// A task representing the asynchronous operation. void Start(CancellationToken? cancellationToken = null); + + /// + /// Changes the id of a client. + /// + /// The client to update + /// The new id of the client + /// Throws when the client is not found + /// Throws when the new id is already in use + void ChangeClientId(WebSocketServerClient client, string newId); } diff --git a/Jung.SimpleWebSocket/Exceptions/ClientIdAlreadyExistsException.cs b/Jung.SimpleWebSocket/Exceptions/ClientIdAlreadyExistsException.cs new file mode 100644 index 0000000..91635d6 --- /dev/null +++ b/Jung.SimpleWebSocket/Exceptions/ClientIdAlreadyExistsException.cs @@ -0,0 +1,13 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +namespace Jung.SimpleWebSocket.Exceptions +{ + /// + /// Exception thrown when a client with the same id already exists in the client list. + /// + /// The message to display when the exception is thrown. + public class ClientIdAlreadyExistsException(string message) : Exception(message) + { + } +} diff --git a/Jung.SimpleWebSocket/Exceptions/ClientNotFoundException.cs b/Jung.SimpleWebSocket/Exceptions/ClientNotFoundException.cs new file mode 100644 index 0000000..58d4a95 --- /dev/null +++ b/Jung.SimpleWebSocket/Exceptions/ClientNotFoundException.cs @@ -0,0 +1,13 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +namespace Jung.SimpleWebSocket.Exceptions +{ + /// + /// Exception thrown when a client with the given id is not found in the client list. + /// + /// The message to display when the exception is thrown. + public class ClientNotFoundException(string message) : Exception(message) + { + } +} diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index 04f6d1a..c2cd853 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -213,6 +213,20 @@ public WebSocketServerClient GetClientById(string clientId) return client; } + /// + public void ChangeClientId(WebSocketServerClient client, string newId) + { + // if the client is not found or the new id is already in use, throw an exception + if (!ActiveClients.TryGetValue(client.Id, out var _)) throw new ClientNotFoundException(message: "A client with the given id was not found"); + if (ActiveClients.ContainsKey(newId)) throw new ClientIdAlreadyExistsException(message: "A client with the new id already exists"); + + // because the id is used as a key in the dictionary, + // we have to remove the client and add it again with the new id + ActiveClients.TryRemove(client.Id, out _); + client.UpdateId(newId); + ActiveClients.TryAdd(newId, client); + } + /// /// Handles the client connection. /// diff --git a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs index dc061e7..8f9bcb3 100644 --- a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs +++ b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs @@ -2,6 +2,7 @@ // The project is licensed under the MIT license. using Jung.SimpleWebSocket; +using Jung.SimpleWebSocket.Exceptions; using Jung.SimpleWebSocket.Models; using Jung.SimpleWebSocketTest.Mock; using NUnit.Framework; @@ -39,7 +40,80 @@ public void EndTest() Trace.Flush(); } + [Test] + public void ChangeClientId_UserIdUnique_ShouldUpdateId() + { + // Arrange + var serverOptions = new SimpleWebSocketServerOptions + { + LocalIpAddress = IPAddress.Any, + Port = 8010, + }; + + var connectedClient1 = new WebSocketServerClient(); + var connectedClient2 = new WebSocketServerClient(); + var oldId = connectedClient1.Id; + + using var server = new SimpleWebSocketServer(serverOptions, _serverLoggerMockHelper.Logger); + if (!server.ActiveClients.TryAdd(connectedClient1.Id, connectedClient1) || + !server.ActiveClients.TryAdd(connectedClient2.Id, connectedClient2)) + { + throw new Exception("Could not add clients to the server."); + } + // Act + var newId = Guid.NewGuid().ToString(); + server.ChangeClientId(connectedClient1, newId); + + // Assert + Assert.Multiple(() => + { + Assert.That(connectedClient1.Id, Is.EqualTo(newId)); + Assert.That(server.ActiveClients.ContainsKey(oldId), Is.False); + Assert.That(server.ActiveClients.ContainsKey(newId), Is.True); + }); + } + + [Test] + public void ChangeClientId_UserIdDuplicated_ShouldThrowException() + { + // Arrange + var serverOptions = new SimpleWebSocketServerOptions + { + LocalIpAddress = IPAddress.Any, + Port = 8010, + }; + + var connectedClient1 = new WebSocketServerClient(); + var connectedClient2 = new WebSocketServerClient(); + + + using var server = new SimpleWebSocketServer(serverOptions, _serverLoggerMockHelper.Logger); + if (!server.ActiveClients.TryAdd(connectedClient1.Id, connectedClient1) || + !server.ActiveClients.TryAdd(connectedClient2.Id, connectedClient2)) + { + throw new Exception("Could not add clients to the server."); + } + + // Act & Assert + Assert.That(() => server.ChangeClientId(connectedClient1, connectedClient2.Id), Throws.Exception.TypeOf()); + } + + [Test] + public void ChangeClientId_TargetUserNotExisting_ShouldThrowException() + { + // Arrange + var serverOptions = new SimpleWebSocketServerOptions + { + LocalIpAddress = IPAddress.Any, + Port = 8010, + }; + + using var server = new SimpleWebSocketServer(serverOptions, _serverLoggerMockHelper.Logger); + + // Act & Assert + Assert.That(() => server.ChangeClientId(new WebSocketServerClient(), Guid.NewGuid().ToString()), Throws.Exception.TypeOf()); + } [Test] [Platform("Windows7,Windows8,Windows8.1,Windows10", Reason = "This test establishes a TCP client-server connection using SimpleWebSocket, which relies on specific networking features and behaviors that are only available and consistent on Windows platforms. Running this test on non-Windows platforms could lead to inconsistent results or failures due to differences in networking stack implementations.")] @@ -125,7 +199,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() await client2.ConnectAsync(); await Task.Delay(100); - + await client2.SendMessageAsync("Hello World"); await server.ShutdownServer(CancellationToken.None); From e30b32ab5275863d03cf7388ffecc04048ab4df5 Mon Sep 17 00:00:00 2001 From: Christoph Jung Date: Sat, 8 Feb 2025 19:00:10 +0100 Subject: [PATCH 15/16] Improve handling flow --- .../Flows/ClientHandlingFlow.cs | 58 +++++++++++++------ Jung.SimpleWebSocket/SimpleWebSocketServer.cs | 5 +- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs b/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs index e41e93e..5519676 100644 --- a/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs +++ b/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs @@ -7,6 +7,7 @@ using Jung.SimpleWebSocket.Utility; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; namespace Jung.SimpleWebSocket.Flows { @@ -29,17 +30,17 @@ internal class ClientHandlingFlow(SimpleWebSocketServer server, WebSocketServerC /// /// Gets the request context of the client. /// - internal WebContext Request { get; set; } = null!; + internal WebContext? Request { get; set; } = null!; /// /// Gets the upgrade handler for the client. /// - private WebSocketUpgradeHandler _upgradeHandler = null!; + private WebSocketUpgradeHandler? _upgradeHandler = null; /// /// Gets the response context that is being use to response to the client. /// - private WebContext _responseContext = null!; + private WebContext? _responseContext = null; /// /// Gets the active clients of the server. @@ -56,11 +57,6 @@ internal class ClientHandlingFlow(SimpleWebSocketServer server, WebSocketServerC /// private readonly CancellationToken _cancellationToken = cancellationToken; - /// - /// The lock object for the client dictionaries. - /// - private static readonly object _clientLock = new(); - /// /// Loads the request context. /// @@ -76,11 +72,15 @@ internal async Task LoadRequestContext() /// internal async Task AcceptWebSocketAsync() { + // Check if the response context are initialized + ThrowForResponseContextNotInitialized(_responseContext); + // The client is accepted - await _upgradeHandler.AcceptWebSocketAsync(Request, _responseContext, null, _cancellationToken); + await _upgradeHandler!.AcceptWebSocketAsync(Request!, _responseContext, null, _cancellationToken); // Use the web socket for the client Client.UseWebSocket(_upgradeHandler.CreateWebSocket(isServer: true)); + Cleanup(); } /// @@ -89,7 +89,9 @@ internal async Task AcceptWebSocketAsync() /// The response context to send to the client. internal async Task RejectWebSocketAsync(WebContext responseContext) { - await _upgradeHandler.RejectWebSocketAsync(responseContext, _cancellationToken); + // The client is rejected + await _upgradeHandler!.RejectWebSocketAsync(responseContext, _cancellationToken); + Cleanup(); } /// @@ -97,13 +99,10 @@ internal async Task RejectWebSocketAsync(WebContext responseContext) /// internal void HandleDisconnectedClient() { - lock (_clientLock) - { - _activeClients.TryRemove(Client.Id, out _); - Client.Dispose(); + _activeClients.TryRemove(Client.Id, out _); + Client.Dispose(); - _logger?.LogDebug("Client {clientId} is removed.", Client.Id); - } + _logger?.LogDebug("Client {clientId} is removed.", Client.Id); } /// @@ -113,7 +112,7 @@ internal void HandleDisconnectedClient() /// The event arguments of the upgrade request. internal async Task RaiseUpgradeEventAsync(AsyncEventHandler? clientUpgradeRequestReceivedAsync) { - var eventArgs = new ClientUpgradeRequestReceivedArgs(Client, Request, _logger); + var eventArgs = new ClientUpgradeRequestReceivedArgs(Client, Request!, _logger); await AsyncEventRaiser.RaiseAsync(clientUpgradeRequestReceivedAsync, server, eventArgs, _cancellationToken); _responseContext = eventArgs.ResponseContext; return eventArgs; @@ -127,5 +126,28 @@ internal bool TryAddClientToActiveUserList() { return _activeClients.TryAdd(Client.Id, Client); } + + /// + /// Throws an exception if the response context is not initialized. + /// + /// The response context to check. + /// + private static void ThrowForResponseContextNotInitialized([NotNull] WebContext? responseContext) + { + if (responseContext is null) + { + throw new InvalidOperationException("The response context is not initialized."); + } + } + + /// + /// Disposes the upgrade handler. + /// + private void Cleanup() + { + _upgradeHandler = null; + _responseContext = null; + Request = null; + } } -} \ No newline at end of file +} diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index c2cd853..4b298cf 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -241,7 +241,7 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT // Load the request context await flow.LoadRequestContext(); - // raise async client upgrade request received event + // Raise async client upgrade request received event var eventArgs = await flow.RaiseUpgradeEventAsync(ClientUpgradeRequestReceivedAsync); // Respond to the upgrade request @@ -249,6 +249,7 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT { // Accept the WebSocket connection await flow.AcceptWebSocketAsync(); + if (flow.TryAddClientToActiveUserList()) { Logger?.LogDebug("Connection upgraded, now listening on Client {clientId}", flow.Client.Id); @@ -258,7 +259,7 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT } else { - Logger?.LogDebug("Connection upgraded, now listening on Client {clientId}", flow.Client.Id); + Logger?.LogDebug("Error while adding Client {clientId} to active clients", flow.Client.Id); } } else From d26cbca23d62b38f5620d59c01d642562dd8d6dd Mon Sep 17 00:00:00 2001 From: Christoph Jung Date: Tue, 11 Feb 2025 20:05:53 +0100 Subject: [PATCH 16/16] Move projects in src and tests directories --- Jung.SimpleWebSocket.sln | 10 +++- .../ClientHandlingFlowTest.cs | 57 ------------------- .../Jung.SimpleWebSocket}/AssemblyInfo.cs | 2 +- .../Contracts/INetworkStream.cs | 0 .../Contracts/ITcpClient.cs | 0 .../Contracts/ITcpListener.cs | 0 .../Contracts/IWebSocket.cs | 0 .../Contracts/IWebSocketClient.cs | 0 .../Contracts/IWebSocketServer.cs | 0 .../Delegates/AsyncEventHandler.cs | 0 .../BinaryMessageReceivedEventHandler.cs | 0 ...ClientBinaryMessageReceivedEventHandler.cs | 0 .../Delegates/ClientConnectedEventHandler.cs | 0 .../ClientDisconnectedEventHandler.cs | 0 .../ClientMessageReceivedEventHandler.cs | 0 .../Delegates/DisconnectedEventHandler.cs | 0 .../Delegates/MessageReceivedEventHandler.cs | 0 .../ClientIdAlreadyExistsException.cs | 0 .../Exceptions/ClientNotFoundException.cs | 0 .../Exceptions/SimpleWebSocketException.cs | 0 .../Exceptions/UserNotHandledException.cs | 0 .../Exceptions/WebContextException.cs | 0 .../Exceptions/WebSocketClientException.cs | 0 .../Exceptions/WebSocketServerException.cs | 0 .../Exceptions/WebSocketUpgradeException.cs | 0 .../Flows/ClientHandlingFlow.cs | 0 .../Helpers/WebSocketHelper.cs | 0 .../Jung.SimpleWebSocket.csproj | 0 .../BinaryMessageReceivedArgs.cs | 0 .../ClientBinaryMessageReceivedArgs.cs | 0 .../EventArguments/ClientConnectedArgs.cs | 0 .../EventArguments/ClientDisconnectedArgs.cs | 0 .../ClientMessageReceivedArgs.cs | 0 .../ClientUpgradeRequestReceivedArgs.cs | 0 .../Models/EventArguments/DisconnectedArgs.cs | 0 .../Models/EventArguments/ItemExpiredArgs.cs | 0 .../EventArguments/MessageReceivedArgs.cs | 0 .../Models/SimpleWebSocketServerOptions.cs | 0 .../Models/WebContext.cs | 0 .../Models/WebSocketServerClient.cs | 0 .../SimpleWebSocketClient.cs | 0 .../SimpleWebSocketServer.cs | 0 .../Utility/AsyncEventRaiser.cs | 0 .../Wrappers/NetworkStreamWrapper.cs | 0 .../Wrappers/TcpClientWrapper.cs | 0 .../Wrappers/TcpListenerWrapper.cs | 0 .../Wrappers/WebSocketUpgradeHandler.cs | 0 .../Wrappers/WebSocketWrapper.cs | 0 .../Jung.SimpleWebSocket}/docs/README.md | 0 .../Jung.SimpleWebSocket.UnitTests.csproj | 2 +- .../Mock/ILoggerMockHelper.cs | 7 ++- .../Mock/LoggerMessages.cs | 6 +- .../SimpleWebSocketTest.cs | 7 +-- .../WebContextTest.cs | 5 +- .../WebSocketUpgradeHandlerTests.cs | 6 +- 55 files changed, 30 insertions(+), 72 deletions(-) delete mode 100644 Jung.SimpleWebSocketTest/ClientHandlingFlowTest.cs rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/AssemblyInfo.cs (94%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Contracts/INetworkStream.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Contracts/ITcpClient.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Contracts/ITcpListener.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Contracts/IWebSocket.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Contracts/IWebSocketClient.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Contracts/IWebSocketServer.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Delegates/AsyncEventHandler.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Delegates/BinaryMessageReceivedEventHandler.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Delegates/ClientBinaryMessageReceivedEventHandler.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Delegates/ClientConnectedEventHandler.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Delegates/ClientDisconnectedEventHandler.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Delegates/ClientMessageReceivedEventHandler.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Delegates/DisconnectedEventHandler.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Delegates/MessageReceivedEventHandler.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Exceptions/ClientIdAlreadyExistsException.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Exceptions/ClientNotFoundException.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Exceptions/SimpleWebSocketException.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Exceptions/UserNotHandledException.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Exceptions/WebContextException.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Exceptions/WebSocketClientException.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Exceptions/WebSocketServerException.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Exceptions/WebSocketUpgradeException.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Flows/ClientHandlingFlow.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Helpers/WebSocketHelper.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Jung.SimpleWebSocket.csproj (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Models/EventArguments/BinaryMessageReceivedArgs.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Models/EventArguments/ClientBinaryMessageReceivedArgs.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Models/EventArguments/ClientConnectedArgs.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Models/EventArguments/ClientDisconnectedArgs.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Models/EventArguments/ClientMessageReceivedArgs.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Models/EventArguments/DisconnectedArgs.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Models/EventArguments/ItemExpiredArgs.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Models/EventArguments/MessageReceivedArgs.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Models/SimpleWebSocketServerOptions.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Models/WebContext.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Models/WebSocketServerClient.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/SimpleWebSocketClient.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/SimpleWebSocketServer.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Utility/AsyncEventRaiser.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Wrappers/NetworkStreamWrapper.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Wrappers/TcpClientWrapper.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Wrappers/TcpListenerWrapper.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Wrappers/WebSocketUpgradeHandler.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Wrappers/WebSocketWrapper.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/docs/README.md (100%) rename Jung.SimpleWebSocketTest/Jung.SimpleWebSocketTest.csproj => tests/Jung.SimpleWebSocket.UnitTests/Jung.SimpleWebSocket.UnitTests.csproj (88%) rename {Jung.SimpleWebSocketTest => tests/Jung.SimpleWebSocket.UnitTests}/Mock/ILoggerMockHelper.cs (87%) rename {Jung.SimpleWebSocketTest => tests/Jung.SimpleWebSocket.UnitTests}/Mock/LoggerMessages.cs (76%) rename {Jung.SimpleWebSocketTest => tests/Jung.SimpleWebSocket.UnitTests}/SimpleWebSocketTest.cs (98%) rename {Jung.SimpleWebSocketTest => tests/Jung.SimpleWebSocket.UnitTests}/WebContextTest.cs (97%) rename {Jung.SimpleWebSocketTest => tests/Jung.SimpleWebSocket.UnitTests}/WebSocketUpgradeHandlerTests.cs (98%) diff --git a/Jung.SimpleWebSocket.sln b/Jung.SimpleWebSocket.sln index ed5da37..b5af771 100644 --- a/Jung.SimpleWebSocket.sln +++ b/Jung.SimpleWebSocket.sln @@ -3,9 +3,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.11.35222.181 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jung.SimpleWebSocket", "Jung.SimpleWebSocket\Jung.SimpleWebSocket.csproj", "{793B04E9-6326-425A-A29C-A736CFD1E0C0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jung.SimpleWebSocket", "src\Jung.SimpleWebSocket\Jung.SimpleWebSocket.csproj", "{793B04E9-6326-425A-A29C-A736CFD1E0C0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jung.SimpleWebSocketTest", "Jung.SimpleWebSocketTest\Jung.SimpleWebSocketTest.csproj", "{26725C3C-8E90-49AC-9EE4-2A77ADB2229D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jung.SimpleWebSocket.UnitTests", "tests\Jung.SimpleWebSocket.UnitTests\Jung.SimpleWebSocket.UnitTests.csproj", "{26725C3C-8E90-49AC-9EE4-2A77ADB2229D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jung.SimpleWebSocket.IntegrationTests", "tests\Jung.SimpleWebSocket.IntegrationTests\Jung.SimpleWebSocket.IntegrationTests.csproj", "{144CF5BC-D92F-4BC4-80E9-A2FABA93C8A2}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -21,6 +23,10 @@ Global {26725C3C-8E90-49AC-9EE4-2A77ADB2229D}.Debug|Any CPU.Build.0 = Debug|Any CPU {26725C3C-8E90-49AC-9EE4-2A77ADB2229D}.Release|Any CPU.ActiveCfg = Release|Any CPU {26725C3C-8E90-49AC-9EE4-2A77ADB2229D}.Release|Any CPU.Build.0 = Release|Any CPU + {144CF5BC-D92F-4BC4-80E9-A2FABA93C8A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {144CF5BC-D92F-4BC4-80E9-A2FABA93C8A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {144CF5BC-D92F-4BC4-80E9-A2FABA93C8A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {144CF5BC-D92F-4BC4-80E9-A2FABA93C8A2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Jung.SimpleWebSocketTest/ClientHandlingFlowTest.cs b/Jung.SimpleWebSocketTest/ClientHandlingFlowTest.cs deleted file mode 100644 index c9ba4eb..0000000 --- a/Jung.SimpleWebSocketTest/ClientHandlingFlowTest.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Jung.SimpleWebSocket; -using Jung.SimpleWebSocket.Contracts; -using Jung.SimpleWebSocket.Flows; -using Jung.SimpleWebSocket.Models; -using Jung.SimpleWebSocketTest.Mock; -using Microsoft.Extensions.Logging; -using Moq; -using NUnit.Framework; -using System.Text; - -namespace Jung.SimpleWebSocketTest -{ - [TestFixture] - internal class ClientHandlingFlowTest - { - private ILogger _logger; - - [SetUp] - public void SetUp() - { - var loggerHelper = new ILoggerMockHelper("Server"); - _logger = loggerHelper.Logger; - } - - private ClientHandlingFlow SetupClientHandlingFlow(object serverOptions, List? activeClients = null) - { - var tcpListener = new Mock(); - var serverMoq = new Mock(serverOptions, tcpListener.Object, _logger); - if (activeClients != null) - { - foreach (var client in activeClients) - { - serverMoq.Object.ActiveClients.TryAdd(client.Id, client); - } - } - - var tcpClientMoq = new Mock(); - var serverClientMoq = new WebSocketServerClient(tcpClientMoq.Object); - - return new ClientHandlingFlow(serverMoq.Object, serverClientMoq, CancellationToken.None); - } - - private string CreateUpgradeRequest() - { - var sb = new StringBuilder(); - sb.Append("GET /chat HTTP/1.1\r\n" + - "Host: localhost:8080\r\n" + - "Upgrade: websocket\r\n" + - "Connection: Upgrade\r\n" + - "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n" + - "Sec-WebSocket-Version: 13\r\n"); - - sb.Append("\r\n\r\n"); - return sb.ToString(); - } - } -} diff --git a/Jung.SimpleWebSocket/AssemblyInfo.cs b/src/Jung.SimpleWebSocket/AssemblyInfo.cs similarity index 94% rename from Jung.SimpleWebSocket/AssemblyInfo.cs rename to src/Jung.SimpleWebSocket/AssemblyInfo.cs index dddbe37..44a7198 100644 --- a/Jung.SimpleWebSocket/AssemblyInfo.cs +++ b/src/Jung.SimpleWebSocket/AssemblyInfo.cs @@ -21,5 +21,5 @@ [assembly: Guid("ca34219d-7a2e-4993-ad9d-f27fda1bb9dc")] // Make internals visible to the test project and the dynamic proxy assembly (moq) -[assembly: InternalsVisibleTo("Jung.SimpleWebSocketTest")] +[assembly: InternalsVisibleTo("Jung.SimpleWebSocket.UnitTests")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file diff --git a/Jung.SimpleWebSocket/Contracts/INetworkStream.cs b/src/Jung.SimpleWebSocket/Contracts/INetworkStream.cs similarity index 100% rename from Jung.SimpleWebSocket/Contracts/INetworkStream.cs rename to src/Jung.SimpleWebSocket/Contracts/INetworkStream.cs diff --git a/Jung.SimpleWebSocket/Contracts/ITcpClient.cs b/src/Jung.SimpleWebSocket/Contracts/ITcpClient.cs similarity index 100% rename from Jung.SimpleWebSocket/Contracts/ITcpClient.cs rename to src/Jung.SimpleWebSocket/Contracts/ITcpClient.cs diff --git a/Jung.SimpleWebSocket/Contracts/ITcpListener.cs b/src/Jung.SimpleWebSocket/Contracts/ITcpListener.cs similarity index 100% rename from Jung.SimpleWebSocket/Contracts/ITcpListener.cs rename to src/Jung.SimpleWebSocket/Contracts/ITcpListener.cs diff --git a/Jung.SimpleWebSocket/Contracts/IWebSocket.cs b/src/Jung.SimpleWebSocket/Contracts/IWebSocket.cs similarity index 100% rename from Jung.SimpleWebSocket/Contracts/IWebSocket.cs rename to src/Jung.SimpleWebSocket/Contracts/IWebSocket.cs diff --git a/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs b/src/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs similarity index 100% rename from Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs rename to src/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs diff --git a/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs b/src/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs similarity index 100% rename from Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs rename to src/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs diff --git a/Jung.SimpleWebSocket/Delegates/AsyncEventHandler.cs b/src/Jung.SimpleWebSocket/Delegates/AsyncEventHandler.cs similarity index 100% rename from Jung.SimpleWebSocket/Delegates/AsyncEventHandler.cs rename to src/Jung.SimpleWebSocket/Delegates/AsyncEventHandler.cs diff --git a/Jung.SimpleWebSocket/Delegates/BinaryMessageReceivedEventHandler.cs b/src/Jung.SimpleWebSocket/Delegates/BinaryMessageReceivedEventHandler.cs similarity index 100% rename from Jung.SimpleWebSocket/Delegates/BinaryMessageReceivedEventHandler.cs rename to src/Jung.SimpleWebSocket/Delegates/BinaryMessageReceivedEventHandler.cs diff --git a/Jung.SimpleWebSocket/Delegates/ClientBinaryMessageReceivedEventHandler.cs b/src/Jung.SimpleWebSocket/Delegates/ClientBinaryMessageReceivedEventHandler.cs similarity index 100% rename from Jung.SimpleWebSocket/Delegates/ClientBinaryMessageReceivedEventHandler.cs rename to src/Jung.SimpleWebSocket/Delegates/ClientBinaryMessageReceivedEventHandler.cs diff --git a/Jung.SimpleWebSocket/Delegates/ClientConnectedEventHandler.cs b/src/Jung.SimpleWebSocket/Delegates/ClientConnectedEventHandler.cs similarity index 100% rename from Jung.SimpleWebSocket/Delegates/ClientConnectedEventHandler.cs rename to src/Jung.SimpleWebSocket/Delegates/ClientConnectedEventHandler.cs diff --git a/Jung.SimpleWebSocket/Delegates/ClientDisconnectedEventHandler.cs b/src/Jung.SimpleWebSocket/Delegates/ClientDisconnectedEventHandler.cs similarity index 100% rename from Jung.SimpleWebSocket/Delegates/ClientDisconnectedEventHandler.cs rename to src/Jung.SimpleWebSocket/Delegates/ClientDisconnectedEventHandler.cs diff --git a/Jung.SimpleWebSocket/Delegates/ClientMessageReceivedEventHandler.cs b/src/Jung.SimpleWebSocket/Delegates/ClientMessageReceivedEventHandler.cs similarity index 100% rename from Jung.SimpleWebSocket/Delegates/ClientMessageReceivedEventHandler.cs rename to src/Jung.SimpleWebSocket/Delegates/ClientMessageReceivedEventHandler.cs diff --git a/Jung.SimpleWebSocket/Delegates/DisconnectedEventHandler.cs b/src/Jung.SimpleWebSocket/Delegates/DisconnectedEventHandler.cs similarity index 100% rename from Jung.SimpleWebSocket/Delegates/DisconnectedEventHandler.cs rename to src/Jung.SimpleWebSocket/Delegates/DisconnectedEventHandler.cs diff --git a/Jung.SimpleWebSocket/Delegates/MessageReceivedEventHandler.cs b/src/Jung.SimpleWebSocket/Delegates/MessageReceivedEventHandler.cs similarity index 100% rename from Jung.SimpleWebSocket/Delegates/MessageReceivedEventHandler.cs rename to src/Jung.SimpleWebSocket/Delegates/MessageReceivedEventHandler.cs diff --git a/Jung.SimpleWebSocket/Exceptions/ClientIdAlreadyExistsException.cs b/src/Jung.SimpleWebSocket/Exceptions/ClientIdAlreadyExistsException.cs similarity index 100% rename from Jung.SimpleWebSocket/Exceptions/ClientIdAlreadyExistsException.cs rename to src/Jung.SimpleWebSocket/Exceptions/ClientIdAlreadyExistsException.cs diff --git a/Jung.SimpleWebSocket/Exceptions/ClientNotFoundException.cs b/src/Jung.SimpleWebSocket/Exceptions/ClientNotFoundException.cs similarity index 100% rename from Jung.SimpleWebSocket/Exceptions/ClientNotFoundException.cs rename to src/Jung.SimpleWebSocket/Exceptions/ClientNotFoundException.cs diff --git a/Jung.SimpleWebSocket/Exceptions/SimpleWebSocketException.cs b/src/Jung.SimpleWebSocket/Exceptions/SimpleWebSocketException.cs similarity index 100% rename from Jung.SimpleWebSocket/Exceptions/SimpleWebSocketException.cs rename to src/Jung.SimpleWebSocket/Exceptions/SimpleWebSocketException.cs diff --git a/Jung.SimpleWebSocket/Exceptions/UserNotHandledException.cs b/src/Jung.SimpleWebSocket/Exceptions/UserNotHandledException.cs similarity index 100% rename from Jung.SimpleWebSocket/Exceptions/UserNotHandledException.cs rename to src/Jung.SimpleWebSocket/Exceptions/UserNotHandledException.cs diff --git a/Jung.SimpleWebSocket/Exceptions/WebContextException.cs b/src/Jung.SimpleWebSocket/Exceptions/WebContextException.cs similarity index 100% rename from Jung.SimpleWebSocket/Exceptions/WebContextException.cs rename to src/Jung.SimpleWebSocket/Exceptions/WebContextException.cs diff --git a/Jung.SimpleWebSocket/Exceptions/WebSocketClientException.cs b/src/Jung.SimpleWebSocket/Exceptions/WebSocketClientException.cs similarity index 100% rename from Jung.SimpleWebSocket/Exceptions/WebSocketClientException.cs rename to src/Jung.SimpleWebSocket/Exceptions/WebSocketClientException.cs diff --git a/Jung.SimpleWebSocket/Exceptions/WebSocketServerException.cs b/src/Jung.SimpleWebSocket/Exceptions/WebSocketServerException.cs similarity index 100% rename from Jung.SimpleWebSocket/Exceptions/WebSocketServerException.cs rename to src/Jung.SimpleWebSocket/Exceptions/WebSocketServerException.cs diff --git a/Jung.SimpleWebSocket/Exceptions/WebSocketUpgradeException.cs b/src/Jung.SimpleWebSocket/Exceptions/WebSocketUpgradeException.cs similarity index 100% rename from Jung.SimpleWebSocket/Exceptions/WebSocketUpgradeException.cs rename to src/Jung.SimpleWebSocket/Exceptions/WebSocketUpgradeException.cs diff --git a/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs b/src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs similarity index 100% rename from Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs rename to src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs diff --git a/Jung.SimpleWebSocket/Helpers/WebSocketHelper.cs b/src/Jung.SimpleWebSocket/Helpers/WebSocketHelper.cs similarity index 100% rename from Jung.SimpleWebSocket/Helpers/WebSocketHelper.cs rename to src/Jung.SimpleWebSocket/Helpers/WebSocketHelper.cs diff --git a/Jung.SimpleWebSocket/Jung.SimpleWebSocket.csproj b/src/Jung.SimpleWebSocket/Jung.SimpleWebSocket.csproj similarity index 100% rename from Jung.SimpleWebSocket/Jung.SimpleWebSocket.csproj rename to src/Jung.SimpleWebSocket/Jung.SimpleWebSocket.csproj diff --git a/Jung.SimpleWebSocket/Models/EventArguments/BinaryMessageReceivedArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/BinaryMessageReceivedArgs.cs similarity index 100% rename from Jung.SimpleWebSocket/Models/EventArguments/BinaryMessageReceivedArgs.cs rename to src/Jung.SimpleWebSocket/Models/EventArguments/BinaryMessageReceivedArgs.cs diff --git a/Jung.SimpleWebSocket/Models/EventArguments/ClientBinaryMessageReceivedArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/ClientBinaryMessageReceivedArgs.cs similarity index 100% rename from Jung.SimpleWebSocket/Models/EventArguments/ClientBinaryMessageReceivedArgs.cs rename to src/Jung.SimpleWebSocket/Models/EventArguments/ClientBinaryMessageReceivedArgs.cs diff --git a/Jung.SimpleWebSocket/Models/EventArguments/ClientConnectedArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/ClientConnectedArgs.cs similarity index 100% rename from Jung.SimpleWebSocket/Models/EventArguments/ClientConnectedArgs.cs rename to src/Jung.SimpleWebSocket/Models/EventArguments/ClientConnectedArgs.cs diff --git a/Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs similarity index 100% rename from Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs rename to src/Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs diff --git a/Jung.SimpleWebSocket/Models/EventArguments/ClientMessageReceivedArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/ClientMessageReceivedArgs.cs similarity index 100% rename from Jung.SimpleWebSocket/Models/EventArguments/ClientMessageReceivedArgs.cs rename to src/Jung.SimpleWebSocket/Models/EventArguments/ClientMessageReceivedArgs.cs diff --git a/Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs similarity index 100% rename from Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs rename to src/Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs diff --git a/Jung.SimpleWebSocket/Models/EventArguments/DisconnectedArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/DisconnectedArgs.cs similarity index 100% rename from Jung.SimpleWebSocket/Models/EventArguments/DisconnectedArgs.cs rename to src/Jung.SimpleWebSocket/Models/EventArguments/DisconnectedArgs.cs diff --git a/Jung.SimpleWebSocket/Models/EventArguments/ItemExpiredArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/ItemExpiredArgs.cs similarity index 100% rename from Jung.SimpleWebSocket/Models/EventArguments/ItemExpiredArgs.cs rename to src/Jung.SimpleWebSocket/Models/EventArguments/ItemExpiredArgs.cs diff --git a/Jung.SimpleWebSocket/Models/EventArguments/MessageReceivedArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/MessageReceivedArgs.cs similarity index 100% rename from Jung.SimpleWebSocket/Models/EventArguments/MessageReceivedArgs.cs rename to src/Jung.SimpleWebSocket/Models/EventArguments/MessageReceivedArgs.cs diff --git a/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs b/src/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs similarity index 100% rename from Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs rename to src/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs diff --git a/Jung.SimpleWebSocket/Models/WebContext.cs b/src/Jung.SimpleWebSocket/Models/WebContext.cs similarity index 100% rename from Jung.SimpleWebSocket/Models/WebContext.cs rename to src/Jung.SimpleWebSocket/Models/WebContext.cs diff --git a/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs b/src/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs similarity index 100% rename from Jung.SimpleWebSocket/Models/WebSocketServerClient.cs rename to src/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs diff --git a/Jung.SimpleWebSocket/SimpleWebSocketClient.cs b/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs similarity index 100% rename from Jung.SimpleWebSocket/SimpleWebSocketClient.cs rename to src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs similarity index 100% rename from Jung.SimpleWebSocket/SimpleWebSocketServer.cs rename to src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs diff --git a/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs b/src/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs similarity index 100% rename from Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs rename to src/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs diff --git a/Jung.SimpleWebSocket/Wrappers/NetworkStreamWrapper.cs b/src/Jung.SimpleWebSocket/Wrappers/NetworkStreamWrapper.cs similarity index 100% rename from Jung.SimpleWebSocket/Wrappers/NetworkStreamWrapper.cs rename to src/Jung.SimpleWebSocket/Wrappers/NetworkStreamWrapper.cs diff --git a/Jung.SimpleWebSocket/Wrappers/TcpClientWrapper.cs b/src/Jung.SimpleWebSocket/Wrappers/TcpClientWrapper.cs similarity index 100% rename from Jung.SimpleWebSocket/Wrappers/TcpClientWrapper.cs rename to src/Jung.SimpleWebSocket/Wrappers/TcpClientWrapper.cs diff --git a/Jung.SimpleWebSocket/Wrappers/TcpListenerWrapper.cs b/src/Jung.SimpleWebSocket/Wrappers/TcpListenerWrapper.cs similarity index 100% rename from Jung.SimpleWebSocket/Wrappers/TcpListenerWrapper.cs rename to src/Jung.SimpleWebSocket/Wrappers/TcpListenerWrapper.cs diff --git a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs b/src/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs similarity index 100% rename from Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs rename to src/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs diff --git a/Jung.SimpleWebSocket/Wrappers/WebSocketWrapper.cs b/src/Jung.SimpleWebSocket/Wrappers/WebSocketWrapper.cs similarity index 100% rename from Jung.SimpleWebSocket/Wrappers/WebSocketWrapper.cs rename to src/Jung.SimpleWebSocket/Wrappers/WebSocketWrapper.cs diff --git a/Jung.SimpleWebSocket/docs/README.md b/src/Jung.SimpleWebSocket/docs/README.md similarity index 100% rename from Jung.SimpleWebSocket/docs/README.md rename to src/Jung.SimpleWebSocket/docs/README.md diff --git a/Jung.SimpleWebSocketTest/Jung.SimpleWebSocketTest.csproj b/tests/Jung.SimpleWebSocket.UnitTests/Jung.SimpleWebSocket.UnitTests.csproj similarity index 88% rename from Jung.SimpleWebSocketTest/Jung.SimpleWebSocketTest.csproj rename to tests/Jung.SimpleWebSocket.UnitTests/Jung.SimpleWebSocket.UnitTests.csproj index 90bd5a1..cfa0430 100644 --- a/Jung.SimpleWebSocketTest/Jung.SimpleWebSocketTest.csproj +++ b/tests/Jung.SimpleWebSocket.UnitTests/Jung.SimpleWebSocket.UnitTests.csproj @@ -18,7 +18,7 @@ - + diff --git a/Jung.SimpleWebSocketTest/Mock/ILoggerMockHelper.cs b/tests/Jung.SimpleWebSocket.UnitTests/Mock/ILoggerMockHelper.cs similarity index 87% rename from Jung.SimpleWebSocketTest/Mock/ILoggerMockHelper.cs rename to tests/Jung.SimpleWebSocket.UnitTests/Mock/ILoggerMockHelper.cs index e72d2fa..b784ee1 100644 --- a/Jung.SimpleWebSocketTest/Mock/ILoggerMockHelper.cs +++ b/tests/Jung.SimpleWebSocket.UnitTests/Mock/ILoggerMockHelper.cs @@ -1,7 +1,10 @@ -using Microsoft.Extensions.Logging; +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using Microsoft.Extensions.Logging; using Moq; -namespace Jung.SimpleWebSocketTest.Mock +namespace Jung.SimpleWebSocket.UnitTests.Mock { internal class ILoggerMockHelper where T : class { diff --git a/Jung.SimpleWebSocketTest/Mock/LoggerMessages.cs b/tests/Jung.SimpleWebSocket.UnitTests/Mock/LoggerMessages.cs similarity index 76% rename from Jung.SimpleWebSocketTest/Mock/LoggerMessages.cs rename to tests/Jung.SimpleWebSocket.UnitTests/Mock/LoggerMessages.cs index b5cc9b3..86af14e 100644 --- a/Jung.SimpleWebSocketTest/Mock/LoggerMessages.cs +++ b/tests/Jung.SimpleWebSocket.UnitTests/Mock/LoggerMessages.cs @@ -1,5 +1,7 @@ - -namespace Jung.SimpleWebSocketTest.Mock +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +namespace Jung.SimpleWebSocket.UnitTests.Mock { internal static class LoggerMessages { diff --git a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs b/tests/Jung.SimpleWebSocket.UnitTests/SimpleWebSocketTest.cs similarity index 98% rename from Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs rename to tests/Jung.SimpleWebSocket.UnitTests/SimpleWebSocketTest.cs index 8f9bcb3..2621a9f 100644 --- a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs +++ b/tests/Jung.SimpleWebSocket.UnitTests/SimpleWebSocketTest.cs @@ -1,19 +1,18 @@ // This file is part of the Jung SimpleWebSocket project. // The project is licensed under the MIT license. -using Jung.SimpleWebSocket; using Jung.SimpleWebSocket.Exceptions; using Jung.SimpleWebSocket.Models; -using Jung.SimpleWebSocketTest.Mock; +using Jung.SimpleWebSocket.UnitTests.Mock; using NUnit.Framework; using System.Diagnostics; using System.Net; using System.Runtime.CompilerServices; -// internals of the simple web socket are visible to the test project +// internals of the simple web socket project are visible to the test project // because of the InternalsVisibleTo attribute in the AssemblyInfo.cs -namespace Jung.SimpleWebSocketTest +namespace Jung.SimpleWebSocket.UnitTests { [TestFixture] public class SimpleWebSocketTest diff --git a/Jung.SimpleWebSocketTest/WebContextTest.cs b/tests/Jung.SimpleWebSocket.UnitTests/WebContextTest.cs similarity index 97% rename from Jung.SimpleWebSocketTest/WebContextTest.cs rename to tests/Jung.SimpleWebSocket.UnitTests/WebContextTest.cs index 7999dd7..cc3ad0e 100644 --- a/Jung.SimpleWebSocketTest/WebContextTest.cs +++ b/tests/Jung.SimpleWebSocket.UnitTests/WebContextTest.cs @@ -5,7 +5,10 @@ using Jung.SimpleWebSocket.Models; using NUnit.Framework; -namespace Jung.SimpleWebSocketTest +// internals of the simple web socket project are visible to the test project +// because of the InternalsVisibleTo attribute in the AssemblyInfo.cs + +namespace Jung.SimpleWebSocket.UnitTests { [TestFixture] internal class WebContextTest diff --git a/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs b/tests/Jung.SimpleWebSocket.UnitTests/WebSocketUpgradeHandlerTests.cs similarity index 98% rename from Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs rename to tests/Jung.SimpleWebSocket.UnitTests/WebSocketUpgradeHandlerTests.cs index 158ce22..503df3e 100644 --- a/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs +++ b/tests/Jung.SimpleWebSocket.UnitTests/WebSocketUpgradeHandlerTests.cs @@ -1,7 +1,6 @@ // This file is part of the Jung SimpleWebSocket project. // The project is licensed under the MIT license. -using Jung.SimpleWebSocket; using Jung.SimpleWebSocket.Contracts; using Jung.SimpleWebSocket.Exceptions; using Jung.SimpleWebSocket.Helpers; @@ -10,7 +9,10 @@ using NUnit.Framework; using System.Text; -namespace Jung.SimpleWebSocketTest +// internals of the simple web socket project are visible to the test project +// because of the InternalsVisibleTo attribute in the AssemblyInfo.cs + +namespace Jung.SimpleWebSocket.UnitTests { public class WebSocketUpgradeHandlerTests {