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;
+ }
+
+ }
+}