mirror of
https://github.com/dotnet/MQTTnet.git
synced 2026-10-16 17:23:04 +08:00
Improve topic comparer.
This commit is contained in:
@@ -234,8 +234,6 @@ namespace MQTTnet.Server.Internal
|
||||
if (clientId == null) throw new ArgumentNullException(nameof(clientId));
|
||||
if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters));
|
||||
|
||||
if (topicFilters is null) throw new ArgumentNullException(nameof(topicFilters));
|
||||
|
||||
var fakeSubscribePacket = new MqttSubscribePacket
|
||||
{
|
||||
TopicFilters = topicFilters.ToList()
|
||||
|
||||
@@ -44,22 +44,22 @@ namespace MQTTnet.Server.Internal
|
||||
foreach (var originalTopicFilter in subscribePacket.TopicFilters.OrderByDescending(f => f.QualityOfServiceLevel))
|
||||
{
|
||||
var interceptorContext = await InterceptSubscribe(originalTopicFilter).ConfigureAwait(false);
|
||||
var finalTopicFilter = interceptorContext?.TopicFilter ?? originalTopicFilter;
|
||||
var acceptSubscription = interceptorContext?.AcceptSubscription ?? true;
|
||||
var closeConnection = interceptorContext?.CloseConnection ?? false;
|
||||
|
||||
var finalTopicFilter = interceptorContext.TopicFilter;
|
||||
var acceptSubscription = interceptorContext.ReasonCode <= MqttSubscribeReasonCode.GrantedQoS2;
|
||||
|
||||
if (string.IsNullOrEmpty(finalTopicFilter.Topic) || !acceptSubscription)
|
||||
{
|
||||
// Return codes is for MQTT < 5 and Reason Code is for MQTT > 5.
|
||||
result.ReturnCodes.Add(MqttSubscribeReturnCode.Failure);
|
||||
result.ReasonCodes.Add(MqttSubscribeReasonCode.UnspecifiedError);
|
||||
result.ReasonCodes.Add(interceptorContext.ReasonCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.ReturnCodes.Add(ConvertToSubscribeReturnCode(finalTopicFilter.QualityOfServiceLevel));
|
||||
result.ReasonCodes.Add(ConvertToSubscribeReasonCode(finalTopicFilter.QualityOfServiceLevel));
|
||||
result.ReturnCodes.Add(interceptorContext.ReturnCode);
|
||||
result.ReasonCodes.Add(interceptorContext.ReasonCode);
|
||||
}
|
||||
|
||||
if (closeConnection)
|
||||
if (interceptorContext.CloseConnection)
|
||||
{
|
||||
result.CloseConnection = true;
|
||||
}
|
||||
@@ -69,7 +69,10 @@ namespace MQTTnet.Server.Internal
|
||||
continue;
|
||||
}
|
||||
|
||||
var subscription = CreateSubscription(finalTopicFilter, subscribePacket);
|
||||
var subscription = CreateSubscription(
|
||||
finalTopicFilter,
|
||||
subscribePacket.Properties?.SubscriptionIdentifier ?? 0,
|
||||
interceptorContext.ReasonCode);
|
||||
|
||||
await _eventDispatcher.SafeNotifyClientSubscribedTopicAsync(_clientSession.ClientId, finalTopicFilter).ConfigureAwait(false);
|
||||
|
||||
@@ -148,12 +151,12 @@ namespace MQTTnet.Server.Internal
|
||||
retainAsPublished = true;
|
||||
}
|
||||
|
||||
if (!MqttTopicFilterComparer.IsMatch(topic, subscription.Topic))
|
||||
if (MqttTopicFilterComparer.Compare(topic, subscription.Topic) != MqttTopicFilterCompareResult.IsMatch)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
qosLevels.Add(subscription.QualityOfServiceLevel);
|
||||
qosLevels.Add(subscription.GrantedQualityOfServiceLevel);
|
||||
|
||||
if (subscription.Identifier > 0)
|
||||
{
|
||||
@@ -175,7 +178,7 @@ namespace MQTTnet.Server.Internal
|
||||
};
|
||||
}
|
||||
|
||||
Subscription CreateSubscription(MqttTopicFilter topicFilter, MqttSubscribePacket subscribePacket)
|
||||
Subscription CreateSubscription(MqttTopicFilter topicFilter, uint subscriptionIdentifier, MqttSubscribeReasonCode reasonCode)
|
||||
{
|
||||
var subscription = new Subscription
|
||||
{
|
||||
@@ -183,10 +186,26 @@ namespace MQTTnet.Server.Internal
|
||||
NoLocal = topicFilter.NoLocal,
|
||||
RetainHandling = topicFilter.RetainHandling,
|
||||
RetainAsPublished = topicFilter.RetainAsPublished,
|
||||
QualityOfServiceLevel = topicFilter.QualityOfServiceLevel,
|
||||
Identifier = subscribePacket.Properties?.SubscriptionIdentifier ?? 0
|
||||
Identifier = subscriptionIdentifier
|
||||
};
|
||||
|
||||
if (reasonCode == MqttSubscribeReasonCode.GrantedQoS0)
|
||||
{
|
||||
subscription.GrantedQualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce;
|
||||
}
|
||||
else if (reasonCode == MqttSubscribeReasonCode.GrantedQoS1)
|
||||
{
|
||||
subscription.GrantedQualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce;
|
||||
}
|
||||
else if (reasonCode == MqttSubscribeReasonCode.GrantedQoS2)
|
||||
{
|
||||
subscription.GrantedQualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
_subscriptionsLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
@@ -223,7 +242,7 @@ namespace MQTTnet.Server.Internal
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!MqttTopicFilterComparer.IsMatch(retainedApplicationMessage.Topic, subscription.Topic))
|
||||
if (MqttTopicFilterComparer.Compare(retainedApplicationMessage.Topic, subscription.Topic) != MqttTopicFilterCompareResult.IsMatch)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -232,7 +251,7 @@ namespace MQTTnet.Server.Internal
|
||||
{
|
||||
ApplicationMessage = retainedApplicationMessage,
|
||||
IsRetainedMessage = true,
|
||||
SubscriptionQualityOfServiceLevel = subscription.QualityOfServiceLevel
|
||||
SubscriptionQualityOfServiceLevel = subscription.GrantedQualityOfServiceLevel
|
||||
};
|
||||
|
||||
if (subscription.Identifier > 0)
|
||||
@@ -245,25 +264,9 @@ namespace MQTTnet.Server.Internal
|
||||
retainedApplicationMessages[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
static MqttSubscribeReturnCode ConvertToSubscribeReturnCode(MqttQualityOfServiceLevel qualityOfServiceLevel)
|
||||
{
|
||||
return (MqttSubscribeReturnCode)(int)qualityOfServiceLevel;
|
||||
}
|
||||
|
||||
static MqttSubscribeReasonCode ConvertToSubscribeReasonCode(MqttQualityOfServiceLevel qualityOfServiceLevel)
|
||||
{
|
||||
return (MqttSubscribeReasonCode)(int)qualityOfServiceLevel;
|
||||
}
|
||||
|
||||
|
||||
async Task<MqttSubscriptionInterceptorContext> InterceptSubscribe(MqttTopicFilter topicFilter)
|
||||
{
|
||||
var interceptor = _options.SubscriptionInterceptor;
|
||||
if (interceptor == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var context = new MqttSubscriptionInterceptorContext
|
||||
{
|
||||
ClientId = _clientSession.ClientId,
|
||||
@@ -271,8 +274,39 @@ namespace MQTTnet.Server.Internal
|
||||
SessionItems = _clientSession.Items
|
||||
};
|
||||
|
||||
await interceptor.InterceptSubscriptionAsync(context).ConfigureAwait(false);
|
||||
if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce)
|
||||
{
|
||||
context.ReturnCode = MqttSubscribeReturnCode.SuccessMaximumQoS0;
|
||||
context.ReasonCode = MqttSubscribeReasonCode.GrantedQoS0;
|
||||
}
|
||||
else if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtLeastOnce)
|
||||
{
|
||||
context.ReturnCode = MqttSubscribeReturnCode.SuccessMaximumQoS1;
|
||||
context.ReasonCode = MqttSubscribeReasonCode.GrantedQoS1;
|
||||
}
|
||||
else if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.ExactlyOnce)
|
||||
{
|
||||
context.ReturnCode = MqttSubscribeReturnCode.SuccessMaximumQoS2;
|
||||
context.ReasonCode = MqttSubscribeReasonCode.GrantedQoS2;
|
||||
}
|
||||
|
||||
context.DefaultReturnCode = context.ReturnCode;
|
||||
context.DefaultReasonCode = context.ReasonCode;
|
||||
|
||||
if (topicFilter.Topic.StartsWith("$share/"))
|
||||
{
|
||||
context.ReasonCode = MqttSubscribeReasonCode.SharedSubscriptionsNotSupported;
|
||||
context.ReturnCode = MqttSubscribeReturnCode.Failure;
|
||||
}
|
||||
else
|
||||
{
|
||||
var interceptor = _options.SubscriptionInterceptor;
|
||||
if (interceptor != null)
|
||||
{
|
||||
await interceptor.InterceptSubscriptionAsync(context).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace MQTTnet.Server.Internal
|
||||
{
|
||||
public enum MqttTopicFilterCompareResult
|
||||
{
|
||||
NoMatch,
|
||||
|
||||
IsMatch,
|
||||
|
||||
FilterInvalid,
|
||||
|
||||
TopicInvalid
|
||||
}
|
||||
}
|
||||
@@ -1,128 +1,164 @@
|
||||
using System;
|
||||
|
||||
namespace MQTTnet.Server.Internal
|
||||
namespace MQTTnet.Server.Internal
|
||||
{
|
||||
public static class MqttTopicFilterComparer
|
||||
{
|
||||
const char LevelSeparator = '/';
|
||||
const char MultiLevelWildcard = '#';
|
||||
const char SingleLevelWildcard = '+';
|
||||
const char ReservedTopicPrefix = '$';
|
||||
|
||||
public static bool IsMatch(string topic, string filter)
|
||||
public static MqttTopicFilterCompareResult Compare(string topic, string filter)
|
||||
{
|
||||
if (topic == null) throw new ArgumentNullException(nameof(topic));
|
||||
if (filter == null) throw new ArgumentNullException(nameof(filter));
|
||||
|
||||
var sPos = 0;
|
||||
var sLen = filter.Length;
|
||||
var tPos = 0;
|
||||
var tLen = topic.Length;
|
||||
|
||||
while (sPos < sLen && tPos < tLen)
|
||||
if (string.IsNullOrEmpty(topic))
|
||||
{
|
||||
if (filter[sPos] == topic[tPos])
|
||||
return MqttTopicFilterCompareResult.TopicInvalid;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(filter))
|
||||
{
|
||||
return MqttTopicFilterCompareResult.FilterInvalid;
|
||||
}
|
||||
|
||||
var filterOffset = 0;
|
||||
var filterLength = filter.Length;
|
||||
|
||||
var topicOffset = 0;
|
||||
var topicLength = topic.Length;
|
||||
|
||||
var isMultiLevelFilter = filter[filterLength - 1] == MultiLevelWildcard;
|
||||
var isReservedTopic = topic[0] == ReservedTopicPrefix;
|
||||
|
||||
if (isReservedTopic && filterLength == 1 && isMultiLevelFilter)
|
||||
{
|
||||
// It is not allowed to receive i.e. '$foo/bar' with filter '#'.
|
||||
return MqttTopicFilterCompareResult.NoMatch;
|
||||
}
|
||||
|
||||
if (isReservedTopic && filter[0] == SingleLevelWildcard)
|
||||
{
|
||||
// It is not allowed to receive i.e. '$SYS/monitor/Clients' with filter '+/monitor/Clients'.
|
||||
return MqttTopicFilterCompareResult.NoMatch;
|
||||
}
|
||||
|
||||
if (filterLength == 1 && isMultiLevelFilter)
|
||||
{
|
||||
// Filter '#' matches basically everything.
|
||||
return MqttTopicFilterCompareResult.IsMatch;
|
||||
}
|
||||
|
||||
// Go through the filter char by char.
|
||||
while (filterOffset < filterLength && topicOffset < topicLength)
|
||||
{
|
||||
// Check if the current char is a multi level wildcard. The char is only allowed
|
||||
// at the very las position.
|
||||
if (filter[filterOffset] == MultiLevelWildcard && filterOffset != filterLength - 1)
|
||||
{
|
||||
if (tPos == tLen - 1)
|
||||
return MqttTopicFilterCompareResult.FilterInvalid;
|
||||
}
|
||||
|
||||
if (filter[filterOffset] == topic[topicOffset])
|
||||
{
|
||||
if (topicOffset == topicLength - 1)
|
||||
{
|
||||
// Check for e.g. foo matching foo/#
|
||||
if (sPos == sLen - 3
|
||||
&& filter[sPos + 1] == LevelSeparator
|
||||
&& filter[sPos + 2] == MultiLevelWildcard)
|
||||
// Check for e.g. "foo" matching "foo/#"
|
||||
if (filterOffset == filterLength - 3 &&
|
||||
filter[filterOffset + 1] == LevelSeparator &&
|
||||
isMultiLevelFilter)
|
||||
{
|
||||
return true;
|
||||
return MqttTopicFilterCompareResult.IsMatch;
|
||||
}
|
||||
// Check for e.g. foo/ matching foo/#
|
||||
if (sPos == sLen - 2
|
||||
&& filter[sPos] == LevelSeparator
|
||||
&& filter[sPos + 1] == MultiLevelWildcard)
|
||||
|
||||
// Check for e.g. "foo/" matching "foo/#"
|
||||
if (filterOffset == filterLength - 2 &&
|
||||
filter[filterOffset] == LevelSeparator &&
|
||||
isMultiLevelFilter)
|
||||
{
|
||||
return true;
|
||||
return MqttTopicFilterCompareResult.IsMatch;
|
||||
}
|
||||
}
|
||||
|
||||
sPos++;
|
||||
tPos++;
|
||||
filterOffset++;
|
||||
topicOffset++;
|
||||
|
||||
if (sPos == sLen && tPos == tLen)
|
||||
// Check if the end was reached and i.e. "foo/bar" matches "foo/bar"
|
||||
if (filterOffset == filterLength && topicOffset == topicLength)
|
||||
{
|
||||
return true;
|
||||
return MqttTopicFilterCompareResult.IsMatch;
|
||||
}
|
||||
|
||||
if (tPos == tLen && sPos == sLen - 1 && filter[sPos] == SingleLevelWildcard)
|
||||
var endOfTopic = topicOffset == topicLength;
|
||||
|
||||
if (endOfTopic && filterOffset == filterLength - 1 && filter[filterOffset] == SingleLevelWildcard)
|
||||
{
|
||||
if (sPos > 0 && filter[sPos - 1] != LevelSeparator)
|
||||
if (filterOffset > 0 && filter[filterOffset - 1] != LevelSeparator)
|
||||
{
|
||||
// Invalid filter string
|
||||
return false;
|
||||
// Invalid filter.
|
||||
return MqttTopicFilterCompareResult.FilterInvalid;
|
||||
}
|
||||
|
||||
return true;
|
||||
return MqttTopicFilterCompareResult.IsMatch;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (filter[sPos] == SingleLevelWildcard)
|
||||
if (filter[filterOffset] == SingleLevelWildcard)
|
||||
{
|
||||
// Check for bad "+foo" or "a/+foo" subscription
|
||||
if (sPos > 0 && filter[sPos - 1] != LevelSeparator)
|
||||
// Check for invalid "+foo" or "a/+foo" subscription
|
||||
if (filterOffset > 0 && filter[filterOffset - 1] != LevelSeparator)
|
||||
{
|
||||
// Invalid filter string
|
||||
return false;
|
||||
return MqttTopicFilterCompareResult.FilterInvalid;
|
||||
}
|
||||
|
||||
// Check for bad "foo+" or "foo+/a" subscription
|
||||
if (sPos < sLen - 1 && filter[sPos + 1] != LevelSeparator)
|
||||
if (filterOffset < filterLength - 1 && filter[filterOffset + 1] != LevelSeparator)
|
||||
{
|
||||
// Invalid filter string
|
||||
return false;
|
||||
return MqttTopicFilterCompareResult.FilterInvalid;
|
||||
}
|
||||
|
||||
sPos++;
|
||||
while (tPos < tLen && topic[tPos] != LevelSeparator)
|
||||
filterOffset++;
|
||||
while (topicOffset < topicLength && topic[topicOffset] != LevelSeparator)
|
||||
{
|
||||
tPos++;
|
||||
topicOffset++;
|
||||
}
|
||||
|
||||
if (tPos == tLen && sPos == sLen)
|
||||
if (topicOffset == topicLength && filterOffset == filterLength)
|
||||
{
|
||||
return true;
|
||||
return MqttTopicFilterCompareResult.IsMatch;
|
||||
}
|
||||
}
|
||||
else if (filter[sPos] == MultiLevelWildcard)
|
||||
else if (filter[filterOffset] == MultiLevelWildcard)
|
||||
{
|
||||
if (sPos > 0 && filter[sPos - 1] != LevelSeparator)
|
||||
if (filterOffset > 0 && filter[filterOffset - 1] != LevelSeparator)
|
||||
{
|
||||
// Invalid filter string
|
||||
return false;
|
||||
return MqttTopicFilterCompareResult.FilterInvalid;
|
||||
}
|
||||
|
||||
if (sPos + 1 != sLen)
|
||||
if (filterOffset + 1 != filterLength)
|
||||
{
|
||||
// Invalid filter string
|
||||
return false;
|
||||
return MqttTopicFilterCompareResult.FilterInvalid;
|
||||
}
|
||||
|
||||
return true;
|
||||
return MqttTopicFilterCompareResult.IsMatch;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check for e.g. foo/bar matching foo/+/#
|
||||
if (sPos > 0
|
||||
&& sPos + 2 == sLen
|
||||
&& tPos == tLen
|
||||
&& filter[sPos - 1] == SingleLevelWildcard
|
||||
&& filter[sPos] == LevelSeparator
|
||||
&& filter[sPos + 1] == MultiLevelWildcard)
|
||||
// Check for e.g. "foo/bar" matching "foo/+/#".
|
||||
if (filterOffset > 0 &&
|
||||
filterOffset + 2 == filterLength &&
|
||||
topicOffset == topicLength &&
|
||||
filter[filterOffset - 1] == SingleLevelWildcard &&
|
||||
filter[filterOffset] == LevelSeparator &&
|
||||
isMultiLevelFilter)
|
||||
{
|
||||
return true;
|
||||
return MqttTopicFilterCompareResult.IsMatch;
|
||||
}
|
||||
|
||||
return false;
|
||||
return MqttTopicFilterCompareResult.NoMatch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return MqttTopicFilterCompareResult.NoMatch;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ namespace MQTTnet.Server.Internal
|
||||
|
||||
public bool RetainAsPublished { get; set; }
|
||||
|
||||
public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; }
|
||||
public MqttQualityOfServiceLevel GrantedQualityOfServiceLevel { get; set; }
|
||||
|
||||
public uint Identifier { get; set; }
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
@@ -21,8 +23,39 @@ namespace MQTTnet.Server
|
||||
/// </summary>
|
||||
public IDictionary<object, object> SessionItems { get; internal set; }
|
||||
|
||||
public bool AcceptSubscription { get; set; } = true;
|
||||
// Will be removed together with "AcceptSubscription". It only stores the default value when setting "AcceptSubscription" to true.
|
||||
internal MqttSubscribeReasonCode DefaultReasonCode { get; set; }
|
||||
internal MqttSubscribeReturnCode DefaultReturnCode { get; set; }
|
||||
|
||||
[Obsolete("Please use a proper value for 'ReasonCode' instead. This property will be removed in the future.")]
|
||||
public bool AcceptSubscription
|
||||
{
|
||||
get => ReasonCode >= MqttSubscribeReasonCode.GrantedQoS0 && ReasonCode <= MqttSubscribeReasonCode.GrantedQoS2;
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
ReasonCode = DefaultReasonCode;
|
||||
ReturnCode = DefaultReturnCode;
|
||||
}
|
||||
else
|
||||
{
|
||||
ReturnCode = MqttSubscribeReturnCode.Failure;
|
||||
ReasonCode = MqttSubscribeReasonCode.UnspecifiedError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the reason code which is sent to the client.
|
||||
/// The subscription is skipped when the value is not GrantedQoS_.
|
||||
/// MQTTv5 only.
|
||||
/// </summary>
|
||||
public MqttSubscribeReasonCode ReasonCode { get; set; }
|
||||
|
||||
// MQTT < 5 only!
|
||||
internal MqttSubscribeReturnCode ReturnCode { get; set; }
|
||||
|
||||
public bool CloseConnection { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user