diff --git a/Source/MQTTnet.Tests/Clients/MqttClient/Extended_Authentication_Tests.cs b/Source/MQTTnet.Tests/Clients/MqttClient/Extended_Authentication_Tests.cs new file mode 100644 index 00000000..a21c0f1c --- /dev/null +++ b/Source/MQTTnet.Tests/Clients/MqttClient/Extended_Authentication_Tests.cs @@ -0,0 +1,159 @@ +// 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.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Client; +using MQTTnet.Formatter; +using MQTTnet.Internal; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Tests.Clients.MqttClient +{ + [TestClass] + public sealed class Extended_Authentication_Tests : BaseTestClass + { + [TestMethod] + public async Task ReAuthenticate() + { + using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) + { + var server = await testEnvironment.StartServer(); + + server.ValidatingConnectionAsync += async args => + { + using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10))) + { + // Just do a simple custom authentication. + await args.SendAuthenticationDataAsync(MqttAuthenticateReasonCode.ContinueAuthentication, cancellationToken: timeout.Token); + var clientResponse = await args.ReceiveAuthenticationDataAsync(timeout.Token); + CollectionAssert.AreEqual(clientResponse.AuthenticationData, Encoding.ASCII.GetBytes("TOKEN")); + + var userProperties = new MqttUserPropertiesBuilder().With("x", "y").Build(); + await args.SendAuthenticationDataAsync(MqttAuthenticateReasonCode.ContinueAuthentication, cancellationToken: timeout.Token, userProperties: userProperties); + + args.ReasonCode = MqttConnectReasonCode.Success; + } + }; + + server.ClientReAuthenticatingAsync += args => + { + + return CompletedTask.Instance; + }; + + var client = testEnvironment.CreateClient(); + client.ExtendedAuthenticationExchangeAsync += async args => + { + await args.SendAuthenticationDataAsync(MqttAuthenticateReasonCode.ContinueAuthentication, Encoding.ASCII.GetBytes("TOKEN")); + + var serverResponse = await args.ReceiveAuthenticationDataAsync(CancellationToken.None); + Assert.IsNotNull(serverResponse.UserProperties); + Assert.AreEqual("x", serverResponse.UserProperties[0].Name); + Assert.AreEqual("y", serverResponse.UserProperties[0].Value); + }; + + var clientOptions = testEnvironment.CreateDefaultClientOptionsBuilder().WithTimeout(TimeSpan.FromSeconds(10)).WithAuthentication("CUSTOM").Build(); + await client.ConnectAsync(clientOptions); + + await LongTestDelay(); + + await client.ReAuthenticateAsync(); + + await LongTestDelay(); + + var pingResult = await client.TryPingAsync(); + + Assert.IsTrue(pingResult); + } + } + + [TestMethod] + public async Task Use_Extended_Authentication() + { + var initialContextToken = Encoding.ASCII.GetBytes("initial context token"); + var replyContextToken = Encoding.ASCII.GetBytes("reply context token"); + var outcomeOfAuthentication = Encoding.ASCII.GetBytes("outcome of authentication"); + + using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) + { + var server = await testEnvironment.StartServer(); + + /* + Sample flow from RFC: + + 1. Client to Server CONNECT Authentication Method="GS2-KRB5" + 2. Server to Client AUTH rc=0x18 Authentication Method="GS2-KRB5" + 3. Client to Server AUTH rc=0x18 Authentication Method="GS2-KRB5" Authentication Data=initial context token + 4. Server to Client AUTH rc=0x18 Authentication Method="GS2-KRB5" Authentication Data=reply context token + 5. Client to Server AUTH rc=0x18 Authentication Method="GS2-KRB5" + 6. Server to Client CONNACK rc=0 Authentication Method="GS2-KRB5" Authentication Data=outcome of authentication + */ + + server.ValidatingConnectionAsync += async args => + { + using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10))) + { + // 1. + if (args.AuthenticationMethod != "GS2-KRB5") + { + args.ReasonCode = MqttConnectReasonCode.BadAuthenticationMethod; + return; + } + + // 2. + await args.SendAuthenticationDataAsync(MqttAuthenticateReasonCode.ContinueAuthentication, cancellationToken: timeout.Token); + + // 3. + var clientResponse = await args.ReceiveAuthenticationDataAsync(timeout.Token); + CollectionAssert.AreEqual(clientResponse.AuthenticationData, initialContextToken); + Assert.AreEqual(MqttAuthenticateReasonCode.ContinueAuthentication, clientResponse.ReasonCode); + + // 4. + await args.SendAuthenticationDataAsync(MqttAuthenticateReasonCode.ContinueAuthentication, replyContextToken, cancellationToken: timeout.Token); + + // 5. + clientResponse = await args.ReceiveAuthenticationDataAsync(timeout.Token); + Assert.AreEqual(clientResponse.AuthenticationData, null); + Assert.AreEqual(MqttAuthenticateReasonCode.ContinueAuthentication, clientResponse.ReasonCode); + + // 6. + args.ResponseAuthenticationData = outcomeOfAuthentication; + args.ReasonCode = MqttConnectReasonCode.Success; + } + }; + + var client = testEnvironment.CreateClient(); + client.ExtendedAuthenticationExchangeAsync += async args => + { + if (args.AuthenticationMethod != "GS2-KRB5") + { + Assert.Fail("Authentication method is wrong."); + } + + // 2. THE FACT THAT THE SERVER SENDS THE AUTH PACKET WILL TRIGGER THIS EVENT SO THE INITIAL DATA IS IN THE EVENT ARGS! + Assert.AreEqual(MqttAuthenticateReasonCode.ContinueAuthentication, args.ReasonCode); + + // 3. + await args.SendAuthenticationDataAsync(MqttAuthenticateReasonCode.ContinueAuthentication, initialContextToken); + + // 4. + var serverResponse = await args.ReceiveAuthenticationDataAsync(CancellationToken.None); + CollectionAssert.AreEqual(serverResponse.AuthenticationData, replyContextToken); + Assert.AreEqual(MqttAuthenticateReasonCode.ContinueAuthentication, serverResponse.ReasonCode); + + // 5. + await args.SendAuthenticationDataAsync(MqttAuthenticateReasonCode.ContinueAuthentication); + }; + + var clientOptions = testEnvironment.CreateDefaultClientOptionsBuilder().WithTimeout(TimeSpan.FromSeconds(10)).WithAuthentication("GS2-KRB5").Build(); + await client.ConnectAsync(clientOptions); + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Mockups/TestEnvironment.cs b/Source/MQTTnet.Tests/Mockups/TestEnvironment.cs index b3543706..980a929c 100644 --- a/Source/MQTTnet.Tests/Mockups/TestEnvironment.cs +++ b/Source/MQTTnet.Tests/Mockups/TestEnvironment.cs @@ -277,8 +277,7 @@ namespace MQTTnet.Tests.Mockups // Null is used when the client id is assigned from the server! if (!string.IsNullOrEmpty(e.ClientId) && !e.ClientId.StartsWith(TestContext.TestName)) { - TrackException(new InvalidOperationException($"Invalid client ID used ({e.ClientId}). It must start with UnitTest name.")); - e.ReasonCode = MqttConnectReasonCode.ClientIdentifierNotValid; + Assert.Fail($"Client ID does not start with test name ({TestContext.TestName})."); } } @@ -292,23 +291,12 @@ namespace MQTTnet.Tests.Mockups { foreach (var mqttClient in _clients) { - try - { - //mqttClient.DisconnectAsync().GetAwaiter().GetResult(); - } - catch - { - // This can happen when the test already disconnected the client. - } - finally - { - mqttClient?.Dispose(); - } + mqttClient?.Dispose(); } foreach (var lowLevelMqttClient in _lowLevelClients) { - lowLevelMqttClient.Dispose(); + lowLevelMqttClient?.Dispose(); } try diff --git a/Source/MQTTnet/Client/Authentication/MqttClientPartialAuthenticationResponse.cs b/Source/MQTTnet/Client/Authentication/MqttClientPartialAuthenticationResponse.cs new file mode 100644 index 00000000..409ce922 --- /dev/null +++ b/Source/MQTTnet/Client/Authentication/MqttClientPartialAuthenticationResponse.cs @@ -0,0 +1,19 @@ +// 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.Collections.Generic; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Client +{ + public sealed class MqttClientPartialAuthenticationResponse + { + public byte[] AuthenticationData { get; set; } + + public MqttAuthenticateReasonCode ReasonCode { get; set; } + + public List UserProperties { get; set; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Authentication/MqttExtendedAuthenticationExchangeEventArgs.cs b/Source/MQTTnet/Client/Authentication/MqttExtendedAuthenticationExchangeEventArgs.cs new file mode 100644 index 00000000..dffd22e0 --- /dev/null +++ b/Source/MQTTnet/Client/Authentication/MqttExtendedAuthenticationExchangeEventArgs.cs @@ -0,0 +1,111 @@ +// 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.Client.Internal; +using MQTTnet.Exceptions; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Client +{ + public sealed class MqttExtendedAuthenticationExchangeEventArgs : EventArgs + { + static readonly IReadOnlyCollection EmptyUserProperties = new List(); + + readonly IMqttAuthenticationTransportStrategy _transportStrategy; + + public MqttExtendedAuthenticationExchangeEventArgs(MqttAuthPacket authPacket, IMqttAuthenticationTransportStrategy transportStrategy) + { + if (authPacket == null) + { + throw new ArgumentNullException(nameof(authPacket)); + } + + _transportStrategy = transportStrategy ?? throw new ArgumentNullException(nameof(transportStrategy)); + + ReasonCode = authPacket.ReasonCode; + ReasonString = authPacket.ReasonString; + AuthenticationMethod = authPacket.AuthenticationMethod; + AuthenticationData = authPacket.AuthenticationData; + UserProperties = authPacket.UserProperties ?? EmptyUserProperties; + } + + /// + /// Gets the authentication data. + /// MQTT 5.0.0+ feature. + /// + public byte[] AuthenticationData { get; } + + /// + /// Gets the authentication method. + /// MQTT 5.0.0+ feature. + /// + public string AuthenticationMethod { get; } + + /// + /// Gets the reason code. + /// MQTT 5.0.0+ feature. + /// + public MqttAuthenticateReasonCode ReasonCode { get; } + + /// + /// Gets the reason string. + /// MQTT 5.0.0+ feature. + /// + public string ReasonString { get; } + + // /// + // /// Gets the response which will be sent to the server. + // /// + // public MqttExtendedAuthenticationExchangeResponse Response { get; } = new MqttExtendedAuthenticationExchangeResponse(); + + /// + /// Gets or sets the user properties. + /// MQTT 5.0.0+ feature. + /// + public IReadOnlyCollection UserProperties { get; } + + public async Task ReceiveAuthenticationDataAsync(CancellationToken cancellationToken) + { + var receivePacket = await _transportStrategy.ReceivePacketAsync(AuthenticationMethod, cancellationToken).ConfigureAwait(false); + if (receivePacket == null) + { + throw new MqttCommunicationException("The client closed the connection."); + } + + return new MqttClientPartialAuthenticationResponse + { + ReasonCode = receivePacket.ReasonCode, + AuthenticationData = receivePacket.AuthenticationData, + UserProperties = receivePacket.UserProperties + }; + } + + public Task SendAuthenticationDataAsync( + MqttAuthenticateReasonCode reasonCode, + byte[] authenticationData = null, + string reasonString = null, + List userProperties = null, + CancellationToken cancellationToken = default) + { + // The authentication method will never change so we must use the already known one [MQTT-4.12.0-5]. + //var authPacket = MqttPacketFactories.Auth.Create(AuthenticationMethod, reasonCode); + + var authPacket = new MqttAuthPacket + { + AuthenticationMethod = AuthenticationMethod, + AuthenticationData = authenticationData, + UserProperties = userProperties, + ReasonCode = reasonCode, + ReasonString = reasonString + }; + + return _transportStrategy.SendPacketAsync(authPacket, cancellationToken); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeResponse.cs b/Source/MQTTnet/Client/Authentication/MqttReAuthenticationOptions.cs similarity index 62% rename from Source/MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeResponse.cs rename to Source/MQTTnet/Client/Authentication/MqttReAuthenticationOptions.cs index ee71347a..c9c114af 100644 --- a/Source/MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeResponse.cs +++ b/Source/MQTTnet/Client/Authentication/MqttReAuthenticationOptions.cs @@ -7,18 +7,22 @@ using MQTTnet.Packets; namespace MQTTnet.Client { - public sealed class MqttExtendedAuthenticationExchangeResponse + public sealed class MqttReAuthenticationOptions { /// /// Gets or sets the authentication data. + /// Authentication data is binary information used to transmit multiple iterations of cryptographic secrets of protocol + /// steps. + /// The content of the authentication data is highly dependent on the specific implementation of the authentication + /// method. /// MQTT 5.0.0+ feature. /// public byte[] AuthenticationData { get; set; } /// - /// Gets or sets the user properties which will be sent to the server. + /// Gets or sets the user properties. /// MQTT 5.0.0+ feature. /// - public List UserProperties { get; set; } = new List(); + public List UserProperties { get; set; } } } \ No newline at end of file diff --git a/Source/MQTTnet/Client/Connecting/MqttClientConnectResult.cs b/Source/MQTTnet/Client/Connecting/MqttClientConnectResult.cs index 831d14cb..e9b2a46c 100644 --- a/Source/MQTTnet/Client/Connecting/MqttClientConnectResult.cs +++ b/Source/MQTTnet/Client/Connecting/MqttClientConnectResult.cs @@ -11,110 +11,112 @@ namespace MQTTnet.Client public sealed class MqttClientConnectResult { /// - /// Gets the result code. - /// MQTTv5 only. - /// - public MqttClientConnectResultCode ResultCode { get; internal set; } - - /// - /// Gets a value indicating whether a session was already available or not. - /// MQTTv5 only. - /// - public bool IsSessionPresent { get; internal set; } - - /// - /// Gets a value indicating whether wildcards can be used in subscriptions at the current server. - /// MQTTv5 only. - /// - public bool WildcardSubscriptionAvailable { get; internal set; } - - /// - /// Gets whether the server supports retained messages. - /// MQTTv5 only. - /// - public bool RetainAvailable { get; internal set; } - - /// - /// Gets the client identifier which was chosen by the server. - /// MQTTv5 only. + /// Gets the client identifier which was chosen by the server. + /// MQTT 5.0.0+ feature. /// public string AssignedClientIdentifier { get; internal set; } /// - /// Gets the authentication method. - /// MQTTv5 only. + /// Gets the authentication data. + /// MQTT 5.0.0+ feature. + /// + public byte[] AuthenticationData { get; internal set; } + + /// + /// Gets the authentication method. + /// MQTT 5.0.0+ feature. /// public string AuthenticationMethod { get; internal set; } /// - /// Gets the authentication data. - /// MQTTv5 only. + /// Gets a value indicating whether a session was already available or not. + /// MQTT 5.0.0+ feature. /// - public byte[] AuthenticationData { get; internal set; } + public bool IsSessionPresent { get; internal set; } public uint? MaximumPacketSize { get; internal set; } /// - /// Gets the reason string. - /// MQTTv5 only. - /// - public string ReasonString { get; internal set; } - - public ushort? ReceiveMaximum { get; internal set; } - - /// - /// Gets the maximum QoS which is supported by the server. - /// MQTTv5 only. + /// Gets the maximum QoS which is supported by the server. + /// MQTT 5.0.0+ feature. /// public MqttQualityOfServiceLevel MaximumQoS { get; internal set; } /// - /// Gets the response information. - /// MQTTv5 only. + /// Gets the reason string. + /// MQTT 5.0.0+ feature. + /// + public string ReasonString { get; internal set; } + + public ushort? ReceiveMaximum { get; internal set; } + + /// + /// Gets the response information. + /// MQTT 5.0.0+ feature. /// public string ResponseInformation { get; internal set; } /// - /// Gets the maximum value for a topic alias. 0 means not supported. - /// MQTTv5 only. + /// Gets the result code. + /// MQTT 5.0.0+ feature. /// - public ushort TopicAliasMaximum { get; internal set; } + public MqttClientConnectResultCode ResultCode { get; internal set; } /// - /// Gets an alternate server which should be used instead of the current one. - /// MQTTv5 only. + /// Gets whether the server supports retained messages. + /// MQTT 5.0.0+ feature. /// - public string ServerReference { get; internal set; } + public bool RetainAvailable { get; internal set; } /// - /// MQTTv5 only. - /// Gets the keep alive interval which was chosen by the server instead of the - /// keep alive interval from the client CONNECT packet. - /// A value of 0 indicates that the feature is not used. + /// MQTTv5 only. + /// Gets the keep alive interval which was chosen by the server instead of the + /// keep alive interval from the client CONNECT packet. + /// A value of 0 indicates that the feature is not used. /// public ushort ServerKeepAlive { get; internal set; } + /// + /// Gets an alternate server which should be used instead of the current one. + /// MQTT 5.0.0+ feature. + /// + public string ServerReference { get; internal set; } + public uint? SessionExpiryInterval { get; internal set; } /// - /// Gets a value indicating whether the subscription identifiers are available or not. - /// MQTTv5 only. - /// - public bool SubscriptionIdentifiersAvailable { get; internal set; } - - /// - /// Gets a value indicating whether the shared subscriptions are available or not. - /// MQTTv5 only. + /// Gets a value indicating whether the shared subscriptions are available or not. + /// MQTT 5.0.0+ feature. /// public bool SharedSubscriptionAvailable { get; internal set; } /// - /// 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. - /// MQTTv5 only. + /// Gets a value indicating whether the subscription identifiers are available or not. + /// MQTT 5.0.0+ feature. + /// + public bool SubscriptionIdentifiersAvailable { get; internal set; } + + /// + /// Gets the maximum value for a topic alias. 0 means not supported. + /// MQTT 5.0.0+ feature. + /// + public ushort TopicAliasMaximum { get; internal set; } + + /// + /// 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. + /// MQTT 5.0.0+ feature. /// public List UserProperties { get; internal set; } + + /// + /// Gets a value indicating whether wildcards can be used in subscriptions at the current server. + /// MQTT 5.0.0+ feature. + /// + public bool WildcardSubscriptionAvailable { get; internal set; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/ExtendedAuthenticationExchange/IMqttExtendedAuthenticationExchangeHandler.cs b/Source/MQTTnet/Client/ExtendedAuthenticationExchange/IMqttExtendedAuthenticationExchangeHandler.cs deleted file mode 100644 index 3ff23aef..00000000 --- a/Source/MQTTnet/Client/ExtendedAuthenticationExchange/IMqttExtendedAuthenticationExchangeHandler.cs +++ /dev/null @@ -1,13 +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.Threading.Tasks; - -namespace MQTTnet.Client -{ - public interface IMqttExtendedAuthenticationExchangeHandler - { - Task HandleRequestAsync(MqttExtendedAuthenticationExchangeEventArgs eventArgs); - } -} diff --git a/Source/MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeData.cs b/Source/MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeData.cs deleted file mode 100644 index db7ef33e..00000000 --- a/Source/MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeData.cs +++ /dev/null @@ -1,42 +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.Collections.Generic; -using MQTTnet.Packets; -using MQTTnet.Protocol; - -namespace MQTTnet.Client -{ - public class MqttExtendedAuthenticationExchangeData - { - /// - /// Gets or sets the reason code. - /// Hint: MQTT 5 feature only. - /// - public MqttAuthenticateReasonCode ReasonCode { get; set; } - - /// - /// Gets or sets the reason string. - /// Hint: MQTT 5 feature only. - /// - public string ReasonString { get; set; } - - /// - /// Gets or sets the authentication data. - /// Authentication data is binary information used to transmit multiple iterations of cryptographic secrets of protocol steps. - /// The content of the authentication data is highly dependent on the specific implementation of the authentication method. - /// Hint: MQTT 5 feature only. - /// - public byte[] AuthenticationData { get; set; } - - /// - /// 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. - /// Hint: MQTT 5 feature only. - /// - public List UserProperties { get; } - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeEventArgs.cs b/Source/MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeEventArgs.cs deleted file mode 100644 index df8a9122..00000000 --- a/Source/MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeEventArgs.cs +++ /dev/null @@ -1,65 +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.Client -{ - public sealed class MqttExtendedAuthenticationExchangeEventArgs : EventArgs - { - static readonly IReadOnlyCollection EmptyUserProperties = new List(); - - public MqttExtendedAuthenticationExchangeEventArgs(MqttAuthPacket authPacket) - { - if (authPacket == null) - { - throw new ArgumentNullException(nameof(authPacket)); - } - - ReasonCode = authPacket.ReasonCode; - ReasonString = authPacket.ReasonString; - AuthenticationMethod = authPacket.AuthenticationMethod; - AuthenticationData = authPacket.AuthenticationData; - UserProperties = authPacket.UserProperties ?? EmptyUserProperties; - } - - /// - /// Gets the authentication data. - /// MQTT 5.0.0+ feature. - /// - public byte[] AuthenticationData { get; } - - /// - /// Gets the authentication method. - /// MQTT 5.0.0+ feature. - /// - public string AuthenticationMethod { get; } - - /// - /// Gets the reason code. - /// MQTT 5.0.0+ feature. - /// - public MqttAuthenticateReasonCode ReasonCode { get; } - - /// - /// Gets the reason string. - /// MQTT 5.0.0+ feature. - /// - public string ReasonString { get; } - - /// - /// Gets or sets the user properties. - /// MQTT 5.0.0+ feature. - /// - public IReadOnlyCollection UserProperties { get; } - - /// - /// Gets the response which will be sent to the server. - /// - public MqttExtendedAuthenticationExchangeResponse Response { get; } = new MqttExtendedAuthenticationExchangeResponse(); - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Client/IMqttClient.cs b/Source/MQTTnet/Client/IMqttClient.cs index 37ecbe12..df7f8401 100644 --- a/Source/MQTTnet/Client/IMqttClient.cs +++ b/Source/MQTTnet/Client/IMqttClient.cs @@ -15,6 +15,8 @@ namespace MQTTnet.Client event Func DisconnectedAsync; + event Func ExtendedAuthenticationExchangeAsync; + event Func InspectPacketAsync; bool IsConnected { get; } @@ -27,10 +29,10 @@ namespace MQTTnet.Client Task PingAsync(CancellationToken cancellationToken = default); - Task SendExtendedAuthenticationExchangeDataAsync(MqttExtendedAuthenticationExchangeData data, CancellationToken cancellationToken = default); - Task PublishAsync(MqttApplicationMessage applicationMessage, CancellationToken cancellationToken = default); + Task ReAuthenticateAsync(MqttReAuthenticationOptions options, CancellationToken cancellationToken = default); + Task SubscribeAsync(MqttClientSubscribeOptions options, CancellationToken cancellationToken = default); Task UnsubscribeAsync(MqttClientUnsubscribeOptions options, CancellationToken cancellationToken = default); diff --git a/Source/MQTTnet/Client/Internal/MqttClientAuthenticationHandler.cs b/Source/MQTTnet/Client/Internal/MqttClientAuthenticationHandler.cs index bf57f235..dcaaf3ba 100644 --- a/Source/MQTTnet/Client/Internal/MqttClientAuthenticationHandler.cs +++ b/Source/MQTTnet/Client/Internal/MqttClientAuthenticationHandler.cs @@ -15,9 +15,65 @@ using MQTTnet.Protocol; namespace MQTTnet.Client.Internal { + public sealed class MqttInitialAuthenticationStrategy : IMqttAuthenticationTransportStrategy + { + readonly IMqttChannelAdapter _channelAdapter; + + public MqttInitialAuthenticationStrategy(IMqttChannelAdapter channelAdapter) + { + _channelAdapter = channelAdapter ?? throw new ArgumentNullException(nameof(channelAdapter)); + } + + + public Task SendPacketAsync(MqttAuthPacket authPacket, CancellationToken cancellationToken) + { + if (authPacket == null) + { + throw new ArgumentNullException(nameof(authPacket)); + } + + return _channelAdapter.SendPacketAsync(authPacket, cancellationToken); + } + + public async Task ReceivePacketAsync(string authenticationMethod, CancellationToken cancellationToken) + { + if (authenticationMethod == null) + { + throw new ArgumentNullException(nameof(authenticationMethod)); + } + + var receivePacket = await _channelAdapter.ReceivePacketAsync(cancellationToken).ConfigureAwait(false); + if (receivePacket is MqttAuthPacket authPacket) + { + if (!string.Equals(authPacket.AuthenticationMethod, authenticationMethod, StringComparison.Ordinal)) + { + throw new MqttProtocolViolationException("The authentication method is not allowed to change while authenticating."); + } + + return authPacket; + } + + if (receivePacket == null) + { + throw new MqttCommunicationException("The server closed the connection."); + } + + throw new MqttProtocolViolationException("Expected an AUTH packet from the client."); + } + } + + public interface IMqttAuthenticationTransportStrategy + { + Task SendPacketAsync(MqttAuthPacket authPacket, CancellationToken cancellationToken); + + Task ReceivePacketAsync(string authenticationMethod, CancellationToken cancellationToken); + } + public sealed class MqttClientAuthenticationHandler { readonly MqttClient _client; + + readonly AsyncEvent _extendedAuthenticationExchangeEvent = new AsyncEvent(); readonly MqttNetSourceLogger _logger; public MqttClientAuthenticationHandler(MqttClient client, IMqttNetLogger logger) @@ -31,7 +87,10 @@ namespace MQTTnet.Client.Internal _logger = logger.WithSource(nameof(MqttClientAuthenticationHandler)); } - public AsyncEvent ExtendedAuthenticationExchangeEvent { get; } = new AsyncEvent(); + public void AddHandler(Func handler) + { + _extendedAuthenticationExchangeEvent.AddHandler(handler); + } public async Task Authenticate(IMqttChannelAdapter channelAdapter, MqttClientOptions options, CancellationToken cancellationToken) { @@ -64,25 +123,43 @@ namespace MQTTnet.Client.Internal { var receivedPacket = await channelAdapter.ReceivePacketAsync(cancellationToken).ConfigureAwait(false); - if (receivedPacket is MqttAuthPacket authPacket) + if (receivedPacket is MqttAuthPacket initialAuthPacket) { // MQTT v3.1.1 cannot send an AUTH packet. // If the Server requires additional information to complete the authentication, it can send an AUTH packet to the Client. // This packet MUST contain a Reason Code of 0x18 (Continue authentication) - if (authPacket.ReasonCode != MqttAuthenticateReasonCode.ContinueAuthentication) + if (initialAuthPacket.ReasonCode != MqttAuthenticateReasonCode.ContinueAuthentication) { throw new MqttProtocolViolationException("Wrong reason code received [MQTT-4.12.0-2]."); } - var response = await OnExtendedAuthentication(authPacket).ConfigureAwait(false); - authPacket = MqttPacketFactories.Auth.Create(authPacket, response); - await _client.Send(authPacket, cancellationToken).ConfigureAwait(false); + //var response = await HandleExtendedAuthentication(authPacket, channelAdapter).ConfigureAwait(false); + await HandleExtendedAuthentication(initialAuthPacket, new MqttInitialAuthenticationStrategy(channelAdapter)).ConfigureAwait(false); } else if (receivedPacket is MqttConnAckPacket connAckPacketBuffer) { connAckPacket = connAckPacketBuffer; + + // The CONNACK packet is the last packet when authenticating so there is no further AUTH packet allowed! break; } + else + { + throw new MqttProtocolViolationException($"Received {receivedPacket.GetRfcName()} while authenticating."); + } + } + + cancellationToken.ThrowIfCancellationRequested(); + + // 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]. + if (!string.IsNullOrEmpty(connectPacket.AuthenticationMethod)) + { + if (!string.Equals(connectPacket.AuthenticationMethod, connAckPacket?.AuthenticationMethod)) + { + throw new MqttProtocolViolationException("The CONNACK packet does not have the same authentication method as the CONNECT packet."); + } } result = MqttClientResultFactory.ConnectResult.Create(connAckPacket, channelAdapter.PacketFormatterAdapter.ProtocolVersion); @@ -97,8 +174,9 @@ namespace MQTTnet.Client.Internal // did send a proper ACK packet. if (options.ThrowOnNonSuccessfulResponseFromServer) { - _logger.Warning("Client will now throw an _MqttConnectingFailedException_. This is obsolete and will be removed in the future. Consider setting _ThrowOnNonSuccessfulResponseFromServer=False_ in client options."); - + _logger.Warning( + "Client will now throw an _MqttConnectingFailedException_. This is obsolete and will be removed in the future. Consider setting _ThrowOnNonSuccessfulResponseFromServer=False_ in client options."); + if (result.ResultCode != MqttClientConnectResultCode.Success) { throw new MqttConnectingFailedException($"Connecting with MQTT server failed ({result.ResultCode}).", null, result); @@ -110,16 +188,38 @@ namespace MQTTnet.Client.Internal return result; } - async Task OnExtendedAuthentication(MqttAuthPacket authPacket) + public Task HandleExtendedAuthentication(MqttAuthPacket initialAuthPacket, IMqttAuthenticationTransportStrategy transportStrategy) { - if (!ExtendedAuthenticationExchangeEvent.HasHandlers) + ValidateEventHandler(); + + var eventArgs = new MqttExtendedAuthenticationExchangeEventArgs(initialAuthPacket, transportStrategy); + return _extendedAuthenticationExchangeEvent.InvokeAsync(eventArgs); + } + + void ValidateEventHandler() + { + if (!_extendedAuthenticationExchangeEvent.HasHandlers) { throw new InvalidOperationException("Cannot handle extended authentication without attached event handler."); } + } + + public void RemoveHandler(Func handler) + { + _extendedAuthenticationExchangeEvent.RemoveHandler(handler); + } - var eventArgs = new MqttExtendedAuthenticationExchangeEventArgs(authPacket); - await ExtendedAuthenticationExchangeEvent.InvokeAsync(eventArgs).ConfigureAwait(false); - return eventArgs.Response; + public async Task ReAuthenticate(MqttReAuthenticationOptions options, CancellationToken cancellationToken) + { + ValidateEventHandler(); + + var authPacket = MqttPacketFactories.Auth.CreateReAuthenticationPacket(_client.Options, options); + + + + // Sending the AUTH packet will trigger the re authentication and the event handler will + // handle the details. + await _client.Send(authPacket, cancellationToken).ConfigureAwait(false); } } } \ No newline at end of file diff --git a/Source/MQTTnet/Client/MqttClient.cs b/Source/MQTTnet/Client/MqttClient.cs index edfb6260..b260d1f2 100644 --- a/Source/MQTTnet/Client/MqttClient.cs +++ b/Source/MQTTnet/Client/MqttClient.cs @@ -29,7 +29,7 @@ namespace MQTTnet.Client readonly MqttPacketIdentifierProvider _packetIdentifierProvider = new MqttPacketIdentifierProvider(); readonly IMqttNetLogger _rootLogger; - IMqttChannelAdapter _adapter; + IMqttChannelAdapter _channelAdapter; internal bool _cleanDisconnectInitiated; @@ -82,8 +82,8 @@ namespace MQTTnet.Client public event Func ExtendedAuthenticationExchangeAsync { - add => _authenticationHandler.ExtendedAuthenticationExchangeEvent.AddHandler(value); - remove => _authenticationHandler.ExtendedAuthenticationExchangeEvent.RemoveHandler(value); + add => _authenticationHandler.AddHandler(value); + remove => _authenticationHandler.RemoveHandler(value); } public event Func InspectPacketAsync @@ -125,7 +125,7 @@ namespace MQTTnet.Client _clientAlive = new CancellationTokenSource(); var adapter = _adapterFactory.CreateClientAdapter(options, new MqttPacketInspector(_events.InspectPacketEvent, _rootLogger), _rootLogger); - _adapter = adapter; + _channelAdapter = adapter; if (cancellationToken.CanBeCanceled) { @@ -194,7 +194,7 @@ namespace MQTTnet.Client if (Options.ValidateFeatures) { - MqttClientDisconnectOptionsValidator.ThrowIfNotSupported(options, _adapter.PacketFormatterAdapter.ProtocolVersion); + MqttClientDisconnectOptionsValidator.ThrowIfNotSupported(options, _channelAdapter.PacketFormatterAdapter.ProtocolVersion); } // Sending the DISCONNECT may fail due to connection issues. The resulting exception @@ -245,7 +245,7 @@ namespace MQTTnet.Client if (Options.ValidateFeatures) { - MqttApplicationMessageValidator.ThrowIfNotSupported(applicationMessage, _adapter.PacketFormatterAdapter.ProtocolVersion); + MqttApplicationMessageValidator.ThrowIfNotSupported(applicationMessage, _channelAdapter.PacketFormatterAdapter.ProtocolVersion); } var publishPacket = MqttPacketFactories.Publish.Create(applicationMessage); @@ -271,26 +271,25 @@ namespace MQTTnet.Client } } - public Task SendExtendedAuthenticationExchangeDataAsync(MqttExtendedAuthenticationExchangeData data, CancellationToken cancellationToken = default) + public Task ReAuthenticateAsync(MqttReAuthenticationOptions options, CancellationToken cancellationToken = default) { - if (data == null) + if (options == null) { - throw new ArgumentNullException(nameof(data)); + throw new ArgumentNullException(nameof(options)); } ThrowIfDisposed(); ThrowIfNotConnected(); - var authPacket = new MqttAuthPacket + if (cancellationToken.CanBeCanceled) { - // This must always be equal to the value from the CONNECT packet. So we use it here to ensure that. - AuthenticationMethod = Options.AuthenticationMethod, - AuthenticationData = data.AuthenticationData, - ReasonString = data.ReasonString, - UserProperties = data.UserProperties - }; + return _authenticationHandler.ReAuthenticate(options, cancellationToken); + } - return Send(authPacket, cancellationToken); + using (var timeout = new CancellationTokenSource(Options.Timeout)) + { + return _authenticationHandler.ReAuthenticate(options, timeout.Token); + } } public async Task SubscribeAsync(MqttClientSubscribeOptions options, CancellationToken cancellationToken = default) @@ -307,7 +306,7 @@ namespace MQTTnet.Client if (Options.ValidateFeatures) { - MqttClientSubscribeOptionsValidator.ThrowIfNotSupported(options, _adapter.PacketFormatterAdapter.ProtocolVersion); + MqttClientSubscribeOptionsValidator.ThrowIfNotSupported(options, _channelAdapter.PacketFormatterAdapter.ProtocolVersion); } ThrowIfDisposed(); @@ -349,7 +348,7 @@ namespace MQTTnet.Client if (Options.ValidateFeatures) { - MqttClientUnsubscribeOptionsValidator.ThrowIfNotSupported(options, _adapter.PacketFormatterAdapter.ProtocolVersion); + MqttClientUnsubscribeOptionsValidator.ThrowIfNotSupported(options, _channelAdapter.PacketFormatterAdapter.ProtocolVersion); } var unsubscribePacket = MqttPacketFactories.Unsubscribe.Create(options); @@ -389,7 +388,7 @@ namespace MQTTnet.Client _keepAliveHandler.TrackSentPacket(); - return _adapter.SendPacketAsync(packet, cancellationToken); + return _channelAdapter.SendPacketAsync(packet, cancellationToken); } protected override void Dispose(bool disposing) @@ -446,8 +445,8 @@ namespace MQTTnet.Client _publishPacketReceiverQueue?.Dispose(); _publishPacketReceiverQueue = null; - _adapter?.Dispose(); - _adapter = null; + _channelAdapter?.Dispose(); + _channelAdapter = null; _packetDispatcher?.Dispose(); _packetDispatcher = null; @@ -467,11 +466,11 @@ namespace MQTTnet.Client { _logger.Verbose("Trying to connect with server '{0}'", Options.ChannelOptions); - await _adapter.ConnectAsync(effectiveCancellationToken.Token).ConfigureAwait(false); + await _channelAdapter.ConnectAsync(effectiveCancellationToken.Token).ConfigureAwait(false); _logger.Verbose("Connection with server established"); - var connectResult = await _authenticationHandler.Authenticate(_adapter, Options, effectiveCancellationToken.Token).ConfigureAwait(false); + var connectResult = await _authenticationHandler.Authenticate(_channelAdapter, Options, effectiveCancellationToken.Token).ConfigureAwait(false); if (connectResult.ResultCode == MqttClientConnectResultCode.Success) { @@ -494,13 +493,13 @@ namespace MQTTnet.Client try { - if (_adapter != null) + if (_channelAdapter != null) { _logger.Verbose("Disconnecting [Timeout={0}]", Options.Timeout); using (var timeout = new CancellationTokenSource(Options.Timeout)) { - await _adapter.DisconnectAsync(timeout.Token).ConfigureAwait(false); + await _channelAdapter.DisconnectAsync(timeout.Token).ConfigureAwait(false); } } @@ -619,17 +618,6 @@ namespace MQTTnet.Client return CompletedTask.Instance; } - Task ProcessReceivedAuthPacket(MqttAuthPacket authPacket) - { - var extendedAuthenticationExchangeHandler = Options.ExtendedAuthenticationExchangeHandler; - if (extendedAuthenticationExchangeHandler != null) - { - return extendedAuthenticationExchangeHandler.HandleRequestAsync(new MqttExtendedAuthenticationExchangeEventArgs(authPacket)); - } - - return CompletedTask.Instance; - } - Task ProcessReceivedDisconnectPacket(MqttDisconnectPacket disconnectPacket) { _disconnectReason = (int)disconnectPacket.ReasonCode; @@ -722,7 +710,7 @@ namespace MQTTnet.Client async Task Receive(CancellationToken cancellationToken) { - var packetTask = _adapter.ReceivePacketAsync(cancellationToken); + var packetTask = _channelAdapter.ReceivePacketAsync(cancellationToken); MqttPacket packet; if (packetTask.IsCompleted) @@ -796,15 +784,36 @@ namespace MQTTnet.Client } } - async Task Request(MqttPacket requestPacket, CancellationToken cancellationToken) where TResponsePacket : MqttPacket + internal async Task Receive(int packetIdentifier, CancellationToken cancellationToken) where TPacket : MqttPacket + { + using (var packetAwaitable = _packetDispatcher.AddAwaitable(packetIdentifier)) + { + return await packetAwaitable.WaitOneAsync(cancellationToken); + } + } + + internal async Task Request(MqttPacket requestPacket, CancellationToken cancellationToken) where TResponsePacket : MqttPacket { cancellationToken.ThrowIfCancellationRequested(); - ushort packetIdentifier = 0; + // Some packets are a direct response to another packet but have no packet identifier for correlation! + int packetIdentifier; if (requestPacket is MqttPacketWithIdentifier packetWithIdentifier) { packetIdentifier = packetWithIdentifier.PacketIdentifier; } + else if (requestPacket is MqttPingReqPacket) + { + packetIdentifier = MqttPacketDispatcher.PingRespPacketFakeIdentifier; + } + else if (requestPacket is MqttAuthPacket) + { + packetIdentifier = MqttPacketDispatcher.AuthPacketFakeIdentifier; + } + else + { + throw new MqttProtocolViolationException($"Expecting a response for packet {requestPacket.GetRfcName()} is not supported."); + } using (var packetAwaitable = _packetDispatcher.AddAwaitable(packetIdentifier)) { @@ -908,23 +917,31 @@ namespace MQTTnet.Client { await ProcessReceivedDisconnectPacket(disconnectPacket).ConfigureAwait(false); } - else if (packet is MqttAuthPacket authPacket) - { - await ProcessReceivedAuthPacket(authPacket).ConfigureAwait(false); - } + // else if (packet is MqttAuthPacket authPacket) + // { + // await _authenticationHandler.HandleExtendedAuthentication(authPacket, new MqttInitialAuthenticationStrategy(_channelAdapter)).ConfigureAwait(false); + // } else if (packet is MqttPingRespPacket) { _packetDispatcher.TryDispatch(packet); } else if (packet is MqttPingReqPacket) { - throw new MqttProtocolViolationException("The PINGREQ Packet is sent from a Client to the Server only."); + throw new MqttProtocolViolationException("The PINGREQ packet can only be sent from client to server."); + } + else if (packet is MqttConnAckPacket) + { + throw new MqttProtocolViolationException("The CONNACK packet can only be send while connecting."); + } + else if (packet is MqttConnectPacket) + { + throw new MqttProtocolViolationException("The CONNECT packet can only be sent from client to server."); } else { if (!_packetDispatcher.TryDispatch(packet)) { - throw new MqttProtocolViolationException($"Received packet '{packet}' at an unexpected time."); + throw new MqttProtocolViolationException($"Received unexpected {packet} packet."); } } } diff --git a/Source/MQTTnet/Client/MqttClientExtensions.cs b/Source/MQTTnet/Client/MqttClientExtensions.cs index 38860001..be0ada98 100644 --- a/Source/MQTTnet/Client/MqttClientExtensions.cs +++ b/Source/MQTTnet/Client/MqttClientExtensions.cs @@ -14,6 +14,8 @@ namespace MQTTnet.Client { public static class MqttClientExtensions { + static readonly MqttReAuthenticationOptions EmptyReAuthenticationOptions = new MqttReAuthenticationOptions(); + public static Task DisconnectAsync( this IMqttClient client, MqttClientDisconnectOptionsReason reason = MqttClientDisconnectOptionsReason.NormalDisconnection, @@ -77,6 +79,11 @@ namespace MQTTnet.Client return mqttClient.PublishBinaryAsync(topic, payloadBuffer, qualityOfServiceLevel, retain, cancellationToken); } + public static Task ReAuthenticateAsync(this IMqttClient client, CancellationToken cancellationToken = default) + { + return client.ReAuthenticateAsync(EmptyReAuthenticationOptions, cancellationToken); + } + public static Task ReconnectAsync(this IMqttClient client, CancellationToken cancellationToken = default) { if (client.Options == null) @@ -87,7 +94,7 @@ namespace MQTTnet.Client return client.ConnectAsync(client.Options, cancellationToken); } - + public static Task SubscribeAsync(this IMqttClient mqttClient, MqttTopicFilter topicFilter, CancellationToken cancellationToken = default) { if (mqttClient == null) diff --git a/Source/MQTTnet/Client/Options/MqttClientOptions.cs b/Source/MQTTnet/Client/Options/MqttClientOptions.cs index e7842bbe..94ce547e 100644 --- a/Source/MQTTnet/Client/Options/MqttClientOptions.cs +++ b/Source/MQTTnet/Client/Options/MqttClientOptions.cs @@ -53,9 +53,7 @@ namespace MQTTnet.Client public string ClientId { get; set; } = Guid.NewGuid().ToString("N"); public IMqttClientCredentialsProvider Credentials { get; set; } - - public IMqttExtendedAuthenticationExchangeHandler ExtendedAuthenticationExchangeHandler { get; set; } - + /// /// 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. diff --git a/Source/MQTTnet/Client/Options/MqttClientOptionsBuilder.cs b/Source/MQTTnet/Client/Options/MqttClientOptionsBuilder.cs index 3f9f3adb..bfeb63fa 100644 --- a/Source/MQTTnet/Client/Options/MqttClientOptionsBuilder.cs +++ b/Source/MQTTnet/Client/Options/MqttClientOptionsBuilder.cs @@ -79,7 +79,7 @@ namespace MQTTnet.Client return _options; } - public MqttClientOptionsBuilder WithAuthentication(string method, byte[] data) + public MqttClientOptionsBuilder WithAuthentication(string method, byte[] data = null) { _options.AuthenticationMethod = method; _options.AuthenticationData = data; @@ -177,12 +177,6 @@ namespace MQTTnet.Client return this; } - public MqttClientOptionsBuilder WithExtendedAuthenticationExchangeHandler(IMqttExtendedAuthenticationExchangeHandler handler) - { - _options.ExtendedAuthenticationExchangeHandler = handler; - return this; - } - public MqttClientOptionsBuilder WithKeepAlivePeriod(TimeSpan value) { _options.KeepAlivePeriod = value; diff --git a/Source/MQTTnet/Formatter/MqttAuthPacketFactory.cs b/Source/MQTTnet/Formatter/MqttAuthPacketFactory.cs index fd745cde..2d4db6c3 100644 --- a/Source/MQTTnet/Formatter/MqttAuthPacketFactory.cs +++ b/Source/MQTTnet/Formatter/MqttAuthPacketFactory.cs @@ -11,25 +11,26 @@ namespace MQTTnet.Formatter { public sealed class MqttAuthPacketFactory { - public MqttAuthPacket Create(MqttAuthPacket serverAuthPacket, MqttExtendedAuthenticationExchangeResponse response) + public MqttAuthPacket CreateReAuthenticationPacket(MqttClientOptions clientOptions, MqttReAuthenticationOptions reAuthenticationOptions) { - if (serverAuthPacket == null) + if (clientOptions == null) { - throw new ArgumentNullException(nameof(serverAuthPacket)); + throw new ArgumentNullException(nameof(clientOptions)); } - if (response == null) + if (reAuthenticationOptions == null) { - throw new ArgumentNullException(nameof(response)); + throw new ArgumentNullException(nameof(reAuthenticationOptions)); } return new MqttAuthPacket { - ReasonCode = MqttAuthenticateReasonCode.ContinueAuthentication, - ReasonString = null, - AuthenticationMethod = serverAuthPacket.AuthenticationMethod, - AuthenticationData = response.AuthenticationData, - UserProperties = response.UserProperties + // The authentication method cannot change and must be always the same as long as the client is connected. + AuthenticationMethod = clientOptions.AuthenticationMethod, + ReasonCode = MqttAuthenticateReasonCode.ReAuthenticate, + AuthenticationData = reAuthenticationOptions.AuthenticationData, + UserProperties = reAuthenticationOptions.UserProperties, + ReasonString = null }; } } diff --git a/Source/MQTTnet/Formatter/MqttConnectReasonCodeConverter.cs b/Source/MQTTnet/Formatter/MqttConnectReasonCodeConverter.cs index 6278a605..45270652 100644 --- a/Source/MQTTnet/Formatter/MqttConnectReasonCodeConverter.cs +++ b/Source/MQTTnet/Formatter/MqttConnectReasonCodeConverter.cs @@ -59,6 +59,8 @@ namespace MQTTnet.Formatter return MqttConnectReturnCode.ConnectionAccepted; } + case MqttConnectReasonCode.BadAuthenticationMethod: + case MqttConnectReasonCode.Banned: case MqttConnectReasonCode.NotAuthorized: { return MqttConnectReturnCode.ConnectionRefusedNotAuthorized; @@ -73,12 +75,13 @@ namespace MQTTnet.Formatter { return MqttConnectReturnCode.ConnectionRefusedIdentifierRejected; } - + case MqttConnectReasonCode.UnsupportedProtocolVersion: { return MqttConnectReturnCode.ConnectionRefusedUnacceptableProtocolVersion; } - + + case MqttConnectReasonCode.UseAnotherServer: case MqttConnectReasonCode.ServerUnavailable: case MqttConnectReasonCode.ServerBusy: case MqttConnectReasonCode.ServerMoved: @@ -88,7 +91,8 @@ namespace MQTTnet.Formatter default: { - throw new MqttProtocolViolationException("Unable to convert connect reason code (MQTTv5) to return code (MQTTv3)."); + // This is the most best matching value. + return MqttConnectReturnCode.ConnectionRefusedUnacceptableProtocolVersion; } } } diff --git a/Source/MQTTnet/MQTTnet.csproj.DotSettings b/Source/MQTTnet/MQTTnet.csproj.DotSettings index a756552c..bf3dbff1 100644 --- a/Source/MQTTnet/MQTTnet.csproj.DotSettings +++ b/Source/MQTTnet/MQTTnet.csproj.DotSettings @@ -1,4 +1,5 @@  + True True True True diff --git a/Source/MQTTnet/PacketDispatcher/IMqttPacketAwaitable.cs b/Source/MQTTnet/PacketDispatcher/IMqttPacketAwaitable.cs index a467edb8..6a3277e9 100644 --- a/Source/MQTTnet/PacketDispatcher/IMqttPacketAwaitable.cs +++ b/Source/MQTTnet/PacketDispatcher/IMqttPacketAwaitable.cs @@ -9,7 +9,7 @@ namespace MQTTnet.PacketDispatcher { public interface IMqttPacketAwaitable : IDisposable { - MqttPacketAwaitableFilter Filter { get; } + int PacketIdentifier { get; } void Complete(MqttPacket packet); diff --git a/Source/MQTTnet/PacketDispatcher/MqttPacketAwaitable.cs b/Source/MQTTnet/PacketDispatcher/MqttPacketAwaitable.cs index 895e2b53..c8aa4c2b 100644 --- a/Source/MQTTnet/PacketDispatcher/MqttPacketAwaitable.cs +++ b/Source/MQTTnet/PacketDispatcher/MqttPacketAwaitable.cs @@ -16,18 +16,13 @@ namespace MQTTnet.PacketDispatcher readonly AsyncTaskCompletionSource _promise = new AsyncTaskCompletionSource(); readonly MqttPacketDispatcher _owningPacketDispatcher; - public MqttPacketAwaitable(ushort packetIdentifier, MqttPacketDispatcher owningPacketDispatcher) + public MqttPacketAwaitable(int packetPacketIdentifier, MqttPacketDispatcher owningPacketDispatcher) { - Filter = new MqttPacketAwaitableFilter - { - Type = typeof(TPacket), - Identifier = packetIdentifier - }; - + PacketIdentifier = packetPacketIdentifier; _owningPacketDispatcher = owningPacketDispatcher ?? throw new ArgumentNullException(nameof(owningPacketDispatcher)); } - public MqttPacketAwaitableFilter Filter { get; } + public int PacketIdentifier { get; } public async Task WaitOneAsync(CancellationToken cancellationToken) { diff --git a/Source/MQTTnet/PacketDispatcher/MqttPacketDispatcher.cs b/Source/MQTTnet/PacketDispatcher/MqttPacketDispatcher.cs index 85413a2c..f8b9f55f 100644 --- a/Source/MQTTnet/PacketDispatcher/MqttPacketDispatcher.cs +++ b/Source/MQTTnet/PacketDispatcher/MqttPacketDispatcher.cs @@ -10,11 +10,15 @@ namespace MQTTnet.PacketDispatcher { public sealed class MqttPacketDispatcher : IDisposable { + public const int PingRespPacketFakeIdentifier = 2147483647; + public const int AuthPacketFakeIdentifier = 2147483646; + public const int SuccessAuthPacketFakeIdentifier = 2147483647; + readonly List _waiters = new List(); bool _isDisposed; - public MqttPacketAwaitable AddAwaitable(ushort packetIdentifier) where TResponsePacket : MqttPacket + public MqttPacketAwaitable AddAwaitable(int packetIdentifier) where TResponsePacket : MqttPacket { var awaitable = new MqttPacketAwaitable(packetIdentifier, this); @@ -99,15 +103,21 @@ namespace MQTTnet.PacketDispatcher throw new ArgumentNullException(nameof(packet)); } - ushort identifier = 0; + var identifier = 0; if (packet is MqttPacketWithIdentifier packetWithIdentifier) { identifier = packetWithIdentifier.PacketIdentifier; } - - var packetType = packet.GetType(); + else if (packet is MqttPingRespPacket) + { + identifier = PingRespPacketFakeIdentifier; + } + else + { + throw new InvalidOperationException($"Cannot dispatch {packet.GetRfcName()} packet."); + } + var waiters = new List(); - lock (_waiters) { ThrowIfDisposed(); @@ -115,11 +125,8 @@ namespace MQTTnet.PacketDispatcher for (var i = _waiters.Count - 1; i >= 0; i--) { var entry = _waiters[i]; - - // Note: The PingRespPacket will also arrive here and has NO identifier but there - // is code which waits for it. So the code must be able to deal with filters which - // are referring to the type only (identifier is 0)! - if (entry.Filter.Type != packetType || entry.Filter.Identifier != identifier) + + if (entry.PacketIdentifier != identifier) { continue; } diff --git a/Source/MQTTnet/Packets/MqttConnAckPacket.cs b/Source/MQTTnet/Packets/MqttConnAckPacket.cs index 3ae7bc6b..968e98d7 100644 --- a/Source/MQTTnet/Packets/MqttConnAckPacket.cs +++ b/Source/MQTTnet/Packets/MqttConnAckPacket.cs @@ -28,7 +28,7 @@ namespace MQTTnet.Packets public MqttQualityOfServiceLevel MaximumQoS { get; set; } /// - /// Added in MQTTv5. + /// The return code is only used in MQTTv 5.0.0 and higher. /// public MqttConnectReasonCode ReasonCode { get; set; } @@ -39,13 +39,16 @@ namespace MQTTnet.Packets public string ResponseInformation { get; set; } public bool RetainAvailable { get; set; } - + + /// + /// The return code is only used in MQTTv 3.1.1 and lower. + /// public MqttConnectReturnCode ReturnCode { get; set; } public ushort ServerKeepAlive { get; set; } public string ServerReference { get; set; } - + public uint SessionExpiryInterval { get; set; } public bool SharedSubscriptionAvailable { get; set; } diff --git a/Source/MQTTnet/Packets/MqttUserPropertiesBuilder.cs b/Source/MQTTnet/Packets/MqttUserPropertiesBuilder.cs new file mode 100644 index 00000000..8c56f16e --- /dev/null +++ b/Source/MQTTnet/Packets/MqttUserPropertiesBuilder.cs @@ -0,0 +1,24 @@ +// 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.Collections.Generic; + +namespace MQTTnet.Packets +{ + public sealed class MqttUserPropertiesBuilder + { + readonly List _properties = new List(); + + public List Build() + { + return _properties; + } + + public MqttUserPropertiesBuilder With(string name, string value = "") + { + _properties.Add(new MqttUserProperty(name, value)); + return this; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/PacketDispatcher/MqttPacketAwaitableFilter.cs b/Source/MQTTnet/Server/Events/ClientReAuthenticatingEventArgs.cs similarity index 58% rename from Source/MQTTnet/PacketDispatcher/MqttPacketAwaitableFilter.cs rename to Source/MQTTnet/Server/Events/ClientReAuthenticatingEventArgs.cs index ab94969f..40d5c086 100644 --- a/Source/MQTTnet/PacketDispatcher/MqttPacketAwaitableFilter.cs +++ b/Source/MQTTnet/Server/Events/ClientReAuthenticatingEventArgs.cs @@ -4,12 +4,10 @@ using System; -namespace MQTTnet.PacketDispatcher +namespace MQTTnet.Server { - public sealed class MqttPacketAwaitableFilter + public sealed class ClientReAuthenticatingEventArgs : EventArgs { - public Type Type { get; set; } - public ushort Identifier { get; set; } } } \ No newline at end of file diff --git a/Source/MQTTnet/Server/Events/PartialAuthenticationResponse.cs b/Source/MQTTnet/Server/Events/PartialAuthenticationResponse.cs new file mode 100644 index 00000000..07eb5363 --- /dev/null +++ b/Source/MQTTnet/Server/Events/PartialAuthenticationResponse.cs @@ -0,0 +1,19 @@ +// 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.Collections.Generic; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Server +{ + public sealed class PartialAuthenticationResponse + { + public byte[] AuthenticationData { get; set; } + + public MqttAuthenticateReasonCode ReasonCode { get; set; } + + public List UserProperties { get; set; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/Events/ValidatingConnectionEventArgs.cs b/Source/MQTTnet/Server/Events/ValidatingConnectionEventArgs.cs index 096b2da1..028f70be 100644 --- a/Source/MQTTnet/Server/Events/ValidatingConnectionEventArgs.cs +++ b/Source/MQTTnet/Server/Events/ValidatingConnectionEventArgs.cs @@ -7,7 +7,10 @@ using System.Collections; using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; using System.Text; +using System.Threading; +using System.Threading.Tasks; using MQTTnet.Adapter; +using MQTTnet.Exceptions; using MQTTnet.Formatter; using MQTTnet.Internal; using MQTTnet.Packets; @@ -19,11 +22,12 @@ namespace MQTTnet.Server { readonly MqttConnectPacket _connectPacket; - public ValidatingConnectionEventArgs(MqttConnectPacket connectPacket, IMqttChannelAdapter clientAdapter, IDictionary sessionItems) + 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; } /// @@ -44,6 +48,11 @@ namespace MQTTnet.Server /// public string AuthenticationMethod => _connectPacket.AuthenticationMethod; + /// + /// Gets a cancellation token which is being canceled as soon as the transport connection is closed. + /// + 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. @@ -189,5 +198,51 @@ namespace MQTTnet.Server /// A value of 0 indicates that the value is not used. /// public uint WillDelayInterval => _connectPacket.WillDelayInterval; + + public async Task ReceiveAuthenticationDataAsync(CancellationToken cancellationToken = default) + { + var receivePacket = await ChannelAdapter.ReceivePacketAsync(cancellationToken).ConfigureAwait(false); + if (receivePacket is MqttAuthPacket authPacket) + { + if (!string.Equals(authPacket.AuthenticationMethod, AuthenticationMethod, StringComparison.Ordinal)) + { + throw new MqttProtocolViolationException("The authentication method is not allowed to change while authenticating."); + } + + return new PartialAuthenticationResponse + { + ReasonCode = authPacket.ReasonCode, + AuthenticationData = authPacket.AuthenticationData, + UserProperties = authPacket.UserProperties + }; + } + + if (receivePacket == null) + { + throw new MqttCommunicationException("The client closed the connection."); + } + + throw new MqttProtocolViolationException("Expected an AUTH packet from the client."); + } + + public Task SendAuthenticationDataAsync( + MqttAuthenticateReasonCode reasonCode, + byte[] authenticationData = null, + string reasonString = null, + List userProperties = null, + CancellationToken cancellationToken = default) + { + // The authentication method will never change so we must use the already known one [MQTT-4.12.0-5]. + var authPacket = new MqttAuthPacket + { + AuthenticationMethod = AuthenticationMethod, + AuthenticationData = authenticationData, + UserProperties = userProperties, + ReasonCode = reasonCode, + ReasonString = reasonString + }; + + return ChannelAdapter.SendPacketAsync(authPacket, cancellationToken); + } } } \ No newline at end of file diff --git a/Source/MQTTnet/Server/Internal/MqttClient.cs b/Source/MQTTnet/Server/Internal/MqttClient.cs index 53b49fdc..0f782a79 100644 --- a/Source/MQTTnet/Server/Internal/MqttClient.cs +++ b/Source/MQTTnet/Server/Internal/MqttClient.cs @@ -95,8 +95,9 @@ namespace MQTTnet.Server { var cancellationToken = _cancellationToken.Token; IsRunning = true; - - _ = Task.Factory.StartNew(() => SendPacketsLoop(cancellationToken), cancellationToken, TaskCreationOptions.PreferFairness, TaskScheduler.Default).ConfigureAwait(false); + + _ = Task.Factory.StartNew(() => SendPacketsLoop(cancellationToken), cancellationToken, TaskCreationOptions.PreferFairness, TaskScheduler.Default) + .ConfigureAwait(false); await ReceivePackagesLoop(cancellationToken).ConfigureAwait(false); } @@ -176,6 +177,22 @@ namespace MQTTnet.Server return CompletedTask.Instance; } + async Task HandleIncomingAuthPacket(MqttAuthPacket authPacket) + { + if (authPacket.ReasonCode != MqttAuthenticateReasonCode.ReAuthenticate) + { + throw new MqttProtocolViolationException("Clients are only allowed to send AUTH packets after connecting for re authentication."); + } + + // We have to handle the re authentication in another task because we rely on the package retrieval. + _ = Task.Run( + async () => + { + var eventArgs = new ClientReAuthenticatingEventArgs(); + await _eventContainer.ClientReAuthenticatingEvent.InvokeAsync(eventArgs); + }); + } + void HandleIncomingPingReqPacket() { // See: The Server MUST send a PINGRESP packet in response to a PINGREQ packet [MQTT-3.12.4-1]. @@ -436,9 +453,13 @@ namespace MQTTnet.Server DisconnectPacket = disconnectPacket; return; } + else if (currentPacket is MqttAuthPacket authPacket) + { + await HandleIncomingAuthPacket(authPacket); + } else { - throw new MqttProtocolViolationException("Packet not allowed"); + throw new MqttProtocolViolationException("Packet not allowed."); } } } diff --git a/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs b/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs index 4053e438..edabc5a2 100644 --- a/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs +++ b/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs @@ -360,7 +360,7 @@ namespace MQTTnet.Server return; } - var validatingConnectionEventArgs = await ValidateConnection(connectPacket, channelAdapter).ConfigureAwait(false); + var validatingConnectionEventArgs = await ValidateConnection(connectPacket, channelAdapter, cancellationToken).ConfigureAwait(false); var connAckPacket = MqttPacketFactories.ConnAck.Create(validatingConnectionEventArgs); if (validatingConnectionEventArgs.ReasonCode != MqttConnectReasonCode.Success) @@ -397,7 +397,7 @@ namespace MQTTnet.Server } catch (Exception exception) { - _logger.Error(exception, exception.Message); + _logger.Error(exception, $"Error while validating client {channelAdapter.Endpoint}. {exception.Message}"); } finally { @@ -719,11 +719,11 @@ namespace MQTTnet.Server return null; } - 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/MqttServerEventContainer.cs b/Source/MQTTnet/Server/Internal/MqttServerEventContainer.cs index 5f06b6d9..98134700 100644 --- a/Source/MQTTnet/Server/Internal/MqttServerEventContainer.cs +++ b/Source/MQTTnet/Server/Internal/MqttServerEventContainer.cs @@ -17,11 +17,14 @@ namespace MQTTnet.Server public AsyncEvent ClientDisconnectedEvent { get; } = new AsyncEvent(); + public AsyncEvent ClientReAuthenticatingEvent { get; } = new AsyncEvent(); + public AsyncEvent ClientSubscribedTopicEvent { get; } = new AsyncEvent(); public AsyncEvent ClientUnsubscribedTopicEvent { get; } = new AsyncEvent(); - public AsyncEvent InterceptingClientEnqueueEvent { get; } = new AsyncEvent(); + public AsyncEvent InterceptingClientEnqueueEvent { get; } = + new AsyncEvent(); public AsyncEvent InterceptingInboundPacketEvent { get; } = new AsyncEvent(); diff --git a/Source/MQTTnet/Server/MqttServer.cs b/Source/MQTTnet/Server/MqttServer.cs index daed2055..a4cf37d9 100644 --- a/Source/MQTTnet/Server/MqttServer.cs +++ b/Source/MQTTnet/Server/MqttServer.cs @@ -73,6 +73,12 @@ namespace MQTTnet.Server remove => _eventContainer.ClientDisconnectedEvent.RemoveHandler(value); } + public event Func ClientReAuthenticatingAsync + { + add => _eventContainer.ClientReAuthenticatingEvent.AddHandler(value); + remove => _eventContainer.ClientReAuthenticatingEvent.RemoveHandler(value); + } + public event Func ClientSubscribedTopicAsync { add => _eventContainer.ClientSubscribedTopicEvent.AddHandler(value); @@ -203,13 +209,6 @@ namespace MQTTnet.Server return _clientSessionsManager.GetClientStatusesAsync(); } - public Task> GetRetainedMessagesAsync() - { - ThrowIfNotStarted(); - - return _retainedMessagesManager.GetMessages(); - } - public Task GetRetainedMessageAsync(string topic) { if (topic == null) @@ -222,6 +221,13 @@ namespace MQTTnet.Server return _retainedMessagesManager.GetMessage(topic); } + public Task> GetRetainedMessagesAsync() + { + ThrowIfNotStarted(); + + return _retainedMessagesManager.GetMessages(); + } + public Task> GetSessionsAsync() { ThrowIfNotStarted();