Fix upgrading and downgrading of retained messages QoS for subscriptions (#1564)

This commit is contained in:
Christian
2022-11-06 10:35:59 +01:00
committed by GitHub
parent 5b1cc7d713
commit 3fc6982e9a
9 changed files with 111 additions and 23 deletions
+2 -1
View File
@@ -1,2 +1,3 @@
* [Core] Fixed several dead locks by implementing a new async signal (#1552) * [Core] Fixed several dead locks by implementing a new async signal (#1552)
* [Server] Fixed duplicated invocation of the event _ClientAcknowledgedPublishPacketAsync_ for QoS level 2 (#1550) * [Server] Fixed duplicated invocation of the event _ClientAcknowledgedPublishPacketAsync_ for QoS level 2 (#1550)
* [Server] Fixed issue in upgrading and downgrading of QoS levels for subscriptions and retained messages (#1560)
@@ -65,6 +65,66 @@ namespace MQTTnet.Tests.Server
} }
} }
[TestMethod]
public async Task Downgrade_QoS_Level()
{
using (var testEnvironment = CreateTestEnvironment())
{
await testEnvironment.StartServer();
var c1 = await testEnvironment.ConnectClient();
// Add the retained message with QoS 2!
await c1.PublishAsync(
new MqttApplicationMessageBuilder().WithTopic("retained")
.WithPayload(new byte[3])
.WithRetainFlag()
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.ExactlyOnce)
.Build());
// The second client uses QoS 1 so a downgrade is required.
var c2 = await testEnvironment.ConnectClient();
var messageHandler = testEnvironment.CreateApplicationMessageHandler(c2);
await c2.SubscribeAsync(new MqttTopicFilter { Topic = "retained", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce });
await LongTestDelay();
messageHandler.AssertReceivedCountEquals(1);
Assert.AreEqual(MqttQualityOfServiceLevel.AtLeastOnce, messageHandler.ReceivedEventArgs.First().ApplicationMessage.QualityOfServiceLevel);
}
}
[TestMethod]
public async Task No_Upgrade_QoS_Level()
{
using (var testEnvironment = CreateTestEnvironment())
{
await testEnvironment.StartServer();
var c1 = await testEnvironment.ConnectClient();
// Add the retained message with QoS 1!
await c1.PublishAsync(
new MqttApplicationMessageBuilder().WithTopic("retained")
.WithPayload(new byte[3])
.WithRetainFlag()
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
.Build());
// The second client uses QoS 2 so an upgrade is expected but according to the MQTT spec this is not supported!
var c2 = await testEnvironment.ConnectClient();
var messageHandler = testEnvironment.CreateApplicationMessageHandler(c2);
await c2.SubscribeAsync(new MqttTopicFilter { Topic = "retained", QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce });
await LongTestDelay();
messageHandler.AssertReceivedCountEquals(1);
Assert.AreEqual(MqttQualityOfServiceLevel.AtLeastOnce, messageHandler.ReceivedEventArgs.First().ApplicationMessage.QualityOfServiceLevel);
}
}
[TestMethod] [TestMethod]
public async Task Receive_No_Retained_Message_After_Subscribe() public async Task Receive_No_Retained_Message_After_Subscribe()
{ {
@@ -124,7 +184,7 @@ namespace MQTTnet.Tests.Server
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
.WithRetainFlag() .WithRetainFlag()
.Build()); .Build());
await c1.DisconnectAsync(); await c1.DisconnectAsync();
// Subscribe using a new client. // Subscribe using a new client.
@@ -7,6 +7,7 @@ namespace MQTTnet.Formatter
public sealed class MqttPacketFactories public sealed class MqttPacketFactories
{ {
public MqttConnAckPacketFactory ConnAck { get; } = new MqttConnAckPacketFactory(); public MqttConnAckPacketFactory ConnAck { get; } = new MqttConnAckPacketFactory();
public MqttConnectPacketFactory Connect { get; } = new MqttConnectPacketFactory(); public MqttConnectPacketFactory Connect { get; } = new MqttConnectPacketFactory();
public MqttDisconnectPacketFactory Disconnect { get; } = new MqttDisconnectPacketFactory(); public MqttDisconnectPacketFactory Disconnect { get; } = new MqttDisconnectPacketFactory();
@@ -5,6 +5,7 @@
using System; using System;
using MQTTnet.Exceptions; using MQTTnet.Exceptions;
using MQTTnet.Packets; using MQTTnet.Packets;
using MQTTnet.Server;
namespace MQTTnet.Formatter namespace MQTTnet.Formatter
{ {
@@ -80,5 +81,17 @@ namespace MQTTnet.Formatter
return packet; return packet;
} }
public MqttPublishPacket Create(MqttRetainedMessageMatch retainedMessage)
{
if (retainedMessage == null)
{
throw new ArgumentNullException(nameof(retainedMessage));
}
var publishPacket = Create(retainedMessage.ApplicationMessage);
publishPacket.QualityOfServiceLevel = retainedMessage.SubscriptionQualityOfServiceLevel;
return publishPacket;
}
} }
} }
+2 -3
View File
@@ -296,15 +296,14 @@ namespace MQTTnet.Server
if (subscribeResult.RetainedMessages != null) if (subscribeResult.RetainedMessages != null)
{ {
foreach (var retainedApplicationMessage in subscribeResult.RetainedMessages) foreach (var retainedMessageMatch in subscribeResult.RetainedMessages)
{ {
var publishPacket = _packetFactories.Publish.Create(retainedApplicationMessage.ApplicationMessage); var publishPacket = _packetFactories.Publish.Create(retainedMessageMatch);
Session.EnqueueDataPacket(new MqttPacketBusItem(publishPacket)); Session.EnqueueDataPacket(new MqttPacketBusItem(publishPacket));
} }
} }
} }
async Task HandleIncomingUnsubscribePacket(MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken) async Task HandleIncomingUnsubscribePacket(MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken)
{ {
var unsubscribeResult = await Session.SubscriptionsManager.Unsubscribe(unsubscribePacket, cancellationToken).ConfigureAwait(false); var unsubscribeResult = await Session.SubscriptionsManager.Unsubscribe(unsubscribePacket, cancellationToken).ConfigureAwait(false);
@@ -418,9 +418,9 @@ namespace MQTTnet.Server
if (subscribeResult.RetainedMessages != null) if (subscribeResult.RetainedMessages != null)
{ {
foreach (var retainedApplicationMessage in subscribeResult.RetainedMessages) foreach (var retainedMessageMatch in subscribeResult.RetainedMessages)
{ {
var publishPacket = _packetFactories.Publish.Create(retainedApplicationMessage.ApplicationMessage); var publishPacket = _packetFactories.Publish.Create(retainedMessageMatch);
clientSession.EnqueueDataPacket(new MqttPacketBusItem(publishPacket)); clientSession.EnqueueDataPacket(new MqttPacketBusItem(publishPacket));
} }
} }
@@ -169,10 +169,7 @@ namespace MQTTnet.Server
} }
var retainedApplicationMessages = await _retainedMessagesManager.GetMessages().ConfigureAwait(false); var retainedApplicationMessages = await _retainedMessagesManager.GetMessages().ConfigureAwait(false);
var result = new SubscribeResult var result = new SubscribeResult(subscribePacket.TopicFilters.Count);
{
ReasonCodes = new List<MqttSubscribeReasonCode>(subscribePacket.TopicFilters.Count)
};
var addedSubscriptions = new List<string>(); var addedSubscriptions = new List<string>();
var finalTopicFilters = new List<MqttTopicFilter>(); var finalTopicFilters = new List<MqttTopicFilter>();
@@ -412,14 +409,14 @@ namespace MQTTnet.Server
} }
static void FilterRetainedApplicationMessages( static void FilterRetainedApplicationMessages(
IList<MqttApplicationMessage> retainedApplicationMessages, IList<MqttApplicationMessage> retainedMessages,
CreateSubscriptionResult createSubscriptionResult, CreateSubscriptionResult createSubscriptionResult,
SubscribeResult subscribeResult) SubscribeResult subscribeResult)
{ {
for (var index = retainedApplicationMessages.Count - 1; index >= 0; index--) for (var index = retainedMessages.Count - 1; index >= 0; index--)
{ {
var retainedApplicationMessage = retainedApplicationMessages[index]; var retainedMessage = retainedMessages[index];
if (retainedApplicationMessage == null) if (retainedMessage == null)
{ {
continue; continue;
} }
@@ -436,16 +433,21 @@ namespace MQTTnet.Server
continue; continue;
} }
if (MqttTopicFilterComparer.Compare(retainedApplicationMessage.Topic, createSubscriptionResult.Subscription.Topic) != MqttTopicFilterCompareResult.IsMatch) if (MqttTopicFilterComparer.Compare(retainedMessage.Topic, createSubscriptionResult.Subscription.Topic) != MqttTopicFilterCompareResult.IsMatch)
{ {
continue; continue;
} }
var retainedMessageMatch = new MqttRetainedMessageMatch var retainedMessageMatch = new MqttRetainedMessageMatch(retainedMessage, createSubscriptionResult.Subscription.GrantedQualityOfServiceLevel);
if (retainedMessageMatch.SubscriptionQualityOfServiceLevel > retainedMessageMatch.ApplicationMessage.QualityOfServiceLevel)
{ {
ApplicationMessage = retainedApplicationMessage, // UPGRADING the QoS is not allowed!
SubscriptionQualityOfServiceLevel = createSubscriptionResult.Subscription.GrantedQualityOfServiceLevel // From MQTT spec: Subscribing to a Topic Filter at QoS 2 is equivalent to saying
}; // "I would like to receive Messages matching this filter at the QoS with which they were published".
// This means a publisher is responsible for determining the maximum QoS a Message can be delivered at,
// but a subscriber is able to require that the Server downgrades the QoS to one more suitable for its usage.
retainedMessageMatch.SubscriptionQualityOfServiceLevel = retainedMessageMatch.ApplicationMessage.QualityOfServiceLevel;
}
if (subscribeResult.RetainedMessages == null) if (subscribeResult.RetainedMessages == null)
{ {
@@ -456,7 +458,7 @@ namespace MQTTnet.Server
// Clear the retained message from the list because the client should receive every message only // Clear the retained message from the list because the client should receive every message only
// one time even if multiple subscriptions affect them. // one time even if multiple subscriptions affect them.
retainedApplicationMessages[index] = null; retainedMessages[index] = null;
} }
} }
@@ -10,8 +10,13 @@ namespace MQTTnet.Server
{ {
public sealed class SubscribeResult public sealed class SubscribeResult
{ {
public SubscribeResult(int topicsCount)
{
ReasonCodes = new List<MqttSubscribeReasonCode>(topicsCount);
}
public bool CloseConnection { get; set; } public bool CloseConnection { get; set; }
public List<MqttSubscribeReasonCode> ReasonCodes { get; set; } public List<MqttSubscribeReasonCode> ReasonCodes { get; set; }
public string ReasonString { get; set; } public string ReasonString { get; set; }
@@ -2,13 +2,20 @@
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
using MQTTnet.Protocol; using MQTTnet.Protocol;
namespace MQTTnet.Server namespace MQTTnet.Server
{ {
public sealed class MqttRetainedMessageMatch public sealed class MqttRetainedMessageMatch
{ {
public MqttApplicationMessage ApplicationMessage { get; set; } public MqttRetainedMessageMatch(MqttApplicationMessage applicationMessage, MqttQualityOfServiceLevel subscriptionQualityOfServiceLevel)
{
ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage));
SubscriptionQualityOfServiceLevel = subscriptionQualityOfServiceLevel;
}
public MqttApplicationMessage ApplicationMessage { get; }
public MqttQualityOfServiceLevel SubscriptionQualityOfServiceLevel { get; set; } public MqttQualityOfServiceLevel SubscriptionQualityOfServiceLevel { get; set; }
} }