From 7784d584bb8af08b1f7217fa6d7c572ac1918338 Mon Sep 17 00:00:00 2001 From: christian <6939810+chkr1011@users.noreply.github.com> Date: Sun, 27 Oct 2024 18:54:00 +0100 Subject: [PATCH] Implement enhanced authentication for client and server. --- .../Events/ValidatingConnectionEventArgs.cs | 372 ++++++++++-------- .../Internal/MqttClientSessionsManager.cs | 6 +- .../Internal/MqttRetainedMessagesManager.cs | 1 + .../MqttClient/MqttClient_Connection_Tests.cs | 276 +++++++------ Source/MQTTnet.Tests/MQTTnet.Tests.csproj | 6 +- ...ttExtendedAuthenticationExchangeContext.cs | 63 --- .../Formatter/ReadFixedHeaderResult.cs | 35 +- .../Formatter/V5/MqttV5PacketDecoder.cs | 2 +- Source/MQTTnet/IMqttClient.cs | 2 +- Source/MQTTnet/MqttClient.cs | 60 ++- Source/MQTTnet/MqttClientExtensions.cs | 4 +- .../IMqttEnhancedAuthenticationHandler.cs} | 4 +- Source/MQTTnet/Options/MqttClientOptions.cs | 13 +- .../Options/MqttClientOptionsBuilder.cs | 6 +- .../Options/MqttClientOptionsValidator.cs | 5 + .../MqttEnhancedAuthenticationEventArgs.cs | 96 +++++ ...MqttEnhancedAuthenticationExchangeData.cs} | 2 +- 17 files changed, 544 insertions(+), 409 deletions(-) delete mode 100644 Source/MQTTnet/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeContext.cs rename Source/MQTTnet/{ExtendedAuthenticationExchange/IMqttExtendedAuthenticationExchangeHandler.cs => Options/IMqttEnhancedAuthenticationHandler.cs} (64%) create mode 100644 Source/MQTTnet/Options/MqttEnhancedAuthenticationEventArgs.cs rename Source/MQTTnet/{ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeData.cs => Options/MqttEnhancedAuthenticationExchangeData.cs} (97%) diff --git a/Source/MQTTnet.Server/Events/ValidatingConnectionEventArgs.cs b/Source/MQTTnet.Server/Events/ValidatingConnectionEventArgs.cs index cf44bdf52..6c8002f5d 100644 --- a/Source/MQTTnet.Server/Events/ValidatingConnectionEventArgs.cs +++ b/Source/MQTTnet.Server/Events/ValidatingConnectionEventArgs.cs @@ -2,189 +2,223 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Collections; -using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; using System.Text; using MQTTnet.Adapter; +using MQTTnet.Exceptions; using MQTTnet.Formatter; using MQTTnet.Internal; using MQTTnet.Packets; using MQTTnet.Protocol; -namespace MQTTnet.Server +namespace MQTTnet.Server; + +public sealed class ValidatingConnectionEventArgs : EventArgs { - public sealed class ValidatingConnectionEventArgs : EventArgs + readonly MqttConnectPacket _connectPacket; + + public ValidatingConnectionEventArgs(MqttConnectPacket connectPacket, IMqttChannelAdapter clientAdapter, IDictionary sessionItems, CancellationToken cancellationToken) + { + _connectPacket = connectPacket ?? throw new ArgumentNullException(nameof(connectPacket)); + ChannelAdapter = clientAdapter ?? throw new ArgumentNullException(nameof(clientAdapter)); + SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems)); + CancellationToken = cancellationToken; + } + + /// + /// Gets or sets the assigned client identifier. + /// MQTTv5 only. + /// + public string AssignedClientIdentifier { get; set; } + + /// + /// Gets or sets the authentication data. + /// MQTT 5.0.0+ feature. + /// + public byte[] AuthenticationData => _connectPacket.AuthenticationData; + + /// + /// Gets or sets the authentication method. + /// MQTT 5.0.0+ feature. + /// + public string AuthenticationMethod => _connectPacket.AuthenticationMethod; + + public CancellationToken CancellationToken { get; } + + /// + /// Gets the channel adapter. This can be a _MqttConnectionContext_ (used in ASP.NET), a _MqttChannelAdapter_ (used for + /// TCP or WebSockets) or a custom implementation. + /// + public IMqttChannelAdapter ChannelAdapter { get; } + + /// + /// Gets or sets a value indicating whether clean sessions are used or not. + /// When a client connects to a broker it can connect using either a non persistent connection (clean session) or a + /// persistent connection. + /// With a non persistent connection the broker doesn't store any subscription information or undelivered messages for + /// the client. + /// This mode is ideal when the client only publishes messages. + /// It can also connect as a durable client using a persistent connection. + /// In this mode, the broker will store subscription information, and undelivered messages for the client. + /// + public bool? CleanSession => _connectPacket.CleanSession; + + public X509Certificate2 ClientCertificate => ChannelAdapter.ClientCertificate; + + /// + /// Gets the client identifier. + /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. + /// + public string ClientId => _connectPacket.ClientId; + + public string Endpoint => ChannelAdapter.Endpoint; + + public bool IsSecureConnection => ChannelAdapter.IsSecureConnection; + + /// + /// Gets or sets the keep alive period. + /// The connection is normally left open by the client so that is can send and receive data at any time. + /// If no data flows over an open connection for a certain time period then the client will generate a PINGREQ and + /// expect to receive a PINGRESP from the broker. + /// This message exchange confirms that the connection is open and working. + /// This period is known as the keep alive period. + /// + public ushort? KeepAlivePeriod => _connectPacket.KeepAlivePeriod; + + /// + /// A value of 0 indicates that the value is not used. + /// + public uint MaximumPacketSize => _connectPacket.MaximumPacketSize; + + public string Password => Encoding.UTF8.GetString(RawPassword ?? EmptyBuffer.Array); + + public MqttProtocolVersion ProtocolVersion => ChannelAdapter.PacketFormatterAdapter.ProtocolVersion; + + public byte[] RawPassword => _connectPacket.Password; + + /// + /// Gets or sets the reason code. When a MQTTv3 client connects the enum value must be one which is + /// also supported in MQTTv3. Otherwise the connection attempt will fail because not all codes can be + /// converted properly. + /// MQTT 5.0.0+ feature. + /// + public MqttConnectReasonCode ReasonCode { get; set; } = MqttConnectReasonCode.Success; + + public string ReasonString { get; set; } + + /// + /// Gets or sets the receive maximum. + /// This gives the maximum length of the receive messages. + /// A value of 0 indicates that the value is not used. + /// + public ushort ReceiveMaximum => _connectPacket.ReceiveMaximum; + + /// + /// Gets the request problem information. + /// MQTT 5.0.0+ feature. + /// + public bool RequestProblemInformation => _connectPacket.RequestProblemInformation; + + /// + /// Gets the request response information. + /// MQTT 5.0.0+ feature. + /// + public bool RequestResponseInformation => _connectPacket.RequestResponseInformation; + + /// + /// Gets or sets the response authentication data. + /// MQTT 5.0.0+ feature. + /// + public byte[] ResponseAuthenticationData { get; set; } + + /// + /// Gets or sets the response user properties. + /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT + /// packet. + /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add + /// metadata to MQTT messages and pass information between publisher, broker, and subscriber. + /// The feature is very similar to the HTTP header concept. + /// MQTT 5.0.0+ feature. + /// + public List ResponseUserProperties { get; set; } + + /// + /// Gets or sets the server reference. This can be used together with i.e. "Server Moved" to send + /// a different server address to the client. + /// MQTT 5.0.0+ feature. + /// + public string ServerReference { get; set; } + + /// + /// Gets the session expiry interval. + /// The time after a session expires when it's not actively used. + /// A value of 0 means no expiation. + /// + public uint SessionExpiryInterval => _connectPacket.SessionExpiryInterval; + + /// + /// Gets or sets a key/value collection that can be used to share data within the scope of this session. + /// + public IDictionary SessionItems { get; } + + /// + /// Gets or sets the topic alias maximum. + /// This gives the maximum length of the topic alias. + /// A value of 0 indicates that the value is not used. + /// + public ushort TopicAliasMaximum => _connectPacket.TopicAliasMaximum; + + public string UserName => _connectPacket.Username; + + /// + /// Gets or sets the user properties. + /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT + /// packet. + /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add + /// metadata to MQTT messages and pass information between publisher, broker, and subscriber. + /// The feature is very similar to the HTTP header concept. + /// MQTT 5.0.0+ feature. + /// + public List UserProperties => _connectPacket.UserProperties; + + /// + /// Gets or sets the will delay interval. + /// This is the time between the client disconnect and the time the will message will be sent. + /// A value of 0 indicates that the value is not used. + /// + public uint WillDelayInterval => _connectPacket.WillDelayInterval; + + public async Task ExchangeEnhancedAuthenticationAsync(byte[] authenticationData, CancellationToken cancellationToken = default) { - readonly MqttConnectPacket _connectPacket; + var requestAuthPacket = new MqttAuthPacket + { + // From RFC: If the initial CONNECT packet included an Authentication Method property then all AUTH packets, + // and any successful CONNACK packet MUST include an Authentication Method Property with the same value as in the CONNECT packet [MQTT-4.12.0-5]. + AuthenticationMethod = AuthenticationMethod, + AuthenticationData = authenticationData, + + // The reason code will stay at continue all the time when connecting. The server will respond with the + // CONNACK packet when authentication is done! + ReasonCode = MqttAuthenticateReasonCode.ContinueAuthentication + }; + + await ChannelAdapter.SendPacketAsync(requestAuthPacket, cancellationToken).ConfigureAwait(false); + + var responsePacket = await ChannelAdapter.ReceivePacketAsync(cancellationToken).ConfigureAwait(false); + + if (responsePacket == null) + { + throw new MqttCommunicationException("The client closed the connection."); + } - public ValidatingConnectionEventArgs(MqttConnectPacket connectPacket, IMqttChannelAdapter clientAdapter, IDictionary sessionItems) + if (responsePacket is MqttAuthPacket responseAuthPacket) { - _connectPacket = connectPacket ?? throw new ArgumentNullException(nameof(connectPacket)); - ChannelAdapter = clientAdapter ?? throw new ArgumentNullException(nameof(clientAdapter)); - SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems)); + // TODO: Wrap! + return responseAuthPacket; } - /// - /// Gets or sets the assigned client identifier. - /// MQTTv5 only. - /// - public string AssignedClientIdentifier { get; set; } - - /// - /// Gets or sets the authentication data. - /// MQTT 5.0.0+ feature. - /// - public byte[] AuthenticationData => _connectPacket.AuthenticationData; - - /// - /// Gets or sets the authentication method. - /// MQTT 5.0.0+ feature. - /// - public string AuthenticationMethod => _connectPacket.AuthenticationMethod; - - /// - /// Gets the channel adapter. This can be a _MqttConnectionContext_ (used in ASP.NET), a _MqttChannelAdapter_ (used for - /// TCP or WebSockets) or a custom implementation. - /// - public IMqttChannelAdapter ChannelAdapter { get; } - - /// - /// Gets or sets a value indicating whether clean sessions are used or not. - /// When a client connects to a broker it can connect using either a non persistent connection (clean session) or a - /// persistent connection. - /// With a non persistent connection the broker doesn't store any subscription information or undelivered messages for - /// the client. - /// This mode is ideal when the client only publishes messages. - /// It can also connect as a durable client using a persistent connection. - /// In this mode, the broker will store subscription information, and undelivered messages for the client. - /// - public bool? CleanSession => _connectPacket.CleanSession; - - public X509Certificate2 ClientCertificate => ChannelAdapter.ClientCertificate; - - /// - /// Gets the client identifier. - /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. - /// - public string ClientId => _connectPacket.ClientId; - - public string Endpoint => ChannelAdapter.Endpoint; - - public bool IsSecureConnection => ChannelAdapter.IsSecureConnection; - - /// - /// Gets or sets the keep alive period. - /// The connection is normally left open by the client so that is can send and receive data at any time. - /// If no data flows over an open connection for a certain time period then the client will generate a PINGREQ and - /// expect to receive a PINGRESP from the broker. - /// This message exchange confirms that the connection is open and working. - /// This period is known as the keep alive period. - /// - public ushort? KeepAlivePeriod => _connectPacket.KeepAlivePeriod; - - /// - /// A value of 0 indicates that the value is not used. - /// - public uint MaximumPacketSize => _connectPacket.MaximumPacketSize; - - public string Password => Encoding.UTF8.GetString(RawPassword ?? EmptyBuffer.Array); - - public MqttProtocolVersion ProtocolVersion => ChannelAdapter.PacketFormatterAdapter.ProtocolVersion; - - public byte[] RawPassword => _connectPacket.Password; - - /// - /// Gets or sets the reason code. When a MQTTv3 client connects the enum value must be one which is - /// also supported in MQTTv3. Otherwise the connection attempt will fail because not all codes can be - /// converted properly. - /// MQTT 5.0.0+ feature. - /// - public MqttConnectReasonCode ReasonCode { get; set; } = MqttConnectReasonCode.Success; - - public string ReasonString { get; set; } - - /// - /// Gets or sets the receive maximum. - /// This gives the maximum length of the receive messages. - /// A value of 0 indicates that the value is not used. - /// - public ushort ReceiveMaximum => _connectPacket.ReceiveMaximum; - - /// - /// Gets the request problem information. - /// MQTT 5.0.0+ feature. - /// - public bool RequestProblemInformation => _connectPacket.RequestProblemInformation; - - /// - /// Gets the request response information. - /// MQTT 5.0.0+ feature. - /// - public bool RequestResponseInformation => _connectPacket.RequestResponseInformation; - - /// - /// Gets or sets the response authentication data. - /// MQTT 5.0.0+ feature. - /// - public byte[] ResponseAuthenticationData { get; set; } - - /// - /// Gets or sets the response user properties. - /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT - /// packet. - /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add - /// metadata to MQTT messages and pass information between publisher, broker, and subscriber. - /// The feature is very similar to the HTTP header concept. - /// MQTT 5.0.0+ feature. - /// - public List ResponseUserProperties { get; set; } - - /// - /// Gets or sets the server reference. This can be used together with i.e. "Server Moved" to send - /// a different server address to the client. - /// MQTT 5.0.0+ feature. - /// - public string ServerReference { get; set; } - - /// - /// Gets the session expiry interval. - /// The time after a session expires when it's not actively used. - /// A value of 0 means no expiation. - /// - public uint SessionExpiryInterval => _connectPacket.SessionExpiryInterval; - - /// - /// Gets or sets a key/value collection that can be used to share data within the scope of this session. - /// - public IDictionary SessionItems { get; } - - /// - /// Gets or sets the topic alias maximum. - /// This gives the maximum length of the topic alias. - /// A value of 0 indicates that the value is not used. - /// - public ushort TopicAliasMaximum => _connectPacket.TopicAliasMaximum; - - public string UserName => _connectPacket.Username; - - /// - /// Gets or sets the user properties. - /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT - /// packet. - /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add - /// metadata to MQTT messages and pass information between publisher, broker, and subscriber. - /// The feature is very similar to the HTTP header concept. - /// MQTT 5.0.0+ feature. - /// - public List UserProperties => _connectPacket.UserProperties; - - /// - /// Gets or sets the will delay interval. - /// This is the time between the client disconnect and the time the will message will be sent. - /// A value of 0 indicates that the value is not used. - /// - public uint WillDelayInterval => _connectPacket.WillDelayInterval; + // TODO: Support DISCONNECT- + throw new MqttProtocolViolationException("Received other packet than AUTH while authenticating"); } } \ No newline at end of file diff --git a/Source/MQTTnet.Server/Internal/MqttClientSessionsManager.cs b/Source/MQTTnet.Server/Internal/MqttClientSessionsManager.cs index 6c0cf68ec..66d8f78b6 100644 --- a/Source/MQTTnet.Server/Internal/MqttClientSessionsManager.cs +++ b/Source/MQTTnet.Server/Internal/MqttClientSessionsManager.cs @@ -348,7 +348,7 @@ public async Task HandleClientConnectionAsync(IMqttChannelAdapter channelAdapter return; } - var validatingConnectionEventArgs = await ValidateConnection(connectPacket, channelAdapter).ConfigureAwait(false); + var validatingConnectionEventArgs = await ValidateConnection(connectPacket, channelAdapter, cancellationToken).ConfigureAwait(false); var connAckPacket = MqttConnAckPacketFactory.Create(validatingConnectionEventArgs); if (validatingConnectionEventArgs.ReasonCode != MqttConnectReasonCode.Success) @@ -710,11 +710,11 @@ static bool ShouldPersistSession(MqttConnectedClient connectedClient) } } - async Task ValidateConnection(MqttConnectPacket connectPacket, IMqttChannelAdapter channelAdapter) + async Task ValidateConnection(MqttConnectPacket connectPacket, IMqttChannelAdapter channelAdapter, CancellationToken cancellationToken) { // TODO: Load session items from persisted sessions in the future. var sessionItems = new ConcurrentDictionary(); - var eventArgs = new ValidatingConnectionEventArgs(connectPacket, channelAdapter, sessionItems); + var eventArgs = new ValidatingConnectionEventArgs(connectPacket, channelAdapter, sessionItems, cancellationToken); await _eventContainer.ValidatingConnectionEvent.InvokeAsync(eventArgs).ConfigureAwait(false); // Check the client ID and set a random one if supported. diff --git a/Source/MQTTnet.Server/Internal/MqttRetainedMessagesManager.cs b/Source/MQTTnet.Server/Internal/MqttRetainedMessagesManager.cs index 854696c73..f4bd1965e 100644 --- a/Source/MQTTnet.Server/Internal/MqttRetainedMessagesManager.cs +++ b/Source/MQTTnet.Server/Internal/MqttRetainedMessagesManager.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.ObjectModel; using MQTTnet.Diagnostics.Logger; using MQTTnet.Internal; diff --git a/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Connection_Tests.cs b/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Connection_Tests.cs index 75b00d39d..b71782ce5 100644 --- a/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Connection_Tests.cs +++ b/Source/MQTTnet.Tests/Clients/MqttClient/MqttClient_Connection_Tests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -24,10 +25,8 @@ public sealed class MqttClient_Connection_Tests : BaseTestClass public async Task Connect_To_Invalid_Server_Port_Not_Opened() { var client = new MqttClientFactory().CreateMqttClient(); - using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5))) - { - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", 12345).Build(), timeout.Token); - } + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", 12345).Build(), timeout.Token); } [TestMethod] @@ -35,10 +34,8 @@ public async Task Connect_To_Invalid_Server_Port_Not_Opened() public async Task Connect_To_Invalid_Server_Wrong_IP() { var client = new MqttClientFactory().CreateMqttClient(); - using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(2))) - { - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("1.2.3.4").Build(), timeout.Token); - } + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("1.2.3.4").Build(), timeout.Token); } [TestMethod] @@ -53,179 +50,220 @@ public async Task Connect_To_Invalid_Server_Wrong_Protocol() public async Task ConnectTimeout_Throws_Exception() { var factory = new MqttClientFactory(); - using (var client = factory.CreateMqttClient()) + using var client = factory.CreateMqttClient(); + var disconnectHandlerCalled = false; + try { - var disconnectHandlerCalled = false; - try + client.DisconnectedAsync += args => { - client.DisconnectedAsync += args => - { - disconnectHandlerCalled = true; - return CompletedTask.Instance; - }; + disconnectHandlerCalled = true; + return CompletedTask.Instance; + }; - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1").Build()); + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1").Build()); - Assert.Fail("Must fail!"); - } - catch (Exception exception) - { - Assert.IsNotNull(exception); - Assert.IsInstanceOfType(exception, typeof(MqttCommunicationException)); - } - - await LongTestDelay(); // disconnected handler is called async - Assert.IsTrue(disconnectHandlerCalled); + Assert.Fail("Must fail!"); + } + catch (Exception exception) + { + Assert.IsNotNull(exception); + Assert.IsInstanceOfType(exception, typeof(MqttCommunicationException)); } + + await LongTestDelay(); // disconnected handler is called async + Assert.IsTrue(disconnectHandlerCalled); } [TestMethod] public async Task Disconnect_Clean() { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) - { - var server = await testEnvironment.StartServer(); + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + var server = await testEnvironment.StartServer(); - ClientDisconnectedEventArgs eventArgs = null; - server.ClientDisconnectedAsync += args => - { - eventArgs = args; - return CompletedTask.Instance; - }; + ClientDisconnectedEventArgs eventArgs = null; + server.ClientDisconnectedAsync += args => + { + eventArgs = args; + return CompletedTask.Instance; + }; - var client = await testEnvironment.ConnectClient(); + var client = await testEnvironment.ConnectClient(); - var disconnectOptions = testEnvironment.ClientFactory.CreateClientDisconnectOptionsBuilder().WithReason(MqttClientDisconnectOptionsReason.MessageRateTooHigh).Build(); + var disconnectOptions = testEnvironment.ClientFactory.CreateClientDisconnectOptionsBuilder().WithReason(MqttClientDisconnectOptionsReason.MessageRateTooHigh).Build(); - // Perform a clean disconnect. - await client.DisconnectAsync(disconnectOptions); + // Perform a clean disconnect. + await client.DisconnectAsync(disconnectOptions); - await LongTestDelay(); + await LongTestDelay(); - Assert.IsNotNull(eventArgs); - Assert.AreEqual(MqttClientDisconnectType.Clean, eventArgs.DisconnectType); - } + Assert.IsNotNull(eventArgs); + Assert.AreEqual(MqttClientDisconnectType.Clean, eventArgs.DisconnectType); } [TestMethod] public async Task Disconnect_Clean_With_Custom_Reason() { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) - { - var server = await testEnvironment.StartServer(); + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + var server = await testEnvironment.StartServer(); - ClientDisconnectedEventArgs eventArgs = null; - server.ClientDisconnectedAsync += args => - { - eventArgs = args; - return CompletedTask.Instance; - }; + ClientDisconnectedEventArgs eventArgs = null; + server.ClientDisconnectedAsync += args => + { + eventArgs = args; + return CompletedTask.Instance; + }; - var client = await testEnvironment.ConnectClient(); + var client = await testEnvironment.ConnectClient(); - var disconnectOptions = testEnvironment.ClientFactory.CreateClientDisconnectOptionsBuilder().WithReason(MqttClientDisconnectOptionsReason.MessageRateTooHigh).Build(); + var disconnectOptions = testEnvironment.ClientFactory.CreateClientDisconnectOptionsBuilder().WithReason(MqttClientDisconnectOptionsReason.MessageRateTooHigh).Build(); - // Perform a clean disconnect. - await client.DisconnectAsync(disconnectOptions); + // Perform a clean disconnect. + await client.DisconnectAsync(disconnectOptions); - await LongTestDelay(); + await LongTestDelay(); - Assert.IsNotNull(eventArgs); - Assert.AreEqual(MqttDisconnectReasonCode.MessageRateTooHigh, eventArgs.ReasonCode); - } + Assert.IsNotNull(eventArgs); + Assert.AreEqual(MqttDisconnectReasonCode.MessageRateTooHigh, eventArgs.ReasonCode); } [TestMethod] public async Task Disconnect_Clean_With_User_Properties() { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + var server = await testEnvironment.StartServer(); + + ClientDisconnectedEventArgs eventArgs = null; + server.ClientDisconnectedAsync += args => { - var server = await testEnvironment.StartServer(); + eventArgs = args; + return CompletedTask.Instance; + }; - ClientDisconnectedEventArgs eventArgs = null; - server.ClientDisconnectedAsync += args => - { - eventArgs = args; - return CompletedTask.Instance; - }; + var client = await testEnvironment.ConnectClient(); - var client = await testEnvironment.ConnectClient(); + var disconnectOptions = testEnvironment.ClientFactory.CreateClientDisconnectOptionsBuilder().WithUserProperty("test_name", "test_value").Build(); - var disconnectOptions = testEnvironment.ClientFactory.CreateClientDisconnectOptionsBuilder().WithUserProperty("test_name", "test_value").Build(); + // Perform a clean disconnect. + await client.DisconnectAsync(disconnectOptions); - // Perform a clean disconnect. - await client.DisconnectAsync(disconnectOptions); + await LongTestDelay(); - await LongTestDelay(); + Assert.IsNotNull(eventArgs); + Assert.IsNotNull(eventArgs.UserProperties); + Assert.AreEqual(1, eventArgs.UserProperties.Count); + Assert.AreEqual("test_name", eventArgs.UserProperties[0].Name); + Assert.AreEqual("test_value", eventArgs.UserProperties[0].Value); + } - Assert.IsNotNull(eventArgs); - Assert.IsNotNull(eventArgs.UserProperties); - Assert.AreEqual(1, eventArgs.UserProperties.Count); - Assert.AreEqual("test_name", eventArgs.UserProperties[0].Name); - Assert.AreEqual("test_value", eventArgs.UserProperties[0].Value); + class TestClientKerberosAuthenticationHandler : IMqttEnhancedAuthenticationHandler + { + public async Task HandleEnhancedAuthenticationAsync(MqttEnhancedAuthenticationEventArgs eventArgs) + { + if (eventArgs.AuthenticationMethod != "GS2-KRB5") + { + throw new InvalidOperationException("Wrong authentication method"); + } + + await eventArgs.SendAsync("initial context token"u8.ToArray()); + + var response = await eventArgs.ReceiveAsync(CancellationToken.None); + + Assert.AreEqual(Encoding.UTF8.GetString(response.AuthenticationData), "reply context token"); + + // No further data is required, but we have to fulfil the exchange. + await eventArgs.SendAsync([], CancellationToken.None); } } [TestMethod] - public async Task No_Unobserved_Exception() + public async Task Use_Enhanced_Authentication() { - using (var testEnvironment = CreateTestEnvironment()) + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + var server = await testEnvironment.StartServer(); + + server.ValidatingConnectionAsync += async args => { - testEnvironment.IgnoreClientLogErrors = true; + if (args.AuthenticationMethod == "GS2-KRB5") + { + var authPacket = await args.ExchangeEnhancedAuthenticationAsync(null, args.CancellationToken); - var client = testEnvironment.CreateClient(); - var options = new MqttClientOptionsBuilder().WithTcpServer("1.2.3.4").WithTimeout(TimeSpan.FromSeconds(2)).Build(); + Assert.AreEqual(Encoding.UTF8.GetString(authPacket.AuthenticationData), "initial context token"); - try - { - using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(0.5))) - { - await client.ConnectAsync(options, timeout.Token); - } + var response = await args.ExchangeEnhancedAuthenticationAsync("reply context token"u8.ToArray(), args.CancellationToken); + + Assert.AreEqual(Encoding.UTF8.GetString(response.AuthenticationData), ""); + + args.ResponseAuthenticationData = "outcome of authentication"u8.ToArray(); } - catch (OperationCanceledException) + else { + args.ReasonCode = MqttConnectReasonCode.BadAuthenticationMethod; } + }; + + // Use Kerberos sample from the MQTT RFC. + var kerberosAuthenticationHandler = new TestClientKerberosAuthenticationHandler(); + + var clientOptions = testEnvironment.CreateDefaultClientOptionsBuilder().WithAuthentication("GS2-KRB5").WithEnhancedAuthenticationHandler(kerberosAuthenticationHandler); + var client = await testEnvironment.ConnectClient(clientOptions); - client.Dispose(); + Assert.IsTrue(client.IsConnected); + } - // These delays and GC calls are required in order to make calling the finalizer reproducible. - GC.Collect(); - GC.WaitForPendingFinalizers(); - await LongTestDelay(); - await LongTestDelay(); - await LongTestDelay(); + [TestMethod] + public async Task No_Unobserved_Exception() + { + using var testEnvironment = CreateTestEnvironment(); + testEnvironment.IgnoreClientLogErrors = true; + + var client = testEnvironment.CreateClient(); + var options = new MqttClientOptionsBuilder().WithTcpServer("1.2.3.4").WithTimeout(TimeSpan.FromSeconds(2)).Build(); + + try + { + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(0.5)); + await client.ConnectAsync(options, timeout.Token); } + catch (OperationCanceledException) + { + } + + client.Dispose(); + + // These delays and GC calls are required in order to make calling the finalizer reproducible. + GC.Collect(); + GC.WaitForPendingFinalizers(); + await LongTestDelay(); + await LongTestDelay(); + await LongTestDelay(); } [TestMethod] public async Task Return_Non_Success() { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) - { - var server = await testEnvironment.StartServer(); + using var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500); + var server = await testEnvironment.StartServer(); - server.ValidatingConnectionAsync += args => + server.ValidatingConnectionAsync += args => + { + args.ResponseUserProperties = new List { - args.ResponseUserProperties = new List - { - new MqttUserProperty("Property", "Value") - }; + new MqttUserProperty("Property", "Value") + }; - args.ReasonCode = MqttConnectReasonCode.QuotaExceeded; + args.ReasonCode = MqttConnectReasonCode.QuotaExceeded; - return CompletedTask.Instance; - }; + return CompletedTask.Instance; + }; - var client = testEnvironment.CreateClient(); + var client = testEnvironment.CreateClient(); - var response = await client.ConnectAsync(testEnvironment.CreateDefaultClientOptionsBuilder().Build()); + var response = await client.ConnectAsync(testEnvironment.CreateDefaultClientOptionsBuilder().Build()); - Assert.IsNotNull(response); - Assert.AreEqual(MqttClientConnectResultCode.QuotaExceeded, response.ResultCode); - Assert.AreEqual(response.UserProperties[0].Name, "Property"); - Assert.AreEqual(response.UserProperties[0].Value, "Value"); - } + Assert.IsNotNull(response); + Assert.AreEqual(MqttClientConnectResultCode.QuotaExceeded, response.ResultCode); + Assert.AreEqual(response.UserProperties[0].Name, "Property"); + Assert.AreEqual(response.UserProperties[0].Value, "Value"); } [TestMethod] @@ -234,10 +272,8 @@ public async Task Throw_Proper_Exception_When_Not_Connected() try { var mqttFactory = new MqttClientFactory(); - using (var mqttClient = mqttFactory.CreateMqttClient()) - { - await mqttClient.SubscribeAsync("test", MqttQualityOfServiceLevel.AtLeastOnce); - } + using var mqttClient = mqttFactory.CreateMqttClient(); + await mqttClient.SubscribeAsync("test", MqttQualityOfServiceLevel.AtLeastOnce); } catch (MqttClientNotConnectedException exception) { diff --git a/Source/MQTTnet.Tests/MQTTnet.Tests.csproj b/Source/MQTTnet.Tests/MQTTnet.Tests.csproj index b29190740..10482e4df 100644 --- a/Source/MQTTnet.Tests/MQTTnet.Tests.csproj +++ b/Source/MQTTnet.Tests/MQTTnet.Tests.csproj @@ -16,9 +16,9 @@ - - - + + + diff --git a/Source/MQTTnet/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeContext.cs b/Source/MQTTnet/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeContext.cs deleted file mode 100644 index 1f93a8075..000000000 --- a/Source/MQTTnet/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeContext.cs +++ /dev/null @@ -1,63 +0,0 @@ -// 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. - -using System; -using System.Collections.Generic; -using MQTTnet.Packets; -using MQTTnet.Protocol; - -namespace MQTTnet; - -public class MqttExtendedAuthenticationExchangeContext -{ - public MqttExtendedAuthenticationExchangeContext(MqttAuthPacket authPacket, MqttClient client) - { - ArgumentNullException.ThrowIfNull(authPacket); - - ReasonCode = authPacket.ReasonCode; - ReasonString = authPacket.ReasonString; - AuthenticationMethod = authPacket.AuthenticationMethod; - AuthenticationData = authPacket.AuthenticationData; - UserProperties = authPacket.UserProperties; - - Client = client ?? throw new ArgumentNullException(nameof(client)); - } - - /// - /// Gets the authentication data. - /// Hint: MQTT 5 feature only. - /// - public byte[] AuthenticationData { get; } - - /// - /// Gets the authentication method. - /// Hint: MQTT 5 feature only. - /// - public string AuthenticationMethod { get; } - - public MqttClient Client { get; } - - /// - /// Gets the reason code. - /// Hint: MQTT 5 feature only. - /// - public MqttAuthenticateReasonCode ReasonCode { get; } - - /// - /// Gets the reason string. - /// Hint: MQTT 5 feature only. - /// - public string ReasonString { get; } - - /// - /// Gets the user properties. - /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT - /// packet. - /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add - /// metadata to MQTT messages and pass information between publisher, broker, and subscriber. - /// The feature is very similar to the HTTP header concept. - /// Hint: MQTT 5 feature only. - /// - public List UserProperties { get; } -} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/ReadFixedHeaderResult.cs b/Source/MQTTnet/Formatter/ReadFixedHeaderResult.cs index a471a6a7b..d183e1f4b 100644 --- a/Source/MQTTnet/Formatter/ReadFixedHeaderResult.cs +++ b/Source/MQTTnet/Formatter/ReadFixedHeaderResult.cs @@ -2,24 +2,23 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MQTTnet.Formatter +namespace MQTTnet.Formatter; + +public struct ReadFixedHeaderResult { - public struct ReadFixedHeaderResult + public static ReadFixedHeaderResult Canceled { get; } = new() + { + IsCanceled = true + }; + + public static ReadFixedHeaderResult ConnectionClosed { get; } = new() { - public static ReadFixedHeaderResult Canceled { get; } = new ReadFixedHeaderResult - { - IsCanceled = true - }; - - public static ReadFixedHeaderResult ConnectionClosed { get; } = new ReadFixedHeaderResult - { - IsConnectionClosed = true - }; - - public bool IsCanceled { get; set; } - - public bool IsConnectionClosed { get; set; } + IsConnectionClosed = true + }; + + public bool IsCanceled { get; set; } + + public bool IsConnectionClosed { get; init; } - public MqttFixedHeader FixedHeader { get; set; } - } -} + public MqttFixedHeader FixedHeader { get; init; } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/V5/MqttV5PacketDecoder.cs b/Source/MQTTnet/Formatter/V5/MqttV5PacketDecoder.cs index e27f129fc..7f6a11f1a 100644 --- a/Source/MQTTnet/Formatter/V5/MqttV5PacketDecoder.cs +++ b/Source/MQTTnet/Formatter/V5/MqttV5PacketDecoder.cs @@ -13,7 +13,7 @@ namespace MQTTnet.Formatter.V5 { public sealed class MqttV5PacketDecoder { - readonly MqttBufferReader _bufferReader = new MqttBufferReader(); + readonly MqttBufferReader _bufferReader = new(); public MqttPacket Decode(ReceivedMqttPacket receivedMqttPacket) { diff --git a/Source/MQTTnet/IMqttClient.cs b/Source/MQTTnet/IMqttClient.cs index f854c8ae3..445fd4e10 100644 --- a/Source/MQTTnet/IMqttClient.cs +++ b/Source/MQTTnet/IMqttClient.cs @@ -29,7 +29,7 @@ public interface IMqttClient : IDisposable Task PublishAsync(MqttApplicationMessage applicationMessage, CancellationToken cancellationToken = default); - Task SendExtendedAuthenticationExchangeDataAsync(MqttExtendedAuthenticationExchangeData data, CancellationToken cancellationToken = default); + Task SendEnhancedAuthenticationExchangeDataAsync(MqttEnhancedAuthenticationExchangeData data, CancellationToken cancellationToken = default); Task SubscribeAsync(MqttClientSubscribeOptions options, CancellationToken cancellationToken = default); diff --git a/Source/MQTTnet/MqttClient.cs b/Source/MQTTnet/MqttClient.cs index 9d19ce574..2f46c2626 100644 --- a/Source/MQTTnet/MqttClient.cs +++ b/Source/MQTTnet/MqttClient.cs @@ -288,7 +288,7 @@ public Task PublishAsync(MqttApplicationMessage applica } } - public Task SendExtendedAuthenticationExchangeDataAsync(MqttExtendedAuthenticationExchangeData data, CancellationToken cancellationToken = default) + public Task SendEnhancedAuthenticationExchangeDataAsync(MqttEnhancedAuthenticationExchangeData data, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(data); @@ -437,27 +437,30 @@ async Task Authenticate(IMqttChannelAdapter channelAdap var connectPacket = MqttConnectPacketFactory.Create(options); await Send(connectPacket, cancellationToken).ConfigureAwait(false); - var receivedPacket = await Receive(cancellationToken).ConfigureAwait(false); - - switch (receivedPacket) + while (true) { - case MqttConnAckPacket connAckPacket: - { - result = MqttClientResultFactory.ConnectResult.Create(connAckPacket, channelAdapter.PacketFormatterAdapter.ProtocolVersion); - break; - } - case MqttAuthPacket _: + cancellationToken.ThrowIfCancellationRequested(); + + var receivedPacket = await Receive(cancellationToken).ConfigureAwait(false); + + if (receivedPacket is MqttAuthPacket authPacket) { - throw new NotSupportedException("Extended authentication handler is not yet supported"); + await HandleEnhancedAuthentication(authPacket); + continue; } - case null: + + if (receivedPacket is MqttConnAckPacket connAckPacket) { - throw new MqttCommunicationException("Connection closed."); + result = MqttClientResultFactory.ConnectResult.Create(connAckPacket, channelAdapter.PacketFormatterAdapter.ProtocolVersion); + break; } - default: + + if (receivedPacket != null) { - throw new InvalidOperationException($"Received an unexpected MQTT packet ({receivedPacket})."); + throw new MqttProtocolViolationException($"Received other packet than CONNACK or AUTH while connecting ({receivedPacket})."); } + + throw new MqttCommunicationException("Connection closed."); } } catch (Exception exception) @@ -470,6 +473,12 @@ async Task Authenticate(IMqttChannelAdapter channelAdap return result; } + async Task HandleEnhancedAuthentication(MqttAuthPacket authPacket) + { + var eventArgs = new MqttEnhancedAuthenticationEventArgs(authPacket, _adapter); + await Options.EnhancedAuthenticationHandler.HandleEnhancedAuthenticationAsync(eventArgs); + } + void Cleanup() { try @@ -648,10 +657,23 @@ Task OnConnected(MqttClientConnectResult connectResult) Task ProcessReceivedAuthPacket(MqttAuthPacket authPacket) { - var extendedAuthenticationExchangeHandler = Options.ExtendedAuthenticationExchangeHandler; - return extendedAuthenticationExchangeHandler != null - ? extendedAuthenticationExchangeHandler.HandleRequestAsync(new MqttExtendedAuthenticationExchangeContext(authPacket, this)) - : CompletedTask.Instance; + if (Options.EnhancedAuthenticationHandler == null) + { + // From RFC: If the re-authentication fails, the Client or Server SHOULD send DISCONNECT with an appropriate Reason Code + // as described in section 4.13, and MUST close the Network Connection [MQTT-4.12.1-2]. + // + // Since we have no handler there is no chance to fulfil the re-authentication request. + _ = DisconnectAsync(new MqttClientDisconnectOptions + { + Reason = MqttClientDisconnectOptionsReason.ImplementationSpecificError, + ReasonString = "Unable to handle AUTH packet" + }); + + return CompletedTask.Instance; + } + + var eventArgs = new MqttEnhancedAuthenticationEventArgs(authPacket, _adapter); + return Options.EnhancedAuthenticationHandler.HandleEnhancedAuthenticationAsync(eventArgs); } Task ProcessReceivedDisconnectPacket(MqttDisconnectPacket disconnectPacket) diff --git a/Source/MQTTnet/MqttClientExtensions.cs b/Source/MQTTnet/MqttClientExtensions.cs index f6d09dd08..58d9c9f02 100644 --- a/Source/MQTTnet/MqttClientExtensions.cs +++ b/Source/MQTTnet/MqttClientExtensions.cs @@ -99,11 +99,11 @@ public static Task ReconnectAsync(this IMqttClient client, CancellationToken can return client.ConnectAsync(client.Options, cancellationToken); } - public static Task SendExtendedAuthenticationExchangeDataAsync(this IMqttClient client, MqttExtendedAuthenticationExchangeData data) + public static Task SendEnhancedAuthenticationExchangeDataAsync(this IMqttClient client, MqttEnhancedAuthenticationExchangeData data) { ArgumentNullException.ThrowIfNull(client); - return client.SendExtendedAuthenticationExchangeDataAsync(data, CancellationToken.None); + return client.SendEnhancedAuthenticationExchangeDataAsync(data, CancellationToken.None); } public static Task SubscribeAsync(this IMqttClient mqttClient, MqttTopicFilter topicFilter, CancellationToken cancellationToken = default) diff --git a/Source/MQTTnet/ExtendedAuthenticationExchange/IMqttExtendedAuthenticationExchangeHandler.cs b/Source/MQTTnet/Options/IMqttEnhancedAuthenticationHandler.cs similarity index 64% rename from Source/MQTTnet/ExtendedAuthenticationExchange/IMqttExtendedAuthenticationExchangeHandler.cs rename to Source/MQTTnet/Options/IMqttEnhancedAuthenticationHandler.cs index b9454421d..e441eb05a 100644 --- a/Source/MQTTnet/ExtendedAuthenticationExchange/IMqttExtendedAuthenticationExchangeHandler.cs +++ b/Source/MQTTnet/Options/IMqttEnhancedAuthenticationHandler.cs @@ -6,7 +6,7 @@ namespace MQTTnet; -public interface IMqttExtendedAuthenticationExchangeHandler +public interface IMqttEnhancedAuthenticationHandler { - Task HandleRequestAsync(MqttExtendedAuthenticationExchangeContext context); + Task HandleEnhancedAuthenticationAsync(MqttEnhancedAuthenticationEventArgs eventArgs); } \ No newline at end of file diff --git a/Source/MQTTnet/Options/MqttClientOptions.cs b/Source/MQTTnet/Options/MqttClientOptions.cs index d337ea0e8..da9848a74 100644 --- a/Source/MQTTnet/Options/MqttClientOptions.cs +++ b/Source/MQTTnet/Options/MqttClientOptions.cs @@ -13,7 +13,7 @@ namespace MQTTnet; public sealed class MqttClientOptions { /// - /// Usually the MQTT packets can be send partially. This is done by using multiple TCP packets + /// Usually the MQTT packets can be sent partially. This is done by using multiple TCP packets /// or WebSocket frames etc. Unfortunately not all brokers (like Amazon Web Services (AWS)) do support this feature and /// will close the connection when receiving such packets. If such a service is used this flag must /// be set to _false_. @@ -38,7 +38,7 @@ public sealed class MqttClientOptions /// Gets or sets a value indicating whether clean sessions are used or not. /// When a client connects to a broker it can connect using either a non persistent connection (clean session) or a /// persistent connection. - /// With a non persistent connection the broker doesn't store any subscription information or undelivered messages for + /// With a non-persistent connection the broker doesn't store any subscription information or undelivered messages for /// the client. /// This mode is ideal when the client only publishes messages. /// It can also connect as a durable client using a persistent connection. @@ -54,7 +54,12 @@ public sealed class MqttClientOptions public IMqttClientCredentialsProvider Credentials { get; set; } - public IMqttExtendedAuthenticationExchangeHandler ExtendedAuthenticationExchangeHandler { get; set; } + /// + /// Gets or sets the handler for AUTH packets. + /// This can happen when connecting or at any time while being already connected. + /// MQTT 5.0.0+ feature. + /// + public IMqttEnhancedAuthenticationHandler EnhancedAuthenticationHandler { get; set; } /// /// Gets or sets the keep alive period. @@ -80,7 +85,7 @@ public sealed class MqttClientOptions /// /// Gets or sets the receive maximum. - /// This gives the maximum length of the receive messages. + /// This gives the maximum length of the received messages. /// MQTT 5.0.0+ feature. /// public ushort ReceiveMaximum { get; set; } diff --git a/Source/MQTTnet/Options/MqttClientOptionsBuilder.cs b/Source/MQTTnet/Options/MqttClientOptionsBuilder.cs index 0afb64de0..ae5f48329 100644 --- a/Source/MQTTnet/Options/MqttClientOptionsBuilder.cs +++ b/Source/MQTTnet/Options/MqttClientOptionsBuilder.cs @@ -98,7 +98,7 @@ public MqttClientOptionsBuilder WithAddressFamily(AddressFamily addressFamily) return this; } - public MqttClientOptionsBuilder WithAuthentication(string method, byte[] data) + public MqttClientOptionsBuilder WithAuthentication(string method, byte[] data = null) { _options.AuthenticationMethod = method; _options.AuthenticationData = data; @@ -205,9 +205,9 @@ public MqttClientOptionsBuilder WithEndPoint(EndPoint endPoint) return this; } - public MqttClientOptionsBuilder WithExtendedAuthenticationExchangeHandler(IMqttExtendedAuthenticationExchangeHandler handler) + public MqttClientOptionsBuilder WithEnhancedAuthenticationHandler(IMqttEnhancedAuthenticationHandler handler) { - _options.ExtendedAuthenticationExchangeHandler = handler; + _options.EnhancedAuthenticationHandler = handler; return this; } diff --git a/Source/MQTTnet/Options/MqttClientOptionsValidator.cs b/Source/MQTTnet/Options/MqttClientOptionsValidator.cs index 6e9c09f9b..c749f4b22 100644 --- a/Source/MQTTnet/Options/MqttClientOptionsValidator.cs +++ b/Source/MQTTnet/Options/MqttClientOptionsValidator.cs @@ -101,6 +101,11 @@ public static void ThrowIfNotSupported(MqttClientOptions options) { Throw(nameof(options.WillUserProperties)); } + + if (options.EnhancedAuthenticationHandler != null) + { + Throw(nameof(options.EnhancedAuthenticationHandler)); + } } static void Throw(string featureName) diff --git a/Source/MQTTnet/Options/MqttEnhancedAuthenticationEventArgs.cs b/Source/MQTTnet/Options/MqttEnhancedAuthenticationEventArgs.cs new file mode 100644 index 000000000..02a90f765 --- /dev/null +++ b/Source/MQTTnet/Options/MqttEnhancedAuthenticationEventArgs.cs @@ -0,0 +1,96 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Adapter; +using MQTTnet.Exceptions; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet; + +public class MqttEnhancedAuthenticationEventArgs : EventArgs +{ + readonly IMqttChannelAdapter _channelAdapter; + + public MqttEnhancedAuthenticationEventArgs(MqttAuthPacket initialAuthPacket, IMqttChannelAdapter channelAdapter) + { + ArgumentNullException.ThrowIfNull(initialAuthPacket); + + _channelAdapter = channelAdapter ?? throw new ArgumentNullException(nameof(channelAdapter)); + + ReasonCode = initialAuthPacket.ReasonCode; + ReasonString = initialAuthPacket.ReasonString; + AuthenticationMethod = initialAuthPacket.AuthenticationMethod; + AuthenticationData = initialAuthPacket.AuthenticationData; + UserProperties = initialAuthPacket.UserProperties; + } + + /// + /// Gets the authentication data. + /// Hint: MQTT 5 feature only. + /// + public byte[] AuthenticationData { get; } + + /// + /// Gets the authentication method. + /// Hint: MQTT 5 feature only. + /// + public string AuthenticationMethod { get; } + + /// + /// Gets the reason code. + /// Hint: MQTT 5 feature only. + /// + public MqttAuthenticateReasonCode ReasonCode { get; } + + /// + /// Gets the reason string. + /// Hint: MQTT 5 feature only. + /// + public string ReasonString { get; } + + /// + /// Gets the user properties. + /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT + /// packet. + /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add + /// metadata to MQTT messages and pass information between publisher, broker, and subscriber. + /// The feature is very similar to the HTTP header concept. + /// Hint: MQTT 5 feature only. + /// + public List UserProperties { get; } + + public async Task ReceiveAsync(CancellationToken cancellationToken = default) + { + var receivedPacket = await _channelAdapter.ReceivePacketAsync(cancellationToken).ConfigureAwait(false); + + if (receivedPacket is MqttAuthPacket authPacket) + { + return authPacket; + } + + if (receivedPacket is MqttConnAckPacket connAckPacket) + { + throw new InvalidOperationException("The enhanced authentication handler must not wait for the CONNACK packet."); + } + + throw new MqttProtocolViolationException("Received wrong paket."); + } + + public Task SendAsync(byte[] authenticationData, CancellationToken cancellationToken = default) + { + return _channelAdapter.SendPacketAsync( + new MqttAuthPacket + { + ReasonCode = MqttAuthenticateReasonCode.ContinueAuthentication, + AuthenticationMethod = AuthenticationMethod, + AuthenticationData = authenticationData + }, + cancellationToken); + } +} \ No newline at end of file diff --git a/Source/MQTTnet/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeData.cs b/Source/MQTTnet/Options/MqttEnhancedAuthenticationExchangeData.cs similarity index 97% rename from Source/MQTTnet/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeData.cs rename to Source/MQTTnet/Options/MqttEnhancedAuthenticationExchangeData.cs index 2c58774c2..0e7ceafb8 100644 --- a/Source/MQTTnet/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeData.cs +++ b/Source/MQTTnet/Options/MqttEnhancedAuthenticationExchangeData.cs @@ -8,7 +8,7 @@ namespace MQTTnet; -public class MqttExtendedAuthenticationExchangeData +public class MqttEnhancedAuthenticationExchangeData { /// /// Gets or sets the authentication data.