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.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 73%
rename from Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs
rename to src/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs
index 4e9e79b..a12093e 100644
--- a/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs
+++ b/src/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;
@@ -41,22 +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;
+
+ ///
+ /// Async Event that is raised when a client upgrade request is received.
+ ///
+ event AsyncEventHandler? ClientUpgradeRequestReceivedAsync;
///
/// Gets a client by its id.
@@ -87,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/src/Jung.SimpleWebSocket/Delegates/AsyncEventHandler.cs b/src/Jung.SimpleWebSocket/Delegates/AsyncEventHandler.cs
new file mode 100644
index 0000000..ba551fe
--- /dev/null
+++ b/src/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/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/src/Jung.SimpleWebSocket/Exceptions/ClientIdAlreadyExistsException.cs b/src/Jung.SimpleWebSocket/Exceptions/ClientIdAlreadyExistsException.cs
new file mode 100644
index 0000000..91635d6
--- /dev/null
+++ b/src/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/src/Jung.SimpleWebSocket/Exceptions/ClientNotFoundException.cs b/src/Jung.SimpleWebSocket/Exceptions/ClientNotFoundException.cs
new file mode 100644
index 0000000..58d4a95
--- /dev/null
+++ b/src/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/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/src/Jung.SimpleWebSocket/Exceptions/UserNotHandledException.cs b/src/Jung.SimpleWebSocket/Exceptions/UserNotHandledException.cs
new file mode 100644
index 0000000..148c6f7
--- /dev/null
+++ b/src/Jung.SimpleWebSocket/Exceptions/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.Exceptions
+{
+ [Serializable]
+ internal class UserNotHandledException(WebContext responseContext) : Exception
+ {
+ public WebContext ResponseContext { get; set; } = responseContext;
+ }
+}
\ No newline at end of file
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/src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs b/src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs
new file mode 100644
index 0000000..5519676
--- /dev/null
+++ b/src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs
@@ -0,0 +1,153 @@
+// 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.Collections.Concurrent;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Jung.SimpleWebSocket.Flows
+{
+ ///
+ /// 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 response context that is being use to response to the client.
+ ///
+ private WebContext? _responseContext = null;
+
+ ///
+ /// Gets the active clients of the server.
+ ///
+ private readonly ConcurrentDictionary _activeClients = server.ActiveClients;
+
+ ///
+ /// Gets the logger of the server.
+ ///
+ private readonly ILogger? _logger = server.Logger;
+
+ ///
+ /// Gets the cancellation token of the server.
+ ///
+ private readonly CancellationToken _cancellationToken = cancellationToken;
+
+ ///
+ /// Loads the request context.
+ ///
+ internal async Task LoadRequestContext()
+ {
+ var stream = Client.ClientConnection!.GetStream();
+ _upgradeHandler = new WebSocketUpgradeHandler(stream);
+ Request = await _upgradeHandler.AwaitContextAsync(_cancellationToken);
+ }
+
+ ///
+ /// Accepts the web socket connection.
+ ///
+ internal async Task AcceptWebSocketAsync()
+ {
+ // Check if the response context are initialized
+ ThrowForResponseContextNotInitialized(_responseContext);
+
+ // The client is accepted
+ await _upgradeHandler!.AcceptWebSocketAsync(Request!, _responseContext, null, _cancellationToken);
+
+ // Use the web socket for the client
+ Client.UseWebSocket(_upgradeHandler.CreateWebSocket(isServer: true));
+ Cleanup();
+ }
+
+ ///
+ /// Rejects the web socket connection.
+ ///
+ /// The response context to send to the client.
+ internal async Task RejectWebSocketAsync(WebContext responseContext)
+ {
+ // The client is rejected
+ await _upgradeHandler!.RejectWebSocketAsync(responseContext, _cancellationToken);
+ Cleanup();
+ }
+
+ ///
+ /// Handles the disconnected client.
+ ///
+ internal void HandleDisconnectedClient()
+ {
+ _activeClients.TryRemove(Client.Id, out _);
+ Client.Dispose();
+
+ _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);
+ }
+
+ ///
+ /// 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;
+ }
+ }
+}
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/src/Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs
new file mode 100644
index 0000000..0f238b0
--- /dev/null
+++ b/src/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/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/src/Jung.SimpleWebSocket/Models/EventArguments/ItemExpiredArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/ItemExpiredArgs.cs
new file mode 100644
index 0000000..f2b78be
--- /dev/null
+++ b/src/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/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/src/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs b/src/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs
new file mode 100644
index 0000000..ea58fde
--- /dev/null
+++ b/src/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs
@@ -0,0 +1,23 @@
+// This file is part of the Jung SimpleWebSocket project.
+// The project is licensed under the MIT license.
+
+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; }
+ }
+}
diff --git a/Jung.SimpleWebSocket/Models/WebContext.cs b/src/Jung.SimpleWebSocket/Models/WebContext.cs
similarity index 72%
rename from Jung.SimpleWebSocket/Models/WebContext.cs
rename to src/Jung.SimpleWebSocket/Models/WebContext.cs
index c2603f6..589ab61 100644
--- a/Jung.SimpleWebSocket/Models/WebContext.cs
+++ b/src/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)
{
///
@@ -44,7 +45,23 @@ internal class WebContext(string? content = null)
private string? _requestPath = null;
///
- /// Gets the headers of the web request.
+ /// The status code of the context.
+ ///
+ private HttpStatusCode? _statusCode;
+
+
+ // A Regular Expression to split a string by uppercase letters.
+ [GeneratedRegex(@"(?
+ /// The body content.
+ ///
+ private string? _bodyContent = null;
+
+ ///
+ /// Gets the headers.
///
public NameValueCollection Headers
{
@@ -55,6 +72,30 @@ public NameValueCollection Headers
}
}
+ ///
+ /// Gets or Sets the body content.
+ ///
+ 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.
///
@@ -140,6 +181,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.
///
@@ -173,24 +241,26 @@ private NameValueCollection ParseHeaders()
/// The created web request context.
internal static WebContext CreateRequest(string hostName, int port, string requestPath)
{
- return new WebContext()
+ var context = new WebContext()
{
HostName = hostName,
Port = port,
- RequestPath = requestPath
+ RequestPath = requestPath,
};
+
+ return context;
}
///
/// 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));
}
///
@@ -255,7 +325,6 @@ internal IEnumerable GetAllHeaderValues(string headerName)
}
}
-
///
/// Gets the concatenated headers of the web request.
///
@@ -300,6 +369,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/src/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs
similarity index 75%
rename from Jung.SimpleWebSocket/Models/WebSocketServerClient.cs
rename to src/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs
index d737b17..f48c7c3 100644
--- a/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs
+++ b/src/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs
@@ -16,10 +16,18 @@ 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.
///
- internal ITcpClient ClientConnection { get; private set; }
+ internal ITcpClient? ClientConnection { get; private set; }
///
/// Gets the timestamp when the WebSocket client was last seen.
@@ -34,7 +42,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.
@@ -46,10 +54,18 @@ public class WebSocketServerClient : IDisposable
///
/// The connection of the client.
internal WebSocketServerClient(ITcpClient clientConnection)
+ : base()
+ {
+ ClientConnection = clientConnection;
+ }
+
+ ///
+ /// Initializes a new instance of the class without a client connection.
+ ///
+ internal WebSocketServerClient()
{
FirstSeen = DateTime.UtcNow;
LastConnectionTimestamp = FirstSeen;
- ClientConnection = clientConnection;
}
///
@@ -62,6 +78,16 @@ internal void UpdateClient(ITcpClient client)
ClientConnection = client;
}
+ ///
+ /// Updates the client with a new WebSocket.
+ ///
+ /// The web socket 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 +108,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/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs
similarity index 99%
rename from Jung.SimpleWebSocket/SimpleWebSocketClient.cs
rename to src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs
index b690ab7..6793e3d 100644
--- a/Jung.SimpleWebSocket/SimpleWebSocketClient.cs
+++ b/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs
@@ -154,6 +154,7 @@ private async Task HandleWebSocketInitiation(TcpClientWrapper client, Cancellati
await socketWrapper.SendUpgradeRequestAsync(requestContext, cancellationToken);
var response = await socketWrapper.AwaitContextAsync(cancellationToken);
WebSocketUpgradeHandler.ValidateUpgradeResponse(response, requestContext);
+
_webSocket = socketWrapper.CreateWebSocket(isServer: false);
}
diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs
similarity index 58%
rename from Jung.SimpleWebSocket/SimpleWebSocketServer.cs
rename to src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs
index 943ca4a..4b298cf 100644
--- a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs
+++ b/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs
@@ -4,10 +4,13 @@
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;
using Jung.SimpleWebSocket.Wrappers;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
using System.Collections.Concurrent;
using System.Net;
using System.Net.WebSockets;
@@ -18,26 +21,35 @@ 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 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 AsyncEventHandler? ClientUpgradeRequestReceivedAsync;
///
/// A dictionary of active clients.
///
- private ConcurrentDictionary ActiveClients { get; } = [];
+ internal ConcurrentDictionary ActiveClients { get; } = [];
///
public string[] ClientIds => [.. ActiveClients.Keys];
@@ -45,13 +57,18 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable
///
public int ClientCount => ActiveClients.Count;
+ ///
+ public bool IsListening => _tcpListener?.IsListening ?? false;
+
///
- /// Future: Handle passive (disconnected) clients, delete them after a period of time, configurate this behavior in the WebSocketServerOptions
+ /// A logger to write internal log messages.
///
- private ConcurrentDictionary PassiveClients { get; } = [];
+ internal ILogger? Logger { get; } = logger;
- ///
- public bool IsListening => _server?.IsListening ?? false;
+ ///
+ /// The options for the server.
+ ///
+ internal SimpleWebSocketServerOptions Options { get; } = options;
///
/// A flag indicating whether the server is started.
@@ -71,40 +88,29 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable
///
/// The server that listens for incoming connection attempts.
///
- private ITcpListener? _server;
-
- ///
- /// A logger to write internal log messages.
- ///
- private readonly ILogger? _logger;
+ 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.
///
- /// 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(IOptions options, ILogger? logger = null)
+ : this(options.Value, logger)
{
- LocalIpAddress = localIpAddress;
- Port = port;
- _logger = logger;
}
///
/// 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;
+ _tcpListener = tcpListener;
}
///
@@ -117,25 +123,19 @@ 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);
+ Logger?.LogInformation("Server started at {LocalIpAddress}:{Port}", LocalIpAddress, Port);
while (!linkedTokenSource.IsCancellationRequested)
{
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);
- 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);
}
@@ -145,7 +145,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);
@@ -161,7 +161,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();
@@ -176,9 +176,9 @@ public async Task ShutdownServer(CancellationToken? cancellationToken = null)
}
_cancellationTokenSource?.Cancel();
- _server?.Dispose();
- _server = null;
- _logger?.LogInformation("Server stopped");
+ _tcpListener?.Dispose();
+ _tcpListener = null;
+ Logger?.LogInformation("Server stopped");
}
///
@@ -196,11 +196,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);
}
}
@@ -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.
///
@@ -221,35 +235,59 @@ public WebSocketServerClient GetClientById(string clientId)
/// A asynchronous task
private async Task HandleClientAsync(WebSocketServerClient client, CancellationToken cancellationToken)
{
+ var flow = new ClientHandlingFlow(this, client, cancellationToken);
try
{
- // Upgrade the connection to a WebSocket
- 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);
-
- // Start listening for messages
- _logger?.LogDebug("Connection upgraded, now listening on client {clientId}", client.Id);
- await ProcessWebSocketMessagesAsync(client, cancellationToken);
+ // Load the request context
+ await flow.LoadRequestContext();
+
+ // Raise async client upgrade request received event
+ var eventArgs = await flow.RaiseUpgradeEventAsync(ClientUpgradeRequestReceivedAsync);
+
+ // Respond to the upgrade request
+ if (eventArgs.Handle)
+ {
+ // Accept the WebSocket connection
+ await flow.AcceptWebSocketAsync();
+
+ if (flow.TryAddClientToActiveUserList())
+ {
+ 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);
+ }
+ else
+ {
+ Logger?.LogDebug("Error while adding Client {clientId} to active clients", flow.Client.Id);
+ }
+ }
+ else
+ {
+ // Reject the WebSocket connection
+ 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 (!_serverShuttingDown)
{
- ActiveClients.TryRemove(client.Id, out _);
- client?.Dispose();
+ flow.HandleDisconnectedClient();
}
}
}
@@ -281,21 +319,21 @@ 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);
+ Logger?.LogDebug("Message received: {message}", receivedMessage);
+ 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);
+ Logger?.LogDebug("Binary message received, length: {length} bytes", result.Count);
+ 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);
+ Logger?.LogInformation("Received close message from Client");
+ AsyncEventRaiser.RaiseAsyncInNewTask(ClientDisconnected, this, new ClientDisconnectedArgs(result.CloseStatusDescription ?? string.Empty, client.Id), cancellationToken);
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
break;
}
@@ -306,8 +344,8 @@ private async Task ProcessWebSocketMessagesAsync(WebSocketServerClient client, C
public void Dispose()
{
_cancellationTokenSource?.Cancel();
- _server?.Dispose();
- _server = null;
+ _tcpListener?.Dispose();
+ _tcpListener = null;
GC.SuppressFinalize(this);
}
}
diff --git a/src/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs b/src/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs
new file mode 100644
index 0000000..a084e00
--- /dev/null
+++ b/src/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs
@@ -0,0 +1,76 @@
+// 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);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Helper method to raise an Event in a new Task.
+ ///
+ /// 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();
+ Task.Run(() =>
+ {
+ foreach (var handler in invocationList)
+ {
+ var handle = (EventHandler)handler;
+ handle(sender, e);
+ }
+ }, cancellationToken);
+ }
+ }
+ }
+}
\ No newline at end of file
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 81%
rename from Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs
rename to src/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs
index 6da2ffc..5d9f87b 100644
--- a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs
+++ b/src/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;
@@ -24,7 +25,7 @@ internal partial class WebSocketUpgradeHandler
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)]
@@ -34,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)
@@ -61,16 +62,10 @@ public async Task AwaitContextAsync(CancellationToken cancellationTo
return context;
}
- public async Task AcceptWebSocketAsync(WebContext request, CancellationToken cancellationToken)
- {
- await AcceptWebSocketAsync(request, null, cancellationToken);
- }
-
- public async Task AcceptWebSocketAsync(WebContext request, string? subProtocol, CancellationToken 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 +77,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;
}
@@ -91,16 +87,28 @@ public async Task AcceptWebSocketAsync(WebContext request, string? subProtocol,
}
catch (Exception message)
{
- throw new WebSocketException("Error while accepting the websocket", message);
+ throw new WebSocketException("Error while accepting the web socket", message);
}
}
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);
+ 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);
- FinishMessage(sb);
+ CompleteHeaderSection(sb);
+ AddBody(context, sb);
byte[] responseBytes = Encoding.UTF8.GetBytes(sb.ToString());
await _networkStream.WriteAsync(responseBytes, cancellationToken);
@@ -112,7 +120,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 +134,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 static void AddBody(WebContext context, StringBuilder sb)
+ {
+ sb.Append(context.BodyContent);
+ }
+
private static void ValidateWebSocketHeaders(WebContext context)
{
if (!context.IsWebSocketRequest)
@@ -186,7 +199,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)
@@ -219,15 +232,24 @@ 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"))
{
+ 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.");
}
@@ -272,6 +294,14 @@ 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)
+ {
+ 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.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 87%
rename from Jung.SimpleWebSocket/docs/README.md
rename to src/Jung.SimpleWebSocket/docs/README.md
index 7d712e8..a0c5979 100644
--- a/Jung.SimpleWebSocket/docs/README.md
+++ b/src/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/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/tests/Jung.SimpleWebSocket.UnitTests/Mock/ILoggerMockHelper.cs b/tests/Jung.SimpleWebSocket.UnitTests/Mock/ILoggerMockHelper.cs
new file mode 100644
index 0000000..b784ee1
--- /dev/null
+++ b/tests/Jung.SimpleWebSocket.UnitTests/Mock/ILoggerMockHelper.cs
@@ -0,0 +1,42 @@
+// 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.SimpleWebSocket.UnitTests.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/tests/Jung.SimpleWebSocket.UnitTests/Mock/LoggerMessages.cs b/tests/Jung.SimpleWebSocket.UnitTests/Mock/LoggerMessages.cs
new file mode 100644
index 0000000..86af14e
--- /dev/null
+++ b/tests/Jung.SimpleWebSocket.UnitTests/Mock/LoggerMessages.cs
@@ -0,0 +1,27 @@
+// 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
+ {
+ 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/tests/Jung.SimpleWebSocket.UnitTests/SimpleWebSocketTest.cs
similarity index 57%
rename from Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs
rename to tests/Jung.SimpleWebSocket.UnitTests/SimpleWebSocketTest.cs
index 0b058ec..2621a9f 100644
--- a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs
+++ b/tests/Jung.SimpleWebSocket.UnitTests/SimpleWebSocketTest.cs
@@ -1,25 +1,24 @@
// This file is part of the Jung SimpleWebSocket project.
// The project is licensed under the MIT license.
-using Jung.SimpleWebSocket;
-using Microsoft.Extensions.Logging;
-using Moq;
+using Jung.SimpleWebSocket.Exceptions;
+using Jung.SimpleWebSocket.Models;
+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
{
- private List _logMessages = [];
- private Mock> _serverLogger;
- private Mock> _clientLogger;
+ private ILoggerMockHelper _serverLoggerMockHelper;
+ private ILoggerMockHelper _clientLoggerMockHelper;
[OneTimeSetUp]
public void SetUpOnce()
@@ -30,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]
@@ -43,26 +39,79 @@ public void EndTest()
Trace.Flush();
}
- private void SetUpLogger(Mock> mock, string loggerName)
+ [Test]
+ public void ChangeClientId_UserIdUnique_ShouldUpdateId()
{
- mock.Setup(m => m.Log(
- It.IsAny(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny>()!
- )).Callback(new InvocationAction(invocation =>
+ // 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(() =>
{
- 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, new[] { state, exception });
- _logMessages.Add($"{loggerName}({logLevel}): {logMessage}");
- }));
+ 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]
@@ -70,8 +119,14 @@ private void SetUpLogger(Mock> mock, string loggerName)
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);
+ var serverOptions = new SimpleWebSocketServerOptions
+ {
+ LocalIpAddress = IPAddress.Any,
+ Port = 8010,
+ };
+
+ 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";
@@ -106,20 +161,48 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld()
messageResetEvent.Set();
};
+ server.ClientUpgradeRequestReceivedAsync += async (sender, args, cancellationToken) =>
+ {
+ // Get the IP address of the client
+ var IpAddress = (args.Client.RemoteEndPoint as IPEndPoint)?.Address;
+ 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;
+ args.ResponseContext.BodyContent = "Connection only possible via local network.";
+ args.Handle = false;
+ }
+ args.Client.Properties["test"] = "test";
+ };
+
// 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, "/", logger: _clientLoggerMockHelper.Logger);
+ await client2.ConnectAsync();
+
+ await Task.Delay(100);
+
+ 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(() =>
@@ -129,13 +212,32 @@ 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(IPAddress ipAddress, CancellationToken cancellationToken)
+ {
+ await Task.Delay(100, cancellationToken);
+ 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_ShouldSendAndReceiveHelloWorld2()
{
// Arrange
- using var server = new SimpleWebSocketServer(IPAddress.Any, 8010, _serverLogger.Object);
- using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", _clientLogger.Object);
+ var serverOptions = new SimpleWebSocketServerOptions
+ {
+ LocalIpAddress = IPAddress.Any,
+ Port = 8010
+ };
+
+ 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";
@@ -182,7 +284,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(() =>
@@ -197,7 +299,13 @@ 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
+ };
+
+ using var server = new SimpleWebSocketServer(serverOptions);
List clients = [];
var message = "Hello World";
const int clientsCount = 200;
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 95%
rename from Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs
rename to tests/Jung.SimpleWebSocket.UnitTests/WebSocketUpgradeHandlerTests.cs
index f8ba27e..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
{
@@ -64,7 +66,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, new WebContext(), null, cancellationToken);
// Assert
Assert.That(response, Does.Contain("HTTP/1.1 101 Switching Protocols"));
@@ -90,7 +92,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, new WebContext(), serverSubprotocol, cancellationToken);
// Assert
Assert.Multiple(() =>