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(() =>