mirror of
https://github.com/dotnet/MQTTnet.git
synced 2026-10-22 04:43:04 +08:00
Improve performance and drop support for old frameworks (#2068)
* Remove WebSocket4Net extension library * Remove UWP support * Drop support for all old frameworks * Update CI * Update CI * Update CI * Update CI * Remove obsolete code * Set default protocol version to 5.0.0 * Remove obsolete tests * Remove old dependencies * Reset release notes * Move server to dedicated project * Fix unit tests * Update and cleanup nuget packages * Fix unit tests Refactor code * No longer throw exception when receiving non success CONNACK * Enable nuget signing * Enable nuget signing * Enable nuget signing * Enable nuget signing * Enable nuget signing * Enable nuget signing * Enable nuget signing * Enable nuget signing * Enable code signing * Enable code signing * Enable code signing * Fix client endpoint handling * Improve CI * Fix CI * Fix CI * Fix CI * Fix CI * Import changes from version 4 * Cleanup target frameworks and remove obsolete code * Drop support for dotnet7.0 * Remove managed client * Version5 auditmode (#2016) Added NuGet audit mode and some other settings to get informed about security issues. * Address warnings from analyzers * Support for ReadOnlySequence as payload (#2046) * use ReadOnlySequence as payload * fix build * fix build * skip sign in forks * remove unnecessary owner * tests and extensions * remove test * simplify WritePacketBuffer, remove unused helper * Adjust namespaces * Fix build errors * Import latest fixes from main branch * Adjust namespaces --------- Co-authored-by: SeppPenner <HansM2013@gmx.de> Co-authored-by: Martin Regen <mregen@microsoft.com>
This commit is contained in:
co-authored by
SeppPenner
Martin Regen
parent
e18a91a4b5
commit
980a5d0a6d
@@ -0,0 +1,32 @@
|
||||
// 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 MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class MqttServerClientDisconnectOptions
|
||||
{
|
||||
public MqttDisconnectReasonCode ReasonCode { get; set; } = MqttDisconnectReasonCode.NormalDisconnection;
|
||||
|
||||
/// <summary>
|
||||
/// The reason string is sent to every client via a DISCONNECT packet.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public string ReasonString { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The server reference is sent to every client via a DISCONNECT packet.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public string ServerReference { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// These user properties are sent to every client via a DISCONNECT packet.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public List<MqttUserProperty> UserProperties { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// 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 MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class MqttServerClientDisconnectOptionsBuilder
|
||||
{
|
||||
readonly MqttServerClientDisconnectOptions _options = new MqttServerClientDisconnectOptions();
|
||||
|
||||
public MqttServerClientDisconnectOptions Build()
|
||||
{
|
||||
return _options;
|
||||
}
|
||||
|
||||
public MqttServerClientDisconnectOptionsBuilder WithReasonCode(MqttDisconnectReasonCode value)
|
||||
{
|
||||
_options.ReasonCode = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerClientDisconnectOptionsBuilder WithReasonString(string value)
|
||||
{
|
||||
_options.ReasonString = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerClientDisconnectOptionsBuilder WithServerReference(string value)
|
||||
{
|
||||
_options.ServerReference = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerClientDisconnectOptionsBuilder WithUserProperties(List<MqttUserProperty> value)
|
||||
{
|
||||
_options.UserProperties = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerClientDisconnectOptionsBuilder WithUserProperty(string name, string value)
|
||||
{
|
||||
if (_options.UserProperties == null)
|
||||
{
|
||||
_options.UserProperties = new List<MqttUserProperty>();
|
||||
}
|
||||
|
||||
_options.UserProperties.Add(new MqttUserProperty(name, value));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class ApplicationMessageEnqueuedEventArgs : EventArgs
|
||||
{
|
||||
public ApplicationMessageEnqueuedEventArgs(string senderClientId, string receiverClientId, MqttApplicationMessage applicationMessage, bool isDropped)
|
||||
{
|
||||
SenderClientId = senderClientId ?? throw new ArgumentNullException( nameof(senderClientId));
|
||||
ReceiverClientId = receiverClientId ?? throw new ArgumentNullException(nameof(receiverClientId));
|
||||
ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage));
|
||||
IsDropped = isDropped;
|
||||
}
|
||||
|
||||
public string SenderClientId { get; }
|
||||
|
||||
public string ReceiverClientId { get; }
|
||||
|
||||
public bool IsDropped { get; }
|
||||
|
||||
public MqttApplicationMessage ApplicationMessage { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class ApplicationMessageNotConsumedEventArgs : EventArgs
|
||||
{
|
||||
public ApplicationMessageNotConsumedEventArgs(MqttApplicationMessage applicationMessage, string senderId)
|
||||
{
|
||||
ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage));
|
||||
SenderId = senderId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the application message which was not consumed by any client.
|
||||
/// </summary>
|
||||
public MqttApplicationMessage ApplicationMessage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ID of the client which has sent the affected application message.
|
||||
/// </summary>
|
||||
public string SenderId { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// 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;
|
||||
using MQTTnet.Packets;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class ClientAcknowledgedPublishPacketEventArgs : EventArgs
|
||||
{
|
||||
public ClientAcknowledgedPublishPacketEventArgs(string clientId, IDictionary sessionItems, MqttPublishPacket publishPacket, MqttPacketWithIdentifier acknowledgePacket)
|
||||
{
|
||||
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
||||
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
||||
PublishPacket = publishPacket ?? throw new ArgumentNullException(nameof(publishPacket));
|
||||
AcknowledgePacket = acknowledgePacket ?? throw new ArgumentNullException(nameof(acknowledgePacket));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the packet which was used for acknowledge. This can be a PubAck or PubComp packet.
|
||||
/// </summary>
|
||||
public MqttPacketWithIdentifier AcknowledgePacket { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ID of the client which acknowledged a PUBLISH packet.
|
||||
/// </summary>
|
||||
public string ClientId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the PUBLISH packet is fully acknowledged. This is the case for PUBACK (QoS 1) and PUBCOMP (QoS 2.
|
||||
/// </summary>
|
||||
public bool IsCompleted => AcknowledgePacket is MqttPubAckPacket || AcknowledgePacket is MqttPubCompPacket;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the PUBLISH packet which was acknowledged.
|
||||
/// </summary>
|
||||
public MqttPublishPacket PublishPacket { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the session items which contain custom user data per session.
|
||||
/// </summary>
|
||||
public IDictionary SessionItems { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using MQTTnet.Formatter;
|
||||
using MQTTnet.Packets;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class ClientConnectedEventArgs : EventArgs
|
||||
{
|
||||
readonly MqttConnectPacket _connectPacket;
|
||||
|
||||
public ClientConnectedEventArgs(MqttConnectPacket connectPacket, MqttProtocolVersion protocolVersion, string endpoint, IDictionary sessionItems)
|
||||
{
|
||||
_connectPacket = connectPacket ?? throw new ArgumentNullException(nameof(connectPacket));
|
||||
ProtocolVersion = protocolVersion;
|
||||
Endpoint = endpoint;
|
||||
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
||||
}
|
||||
|
||||
public byte[] AuthenticationData => _connectPacket.AuthenticationData;
|
||||
|
||||
public string AuthenticationMethod => _connectPacket.AuthenticationMethod;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the client identifier of the connected client.
|
||||
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues.
|
||||
/// </summary>
|
||||
public string ClientId => _connectPacket.ClientId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the endpoint of the connected client.
|
||||
/// </summary>
|
||||
public string Endpoint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the protocol version which is used by the connected client.
|
||||
/// </summary>
|
||||
public MqttProtocolVersion ProtocolVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
||||
/// </summary>
|
||||
public IDictionary SessionItems { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user name of the connected client.
|
||||
/// </summary>
|
||||
public string UserName => _connectPacket.Username;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user properties sent by the client.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public List<MqttUserProperty> UserProperties => _connectPacket?.UserProperties;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class ClientDisconnectedEventArgs : EventArgs
|
||||
{
|
||||
readonly MqttDisconnectPacket _disconnectPacket;
|
||||
|
||||
public ClientDisconnectedEventArgs(
|
||||
string clientId,
|
||||
MqttDisconnectPacket disconnectPacket,
|
||||
MqttClientDisconnectType disconnectType,
|
||||
string endpoint,
|
||||
IDictionary sessionItems)
|
||||
{
|
||||
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
||||
DisconnectType = disconnectType;
|
||||
Endpoint = endpoint;
|
||||
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
||||
|
||||
// The DISCONNECT packet can be null in case of a non clean disconnect or session takeover.
|
||||
_disconnectPacket = disconnectPacket;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the client identifier.
|
||||
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues.
|
||||
/// </summary>
|
||||
public string ClientId { get; }
|
||||
|
||||
public MqttClientDisconnectType DisconnectType { get; }
|
||||
|
||||
public string Endpoint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reason code sent by the client.
|
||||
/// Only available for clean disconnects.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public MqttDisconnectReasonCode? ReasonCode => _disconnectPacket?.ReasonCode;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reason string sent by the client.
|
||||
/// Only available for clean disconnects.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public string ReasonString => _disconnectPacket?.ReasonString;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the session expiry interval sent by the client.
|
||||
/// Only available for clean disconnects.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public uint SessionExpiryInterval => _disconnectPacket?.SessionExpiryInterval ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
||||
/// </summary>
|
||||
public IDictionary SessionItems { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user properties sent by the client.
|
||||
/// Only available for clean disconnects.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public List<MqttUserProperty> UserProperties => _disconnectPacket?.UserProperties;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// 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;
|
||||
using MQTTnet.Packets;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class ClientSubscribedTopicEventArgs : EventArgs
|
||||
{
|
||||
public ClientSubscribedTopicEventArgs(string clientId, MqttTopicFilter topicFilter, IDictionary sessionItems)
|
||||
{
|
||||
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
||||
TopicFilter = topicFilter ?? throw new ArgumentNullException(nameof(topicFilter));
|
||||
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the client identifier.
|
||||
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues.
|
||||
/// </summary>
|
||||
public string ClientId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
||||
/// </summary>
|
||||
public IDictionary SessionItems { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the topic filter.
|
||||
/// The topic filter can contain topics and wildcards.
|
||||
/// </summary>
|
||||
public MqttTopicFilter TopicFilter { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// 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;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class ClientUnsubscribedTopicEventArgs : EventArgs
|
||||
{
|
||||
public ClientUnsubscribedTopicEventArgs(string clientId, string topicFilter, IDictionary sessionItems)
|
||||
{
|
||||
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
||||
TopicFilter = topicFilter ?? throw new ArgumentNullException(nameof(topicFilter));
|
||||
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the client identifier.
|
||||
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues.
|
||||
/// </summary>
|
||||
public string ClientId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
||||
/// </summary>
|
||||
public IDictionary SessionItems { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the topic filter.
|
||||
/// The topic filter can contain topics and wildcards.
|
||||
/// </summary>
|
||||
public string TopicFilter { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// 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;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class InterceptingClientApplicationMessageEnqueueEventArgs : EventArgs
|
||||
{
|
||||
public InterceptingClientApplicationMessageEnqueueEventArgs(string senderClientId, string receiverClientId, MqttApplicationMessage applicationMessage)
|
||||
{
|
||||
SenderClientId = senderClientId ?? throw new ArgumentNullException(nameof(senderClientId));
|
||||
ReceiverClientId = receiverClientId ?? throw new ArgumentNullException(nameof(receiverClientId));
|
||||
ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the enqueue of the application message should be performed or not.
|
||||
/// If set to _False_ the client will not receive the application message.
|
||||
/// </summary>
|
||||
public bool AcceptEnqueue { get; set; } = true;
|
||||
|
||||
public MqttApplicationMessage ApplicationMessage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the connection with the sender should be closed.
|
||||
/// </summary>
|
||||
public bool CloseSenderConnection { get; set; }
|
||||
|
||||
public string ReceiverClientId { get; }
|
||||
|
||||
public string SenderClientId { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// 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;
|
||||
using System.Threading;
|
||||
using MQTTnet.Packets;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class InterceptingPacketEventArgs : EventArgs
|
||||
{
|
||||
public InterceptingPacketEventArgs(CancellationToken cancellationToken, string clientId, string endpoint, MqttPacket packet, IDictionary sessionItems)
|
||||
{
|
||||
CancellationToken = cancellationToken;
|
||||
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
||||
Endpoint = endpoint;
|
||||
Packet = packet ?? throw new ArgumentNullException(nameof(packet));
|
||||
SessionItems = sessionItems;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cancellation token from the connection managing thread.
|
||||
/// Use this in further event processing.
|
||||
/// </summary>
|
||||
public CancellationToken CancellationToken { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the client ID which has sent the packet or will receive the packet.
|
||||
/// </summary>
|
||||
public string ClientId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the endpoint of the sending or receiving client.
|
||||
/// </summary>
|
||||
public string Endpoint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the MQTT packet which was received or will be sent.
|
||||
/// </summary>
|
||||
public MqttPacket Packet { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the packet should be processed or not.
|
||||
/// </summary>
|
||||
public bool ProcessPacket { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
||||
/// </summary>
|
||||
public IDictionary SessionItems { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// 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;
|
||||
using System.Threading;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class InterceptingPublishEventArgs : EventArgs
|
||||
{
|
||||
public InterceptingPublishEventArgs(MqttApplicationMessage applicationMessage, CancellationToken cancellationToken, string clientId, IDictionary sessionItems)
|
||||
{
|
||||
ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage));
|
||||
CancellationToken = cancellationToken;
|
||||
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
||||
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
||||
}
|
||||
|
||||
public MqttApplicationMessage ApplicationMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cancellation token which can indicate that the client connection gets down.
|
||||
/// </summary>
|
||||
public CancellationToken CancellationToken { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the client identifier.
|
||||
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues.
|
||||
/// </summary>
|
||||
public string ClientId { get; }
|
||||
|
||||
public bool CloseConnection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the publish should be processed internally.
|
||||
/// </summary>
|
||||
public bool ProcessPublish { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the response which will be sent to the client via the PUBACK etc. packets.
|
||||
/// </summary>
|
||||
public PublishResponse Response { get; } = new PublishResponse();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
||||
/// </summary>
|
||||
public IDictionary SessionItems { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using MQTTnet.Packets;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class InterceptingSubscriptionEventArgs : EventArgs
|
||||
{
|
||||
public InterceptingSubscriptionEventArgs(
|
||||
CancellationToken cancellationToken,
|
||||
string clientId,
|
||||
MqttSessionStatus session,
|
||||
MqttTopicFilter topicFilter,
|
||||
List<MqttUserProperty> userProperties)
|
||||
{
|
||||
CancellationToken = cancellationToken;
|
||||
ClientId = clientId;
|
||||
Session = session;
|
||||
TopicFilter = topicFilter;
|
||||
UserProperties = userProperties;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cancellation token which can indicate that the client connection gets down.
|
||||
/// </summary>
|
||||
public CancellationToken CancellationToken { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the client identifier.
|
||||
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues.
|
||||
/// </summary>
|
||||
public string ClientId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the broker should close the client connection.
|
||||
/// </summary>
|
||||
public bool CloseConnection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the broker should create an internal subscription for the client.
|
||||
/// The broker can also avoid this and return "success" to the client.
|
||||
/// This feature allows using the MQTT Broker as the Frontend and another system as the backend.
|
||||
/// </summary>
|
||||
public bool ProcessSubscription { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the reason string which will be sent to the client in the SUBACK packet.
|
||||
/// </summary>
|
||||
public string ReasonString { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the response which will be sent to the client via the SUBACK packet.
|
||||
/// </summary>
|
||||
public SubscribeResponse Response { get; } = new SubscribeResponse();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current client session.
|
||||
/// </summary>
|
||||
public MqttSessionStatus Session { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
||||
/// </summary>
|
||||
public IDictionary SessionItems => Session.Items;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the topic filter.
|
||||
/// The topic filter can contain topics and wildcards.
|
||||
/// </summary>
|
||||
public MqttTopicFilter TopicFilter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user properties.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public List<MqttUserProperty> UserProperties { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using MQTTnet.Packets;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class InterceptingUnsubscriptionEventArgs : EventArgs
|
||||
{
|
||||
public InterceptingUnsubscriptionEventArgs(CancellationToken cancellationToken, string clientId, IDictionary sessionItems, string topic, List<MqttUserProperty> userProperties)
|
||||
{
|
||||
CancellationToken = cancellationToken;
|
||||
ClientId = clientId;
|
||||
SessionItems = sessionItems;
|
||||
Topic = topic;
|
||||
UserProperties = userProperties;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cancellation token which can indicate that the client connection gets down.
|
||||
/// </summary>
|
||||
public CancellationToken CancellationToken { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the client identifier.
|
||||
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues.
|
||||
/// </summary>
|
||||
public string ClientId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the broker should close the client connection.
|
||||
/// </summary>
|
||||
public bool CloseConnection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the broker should remove an internal subscription for the client.
|
||||
/// The broker can also avoid this and return "success" to the client.
|
||||
/// This feature allows using the MQTT Broker as the Frontend and another system as the backend.
|
||||
/// </summary>
|
||||
public bool ProcessUnsubscription { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the response which will be sent to the client via the UNSUBACK pocket.
|
||||
/// </summary>
|
||||
public UnsubscribeResponse Response { get; } = new UnsubscribeResponse();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
||||
/// </summary>
|
||||
public IDictionary SessionItems { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the MQTT topic.
|
||||
/// In MQTT, the word topic refers to an UTF-8 string that the broker uses to filter messages for each connected
|
||||
/// client.
|
||||
/// The topic consists of one or more topic levels. Each topic level is separated by a forward slash (topic level
|
||||
/// separator).
|
||||
/// </summary>
|
||||
public string Topic { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user properties.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public List<MqttUserProperty> UserProperties { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// 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;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class LoadingRetainedMessagesEventArgs : EventArgs
|
||||
{
|
||||
public List<MqttApplicationMessage> LoadedRetainedMessages { get; set; } = new List<MqttApplicationMessage>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// 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.Server.Internal;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class PreparingSessionEventArgs : EventArgs
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
// TODO: Allow adding of packets to the queue etc.
|
||||
|
||||
/*
|
||||
* The Session State in the Server consists of:
|
||||
· The existence of a Session, even if the rest of the Session State is empty.
|
||||
· The Clients subscriptions, including any Subscription Identifiers.
|
||||
· QoS 1 and QoS 2 messages which have been sent to the Client, but have not been completely acknowledged.
|
||||
· QoS 1 and QoS 2 messages pending transmission to the Client and OPTIONALLY QoS 0 messages pending transmission to the Client.
|
||||
· QoS 2 messages which have been received from the Client, but have not been completely acknowledged.The Will Message and the Will Delay Interval
|
||||
· If the Session is currently not connected, the time at which the Session will end and Session State will be discarded.
|
||||
*/
|
||||
|
||||
public bool IsExistingSession { get; set; }
|
||||
|
||||
public IDictionary<object, object> Items { get; set; }
|
||||
|
||||
public List<MqttPublishPacket> PublishPackets { get; } = new List<MqttPublishPacket>();
|
||||
|
||||
DateTime? SessionExpiryTimestamp { get; set; }
|
||||
|
||||
public List<MqttSubscription> Subscriptions { get; } = new List<MqttSubscription>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the will delay interval.
|
||||
/// This is the time between the client disconnect and the time the will message will be sent.
|
||||
/// </summary>
|
||||
public uint? WillDelayInterval { get; set; }
|
||||
|
||||
// <summary>
|
||||
// Gets the last will message.
|
||||
// In MQTT, you use the last will message feature to notify other clients about an ungracefully disconnected client.
|
||||
// </summary>
|
||||
// TODO: Use single properties. No entire will message.
|
||||
//MqttApplicationMessage WillMessage { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// 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 MQTTnet.Packets;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class QueueMessageOverwrittenEventArgs : EventArgs
|
||||
{
|
||||
public QueueMessageOverwrittenEventArgs(string receiverClientId, MqttPacket packet)
|
||||
{
|
||||
ReceiverClientId = receiverClientId ?? throw new ArgumentNullException(nameof(receiverClientId));
|
||||
Packet = packet ?? throw new ArgumentNullException(nameof(packet));
|
||||
}
|
||||
|
||||
public MqttPacket Packet { get; }
|
||||
|
||||
public string ReceiverClientId { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// 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;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class RetainedMessageChangedEventArgs : EventArgs
|
||||
{
|
||||
public RetainedMessageChangedEventArgs(string clientId, MqttApplicationMessage changedRetainedMessage, List<MqttApplicationMessage> storedRetainedMessages)
|
||||
{
|
||||
ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
|
||||
ChangedRetainedMessage = changedRetainedMessage ?? throw new ArgumentNullException(nameof(changedRetainedMessage));
|
||||
StoredRetainedMessages = storedRetainedMessages ?? throw new ArgumentNullException(nameof(storedRetainedMessages));
|
||||
}
|
||||
|
||||
public MqttApplicationMessage ChangedRetainedMessage { get; }
|
||||
|
||||
public string ClientId { get; }
|
||||
|
||||
public List<MqttApplicationMessage> StoredRetainedMessages { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// 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;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class SessionDeletedEventArgs : EventArgs
|
||||
{
|
||||
public SessionDeletedEventArgs(string id, IDictionary sessionItems)
|
||||
{
|
||||
Id = id ?? throw new ArgumentNullException(nameof(id));
|
||||
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ID of the session.
|
||||
/// </summary>
|
||||
public string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
||||
/// </summary>
|
||||
public IDictionary SessionItems { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using MQTTnet.Adapter;
|
||||
using MQTTnet.Formatter;
|
||||
using MQTTnet.Internal;
|
||||
using MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class ValidatingConnectionEventArgs : EventArgs
|
||||
{
|
||||
readonly MqttConnectPacket _connectPacket;
|
||||
|
||||
public ValidatingConnectionEventArgs(MqttConnectPacket connectPacket, IMqttChannelAdapter clientAdapter, IDictionary sessionItems)
|
||||
{
|
||||
_connectPacket = connectPacket ?? throw new ArgumentNullException(nameof(connectPacket));
|
||||
ChannelAdapter = clientAdapter ?? throw new ArgumentNullException(nameof(clientAdapter));
|
||||
SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the assigned client identifier.
|
||||
/// MQTTv5 only.
|
||||
/// </summary>
|
||||
public string AssignedClientIdentifier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the authentication data.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public byte[] AuthenticationData => _connectPacket.AuthenticationData;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the authentication method.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public string AuthenticationMethod => _connectPacket.AuthenticationMethod;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the channel adapter. This can be a _MqttConnectionContext_ (used in ASP.NET), a _MqttChannelAdapter_ (used for
|
||||
/// TCP or WebSockets) or a custom implementation.
|
||||
/// </summary>
|
||||
public IMqttChannelAdapter ChannelAdapter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether clean sessions are used or not.
|
||||
/// When a client connects to a broker it can connect using either a non persistent connection (clean session) or a
|
||||
/// persistent connection.
|
||||
/// With a non persistent connection the broker doesn't store any subscription information or undelivered messages for
|
||||
/// the client.
|
||||
/// This mode is ideal when the client only publishes messages.
|
||||
/// It can also connect as a durable client using a persistent connection.
|
||||
/// In this mode, the broker will store subscription information, and undelivered messages for the client.
|
||||
/// </summary>
|
||||
public bool? CleanSession => _connectPacket.CleanSession;
|
||||
|
||||
public X509Certificate2 ClientCertificate => ChannelAdapter.ClientCertificate;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the client identifier.
|
||||
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues.
|
||||
/// </summary>
|
||||
public string ClientId => _connectPacket.ClientId;
|
||||
|
||||
public string Endpoint => ChannelAdapter.Endpoint;
|
||||
|
||||
public bool IsSecureConnection => ChannelAdapter.IsSecureConnection;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the keep alive period.
|
||||
/// The connection is normally left open by the client so that is can send and receive data at any time.
|
||||
/// If no data flows over an open connection for a certain time period then the client will generate a PINGREQ and
|
||||
/// expect to receive a PINGRESP from the broker.
|
||||
/// This message exchange confirms that the connection is open and working.
|
||||
/// This period is known as the keep alive period.
|
||||
/// </summary>
|
||||
public ushort? KeepAlivePeriod => _connectPacket.KeepAlivePeriod;
|
||||
|
||||
/// <summary>
|
||||
/// A value of 0 indicates that the value is not used.
|
||||
/// </summary>
|
||||
public uint MaximumPacketSize => _connectPacket.MaximumPacketSize;
|
||||
|
||||
public string Password => Encoding.UTF8.GetString(RawPassword ?? EmptyBuffer.Array);
|
||||
|
||||
public MqttProtocolVersion ProtocolVersion => ChannelAdapter.PacketFormatterAdapter.ProtocolVersion;
|
||||
|
||||
public byte[] RawPassword => _connectPacket.Password;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the reason code. When a MQTTv3 client connects the enum value must be one which is
|
||||
/// also supported in MQTTv3. Otherwise the connection attempt will fail because not all codes can be
|
||||
/// converted properly.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public MqttConnectReasonCode ReasonCode { get; set; } = MqttConnectReasonCode.Success;
|
||||
|
||||
public string ReasonString { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the receive maximum.
|
||||
/// This gives the maximum length of the receive messages.
|
||||
/// A value of 0 indicates that the value is not used.
|
||||
/// </summary>
|
||||
public ushort ReceiveMaximum => _connectPacket.ReceiveMaximum;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the request problem information.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public bool RequestProblemInformation => _connectPacket.RequestProblemInformation;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the request response information.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public bool RequestResponseInformation => _connectPacket.RequestResponseInformation;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the response authentication data.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public byte[] ResponseAuthenticationData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the response user properties.
|
||||
/// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT
|
||||
/// packet.
|
||||
/// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add
|
||||
/// metadata to MQTT messages and pass information between publisher, broker, and subscriber.
|
||||
/// The feature is very similar to the HTTP header concept.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public List<MqttUserProperty> ResponseUserProperties { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server reference. This can be used together with i.e. "Server Moved" to send
|
||||
/// a different server address to the client.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public string ServerReference { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the session expiry interval.
|
||||
/// The time after a session expires when it's not actively used.
|
||||
/// A value of 0 means no expiation.
|
||||
/// </summary>
|
||||
public uint SessionExpiryInterval => _connectPacket.SessionExpiryInterval;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a key/value collection that can be used to share data within the scope of this session.
|
||||
/// </summary>
|
||||
public IDictionary SessionItems { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the topic alias maximum.
|
||||
/// This gives the maximum length of the topic alias.
|
||||
/// A value of 0 indicates that the value is not used.
|
||||
/// </summary>
|
||||
public ushort TopicAliasMaximum => _connectPacket.TopicAliasMaximum;
|
||||
|
||||
public string UserName => _connectPacket.Username;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public List<MqttUserProperty> UserProperties => _connectPacket.UserProperties;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the will delay interval.
|
||||
/// This is the time between the client disconnect and the time the will message will be sent.
|
||||
/// A value of 0 indicates that the value is not used.
|
||||
/// </summary>
|
||||
public uint WillDelayInterval => _connectPacket.WillDelayInterval;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// 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 MQTTnet.Adapter;
|
||||
using MQTTnet.Diagnostics.Logger;
|
||||
|
||||
namespace MQTTnet.Server;
|
||||
|
||||
public interface IMqttServerAdapter : IDisposable
|
||||
{
|
||||
Func<IMqttChannelAdapter, Task> ClientHandler { get; set; }
|
||||
|
||||
Task StartAsync(MqttServerOptions options, IMqttNetLogger logger);
|
||||
|
||||
Task StopAsync();
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// 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;
|
||||
|
||||
namespace MQTTnet.Server;
|
||||
|
||||
public sealed class InjectedMqttApplicationMessage
|
||||
{
|
||||
public InjectedMqttApplicationMessage(MqttApplicationMessage applicationMessage)
|
||||
{
|
||||
ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage));
|
||||
}
|
||||
|
||||
public MqttApplicationMessage ApplicationMessage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the session items which should be used for all event handlers which are involved in message
|
||||
/// processing.
|
||||
/// If _null_ is specified the singleton session items from the server are used instead.
|
||||
/// </summary>
|
||||
public IDictionary CustomSessionItems { get; set; }
|
||||
|
||||
public string SenderClientId { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// 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.Net;
|
||||
using System.Net.Sockets;
|
||||
using MQTTnet.Adapter;
|
||||
using MQTTnet.Diagnostics.Logger;
|
||||
using MQTTnet.Internal;
|
||||
|
||||
namespace MQTTnet.Server.Internal.Adapter
|
||||
{
|
||||
public sealed class MqttTcpServerAdapter : IMqttServerAdapter
|
||||
{
|
||||
readonly List<MqttTcpServerListener> _listeners = new List<MqttTcpServerListener>();
|
||||
|
||||
CancellationTokenSource _cancellationTokenSource;
|
||||
|
||||
MqttServerOptions _serverOptions;
|
||||
|
||||
public Func<IMqttChannelAdapter, Task> ClientHandler { get; set; }
|
||||
|
||||
public bool TreatSocketOpeningErrorAsWarning { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Cleanup();
|
||||
}
|
||||
|
||||
public Task StartAsync(MqttServerOptions options, IMqttNetLogger logger)
|
||||
{
|
||||
if (_cancellationTokenSource != null)
|
||||
{
|
||||
throw new InvalidOperationException("Server is already started.");
|
||||
}
|
||||
|
||||
_serverOptions = options;
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
if (options.DefaultEndpointOptions.IsEnabled)
|
||||
{
|
||||
RegisterListeners(options.DefaultEndpointOptions, logger, _cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
if (options.TlsEndpointOptions?.IsEnabled == true)
|
||||
{
|
||||
if (options.TlsEndpointOptions.CertificateProvider == null)
|
||||
{
|
||||
throw new ArgumentException("TLS certificate is not set.");
|
||||
}
|
||||
|
||||
RegisterListeners(options.TlsEndpointOptions, logger, _cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
return CompletedTask.Instance;
|
||||
}
|
||||
|
||||
public Task StopAsync()
|
||||
{
|
||||
Cleanup();
|
||||
return CompletedTask.Instance;
|
||||
}
|
||||
|
||||
void Cleanup()
|
||||
{
|
||||
try
|
||||
{
|
||||
_cancellationTokenSource?.Cancel(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_cancellationTokenSource = null;
|
||||
|
||||
foreach (var listener in _listeners)
|
||||
{
|
||||
listener.Dispose();
|
||||
}
|
||||
|
||||
_listeners.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
Task OnClientAcceptedAsync(IMqttChannelAdapter channelAdapter)
|
||||
{
|
||||
var clientHandler = ClientHandler;
|
||||
if (clientHandler == null)
|
||||
{
|
||||
return CompletedTask.Instance;
|
||||
}
|
||||
|
||||
return clientHandler(channelAdapter);
|
||||
}
|
||||
|
||||
void RegisterListeners(MqttServerTcpEndpointBaseOptions tcpEndpointOptions, IMqttNetLogger logger, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!tcpEndpointOptions.BoundInterNetworkAddress.Equals(IPAddress.None))
|
||||
{
|
||||
var listenerV4 = new MqttTcpServerListener(AddressFamily.InterNetwork, _serverOptions, tcpEndpointOptions, logger)
|
||||
{
|
||||
ClientHandler = OnClientAcceptedAsync
|
||||
};
|
||||
|
||||
if (listenerV4.Start(TreatSocketOpeningErrorAsWarning, cancellationToken))
|
||||
{
|
||||
_listeners.Add(listenerV4);
|
||||
}
|
||||
}
|
||||
|
||||
if (!tcpEndpointOptions.BoundInterNetworkV6Address.Equals(IPAddress.None))
|
||||
{
|
||||
var listenerV6 = new MqttTcpServerListener(AddressFamily.InterNetworkV6, _serverOptions, tcpEndpointOptions, logger)
|
||||
{
|
||||
ClientHandler = OnClientAcceptedAsync
|
||||
};
|
||||
|
||||
if (listenerV6.Start(TreatSocketOpeningErrorAsWarning, cancellationToken))
|
||||
{
|
||||
_listeners.Add(listenerV6);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
// 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.Net;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using MQTTnet.Adapter;
|
||||
using MQTTnet.Diagnostics.Logger;
|
||||
using MQTTnet.Formatter;
|
||||
using MQTTnet.Implementations;
|
||||
using MQTTnet.Internal;
|
||||
|
||||
namespace MQTTnet.Server.Internal.Adapter
|
||||
{
|
||||
public sealed class MqttTcpServerListener : IDisposable
|
||||
{
|
||||
readonly MqttNetSourceLogger _logger;
|
||||
readonly IMqttNetLogger _rootLogger;
|
||||
readonly AddressFamily _addressFamily;
|
||||
readonly MqttServerOptions _serverOptions;
|
||||
readonly MqttServerTcpEndpointBaseOptions _options;
|
||||
readonly MqttServerTlsTcpEndpointOptions _tlsOptions;
|
||||
|
||||
CrossPlatformSocket _socket;
|
||||
IPEndPoint _localEndPoint;
|
||||
|
||||
public MqttTcpServerListener(
|
||||
AddressFamily addressFamily,
|
||||
MqttServerOptions serverOptions,
|
||||
MqttServerTcpEndpointBaseOptions tcpEndpointOptions,
|
||||
IMqttNetLogger logger)
|
||||
{
|
||||
_addressFamily = addressFamily;
|
||||
_serverOptions = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions));
|
||||
_options = tcpEndpointOptions ?? throw new ArgumentNullException(nameof(tcpEndpointOptions));
|
||||
_rootLogger = logger;
|
||||
_logger = logger.WithSource(nameof(MqttTcpServerListener));
|
||||
|
||||
if (_options is MqttServerTlsTcpEndpointOptions tlsOptions)
|
||||
{
|
||||
_tlsOptions = tlsOptions;
|
||||
}
|
||||
}
|
||||
|
||||
public Func<IMqttChannelAdapter, Task> ClientHandler { get; set; }
|
||||
|
||||
public bool Start(bool treatErrorsAsWarning, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var boundIp = _options.BoundInterNetworkAddress;
|
||||
if (_addressFamily == AddressFamily.InterNetworkV6)
|
||||
{
|
||||
boundIp = _options.BoundInterNetworkV6Address;
|
||||
}
|
||||
|
||||
_localEndPoint = new IPEndPoint(boundIp, _options.Port);
|
||||
|
||||
_logger.Info("Starting TCP listener (Endpoint={0}, TLS={1})", _localEndPoint, _tlsOptions?.CertificateProvider != null);
|
||||
|
||||
_socket = new CrossPlatformSocket(_addressFamily, ProtocolType.Tcp);
|
||||
|
||||
// Usage of socket options is described here: https://docs.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.setsocketoption?view=netcore-2.2
|
||||
if (_options.ReuseAddress)
|
||||
{
|
||||
_socket.ReuseAddress = true;
|
||||
}
|
||||
|
||||
if (_options.NoDelay)
|
||||
{
|
||||
_socket.NoDelay = true;
|
||||
}
|
||||
|
||||
if (_options.LingerState != null)
|
||||
{
|
||||
_socket.LingerState = _options.LingerState;
|
||||
}
|
||||
|
||||
if (_options.KeepAlive.HasValue)
|
||||
{
|
||||
_socket.KeepAlive = _options.KeepAlive.Value;
|
||||
}
|
||||
|
||||
if (_options.TcpKeepAliveInterval.HasValue)
|
||||
{
|
||||
_socket.TcpKeepAliveInterval = _options.TcpKeepAliveInterval.Value;
|
||||
}
|
||||
|
||||
if (_options.TcpKeepAliveRetryCount.HasValue)
|
||||
{
|
||||
_socket.TcpKeepAliveInterval = _options.TcpKeepAliveRetryCount.Value;
|
||||
}
|
||||
|
||||
if (_options.TcpKeepAliveTime.HasValue)
|
||||
{
|
||||
_socket.TcpKeepAliveTime = _options.TcpKeepAliveTime.Value;
|
||||
}
|
||||
|
||||
_socket.Bind(_localEndPoint);
|
||||
|
||||
// Get the local endpoint back from the socket. The port may have changed.
|
||||
// This can happen when port 0 is used. Then the OS will choose the next free port.
|
||||
_localEndPoint = (IPEndPoint)_socket.LocalEndPoint;
|
||||
_options.Port = _localEndPoint.Port;
|
||||
|
||||
_socket.Listen(_options.ConnectionBacklog);
|
||||
|
||||
_logger.Verbose("TCP listener started (Endpoint={0})", _localEndPoint);
|
||||
|
||||
Task.Run(() => AcceptClientConnectionsAsync(cancellationToken), cancellationToken).RunInBackground(_logger);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
if (!treatErrorsAsWarning)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
_logger.Warning(exception, "Error while starting TCP listener (Endpoint={0})", _localEndPoint);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_socket?.Dispose();
|
||||
}
|
||||
|
||||
async Task AcceptClientConnectionsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var clientSocket = await _socket.AcceptAsync().ConfigureAwait(false);
|
||||
if (clientSocket == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_ = Task.Factory.StartNew(() => TryHandleClientConnectionAsync(clientSocket), cancellationToken, TaskCreationOptions.PreferFairness, TaskScheduler.Default).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
if (exception is SocketException socketException)
|
||||
{
|
||||
if (socketException.SocketErrorCode == SocketError.ConnectionAborted ||
|
||||
socketException.SocketErrorCode == SocketError.OperationAborted)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Error(exception, "Error while accepting TCP connection (Endpoint={0})", _localEndPoint);
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async Task TryHandleClientConnectionAsync(CrossPlatformSocket clientSocket)
|
||||
{
|
||||
Stream stream = null;
|
||||
string remoteEndPoint = null;
|
||||
|
||||
try
|
||||
{
|
||||
remoteEndPoint = clientSocket.RemoteEndPoint.ToString();
|
||||
|
||||
_logger.Verbose("TCP client '{0}' accepted (Local endpoint={1})", remoteEndPoint, _localEndPoint);
|
||||
|
||||
clientSocket.NoDelay = _options.NoDelay;
|
||||
stream = clientSocket.GetStream();
|
||||
var clientCertificate = _tlsOptions?.CertificateProvider?.GetCertificate();
|
||||
|
||||
if (clientCertificate != null)
|
||||
{
|
||||
if (!clientCertificate.HasPrivateKey)
|
||||
{
|
||||
throw new InvalidOperationException("The certificate for TLS encryption must contain the private key.");
|
||||
}
|
||||
|
||||
var sslStream = new SslStream(stream, false, _tlsOptions.RemoteCertificateValidationCallback);
|
||||
|
||||
await sslStream.AuthenticateAsServerAsync(
|
||||
new SslServerAuthenticationOptions
|
||||
{
|
||||
ServerCertificate = clientCertificate,
|
||||
ClientCertificateRequired = _tlsOptions.ClientCertificateRequired,
|
||||
EnabledSslProtocols = _tlsOptions.SslProtocol,
|
||||
CertificateRevocationCheckMode = _tlsOptions.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck,
|
||||
EncryptionPolicy = EncryptionPolicy.RequireEncryption,
|
||||
CipherSuitesPolicy = _tlsOptions.CipherSuitesPolicy
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
stream = sslStream;
|
||||
|
||||
clientCertificate = sslStream.RemoteCertificate as X509Certificate2;
|
||||
|
||||
if (clientCertificate == null && sslStream.RemoteCertificate != null)
|
||||
{
|
||||
clientCertificate = new X509Certificate2(sslStream.RemoteCertificate.Export(X509ContentType.Cert));
|
||||
}
|
||||
}
|
||||
|
||||
var clientHandler = ClientHandler;
|
||||
if (clientHandler != null)
|
||||
{
|
||||
var tcpChannel = new MqttTcpChannel(stream, remoteEndPoint, clientCertificate);
|
||||
var bufferWriter = new MqttBufferWriter(_serverOptions.WriterBufferSize, _serverOptions.WriterBufferSizeMax);
|
||||
var packetFormatterAdapter = new MqttPacketFormatterAdapter(bufferWriter);
|
||||
|
||||
using (var clientAdapter = new MqttChannelAdapter(tcpChannel, packetFormatterAdapter, _rootLogger))
|
||||
{
|
||||
clientAdapter.AllowPacketFragmentation = _options.AllowPacketFragmentation;
|
||||
await clientHandler(clientAdapter).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
if (exception is ObjectDisposedException)
|
||||
{
|
||||
// It can happen that the listener socket is accessed after the cancellation token is already set and the listener socket is disposed.
|
||||
return;
|
||||
}
|
||||
|
||||
if (exception is SocketException socketException &&
|
||||
socketException.SocketErrorCode == SocketError.OperationAborted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Error(exception, "Error while handling TCP client connection");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
// ReSharper disable once MethodHasAsyncOverload
|
||||
stream?.Dispose();
|
||||
clientSocket?.Dispose();
|
||||
}
|
||||
catch (Exception disposeException)
|
||||
{
|
||||
_logger.Error(disposeException, "Error while cleaning up client connection");
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Verbose("TCP client '{0}' disconnected (Local endpoint={1})", remoteEndPoint, _localEndPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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 MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server.Internal
|
||||
{
|
||||
public sealed class CheckSubscriptionsResult
|
||||
{
|
||||
public static CheckSubscriptionsResult NotSubscribed { get; } = new CheckSubscriptionsResult();
|
||||
|
||||
public bool IsSubscribed { get; set; }
|
||||
|
||||
public bool RetainAsPublished { get; set; }
|
||||
|
||||
public List<uint> SubscriptionIdentifiers { get; set; }
|
||||
|
||||
public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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 MQTTnet.Packets;
|
||||
|
||||
namespace MQTTnet.Server.Internal
|
||||
{
|
||||
public sealed class DispatchApplicationMessageResult
|
||||
{
|
||||
public DispatchApplicationMessageResult(int reasonCode, bool closeConnection, string reasonString, List<MqttUserProperty> userProperties)
|
||||
{
|
||||
ReasonCode = reasonCode;
|
||||
CloseConnection = closeConnection;
|
||||
ReasonString = reasonString;
|
||||
UserProperties = userProperties;
|
||||
}
|
||||
|
||||
public bool CloseConnection { get; }
|
||||
|
||||
public int ReasonCode { get; }
|
||||
|
||||
public string ReasonString { get; }
|
||||
|
||||
public List<MqttUserProperty> UserProperties { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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.
|
||||
|
||||
namespace MQTTnet.Server.Internal
|
||||
{
|
||||
public enum EnqueueDataPacketResult
|
||||
{
|
||||
Enqueued,
|
||||
Dropped
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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 MQTTnet.Formatter;
|
||||
using MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server.Internal.Formatter;
|
||||
|
||||
public static class MqttConnAckPacketFactory
|
||||
{
|
||||
public static MqttConnAckPacket Create(ValidatingConnectionEventArgs validatingConnectionEventArgs)
|
||||
{
|
||||
if (validatingConnectionEventArgs == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(validatingConnectionEventArgs));
|
||||
}
|
||||
|
||||
var connAckPacket = new MqttConnAckPacket
|
||||
{
|
||||
ReturnCode = MqttConnectReasonCodeConverter.ToConnectReturnCode(validatingConnectionEventArgs.ReasonCode),
|
||||
ReasonCode = validatingConnectionEventArgs.ReasonCode,
|
||||
RetainAvailable = true,
|
||||
SubscriptionIdentifiersAvailable = true,
|
||||
SharedSubscriptionAvailable = false,
|
||||
TopicAliasMaximum = ushort.MaxValue,
|
||||
MaximumQoS = MqttQualityOfServiceLevel.ExactlyOnce,
|
||||
WildcardSubscriptionAvailable = true,
|
||||
|
||||
AuthenticationMethod = validatingConnectionEventArgs.AuthenticationMethod,
|
||||
AuthenticationData = validatingConnectionEventArgs.ResponseAuthenticationData,
|
||||
AssignedClientIdentifier = validatingConnectionEventArgs.AssignedClientIdentifier,
|
||||
ReasonString = validatingConnectionEventArgs.ReasonString,
|
||||
ServerReference = validatingConnectionEventArgs.ServerReference,
|
||||
UserProperties = validatingConnectionEventArgs.ResponseUserProperties,
|
||||
|
||||
ResponseInformation = null,
|
||||
MaximumPacketSize = 0, // Unlimited,
|
||||
ReceiveMaximum = 0 // Unlimited
|
||||
};
|
||||
|
||||
return connAckPacket;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// 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 MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server.Internal.Formatter;
|
||||
|
||||
public static class MqttDisconnectPacketFactory
|
||||
{
|
||||
static readonly MqttDisconnectPacket DefaultNormalDisconnection = new()
|
||||
{
|
||||
ReasonCode = MqttDisconnectReasonCode.NormalDisconnection,
|
||||
UserProperties = null,
|
||||
ReasonString = null,
|
||||
ServerReference = null,
|
||||
SessionExpiryInterval = 0
|
||||
};
|
||||
|
||||
public static MqttDisconnectPacket Create(MqttServerClientDisconnectOptions clientDisconnectOptions)
|
||||
{
|
||||
if (clientDisconnectOptions == null)
|
||||
{
|
||||
return DefaultNormalDisconnection;
|
||||
}
|
||||
|
||||
return new MqttDisconnectPacket
|
||||
{
|
||||
ReasonCode = clientDisconnectOptions.ReasonCode,
|
||||
UserProperties = clientDisconnectOptions.UserProperties,
|
||||
ReasonString = clientDisconnectOptions.ReasonString,
|
||||
ServerReference = clientDisconnectOptions.ServerReference,
|
||||
SessionExpiryInterval = 0 // TODO: Not yet supported!
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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 MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server.Internal.Formatter;
|
||||
|
||||
public static class MqttPubAckPacketFactory
|
||||
{
|
||||
public static MqttPubAckPacket Create(MqttPublishPacket publishPacket, DispatchApplicationMessageResult dispatchApplicationMessageResult)
|
||||
{
|
||||
if (publishPacket == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(publishPacket));
|
||||
}
|
||||
|
||||
if (dispatchApplicationMessageResult == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(dispatchApplicationMessageResult));
|
||||
}
|
||||
|
||||
var pubAckPacket = new MqttPubAckPacket
|
||||
{
|
||||
PacketIdentifier = publishPacket.PacketIdentifier,
|
||||
ReasonCode = (MqttPubAckReasonCode)dispatchApplicationMessageResult.ReasonCode,
|
||||
ReasonString = dispatchApplicationMessageResult.ReasonString,
|
||||
UserProperties = dispatchApplicationMessageResult.UserProperties
|
||||
};
|
||||
|
||||
return pubAckPacket;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// 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 MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server.Internal.Formatter;
|
||||
|
||||
public static class MqttPubCompPacketFactory
|
||||
{
|
||||
public static MqttPubCompPacket Create(MqttPubRelPacket pubRelPacket, MqttApplicationMessageReceivedReasonCode reasonCode)
|
||||
{
|
||||
if (pubRelPacket == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(pubRelPacket));
|
||||
}
|
||||
|
||||
return new MqttPubCompPacket
|
||||
{
|
||||
PacketIdentifier = pubRelPacket.PacketIdentifier,
|
||||
ReasonCode = (MqttPubCompReasonCode)(int)reasonCode
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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 MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server.Internal.Formatter;
|
||||
|
||||
public static class MqttPubRecPacketFactory
|
||||
{
|
||||
public static MqttPacket Create(MqttPublishPacket publishPacket, DispatchApplicationMessageResult dispatchApplicationMessageResult)
|
||||
{
|
||||
if (publishPacket == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(publishPacket));
|
||||
}
|
||||
|
||||
var pubRecPacket = new MqttPubRecPacket
|
||||
{
|
||||
PacketIdentifier = publishPacket.PacketIdentifier,
|
||||
ReasonCode = (MqttPubRecReasonCode)dispatchApplicationMessageResult.ReasonCode,
|
||||
ReasonString = dispatchApplicationMessageResult.ReasonString,
|
||||
UserProperties = dispatchApplicationMessageResult.UserProperties
|
||||
};
|
||||
|
||||
return pubRecPacket;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// 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 MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server.Internal.Formatter;
|
||||
|
||||
public static class MqttPubRelPacketFactory
|
||||
{
|
||||
public static MqttPubRelPacket Create(MqttPubRecPacket pubRecPacket, MqttApplicationMessageReceivedReasonCode reasonCode)
|
||||
{
|
||||
if (pubRecPacket == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(pubRecPacket));
|
||||
}
|
||||
|
||||
return new MqttPubRelPacket
|
||||
{
|
||||
PacketIdentifier = pubRecPacket.PacketIdentifier,
|
||||
ReasonCode = (MqttPubRelReasonCode)(int)reasonCode
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// 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 MQTTnet.Exceptions;
|
||||
using MQTTnet.Packets;
|
||||
|
||||
namespace MQTTnet.Server.Internal.Formatter;
|
||||
|
||||
public static class MqttPublishPacketFactory
|
||||
{
|
||||
public static MqttPublishPacket Create(MqttConnectPacket connectPacket)
|
||||
{
|
||||
if (connectPacket == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(connectPacket));
|
||||
}
|
||||
|
||||
if (!connectPacket.WillFlag)
|
||||
{
|
||||
throw new MqttProtocolViolationException("The CONNECT packet contains no will message (WillFlag).");
|
||||
}
|
||||
|
||||
ArraySegment<byte> willMessageBuffer = default;
|
||||
if (connectPacket.WillMessage?.Length > 0)
|
||||
{
|
||||
willMessageBuffer = new ArraySegment<byte>(connectPacket.WillMessage);
|
||||
}
|
||||
|
||||
var packet = new MqttPublishPacket
|
||||
{
|
||||
Topic = connectPacket.WillTopic,
|
||||
PayloadSegment = willMessageBuffer,
|
||||
QualityOfServiceLevel = connectPacket.WillQoS,
|
||||
Retain = connectPacket.WillRetain,
|
||||
ContentType = connectPacket.WillContentType,
|
||||
CorrelationData = connectPacket.WillCorrelationData,
|
||||
MessageExpiryInterval = connectPacket.WillMessageExpiryInterval,
|
||||
PayloadFormatIndicator = connectPacket.WillPayloadFormatIndicator,
|
||||
ResponseTopic = connectPacket.WillResponseTopic,
|
||||
UserProperties = connectPacket.WillUserProperties
|
||||
};
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
public static MqttPublishPacket Create(MqttApplicationMessage applicationMessage)
|
||||
{
|
||||
if (applicationMessage == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(applicationMessage));
|
||||
}
|
||||
|
||||
// Copy all values to their matching counterparts.
|
||||
// The not supported values in MQTT 3.1.1 are not serialized (excluded) later.
|
||||
var packet = new MqttPublishPacket
|
||||
{
|
||||
Topic = applicationMessage.Topic,
|
||||
Payload = applicationMessage.Payload,
|
||||
QualityOfServiceLevel = applicationMessage.QualityOfServiceLevel,
|
||||
Retain = applicationMessage.Retain,
|
||||
Dup = applicationMessage.Dup,
|
||||
ContentType = applicationMessage.ContentType,
|
||||
CorrelationData = applicationMessage.CorrelationData,
|
||||
MessageExpiryInterval = applicationMessage.MessageExpiryInterval,
|
||||
PayloadFormatIndicator = applicationMessage.PayloadFormatIndicator,
|
||||
ResponseTopic = applicationMessage.ResponseTopic,
|
||||
TopicAlias = applicationMessage.TopicAlias,
|
||||
SubscriptionIdentifiers = applicationMessage.SubscriptionIdentifiers,
|
||||
UserProperties = applicationMessage.UserProperties
|
||||
};
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
public static MqttPublishPacket Create(MqttRetainedMessageMatch retainedMessage)
|
||||
{
|
||||
if (retainedMessage == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(retainedMessage));
|
||||
}
|
||||
|
||||
var publishPacket = Create(retainedMessage.ApplicationMessage);
|
||||
publishPacket.QualityOfServiceLevel = retainedMessage.SubscriptionQualityOfServiceLevel;
|
||||
return publishPacket;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// 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 MQTTnet.Packets;
|
||||
|
||||
namespace MQTTnet.Server.Internal.Formatter;
|
||||
|
||||
public static class MqttSubAckPacketFactory
|
||||
{
|
||||
public static MqttSubAckPacket Create(MqttSubscribePacket subscribePacket, SubscribeResult subscribeResult)
|
||||
{
|
||||
if (subscribePacket == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(subscribePacket));
|
||||
}
|
||||
|
||||
if (subscribeResult == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(subscribeResult));
|
||||
}
|
||||
|
||||
var subAckPacket = new MqttSubAckPacket
|
||||
{
|
||||
PacketIdentifier = subscribePacket.PacketIdentifier,
|
||||
ReasonCodes = subscribeResult.ReasonCodes,
|
||||
ReasonString = subscribeResult.ReasonString,
|
||||
UserProperties = subscribeResult.UserProperties
|
||||
};
|
||||
|
||||
return subAckPacket;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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 MQTTnet.Packets;
|
||||
|
||||
namespace MQTTnet.Server.Internal.Formatter
|
||||
{
|
||||
public static class MqttUnsubAckPacketFactory
|
||||
{
|
||||
public static MqttUnsubAckPacket Create(MqttUnsubscribePacket unsubscribePacket, UnsubscribeResult unsubscribeResult)
|
||||
{
|
||||
if (unsubscribePacket == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(unsubscribePacket));
|
||||
}
|
||||
|
||||
if (unsubscribeResult == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(unsubscribeResult));
|
||||
}
|
||||
|
||||
var unsubAckPacket = new MqttUnsubAckPacket
|
||||
{
|
||||
PacketIdentifier = unsubscribePacket.PacketIdentifier
|
||||
};
|
||||
|
||||
// MQTTv5.0.0 only.
|
||||
unsubAckPacket.ReasonCodes = unsubscribeResult.ReasonCodes;
|
||||
|
||||
return unsubAckPacket;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace MQTTnet.Server.Internal
|
||||
{
|
||||
public interface ISubscriptionChangedNotification
|
||||
{
|
||||
void OnSubscriptionsAdded(MqttSession clientSession, List<string> subscriptionsTopics);
|
||||
|
||||
void OnSubscriptionsRemoved(MqttSession clientSession, List<string> subscriptionTopics);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,753 @@
|
||||
// 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;
|
||||
using System.Collections.Concurrent;
|
||||
using MQTTnet.Adapter;
|
||||
using MQTTnet.Diagnostics.Logger;
|
||||
using MQTTnet.Exceptions;
|
||||
using MQTTnet.Formatter;
|
||||
using MQTTnet.Internal;
|
||||
using MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
using MQTTnet.Server.Internal.Formatter;
|
||||
using MqttPublishPacketFactory = MQTTnet.Server.Internal.Formatter.MqttPublishPacketFactory;
|
||||
|
||||
namespace MQTTnet.Server.Internal;
|
||||
|
||||
public sealed class MqttClientSessionsManager : ISubscriptionChangedNotification, IDisposable
|
||||
{
|
||||
readonly Dictionary<string, MqttConnectedClient> _clients = new(4096);
|
||||
|
||||
readonly AsyncLock _createConnectionSyncRoot = new();
|
||||
|
||||
readonly MqttServerEventContainer _eventContainer;
|
||||
readonly MqttNetSourceLogger _logger;
|
||||
readonly MqttServerOptions _options;
|
||||
|
||||
readonly MqttRetainedMessagesManager _retainedMessagesManager;
|
||||
readonly IMqttNetLogger _rootLogger;
|
||||
|
||||
readonly ReaderWriterLockSlim _sessionsManagementLock = new();
|
||||
|
||||
// The _sessions dictionary contains all session, the _subscriberSessions hash set contains subscriber sessions only.
|
||||
// See the MqttSubscription object for a detailed explanation.
|
||||
readonly MqttSessionsStorage _sessionsStorage = new();
|
||||
readonly HashSet<MqttSession> _subscriberSessions = new();
|
||||
|
||||
public MqttClientSessionsManager(MqttServerOptions options, MqttRetainedMessagesManager retainedMessagesManager, MqttServerEventContainer eventContainer, IMqttNetLogger logger)
|
||||
{
|
||||
if (logger == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
_logger = logger.WithSource(nameof(MqttClientSessionsManager));
|
||||
_rootLogger = logger;
|
||||
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_retainedMessagesManager = retainedMessagesManager ?? throw new ArgumentNullException(nameof(retainedMessagesManager));
|
||||
_eventContainer = eventContainer ?? throw new ArgumentNullException(nameof(eventContainer));
|
||||
}
|
||||
|
||||
public async Task CloseAllConnections(MqttServerClientDisconnectOptions options)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
List<MqttConnectedClient> connections;
|
||||
lock (_clients)
|
||||
{
|
||||
connections = _clients.Values.ToList();
|
||||
_clients.Clear();
|
||||
}
|
||||
|
||||
foreach (var connection in connections)
|
||||
{
|
||||
await connection.StopAsync(options).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteSessionAsync(string clientId)
|
||||
{
|
||||
_logger.Verbose("Deleting session for client '{0}'.", clientId);
|
||||
|
||||
MqttConnectedClient connection;
|
||||
lock (_clients)
|
||||
{
|
||||
_clients.TryGetValue(clientId, out connection);
|
||||
}
|
||||
|
||||
MqttSession session;
|
||||
_sessionsManagementLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
if (_sessionsStorage.TryRemoveSession(clientId, out session))
|
||||
{
|
||||
_subscriberSessions.Remove(session);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sessionsManagementLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (connection != null)
|
||||
{
|
||||
await connection.StopAsync(new MqttServerClientDisconnectOptions { ReasonCode = MqttDisconnectReasonCode.NormalDisconnection }).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.Error(exception, "Error while deleting session '{0}'", clientId);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_eventContainer.SessionDeletedEvent.HasHandlers && session != null)
|
||||
{
|
||||
var eventArgs = new SessionDeletedEventArgs(clientId, session.Items);
|
||||
await _eventContainer.SessionDeletedEvent.TryInvokeAsync(eventArgs, _logger).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.Error(exception, "Error while executing session deleted event for session '{0}'", clientId);
|
||||
}
|
||||
|
||||
session?.Dispose();
|
||||
|
||||
_logger.Verbose("Session of client '{0}' deleted", clientId);
|
||||
}
|
||||
|
||||
public async Task<DispatchApplicationMessageResult> DispatchApplicationMessage(
|
||||
string senderId,
|
||||
IDictionary senderSessionItems,
|
||||
MqttApplicationMessage applicationMessage,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var processPublish = true;
|
||||
var closeConnection = false;
|
||||
string reasonString = null;
|
||||
List<MqttUserProperty> userProperties = null;
|
||||
var reasonCode = 0; // The reason code is later converted into several different but compatible enums!
|
||||
|
||||
// Allow the user to intercept application message...
|
||||
if (_eventContainer.InterceptingPublishEvent.HasHandlers)
|
||||
{
|
||||
var interceptingPublishEventArgs = new InterceptingPublishEventArgs(applicationMessage, cancellationToken, senderId, senderSessionItems);
|
||||
if (string.IsNullOrEmpty(interceptingPublishEventArgs.ApplicationMessage.Topic))
|
||||
{
|
||||
// This can happen if a topic alias us used but the topic is
|
||||
// unknown to the server.
|
||||
interceptingPublishEventArgs.Response.ReasonCode = MqttPubAckReasonCode.TopicNameInvalid;
|
||||
interceptingPublishEventArgs.ProcessPublish = false;
|
||||
}
|
||||
|
||||
await _eventContainer.InterceptingPublishEvent.InvokeAsync(interceptingPublishEventArgs).ConfigureAwait(false);
|
||||
|
||||
applicationMessage = interceptingPublishEventArgs.ApplicationMessage;
|
||||
closeConnection = interceptingPublishEventArgs.CloseConnection;
|
||||
processPublish = interceptingPublishEventArgs.ProcessPublish;
|
||||
reasonString = interceptingPublishEventArgs.Response.ReasonString;
|
||||
userProperties = interceptingPublishEventArgs.Response.UserProperties;
|
||||
reasonCode = (int)interceptingPublishEventArgs.Response.ReasonCode;
|
||||
}
|
||||
|
||||
// Process the application message...
|
||||
if (processPublish && applicationMessage != null)
|
||||
{
|
||||
var matchingSubscribersCount = 0;
|
||||
try
|
||||
{
|
||||
if (applicationMessage.Retain)
|
||||
{
|
||||
await _retainedMessagesManager.UpdateMessage(senderId, applicationMessage).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
List<MqttSession> subscriberSessions;
|
||||
_sessionsManagementLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
subscriberSessions = _subscriberSessions.ToList();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sessionsManagementLock.ExitReadLock();
|
||||
}
|
||||
|
||||
// Calculate application message topic hash once for subscription checks
|
||||
MqttTopicHash.Calculate(applicationMessage.Topic, out var topicHash, out _, out _);
|
||||
|
||||
foreach (var session in subscriberSessions)
|
||||
{
|
||||
if (!session.TryCheckSubscriptions(applicationMessage.Topic, topicHash, applicationMessage.QualityOfServiceLevel, senderId, out var checkSubscriptionsResult))
|
||||
{
|
||||
// Checking the subscriptions has failed for the session. The session
|
||||
// will be ignored.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!checkSubscriptionsResult.IsSubscribed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_eventContainer.InterceptingClientEnqueueEvent.HasHandlers)
|
||||
{
|
||||
var eventArgs = new InterceptingClientApplicationMessageEnqueueEventArgs(senderId, session.Id, applicationMessage);
|
||||
await _eventContainer.InterceptingClientEnqueueEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
||||
|
||||
if (!eventArgs.AcceptEnqueue)
|
||||
{
|
||||
// Continue checking the other subscriptions
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var publishPacketCopy = MqttPublishPacketFactory.Create(applicationMessage);
|
||||
publishPacketCopy.QualityOfServiceLevel = checkSubscriptionsResult.QualityOfServiceLevel;
|
||||
publishPacketCopy.SubscriptionIdentifiers = checkSubscriptionsResult.SubscriptionIdentifiers;
|
||||
|
||||
if (publishPacketCopy.QualityOfServiceLevel > 0)
|
||||
{
|
||||
publishPacketCopy.PacketIdentifier = session.PacketIdentifierProvider.GetNextPacketIdentifier();
|
||||
}
|
||||
|
||||
if (checkSubscriptionsResult.RetainAsPublished)
|
||||
{
|
||||
// Transfer the original retain state from the publisher. This is a MQTTv5 feature.
|
||||
publishPacketCopy.Retain = applicationMessage.Retain;
|
||||
}
|
||||
else
|
||||
{
|
||||
publishPacketCopy.Retain = false;
|
||||
}
|
||||
|
||||
matchingSubscribersCount++;
|
||||
|
||||
var result = session.EnqueueDataPacket(new MqttPacketBusItem(publishPacketCopy));
|
||||
|
||||
if (_eventContainer.ApplicationMessageEnqueuedOrDroppedEvent.HasHandlers)
|
||||
{
|
||||
var eventArgs = new ApplicationMessageEnqueuedEventArgs(senderId, session.Id, applicationMessage, result == EnqueueDataPacketResult.Dropped);
|
||||
await _eventContainer.ApplicationMessageEnqueuedOrDroppedEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_logger.Verbose("Client '{0}': Queued PUBLISH packet with topic '{1}'", session.Id, applicationMessage.Topic);
|
||||
}
|
||||
|
||||
if (matchingSubscribersCount == 0)
|
||||
{
|
||||
if (reasonCode == (int)MqttPubAckReasonCode.Success)
|
||||
{
|
||||
// Only change the value if it was success. Otherwise, we would hide an error or not authorized status.
|
||||
reasonCode = (int)MqttPubAckReasonCode.NoMatchingSubscribers;
|
||||
}
|
||||
|
||||
await FireApplicationMessageNotConsumedEvent(applicationMessage, senderId).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.Error(exception, "Error while processing next queued application message");
|
||||
}
|
||||
}
|
||||
|
||||
return new DispatchApplicationMessageResult(reasonCode, closeConnection, reasonString, userProperties);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_createConnectionSyncRoot.Dispose();
|
||||
|
||||
_sessionsManagementLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_sessionsStorage.Dispose();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sessionsManagementLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
_sessionsManagementLock?.Dispose();
|
||||
}
|
||||
|
||||
public MqttConnectedClient GetClient(string id)
|
||||
{
|
||||
lock (_clients)
|
||||
{
|
||||
if (!_clients.TryGetValue(id, out var client))
|
||||
{
|
||||
throw new InvalidOperationException($"Client with ID '{id}' not found.");
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
public List<MqttConnectedClient> GetClients()
|
||||
{
|
||||
lock (_clients)
|
||||
{
|
||||
return _clients.Values.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public Task<IList<MqttClientStatus>> GetClientsStatus()
|
||||
{
|
||||
var result = new List<MqttClientStatus>();
|
||||
|
||||
lock (_clients)
|
||||
{
|
||||
foreach (var client in _clients.Values)
|
||||
{
|
||||
var clientStatus = new MqttClientStatus(client)
|
||||
{
|
||||
Session = new MqttSessionStatus(client.Session)
|
||||
};
|
||||
|
||||
result.Add(clientStatus);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult((IList<MqttClientStatus>)result);
|
||||
}
|
||||
|
||||
public Task<IList<MqttSessionStatus>> GetSessionsStatus()
|
||||
{
|
||||
var result = new List<MqttSessionStatus>();
|
||||
|
||||
_sessionsManagementLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var session in _sessionsStorage.ReadAllSessions())
|
||||
{
|
||||
var sessionStatus = new MqttSessionStatus(session);
|
||||
result.Add(sessionStatus);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sessionsManagementLock.ExitReadLock();
|
||||
}
|
||||
|
||||
return Task.FromResult((IList<MqttSessionStatus>)result);
|
||||
}
|
||||
|
||||
public async Task HandleClientConnectionAsync(IMqttChannelAdapter channelAdapter, CancellationToken cancellationToken)
|
||||
{
|
||||
MqttConnectedClient connectedClient = null;
|
||||
|
||||
try
|
||||
{
|
||||
var connectPacket = await ReceiveConnectPacket(channelAdapter, cancellationToken).ConfigureAwait(false);
|
||||
if (connectPacket == null)
|
||||
{
|
||||
// Nothing was received in time etc.
|
||||
return;
|
||||
}
|
||||
|
||||
var validatingConnectionEventArgs = await ValidateConnection(connectPacket, channelAdapter).ConfigureAwait(false);
|
||||
var connAckPacket = MqttConnAckPacketFactory.Create(validatingConnectionEventArgs);
|
||||
|
||||
if (validatingConnectionEventArgs.ReasonCode != MqttConnectReasonCode.Success)
|
||||
{
|
||||
// Send failure response here without preparing a connection and session!
|
||||
await channelAdapter.SendPacketAsync(connAckPacket, cancellationToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass connAckPacket so that IsSessionPresent flag can be set if the client session already exists.
|
||||
connectedClient = await CreateClientConnection(connectPacket, connAckPacket, channelAdapter, validatingConnectionEventArgs).ConfigureAwait(false);
|
||||
|
||||
await connectedClient.SendPacketAsync(connAckPacket, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (_eventContainer.ClientConnectedEvent.HasHandlers)
|
||||
{
|
||||
var eventArgs = new ClientConnectedEventArgs(connectPacket, channelAdapter.PacketFormatterAdapter.ProtocolVersion, channelAdapter.Endpoint, connectedClient.Session.Items);
|
||||
|
||||
await _eventContainer.ClientConnectedEvent.TryInvokeAsync(eventArgs, _logger).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await connectedClient.RunAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.Error(exception, exception.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (connectedClient != null)
|
||||
{
|
||||
if (connectedClient.Id != null)
|
||||
{
|
||||
// in case it is a takeover _clientConnections already contains the new connection
|
||||
if (!connectedClient.IsTakenOver)
|
||||
{
|
||||
lock (_clients)
|
||||
{
|
||||
_clients.Remove(connectedClient.Id);
|
||||
}
|
||||
|
||||
if (!_options.EnablePersistentSessions || !ShouldPersistSession(connectedClient))
|
||||
{
|
||||
await DeleteSessionAsync(connectedClient.Id).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var endpoint = connectedClient.Endpoint;
|
||||
|
||||
if (connectedClient.Id != null && !connectedClient.IsTakenOver && _eventContainer.ClientDisconnectedEvent.HasHandlers)
|
||||
{
|
||||
var disconnectType = connectedClient.DisconnectPacket != null ? MqttClientDisconnectType.Clean : MqttClientDisconnectType.NotClean;
|
||||
var eventArgs = new ClientDisconnectedEventArgs(connectedClient.Id, connectedClient.DisconnectPacket, disconnectType, endpoint, connectedClient.Session.Items);
|
||||
|
||||
await _eventContainer.ClientDisconnectedEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
using (var timeout = new CancellationTokenSource(_options.DefaultCommunicationTimeout))
|
||||
{
|
||||
await channelAdapter.DisconnectAsync(timeout.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnSubscriptionsAdded(MqttSession clientSession, List<string> topics)
|
||||
{
|
||||
_sessionsManagementLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
if (!clientSession.HasSubscribedTopics)
|
||||
{
|
||||
// first subscribed topic
|
||||
_subscriberSessions.Add(clientSession);
|
||||
}
|
||||
|
||||
foreach (var topic in topics)
|
||||
{
|
||||
clientSession.AddSubscribedTopic(topic);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sessionsManagementLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnSubscriptionsRemoved(MqttSession clientSession, List<string> subscriptionTopics)
|
||||
{
|
||||
_sessionsManagementLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
foreach (var subscriptionTopic in subscriptionTopics)
|
||||
{
|
||||
clientSession.RemoveSubscribedTopic(subscriptionTopic);
|
||||
}
|
||||
|
||||
if (!clientSession.HasSubscribedTopics)
|
||||
{
|
||||
// last subscription removed
|
||||
_subscriberSessions.Remove(clientSession);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sessionsManagementLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (!_options.EnablePersistentSessions)
|
||||
{
|
||||
_sessionsStorage.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SubscribeAsync(string clientId, ICollection<MqttTopicFilter> topicFilters)
|
||||
{
|
||||
if (clientId == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(clientId));
|
||||
}
|
||||
|
||||
if (topicFilters == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(topicFilters));
|
||||
}
|
||||
|
||||
var fakeSubscribePacket = new MqttSubscribePacket();
|
||||
fakeSubscribePacket.TopicFilters.AddRange(topicFilters);
|
||||
|
||||
var clientSession = GetClientSession(clientId);
|
||||
|
||||
var subscribeResult = await clientSession.Subscribe(fakeSubscribePacket, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
if (subscribeResult.RetainedMessages != null)
|
||||
{
|
||||
foreach (var retainedMessageMatch in subscribeResult.RetainedMessages)
|
||||
{
|
||||
var publishPacket = MqttPublishPacketFactory.Create(retainedMessageMatch);
|
||||
clientSession.EnqueueDataPacket(new MqttPacketBusItem(publishPacket));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task UnsubscribeAsync(string clientId, ICollection<string> topicFilters)
|
||||
{
|
||||
if (clientId == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(clientId));
|
||||
}
|
||||
|
||||
if (topicFilters == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(topicFilters));
|
||||
}
|
||||
|
||||
var fakeUnsubscribePacket = new MqttUnsubscribePacket();
|
||||
fakeUnsubscribePacket.TopicFilters.AddRange(topicFilters);
|
||||
|
||||
return GetClientSession(clientId).Unsubscribe(fakeUnsubscribePacket, CancellationToken.None);
|
||||
}
|
||||
|
||||
MqttConnectedClient CreateClient(MqttConnectPacket connectPacket, IMqttChannelAdapter channelAdapter, MqttSession session)
|
||||
{
|
||||
return new MqttConnectedClient(connectPacket, channelAdapter, session, _options, _eventContainer, this, _rootLogger);
|
||||
}
|
||||
|
||||
async Task<MqttConnectedClient> CreateClientConnection(
|
||||
MqttConnectPacket connectPacket,
|
||||
MqttConnAckPacket connAckPacket,
|
||||
IMqttChannelAdapter channelAdapter,
|
||||
ValidatingConnectionEventArgs validatingConnectionEventArgs)
|
||||
{
|
||||
MqttConnectedClient connectedClient;
|
||||
|
||||
using (await _createConnectionSyncRoot.EnterAsync().ConfigureAwait(false))
|
||||
{
|
||||
MqttSession oldSession;
|
||||
MqttConnectedClient oldConnectedClient;
|
||||
|
||||
_sessionsManagementLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
MqttSession session;
|
||||
|
||||
// Create a new session (if required).
|
||||
if (!_sessionsStorage.TryGetSession(connectPacket.ClientId, out oldSession))
|
||||
{
|
||||
session = CreateSession(connectPacket, validatingConnectionEventArgs);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (connectPacket.CleanSession)
|
||||
{
|
||||
_logger.Verbose("Deleting existing session of client '{0}' due to clean start", connectPacket.ClientId);
|
||||
_subscriberSessions.Remove(oldSession);
|
||||
session = CreateSession(connectPacket, validatingConnectionEventArgs);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Verbose("Reusing existing session of client '{0}'", connectPacket.ClientId);
|
||||
session = oldSession;
|
||||
oldSession = null;
|
||||
|
||||
session.DisconnectedTimestamp = null;
|
||||
session.Recover();
|
||||
|
||||
connAckPacket.IsSessionPresent = true;
|
||||
}
|
||||
}
|
||||
|
||||
_sessionsStorage.UpdateSession(connectPacket.ClientId, session);
|
||||
|
||||
// Create a new client (always required).
|
||||
lock (_clients)
|
||||
{
|
||||
_clients.TryGetValue(connectPacket.ClientId, out oldConnectedClient);
|
||||
if (oldConnectedClient != null)
|
||||
{
|
||||
// This will stop the current client from sending and receiving but remains the connection active
|
||||
// for a later DISCONNECT packet.
|
||||
oldConnectedClient.IsTakenOver = true;
|
||||
}
|
||||
|
||||
connectedClient = CreateClient(connectPacket, channelAdapter, session);
|
||||
_clients[connectPacket.ClientId] = connectedClient;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sessionsManagementLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
if (!connAckPacket.IsSessionPresent)
|
||||
{
|
||||
// TODO: This event is not yet final. It can already be used but restoring sessions from storage will be added later!
|
||||
var preparingSessionEventArgs = new PreparingSessionEventArgs();
|
||||
await _eventContainer.PreparingSessionEvent.TryInvokeAsync(preparingSessionEventArgs, _logger).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (oldConnectedClient != null)
|
||||
{
|
||||
// TODO: Consider event here for session takeover to allow manipulation of user properties etc.
|
||||
await oldConnectedClient.StopAsync(new MqttServerClientDisconnectOptions { ReasonCode = MqttDisconnectReasonCode.SessionTakenOver }).ConfigureAwait(false);
|
||||
|
||||
if (_eventContainer.ClientDisconnectedEvent.HasHandlers)
|
||||
{
|
||||
var eventArgs = new ClientDisconnectedEventArgs(oldConnectedClient.Id, null, MqttClientDisconnectType.Takeover, oldConnectedClient.Endpoint, oldConnectedClient.Session.Items);
|
||||
|
||||
await _eventContainer.ClientDisconnectedEvent.TryInvokeAsync(eventArgs, _logger).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
oldSession?.Dispose();
|
||||
}
|
||||
|
||||
return connectedClient;
|
||||
}
|
||||
|
||||
MqttSession CreateSession(MqttConnectPacket connectPacket, ValidatingConnectionEventArgs validatingConnectionEventArgs)
|
||||
{
|
||||
_logger.Verbose("Created new session for client '{0}'", connectPacket.ClientId);
|
||||
|
||||
return new MqttSession(connectPacket, validatingConnectionEventArgs.SessionItems, _options, _eventContainer, _retainedMessagesManager, this);
|
||||
}
|
||||
|
||||
async Task FireApplicationMessageNotConsumedEvent(MqttApplicationMessage applicationMessage, string senderId)
|
||||
{
|
||||
if (!_eventContainer.ApplicationMessageNotConsumedEvent.HasHandlers)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var eventArgs = new ApplicationMessageNotConsumedEventArgs(applicationMessage, senderId);
|
||||
await _eventContainer.ApplicationMessageNotConsumedEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
MqttSession GetClientSession(string clientId)
|
||||
{
|
||||
_sessionsManagementLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
if (!_sessionsStorage.TryGetSession(clientId, out var session))
|
||||
{
|
||||
throw new InvalidOperationException($"Client session '{clientId}' is unknown.");
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sessionsManagementLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
async Task<MqttConnectPacket> ReceiveConnectPacket(IMqttChannelAdapter channelAdapter, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var timeoutToken = new CancellationTokenSource(_options.DefaultCommunicationTimeout))
|
||||
using (var effectiveCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(timeoutToken.Token, cancellationToken))
|
||||
{
|
||||
var firstPacket = await channelAdapter.ReceivePacketAsync(effectiveCancellationToken.Token).ConfigureAwait(false);
|
||||
if (firstPacket is MqttConnectPacket connectPacket)
|
||||
{
|
||||
return connectPacket;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.Warning("Client '{0}': Connected but did not sent a CONNECT packet.", channelAdapter.Endpoint);
|
||||
}
|
||||
catch (MqttCommunicationTimedOutException)
|
||||
{
|
||||
_logger.Warning("Client '{0}': Connected but did not sent a CONNECT packet.", channelAdapter.Endpoint);
|
||||
}
|
||||
|
||||
_logger.Warning("Client '{0}': First received packet was no 'CONNECT' packet [MQTT-3.1.0-1].", channelAdapter.Endpoint);
|
||||
return null;
|
||||
}
|
||||
|
||||
static bool ShouldPersistSession(MqttConnectedClient connectedClient)
|
||||
{
|
||||
switch (connectedClient.ChannelAdapter.PacketFormatterAdapter.ProtocolVersion)
|
||||
{
|
||||
case MqttProtocolVersion.V500:
|
||||
{
|
||||
// MQTT 5.0 section 3.1.2.11.2
|
||||
// The Client and Server MUST store the Session State after the Network Connection is closed if the Session Expiry Interval is greater than 0 [MQTT-3.1.2-23].
|
||||
//
|
||||
// A Client that only wants to process messages while connected will set the Clean Start to 1 and set the Session Expiry Interval to 0.
|
||||
// It will not receive Application Messages published before it connected and has to subscribe afresh to any topics that it is interested
|
||||
// in each time it connects.
|
||||
|
||||
var effectiveSessionExpiryInterval = connectedClient.DisconnectPacket?.SessionExpiryInterval ?? 0U;
|
||||
if (effectiveSessionExpiryInterval == 0U)
|
||||
{
|
||||
// From RFC: If the Session Expiry Interval is absent, the Session Expiry Interval in the CONNECT packet is used.
|
||||
effectiveSessionExpiryInterval = connectedClient.ConnectPacket.SessionExpiryInterval;
|
||||
}
|
||||
|
||||
return effectiveSessionExpiryInterval != 0U;
|
||||
}
|
||||
|
||||
case MqttProtocolVersion.V311:
|
||||
{
|
||||
// MQTT 3.1.1 section 3.1.2.4: persist only if 'not CleanSession'
|
||||
//
|
||||
// If CleanSession is set to 1, the Client and Server MUST discard any previous Session and start a new one.
|
||||
// This Session lasts as long as the Network Connection. State data associated with this Session MUST NOT be
|
||||
// reused in any subsequent Session [MQTT-3.1.2-6].
|
||||
|
||||
return !connectedClient.ConnectPacket.CleanSession;
|
||||
}
|
||||
|
||||
case MqttProtocolVersion.V310:
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
async Task<ValidatingConnectionEventArgs> ValidateConnection(MqttConnectPacket connectPacket, IMqttChannelAdapter channelAdapter)
|
||||
{
|
||||
// TODO: Load session items from persisted sessions in the future.
|
||||
var sessionItems = new ConcurrentDictionary<object, object>();
|
||||
var eventArgs = new ValidatingConnectionEventArgs(connectPacket, channelAdapter, sessionItems);
|
||||
await _eventContainer.ValidatingConnectionEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
||||
|
||||
// Check the client ID and set a random one if supported.
|
||||
if (string.IsNullOrEmpty(connectPacket.ClientId) && channelAdapter.PacketFormatterAdapter.ProtocolVersion == MqttProtocolVersion.V500)
|
||||
{
|
||||
connectPacket.ClientId = eventArgs.AssignedClientIdentifier;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(connectPacket.ClientId))
|
||||
{
|
||||
eventArgs.ReasonCode = MqttConnectReasonCode.ClientIdentifierNotValid;
|
||||
}
|
||||
|
||||
return eventArgs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// 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 MQTTnet.Packets;
|
||||
|
||||
namespace MQTTnet.Server.Internal
|
||||
{
|
||||
public sealed class MqttClientStatistics
|
||||
{
|
||||
Statistics _statistics = new Statistics // mutable struct, don't make readonly!
|
||||
{
|
||||
// Start with 1 because the CONNACK packet is not counted here.
|
||||
_receivedPacketsCount = 1,
|
||||
|
||||
// Start with 1 because the CONNECT packet is not counted here.
|
||||
_sentPacketsCount = 1
|
||||
};
|
||||
|
||||
public MqttClientStatistics()
|
||||
{
|
||||
ConnectedTimestamp = DateTime.UtcNow;
|
||||
|
||||
LastPacketReceivedTimestamp = ConnectedTimestamp;
|
||||
LastPacketSentTimestamp = ConnectedTimestamp;
|
||||
|
||||
LastNonKeepAlivePacketReceivedTimestamp = ConnectedTimestamp;
|
||||
}
|
||||
|
||||
public DateTime ConnectedTimestamp { get; }
|
||||
public DateTime LastNonKeepAlivePacketReceivedTimestamp { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp of the last package that has been sent to the client ("received" from the client's perspective)
|
||||
/// </summary>
|
||||
public DateTime LastPacketReceivedTimestamp { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp of the last package that has been received from the client ("sent" from the client's perspective)
|
||||
/// </summary>
|
||||
public DateTime LastPacketSentTimestamp { get; private set; }
|
||||
|
||||
public long SentApplicationMessagesCount => Volatile.Read(ref _statistics._sentApplicationMessagesCount);
|
||||
|
||||
public long ReceivedApplicationMessagesCount => Volatile.Read(ref _statistics._receivedApplicationMessagesCount);
|
||||
|
||||
public long SentPacketsCount => Volatile.Read(ref _statistics._sentPacketsCount);
|
||||
|
||||
public long ReceivedPacketsCount => Volatile.Read(ref _statistics._receivedPacketsCount);
|
||||
|
||||
public void HandleReceivedPacket(MqttPacket packet)
|
||||
{
|
||||
if (packet == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(packet));
|
||||
}
|
||||
|
||||
// This class is tracking all values from Clients perspective!
|
||||
LastPacketSentTimestamp = DateTime.UtcNow;
|
||||
|
||||
Interlocked.Increment(ref _statistics._sentPacketsCount);
|
||||
|
||||
if (packet is MqttPublishPacket)
|
||||
{
|
||||
Interlocked.Increment(ref _statistics._sentApplicationMessagesCount);
|
||||
}
|
||||
|
||||
if (!(packet is MqttPingReqPacket || packet is MqttPingRespPacket))
|
||||
{
|
||||
LastNonKeepAlivePacketReceivedTimestamp = LastPacketReceivedTimestamp;
|
||||
}
|
||||
}
|
||||
public void ResetStatistics() => _statistics.Reset();
|
||||
|
||||
public void HandleSentPacket(MqttPacket packet)
|
||||
{
|
||||
if (packet == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(packet));
|
||||
}
|
||||
|
||||
// This class is tracking all values from Clients perspective!
|
||||
LastPacketReceivedTimestamp = DateTime.UtcNow;
|
||||
|
||||
Interlocked.Increment(ref _statistics._receivedPacketsCount);
|
||||
|
||||
if (packet is MqttPublishPacket)
|
||||
{
|
||||
Interlocked.Increment(ref _statistics._receivedApplicationMessagesCount);
|
||||
}
|
||||
}
|
||||
|
||||
struct Statistics
|
||||
{
|
||||
public long _receivedPacketsCount;
|
||||
public long _sentPacketsCount;
|
||||
|
||||
public long _receivedApplicationMessagesCount;
|
||||
public long _sentApplicationMessagesCount;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Volatile.Write(ref _receivedPacketsCount, 0);
|
||||
Volatile.Write(ref _sentPacketsCount, 0);
|
||||
Volatile.Write(ref _receivedApplicationMessagesCount, 0);
|
||||
Volatile.Write(ref _sentApplicationMessagesCount, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
// 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 MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server.Internal
|
||||
{
|
||||
public sealed class MqttClientSubscriptionsManager : IDisposable
|
||||
{
|
||||
static readonly List<uint> EmptySubscriptionIdentifiers = new List<uint>();
|
||||
|
||||
readonly MqttServerEventContainer _eventContainer;
|
||||
readonly Dictionary<ulong, HashSet<MqttSubscription>> _noWildcardSubscriptionsByTopicHash = new Dictionary<ulong, HashSet<MqttSubscription>>();
|
||||
readonly MqttRetainedMessagesManager _retainedMessagesManager;
|
||||
|
||||
readonly MqttSession _session;
|
||||
|
||||
// Callback to maintain list of subscriber clients
|
||||
readonly ISubscriptionChangedNotification _subscriptionChangedNotification;
|
||||
|
||||
// Subscriptions are stored in various dictionaries and use a "topic hash"; see the MqttSubscription object for a detailed explanation.
|
||||
// The additional lock is important to coordinate complex update logic with multiple steps, checks and interceptors.
|
||||
readonly Dictionary<string, MqttSubscription> _subscriptions = new Dictionary<string, MqttSubscription>();
|
||||
|
||||
// Use subscription lock to maintain consistency across subscriptions and topic hash dictionaries
|
||||
readonly ReaderWriterLockSlim _subscriptionsLock = new ReaderWriterLockSlim();
|
||||
readonly Dictionary<ulong, TopicHashMaskSubscriptions> _wildcardSubscriptionsByTopicHash = new Dictionary<ulong, TopicHashMaskSubscriptions>();
|
||||
|
||||
public MqttClientSubscriptionsManager(
|
||||
MqttSession session,
|
||||
MqttServerEventContainer eventContainer,
|
||||
MqttRetainedMessagesManager retainedMessagesManager,
|
||||
ISubscriptionChangedNotification subscriptionChangedNotification)
|
||||
{
|
||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
_eventContainer = eventContainer ?? throw new ArgumentNullException(nameof(eventContainer));
|
||||
_retainedMessagesManager = retainedMessagesManager ?? throw new ArgumentNullException(nameof(retainedMessagesManager));
|
||||
_subscriptionChangedNotification = subscriptionChangedNotification;
|
||||
}
|
||||
|
||||
public CheckSubscriptionsResult CheckSubscriptions(string topic, ulong topicHash, MqttQualityOfServiceLevel qualityOfServiceLevel, string senderId)
|
||||
{
|
||||
var possibleSubscriptions = new List<MqttSubscription>();
|
||||
|
||||
// Check for possible subscriptions. They might have collisions but this is fine.
|
||||
_subscriptionsLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
if (_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var noWildcardSubscriptions))
|
||||
{
|
||||
possibleSubscriptions.AddRange(noWildcardSubscriptions);
|
||||
}
|
||||
|
||||
foreach (var wcs in _wildcardSubscriptionsByTopicHash)
|
||||
{
|
||||
var subscriptionHash = wcs.Key;
|
||||
var subscriptionsByHashMask = wcs.Value.SubscriptionsByHashMask;
|
||||
foreach (var shm in subscriptionsByHashMask)
|
||||
{
|
||||
var subscriptionHashMask = shm.Key;
|
||||
if ((topicHash & subscriptionHashMask) == subscriptionHash)
|
||||
{
|
||||
var subscriptions = shm.Value;
|
||||
possibleSubscriptions.AddRange(subscriptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_subscriptionsLock.ExitReadLock();
|
||||
}
|
||||
|
||||
// The pre check has evaluated that nothing is subscribed.
|
||||
// If there were some possible candidates they get checked below
|
||||
// again to avoid collisions.
|
||||
if (possibleSubscriptions.Count == 0)
|
||||
{
|
||||
return CheckSubscriptionsResult.NotSubscribed;
|
||||
}
|
||||
|
||||
var senderIsReceiver = string.Equals(senderId, _session.Id);
|
||||
var maxQoSLevel = -1; // Not subscribed.
|
||||
|
||||
HashSet<uint> subscriptionIdentifiers = null;
|
||||
var retainAsPublished = false;
|
||||
|
||||
foreach (var subscription in possibleSubscriptions)
|
||||
{
|
||||
if (subscription.NoLocal && senderIsReceiver)
|
||||
{
|
||||
// This is a MQTTv5 feature!
|
||||
continue;
|
||||
}
|
||||
|
||||
if (MqttTopicFilterComparer.Compare(topic, subscription.Topic) != MqttTopicFilterCompareResult.IsMatch)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (subscription.RetainAsPublished)
|
||||
{
|
||||
// This is a MQTTv5 feature!
|
||||
retainAsPublished = true;
|
||||
}
|
||||
|
||||
if ((int)subscription.GrantedQualityOfServiceLevel > maxQoSLevel)
|
||||
{
|
||||
maxQoSLevel = (int)subscription.GrantedQualityOfServiceLevel;
|
||||
}
|
||||
|
||||
if (subscription.Identifier > 0)
|
||||
{
|
||||
if (subscriptionIdentifiers == null)
|
||||
{
|
||||
subscriptionIdentifiers = new HashSet<uint>();
|
||||
}
|
||||
|
||||
subscriptionIdentifiers.Add(subscription.Identifier);
|
||||
}
|
||||
}
|
||||
|
||||
if (maxQoSLevel == -1)
|
||||
{
|
||||
return CheckSubscriptionsResult.NotSubscribed;
|
||||
}
|
||||
|
||||
var result = new CheckSubscriptionsResult
|
||||
{
|
||||
IsSubscribed = true,
|
||||
RetainAsPublished = retainAsPublished,
|
||||
SubscriptionIdentifiers = subscriptionIdentifiers?.ToList() ?? EmptySubscriptionIdentifiers,
|
||||
|
||||
// Start with the same QoS as the publisher.
|
||||
QualityOfServiceLevel = qualityOfServiceLevel
|
||||
};
|
||||
|
||||
// Now downgrade if required.
|
||||
//
|
||||
// If a subscribing Client has been granted maximum QoS 1 for a particular Topic Filter, then a QoS 0 Application Message matching the filter is delivered
|
||||
// to the Client at QoS 0. This means that at most one copy of the message is received by the Client. On the other hand, a QoS 2 Message published to
|
||||
// the same topic is downgraded by the Server to QoS 1 for delivery to the Client, so that Client might receive duplicate copies of the Message.
|
||||
|
||||
// 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.
|
||||
if (maxQoSLevel < (int)qualityOfServiceLevel)
|
||||
{
|
||||
result.QualityOfServiceLevel = (MqttQualityOfServiceLevel)maxQoSLevel;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_subscriptionsLock?.Dispose();
|
||||
}
|
||||
|
||||
public async Task<SubscribeResult> Subscribe(MqttSubscribePacket subscribePacket, CancellationToken cancellationToken)
|
||||
{
|
||||
if (subscribePacket == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(subscribePacket));
|
||||
}
|
||||
|
||||
var retainedApplicationMessages = await _retainedMessagesManager.GetMessages().ConfigureAwait(false);
|
||||
var result = new SubscribeResult(subscribePacket.TopicFilters.Count);
|
||||
|
||||
var addedSubscriptions = new List<string>();
|
||||
var finalTopicFilters = new List<MqttTopicFilter>();
|
||||
|
||||
// The topic filters are order by its QoS so that the higher QoS will win over a
|
||||
// lower one.
|
||||
foreach (var topicFilterItem in subscribePacket.TopicFilters.OrderByDescending(f => f.QualityOfServiceLevel))
|
||||
{
|
||||
var interceptorEventArgs = await InterceptSubscribe(topicFilterItem, subscribePacket.UserProperties, cancellationToken).ConfigureAwait(false);
|
||||
var topicFilter = interceptorEventArgs.TopicFilter;
|
||||
var processSubscription = interceptorEventArgs.ProcessSubscription && interceptorEventArgs.Response.ReasonCode <= MqttSubscribeReasonCode.GrantedQoS2;
|
||||
|
||||
result.UserProperties = interceptorEventArgs.UserProperties;
|
||||
result.ReasonString = interceptorEventArgs.ReasonString;
|
||||
result.ReasonCodes.Add(interceptorEventArgs.Response.ReasonCode);
|
||||
|
||||
if (interceptorEventArgs.CloseConnection)
|
||||
{
|
||||
// When any of the interceptor calls leads to a connection close the connection
|
||||
// must be closed. So do not revert to false!
|
||||
result.CloseConnection = true;
|
||||
}
|
||||
|
||||
if (!processSubscription || string.IsNullOrEmpty(topicFilter.Topic))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var createSubscriptionResult = CreateSubscription(topicFilter, subscribePacket.SubscriptionIdentifier, interceptorEventArgs.Response.ReasonCode);
|
||||
|
||||
addedSubscriptions.Add(topicFilter.Topic);
|
||||
finalTopicFilters.Add(topicFilter);
|
||||
|
||||
FilterRetainedApplicationMessages(retainedApplicationMessages, createSubscriptionResult, result);
|
||||
}
|
||||
|
||||
// This call will add the new subscription to the internal storage.
|
||||
// So the event _ClientSubscribedTopicEvent_ must be called afterwards.
|
||||
_subscriptionChangedNotification?.OnSubscriptionsAdded(_session, addedSubscriptions);
|
||||
|
||||
if (_eventContainer.ClientSubscribedTopicEvent.HasHandlers)
|
||||
{
|
||||
foreach (var finalTopicFilter in finalTopicFilters)
|
||||
{
|
||||
var eventArgs = new ClientSubscribedTopicEventArgs(_session.Id, finalTopicFilter, _session.Items);
|
||||
await _eventContainer.ClientSubscribedTopicEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<UnsubscribeResult> Unsubscribe(MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken)
|
||||
{
|
||||
if (unsubscribePacket == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(unsubscribePacket));
|
||||
}
|
||||
|
||||
var result = new UnsubscribeResult();
|
||||
|
||||
var removedSubscriptions = new List<string>();
|
||||
|
||||
_subscriptionsLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
foreach (var topicFilter in unsubscribePacket.TopicFilters)
|
||||
{
|
||||
_subscriptions.TryGetValue(topicFilter, out var existingSubscription);
|
||||
|
||||
var interceptorEventArgs = await InterceptUnsubscribe(topicFilter, existingSubscription, unsubscribePacket.UserProperties, cancellationToken).ConfigureAwait(false);
|
||||
var acceptUnsubscription = interceptorEventArgs.Response.ReasonCode == MqttUnsubscribeReasonCode.Success;
|
||||
|
||||
result.UserProperties = interceptorEventArgs.UserProperties;
|
||||
result.ReasonCodes.Add(interceptorEventArgs.Response.ReasonCode);
|
||||
|
||||
if (interceptorEventArgs.CloseConnection)
|
||||
{
|
||||
// When any of the interceptor calls leads to a connection close the connection
|
||||
// must be closed. So do not revert to false!
|
||||
result.CloseConnection = true;
|
||||
}
|
||||
|
||||
if (!acceptUnsubscription)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (interceptorEventArgs.ProcessUnsubscription)
|
||||
{
|
||||
_subscriptions.Remove(topicFilter);
|
||||
|
||||
// must remove subscription object from topic hash dictionary also
|
||||
if (existingSubscription != null)
|
||||
{
|
||||
var topicHash = existingSubscription.TopicHash;
|
||||
|
||||
if (existingSubscription.TopicHasWildcard)
|
||||
{
|
||||
if (_wildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
||||
{
|
||||
subscriptions.RemoveSubscription(existingSubscription);
|
||||
if (subscriptions.SubscriptionsByHashMask.Count == 0)
|
||||
{
|
||||
_wildcardSubscriptionsByTopicHash.Remove(topicHash);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
||||
{
|
||||
subscriptions.Remove(existingSubscription);
|
||||
if (subscriptions.Count == 0)
|
||||
{
|
||||
_noWildcardSubscriptionsByTopicHash.Remove(topicHash);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removedSubscriptions.Add(topicFilter);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_subscriptionsLock.ExitWriteLock();
|
||||
_subscriptionChangedNotification?.OnSubscriptionsRemoved(_session, removedSubscriptions);
|
||||
}
|
||||
|
||||
if (_eventContainer.ClientUnsubscribedTopicEvent.HasHandlers)
|
||||
{
|
||||
foreach (var topicFilter in unsubscribePacket.TopicFilters)
|
||||
{
|
||||
var eventArgs = new ClientUnsubscribedTopicEventArgs(_session.Id, topicFilter, _session.Items);
|
||||
await _eventContainer.ClientUnsubscribedTopicEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
CreateSubscriptionResult CreateSubscription(MqttTopicFilter topicFilter, uint subscriptionIdentifier, MqttSubscribeReasonCode reasonCode)
|
||||
{
|
||||
MqttQualityOfServiceLevel grantedQualityOfServiceLevel;
|
||||
|
||||
if (reasonCode == MqttSubscribeReasonCode.GrantedQoS0)
|
||||
{
|
||||
grantedQualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce;
|
||||
}
|
||||
else if (reasonCode == MqttSubscribeReasonCode.GrantedQoS1)
|
||||
{
|
||||
grantedQualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce;
|
||||
}
|
||||
else if (reasonCode == MqttSubscribeReasonCode.GrantedQoS2)
|
||||
{
|
||||
grantedQualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
var subscription = new MqttSubscription(
|
||||
topicFilter.Topic,
|
||||
topicFilter.NoLocal,
|
||||
topicFilter.RetainHandling,
|
||||
topicFilter.RetainAsPublished,
|
||||
grantedQualityOfServiceLevel,
|
||||
subscriptionIdentifier);
|
||||
|
||||
bool isNewSubscription;
|
||||
|
||||
// Add to subscriptions and maintain topic hash dictionaries
|
||||
|
||||
_subscriptionsLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
MqttTopicHash.Calculate(topicFilter.Topic, out var topicHash, out _, out var hasWildcard);
|
||||
|
||||
if (_subscriptions.TryGetValue(topicFilter.Topic, out var existingSubscription))
|
||||
{
|
||||
// must remove object from topic hash dictionary first
|
||||
if (hasWildcard)
|
||||
{
|
||||
if (_wildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
||||
{
|
||||
subscriptions.RemoveSubscription(existingSubscription);
|
||||
// no need to remove empty entry because we'll be adding subscription again below
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
||||
{
|
||||
subscriptions.Remove(existingSubscription);
|
||||
// no need to remove empty entry because we'll be adding subscription again below
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isNewSubscription = existingSubscription == null;
|
||||
_subscriptions[topicFilter.Topic] = subscription;
|
||||
|
||||
// Add or re-add to topic hash dictionary
|
||||
if (hasWildcard)
|
||||
{
|
||||
if (!_wildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
||||
{
|
||||
subscriptions = new TopicHashMaskSubscriptions();
|
||||
_wildcardSubscriptionsByTopicHash.Add(topicHash, subscriptions);
|
||||
}
|
||||
|
||||
subscriptions.AddSubscription(subscription);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions))
|
||||
{
|
||||
subscriptions = new HashSet<MqttSubscription>();
|
||||
_noWildcardSubscriptionsByTopicHash.Add(topicHash, subscriptions);
|
||||
}
|
||||
|
||||
subscriptions.Add(subscription);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_subscriptionsLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
return new CreateSubscriptionResult
|
||||
{
|
||||
IsNewSubscription = isNewSubscription,
|
||||
Subscription = subscription
|
||||
};
|
||||
}
|
||||
|
||||
static void FilterRetainedApplicationMessages(
|
||||
IList<MqttApplicationMessage> retainedMessages,
|
||||
CreateSubscriptionResult createSubscriptionResult,
|
||||
SubscribeResult subscribeResult)
|
||||
{
|
||||
if (createSubscriptionResult.Subscription.RetainHandling == MqttRetainHandling.DoNotSendOnSubscribe)
|
||||
{
|
||||
// This is a MQTT V5+ feature.
|
||||
return;
|
||||
}
|
||||
|
||||
if (createSubscriptionResult.Subscription.RetainHandling == MqttRetainHandling.SendAtSubscribeIfNewSubscriptionOnly && !createSubscriptionResult.IsNewSubscription)
|
||||
{
|
||||
// This is a MQTT V5+ feature.
|
||||
return;
|
||||
}
|
||||
|
||||
for (var index = retainedMessages.Count - 1; index >= 0; index--)
|
||||
{
|
||||
var retainedMessage = retainedMessages[index];
|
||||
if (retainedMessage == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (MqttTopicFilterComparer.Compare(retainedMessage.Topic, createSubscriptionResult.Subscription.Topic) != MqttTopicFilterCompareResult.IsMatch)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var retainedMessageMatch = new MqttRetainedMessageMatch(retainedMessage, createSubscriptionResult.Subscription.GrantedQualityOfServiceLevel);
|
||||
if (retainedMessageMatch.SubscriptionQualityOfServiceLevel > retainedMessageMatch.ApplicationMessage.QualityOfServiceLevel)
|
||||
{
|
||||
// UPGRADING the QoS is not allowed!
|
||||
// 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)
|
||||
{
|
||||
subscribeResult.RetainedMessages = new List<MqttRetainedMessageMatch>();
|
||||
}
|
||||
|
||||
subscribeResult.RetainedMessages.Add(retainedMessageMatch);
|
||||
|
||||
// Clear the retained message from the list because the client should receive every message only
|
||||
// one time even if multiple subscriptions affect them.
|
||||
retainedMessages[index] = null;
|
||||
}
|
||||
}
|
||||
|
||||
async Task<InterceptingSubscriptionEventArgs> InterceptSubscribe(
|
||||
MqttTopicFilter topicFilter,
|
||||
List<MqttUserProperty> userProperties,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var eventArgs = new InterceptingSubscriptionEventArgs(cancellationToken, _session.Id, new MqttSessionStatus(_session), topicFilter, userProperties);
|
||||
|
||||
if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce)
|
||||
{
|
||||
eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.GrantedQoS0;
|
||||
}
|
||||
else if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtLeastOnce)
|
||||
{
|
||||
eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.GrantedQoS1;
|
||||
}
|
||||
else if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.ExactlyOnce)
|
||||
{
|
||||
eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.GrantedQoS2;
|
||||
}
|
||||
|
||||
if (topicFilter.Topic.StartsWith("$share/"))
|
||||
{
|
||||
eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.SharedSubscriptionsNotSupported;
|
||||
}
|
||||
else
|
||||
{
|
||||
await _eventContainer.InterceptingSubscriptionEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return eventArgs;
|
||||
}
|
||||
|
||||
async Task<InterceptingUnsubscriptionEventArgs> InterceptUnsubscribe(
|
||||
string topicFilter,
|
||||
MqttSubscription mqttSubscription,
|
||||
List<MqttUserProperty> userProperties,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var clientUnsubscribingTopicEventArgs = new InterceptingUnsubscriptionEventArgs(cancellationToken, _session.Id, _session.Items, topicFilter, userProperties)
|
||||
{
|
||||
Response =
|
||||
{
|
||||
ReasonCode = mqttSubscription == null ? MqttUnsubscribeReasonCode.NoSubscriptionExisted : MqttUnsubscribeReasonCode.Success
|
||||
}
|
||||
};
|
||||
|
||||
await _eventContainer.InterceptingUnsubscriptionEvent.InvokeAsync(clientUnsubscribingTopicEventArgs).ConfigureAwait(false);
|
||||
|
||||
return clientUnsubscribingTopicEventArgs;
|
||||
}
|
||||
|
||||
sealed class CreateSubscriptionResult
|
||||
{
|
||||
public bool IsNewSubscription { get; set; }
|
||||
|
||||
public MqttSubscription Subscription { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,576 @@
|
||||
// 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 MQTTnet.Adapter;
|
||||
using MQTTnet.Diagnostics.Logger;
|
||||
using MQTTnet.Exceptions;
|
||||
using MQTTnet.Formatter;
|
||||
using MQTTnet.Internal;
|
||||
using MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
using MQTTnet.Server.Internal.Formatter;
|
||||
using MqttDisconnectPacketFactory = MQTTnet.Server.Internal.Formatter.MqttDisconnectPacketFactory;
|
||||
using MqttPubAckPacketFactory = MQTTnet.Server.Internal.Formatter.MqttPubAckPacketFactory;
|
||||
using MqttPubCompPacketFactory = MQTTnet.Server.Internal.Formatter.MqttPubCompPacketFactory;
|
||||
using MqttPublishPacketFactory = MQTTnet.Server.Internal.Formatter.MqttPublishPacketFactory;
|
||||
using MqttPubRecPacketFactory = MQTTnet.Server.Internal.Formatter.MqttPubRecPacketFactory;
|
||||
using MqttPubRelPacketFactory = MQTTnet.Server.Internal.Formatter.MqttPubRelPacketFactory;
|
||||
|
||||
namespace MQTTnet.Server.Internal;
|
||||
|
||||
public sealed class MqttConnectedClient : IDisposable
|
||||
{
|
||||
readonly MqttServerEventContainer _eventContainer;
|
||||
readonly MqttNetSourceLogger _logger;
|
||||
readonly MqttServerOptions _serverOptions;
|
||||
readonly MqttClientSessionsManager _sessionsManager;
|
||||
readonly Dictionary<ushort, string> _topicAlias = new();
|
||||
|
||||
CancellationTokenSource _cancellationToken = new();
|
||||
bool _disconnectPacketSent;
|
||||
|
||||
public MqttConnectedClient(
|
||||
MqttConnectPacket connectPacket,
|
||||
IMqttChannelAdapter channelAdapter,
|
||||
MqttSession session,
|
||||
MqttServerOptions serverOptions,
|
||||
MqttServerEventContainer eventContainer,
|
||||
MqttClientSessionsManager sessionsManager,
|
||||
IMqttNetLogger logger)
|
||||
{
|
||||
_serverOptions = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions));
|
||||
_eventContainer = eventContainer ?? throw new ArgumentNullException(nameof(eventContainer));
|
||||
_sessionsManager = sessionsManager ?? throw new ArgumentNullException(nameof(sessionsManager));
|
||||
ConnectPacket = connectPacket ?? throw new ArgumentNullException(nameof(connectPacket));
|
||||
|
||||
ChannelAdapter = channelAdapter ?? throw new ArgumentNullException(nameof(channelAdapter));
|
||||
Endpoint = channelAdapter.Endpoint;
|
||||
Session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
|
||||
if (logger == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
_logger = logger.WithSource(nameof(MqttConnectedClient));
|
||||
}
|
||||
|
||||
public IMqttChannelAdapter ChannelAdapter { get; }
|
||||
|
||||
public MqttConnectPacket ConnectPacket { get; }
|
||||
|
||||
public MqttDisconnectPacket DisconnectPacket { get; private set; }
|
||||
|
||||
public string Endpoint { get; }
|
||||
|
||||
public string Id => ConnectPacket.ClientId;
|
||||
|
||||
public bool IsRunning { get; private set; }
|
||||
|
||||
public bool IsTakenOver { get; set; }
|
||||
|
||||
public MqttSession Session { get; }
|
||||
|
||||
public MqttClientStatistics Statistics { get; } = new();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cancellationToken?.Dispose();
|
||||
}
|
||||
|
||||
public void ResetStatistics()
|
||||
{
|
||||
ChannelAdapter.ResetStatistics();
|
||||
Statistics.ResetStatistics();
|
||||
}
|
||||
|
||||
public async Task RunAsync()
|
||||
{
|
||||
_logger.Info("Client '{0}': Session started", Id);
|
||||
|
||||
Session.LatestConnectPacket = ConnectPacket;
|
||||
Session.WillMessageSent = false;
|
||||
|
||||
try
|
||||
{
|
||||
var cancellationToken = _cancellationToken.Token;
|
||||
IsRunning = true;
|
||||
|
||||
_ = Task.Factory.StartNew(() => SendPacketsLoop(cancellationToken), cancellationToken, TaskCreationOptions.PreferFairness, TaskScheduler.Default).ConfigureAwait(false);
|
||||
|
||||
await ReceivePackagesLoop(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsRunning = false;
|
||||
|
||||
Session.DisconnectedTimestamp = DateTime.UtcNow;
|
||||
|
||||
_cancellationToken?.TryCancel();
|
||||
_cancellationToken?.Dispose();
|
||||
_cancellationToken = null;
|
||||
}
|
||||
|
||||
var isCleanDisconnect = DisconnectPacket != null;
|
||||
|
||||
if (!IsTakenOver && !isCleanDisconnect && Session.LatestConnectPacket.WillFlag && !Session.WillMessageSent)
|
||||
{
|
||||
var willPublishPacket = MqttPublishPacketFactory.Create(Session.LatestConnectPacket);
|
||||
var willApplicationMessage = MqttApplicationMessageFactory.Create(willPublishPacket);
|
||||
|
||||
_ = _sessionsManager.DispatchApplicationMessage(Id, Session.Items, willApplicationMessage, CancellationToken.None);
|
||||
Session.WillMessageSent = true;
|
||||
|
||||
_logger.Info("Client '{0}': Published will message", Id);
|
||||
}
|
||||
|
||||
_logger.Info("Client '{0}': Connection stopped", Id);
|
||||
}
|
||||
|
||||
public async Task SendPacketAsync(MqttPacket packet, CancellationToken cancellationToken)
|
||||
{
|
||||
packet = await InterceptPacketAsync(packet, cancellationToken).ConfigureAwait(false);
|
||||
if (packet == null)
|
||||
{
|
||||
// The interceptor has decided that this packet will not used at all.
|
||||
// This might break the protocol but the user wants that.
|
||||
return;
|
||||
}
|
||||
|
||||
await ChannelAdapter.SendPacketAsync(packet, cancellationToken).ConfigureAwait(false);
|
||||
Statistics.HandleSentPacket(packet);
|
||||
}
|
||||
|
||||
public async Task StopAsync(MqttServerClientDisconnectOptions disconnectOptions)
|
||||
{
|
||||
IsRunning = false;
|
||||
|
||||
if (!_disconnectPacketSent)
|
||||
{
|
||||
// // Sending DISCONNECT packets from the server to the client is only supported when using MQTTv5+.
|
||||
if (ChannelAdapter.PacketFormatterAdapter.ProtocolVersion == MqttProtocolVersion.V500)
|
||||
{
|
||||
// From RFC: The Client or Server MAY send a DISCONNECT packet before closing the Network Connection.
|
||||
// This library does not sent a DISCONNECT packet for a normal disconnection.
|
||||
// TODO: Maybe adding a configuration option is requested in the future.
|
||||
if (disconnectOptions != null)
|
||||
{
|
||||
if (disconnectOptions.ReasonCode != MqttDisconnectReasonCode.NormalDisconnection || disconnectOptions.UserProperties?.Any() == true ||
|
||||
!string.IsNullOrEmpty(disconnectOptions.ReasonString) || !string.IsNullOrEmpty(disconnectOptions.ServerReference))
|
||||
{
|
||||
// It is very important to send the DISCONNECT packet here BEFORE cancelling the
|
||||
// token because the entire connection is closed (disposed) as soon as the cancellation
|
||||
// token is cancelled. To there is no chance that the DISCONNECT packet will ever arrive
|
||||
// at the client!
|
||||
await TrySendDisconnectPacket(disconnectOptions).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StopInternal();
|
||||
}
|
||||
|
||||
Task ClientAcknowledgedPublishPacket(MqttPublishPacket publishPacket, MqttPacketWithIdentifier acknowledgePacket)
|
||||
{
|
||||
if (_eventContainer.ClientAcknowledgedPublishPacketEvent.HasHandlers)
|
||||
{
|
||||
var eventArgs = new ClientAcknowledgedPublishPacketEventArgs(Id, Session.Items, publishPacket, acknowledgePacket);
|
||||
return _eventContainer.ClientAcknowledgedPublishPacketEvent.TryInvokeAsync(eventArgs, _logger);
|
||||
}
|
||||
|
||||
return CompletedTask.Instance;
|
||||
}
|
||||
|
||||
void HandleIncomingPingReqPacket()
|
||||
{
|
||||
// See: The Server MUST send a PINGRESP packet in response to a PINGREQ packet [MQTT-3.12.4-1].
|
||||
Session.EnqueueHealthPacket(new MqttPacketBusItem(MqttPingRespPacket.Instance));
|
||||
}
|
||||
|
||||
Task HandleIncomingPubAckPacket(MqttPubAckPacket pubAckPacket)
|
||||
{
|
||||
var acknowledgedPublishPacket = Session.AcknowledgePublishPacket(pubAckPacket.PacketIdentifier);
|
||||
|
||||
if (acknowledgedPublishPacket != null)
|
||||
{
|
||||
return ClientAcknowledgedPublishPacket(acknowledgedPublishPacket, pubAckPacket);
|
||||
}
|
||||
|
||||
return CompletedTask.Instance;
|
||||
}
|
||||
|
||||
Task HandleIncomingPubCompPacket(MqttPubCompPacket pubCompPacket)
|
||||
{
|
||||
var acknowledgedPublishPacket = Session.AcknowledgePublishPacket(pubCompPacket.PacketIdentifier);
|
||||
|
||||
if (acknowledgedPublishPacket != null)
|
||||
{
|
||||
return ClientAcknowledgedPublishPacket(acknowledgedPublishPacket, pubCompPacket);
|
||||
}
|
||||
|
||||
return CompletedTask.Instance;
|
||||
}
|
||||
|
||||
async Task HandleIncomingPublishPacket(MqttPublishPacket publishPacket, CancellationToken cancellationToken)
|
||||
{
|
||||
HandleTopicAlias(publishPacket);
|
||||
|
||||
var applicationMessage = MqttApplicationMessageFactory.Create(publishPacket);
|
||||
|
||||
var dispatchApplicationMessageResult = await _sessionsManager.DispatchApplicationMessage(Id, Session.Items, applicationMessage, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (dispatchApplicationMessageResult.CloseConnection)
|
||||
{
|
||||
await StopAsync(new MqttServerClientDisconnectOptions { ReasonCode = MqttDisconnectReasonCode.UnspecifiedError });
|
||||
return;
|
||||
}
|
||||
|
||||
switch (publishPacket.QualityOfServiceLevel)
|
||||
{
|
||||
case MqttQualityOfServiceLevel.AtMostOnce:
|
||||
{
|
||||
// Do nothing since QoS 0 has no ACK at all!
|
||||
break;
|
||||
}
|
||||
case MqttQualityOfServiceLevel.AtLeastOnce:
|
||||
{
|
||||
var pubAckPacket = MqttPubAckPacketFactory.Create(publishPacket, dispatchApplicationMessageResult);
|
||||
Session.EnqueueControlPacket(new MqttPacketBusItem(pubAckPacket));
|
||||
break;
|
||||
}
|
||||
case MqttQualityOfServiceLevel.ExactlyOnce:
|
||||
{
|
||||
var pubRecPacket = MqttPubRecPacketFactory.Create(publishPacket, dispatchApplicationMessageResult);
|
||||
Session.EnqueueControlPacket(new MqttPacketBusItem(pubRecPacket));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
throw new MqttCommunicationException("Received a not supported QoS level");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Task HandleIncomingPubRecPacket(MqttPubRecPacket pubRecPacket)
|
||||
{
|
||||
// Do not fire the event _ClientAcknowledgedPublishPacket_ here because the QoS 2 process is only finished
|
||||
// properly when the client has sent the PUBCOMP packet.
|
||||
var pubRelPacket = MqttPubRelPacketFactory.Create(pubRecPacket, MqttApplicationMessageReceivedReasonCode.Success);
|
||||
Session.EnqueueControlPacket(new MqttPacketBusItem(pubRelPacket));
|
||||
|
||||
return CompletedTask.Instance;
|
||||
}
|
||||
|
||||
void HandleIncomingPubRelPacket(MqttPubRelPacket pubRelPacket)
|
||||
{
|
||||
var pubCompPacket = MqttPubCompPacketFactory.Create(pubRelPacket, MqttApplicationMessageReceivedReasonCode.Success);
|
||||
Session.EnqueueControlPacket(new MqttPacketBusItem(pubCompPacket));
|
||||
}
|
||||
|
||||
async Task HandleIncomingSubscribePacket(MqttSubscribePacket subscribePacket, CancellationToken cancellationToken)
|
||||
{
|
||||
var subscribeResult = await Session.Subscribe(subscribePacket, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var subAckPacket = MqttSubAckPacketFactory.Create(subscribePacket, subscribeResult);
|
||||
|
||||
Session.EnqueueControlPacket(new MqttPacketBusItem(subAckPacket));
|
||||
|
||||
if (subscribeResult.CloseConnection)
|
||||
{
|
||||
StopInternal();
|
||||
return;
|
||||
}
|
||||
|
||||
if (subscribeResult.RetainedMessages != null)
|
||||
{
|
||||
foreach (var retainedMessageMatch in subscribeResult.RetainedMessages)
|
||||
{
|
||||
var publishPacket = MqttPublishPacketFactory.Create(retainedMessageMatch);
|
||||
Session.EnqueueDataPacket(new MqttPacketBusItem(publishPacket));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async Task HandleIncomingUnsubscribePacket(MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken)
|
||||
{
|
||||
var unsubscribeResult = await Session.Unsubscribe(unsubscribePacket, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var unsubAckPacket = MqttUnsubAckPacketFactory.Create(unsubscribePacket, unsubscribeResult);
|
||||
|
||||
Session.EnqueueControlPacket(new MqttPacketBusItem(unsubAckPacket));
|
||||
|
||||
if (unsubscribeResult.CloseConnection)
|
||||
{
|
||||
StopInternal();
|
||||
}
|
||||
}
|
||||
|
||||
void HandleTopicAlias(MqttPublishPacket publishPacket)
|
||||
{
|
||||
if (publishPacket.TopicAlias == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_topicAlias)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(publishPacket.Topic))
|
||||
{
|
||||
_topicAlias[publishPacket.TopicAlias] = publishPacket.Topic;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_topicAlias.TryGetValue(publishPacket.TopicAlias, out var topic))
|
||||
{
|
||||
publishPacket.Topic = topic;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warning("Client '{0}': Received invalid topic alias ({1})", Id, publishPacket.TopicAlias);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async Task<MqttPacket> InterceptPacketAsync(MqttPacket packet, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_eventContainer.InterceptingOutboundPacketEvent.HasHandlers)
|
||||
{
|
||||
return packet;
|
||||
}
|
||||
|
||||
var interceptingPacketEventArgs = new InterceptingPacketEventArgs(cancellationToken, Id, Endpoint, packet, Session.Items);
|
||||
await _eventContainer.InterceptingOutboundPacketEvent.InvokeAsync(interceptingPacketEventArgs).ConfigureAwait(false);
|
||||
|
||||
if (!interceptingPacketEventArgs.ProcessPacket || packet == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return interceptingPacketEventArgs.Packet;
|
||||
}
|
||||
|
||||
async Task ReceivePackagesLoop(CancellationToken cancellationToken)
|
||||
{
|
||||
MqttPacket currentPacket = null;
|
||||
try
|
||||
{
|
||||
// We do not listen for the cancellation token here because the internal buffer might still
|
||||
// contain data to be read even if the TCP connection was already dropped. So we rely on an
|
||||
// own exception in the reading loop!
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Yield();
|
||||
|
||||
currentPacket = await ChannelAdapter.ReceivePacketAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (currentPacket == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for cancellation again because receive packet might block some time.
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// The TCP connection of this client may be still open but the client has already been taken over by
|
||||
// a new TCP connection. So we must exit here to make sure to no longer process any message.
|
||||
if (IsTakenOver || !IsRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var processPacket = true;
|
||||
|
||||
if (_eventContainer.InterceptingInboundPacketEvent.HasHandlers)
|
||||
{
|
||||
var interceptingPacketEventArgs = new InterceptingPacketEventArgs(cancellationToken, Id, Endpoint, currentPacket, Session.Items);
|
||||
await _eventContainer.InterceptingInboundPacketEvent.InvokeAsync(interceptingPacketEventArgs).ConfigureAwait(false);
|
||||
currentPacket = interceptingPacketEventArgs.Packet;
|
||||
processPacket = interceptingPacketEventArgs.ProcessPacket;
|
||||
}
|
||||
|
||||
if (!processPacket || currentPacket == null)
|
||||
{
|
||||
// Restart the receiving process to get the next packet ignoring the current one..
|
||||
continue;
|
||||
}
|
||||
|
||||
Statistics.HandleReceivedPacket(currentPacket);
|
||||
|
||||
if (currentPacket is MqttPublishPacket publishPacket)
|
||||
{
|
||||
await HandleIncomingPublishPacket(publishPacket, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if (currentPacket is MqttPubAckPacket pubAckPacket)
|
||||
{
|
||||
await HandleIncomingPubAckPacket(pubAckPacket).ConfigureAwait(false);
|
||||
}
|
||||
else if (currentPacket is MqttPubCompPacket pubCompPacket)
|
||||
{
|
||||
await HandleIncomingPubCompPacket(pubCompPacket).ConfigureAwait(false);
|
||||
}
|
||||
else if (currentPacket is MqttPubRecPacket pubRecPacket)
|
||||
{
|
||||
await HandleIncomingPubRecPacket(pubRecPacket).ConfigureAwait(false);
|
||||
}
|
||||
else if (currentPacket is MqttPubRelPacket pubRelPacket)
|
||||
{
|
||||
HandleIncomingPubRelPacket(pubRelPacket);
|
||||
}
|
||||
else if (currentPacket is MqttSubscribePacket subscribePacket)
|
||||
{
|
||||
await HandleIncomingSubscribePacket(subscribePacket, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if (currentPacket is MqttUnsubscribePacket unsubscribePacket)
|
||||
{
|
||||
await HandleIncomingUnsubscribePacket(unsubscribePacket, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if (currentPacket is MqttPingReqPacket)
|
||||
{
|
||||
HandleIncomingPingReqPacket();
|
||||
}
|
||||
else if (currentPacket is MqttPingRespPacket)
|
||||
{
|
||||
throw new MqttProtocolViolationException("A PINGRESP Packet is sent by the Server to the Client in response to a PINGREQ Packet only.");
|
||||
}
|
||||
else if (currentPacket is MqttDisconnectPacket disconnectPacket)
|
||||
{
|
||||
DisconnectPacket = disconnectPacket;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new MqttProtocolViolationException("Packet not allowed");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
if (exception is MqttCommunicationException)
|
||||
{
|
||||
_logger.Warning(exception, "Client '{0}': Communication exception while receiving packets", Id);
|
||||
return;
|
||||
}
|
||||
|
||||
var logLevel = MqttNetLogLevel.Error;
|
||||
|
||||
if (!IsRunning)
|
||||
{
|
||||
// There was an exception but the connection is already closed. So there is no chance to send a response to the client etc.
|
||||
logLevel = MqttNetLogLevel.Warning;
|
||||
}
|
||||
|
||||
if (currentPacket == null)
|
||||
{
|
||||
_logger.Publish(logLevel, exception, "Client '{0}': Error while receiving packets", Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Publish(logLevel, exception, "Client '{0}': Error while processing {1} packet", Id, currentPacket.GetRfcName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async Task SendPacketsLoop(CancellationToken cancellationToken)
|
||||
{
|
||||
MqttPacketBusItem packetBusItem = null;
|
||||
|
||||
try
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested && !IsTakenOver && IsRunning)
|
||||
{
|
||||
packetBusItem = await Session.DequeuePacketAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Also check the cancellation token here because the dequeue is blocking and may take some time.
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsTakenOver || !IsRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await SendPacketAsync(packetBusItem.Packet, cancellationToken).ConfigureAwait(false);
|
||||
packetBusItem.Complete();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
packetBusItem.Cancel();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
packetBusItem.Fail(exception);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await Task.Yield();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
if (exception is MqttCommunicationTimedOutException)
|
||||
{
|
||||
_logger.Warning(exception, "Client '{0}': Sending PUBLISH packet failed due to timeout", Id);
|
||||
}
|
||||
else if (exception is MqttCommunicationException)
|
||||
{
|
||||
_logger.Warning(exception, "Client '{0}': Sending PUBLISH packet failed due to communication exception", Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Error(exception, "Client '{0}': Sending PUBLISH packet failed", Id);
|
||||
}
|
||||
|
||||
if (packetBusItem?.Packet is MqttPublishPacket publishPacket)
|
||||
{
|
||||
if (publishPacket.QualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce)
|
||||
{
|
||||
publishPacket.Dup = true;
|
||||
Session.EnqueueDataPacket(new MqttPacketBusItem(publishPacket));
|
||||
}
|
||||
}
|
||||
|
||||
StopInternal();
|
||||
}
|
||||
}
|
||||
|
||||
void StopInternal()
|
||||
{
|
||||
_cancellationToken?.TryCancel();
|
||||
}
|
||||
|
||||
async Task TrySendDisconnectPacket(MqttServerClientDisconnectOptions options)
|
||||
{
|
||||
try
|
||||
{
|
||||
// This also indicates that it was tried at least!
|
||||
_disconnectPacketSent = true;
|
||||
|
||||
var disconnectPacket = MqttDisconnectPacketFactory.Create(options);
|
||||
|
||||
using (var timeout = new CancellationTokenSource(_serverOptions.DefaultCommunicationTimeout))
|
||||
{
|
||||
await SendPacketAsync(disconnectPacket, timeout.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.Warning(exception, "Client '{0}': Error while sending DISCONNECT packet (ReasonCode = {1})", Id, options.ReasonCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// 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 MQTTnet.Diagnostics.Logger;
|
||||
using MQTTnet.Internal;
|
||||
|
||||
namespace MQTTnet.Server.Internal
|
||||
{
|
||||
public sealed class MqttRetainedMessagesManager
|
||||
{
|
||||
readonly Dictionary<string, MqttApplicationMessage> _messages = new Dictionary<string, MqttApplicationMessage>(4096);
|
||||
readonly AsyncLock _storageAccessLock = new AsyncLock();
|
||||
|
||||
readonly MqttServerEventContainer _eventContainer;
|
||||
readonly MqttNetSourceLogger _logger;
|
||||
|
||||
public MqttRetainedMessagesManager(MqttServerEventContainer eventContainer, IMqttNetLogger logger)
|
||||
{
|
||||
_eventContainer = eventContainer ?? throw new ArgumentNullException(nameof(eventContainer));
|
||||
|
||||
if (logger == null)
|
||||
throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
_logger = logger.WithSource(nameof(MqttRetainedMessagesManager));
|
||||
}
|
||||
|
||||
public async Task Start()
|
||||
{
|
||||
try
|
||||
{
|
||||
var eventArgs = new LoadingRetainedMessagesEventArgs();
|
||||
await _eventContainer.LoadingRetainedMessagesEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
||||
|
||||
lock (_messages)
|
||||
{
|
||||
_messages.Clear();
|
||||
|
||||
if (eventArgs.LoadedRetainedMessages != null)
|
||||
{
|
||||
foreach (var retainedMessage in eventArgs.LoadedRetainedMessages)
|
||||
{
|
||||
_messages[retainedMessage.Topic] = retainedMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.Error(exception, "Unhandled exception while loading retained messages.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateMessage(string clientId, MqttApplicationMessage applicationMessage)
|
||||
{
|
||||
if (applicationMessage == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(applicationMessage));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
List<MqttApplicationMessage> messagesForSave = null;
|
||||
var saveIsRequired = false;
|
||||
|
||||
lock (_messages)
|
||||
{
|
||||
var payload = applicationMessage.Payload;
|
||||
var hasPayload = payload.Length > 0;
|
||||
|
||||
if (!hasPayload)
|
||||
{
|
||||
saveIsRequired = _messages.Remove(applicationMessage.Topic);
|
||||
_logger.Verbose("Client '{0}' cleared retained message for topic '{1}'.", clientId, applicationMessage.Topic);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_messages.TryGetValue(applicationMessage.Topic, out var existingMessage))
|
||||
{
|
||||
_messages[applicationMessage.Topic] = applicationMessage;
|
||||
saveIsRequired = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (existingMessage.QualityOfServiceLevel != applicationMessage.QualityOfServiceLevel ||
|
||||
!MqttMemoryHelper.SequenceEqual(existingMessage.Payload, payload))
|
||||
{
|
||||
_messages[applicationMessage.Topic] = applicationMessage;
|
||||
saveIsRequired = true;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Verbose("Client '{0}' set retained message for topic '{1}'.", clientId, applicationMessage.Topic);
|
||||
}
|
||||
|
||||
if (saveIsRequired)
|
||||
{
|
||||
messagesForSave = new List<MqttApplicationMessage>(_messages.Values);
|
||||
}
|
||||
}
|
||||
|
||||
if (saveIsRequired)
|
||||
{
|
||||
using (await _storageAccessLock.EnterAsync().ConfigureAwait(false))
|
||||
{
|
||||
var eventArgs = new RetainedMessageChangedEventArgs(clientId, applicationMessage, messagesForSave);
|
||||
await _eventContainer.RetainedMessageChangedEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.Error(exception, "Unhandled exception while handling retained messages.");
|
||||
}
|
||||
}
|
||||
|
||||
public Task<IList<MqttApplicationMessage>> GetMessages()
|
||||
{
|
||||
lock (_messages)
|
||||
{
|
||||
var result = new List<MqttApplicationMessage>(_messages.Values);
|
||||
return Task.FromResult((IList<MqttApplicationMessage>)result);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<MqttApplicationMessage> GetMessage(string topic)
|
||||
{
|
||||
lock (_messages)
|
||||
{
|
||||
if (_messages.TryGetValue(topic, out var message))
|
||||
{
|
||||
return Task.FromResult(message);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult<MqttApplicationMessage>(null);
|
||||
}
|
||||
|
||||
public async Task ClearMessages()
|
||||
{
|
||||
lock (_messages)
|
||||
{
|
||||
_messages.Clear();
|
||||
}
|
||||
|
||||
using (await _storageAccessLock.EnterAsync().ConfigureAwait(false))
|
||||
{
|
||||
await _eventContainer.RetainedMessagesClearedEvent.InvokeAsync(EventArgs.Empty).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// 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 MQTTnet.Internal;
|
||||
|
||||
namespace MQTTnet.Server.Internal
|
||||
{
|
||||
public sealed class MqttServerEventContainer
|
||||
{
|
||||
public AsyncEvent<ApplicationMessageNotConsumedEventArgs> ApplicationMessageNotConsumedEvent { get; } = new AsyncEvent<ApplicationMessageNotConsumedEventArgs>();
|
||||
|
||||
public AsyncEvent<ClientAcknowledgedPublishPacketEventArgs> ClientAcknowledgedPublishPacketEvent { get; } = new AsyncEvent<ClientAcknowledgedPublishPacketEventArgs>();
|
||||
|
||||
public AsyncEvent<ClientConnectedEventArgs> ClientConnectedEvent { get; } = new AsyncEvent<ClientConnectedEventArgs>();
|
||||
|
||||
public AsyncEvent<ClientDisconnectedEventArgs> ClientDisconnectedEvent { get; } = new AsyncEvent<ClientDisconnectedEventArgs>();
|
||||
|
||||
public AsyncEvent<ClientSubscribedTopicEventArgs> ClientSubscribedTopicEvent { get; } = new AsyncEvent<ClientSubscribedTopicEventArgs>();
|
||||
|
||||
public AsyncEvent<ClientUnsubscribedTopicEventArgs> ClientUnsubscribedTopicEvent { get; } = new AsyncEvent<ClientUnsubscribedTopicEventArgs>();
|
||||
|
||||
public AsyncEvent<InterceptingClientApplicationMessageEnqueueEventArgs> InterceptingClientEnqueueEvent { get; } = new AsyncEvent<InterceptingClientApplicationMessageEnqueueEventArgs>();
|
||||
|
||||
public AsyncEvent<ApplicationMessageEnqueuedEventArgs> ApplicationMessageEnqueuedOrDroppedEvent { get; } = new AsyncEvent<ApplicationMessageEnqueuedEventArgs>();
|
||||
|
||||
public AsyncEvent<QueueMessageOverwrittenEventArgs> QueuedApplicationMessageOverwrittenEvent { get; } = new AsyncEvent<QueueMessageOverwrittenEventArgs>();
|
||||
|
||||
public AsyncEvent<InterceptingPacketEventArgs> InterceptingInboundPacketEvent { get; } = new AsyncEvent<InterceptingPacketEventArgs>();
|
||||
|
||||
public AsyncEvent<InterceptingPacketEventArgs> InterceptingOutboundPacketEvent { get; } = new AsyncEvent<InterceptingPacketEventArgs>();
|
||||
|
||||
public AsyncEvent<InterceptingPublishEventArgs> InterceptingPublishEvent { get; } = new AsyncEvent<InterceptingPublishEventArgs>();
|
||||
|
||||
public AsyncEvent<InterceptingSubscriptionEventArgs> InterceptingSubscriptionEvent { get; } = new AsyncEvent<InterceptingSubscriptionEventArgs>();
|
||||
|
||||
public AsyncEvent<InterceptingUnsubscriptionEventArgs> InterceptingUnsubscriptionEvent { get; } = new AsyncEvent<InterceptingUnsubscriptionEventArgs>();
|
||||
|
||||
public AsyncEvent<LoadingRetainedMessagesEventArgs> LoadingRetainedMessagesEvent { get; } = new AsyncEvent<LoadingRetainedMessagesEventArgs>();
|
||||
|
||||
public AsyncEvent<EventArgs> PreparingSessionEvent { get; } = new AsyncEvent<EventArgs>();
|
||||
|
||||
public AsyncEvent<RetainedMessageChangedEventArgs> RetainedMessageChangedEvent { get; } = new AsyncEvent<RetainedMessageChangedEventArgs>();
|
||||
|
||||
public AsyncEvent<EventArgs> RetainedMessagesClearedEvent { get; } = new AsyncEvent<EventArgs>();
|
||||
|
||||
public AsyncEvent<SessionDeletedEventArgs> SessionDeletedEvent { get; } = new AsyncEvent<SessionDeletedEventArgs>();
|
||||
|
||||
public AsyncEvent<EventArgs> StartedEvent { get; } = new AsyncEvent<EventArgs>();
|
||||
|
||||
public AsyncEvent<EventArgs> StoppedEvent { get; } = new AsyncEvent<EventArgs>();
|
||||
|
||||
public AsyncEvent<ValidatingConnectionEventArgs> ValidatingConnectionEvent { get; } = new AsyncEvent<ValidatingConnectionEventArgs>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// 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 MQTTnet.Diagnostics.Logger;
|
||||
using MQTTnet.Internal;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server.Internal
|
||||
{
|
||||
public sealed class MqttServerKeepAliveMonitor
|
||||
{
|
||||
static readonly MqttServerClientDisconnectOptions DefaultDisconnectOptions = new MqttServerClientDisconnectOptions
|
||||
{
|
||||
ReasonCode = MqttDisconnectReasonCode.KeepAliveTimeout,
|
||||
UserProperties = null,
|
||||
ReasonString = null,
|
||||
ServerReference = null
|
||||
};
|
||||
|
||||
readonly MqttNetSourceLogger _logger;
|
||||
readonly MqttServerOptions _options;
|
||||
readonly MqttClientSessionsManager _sessionsManager;
|
||||
|
||||
public MqttServerKeepAliveMonitor(MqttServerOptions options, MqttClientSessionsManager sessionsManager, IMqttNetLogger logger)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_sessionsManager = sessionsManager ?? throw new ArgumentNullException(nameof(sessionsManager));
|
||||
|
||||
if (logger == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
_logger = logger.WithSource(nameof(MqttServerKeepAliveMonitor));
|
||||
}
|
||||
|
||||
public void Start(CancellationToken cancellationToken)
|
||||
{
|
||||
// The keep alive monitor spawns a real new thread (LongRunning) because it does not
|
||||
// support async/await. Async etc. is avoided here because the thread will usually check
|
||||
// the connections every few milliseconds and thus the context changes (due to async) are
|
||||
// only consuming resources. Also there is just 1 thread for the entire server which is fine at all!
|
||||
Task.Factory.StartNew(_ => DoWork(cancellationToken), cancellationToken, TaskCreationOptions.LongRunning).RunInBackground(_logger);
|
||||
}
|
||||
|
||||
void DoWork(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.Info("Starting keep alive monitor");
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
TryProcessClients();
|
||||
Sleep(_options.KeepAliveOptions.MonitorInterval);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.Error(exception, "Unhandled exception while checking keep alive timeouts");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_logger.Verbose("Stopped checking keep alive timeout");
|
||||
}
|
||||
}
|
||||
|
||||
static void Sleep(TimeSpan timeout)
|
||||
{
|
||||
try
|
||||
{
|
||||
Thread.Sleep(timeout);
|
||||
}
|
||||
catch (ThreadAbortException)
|
||||
{
|
||||
// The ThreadAbortException is not actively catched in this project.
|
||||
// So we use a one which is similar and will be catched properly.
|
||||
throw new OperationCanceledException();
|
||||
}
|
||||
}
|
||||
|
||||
void TryProcessClient(MqttConnectedClient connection, DateTime now)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!connection.IsRunning)
|
||||
{
|
||||
// The connection is already dead or just created so there is no need to check it.
|
||||
return;
|
||||
}
|
||||
|
||||
if (connection.ConnectPacket.KeepAlivePeriod == 0)
|
||||
{
|
||||
// The keep alive feature is not used by the current connection.
|
||||
return;
|
||||
}
|
||||
|
||||
// Values described here: [MQTT-3.1.2-24].
|
||||
// If the client sends 5 sec. the server will allow up to 7.5 seconds.
|
||||
// If the client sends 1 sec. the server will allow up to 1.5 seconds.
|
||||
var maxSecondsWithoutPacket = connection.ConnectPacket.KeepAlivePeriod * 1.5D;
|
||||
|
||||
var secondsWithoutPackage = (now - connection.Statistics.LastPacketSentTimestamp).TotalSeconds;
|
||||
if (secondsWithoutPackage < maxSecondsWithoutPacket)
|
||||
{
|
||||
// A packet was received before the timeout is affected.
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Warning("Client '{0}': Did not receive any packet or keep alive signal", connection.Id);
|
||||
|
||||
// Execute the disconnection in background so that the keep alive monitor can continue
|
||||
// with checking other connections.
|
||||
// We do not need to wait for the task so no await is needed.
|
||||
// Also the internal state of the connection must be swapped to "Finalizing" because the
|
||||
// next iteration of the keep alive timer happens.
|
||||
_ = connection.StopAsync(DefaultDisconnectOptions);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.Error(exception, "Client {0}: Unhandled exception while checking keep alive timeouts", connection.Id);
|
||||
}
|
||||
}
|
||||
|
||||
void TryProcessClients()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
foreach (var client in _sessionsManager.GetClients())
|
||||
{
|
||||
TryProcessClient(client, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
// 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;
|
||||
using MQTTnet.Internal;
|
||||
using MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server.Internal;
|
||||
|
||||
public sealed class MqttSession : IDisposable
|
||||
{
|
||||
readonly MqttClientSessionsManager _clientSessionsManager;
|
||||
readonly MqttConnectPacket _connectPacket;
|
||||
readonly MqttServerEventContainer _eventContainer;
|
||||
readonly MqttPacketBus _packetBus = new();
|
||||
readonly MqttPacketIdentifierProvider _packetIdentifierProvider = new();
|
||||
readonly MqttServerOptions _serverOptions;
|
||||
readonly MqttClientSubscriptionsManager _subscriptionsManager;
|
||||
|
||||
// Do not use a dictionary in order to keep the ordering of the messages.
|
||||
readonly List<MqttPublishPacket> _unacknowledgedPublishPackets = new();
|
||||
|
||||
// Bookkeeping to know if this is a subscribing client; lazy initialize later.
|
||||
HashSet<string> _subscribedTopics;
|
||||
|
||||
public MqttSession(
|
||||
MqttConnectPacket connectPacket,
|
||||
IDictionary items,
|
||||
MqttServerOptions serverOptions,
|
||||
MqttServerEventContainer eventContainer,
|
||||
MqttRetainedMessagesManager retainedMessagesManager,
|
||||
MqttClientSessionsManager clientSessionsManager)
|
||||
{
|
||||
Items = items ?? throw new ArgumentNullException(nameof(items));
|
||||
|
||||
_connectPacket = connectPacket ?? throw new ArgumentNullException(nameof(connectPacket));
|
||||
_serverOptions = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions));
|
||||
_clientSessionsManager = clientSessionsManager ?? throw new ArgumentNullException(nameof(clientSessionsManager));
|
||||
_eventContainer = eventContainer ?? throw new ArgumentNullException(nameof(eventContainer));
|
||||
|
||||
_subscriptionsManager = new MqttClientSubscriptionsManager(this, eventContainer, retainedMessagesManager, clientSessionsManager);
|
||||
}
|
||||
|
||||
public DateTime CreatedTimestamp { get; } = DateTime.UtcNow;
|
||||
|
||||
public DateTime? DisconnectedTimestamp { get; set; }
|
||||
|
||||
public uint ExpiryInterval => _connectPacket.SessionExpiryInterval;
|
||||
|
||||
public bool HasSubscribedTopics => _subscribedTopics != null && _subscribedTopics.Count > 0;
|
||||
|
||||
public string Id => _connectPacket.ClientId;
|
||||
|
||||
public IDictionary Items { get; }
|
||||
|
||||
public MqttConnectPacket LatestConnectPacket { get; set; }
|
||||
|
||||
public MqttPacketIdentifierProvider PacketIdentifierProvider { get; } = new();
|
||||
|
||||
public long PendingDataPacketsCount => _packetBus.PartitionItemsCount(MqttPacketBusPartition.Data);
|
||||
|
||||
public bool WillMessageSent { get; set; }
|
||||
|
||||
public MqttPublishPacket AcknowledgePublishPacket(ushort packetIdentifier)
|
||||
{
|
||||
MqttPublishPacket publishPacket;
|
||||
|
||||
lock (_unacknowledgedPublishPackets)
|
||||
{
|
||||
publishPacket = _unacknowledgedPublishPackets.FirstOrDefault(p => p.PacketIdentifier.Equals(packetIdentifier));
|
||||
_unacknowledgedPublishPackets.Remove(publishPacket);
|
||||
}
|
||||
|
||||
return publishPacket;
|
||||
}
|
||||
|
||||
public void AddSubscribedTopic(string topic)
|
||||
{
|
||||
if (_subscribedTopics == null)
|
||||
{
|
||||
_subscribedTopics = new HashSet<string>();
|
||||
}
|
||||
|
||||
_subscribedTopics.Add(topic);
|
||||
}
|
||||
|
||||
public Task DeleteAsync()
|
||||
{
|
||||
return _clientSessionsManager.DeleteSessionAsync(Id);
|
||||
}
|
||||
|
||||
public Task<MqttPacketBusItem> DequeuePacketAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return _packetBus.DequeueItemAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_packetBus.Dispose();
|
||||
_subscriptionsManager.Dispose();
|
||||
}
|
||||
|
||||
public void EnqueueControlPacket(MqttPacketBusItem packetBusItem)
|
||||
{
|
||||
_packetBus.EnqueueItem(packetBusItem, MqttPacketBusPartition.Control);
|
||||
}
|
||||
|
||||
public EnqueueDataPacketResult EnqueueDataPacket(MqttPacketBusItem packetBusItem)
|
||||
{
|
||||
if (_packetBus.ItemsCount(MqttPacketBusPartition.Data) >= _serverOptions.MaxPendingMessagesPerClient)
|
||||
{
|
||||
if (_serverOptions.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropNewMessage)
|
||||
{
|
||||
return EnqueueDataPacketResult.Dropped;
|
||||
}
|
||||
|
||||
if (_serverOptions.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage)
|
||||
{
|
||||
// Only drop from the data partition. Dropping from control partition might break the connection
|
||||
// because the client does not receive PINGREQ packets etc. any longer.
|
||||
var firstItem = _packetBus.DropFirstItem(MqttPacketBusPartition.Data);
|
||||
if (firstItem != null && _eventContainer.QueuedApplicationMessageOverwrittenEvent.HasHandlers)
|
||||
{
|
||||
var eventArgs = new QueueMessageOverwrittenEventArgs(Id, firstItem.Packet);
|
||||
_eventContainer.QueuedApplicationMessageOverwrittenEvent.InvokeAsync(eventArgs).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var publishPacket = (MqttPublishPacket)packetBusItem.Packet;
|
||||
|
||||
if (publishPacket.QualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce)
|
||||
{
|
||||
publishPacket.PacketIdentifier = _packetIdentifierProvider.GetNextPacketIdentifier();
|
||||
|
||||
lock (_unacknowledgedPublishPackets)
|
||||
{
|
||||
_unacknowledgedPublishPackets.Add(publishPacket);
|
||||
}
|
||||
}
|
||||
|
||||
_packetBus.EnqueueItem(packetBusItem, MqttPacketBusPartition.Data);
|
||||
return EnqueueDataPacketResult.Enqueued;
|
||||
}
|
||||
|
||||
public void EnqueueHealthPacket(MqttPacketBusItem packetBusItem)
|
||||
{
|
||||
_packetBus.EnqueueItem(packetBusItem, MqttPacketBusPartition.Health);
|
||||
}
|
||||
|
||||
public MqttPublishPacket PeekAcknowledgePublishPacket(ushort packetIdentifier)
|
||||
{
|
||||
// This will only return the matching PUBLISH packet but does not remove it.
|
||||
// This is required for QoS 2.
|
||||
lock (_unacknowledgedPublishPackets)
|
||||
{
|
||||
return _unacknowledgedPublishPackets.FirstOrDefault(p => p.PacketIdentifier.Equals(packetIdentifier));
|
||||
}
|
||||
}
|
||||
|
||||
public void Recover()
|
||||
{
|
||||
// TODO: Keep the bus and only insert pending items again.
|
||||
// TODO: Check if packet identifier must be restarted or not.
|
||||
// TODO: Recover package identifier.
|
||||
|
||||
/*
|
||||
The Session state in the Client consists of:
|
||||
· QoS 1 and QoS 2 messages which have been sent to the Server, but have not been completely acknowledged.
|
||||
· QoS 2 messages which have been received from the Server, but have not been completely acknowledged.
|
||||
|
||||
The Session state in the Server consists of:
|
||||
· The existence of a Session, even if the rest of the Session state is empty.
|
||||
· The Client’s subscriptions.
|
||||
· QoS 1 and QoS 2 messages which have been sent to the Client, but have not been completely acknowledged.
|
||||
· QoS 1 and QoS 2 messages pending transmission to the Client.
|
||||
· QoS 2 messages which have been received from the Client, but have not been completely acknowledged.
|
||||
· Optionally, QoS 0 messages pending transmission to the Client.
|
||||
*/
|
||||
|
||||
// Create a copy of all currently unacknowledged publish packets and clear the storage.
|
||||
// We must re-enqueue them in order to trigger other code.
|
||||
List<MqttPublishPacket> unacknowledgedPublishPackets;
|
||||
lock (_unacknowledgedPublishPackets)
|
||||
{
|
||||
unacknowledgedPublishPackets = _unacknowledgedPublishPackets.ToList();
|
||||
_unacknowledgedPublishPackets.Clear();
|
||||
}
|
||||
|
||||
_packetBus.Clear();
|
||||
|
||||
foreach (var publishPacket in unacknowledgedPublishPackets)
|
||||
{
|
||||
EnqueueDataPacket(new MqttPacketBusItem(publishPacket));
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveSubscribedTopic(string topic)
|
||||
{
|
||||
_subscribedTopics?.Remove(topic);
|
||||
}
|
||||
|
||||
public Task<SubscribeResult> Subscribe(MqttSubscribePacket subscribePacket, CancellationToken cancellationToken)
|
||||
{
|
||||
return _subscriptionsManager.Subscribe(subscribePacket, cancellationToken);
|
||||
}
|
||||
|
||||
public bool TryCheckSubscriptions(string topic, ulong topicHash, MqttQualityOfServiceLevel qualityOfServiceLevel, string senderId, out CheckSubscriptionsResult result)
|
||||
{
|
||||
result = null;
|
||||
|
||||
try
|
||||
{
|
||||
result = _subscriptionsManager.CheckSubscriptions(topic, topicHash, qualityOfServiceLevel, senderId);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<UnsubscribeResult> Unsubscribe(MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken)
|
||||
{
|
||||
return _subscriptionsManager.Unsubscribe(unsubscribePacket, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// 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.ObjectModel;
|
||||
|
||||
namespace MQTTnet.Server.Internal
|
||||
{
|
||||
public sealed class MqttSessionsStorage
|
||||
{
|
||||
readonly Dictionary<string, MqttSession> _sessions = new Dictionary<string, MqttSession>(4096);
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
// Make sure that the sessions are also getting disposed!
|
||||
// This object can be reused after disposing.
|
||||
Dispose();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var sessionItem in _sessions)
|
||||
{
|
||||
sessionItem.Value.Dispose();
|
||||
}
|
||||
|
||||
_sessions.Clear();
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<MqttSession> ReadAllSessions()
|
||||
{
|
||||
return new ReadOnlyCollection<MqttSession>(_sessions.Values.ToList());
|
||||
}
|
||||
|
||||
public bool TryGetSession(string id, out MqttSession session)
|
||||
{
|
||||
if (id == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(id));
|
||||
}
|
||||
|
||||
if (!_sessions.TryGetValue(id, out session))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the Session Expiry Interval is 0xFFFFFFFF (UINT_MAX), the Session does not expire.
|
||||
if (session.ExpiryInterval > 0 && session.ExpiryInterval != uint.MaxValue)
|
||||
{
|
||||
if (session.DisconnectedTimestamp.HasValue)
|
||||
{
|
||||
if (DateTime.UtcNow > session.DisconnectedTimestamp.Value.AddSeconds(session.ExpiryInterval))
|
||||
{
|
||||
TryRemoveSession(id, out var oldSession);
|
||||
oldSession?.Dispose();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryRemoveSession(string id, out MqttSession session)
|
||||
{
|
||||
if (id == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(id));
|
||||
}
|
||||
|
||||
if (_sessions.TryGetValue(id, out session))
|
||||
{
|
||||
_sessions.Remove(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void UpdateSession(string id, MqttSession session)
|
||||
{
|
||||
if (id == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(id));
|
||||
}
|
||||
|
||||
_sessions[id] = session;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// 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 MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server.Internal
|
||||
{
|
||||
public sealed class MqttSubscription
|
||||
{
|
||||
public MqttSubscription(
|
||||
string topic,
|
||||
bool noLocal,
|
||||
MqttRetainHandling retainHandling,
|
||||
bool retainAsPublished,
|
||||
MqttQualityOfServiceLevel qualityOfServiceLevel,
|
||||
uint identifier)
|
||||
{
|
||||
Topic = topic;
|
||||
NoLocal = noLocal;
|
||||
RetainHandling = retainHandling;
|
||||
RetainAsPublished = retainAsPublished;
|
||||
GrantedQualityOfServiceLevel = qualityOfServiceLevel;
|
||||
Identifier = identifier;
|
||||
|
||||
MqttTopicHash.Calculate(Topic, out var hash, out var hashMask, out var hasWildcard);
|
||||
TopicHash = hash;
|
||||
TopicHashMask = hashMask;
|
||||
TopicHasWildcard = hasWildcard;
|
||||
}
|
||||
|
||||
public MqttQualityOfServiceLevel GrantedQualityOfServiceLevel { get; }
|
||||
|
||||
public uint Identifier { get; }
|
||||
|
||||
public bool NoLocal { get; }
|
||||
|
||||
public bool RetainAsPublished { get; }
|
||||
|
||||
public MqttRetainHandling RetainHandling { get; }
|
||||
|
||||
public string Topic { get; }
|
||||
|
||||
public ulong TopicHash { get; }
|
||||
|
||||
public ulong TopicHashMask { get; }
|
||||
|
||||
public bool TopicHasWildcard { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
// 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.
|
||||
|
||||
namespace MQTTnet.Server.Internal
|
||||
{
|
||||
/*
|
||||
* The MqttSubscription object stores subscription parameters and calculates
|
||||
* topic hashes.
|
||||
*
|
||||
* Use of Topic Hashes to improve message processing performance
|
||||
* =============================================================
|
||||
*
|
||||
* Motivation
|
||||
* -----------
|
||||
* In a typical use case for MQTT there may be many publishers (sensors or
|
||||
* other devices in the field) and few subscribers (monitoring all or many topics).
|
||||
* Each publisher may have one or more topic(s) to publish and therefore both, the
|
||||
* number of publishers and the number of topics may be large.
|
||||
*
|
||||
* Maintaining subscribers in a separate container
|
||||
* -----------------------------------------------
|
||||
* Instead of placing all sessions into a single _sessions container, subscribers
|
||||
* are added into another _subscriberSessions container (if a client is a
|
||||
* subscriber and a publisher then the client is present in both containers). The
|
||||
* cost is some additional bookkeeping work upon subscription where each client
|
||||
* session needs to maintain a list of subscribed topics.
|
||||
*
|
||||
* When the client subscribes to the first topic, then the session manager adds
|
||||
* the client to the _subscriberSessions container, and when the client
|
||||
* unsubscribes from the last topic then the session manager removes the client
|
||||
* from the container. Now, when an application message arrives, only the list of
|
||||
* subscribers need processing instead of looping through potentially thousands of
|
||||
* publishers.
|
||||
*
|
||||
* Improving subscriber topic lookup
|
||||
* ---------------------------------
|
||||
* For each subscriber, it needs to be determined whether an application message
|
||||
* matches any topic the subscriber has subscribed to. There may only be few
|
||||
* subscribers but there may be many subscribed topics, including wildcard topics.
|
||||
*
|
||||
* The implemented approach uses a topic hash and a hash mask calculated on the
|
||||
* subscribed topic and the published topic (the application message topic) to
|
||||
* find candidates for a match, with the existing match logic evaluating a reduced
|
||||
* number of candidates.
|
||||
*
|
||||
* For each subscription, the topic hash and a hash mask is stored with the
|
||||
* subscription, and for each application message received, the hash is calculated
|
||||
* for the published topic before attempting to find matching subscriptions. The
|
||||
* hash calculation itself is simple and does not have a large performance impact.
|
||||
*
|
||||
* We'll first explain how topic hashes and hash masks are constructed and then how
|
||||
* they are used.
|
||||
*
|
||||
* Topic hash
|
||||
* ----------
|
||||
* Topic hashes are stored as 64-bit numbers. Each byte within the 64-bit number
|
||||
* relates to one MQTT topic level. A checksum is calculated for each topic level
|
||||
* by iterating over the characters within the topic level (cast to byte) and the
|
||||
* result is stored into the corresponding byte of the 64-bit number. If a topic
|
||||
* level contains a wildcard character, then 0x00 is stored instead of the
|
||||
* checksum.
|
||||
*
|
||||
* If there are less than 8 levels then the rest of the 64-bit number is filled
|
||||
* with 0xff. If there are more than 8 levels then topics where the first 8 MQTT
|
||||
* topic levels are identical will have the same hash value.
|
||||
*
|
||||
* This is the topic hash for the MQTT topic below: 0x655D4AF1FFFFFF
|
||||
*
|
||||
* client1/building1/level1/sensor1 (empty) (empty) (empty) (empty)
|
||||
* \_____/ \_______/ \____/ \_____/ \_____/ \_____/ \_____/ \_____/
|
||||
* | | | | | | | |
|
||||
* 0x65 0x5D 0x4A 0xF1 0xFF 0xFF 0xFF 0xFF
|
||||
*
|
||||
* This is the topic hash for an MQTT topic containing a wildcard: 0x655D00F1FFFFFF
|
||||
*
|
||||
* client1/building1/ + /sensor1 (empty) (empty) (empty) (empty)
|
||||
* \_____/ \_______/ \_/ \_____/ \_____/ \_____/ \_____/ \_____/
|
||||
* | | | | | | | |
|
||||
* 0x65 0x5D 0 0xF1 0xFF 0xFF 0xFF 0xFF
|
||||
*
|
||||
* For topics that contain the multi level wildcard # at the end, the topic hash
|
||||
* is filled with 0x00: 0x65004A00000000
|
||||
*
|
||||
* client1/ + /level1/ # (empty) (empty) (empty) (empty)
|
||||
* \_____/ \_/ \____/ \_/ \_____/ \_____/ \_____/ \_____/
|
||||
* | | | | | | | |
|
||||
* 0x65 0 0x4A 0 0 0 0 0
|
||||
*
|
||||
*
|
||||
* Topic hash mask
|
||||
* ---------------
|
||||
* The hash mask simply contains 0xFF for non-wildcard topic levels and 0x00 for
|
||||
* wildcard topic levels. Here are the topic hash masks for the examples above.
|
||||
*
|
||||
* client1/building1/level1/sensor1 (empty) (empty) (empty) (empty)
|
||||
* \_____/ \_______/ \____/ \_____/ \_____/ \_____/ \_____/ \_____/
|
||||
* | | | | | | | |
|
||||
* 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF
|
||||
*
|
||||
* client1/building1/ + /sensor1 (empty) (empty) (empty) (empty)
|
||||
* \_____/ \_______/ \_/ \_____/ \_____/ \_____/ \_____/ \_____/
|
||||
* | | | | | | | |
|
||||
* 0xFF 0xFF 0 0xFF 0xFF 0xFF 0xFF 0xFF
|
||||
*
|
||||
* client1/ + /level1/ # (empty) (empty) (empty) (empty)
|
||||
* \_____/ \_/ \____/ \_/ \_____/ \_____/ \_____/ \_____/
|
||||
* | | | | | | | |
|
||||
* 0xFF 0 0xFF 0 0 0 0 0
|
||||
*
|
||||
*
|
||||
* Topic hash and hash mask properties
|
||||
* -----------------------------------
|
||||
* The following properties of topic hashes and hash masks can be exploited to
|
||||
* find potentially matching subscribed topics for a given published topic.
|
||||
*
|
||||
* (1) If a subscribed topic does not contain wildcards then the topic hash of the
|
||||
* subscribed topic must be equal to the topic hash of the published topic,
|
||||
* otherwise the subscribed topic cannot be a candidate for a match.
|
||||
*
|
||||
* (2) If a subscribed topic contains wildcards then the hash of the published
|
||||
* topic masked with the subscribed topic's hash mask must be equal to the hash of
|
||||
* the subscribed topic. I.e. a subscribed topic is a candidate for a match if:
|
||||
* (publishedTopicHash & subscribedTopicHashMask) == subscribedTopicHash
|
||||
*
|
||||
* (3) If a subscribed topic contains wildcards then any potentially matching
|
||||
* published topic must have a hash value that is greater than or equal to the
|
||||
* hash value of the subscribed topic (because the subscribed topic contains
|
||||
* zeroes in wildcard positions).
|
||||
*
|
||||
* Match finding
|
||||
* -------------
|
||||
* The subscription manager maintains two separate dictionaries to assist finding
|
||||
* matches using topic hashes: a _noWildcardSubscriptionsByTopicHash dictionary
|
||||
* containing all subscriptions that do not have wildcards, and a
|
||||
* _wildcardSubscriptionsByTopicHash dictionary containing subscriptions with
|
||||
* wildcards.
|
||||
*
|
||||
* For subscriptions without wildcards, all potential candidates for a match are
|
||||
* obtained by a single look-up (exploiting point 1 above).
|
||||
*
|
||||
* For subscriptions with wildcards, the subscription manager loops through the
|
||||
* wildcard subscriptions and selects candidates that satisfy condition
|
||||
* (publishedTopicHash & subscribedTopicMask) == subscribedTopicHash (point 2).
|
||||
* The loop could exit early if wildcard subscriptions were stored into a sorted
|
||||
* dictionary (utilizing point 3), but, after testing, there does not seem to be
|
||||
* any real benefit doing so.
|
||||
*
|
||||
* Other considerations
|
||||
* --------------------
|
||||
* Characters in the topic string are cast to byte and any additional bytes in a
|
||||
* multi-byte character are disregarded. Best guess is that this does not impact
|
||||
* performance in practice.
|
||||
*
|
||||
* Instead of one-byte checksums per topic level, one-word checksums per topic
|
||||
* level could be used. If most topics contained four levels or less then hash
|
||||
* buckets would be shallower.
|
||||
*
|
||||
* For very large numbers of topics, performing a parallel search may help further.
|
||||
*
|
||||
* To also handle a larger number of subscribers, it may be beneficial to maintain
|
||||
* a subscribers-by-subscription-topic dictionary.
|
||||
*/
|
||||
public static class MqttTopicHash
|
||||
{
|
||||
public static void Calculate(string topic, out ulong resultHash, out ulong resultHashMask, out bool resultHasWildcard)
|
||||
{
|
||||
// calculate topic hash
|
||||
ulong hash = 0;
|
||||
ulong hashMaskInverted = 0;
|
||||
ulong levelBitMask = 0;
|
||||
ulong fillLevelBitMask = 0;
|
||||
var hasWildcard = false;
|
||||
byte checkSum = 0;
|
||||
var level = 0;
|
||||
|
||||
var i = 0;
|
||||
while (i < topic.Length)
|
||||
{
|
||||
var c = topic[i];
|
||||
if (c == MqttTopicFilterComparer.LevelSeparator)
|
||||
{
|
||||
// done with this level
|
||||
hash <<= 8;
|
||||
hash |= checkSum;
|
||||
hashMaskInverted <<= 8;
|
||||
hashMaskInverted |= levelBitMask;
|
||||
checkSum = 0;
|
||||
levelBitMask = 0;
|
||||
++level;
|
||||
if (level >= 8)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (c == MqttTopicFilterComparer.SingleLevelWildcard)
|
||||
{
|
||||
levelBitMask = 0xff;
|
||||
hasWildcard = true;
|
||||
}
|
||||
else if (c == MqttTopicFilterComparer.MultiLevelWildcard)
|
||||
{
|
||||
// checksum is zero for a valid topic
|
||||
levelBitMask = 0xff;
|
||||
// fill rest with this fillLevelBitMask
|
||||
fillLevelBitMask = 0xff;
|
||||
hasWildcard = true;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The checksum should be designed to reduce the hash bucket depth for the expected
|
||||
// fairly regularly named MQTT topics that don't differ much,
|
||||
// i.e. "room1/sensor1"
|
||||
// "room1/sensor2"
|
||||
// "room1/sensor3"
|
||||
// etc.
|
||||
if ((c & 1) == 0)
|
||||
{
|
||||
checkSum += (byte)c;
|
||||
}
|
||||
else
|
||||
{
|
||||
checkSum ^= (byte)(c >> 1);
|
||||
}
|
||||
}
|
||||
|
||||
++i;
|
||||
}
|
||||
|
||||
// Shift hash left and leave zeroes to fill ulong
|
||||
if (level < 8)
|
||||
{
|
||||
hash <<= 8;
|
||||
hash |= checkSum;
|
||||
hashMaskInverted <<= 8;
|
||||
hashMaskInverted |= levelBitMask;
|
||||
++level;
|
||||
while (level < 8)
|
||||
{
|
||||
hash <<= 8;
|
||||
hashMaskInverted <<= 8;
|
||||
hashMaskInverted |= fillLevelBitMask;
|
||||
++level;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasWildcard)
|
||||
{
|
||||
while (i < topic.Length)
|
||||
{
|
||||
var c = topic[i];
|
||||
if (c == MqttTopicFilterComparer.SingleLevelWildcard || c == MqttTopicFilterComparer.MultiLevelWildcard)
|
||||
{
|
||||
hasWildcard = true;
|
||||
break;
|
||||
}
|
||||
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
resultHash = hash;
|
||||
resultHashMask = ~hashMaskInverted;
|
||||
resultHasWildcard = hasWildcard;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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 MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server.Internal
|
||||
{
|
||||
public sealed class SubscribeResult
|
||||
{
|
||||
public SubscribeResult(int topicsCount)
|
||||
{
|
||||
ReasonCodes = new List<MqttSubscribeReasonCode>(topicsCount);
|
||||
}
|
||||
|
||||
public bool CloseConnection { get; set; }
|
||||
|
||||
public List<MqttSubscribeReasonCode> ReasonCodes { get; set; }
|
||||
|
||||
public string ReasonString { get; set; }
|
||||
|
||||
public List<MqttRetainedMessageMatch> RetainedMessages { get; set; }
|
||||
|
||||
public List<MqttUserProperty> UserProperties { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace MQTTnet.Server.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class that stores subscriptions by their topic hash mask.
|
||||
/// </summary>
|
||||
public sealed class TopicHashMaskSubscriptions
|
||||
{
|
||||
public Dictionary<ulong, HashSet<MqttSubscription>> SubscriptionsByHashMask { get; } = new Dictionary<ulong, HashSet<MqttSubscription>>();
|
||||
|
||||
public void AddSubscription(MqttSubscription subscription)
|
||||
{
|
||||
if (!SubscriptionsByHashMask.TryGetValue(subscription.TopicHashMask, out var subscriptions))
|
||||
{
|
||||
subscriptions = new HashSet<MqttSubscription>();
|
||||
SubscriptionsByHashMask.Add(subscription.TopicHashMask, subscriptions);
|
||||
}
|
||||
subscriptions.Add(subscription);
|
||||
}
|
||||
|
||||
public void RemoveSubscription(MqttSubscription subscription)
|
||||
{
|
||||
if (SubscriptionsByHashMask.TryGetValue(subscription.TopicHashMask, out var subscriptions))
|
||||
{
|
||||
subscriptions.Remove(subscription);
|
||||
if (subscriptions.Count == 0)
|
||||
{
|
||||
SubscriptionsByHashMask.Remove(subscription.TopicHashMask);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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 MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server.Internal
|
||||
{
|
||||
public sealed class UnsubscribeResult
|
||||
{
|
||||
public List<MqttUnsubscribeReasonCode> ReasonCodes { get; } = new List<MqttUnsubscribeReasonCode>(128);
|
||||
|
||||
public bool CloseConnection { get; set; }
|
||||
|
||||
public List<MqttUserProperty> UserProperties { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
<AssemblyName>MQTTnet.Server</AssemblyName>
|
||||
<RootNamespace>MQTTnet.Server</RootNamespace>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
<Company>The contributors of MQTTnet</Company>
|
||||
<Product>MQTTnet</Product>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<Description>This is the server implementation of MQTTnet.</Description>
|
||||
<Authors>The contributors of MQTTnet</Authors>
|
||||
<PackageId>MQTTnet.Server</PackageId>
|
||||
<DelaySign>false</DelaySign>
|
||||
<SignAssembly>false</SignAssembly>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<Copyright>Copyright (c) .NET Foundation and Contributors</Copyright>
|
||||
<PackageProjectUrl>https://github.com/dotnet/MQTTnet</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/dotnet/MQTTnet.git</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageTags>MQTT Message Queue Telemetry Transport MQTTClient MQTTServer Server MQTTBroker Broker NETStandard IoT InternetOfThings Messaging Hardware Arduino Sensor Actuator M2M ESP Smart Home Cities Automation Xamarin Blazor</PackageTags>
|
||||
<NeutralLanguage>en-US</NeutralLanguage>
|
||||
<EnableNETAnalyzers>false</EnableNETAnalyzers>
|
||||
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
|
||||
<PackageIcon>nuget.png</PackageIcon>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<DisableImplicitAspNetCoreAnalyzers>true</DisableImplicitAspNetCoreAnalyzers>
|
||||
<NoWarn>1591;NETSDK1138;NU1803;NU1901;NU1902</NoWarn>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<NuGetAuditMode>all</NuGetAuditMode>
|
||||
<NuGetAudit>true</NuGetAudit>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<AnalysisLevel>latest-Recommended</AnalysisLevel>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MQTTnet\MQTTnet.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\Images\nuget.png">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,12 @@
|
||||
<wpf:ResourceDictionary xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib"
|
||||
xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xml:space="preserve">
|
||||
<s:Boolean
|
||||
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=disconnecting/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean
|
||||
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=events/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean
|
||||
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=options/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean
|
||||
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=status/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=stopping/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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.
|
||||
|
||||
namespace MQTTnet.Server;
|
||||
|
||||
public enum MqttClientDisconnectType
|
||||
{
|
||||
Clean,
|
||||
NotClean,
|
||||
Takeover
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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 MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server;
|
||||
|
||||
public sealed class MqttRetainedMessageMatch
|
||||
{
|
||||
public MqttRetainedMessageMatch(MqttApplicationMessage applicationMessage, MqttQualityOfServiceLevel subscriptionQualityOfServiceLevel)
|
||||
{
|
||||
ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage));
|
||||
SubscriptionQualityOfServiceLevel = subscriptionQualityOfServiceLevel;
|
||||
}
|
||||
|
||||
public MqttApplicationMessage ApplicationMessage { get; }
|
||||
|
||||
public MqttQualityOfServiceLevel SubscriptionQualityOfServiceLevel { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
// 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;
|
||||
using System.Collections.Concurrent;
|
||||
using MQTTnet.Adapter;
|
||||
using MQTTnet.Diagnostics.Logger;
|
||||
using MQTTnet.Internal;
|
||||
using MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
using MQTTnet.Server.Internal;
|
||||
|
||||
namespace MQTTnet.Server;
|
||||
|
||||
public class MqttServer : Disposable
|
||||
{
|
||||
readonly ICollection<IMqttServerAdapter> _adapters;
|
||||
readonly MqttClientSessionsManager _clientSessionsManager;
|
||||
readonly MqttServerEventContainer _eventContainer = new();
|
||||
readonly MqttServerKeepAliveMonitor _keepAliveMonitor;
|
||||
readonly MqttNetSourceLogger _logger;
|
||||
readonly MqttServerOptions _options;
|
||||
readonly MqttRetainedMessagesManager _retainedMessagesManager;
|
||||
readonly IMqttNetLogger _rootLogger;
|
||||
|
||||
CancellationTokenSource _cancellationTokenSource;
|
||||
bool _isStopping;
|
||||
|
||||
public MqttServer(MqttServerOptions options, IEnumerable<IMqttServerAdapter> adapters, IMqttNetLogger logger)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
|
||||
if (adapters == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(adapters));
|
||||
}
|
||||
|
||||
_adapters = adapters.ToList();
|
||||
|
||||
_rootLogger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_logger = logger.WithSource(nameof(MqttServer));
|
||||
|
||||
_retainedMessagesManager = new MqttRetainedMessagesManager(_eventContainer, _rootLogger);
|
||||
_clientSessionsManager = new MqttClientSessionsManager(options, _retainedMessagesManager, _eventContainer, _rootLogger);
|
||||
_keepAliveMonitor = new MqttServerKeepAliveMonitor(options, _clientSessionsManager, _rootLogger);
|
||||
}
|
||||
|
||||
public event Func<ApplicationMessageEnqueuedEventArgs, Task> ApplicationMessageEnqueuedOrDroppedAsync
|
||||
{
|
||||
add => _eventContainer.ApplicationMessageEnqueuedOrDroppedEvent.AddHandler(value);
|
||||
remove => _eventContainer.ApplicationMessageEnqueuedOrDroppedEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
public event Func<ApplicationMessageNotConsumedEventArgs, Task> ApplicationMessageNotConsumedAsync
|
||||
{
|
||||
add => _eventContainer.ApplicationMessageNotConsumedEvent.AddHandler(value);
|
||||
remove => _eventContainer.ApplicationMessageNotConsumedEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
public event Func<ClientAcknowledgedPublishPacketEventArgs, Task> ClientAcknowledgedPublishPacketAsync
|
||||
{
|
||||
add => _eventContainer.ClientAcknowledgedPublishPacketEvent.AddHandler(value);
|
||||
remove => _eventContainer.ClientAcknowledgedPublishPacketEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
public event Func<ClientConnectedEventArgs, Task> ClientConnectedAsync
|
||||
{
|
||||
add => _eventContainer.ClientConnectedEvent.AddHandler(value);
|
||||
remove => _eventContainer.ClientConnectedEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
public event Func<ClientDisconnectedEventArgs, Task> ClientDisconnectedAsync
|
||||
{
|
||||
add => _eventContainer.ClientDisconnectedEvent.AddHandler(value);
|
||||
remove => _eventContainer.ClientDisconnectedEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
public event Func<ClientSubscribedTopicEventArgs, Task> ClientSubscribedTopicAsync
|
||||
{
|
||||
add => _eventContainer.ClientSubscribedTopicEvent.AddHandler(value);
|
||||
remove => _eventContainer.ClientSubscribedTopicEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
public event Func<ClientUnsubscribedTopicEventArgs, Task> ClientUnsubscribedTopicAsync
|
||||
{
|
||||
add => _eventContainer.ClientUnsubscribedTopicEvent.AddHandler(value);
|
||||
remove => _eventContainer.ClientUnsubscribedTopicEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
public event Func<InterceptingClientApplicationMessageEnqueueEventArgs, Task> InterceptingClientEnqueueAsync
|
||||
{
|
||||
add => _eventContainer.InterceptingClientEnqueueEvent.AddHandler(value);
|
||||
remove => _eventContainer.InterceptingClientEnqueueEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
public event Func<InterceptingPacketEventArgs, Task> InterceptingInboundPacketAsync
|
||||
{
|
||||
add => _eventContainer.InterceptingInboundPacketEvent.AddHandler(value);
|
||||
remove => _eventContainer.InterceptingInboundPacketEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
public event Func<InterceptingPacketEventArgs, Task> InterceptingOutboundPacketAsync
|
||||
{
|
||||
add => _eventContainer.InterceptingOutboundPacketEvent.AddHandler(value);
|
||||
remove => _eventContainer.InterceptingOutboundPacketEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
public event Func<InterceptingPublishEventArgs, Task> InterceptingPublishAsync
|
||||
{
|
||||
add => _eventContainer.InterceptingPublishEvent.AddHandler(value);
|
||||
remove => _eventContainer.InterceptingPublishEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
public event Func<InterceptingSubscriptionEventArgs, Task> InterceptingSubscriptionAsync
|
||||
{
|
||||
add => _eventContainer.InterceptingSubscriptionEvent.AddHandler(value);
|
||||
remove => _eventContainer.InterceptingSubscriptionEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
public event Func<InterceptingUnsubscriptionEventArgs, Task> InterceptingUnsubscriptionAsync
|
||||
{
|
||||
add => _eventContainer.InterceptingUnsubscriptionEvent.AddHandler(value);
|
||||
remove => _eventContainer.InterceptingUnsubscriptionEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
public event Func<LoadingRetainedMessagesEventArgs, Task> LoadingRetainedMessageAsync
|
||||
{
|
||||
add => _eventContainer.LoadingRetainedMessagesEvent.AddHandler(value);
|
||||
remove => _eventContainer.LoadingRetainedMessagesEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
public event Func<EventArgs, Task> PreparingSessionAsync
|
||||
{
|
||||
add => _eventContainer.PreparingSessionEvent.AddHandler(value);
|
||||
remove => _eventContainer.PreparingSessionEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
public event Func<QueueMessageOverwrittenEventArgs, Task> QueuedApplicationMessageOverwrittenAsync
|
||||
{
|
||||
add => _eventContainer.QueuedApplicationMessageOverwrittenEvent.AddHandler(value);
|
||||
remove => _eventContainer.QueuedApplicationMessageOverwrittenEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
public event Func<RetainedMessageChangedEventArgs, Task> RetainedMessageChangedAsync
|
||||
{
|
||||
add => _eventContainer.RetainedMessageChangedEvent.AddHandler(value);
|
||||
remove => _eventContainer.RetainedMessageChangedEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
public event Func<EventArgs, Task> RetainedMessagesClearedAsync
|
||||
{
|
||||
add => _eventContainer.RetainedMessagesClearedEvent.AddHandler(value);
|
||||
remove => _eventContainer.RetainedMessagesClearedEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
public event Func<SessionDeletedEventArgs, Task> SessionDeletedAsync
|
||||
{
|
||||
add => _eventContainer.SessionDeletedEvent.AddHandler(value);
|
||||
remove => _eventContainer.SessionDeletedEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
public event Func<EventArgs, Task> StartedAsync
|
||||
{
|
||||
add => _eventContainer.StartedEvent.AddHandler(value);
|
||||
remove => _eventContainer.StartedEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
public event Func<EventArgs, Task> StoppedAsync
|
||||
{
|
||||
add => _eventContainer.StoppedEvent.AddHandler(value);
|
||||
remove => _eventContainer.StoppedEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
public event Func<ValidatingConnectionEventArgs, Task> ValidatingConnectionAsync
|
||||
{
|
||||
add => _eventContainer.ValidatingConnectionEvent.AddHandler(value);
|
||||
remove => _eventContainer.ValidatingConnectionEvent.RemoveHandler(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the server will accept new connections.
|
||||
/// If not, the server will close the connection without any notification (DISCONNECT packet).
|
||||
/// This feature can be used when the server is shutting down.
|
||||
/// </summary>
|
||||
public bool AcceptNewConnections { get; set; } = true;
|
||||
|
||||
public bool IsStarted => _cancellationTokenSource != null;
|
||||
|
||||
/// <summary>
|
||||
/// Gives access to the session items which belong to this server. This session items are passed
|
||||
/// to several events instead of the client session items if the event is caused by the server instead of a client.
|
||||
/// </summary>
|
||||
public IDictionary ServerSessionItems { get; } = new ConcurrentDictionary<object, object>();
|
||||
|
||||
public Task DeleteRetainedMessagesAsync()
|
||||
{
|
||||
ThrowIfNotStarted();
|
||||
|
||||
return _retainedMessagesManager?.ClearMessages() ?? CompletedTask.Instance;
|
||||
}
|
||||
|
||||
public Task DisconnectClientAsync(string id, MqttServerClientDisconnectOptions options)
|
||||
{
|
||||
if (id == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(id));
|
||||
}
|
||||
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
ThrowIfNotStarted();
|
||||
|
||||
return _clientSessionsManager.GetClient(id).StopAsync(options);
|
||||
}
|
||||
|
||||
public Task<IList<MqttClientStatus>> GetClientsAsync()
|
||||
{
|
||||
ThrowIfNotStarted();
|
||||
|
||||
return _clientSessionsManager.GetClientsStatus();
|
||||
}
|
||||
|
||||
public Task<MqttApplicationMessage> GetRetainedMessageAsync(string topic)
|
||||
{
|
||||
if (topic == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(topic));
|
||||
}
|
||||
|
||||
ThrowIfNotStarted();
|
||||
|
||||
return _retainedMessagesManager.GetMessage(topic);
|
||||
}
|
||||
|
||||
public Task<IList<MqttApplicationMessage>> GetRetainedMessagesAsync()
|
||||
{
|
||||
ThrowIfNotStarted();
|
||||
|
||||
return _retainedMessagesManager.GetMessages();
|
||||
}
|
||||
|
||||
public Task<IList<MqttSessionStatus>> GetSessionsAsync()
|
||||
{
|
||||
ThrowIfNotStarted();
|
||||
|
||||
return _clientSessionsManager.GetSessionsStatus();
|
||||
}
|
||||
|
||||
public Task InjectApplicationMessage(InjectedMqttApplicationMessage injectedApplicationMessage, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (injectedApplicationMessage == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(injectedApplicationMessage));
|
||||
}
|
||||
|
||||
if (injectedApplicationMessage.ApplicationMessage == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(injectedApplicationMessage.ApplicationMessage));
|
||||
}
|
||||
|
||||
MqttTopicValidator.ThrowIfInvalid(injectedApplicationMessage.ApplicationMessage.Topic);
|
||||
|
||||
ThrowIfNotStarted();
|
||||
|
||||
if (string.IsNullOrEmpty(injectedApplicationMessage.ApplicationMessage.Topic))
|
||||
{
|
||||
throw new NotSupportedException("Injected application messages must contain a topic (topic alias is not supported)");
|
||||
}
|
||||
|
||||
var sessionItems = injectedApplicationMessage.CustomSessionItems ?? ServerSessionItems;
|
||||
|
||||
return _clientSessionsManager.DispatchApplicationMessage(
|
||||
injectedApplicationMessage.SenderClientId,
|
||||
sessionItems,
|
||||
injectedApplicationMessage.ApplicationMessage,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task StartAsync()
|
||||
{
|
||||
ThrowIfStarted();
|
||||
|
||||
_isStopping = false;
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
var cancellationToken = _cancellationTokenSource.Token;
|
||||
|
||||
await _retainedMessagesManager.Start().ConfigureAwait(false);
|
||||
_clientSessionsManager.Start();
|
||||
_keepAliveMonitor.Start(cancellationToken);
|
||||
|
||||
foreach (var adapter in _adapters)
|
||||
{
|
||||
adapter.ClientHandler = c => OnHandleClient(c, cancellationToken);
|
||||
await adapter.StartAsync(_options, _rootLogger).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await _eventContainer.StartedEvent.InvokeAsync(EventArgs.Empty).ConfigureAwait(false);
|
||||
|
||||
_logger.Info("Started");
|
||||
}
|
||||
|
||||
public async Task StopAsync(MqttServerStopOptions options)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_cancellationTokenSource == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isStopping = true;
|
||||
|
||||
_cancellationTokenSource.Cancel(false);
|
||||
|
||||
await _clientSessionsManager.CloseAllConnections(options.DefaultClientDisconnectOptions).ConfigureAwait(false);
|
||||
|
||||
foreach (var adapter in _adapters)
|
||||
{
|
||||
adapter.ClientHandler = null;
|
||||
await adapter.StopAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_cancellationTokenSource = null;
|
||||
}
|
||||
|
||||
await _eventContainer.StoppedEvent.InvokeAsync(EventArgs.Empty).ConfigureAwait(false);
|
||||
|
||||
_logger.Info("Stopped");
|
||||
}
|
||||
|
||||
public Task SubscribeAsync(string clientId, ICollection<MqttTopicFilter> topicFilters)
|
||||
{
|
||||
if (clientId == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(clientId));
|
||||
}
|
||||
|
||||
if (topicFilters == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(topicFilters));
|
||||
}
|
||||
|
||||
foreach (var topicFilter in topicFilters)
|
||||
{
|
||||
MqttTopicValidator.ThrowIfInvalidSubscribe(topicFilter.Topic);
|
||||
}
|
||||
|
||||
ThrowIfDisposed();
|
||||
ThrowIfNotStarted();
|
||||
|
||||
return _clientSessionsManager.SubscribeAsync(clientId, topicFilters);
|
||||
}
|
||||
|
||||
public Task UnsubscribeAsync(string clientId, ICollection<string> topicFilters)
|
||||
{
|
||||
if (clientId == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(clientId));
|
||||
}
|
||||
|
||||
if (topicFilters == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(topicFilters));
|
||||
}
|
||||
|
||||
ThrowIfDisposed();
|
||||
ThrowIfNotStarted();
|
||||
|
||||
return _clientSessionsManager.UnsubscribeAsync(clientId, topicFilters);
|
||||
}
|
||||
|
||||
public Task UpdateRetainedMessageAsync(MqttApplicationMessage retainedMessage)
|
||||
{
|
||||
if (retainedMessage == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(retainedMessage));
|
||||
}
|
||||
|
||||
ThrowIfDisposed();
|
||||
ThrowIfNotStarted();
|
||||
|
||||
return _retainedMessagesManager?.UpdateMessage(string.Empty, retainedMessage);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
StopAsync(new MqttServerStopOptions()).GetAwaiter().GetResult();
|
||||
|
||||
foreach (var adapter in _adapters)
|
||||
{
|
||||
adapter.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
Task OnHandleClient(IMqttChannelAdapter channelAdapter, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_isStopping || !AcceptNewConnections)
|
||||
{
|
||||
return CompletedTask.Instance;
|
||||
}
|
||||
|
||||
return _clientSessionsManager.HandleClientConnectionAsync(channelAdapter, cancellationToken);
|
||||
}
|
||||
|
||||
void ThrowIfNotStarted()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (_cancellationTokenSource == null)
|
||||
{
|
||||
throw new InvalidOperationException("The MQTT server is not started.");
|
||||
}
|
||||
}
|
||||
|
||||
void ThrowIfStarted()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (_cancellationTokenSource != null)
|
||||
{
|
||||
throw new InvalidOperationException("The MQTT server is already started.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// 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.Text;
|
||||
using MQTTnet.Internal;
|
||||
using MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server;
|
||||
|
||||
public static class MqttServerExtensions
|
||||
{
|
||||
public static Task DisconnectClientAsync(this MqttServer server, string id, MqttDisconnectReasonCode reasonCode = MqttDisconnectReasonCode.NormalDisconnection)
|
||||
{
|
||||
if (server == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(server));
|
||||
}
|
||||
|
||||
return server.DisconnectClientAsync(id, new MqttServerClientDisconnectOptions { ReasonCode = reasonCode });
|
||||
}
|
||||
|
||||
public static Task InjectApplicationMessage(
|
||||
this MqttServer server,
|
||||
string topic,
|
||||
string payload = null,
|
||||
MqttQualityOfServiceLevel qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce,
|
||||
bool retain = false)
|
||||
{
|
||||
if (server == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(server));
|
||||
}
|
||||
|
||||
if (topic == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(topic));
|
||||
}
|
||||
|
||||
var payloadBuffer = EmptyBuffer.Array;
|
||||
if (payload is string stringPayload)
|
||||
{
|
||||
payloadBuffer = Encoding.UTF8.GetBytes(stringPayload);
|
||||
}
|
||||
|
||||
return server.InjectApplicationMessage(
|
||||
new InjectedMqttApplicationMessage(
|
||||
new MqttApplicationMessage
|
||||
{
|
||||
Topic = topic,
|
||||
PayloadSegment = new ArraySegment<byte>(payloadBuffer),
|
||||
QualityOfServiceLevel = qualityOfServiceLevel,
|
||||
Retain = retain
|
||||
}));
|
||||
}
|
||||
|
||||
public static Task StopAsync(this MqttServer server)
|
||||
{
|
||||
if (server == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(server));
|
||||
}
|
||||
|
||||
return server.StopAsync(new MqttServerStopOptions());
|
||||
}
|
||||
|
||||
public static Task SubscribeAsync(this MqttServer server, string clientId, params MqttTopicFilter[] topicFilters)
|
||||
{
|
||||
if (server == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(server));
|
||||
}
|
||||
|
||||
if (clientId == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(clientId));
|
||||
}
|
||||
|
||||
if (topicFilters == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(topicFilters));
|
||||
}
|
||||
|
||||
return server.SubscribeAsync(clientId, topicFilters);
|
||||
}
|
||||
|
||||
public static Task SubscribeAsync(this MqttServer server, string clientId, string topic)
|
||||
{
|
||||
if (server == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(server));
|
||||
}
|
||||
|
||||
if (clientId == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(clientId));
|
||||
}
|
||||
|
||||
if (topic == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(topic));
|
||||
}
|
||||
|
||||
var topicFilters = new MqttTopicFilterBuilder().WithTopic(topic).Build();
|
||||
return server.SubscribeAsync(clientId, topicFilters);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// 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 MQTTnet.Diagnostics.Logger;
|
||||
using MQTTnet.Server.Internal.Adapter;
|
||||
|
||||
namespace MQTTnet.Server;
|
||||
|
||||
public sealed class MqttServerFactory
|
||||
{
|
||||
public MqttServerFactory() : this(new MqttNetNullLogger())
|
||||
{
|
||||
}
|
||||
|
||||
public MqttServerFactory(IMqttNetLogger logger)
|
||||
{
|
||||
DefaultLogger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public IMqttNetLogger DefaultLogger { get; }
|
||||
|
||||
public IList<Func<MqttServerFactory, IMqttServerAdapter>> DefaultServerAdapters { get; } = new List<Func<MqttServerFactory, IMqttServerAdapter>>
|
||||
{
|
||||
factory => new MqttTcpServerAdapter()
|
||||
};
|
||||
|
||||
public IDictionary<object, object> Properties { get; } = new Dictionary<object, object>();
|
||||
|
||||
public MqttApplicationMessageBuilder CreateApplicationMessageBuilder()
|
||||
{
|
||||
return new MqttApplicationMessageBuilder();
|
||||
}
|
||||
|
||||
public MqttServer CreateMqttServer(MqttServerOptions options)
|
||||
{
|
||||
return CreateMqttServer(options, DefaultLogger);
|
||||
}
|
||||
|
||||
public MqttServer CreateMqttServer(MqttServerOptions options, IMqttNetLogger logger)
|
||||
{
|
||||
if (logger == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
var serverAdapters = DefaultServerAdapters.Select(a => a.Invoke(this));
|
||||
return CreateMqttServer(options, serverAdapters, logger);
|
||||
}
|
||||
|
||||
public MqttServer CreateMqttServer(MqttServerOptions options, IEnumerable<IMqttServerAdapter> serverAdapters, IMqttNetLogger logger)
|
||||
{
|
||||
if (serverAdapters == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(serverAdapters));
|
||||
}
|
||||
|
||||
if (logger == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
return new MqttServer(options, serverAdapters, logger);
|
||||
}
|
||||
|
||||
public MqttServer CreateMqttServer(MqttServerOptions options, IEnumerable<IMqttServerAdapter> serverAdapters)
|
||||
{
|
||||
if (serverAdapters == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(serverAdapters));
|
||||
}
|
||||
|
||||
return new MqttServer(options, serverAdapters, DefaultLogger);
|
||||
}
|
||||
|
||||
public MqttServerClientDisconnectOptionsBuilder CreateMqttServerClientDisconnectOptionsBuilder()
|
||||
{
|
||||
return new MqttServerClientDisconnectOptionsBuilder();
|
||||
}
|
||||
|
||||
|
||||
public MqttServerStopOptionsBuilder CreateMqttServerStopOptionsBuilder()
|
||||
{
|
||||
return new MqttServerStopOptionsBuilder();
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder CreateServerOptionsBuilder()
|
||||
{
|
||||
return new MqttServerOptionsBuilder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// 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.
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public interface IMqttServerCertificateCredentials
|
||||
{
|
||||
string Password { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// 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.
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public enum MqttPendingMessagesOverflowStrategy
|
||||
{
|
||||
DropOldestQueuedMessage,
|
||||
|
||||
DropNewMessage
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// 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.
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public class MqttServerCertificateCredentials : IMqttServerCertificateCredentials
|
||||
{
|
||||
public string Password { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class MqttServerKeepAliveOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// When this mode is enabled the MQTT server will not close a connection when the
|
||||
/// client is currently sending a (large) payload. This may lead to "dead" connections
|
||||
/// When this mode is disabled the MQTT server will disconnect a client when the keep
|
||||
/// alive timeout is reached even if the client is currently sending a (large) payload.
|
||||
/// </summary>
|
||||
public bool DisconnectClientWhenReadingPayload { get; set; }
|
||||
|
||||
public TimeSpan MonitorInterval { get; set; } = TimeSpan.FromMilliseconds(500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// 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;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class MqttServerOptions
|
||||
{
|
||||
public TimeSpan DefaultCommunicationTimeout { get; set; } = TimeSpan.FromSeconds(100);
|
||||
|
||||
public MqttServerTcpEndpointOptions DefaultEndpointOptions { get; } = new MqttServerTcpEndpointOptions();
|
||||
|
||||
public bool EnablePersistentSessions { get; set; }
|
||||
|
||||
public MqttServerKeepAliveOptions KeepAliveOptions { get; } = new MqttServerKeepAliveOptions();
|
||||
|
||||
public int MaxPendingMessagesPerClient { get; set; } = 250;
|
||||
|
||||
public MqttPendingMessagesOverflowStrategy PendingMessagesOverflowStrategy { get; set; } = MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage;
|
||||
|
||||
public MqttServerTlsTcpEndpointOptions TlsEndpointOptions { get; } = new MqttServerTlsTcpEndpointOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default and initial size of the packet write buffer.
|
||||
/// It is recommended to set this to a value close to the usual expected packet size * 1.5.
|
||||
/// Do not change this value when no memory issues are experienced.
|
||||
/// </summary>
|
||||
public int WriterBufferSize { get; set; } = 4096;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum size of the buffer writer. The writer will reduce its internal buffer
|
||||
/// to this value after serializing a packet.
|
||||
/// Do not change this value when no memory issues are experienced.
|
||||
/// </summary>
|
||||
public int WriterBufferSizeMax { get; set; } = 65535;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
// 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.Net;
|
||||
using System.Net.Security;
|
||||
using System.Security.Authentication;
|
||||
using MQTTnet.Certificates;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
// ReSharper disable UnusedMember.Global
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public class MqttServerOptionsBuilder
|
||||
{
|
||||
readonly MqttServerOptions _options = new MqttServerOptions();
|
||||
|
||||
public MqttServerOptions Build()
|
||||
{
|
||||
return _options;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithClientCertificate(RemoteCertificateValidationCallback validationCallback = null, bool checkCertificateRevocation = false)
|
||||
{
|
||||
_options.TlsEndpointOptions.ClientCertificateRequired = true;
|
||||
_options.TlsEndpointOptions.CheckCertificateRevocation = checkCertificateRevocation;
|
||||
_options.TlsEndpointOptions.RemoteCertificateValidationCallback = validationCallback;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithConnectionBacklog(int value)
|
||||
{
|
||||
_options.DefaultEndpointOptions.ConnectionBacklog = value;
|
||||
_options.TlsEndpointOptions.ConnectionBacklog = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithDefaultCommunicationTimeout(TimeSpan value)
|
||||
{
|
||||
_options.DefaultCommunicationTimeout = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithDefaultEndpoint()
|
||||
{
|
||||
_options.DefaultEndpointOptions.IsEnabled = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithDefaultEndpointBoundIPAddress(IPAddress value)
|
||||
{
|
||||
_options.DefaultEndpointOptions.BoundInterNetworkAddress = value ?? IPAddress.Any;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithDefaultEndpointBoundIPV6Address(IPAddress value)
|
||||
{
|
||||
_options.DefaultEndpointOptions.BoundInterNetworkV6Address = value ?? IPAddress.Any;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithDefaultEndpointPort(int value)
|
||||
{
|
||||
_options.DefaultEndpointOptions.Port = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithDefaultEndpointReuseAddress()
|
||||
{
|
||||
_options.DefaultEndpointOptions.ReuseAddress = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithEncryptedEndpoint()
|
||||
{
|
||||
_options.TlsEndpointOptions.IsEnabled = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithEncryptedEndpointBoundIPAddress(IPAddress value)
|
||||
{
|
||||
_options.TlsEndpointOptions.BoundInterNetworkAddress = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithEncryptedEndpointBoundIPV6Address(IPAddress value)
|
||||
{
|
||||
_options.TlsEndpointOptions.BoundInterNetworkV6Address = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithEncryptedEndpointPort(int value)
|
||||
{
|
||||
_options.TlsEndpointOptions.Port = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithEncryptionSslProtocol(SslProtocols value)
|
||||
{
|
||||
_options.TlsEndpointOptions.SslProtocol = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithKeepAlive()
|
||||
{
|
||||
_options.DefaultEndpointOptions.KeepAlive = true;
|
||||
_options.TlsEndpointOptions.KeepAlive = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithMaxPendingMessagesPerClient(int value)
|
||||
{
|
||||
_options.MaxPendingMessagesPerClient = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithoutDefaultEndpoint()
|
||||
{
|
||||
_options.DefaultEndpointOptions.IsEnabled = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithoutEncryptedEndpoint()
|
||||
{
|
||||
_options.TlsEndpointOptions.IsEnabled = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Usually the MQTT packets can be send partially to improve memory allocations.
|
||||
/// This is done by using multiple TCP packets or WebSocket frames etc.
|
||||
/// Unfortunately not all clients do support this and will close the connection when receiving partial packets.
|
||||
/// </summary>
|
||||
public MqttServerOptionsBuilder WithoutPacketFragmentation()
|
||||
{
|
||||
_options.DefaultEndpointOptions.AllowPacketFragmentation = false;
|
||||
_options.TlsEndpointOptions.AllowPacketFragmentation = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithPersistentSessions(bool value = true)
|
||||
{
|
||||
_options.EnablePersistentSessions = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithRemoteCertificateValidationCallback(RemoteCertificateValidationCallback value)
|
||||
{
|
||||
_options.TlsEndpointOptions.RemoteCertificateValidationCallback = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithTcpKeepAliveInterval(int value)
|
||||
{
|
||||
_options.DefaultEndpointOptions.TcpKeepAliveInterval = value;
|
||||
_options.TlsEndpointOptions.TcpKeepAliveInterval = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithTcpKeepAliveRetryCount(int value)
|
||||
{
|
||||
_options.DefaultEndpointOptions.TcpKeepAliveRetryCount = value;
|
||||
_options.TlsEndpointOptions.TcpKeepAliveRetryCount = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithTcpKeepAliveTime(int value)
|
||||
{
|
||||
_options.DefaultEndpointOptions.TcpKeepAliveTime = value;
|
||||
_options.TlsEndpointOptions.TcpKeepAliveTime = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithTlsEndpointReuseAddress()
|
||||
{
|
||||
_options.TlsEndpointOptions.ReuseAddress = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithEncryptionCertificate(byte[] value, IMqttServerCertificateCredentials credentials = null)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
_options.TlsEndpointOptions.CertificateProvider = new BlobCertificateProvider(value)
|
||||
{
|
||||
Password = credentials?.Password
|
||||
};
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithEncryptionCertificate(X509Certificate2 certificate)
|
||||
{
|
||||
if (certificate == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(certificate));
|
||||
}
|
||||
|
||||
_options.TlsEndpointOptions.CertificateProvider = new X509CertificateProvider(certificate);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerOptionsBuilder WithEncryptionCertificate(ICertificateProvider certificateProvider)
|
||||
{
|
||||
if (certificateProvider == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(certificateProvider));
|
||||
}
|
||||
|
||||
_options.TlsEndpointOptions.CertificateProvider = certificateProvider;
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// 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.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public abstract class MqttServerTcpEndpointBaseOptions
|
||||
{
|
||||
public bool IsEnabled { get; set; }
|
||||
|
||||
public int Port { get; set; }
|
||||
|
||||
public int ConnectionBacklog { get; set; } = 100;
|
||||
|
||||
public bool NoDelay { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the sockets keep alive feature should be used.
|
||||
/// The value _null_ indicates that the OS and framework defaults should be used.
|
||||
/// </summary>
|
||||
public bool? KeepAlive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Usually the MQTT packets can be send partially. This is done by using multiple TCP packets
|
||||
/// or WebSocket frames etc. Unfortunately not all clients do support this feature and
|
||||
/// will close the connection when receiving such packets. If such clients are connecting to this
|
||||
/// server the flag must be set to _false_.
|
||||
/// </summary>
|
||||
public bool AllowPacketFragmentation { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the TCP keep alive interval.
|
||||
/// The value _null_ indicates that the OS and framework defaults should be used.
|
||||
/// </summary>
|
||||
public int? TcpKeepAliveInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the TCP keep alive retry count.
|
||||
/// The value _null_ indicates that the OS and framework defaults should be used.
|
||||
/// </summary>
|
||||
public int? TcpKeepAliveRetryCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the TCP keep alive time.
|
||||
/// The value _null_ indicates that the OS and framework defaults should be used.
|
||||
/// </summary>
|
||||
public int? TcpKeepAliveTime { get; set; }
|
||||
|
||||
public LingerOption LingerState { get; set; } = new LingerOption(true, 0);
|
||||
|
||||
public IPAddress BoundInterNetworkAddress { get; set; } = IPAddress.Any;
|
||||
|
||||
public IPAddress BoundInterNetworkV6Address { get; set; } = IPAddress.IPv6Any;
|
||||
|
||||
/// <summary>
|
||||
/// This requires admin permissions on Linux.
|
||||
/// </summary>
|
||||
public bool ReuseAddress { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// 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.
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public class MqttServerTcpEndpointOptions : MqttServerTcpEndpointBaseOptions
|
||||
{
|
||||
public MqttServerTcpEndpointOptions()
|
||||
{
|
||||
Port = 1883;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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.Security.Authentication;
|
||||
using MQTTnet.Certificates;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class MqttServerTlsTcpEndpointOptions : MqttServerTcpEndpointBaseOptions
|
||||
{
|
||||
public MqttServerTlsTcpEndpointOptions()
|
||||
{
|
||||
Port = 8883;
|
||||
}
|
||||
|
||||
public System.Net.Security.RemoteCertificateValidationCallback RemoteCertificateValidationCallback { get; set; }
|
||||
|
||||
public ICertificateProvider CertificateProvider { get; set; }
|
||||
|
||||
public bool ClientCertificateRequired { get; set; }
|
||||
|
||||
public bool CheckCertificateRevocation { get; set; }
|
||||
|
||||
public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls12;
|
||||
|
||||
public System.Net.Security.CipherSuitesPolicy CipherSuitesPolicy { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// 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 MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server;
|
||||
|
||||
public sealed class PublishResponse
|
||||
{
|
||||
public MqttPubAckReasonCode ReasonCode { get; set; } = MqttPubAckReasonCode.Success;
|
||||
|
||||
public string ReasonString { get; set; }
|
||||
|
||||
public List<MqttUserProperty> UserProperties { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// 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 MQTTnet.Formatter;
|
||||
using MQTTnet.Server.Internal;
|
||||
|
||||
namespace MQTTnet.Server;
|
||||
|
||||
public sealed class MqttClientStatus
|
||||
{
|
||||
readonly MqttConnectedClient _client;
|
||||
|
||||
public MqttClientStatus(MqttConnectedClient client)
|
||||
{
|
||||
_client = client ?? throw new ArgumentNullException(nameof(client));
|
||||
}
|
||||
|
||||
public long BytesReceived => _client.ChannelAdapter.BytesReceived;
|
||||
|
||||
public long BytesSent => _client.ChannelAdapter.BytesSent;
|
||||
|
||||
public DateTime ConnectedTimestamp => _client.Statistics.ConnectedTimestamp;
|
||||
|
||||
public string Endpoint => _client.Endpoint;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the client identifier.
|
||||
/// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues.
|
||||
/// </summary>
|
||||
public string Id => _client.Id;
|
||||
|
||||
public DateTime LastNonKeepAlivePacketReceivedTimestamp => _client.Statistics.LastNonKeepAlivePacketReceivedTimestamp;
|
||||
|
||||
public DateTime LastPacketReceivedTimestamp => _client.Statistics.LastPacketReceivedTimestamp;
|
||||
|
||||
public DateTime LastPacketSentTimestamp => _client.Statistics.LastPacketSentTimestamp;
|
||||
|
||||
public MqttProtocolVersion ProtocolVersion => _client.ChannelAdapter.PacketFormatterAdapter.ProtocolVersion;
|
||||
|
||||
public long ReceivedApplicationMessagesCount => _client.Statistics.ReceivedApplicationMessagesCount;
|
||||
|
||||
public long ReceivedPacketsCount => _client.Statistics.ReceivedPacketsCount;
|
||||
|
||||
public long SentApplicationMessagesCount => _client.Statistics.SentApplicationMessagesCount;
|
||||
|
||||
public long SentPacketsCount => _client.Statistics.SentPacketsCount;
|
||||
|
||||
public MqttSessionStatus Session { get; set; }
|
||||
|
||||
public Task DisconnectAsync(MqttServerClientDisconnectOptions options)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
return _client.StopAsync(options);
|
||||
}
|
||||
|
||||
public void ResetStatistics()
|
||||
{
|
||||
_client.ResetStatistics();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// 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 MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server;
|
||||
|
||||
public static class MqttClientStatusExtensions
|
||||
{
|
||||
static readonly MqttServerClientDisconnectOptions DefaultDisconnectOptions = new()
|
||||
{
|
||||
ReasonCode = MqttDisconnectReasonCode.NormalDisconnection,
|
||||
ReasonString = null,
|
||||
UserProperties = null,
|
||||
ServerReference = null
|
||||
};
|
||||
|
||||
public static Task DisconnectAsync(this MqttClientStatus clientStatus)
|
||||
{
|
||||
if (clientStatus == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(clientStatus));
|
||||
}
|
||||
|
||||
return clientStatus.DisconnectAsync(DefaultDisconnectOptions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// 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;
|
||||
using MQTTnet.Internal;
|
||||
using MQTTnet.Server.Internal;
|
||||
using MQTTnet.Server.Internal.Formatter;
|
||||
|
||||
namespace MQTTnet.Server;
|
||||
|
||||
public sealed class MqttSessionStatus
|
||||
{
|
||||
readonly MqttSession _session;
|
||||
|
||||
public MqttSessionStatus(MqttSession session)
|
||||
{
|
||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
}
|
||||
|
||||
public DateTime CreatedTimestamp => _session.CreatedTimestamp;
|
||||
|
||||
public DateTime? DisconnectedTimestamp => _session.DisconnectedTimestamp;
|
||||
|
||||
public uint ExpiryInterval => _session.ExpiryInterval;
|
||||
|
||||
public string Id => _session.Id;
|
||||
|
||||
public IDictionary Items => _session.Items;
|
||||
|
||||
public long PendingApplicationMessagesCount => _session.PendingDataPacketsCount;
|
||||
|
||||
public Task ClearApplicationMessagesQueueAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task DeleteAsync()
|
||||
{
|
||||
return _session.DeleteAsync();
|
||||
}
|
||||
|
||||
public Task DeliverApplicationMessageAsync(MqttApplicationMessage applicationMessage)
|
||||
{
|
||||
if (applicationMessage == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(applicationMessage));
|
||||
}
|
||||
|
||||
var packetBusItem = new MqttPacketBusItem(MqttPublishPacketFactory.Create(applicationMessage));
|
||||
_session.EnqueueDataPacket(packetBusItem);
|
||||
|
||||
return packetBusItem.WaitAsync();
|
||||
}
|
||||
|
||||
public Task EnqueueApplicationMessageAsync(MqttApplicationMessage applicationMessage)
|
||||
{
|
||||
if (applicationMessage == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(applicationMessage));
|
||||
}
|
||||
|
||||
_session.EnqueueDataPacket(new MqttPacketBusItem(MqttPublishPacketFactory.Create(applicationMessage)));
|
||||
|
||||
return CompletedTask.Instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// 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 MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class MqttServerStopOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// These disconnect options are sent to every connected client via a DISCONNECT packet.
|
||||
/// <remarks>MQTT 5.0.0+ feature.</remarks>
|
||||
/// </summary>
|
||||
public MqttServerClientDisconnectOptions DefaultClientDisconnectOptions { get; set; } = new MqttServerClientDisconnectOptions
|
||||
{
|
||||
ReasonCode = MqttDisconnectReasonCode.ServerShuttingDown,
|
||||
UserProperties = null,
|
||||
ReasonString = null,
|
||||
ServerReference = null
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// 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;
|
||||
|
||||
namespace MQTTnet.Server
|
||||
{
|
||||
public sealed class MqttServerStopOptionsBuilder
|
||||
{
|
||||
readonly MqttServerStopOptions _options = new MqttServerStopOptions();
|
||||
|
||||
public MqttServerStopOptionsBuilder WithDefaultClientDisconnectOptions(MqttServerClientDisconnectOptions value)
|
||||
{
|
||||
_options.DefaultClientDisconnectOptions = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerStopOptionsBuilder WithDefaultClientDisconnectOptions(Action<MqttServerClientDisconnectOptionsBuilder> builder)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
var optionsBuilder = new MqttServerClientDisconnectOptionsBuilder();
|
||||
builder.Invoke(optionsBuilder);
|
||||
|
||||
_options.DefaultClientDisconnectOptions = optionsBuilder.Build();
|
||||
return this;
|
||||
}
|
||||
|
||||
public MqttServerStopOptions Build()
|
||||
{
|
||||
return _options;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// 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 MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server;
|
||||
|
||||
public sealed class SubscribeResponse
|
||||
{
|
||||
/// <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; }
|
||||
|
||||
public string ReasonString { get; set; }
|
||||
|
||||
public List<MqttUserProperty> UserProperties { get; } = new();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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 MQTTnet.Packets;
|
||||
using MQTTnet.Protocol;
|
||||
|
||||
namespace MQTTnet.Server;
|
||||
|
||||
public sealed class UnsubscribeResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the reason code which is sent to the client.
|
||||
/// MQTTv5 only.
|
||||
/// </summary>
|
||||
public MqttUnsubscribeReasonCode ReasonCode { get; set; }
|
||||
|
||||
public string ReasonString { get; set; }
|
||||
|
||||
public List<MqttUserProperty> UserProperties { get; } = new();
|
||||
}
|
||||
Reference in New Issue
Block a user