diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md index fe734924a..24f0a968e 100644 --- a/.github/workflows/ReleaseNotes.md +++ b/.github/workflows/ReleaseNotes.md @@ -1,7 +1,7 @@ * [Client] Added support for custom CA chain validation (#1851, thanks to @rido-min). -* [Client] Added support for custom CA chain validation (#1851, thanks to @rido-min). * [Client] Fixed handling of unobserved tasks exceptions (#1871). * [Client] Fixed not specified ReasonCode when using _SendExtendedAuthenticationExchangeDataAsync_ (#1882, thanks to @rido-min). * [Server] Fixed not working _UpdateRetainedMessageAsync_ public api (#1858, thanks to @kimdiego2098). * [Server] Added support for custom DISCONNECT packets when stopping the server or disconnect a client (BREAKING CHANGE!, #1846). * [Server] Added new property to stop the server from accepting new connections even if it is running (#1846). +* [Server] Added a new extension nuget which allows hosting a MQTT server via the Microsoft.Extensions.Hosting library (#1653, thanks to @YAJeff). diff --git a/MQTTnet.sln b/MQTTnet.sln index 34abf629f..b6a6636c1 100644 --- a/MQTTnet.sln +++ b/MQTTnet.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31919.166 @@ -32,6 +32,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.TestApp", "Source\M EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.AspTestApp", "Source\MQTTnet.AspTestApp\MQTTnet.AspTestApp.csproj", "{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.Hosting", "Source\MQTTnet.Extensions.Hosting\MQTTnet.Extensions.Hosting.csproj", "{B53FC20B-862C-4F8F-B9FF-E8C8D76A870D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{26138A7E-435D-4C37-92B8-F506C640266D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.Extensions.Hosting.Tests", "Source\MQTTnet.Extensions.Hosting.Tests\MQTTnet.Extensions.Hosting.Tests.csproj", "{C4DE2742-2177-4AD0-BC01-74F29E9595C3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -82,11 +88,25 @@ Global {72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Debug|Any CPU.Build.0 = Debug|Any CPU {72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Release|Any CPU.ActiveCfg = Release|Any CPU {72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Release|Any CPU.Build.0 = Release|Any CPU + {B53FC20B-862C-4F8F-B9FF-E8C8D76A870D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B53FC20B-862C-4F8F-B9FF-E8C8D76A870D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B53FC20B-862C-4F8F-B9FF-E8C8D76A870D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B53FC20B-862C-4F8F-B9FF-E8C8D76A870D}.Release|Any CPU.Build.0 = Release|Any CPU + {C4DE2742-2177-4AD0-BC01-74F29E9595C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4DE2742-2177-4AD0-BC01-74F29E9595C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4DE2742-2177-4AD0-BC01-74F29E9595C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4DE2742-2177-4AD0-BC01-74F29E9595C3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution + {72867E4C-4E15-4E8E-8FAB-AE9253286BBC} = {26138A7E-435D-4C37-92B8-F506C640266D} + {A238BBBF-C75F-482D-9CC3-BB34ABA9B675} = {26138A7E-435D-4C37-92B8-F506C640266D} + {B270F32A-9F3E-42EE-A989-813E35E29ADB} = {26138A7E-435D-4C37-92B8-F506C640266D} + {175D5340-CC5B-4542-939D-4E7D15A0BC8D} = {26138A7E-435D-4C37-92B8-F506C640266D} + {C4DE2742-2177-4AD0-BC01-74F29E9595C3} = {26138A7E-435D-4C37-92B8-F506C640266D} + {2F516E76-AAC4-4219-B7D1-34CDD3CFF381} = {26138A7E-435D-4C37-92B8-F506C640266D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {07536672-5CBC-4BE3-ACE0-708A431A7894} diff --git a/Samples/MQTTnet.Samples.csproj b/Samples/MQTTnet.Samples.csproj index b45acc682..9c4f7543f 100644 --- a/Samples/MQTTnet.Samples.csproj +++ b/Samples/MQTTnet.Samples.csproj @@ -18,6 +18,7 @@ + diff --git a/Samples/Server/Server_Hosting_Extensions_Samples.cs b/Samples/Server/Server_Hosting_Extensions_Samples.cs new file mode 100644 index 000000000..97892595a --- /dev/null +++ b/Samples/Server/Server_Hosting_Extensions_Samples.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedMember.Global +// ReSharper disable InconsistentNaming +// ReSharper disable EmptyConstructor +// ReSharper disable MemberCanBeMadeStatic.Local + +using Microsoft.Extensions.Hosting; +using MQTTnet.Extensions.Hosting.Extensions; + +namespace MQTTnet.Samples.Server; + +public static class Server_Hosting_Extensions_Samples +{ + public static Task Start_Server() + { + var builder = new HostBuilder(); + + builder.UseMqttServer( + mqtt => + { + mqtt.WithDefaultEndpoint(); + }); + + var host = builder.Build(); + return host.RunAsync(); + } + + public static Task Start_Simple_Server() + { + var host = new HostBuilder().UseMqttServer().Build(); + + return host.RunAsync(); + } + + // This could be called as a top-level statement in a Program.cs file + public static Task Start_Single_Line_Server() + { + return new HostBuilder().UseMqttServer().Build().RunAsync(); + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.Hosting.Tests/Hosting_Tests.cs b/Source/MQTTnet.Extensions.Hosting.Tests/Hosting_Tests.cs new file mode 100644 index 000000000..bf20ef7da --- /dev/null +++ b/Source/MQTTnet.Extensions.Hosting.Tests/Hosting_Tests.cs @@ -0,0 +1,198 @@ +#if NET6_0_OR_GREATER +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Extensions.Hosting.Extensions; +using MQTTnet.Server; + +namespace MQTTnet.Tests.Server +{ + [TestClass] + public sealed class Hosting_Tests + { + [TestMethod] + public async Task Advanced_Host_Configuration() + { + var syncLock = new object(); + var connectedClientCount = 0; + var host = new HostBuilder().UseMqttServer( + mqtt => + { + mqtt.WithDefaultEndpoint().WithKeepAlive(); + + mqtt.ClientConnectedAsync += e => + { + lock (syncLock) + { + connectedClientCount++; + } + + return Task.CompletedTask; + }; + }) + .Build(); + + await host.StartAsync(); + + // Perform client connect test + try + { + var factory = new MqttFactory(); + var client = factory.CreateMqttClient(); + var options = factory.CreateClientOptionsBuilder(); + options.WithTcpServer("127.0.0.1"); + + await client.ConnectAsync(options.Build()); + } + finally + { + await host.StopAsync(); + } + + Assert.AreEqual(1, connectedClientCount); + } + + [TestMethod] + public async Task Custom_Host_Configuration() + { + var host = new HostBuilder().UseMqttServer( + mqtt => + { + mqtt.WithDefaultEndpoint().WithKeepAlive(); + }) + .Build(); + await host.StartAsync(); + + // Perform client connect test + try + { + var factory = new MqttFactory(); + var client = factory.CreateMqttClient(); + var options = factory.CreateClientOptionsBuilder(); + options.WithTcpServer("127.0.0.1"); + + await client.ConnectAsync(options.Build()); + } + finally + { + await host.StopAsync(); + } + } + + [TestMethod] + public async Task Default_Host_Configuration() + { + var host = new HostBuilder().UseMqttServer().Build(); + await host.StartAsync(); + + // Perform client connect test + try + { + var factory = new MqttFactory(); + var client = factory.CreateMqttClient(); + var options = factory.CreateClientOptionsBuilder(); + options.WithTcpServer("127.0.0.1"); + + await client.ConnectAsync(options.Build()); + } + finally + { + await host.StopAsync(); + } + } + + [TestMethod] + public async Task Default_WebSocket_Configuration_Connect() + { + var host = new HostBuilder().UseMqttServer( + mqtt => + { + mqtt.WithDefaultWebSocketEndpoint().WithDefaultWebSocketEndpointPort(8080); + }) + .Build(); + await host.StartAsync(); + + await Task.Delay(5000); + + // Perform client connect test + try + { + var factory = new MqttFactory(); + var client = factory.CreateMqttClient(); + var options = factory.CreateClientOptionsBuilder(); + options.WithWebSocketServer(o => o.WithUri("127.0.0.1:8080/mqtt")); + + await client.ConnectAsync(options.Build()); + } + finally + { + await host.StopAsync(); + } + } + + [TestMethod] + public async Task External_HttpListener_WebSocket_Configuration_Connect() + { + using (var tcs = new CancellationTokenSource()) + { + var host = new HostBuilder().UseMqttServer().Build(); + await host.StartAsync(); + + var httpListener = new HttpListener(); + httpListener.Prefixes.Add("http://127.0.0.1:8080/"); + httpListener.Start(); + + _ = Task.Factory.StartNew( + async () => + { + while (!tcs.IsCancellationRequested) + { + try + { + var context = await httpListener.GetContextAsync(); + + if (context.Request.Url.AbsolutePath.Equals("/mqtt", StringComparison.OrdinalIgnoreCase) && context.Request.IsWebSocketRequest) + { + var mqttServer = host.Services.GetService(); + var webSocketContext = await context.AcceptWebSocketAsync("MQTT"); + mqttServer.HandleWebSocketConnection(webSocketContext, context); + } + else + { + context.Response.StatusCode = 404; + context.Response.Close(); + } + } + catch + { + } + } + }); + + await Task.Delay(5000); + + // Perform client connect test + try + { + var factory = new MqttFactory(); + var client = factory.CreateMqttClient(); + var options = factory.CreateClientOptionsBuilder(); + options.WithWebSocketServer(o => o.WithUri("127.0.0.1:8080/mqtt")); + + await client.ConnectAsync(options.Build()); + } + finally + { + await host.StopAsync(); + httpListener.Stop(); + } + } + } + } +} + +#endif \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.Hosting.Tests/MQTTnet.Extensions.Hosting.Tests.csproj b/Source/MQTTnet.Extensions.Hosting.Tests/MQTTnet.Extensions.Hosting.Tests.csproj new file mode 100644 index 000000000..cbed9b57c --- /dev/null +++ b/Source/MQTTnet.Extensions.Hosting.Tests/MQTTnet.Extensions.Hosting.Tests.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + false + 7.3 + false + false + true + 1591;NETSDK1138 + + + + + + + + + + + 6.0.0 + + + + + + + + + \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.Hosting/Events/HttpWebSocketClientAuthenticationCallback.cs b/Source/MQTTnet.Extensions.Hosting/Events/HttpWebSocketClientAuthenticationCallback.cs new file mode 100644 index 000000000..291859090 --- /dev/null +++ b/Source/MQTTnet.Extensions.Hosting/Events/HttpWebSocketClientAuthenticationCallback.cs @@ -0,0 +1,6 @@ +using System.Threading.Tasks; + +namespace MQTTnet.Extensions.Hosting.Events +{ + public delegate Task HttpWebSocketClientAuthenticationCallback(); +} diff --git a/Source/MQTTnet.Extensions.Hosting/Extensions/HostBuilderExtensions.cs b/Source/MQTTnet.Extensions.Hosting/Extensions/HostBuilderExtensions.cs new file mode 100644 index 000000000..23e383532 --- /dev/null +++ b/Source/MQTTnet.Extensions.Hosting/Extensions/HostBuilderExtensions.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; +using MQTTnet.Extensions.Hosting.Implementations; +using MQTTnet.Extensions.Hosting.Options; +using MQTTnet.Implementations; +using MQTTnet.Server; + +namespace MQTTnet.Extensions.Hosting.Extensions +{ + public static class HostBuilderExtensions + { + public static IHostBuilder UseMqttServer(this IHostBuilder hostBuilder) + { + if (hostBuilder == null) + { + throw new ArgumentNullException(nameof(hostBuilder)); + } + + return hostBuilder.UseMqttServer( + builder => + { + builder.WithDefaultEndpoint(); + }); + } + + public static IHostBuilder UseMqttServer(this IHostBuilder hostBuilder, Action configure) + { + if (hostBuilder == null) + { + throw new ArgumentNullException(nameof(hostBuilder)); + } + + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + var startActions = new List>(); + var stopActions = new List>(); + + hostBuilder.ConfigureServices( + (_, services) => + { + services.AddSingleton( + s => + { + var builder = new MqttServerHostingBuilder(s, startActions, stopActions); + configure(builder); + return builder.Build(); + }); + + var logger = new MqttNetEventLogger(); + + services.AddSingleton(logger) + .AddSingleton() + .AddSingleton(new MqttNetNullLogger()) + .AddSingleton(new MqttFactory()) + .AddSingleton() + .AddSingleton(s => s.GetRequiredService()) + .AddSingleton(s => new MqttServerConfigurationHostedService(s, startActions, stopActions)) + .AddSingleton(s => s.GetRequiredService()) + .AddSingleton() + .AddSingleton(s => s.GetRequiredService()) + .AddSingleton() + .AddSingleton() + .AddSingleton(s => s.GetRequiredService()); + }); + + return hostBuilder; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.Hosting/Extensions/HostingMqttServerExtensions.cs b/Source/MQTTnet.Extensions.Hosting/Extensions/HostingMqttServerExtensions.cs new file mode 100644 index 000000000..f5dae5562 --- /dev/null +++ b/Source/MQTTnet.Extensions.Hosting/Extensions/HostingMqttServerExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.Net; +using System.Net.WebSockets; +using Microsoft.Extensions.DependencyInjection; +using MQTTnet.Extensions.Hosting.Implementations; +using MQTTnet.Server; + +namespace MQTTnet.Extensions.Hosting.Extensions +{ + public static class HostingMqttServerExtensions + { + public static void HandleWebSocketConnection(this MqttServer server, HttpListenerWebSocketContext webSocketContext, HttpListenerContext httpListenerContext) + { + if (server == null) + { + throw new ArgumentNullException(nameof(server)); + } + + if (!(server is MqttHostedServer mqttHostedServer)) + { + throw new InvalidOperationException("The server must be started through hosting extensions."); + } + + mqttHostedServer.ServiceProvider.GetRequiredService().HandleWebSocketConnection(webSocketContext, httpListenerContext); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.Hosting/Implementations/MqttServerWebSocketConnectionHandler.cs b/Source/MQTTnet.Extensions.Hosting/Implementations/MqttServerWebSocketConnectionHandler.cs new file mode 100644 index 000000000..9f9bb647b --- /dev/null +++ b/Source/MQTTnet.Extensions.Hosting/Implementations/MqttServerWebSocketConnectionHandler.cs @@ -0,0 +1,77 @@ +using System; +using System.Net; +using System.Net.WebSockets; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; +using MQTTnet.Formatter; +using MQTTnet.Implementations; + +namespace MQTTnet.Extensions.Hosting.Implementations +{ + public sealed class MqttServerWebSocketConnectionHandler : IHostedService, IDisposable + { + readonly MqttWebSocketServerAdapter _adapter; + readonly CancellationTokenSource _cancellationToken = new CancellationTokenSource(); + readonly IMqttNetLogger _logger; + + public MqttServerWebSocketConnectionHandler(MqttWebSocketServerAdapter adapter, IMqttNetLogger logger) + { + _adapter = adapter; + _logger = logger; + } + + public void Dispose() + { + _cancellationToken.Dispose(); + } + + public void HandleWebSocketConnection(HttpListenerWebSocketContext webSocketContext, HttpListenerContext httpListenerContext, X509Certificate2? clientCertificate = null) + { + _ = Task.Factory.StartNew(() => TryHandleWebSocketConnectionAsync(webSocketContext, httpListenerContext, clientCertificate)); + } + + public Task StartAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _cancellationToken.Cancel(); + + return Task.CompletedTask; + } + + async Task TryHandleWebSocketConnectionAsync(HttpListenerWebSocketContext webSocketContext, HttpListenerContext httpListenerContext, X509Certificate2? clientCertificate) + { + if (webSocketContext == null) + { + throw new ArgumentNullException(nameof(webSocketContext)); + } + + var endpoint = $"{httpListenerContext.Request.RemoteEndPoint.Address}:{httpListenerContext.Request.RemoteEndPoint.Port}"; + + try + { + var clientHandler = _adapter.ClientHandler; + if (clientHandler != null) + { + var formatter = new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)); + var channel = new MqttWebSocketChannel(webSocketContext.WebSocket, endpoint, webSocketContext.IsSecureConnection, clientCertificate); + using (var channelAdapter = new MqttChannelAdapter(channel, formatter, _logger)) + { + await clientHandler(channelAdapter).ConfigureAwait(false); + } + } + } + finally + { + clientCertificate?.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.Hosting/Implementations/MqttWebSocketServerAdapter.cs b/Source/MQTTnet.Extensions.Hosting/Implementations/MqttWebSocketServerAdapter.cs new file mode 100644 index 000000000..cd68eac3c --- /dev/null +++ b/Source/MQTTnet.Extensions.Hosting/Implementations/MqttWebSocketServerAdapter.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; +using MQTTnet.Extensions.Hosting.Options; +using MQTTnet.Internal; +using MQTTnet.Server; + +namespace MQTTnet.Extensions.Hosting.Implementations +{ + public sealed class MqttWebSocketServerAdapter : IMqttServerAdapter + { + readonly MqttServerHostingOptions _hostingOptions; + readonly List _listeners = new List(); + readonly IServiceProvider _services; + + public MqttWebSocketServerAdapter(IServiceProvider services, MqttServerHostingOptions hostingOptions) + { + _services = services ?? throw new ArgumentNullException(nameof(services)); + _hostingOptions = hostingOptions ?? throw new ArgumentNullException(nameof(hostingOptions)); + } + + public Func? ClientHandler { get; set; } + + public void Dispose() + { + foreach (var listener in _listeners) + { + listener.Dispose(); + } + } + + public Task StartAsync(MqttServerOptions options, IMqttNetLogger logger) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (_hostingOptions.DefaultWebSocketEndpointOptions.IsEnabled) + { + _listeners.Add(ActivatorUtilities.CreateInstance(_services, options, _hostingOptions.DefaultWebSocketEndpointOptions)); + } + + if (_hostingOptions.DefaultTlsWebSocketEndpointOptions.IsEnabled) + { + _listeners.Add(ActivatorUtilities.CreateInstance(_services, options, _hostingOptions.DefaultTlsWebSocketEndpointOptions)); + } + + foreach (var listener in _listeners) + { + listener.Start(CancellationToken.None); + } + + return CompletedTask.Instance; + } + + public Task StopAsync() + { + foreach (var listener in _listeners) + { + listener.Dispose(); + } + + _listeners.Clear(); + + return CompletedTask.Instance; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.Hosting/Implementations/MqttWebSocketServerListener.cs b/Source/MQTTnet.Extensions.Hosting/Implementations/MqttWebSocketServerListener.cs new file mode 100644 index 000000000..db710ffd4 --- /dev/null +++ b/Source/MQTTnet.Extensions.Hosting/Implementations/MqttWebSocketServerListener.cs @@ -0,0 +1,123 @@ +using System; +using System.Net; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Extensions.Hosting.Options; +using MQTTnet.Server; + +namespace MQTTnet.Extensions.Hosting.Implementations +{ + public sealed class MqttWebSocketServerListener : IDisposable + { + readonly MqttServerWebSocketConnectionHandler _connectionHandler; + readonly MqttServerWebSocketEndpointBaseOptions _endpointOptions; + readonly MqttServerOptions _serverOptions; + + HttpListener? _listener; + + public MqttWebSocketServerListener( + MqttServerOptions serverOptions, + MqttServerWebSocketEndpointBaseOptions endpointOptions, + MqttServerWebSocketConnectionHandler connectionHandler) + { + _serverOptions = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions)); + _endpointOptions = endpointOptions ?? throw new ArgumentNullException(nameof(endpointOptions)); + _connectionHandler = connectionHandler ?? throw new ArgumentNullException(nameof(connectionHandler)); + } + + public void Dispose() + { + _listener?.Stop(); + _listener?.Close(); + } + + public bool Start(CancellationToken cancellationToken) + { + try + { + _listener = new HttpListener(); + + if (_endpointOptions is MqttServerTlsWebSocketEndpointOptions tlsEndpointOptions) + { + if (tlsEndpointOptions.BoundInterNetworkAddress != null && tlsEndpointOptions.BoundInterNetworkAddress != IPAddress.Any) + { + _listener.Prefixes.Add($"https://{tlsEndpointOptions.BoundInterNetworkAddress}:{tlsEndpointOptions.Port}/"); + } + + if (tlsEndpointOptions.BoundInterNetworkV6Address != null && tlsEndpointOptions.BoundInterNetworkV6Address != IPAddress.IPv6Any) + { + _listener.Prefixes.Add($"https://{tlsEndpointOptions.BoundInterNetworkV6Address}:{tlsEndpointOptions.Port}/"); + } + + if ((tlsEndpointOptions.BoundInterNetworkAddress == null || tlsEndpointOptions.BoundInterNetworkAddress == IPAddress.Any) && + (tlsEndpointOptions.BoundInterNetworkV6Address == null || tlsEndpointOptions.BoundInterNetworkV6Address == IPAddress.IPv6Any)) + { + _listener.Prefixes.Add($"https://*:{tlsEndpointOptions.Port}/"); + } + } + else if (_endpointOptions is MqttServerWebSocketEndpointOptions defaultEndpointOptions) + { + if (defaultEndpointOptions.BoundInterNetworkAddress != null && defaultEndpointOptions.BoundInterNetworkAddress != IPAddress.Any) + { + _listener.Prefixes.Add($"http://{defaultEndpointOptions.BoundInterNetworkAddress}:{defaultEndpointOptions.Port}/"); + } + + if (defaultEndpointOptions.BoundInterNetworkV6Address != null && defaultEndpointOptions.BoundInterNetworkV6Address != IPAddress.IPv6Any) + { + _listener.Prefixes.Add($"http://{defaultEndpointOptions.BoundInterNetworkV6Address}:{defaultEndpointOptions.Port}/"); + } + + if ((defaultEndpointOptions.BoundInterNetworkAddress == null || defaultEndpointOptions.BoundInterNetworkAddress == IPAddress.Any) && + (defaultEndpointOptions.BoundInterNetworkV6Address == null || defaultEndpointOptions.BoundInterNetworkV6Address == IPAddress.IPv6Any)) + { + _listener.Prefixes.Add($"http://127.0.0.1:{defaultEndpointOptions.Port}/"); // TODO: Correct this to proper wildcard + } + } + + _listener.Start(); + + Task.Run(() => AcceptClientConnectionsAsync(cancellationToken), cancellationToken); + + return true; + } + catch + { + return false; + } + } + + async Task AcceptClientConnectionsAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + var context = await _listener!.GetContextAsync(); + if (_serverOptions.TlsEndpointOptions.ClientCertificateRequired) + { + var clientCertificate = await context.Request.GetClientCertificateAsync().ConfigureAwait(false); + using var chain = X509Chain.Create(); + if (!_serverOptions.TlsEndpointOptions.RemoteCertificateValidationCallback(this, clientCertificate, chain, SslPolicyErrors.None)) + { + context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + context.Response.Close(); + + continue; + } + } + + if (!context.Request.IsWebSocketRequest) + { + context.Response.StatusCode = (int)HttpStatusCode.BadRequest; + context.Response.Close(); + + continue; + } + + var webSocketContext = await context.AcceptWebSocketAsync("MQTT", _serverOptions.WriterBufferSize, _serverOptions.KeepAliveMonitorInterval); + + _connectionHandler.HandleWebSocketConnection(webSocketContext, context); + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.Hosting/MQTTnet.Extensions.Hosting.csproj b/Source/MQTTnet.Extensions.Hosting/MQTTnet.Extensions.Hosting.csproj new file mode 100644 index 000000000..9fbad72bd --- /dev/null +++ b/Source/MQTTnet.Extensions.Hosting/MQTTnet.Extensions.Hosting.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.1;net5.0;net6.0;net7.0 + enable + NETSDK1138 + + + + + + + + + + + + + + diff --git a/Source/MQTTnet.Extensions.Hosting/MqttHostedServer.cs b/Source/MQTTnet.Extensions.Hosting/MqttHostedServer.cs new file mode 100644 index 000000000..ccdbf92cd --- /dev/null +++ b/Source/MQTTnet.Extensions.Hosting/MqttHostedServer.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Hosting; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Threading; +using MQTTnet.Server; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; + +namespace MQTTnet.Extensions.Hosting +{ + public sealed class MqttHostedServer : MqttServer, IHostedService + { + public MqttHostedServer(IServiceProvider serviceProvider, MqttServerOptions options, IEnumerable adapters, IMqttNetLogger logger) + : base(options, adapters, logger) + { + ServiceProvider = serviceProvider; + } + + public IServiceProvider ServiceProvider { get; } + + public Task StartAsync(CancellationToken cancellationToken) + { + return StartAsync(); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return StopAsync(new MqttServerStopOptions()); + } + } +} diff --git a/Source/MQTTnet.Extensions.Hosting/MqttServerConfigurationHostedService.cs b/Source/MQTTnet.Extensions.Hosting/MqttServerConfigurationHostedService.cs new file mode 100644 index 000000000..f45e30391 --- /dev/null +++ b/Source/MQTTnet.Extensions.Hosting/MqttServerConfigurationHostedService.cs @@ -0,0 +1,41 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using MQTTnet.Server; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MQTTnet.Extensions.Hosting +{ + public class MqttServerConfigurationHostedService : IHostedService + { + private readonly IServiceProvider _serviceProvider; + private readonly List> _startActions; + private readonly List> _stopActions; + + public MqttServerConfigurationHostedService(IServiceProvider serviceProvider, List> startActions, List> stopActions) + { + _serviceProvider = serviceProvider; + _startActions = startActions; + _stopActions = stopActions; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + var server = _serviceProvider.GetRequiredService(); + _startActions.ForEach(a => a(server)); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + var server = _serviceProvider.GetRequiredService(); + _stopActions.ForEach(a => a(server)); + + return Task.CompletedTask; + } + } +} diff --git a/Source/MQTTnet.Extensions.Hosting/MqttServerHostingBuilder.cs b/Source/MQTTnet.Extensions.Hosting/MqttServerHostingBuilder.cs new file mode 100644 index 000000000..e8b2a6ff9 --- /dev/null +++ b/Source/MQTTnet.Extensions.Hosting/MqttServerHostingBuilder.cs @@ -0,0 +1,338 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using MQTTnet.Extensions.Hosting.Events; +using MQTTnet.Extensions.Hosting.Options; +using MQTTnet.Server; + +namespace MQTTnet.Extensions.Hosting +{ + public class MqttServerHostingBuilder : MqttServerOptionsBuilder + { + readonly MqttServerHostingOptions _hostingOptions; + readonly List> _startActions; + readonly List> _stopActions; + + public MqttServerHostingBuilder(IServiceProvider serviceProvider, List> startActions, List> stopActions) + { + ServiceProvider = serviceProvider; + _hostingOptions = serviceProvider.GetRequiredService(); + _startActions = startActions; + _stopActions = stopActions; + } + + public event Func ApplicationMessageNotConsumedAsync + { + add + { + _startActions.Add(server => server.ApplicationMessageNotConsumedAsync += value); + if (_hostingOptions.AutoRemoveEventHandlers) + { + _stopActions.Add(server => server.ApplicationMessageNotConsumedAsync -= value); + } + } + remove => _startActions.Add(server => server.ApplicationMessageNotConsumedAsync -= value); + } + + public event Func ClientAcknowledgedPublishPacketAsync + { + add + { + _startActions.Add(server => server.ClientAcknowledgedPublishPacketAsync += value); + if (_hostingOptions.AutoRemoveEventHandlers) + { + _stopActions.Add(server => server.ClientAcknowledgedPublishPacketAsync -= value); + } + } + remove => _startActions.Add(server => server.ClientAcknowledgedPublishPacketAsync -= value); + } + + public event Func ClientConnectedAsync + { + add + { + _startActions.Add(server => server.ClientConnectedAsync += value); + if (_hostingOptions.AutoRemoveEventHandlers) + { + _stopActions.Add(server => server.ClientConnectedAsync -= value); + } + } + remove => _startActions.Add(server => server.ClientConnectedAsync -= value); + } + + public event Func ClientDisconnectedAsync + { + add + { + _startActions.Add(server => server.ClientDisconnectedAsync += value); + if (_hostingOptions.AutoRemoveEventHandlers) + { + _stopActions.Add(server => server.ClientDisconnectedAsync -= value); + } + } + remove => _startActions.Add(server => server.ClientDisconnectedAsync -= value); + } + + public event Func ClientSubscribedTopicAsync + { + add + { + _startActions.Add(server => server.ClientSubscribedTopicAsync += value); + if (_hostingOptions.AutoRemoveEventHandlers) + { + _stopActions.Add(server => server.ClientSubscribedTopicAsync -= value); + } + } + remove => _startActions.Add(server => server.ClientSubscribedTopicAsync -= value); + } + + public event Func ClientUnsubscribedTopicAsync + { + add + { + _startActions.Add(server => server.ClientUnsubscribedTopicAsync += value); + if (_hostingOptions.AutoRemoveEventHandlers) + { + _stopActions.Add(server => server.ClientUnsubscribedTopicAsync -= value); + } + } + remove => _startActions.Add(server => server.ClientUnsubscribedTopicAsync -= value); + } + + public event Func InterceptingInboundPacketAsync + { + add + { + _startActions.Add(server => server.InterceptingInboundPacketAsync += value); + if (_hostingOptions.AutoRemoveEventHandlers) + { + _stopActions.Add(server => server.InterceptingInboundPacketAsync -= value); + } + } + remove => _startActions.Add(server => server.InterceptingInboundPacketAsync -= value); + } + + public event Func InterceptingOutboundPacketAsync + { + add + { + _startActions.Add(server => server.InterceptingOutboundPacketAsync += value); + if (_hostingOptions.AutoRemoveEventHandlers) + { + _stopActions.Add(server => server.InterceptingOutboundPacketAsync -= value); + } + } + remove => _startActions.Add(server => server.InterceptingOutboundPacketAsync -= value); + } + + public event Func InterceptingPublishAsync + { + add + { + _startActions.Add(server => server.InterceptingPublishAsync += value); + if (_hostingOptions.AutoRemoveEventHandlers) + { + _stopActions.Add(server => server.InterceptingPublishAsync -= value); + } + } + remove => _startActions.Add(server => server.InterceptingPublishAsync -= value); + } + + public event Func InterceptingSubscriptionAsync + { + add + { + _startActions.Add(server => server.InterceptingSubscriptionAsync += value); + if (_hostingOptions.AutoRemoveEventHandlers) + { + _stopActions.Add(server => server.InterceptingSubscriptionAsync -= value); + } + } + remove => _startActions.Add(server => server.InterceptingSubscriptionAsync -= value); + } + + public event Func InterceptingUnsubscriptionAsync + { + add + { + _startActions.Add(server => server.InterceptingUnsubscriptionAsync += value); + if (_hostingOptions.AutoRemoveEventHandlers) + { + _stopActions.Add(server => server.InterceptingUnsubscriptionAsync -= value); + } + } + remove => _startActions.Add(server => server.InterceptingUnsubscriptionAsync -= value); + } + + public event Func LoadingRetainedMessageAsync + { + add + { + _startActions.Add(server => server.LoadingRetainedMessageAsync += value); + if (_hostingOptions.AutoRemoveEventHandlers) + { + _stopActions.Add(server => server.LoadingRetainedMessageAsync -= value); + } + } + remove => _startActions.Add(server => server.LoadingRetainedMessageAsync -= value); + } + + public event Func PreparingSessionAsync + { + add + { + _startActions.Add(server => server.PreparingSessionAsync += value); + if (_hostingOptions.AutoRemoveEventHandlers) + { + _stopActions.Add(server => server.PreparingSessionAsync -= value); + } + } + remove => _startActions.Add(server => server.PreparingSessionAsync -= value); + } + + public event Func RetainedMessageChangedAsync + { + add + { + _startActions.Add(server => server.RetainedMessageChangedAsync += value); + if (_hostingOptions.AutoRemoveEventHandlers) + { + _stopActions.Add(server => server.RetainedMessageChangedAsync -= value); + } + } + remove => _startActions.Add(server => server.RetainedMessageChangedAsync -= value); + } + + public event Func RetainedMessagesClearedAsync + { + add + { + _startActions.Add(server => server.RetainedMessagesClearedAsync += value); + if (_hostingOptions.AutoRemoveEventHandlers) + { + _stopActions.Add(server => server.RetainedMessagesClearedAsync -= value); + } + } + remove => _startActions.Add(server => server.RetainedMessagesClearedAsync -= value); + } + + public event Func SessionDeletedAsync + { + add + { + _startActions.Add(server => server.SessionDeletedAsync += value); + if (_hostingOptions.AutoRemoveEventHandlers) + { + _stopActions.Add(server => server.SessionDeletedAsync -= value); + } + } + remove => _startActions.Add(server => server.SessionDeletedAsync -= value); + } + + public event Func StartedAsync + { + add + { + _startActions.Add(server => server.StartedAsync += value); + if (_hostingOptions.AutoRemoveEventHandlers) + { + _stopActions.Add(server => server.StartedAsync -= value); + } + } + remove => _startActions.Add(server => server.StartedAsync -= value); + } + + public event Func StoppedAsync + { + add + { + _startActions.Add(server => server.StoppedAsync += value); + if (_hostingOptions.AutoRemoveEventHandlers) + { + _stopActions.Add(server => server.StoppedAsync -= value); + } + } + remove => _startActions.Add(server => server.StoppedAsync -= value); + } + + public event Func ValidatingConnectionAsync + { + add + { + _startActions.Add(server => server.ValidatingConnectionAsync += value); + if (_hostingOptions.AutoRemoveEventHandlers) + { + _stopActions.Add(server => server.ValidatingConnectionAsync -= value); + } + } + remove => _startActions.Add(server => server.ValidatingConnectionAsync -= value); + } + + public IServiceProvider ServiceProvider { get; } + + public MqttServerHostingBuilder WithDefaultWebSocketEndpoint() + { + _hostingOptions.DefaultWebSocketEndpointOptions.IsEnabled = true; + + return this; + } + + public MqttServerHostingBuilder WithDefaultWebSocketEndpointBoundIPAddress(IPAddress value) + { + _hostingOptions.DefaultWebSocketEndpointOptions.BoundInterNetworkAddress = value; + + return this; + } + + public MqttServerHostingBuilder WithDefaultWebSocketEndpointBoundIPV6Address(IPAddress value) + { + _hostingOptions.DefaultWebSocketEndpointOptions.BoundInterNetworkV6Address = value; + + return this; + } + + public MqttServerHostingBuilder WithDefaultWebSocketEndpointPort(int value) + { + _hostingOptions.DefaultWebSocketEndpointOptions.Port = value; + + return this; + } + + public MqttServerHostingBuilder WithEncryptedWebSocketEndpoint() + { + _hostingOptions.DefaultTlsWebSocketEndpointOptions.IsEnabled = true; + + return this; + } + + public MqttServerHostingBuilder WithEncryptedWebSocketEndpointBoundIPAddress(IPAddress value) + { + _hostingOptions.DefaultTlsWebSocketEndpointOptions.BoundInterNetworkAddress = value; + + return this; + } + + public MqttServerHostingBuilder WithEncryptedWebSocketEndpointBoundIPV6Address(IPAddress value) + { + _hostingOptions.DefaultTlsWebSocketEndpointOptions.BoundInterNetworkV6Address = value; + + return this; + } + + public MqttServerHostingBuilder WithEncryptedWebSocketEndpointPort(int value) + { + _hostingOptions.DefaultTlsWebSocketEndpointOptions.Port = value; + + return this; + } + + public MqttServerHostingBuilder WithoutDefaultWebSocketEndpoint() + { + _hostingOptions.DefaultWebSocketEndpointOptions.IsEnabled = false; + + return this; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.Hosting/Options/MqttServerHostingOptions.cs b/Source/MQTTnet.Extensions.Hosting/Options/MqttServerHostingOptions.cs new file mode 100644 index 000000000..4884a093c --- /dev/null +++ b/Source/MQTTnet.Extensions.Hosting/Options/MqttServerHostingOptions.cs @@ -0,0 +1,13 @@ +using MQTTnet.Extensions.Hosting.Events; + +namespace MQTTnet.Extensions.Hosting.Options +{ + public sealed class MqttServerHostingOptions + { + public bool AutoRemoveEventHandlers { get; set; } = true; + + public MqttServerTlsWebSocketEndpointOptions DefaultTlsWebSocketEndpointOptions { get; } = new MqttServerTlsWebSocketEndpointOptions(); + + public MqttServerWebSocketEndpointOptions DefaultWebSocketEndpointOptions { get; } = new MqttServerWebSocketEndpointOptions(); + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.Hosting/Options/MqttServerTlsWebSocketEndpointOptions.cs b/Source/MQTTnet.Extensions.Hosting/Options/MqttServerTlsWebSocketEndpointOptions.cs new file mode 100644 index 000000000..2a982d73a --- /dev/null +++ b/Source/MQTTnet.Extensions.Hosting/Options/MqttServerTlsWebSocketEndpointOptions.cs @@ -0,0 +1,12 @@ +namespace MQTTnet.Extensions.Hosting.Options +{ + public class MqttServerTlsWebSocketEndpointOptions : MqttServerWebSocketEndpointBaseOptions + { + + public MqttServerTlsWebSocketEndpointOptions() + { + Port = 443; + } + + } +} diff --git a/Source/MQTTnet.Extensions.Hosting/Options/MqttServerWebSocketEndpointBaseOptions.cs b/Source/MQTTnet.Extensions.Hosting/Options/MqttServerWebSocketEndpointBaseOptions.cs new file mode 100644 index 000000000..fe64dee70 --- /dev/null +++ b/Source/MQTTnet.Extensions.Hosting/Options/MqttServerWebSocketEndpointBaseOptions.cs @@ -0,0 +1,17 @@ +using System.Net; + +namespace MQTTnet.Extensions.Hosting.Options +{ + public abstract class MqttServerWebSocketEndpointBaseOptions + { + + public bool IsEnabled { get; set; } + + public int Port { get; set; } + + public IPAddress BoundInterNetworkAddress { get; set; } = IPAddress.Any; + + public IPAddress BoundInterNetworkV6Address { get; set; } = IPAddress.IPv6Any; + + } +} diff --git a/Source/MQTTnet.Extensions.Hosting/Options/MqttServerWebSocketEndpointOptions.cs b/Source/MQTTnet.Extensions.Hosting/Options/MqttServerWebSocketEndpointOptions.cs new file mode 100644 index 000000000..e7b8c14ec --- /dev/null +++ b/Source/MQTTnet.Extensions.Hosting/Options/MqttServerWebSocketEndpointOptions.cs @@ -0,0 +1,12 @@ +namespace MQTTnet.Extensions.Hosting.Options +{ + public class MqttServerWebSocketEndpointOptions : MqttServerWebSocketEndpointBaseOptions + { + + public MqttServerWebSocketEndpointOptions() + { + Port = 80; + } + + } +}