You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

346 lines
13 KiB

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Threading;
  5. using System.Threading.Tasks;
  6. using MQTTnet.Adapter;
  7. using MQTTnet.Diagnostics;
  8. using MQTTnet.Exceptions;
  9. using MQTTnet.Internal;
  10. using MQTTnet.Packets;
  11. using MQTTnet.Protocol;
  12. namespace MQTTnet.Server
  13. {
  14. public sealed class MqttClientSessionsManager : IDisposable
  15. {
  16. private readonly Dictionary<string, MqttClientSession> _sessions = new Dictionary<string, MqttClientSession>();
  17. private readonly AsyncLock _sessionsLock = new AsyncLock();
  18. private readonly MqttRetainedMessagesManager _retainedMessagesManager;
  19. private readonly IMqttServerOptions _options;
  20. private readonly IMqttNetChildLogger _logger;
  21. public MqttClientSessionsManager(IMqttServerOptions options, MqttServer server, MqttRetainedMessagesManager retainedMessagesManager, IMqttNetChildLogger logger)
  22. {
  23. if (logger == null) throw new ArgumentNullException(nameof(logger));
  24. _logger = logger.CreateChildLogger(nameof(MqttClientSessionsManager));
  25. _options = options ?? throw new ArgumentNullException(nameof(options));
  26. Server = server ?? throw new ArgumentNullException(nameof(server));
  27. _retainedMessagesManager = retainedMessagesManager ?? throw new ArgumentNullException(nameof(retainedMessagesManager));
  28. }
  29. public MqttServer Server { get; }
  30. public async Task RunSessionAsync(IMqttChannelAdapter clientAdapter, CancellationToken cancellationToken)
  31. {
  32. var clientId = string.Empty;
  33. var wasCleanDisconnect = false;
  34. MqttClientSession clientSession = null;
  35. try
  36. {
  37. if (!(await clientAdapter.ReceivePacketAsync(_options.DefaultCommunicationTimeout, cancellationToken)
  38. .ConfigureAwait(false) is MqttConnectPacket connectPacket))
  39. {
  40. throw new MqttProtocolViolationException(
  41. "The first packet from a client must be a 'CONNECT' packet [MQTT-3.1.0-1].");
  42. }
  43. clientId = connectPacket.ClientId;
  44. // Switch to the required protocol version before sending any response.
  45. clientAdapter.PacketSerializer.ProtocolVersion = connectPacket.ProtocolVersion;
  46. var connectReturnCode = ValidateConnection(connectPacket);
  47. if (connectReturnCode != MqttConnectReturnCode.ConnectionAccepted)
  48. {
  49. await clientAdapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, new[]
  50. {
  51. new MqttConnAckPacket
  52. {
  53. ConnectReturnCode = connectReturnCode
  54. }
  55. }, cancellationToken).ConfigureAwait(false);
  56. return;
  57. }
  58. var result = await PrepareClientSessionAsync(connectPacket).ConfigureAwait(false);
  59. clientSession = result.Session;
  60. await clientAdapter.SendPacketsAsync(_options.DefaultCommunicationTimeout, new[]
  61. {
  62. new MqttConnAckPacket
  63. {
  64. ConnectReturnCode = connectReturnCode,
  65. IsSessionPresent = result.IsExistingSession
  66. }
  67. }, cancellationToken).ConfigureAwait(false);
  68. Server.OnClientConnected(new ConnectedMqttClient
  69. {
  70. ClientId = clientId,
  71. ProtocolVersion = clientAdapter.PacketSerializer.ProtocolVersion
  72. });
  73. wasCleanDisconnect = await clientSession.RunAsync(connectPacket, clientAdapter).ConfigureAwait(false);
  74. }
  75. catch (OperationCanceledException)
  76. {
  77. }
  78. catch (Exception exception)
  79. {
  80. _logger.Error(exception, exception.Message);
  81. }
  82. finally
  83. {
  84. try
  85. {
  86. await clientAdapter.DisconnectAsync(_options.DefaultCommunicationTimeout, CancellationToken.None).ConfigureAwait(false);
  87. clientAdapter.Dispose();
  88. }
  89. catch (Exception exception)
  90. {
  91. _logger.Error(exception, exception.Message);
  92. }
  93. if (!_options.EnablePersistentSessions)
  94. {
  95. await DeleteSessionAsync(clientId).ConfigureAwait(false);
  96. }
  97. Server.OnClientDisconnected(new ConnectedMqttClient
  98. {
  99. ClientId = clientId,
  100. ProtocolVersion = clientAdapter.PacketSerializer.ProtocolVersion,
  101. PendingApplicationMessages = clientSession?.PendingMessagesQueue.Count ?? 0
  102. },
  103. wasCleanDisconnect);
  104. }
  105. }
  106. public async Task StopAsync()
  107. {
  108. await _sessionsLock.EnterAsync(CancellationToken.None).ConfigureAwait(false);
  109. try
  110. {
  111. foreach (var session in _sessions)
  112. {
  113. session.Value.Stop(MqttClientDisconnectType.NotClean);
  114. }
  115. _sessions.Clear();
  116. }
  117. finally
  118. {
  119. _sessionsLock.Exit();
  120. }
  121. }
  122. public async Task<IList<ConnectedMqttClient>> GetConnectedClientsAsync()
  123. {
  124. await _sessionsLock.EnterAsync(CancellationToken.None).ConfigureAwait(false);
  125. try
  126. {
  127. return _sessions.Where(s => s.Value.IsConnected).Select(s => new ConnectedMqttClient
  128. {
  129. ClientId = s.Value.ClientId,
  130. ProtocolVersion = s.Value.ProtocolVersion,
  131. LastPacketReceived = s.Value.KeepAliveMonitor.LastPacketReceived,
  132. LastNonKeepAlivePacketReceived = s.Value.KeepAliveMonitor.LastNonKeepAlivePacketReceived,
  133. PendingApplicationMessages = s.Value.PendingMessagesQueue.Count
  134. }).ToList();
  135. }
  136. finally
  137. {
  138. _sessionsLock.Exit();
  139. }
  140. }
  141. public void StartDispatchApplicationMessage(MqttClientSession senderClientSession, MqttApplicationMessage applicationMessage)
  142. {
  143. Task.Run(() => DispatchApplicationMessageAsync(senderClientSession, applicationMessage));
  144. }
  145. public async Task SubscribeAsync(string clientId, IList<TopicFilter> topicFilters)
  146. {
  147. if (clientId == null) throw new ArgumentNullException(nameof(clientId));
  148. if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters));
  149. await _sessionsLock.EnterAsync(CancellationToken.None).ConfigureAwait(false);
  150. try
  151. {
  152. if (!_sessions.TryGetValue(clientId, out var session))
  153. {
  154. throw new InvalidOperationException($"Client session '{clientId}' is unknown.");
  155. }
  156. await session.SubscribeAsync(topicFilters).ConfigureAwait(false);
  157. }
  158. finally
  159. {
  160. _sessionsLock.Exit();
  161. }
  162. }
  163. public async Task UnsubscribeAsync(string clientId, IList<string> topicFilters)
  164. {
  165. if (clientId == null) throw new ArgumentNullException(nameof(clientId));
  166. if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters));
  167. await _sessionsLock.EnterAsync(CancellationToken.None).ConfigureAwait(false);
  168. try
  169. {
  170. if (!_sessions.TryGetValue(clientId, out var session))
  171. {
  172. throw new InvalidOperationException($"Client session '{clientId}' is unknown.");
  173. }
  174. await session.UnsubscribeAsync(topicFilters).ConfigureAwait(false);
  175. }
  176. finally
  177. {
  178. _sessionsLock.Exit();
  179. }
  180. }
  181. public void Dispose()
  182. {
  183. _sessionsLock?.Dispose();
  184. }
  185. private MqttConnectReturnCode ValidateConnection(MqttConnectPacket connectPacket)
  186. {
  187. if (_options.ConnectionValidator == null)
  188. {
  189. return MqttConnectReturnCode.ConnectionAccepted;
  190. }
  191. var context = new MqttConnectionValidatorContext(
  192. connectPacket.ClientId,
  193. connectPacket.Username,
  194. connectPacket.Password,
  195. connectPacket.WillMessage);
  196. _options.ConnectionValidator(context);
  197. return context.ReturnCode;
  198. }
  199. private async Task DeleteSessionAsync(string clientId)
  200. {
  201. await _sessionsLock.EnterAsync(CancellationToken.None);
  202. try
  203. {
  204. _sessions.Remove(clientId);
  205. _logger.Verbose("Session for client '{0}' deleted.", clientId);
  206. }
  207. finally
  208. {
  209. _sessionsLock.Exit();
  210. }
  211. }
  212. private async Task<GetOrCreateClientSessionResult> PrepareClientSessionAsync(MqttConnectPacket connectPacket)
  213. {
  214. await _sessionsLock.EnterAsync(CancellationToken.None).ConfigureAwait(false);
  215. try
  216. {
  217. var isSessionPresent = _sessions.TryGetValue(connectPacket.ClientId, out var clientSession);
  218. if (isSessionPresent)
  219. {
  220. if (connectPacket.CleanSession)
  221. {
  222. _sessions.Remove(connectPacket.ClientId);
  223. clientSession.Stop(MqttClientDisconnectType.Clean);
  224. clientSession.Dispose();
  225. clientSession = null;
  226. _logger.Verbose("Stopped existing session of client '{0}'.", connectPacket.ClientId);
  227. }
  228. else
  229. {
  230. _logger.Verbose("Reusing existing session of client '{0}'.", connectPacket.ClientId);
  231. }
  232. }
  233. var isExistingSession = true;
  234. if (clientSession == null)
  235. {
  236. isExistingSession = false;
  237. clientSession = new MqttClientSession(connectPacket.ClientId, _options, this, _retainedMessagesManager, _logger);
  238. _sessions[connectPacket.ClientId] = clientSession;
  239. _logger.Verbose("Created a new session for client '{0}'.", connectPacket.ClientId);
  240. }
  241. return new GetOrCreateClientSessionResult { IsExistingSession = isExistingSession, Session = clientSession };
  242. }
  243. finally
  244. {
  245. _sessionsLock.Exit();
  246. }
  247. }
  248. private async Task DispatchApplicationMessageAsync(MqttClientSession senderClientSession, MqttApplicationMessage applicationMessage)
  249. {
  250. try
  251. {
  252. var interceptorContext = InterceptApplicationMessage(senderClientSession, applicationMessage);
  253. if (interceptorContext.CloseConnection)
  254. {
  255. senderClientSession.Stop(MqttClientDisconnectType.NotClean);
  256. }
  257. if (interceptorContext.ApplicationMessage == null || !interceptorContext.AcceptPublish)
  258. {
  259. return;
  260. }
  261. if (applicationMessage.Retain)
  262. {
  263. await _retainedMessagesManager.HandleMessageAsync(senderClientSession?.ClientId, applicationMessage).ConfigureAwait(false);
  264. }
  265. Server.OnApplicationMessageReceived(senderClientSession?.ClientId, applicationMessage);
  266. }
  267. catch (Exception exception)
  268. {
  269. _logger.Error(exception, "Error while processing application message");
  270. }
  271. await _sessionsLock.EnterAsync(CancellationToken.None).ConfigureAwait(false);
  272. try
  273. {
  274. foreach (var clientSession in _sessions.Values)
  275. {
  276. await clientSession.EnqueueApplicationMessageAsync(applicationMessage).ConfigureAwait(false);
  277. }
  278. }
  279. finally
  280. {
  281. _sessionsLock.Exit();
  282. }
  283. }
  284. private MqttApplicationMessageInterceptorContext InterceptApplicationMessage(MqttClientSession senderClientSession, MqttApplicationMessage applicationMessage)
  285. {
  286. var interceptorContext = new MqttApplicationMessageInterceptorContext(
  287. senderClientSession?.ClientId,
  288. applicationMessage);
  289. var interceptor = _options.ApplicationMessageInterceptor;
  290. if (interceptor == null)
  291. {
  292. return interceptorContext;
  293. }
  294. interceptor(interceptorContext);
  295. return interceptorContext;
  296. }
  297. }
  298. }