From 2ce9b94ec9a104a629851eb4766bece4a6143098 Mon Sep 17 00:00:00 2001 From: Christian <6939810+chkr1011@users.noreply.github.com> Date: Sun, 20 Feb 2022 21:10:46 +0100 Subject: [PATCH] v4.0.0 (#1357) --- .bettercodehub.yml | 3 - .gitattributes | 64 +- .github/ISSUE_TEMPLATE/bug_report.md | 21 +- .github/workflows/ReleaseNotes.md | 27 + .github/workflows/ci.yml | 80 +- Build/MQTTnet.AspNetCore.nuspec | 49 - Build/MQTTnet.Extensions.ManagedClient.nuspec | 52 - Build/MQTTnet.Extensions.Rpc.nuspec | 52 - Build/MQTTnet.Extensions.WebSocket4Net.nuspec | 53 - Build/MQTTnet.nuspec | 78 - Build/build.ps1 | 99 -- Build/codeSigningKey.pfx | Bin 1764 -> 0 bytes Build/upload.ps1 | 13 - Documents/Import_CodeSigningKey.md | 9 - Images/nuget.png | Bin 0 -> 4143 bytes LICENSE | 5 +- MQTTnet.noUWP.sln | 258 --- MQTTnet.sln | 279 +--- MQTTnet.sln.DotSettings | 225 +++ README.md | 110 +- Samples/Client/Client_Connection_Samples.cs | 271 +++ Samples/Client/Client_Publish_Samples.cs | 42 + Samples/Client/Client_Subscribe_Samples.cs | 80 + Samples/Diagnostics/Logger_Samples.cs | 91 + .../Diagnostics/PackageInspection_Samples.cs | 51 + Samples/Helpers/ObjectExtensions.cs | 25 + Samples/MQTTnet.Samples.csproj | 26 + .../Managed_Client_Simple_Samples.cs | 44 + Samples/Program.cs | 48 + Samples/RpcClient/RcpClient_Samples.cs | 84 + Samples/Server/Server_Simple_Samples.cs | 91 + .../MQTTnet.AspNetCore.Tests.csproj | 35 + .../Mockups/ConnectionContextMockup.cs | 6 +- .../Mockups/ConnectionHandlerMockup.cs | 16 +- .../Mockups/DuplexPipeMockup.cs | 6 +- .../Mockups/LimitedMemoryPool.cs | 6 +- .../Mockups/MemoryOwner.cs | 6 +- .../MqttConnectionContextTest.cs | 19 +- .../ReaderExtensionsTest.cs | 24 +- .../MQTTnet.AspTestApp.csproj | 24 + Source/MQTTnet.AspTestApp/Pages/Index.cshtml | 34 + .../MQTTnet.AspTestApp/Pages/Index.cshtml.cs | 23 + .../Pages/Shared/_Layout.cshtml | 14 + .../Pages/_ViewImports.cshtml | 2 + .../Pages/_ViewStart.cshtml | 3 + Source/MQTTnet.AspTestApp/Program.cs | 73 + .../appsettings.Development.json | 9 + Source/MQTTnet.AspTestApp/appsettings.json | 9 + Source/MQTTnet.AspTestApp/libman.json | 5 + .../ApplicationBuilderExtensions.cs | 12 +- .../AspNetMqttServerOptionsBuilder.cs | 10 +- .../MqttClientConnectionContextFactory.cs | 18 +- .../Client/Tcp/BufferExtensions.cs | 6 +- .../Client/Tcp/DuplexPipe.cs | 6 +- .../Client/Tcp/SocketAwaitable.cs | 6 +- .../Client/Tcp/SocketReceiver.cs | 6 +- .../Client/Tcp/SocketSender.cs | 6 +- .../Client/Tcp/TcpConnection.cs | 10 +- .../ConnectionBuilderExtensions.cs | 16 + .../ConnectionRouteBuilderExtensions.cs | 8 +- .../EndpointRouterExtensions.cs | 8 +- .../Extensions/ConnectionBuilderExtensions.cs | 12 - .../MQTTnet.AspNetCore.csproj | 99 +- .../MqttConnectionContext.cs | 25 +- .../MqttConnectionHandler.cs | 20 +- Source/MQTTnet.AspnetCore/MqttHostedServer.cs | 18 +- .../MqttSubProtocolSelector.cs | 6 +- .../MqttWebSocketServerAdapter.cs | 25 +- .../{Extensions => }/ReaderExtensions.cs | 25 +- .../ServiceCollectionExtensions.cs | 50 +- .../SpanBasedMqttPacketBodyReader.cs | 129 -- .../SpanBasedMqttPacketWriter.cs | 144 -- Source/MQTTnet.Benchmarks/BaseBenchmark.cs | 6 + .../ChannelAdapterBenchmark.cs | 12 +- .../Configurations/AllowNonOptimized.cs | 6 +- .../Configurations/BaseConfig.cs | 6 +- .../Configurations/RuntimeCompareConfig.cs | 19 + .../MQTTnet.Benchmarks/LoggerBenchmark.cs | 10 +- .../MQTTnet.Benchmarks.csproj | 39 + .../MessageDeliveryBenchmark.cs | 234 +++ .../MessageProcessingBenchmark.cs | 21 +- ...rocessingMqttConnectionContextBenchmark.cs | 15 +- .../MqttPacketReaderWriterBenchmark.cs | 93 ++ .../MqttTcpChannelBenchmark.cs | 23 +- .../MQTTnet.Benchmarks/Program.cs | 37 +- .../RoundtripProcessingBenchmark.cs | 30 + .../MQTTnet.Benchmarks/SerializerBenchmark.cs | 27 +- .../ServerProcessingBenchmark.cs | 36 + .../MQTTnet.Benchmarks/SubscribeBenchmark.cs | 61 + .../MQTTnet.Benchmarks/TcpPipesBenchmark.cs | 8 +- .../TopicFilterComparerBenchmark.cs | 277 +++ Source/MQTTnet.Benchmarks/TopicGenerator.cs | 81 + .../UnsubscribeBenchmark.cs | 61 + .../MQTTnet.Benchmarks/packages.config | 0 .../ApplicationMessageProcessedEventArgs.cs | 15 +- ...licationMessageProcessedHandlerDelegate.cs | 31 - .../ApplicationMessageSkippedEventArgs.cs | 6 +- ...pplicationMessageSkippedHandlerDelegate.cs | 6 +- .../ConnectingFailedEventArgs.cs | 21 + .../ConnectingFailedHandlerDelegate.cs | 31 - .../IApplicationMessageProcessedHandler.cs | 9 - .../IApplicationMessageSkippedHandler.cs | 6 +- .../IConnectingFailedHandler.cs | 9 - .../IManagedMqttClient.cs | 51 - .../IManagedMqttClientOptions.cs | 27 - .../IManagedMqttClientStorage.cs | 6 +- ...SynchronizingSubscriptionsFailedHandler.cs | 6 +- .../MQTTnet.Extensions.ManagedClient.csproj | 98 +- .../ManagedMqttApplicationMessage.cs | 6 +- .../ManagedMqttApplicationMessageBuilder.cs | 6 +- .../ManagedMqttClient.cs | 627 ++++--- .../ManagedMqttClientExtensions.cs | 240 +-- .../ManagedMqttClientOptions.cs | 14 +- .../ManagedMqttClientOptionsBuilder.cs | 8 +- .../ManagedMqttClientStorageManager.cs | 6 +- .../ManagedProcessFailedEventArgs.cs | 6 +- .../MqttFactoryExtensions.cs | 22 +- .../ReconnectionResult.cs | 6 +- ...izingSubscriptionsFailedHandlerDelegate.cs | 6 +- .../MQTTnet.Extensions.Rpc/IMqttRpcClient.cs | 14 - .../MQTTnet.Extensions.Rpc.csproj | 98 +- .../MQTTnet.Extensions.Rpc.csproj.DotSettings | 3 + .../MqttFactoryExtensions.cs | 37 + .../MQTTnet.Extensions.Rpc/MqttRpcClient.cs | 40 +- .../MqttRpcClientExtensions.cs | 8 +- .../Options/IMqttRpcClientOptions.cs | 9 - .../Options/MqttRpcClientOptions.cs | 8 +- .../Options/MqttRpcClientOptionsBuilder.cs | 9 +- .../Options/MqttRpcTopicPair.cs | 6 +- ...ultMqttRpcClientTopicGenerationStrategy.cs | 8 +- .../IMqttRpcClientTopicGenerationStrategy.cs | 6 +- .../TopicGeneration/TopicGenerationContext.cs | 12 +- ...cAwareApplicationMessageReceivedHandler.cs | 32 - .../MQTTnet.Extensions.WebSocket4Net.csproj | 112 +- .../MqttFactoryExtensions.cs | 10 +- .../WebSocket4NetMqttChannel.cs | 54 +- .../WebSocket4NetMqttClientAdapterFactory.cs | 32 +- .../MQTTnet.TestApp}/ClientFlowTest.cs | 13 +- .../MQTTnet.TestApp}/ClientTest.cs | 37 +- Source/MQTTnet.TestApp/MQTTnet.TestApp.csproj | 32 + .../MQTTnet.TestApp}/ManagedClientTest.cs | 41 +- .../MQTTnet.TestApp/MessageThroughputTest.cs | 344 ++++ .../MQTTnet.TestApp}/MqttNetConsoleLogger.cs | 10 +- .../MQTTnet.TestApp}/PerformanceTest.cs | 53 +- Source/MQTTnet.TestApp/Program.cs | 95 ++ .../MQTTnet.TestApp}/PublicBrokerTest.cs | 27 +- .../MQTTnet.TestApp}/ServerAndClientTest.cs | 16 +- Source/MQTTnet.TestApp/ServerTest.cs | 198 +++ .../MQTTnet.TestApp}/Start.bat | 0 Source/MQTTnet.TestApp/TopicGenerator.cs | 84 + .../MQTTnet.Tests}/BaseTestClass.cs | 6 +- .../Client/LowLevelMqttClient_Tests.cs | 216 +++ .../Client/ManagedMqttClient_Tests.cs | 392 +++-- .../Client/MqttClientOptionsBuilder_Tests.cs | 11 +- .../MQTTnet.Tests}/Client/MqttClient_Tests.cs | 1022 ++++++------ .../Diagnostics}/Logger_Tests.cs | 10 +- .../Diagnostics/PacketInspection_Tests.cs | 48 + .../Diagnostics}/SourceLogger_Tests.cs | 10 +- .../MQTTnet.Tests}/Extension_Tests.cs | 17 +- .../Extensions/MqttPacketWriterExtensions.cs | 20 + .../Factory/MqttFactory_Tests.cs | 61 +- ...MqttPacketSerialization_V3_Binary_Tests.cs | 709 ++++---- .../MqttPacketSerialization_V3_Tests.cs | 495 ++++++ .../MqttPacketSerialization_V5_Tests.cs | 454 +++++ .../Formatter/MqttPacketWriter_Tests.cs | 92 + .../Internal}/AsyncLock_Tests.cs | 8 +- .../Internal}/AsyncQueue_Tests.cs | 8 +- .../Internal}/BlockingQueue_Tests.cs | 10 +- .../Internal}/CrossPlatformSocket_Tests.cs | 10 +- .../Internal/MqttPacketBus_Tests.cs | 120 ++ Source/MQTTnet.Tests/MQTTnet.Tests.csproj | 27 + .../MQTTnet.Tests}/MQTTv5/Client_Tests.cs | 94 +- .../MQTTnet.Tests}/MQTTv5/Server_Tests.cs | 46 +- .../Mockups/MqttPacketAsserts.cs | 22 + .../TestApplicationMessageReceivedHandler.cs | 52 + .../MQTTnet.Tests/Mockups/TestEnvironment.cs | 381 +++++ .../MQTTnet.Tests}/Mockups/TestLogger.cs | 8 +- .../MqttApplicationMessageBuilder_Tests.cs | 6 +- .../MqttPacketIdentifierProvider_Tests.cs | 6 +- .../MqttPacketSerializationHelper.cs | 65 + .../MQTTnet.Tests/MqttPacketWriter_Tests.cs | 34 + .../MQTTnet.Tests}/MqttTcpChannel_Tests.cs | 6 +- .../MqttTopicValidatorSubscribe_Tests.cs | 4 + .../MqttTopicValidator_Tests.cs | 6 +- Source/MQTTnet.Tests/Protocol_Tests.cs | 34 + .../MQTTnet.Tests}/RPC_Tests.cs | 47 +- .../MQTTnet.Tests}/RoundtripTime_Tests.cs | 26 +- .../Server/Assigned_Client_ID_Tests.cs | 46 +- .../MQTTnet.Tests}/Server/Connection_Tests.cs | 6 +- .../MQTTnet.Tests}/Server/Events_Tests.cs | 63 +- .../MQTTnet.Tests}/Server/General.cs | 1484 +++++++---------- .../MQTTnet.Tests/Server/Injection_Tests.cs | 64 + .../MQTTnet.Tests}/Server/Keep_Alive_Tests.cs | 10 +- Source/MQTTnet.Tests/Server/Load_Tests.cs | 172 ++ .../Server/MqttSubscriptionsManager_Tests.cs | 139 ++ .../MQTTnet.Tests}/Server/No_Local_Tests.cs | 8 +- .../Server/Retain_As_Published_Tests.cs | 10 +- .../Server/Retain_Handling_Tests.cs | 10 +- .../Server/Retained_Messages_Tests.cs | 10 +- .../MQTTnet.Tests}/Server/Security_Tests.cs | 34 +- .../Server/Server_Reference_Tests.cs | 23 +- Source/MQTTnet.Tests/Server/Session_Tests.cs | 317 ++++ .../Server/Shared_Subscriptions_Tests.cs | 50 + .../MQTTnet.Tests}/Server/Status_Tests.cs | 46 +- .../MQTTnet.Tests/Server/Subscribe_Tests.cs | 317 ++++ .../Server/Subscription_Identifier_Tests.cs | 16 +- .../Server/Subscription_TopicHash_Tests.cs | 612 +++++++ .../Server/Topic_Alias_Tests.cs | 14 +- .../Server/User_Properties_Tests.cs | 16 +- .../Wildcard_Subscription_Available_Tests.cs | 6 +- .../TopicFilterComparer_Tests.cs | 173 ++ Source/MQTTnet.Tests/TopicGenerator.cs | 81 + Source/MQTTnet/Adapter/IMqttChannelAdapter.cs | 14 +- .../Adapter/IMqttClientAdapterFactory.cs | 9 +- Source/MQTTnet/Adapter/IMqttServerAdapter.cs | 9 +- Source/MQTTnet/Adapter/MqttChannelAdapter.cs | 371 ++--- .../Adapter/MqttConnectingFailedException.cs | 6 +- ...ectorHandler.cs => MqttPacketInspector.cs} | 56 +- Source/MQTTnet/Adapter/ReceivedMqttPacket.cs | 17 +- .../Certificates/BlobCertificateProvider.cs | 6 +- .../Certificates/ICertificateProvider.cs | 6 +- .../Certificates/X509CertificateProvider.cs | 6 +- Source/MQTTnet/Channel/IMqttChannel.cs | 6 +- .../Connecting/IMqttClientConnectedHandler.cs | 9 - .../Connecting/MqttClientConnectResult.cs | 13 +- .../Connecting/MqttClientConnectResultCode.cs | 6 +- .../MqttClientConnectResultFactory.cs | 107 ++ .../MqttClientConnectedEventArgs.cs | 16 +- .../MqttClientConnectedHandlerDelegate.cs | 31 - .../MqttClientConnectingEventArgs.cs | 18 + .../IMqttClientDisconnectedHandler.cs | 9 - .../MqttClientDisconnectOptions.cs | 8 +- .../MqttClientDisconnectOptionsBuilder.cs | 33 + .../MqttClientDisconnectReason.cs | 6 +- .../MqttClientDisconnectedEventArgs.cs | 32 +- .../MqttClientDisconnectedHandlerDelegate.cs | 31 - ...ttExtendedAuthenticationExchangeHandler.cs | 8 +- ...ttExtendedAuthenticationExchangeContext.cs | 20 +- .../MqttExtendedAuthenticationExchangeData.cs | 8 +- Source/MQTTnet/Client/IMqttClient.cs | 50 - Source/MQTTnet/Client/IMqttClientFactory.cs | 28 - Source/MQTTnet/Client/MqttClient.cs | 261 +-- .../Client/MqttClientConnectionStatus.cs | 4 + Source/MQTTnet/Client/MqttClientExtensions.cs | 332 +--- .../Client/MqttPacketIdentifierProvider.cs | 6 +- .../Options/IMqttClientChannelOptions.cs | 6 +- .../Client/Options/IMqttClientCredentials.cs | 8 - .../Options/IMqttClientCredentialsProvider.cs | 13 + .../Client/Options/IMqttClientOptions.cs | 114 -- ...entCertificateValidationCallbackContext.cs | 16 - ...qttClientCertificateValidationEventArgs.cs | 21 + .../Client/Options/MqttClientCredentials.cs | 27 +- ...ientDefaultCertificateValidationHandler.cs | 40 + .../Client/Options/MqttClientOptions.cs | 188 ++- .../Options/MqttClientOptionsBuilder.cs | 397 +++-- .../MqttClientOptionsBuilderTlsParameters.cs | 21 +- ...ClientOptionsBuilderWebSocketParameters.cs | 8 +- .../Client/Options/MqttClientTcpOptions.cs | 22 +- .../Options/MqttClientTcpOptionsExtensions.cs | 8 +- .../Client/Options/MqttClientTlsOptions.cs | 22 +- .../Options/MqttClientWebSocketOptions.cs | 8 +- .../MqttClientWebSocketProxyOptions.cs | 6 +- .../Publishing/MqttClientPublishReasonCode.cs | 7 +- .../Publishing/MqttClientPublishResult.cs | 14 +- .../MqttClientPublishResultFactory.cs | 79 + .../IMqttApplicationMessageReceivedHandler.cs | 9 - ...MqttApplicationMessageReceivedEventArgs.cs | 22 +- ...plicationMessageReceivedHandlerDelegate.cs | 32 - ...qttApplicationMessageReceivedReasonCode.cs | 6 +- .../Subscribing/MqttClientSubscribeOptions.cs | 10 +- .../MqttClientSubscribeOptionsBuilder.cs | 10 +- .../Subscribing/MqttClientSubscribeResult.cs | 32 +- .../MqttClientSubscribeResultCode.cs | 15 +- .../MqttClientSubscribeResultFactory.cs | 57 + .../MqttClientSubscribeResultItem.cs | 20 +- .../MqttClientUnsubscribeOptions.cs | 10 +- .../MqttClientUnsubscribeOptionsBuilder.cs | 8 +- .../MqttClientUnsubscribeResult.cs | 30 +- .../MqttClientUnsubscribeResultCode.cs | 6 +- .../MqttClientUnsubscribeResultFactory.cs | 63 + .../MqttClientUnsubscribeResultItem.cs | 20 +- .../Diagnostics/Logger/IMqttNetLogger.cs | 8 +- .../Diagnostics/Logger/MqttNetEventLogger.cs | 18 +- .../Diagnostics/Logger/MqttNetLogLevel.cs | 6 +- .../Diagnostics/Logger/MqttNetLogMessage.cs | 8 +- .../MqttNetLogMessagePublishedEventArgs.cs | 8 +- .../Diagnostics/Logger/MqttNetNullLogger.cs | 10 +- .../Diagnostics/Logger/MqttNetSourceLogger.cs | 8 +- .../Logger/MqttNetSourceLoggerExtensions.cs | 18 +- .../PacketInspection/IMqttPacketInspector.cs | 7 - .../InspectMqttPacketEventArgs.cs | 15 + .../MqttPacketFlowDirection.cs | 6 +- .../ProcessMqttPacketContext.cs | 9 - .../Runtime/TargetFrameworkProvider.cs | 8 +- .../Exceptions/MqttCommunicationException.cs | 6 +- .../MqttCommunicationTimedOutException.cs | 10 +- .../Exceptions/MqttConfigurationException.cs | 6 +- .../MqttProtocolViolationException.cs | 6 +- ...ttUnexpectedDisconnectReceivedException.cs | 14 +- .../MqttClientOptionsBuilderExtension.cs | 48 - .../Extensions/UserPropertyExtension.cs | 16 - .../MQTTnet/Formatter/IMqttDataConverter.cs | 53 - .../Formatter/IMqttPacketBodyReader.cs | 29 - .../MQTTnet/Formatter/IMqttPacketFormatter.cs | 15 +- Source/MQTTnet/Formatter/IMqttPacketWriter.cs | 29 - .../MqttApplicationMessageFactory.cs | 37 + Source/MQTTnet/Formatter/MqttBufferReader.cs | 150 ++ Source/MQTTnet/Formatter/MqttBufferWriter.cs | 284 ++++ .../Formatter/MqttConnAckPacketFactory.cs | 41 + .../Formatter/MqttConnectPacketFactory.cs | 58 + .../MqttConnectReasonCodeConverter.cs | 96 ++ .../Formatter/MqttDisconnectPacketFactory.cs | 47 + Source/MQTTnet/Formatter/MqttFixedHeader.cs | 8 +- .../MQTTnet/Formatter/MqttPacketBodyReader.cs | 147 -- Source/MQTTnet/Formatter/MqttPacketBuffer.cs | 65 + .../MQTTnet/Formatter/MqttPacketFactories.cs | 32 + .../Formatter/MqttPacketFormatterAdapter.cs | 125 +- Source/MQTTnet/Formatter/MqttPacketWriter.cs | 258 --- .../MQTTnet/Formatter/MqttProtocolVersion.cs | 8 +- .../Formatter/MqttPubAckPacketFactory.cs | 68 + .../Formatter/MqttPubCompPacketFactory.cs | 28 + .../Formatter/MqttPubRecPacketFactory.cs | 62 + .../Formatter/MqttPubRelPacketFactory.cs | 28 + .../Formatter/MqttPublishPacketFactory.cs | 84 + .../Formatter/MqttSubAckPacketFactory.cs | 36 + .../Formatter/MqttSubscribePacketFactory.cs | 30 + .../Formatter/MqttUnsubAckPacketFactory.cs | 36 + .../Formatter/MqttUnsubscribePacketFactory.cs | 33 + .../Formatter/ReadFixedHeaderResult.cs | 22 +- .../Formatter/V3/MqttV310DataConverter.cs | 278 --- .../Formatter/V3/MqttV310PacketFormatter.cs | 614 ------- .../Formatter/V3/MqttV311PacketFormatter.cs | 104 -- .../Formatter/V3/MqttV3PacketFormatter.cs | 807 +++++++++ .../Formatter/V5/MqttV500DataConverter.cs | 369 ---- .../Formatter/V5/MqttV500PacketDecoder.cs | 901 ---------- .../Formatter/V5/MqttV500PacketEncoder.cs | 588 ------- .../Formatter/V5/MqttV500PacketFormatter.cs | 38 - .../Formatter/V5/MqttV500PropertiesWriter.cs | 284 ---- .../Formatter/V5/MqttV5PacketDecoder.cs | 770 +++++++++ .../Formatter/V5/MqttV5PacketEncoder.cs | 538 ++++++ .../Formatter/V5/MqttV5PacketFormatter.cs | 30 + ...iesReader.cs => MqttV5PropertiesReader.cs} | 178 +- .../Formatter/V5/MqttV5PropertiesWriter.cs | 351 ++++ .../MQTTnet/IApplicationMessagePublisher.cs | 11 - Source/MQTTnet/IApplicationMessageReceiver.cs | 13 - Source/MQTTnet/IMqttFactory.cs | 15 - .../Implementations/CrossPlatformSocket.cs | 47 +- .../MqttClientAdapterFactory.cs | 25 +- .../Implementations/MqttTcpChannel.Uwp.cs | 10 +- .../MQTTnet/Implementations/MqttTcpChannel.cs | 116 +- .../MqttTcpServerAdapter.Uwp.cs | 44 +- .../Implementations/MqttTcpServerAdapter.cs | 36 +- .../Implementations/MqttTcpServerListener.cs | 40 +- .../Implementations/MqttWebSocketChannel.cs | 10 +- .../PlatformAbstractionLayer.cs | 8 +- Source/MQTTnet/Internal/AsyncEvent.cs | 88 + .../MQTTnet/Internal/AsyncEventInvocator.cs | 48 + Source/MQTTnet/Internal/AsyncLock.cs | 6 +- Source/MQTTnet/Internal/AsyncQueue.cs | 6 +- .../Internal/AsyncQueueDequeueResult.cs | 6 +- .../Internal/AsyncTaskCompletionSource.cs | 75 + Source/MQTTnet/Internal/BlockingQueue.cs | 6 +- Source/MQTTnet/Internal/Disposable.cs | 6 +- Source/MQTTnet/Internal/MqttPacketBus.cs | 140 ++ Source/MQTTnet/Internal/MqttPacketBusItem.cs | 49 + .../Internal/MqttPacketBusPartition.cs | 15 + Source/MQTTnet/Internal/MqttTaskTimeout.cs | 30 +- Source/MQTTnet/Internal/TaskExtensions.cs | 7 +- Source/MQTTnet/Internal/TestMqttChannel.cs | 6 +- .../LowLevelClient/ILowLevelMqttClient.cs | 19 - .../LowLevelClient/LowLevelMqttClient.cs | 121 +- Source/MQTTnet/MQTTnet.csproj | 146 +- Source/MQTTnet/MQTTnet.csproj.DotSettings | 19 + Source/MQTTnet/MqttApplicationMessage.cs | 155 +- .../MQTTnet/MqttApplicationMessageBuilder.cs | 403 ++--- .../MqttApplicationMessageExtensions.cs | 6 +- Source/MQTTnet/MqttFactory.cs | 66 +- Source/MQTTnet/MqttTopicFilter.cs | 65 - Source/MQTTnet/MqttTopicFilterBuilder.cs | 35 +- .../MQTTnet/MqttTopicFilterCompareResult.cs | 17 + Source/MQTTnet/MqttTopicFilterComparer.cs | 178 ++ .../PacketDispatcher/IMqttPacketAwaitable.cs | 8 +- .../PacketDispatcher/MqttPacketAwaitable.cs | 63 +- .../MqttPacketAwaitableFilter.cs | 6 +- .../PacketDispatcher/MqttPacketDispatcher.cs | 10 +- .../Packets/IMqttPacketWithIdentifier.cs | 7 - Source/MQTTnet/Packets/MqttAuthPacket.cs | 23 +- .../Packets/MqttAuthPacketProperties.cs | 15 - Source/MQTTnet/Packets/MqttBasePacket.cs | 6 - Source/MQTTnet/Packets/MqttConnAckPacket.cs | 63 +- .../Packets/MqttConnAckPacketProperties.cs | 42 - Source/MQTTnet/Packets/MqttConnectPacket.cs | 67 +- .../Packets/MqttConnectPacketProperties.cs | 27 - .../MQTTnet/Packets/MqttDisconnectPacket.cs | 38 +- .../Packets/MqttDisconnectPacketProperties.cs | 15 - Source/MQTTnet/Packets/MqttPacket.cs | 10 + .../Packets/MqttPacketWithIdentifier.cs | 11 + Source/MQTTnet/Packets/MqttPingReqPacket.cs | 8 +- Source/MQTTnet/Packets/MqttPingRespPacket.cs | 8 +- Source/MQTTnet/Packets/MqttPubAckPacket.cs | 32 +- .../Packets/MqttPubAckPacketProperties.cs | 11 - Source/MQTTnet/Packets/MqttPubCompPacket.cs | 32 +- .../Packets/MqttPubCompPacketProperties.cs | 11 - Source/MQTTnet/Packets/MqttPubRecPacket.cs | 32 +- .../Packets/MqttPubRecPacketProperties.cs | 11 - Source/MQTTnet/Packets/MqttPubRelPacket.cs | 32 +- .../Packets/MqttPubRelPacketProperties.cs | 11 - Source/MQTTnet/Packets/MqttPublishPacket.cs | 38 +- .../Packets/MqttPublishPacketProperties.cs | 24 - Source/MQTTnet/Packets/MqttSubAckPacket.cs | 34 +- .../Packets/MqttSubAckPacketProperties.cs | 11 - Source/MQTTnet/Packets/MqttSubscribePacket.cs | 23 +- .../Packets/MqttSubscribePacketProperties.cs | 14 - Source/MQTTnet/Packets/MqttTopicFilter.cs | 55 + Source/MQTTnet/Packets/MqttUnsubAckPacket.cs | 37 +- .../Packets/MqttUnsubAckPacketProperties.cs | 11 - .../MQTTnet/Packets/MqttUnsubscribePacket.cs | 23 +- .../MqttUnsubscribePacketProperties.cs | 9 - Source/MQTTnet/Packets/MqttUserProperty.cs | 26 +- Source/MQTTnet/Properties/AssemblyInfo.cs | 7 +- .../Protocol/MqttAuthenticateReasonCode.cs | 6 +- .../MQTTnet/Protocol/MqttConnectReasonCode.cs | 6 +- .../MqttConnectReasonCodeConverter.cs | 91 - .../MQTTnet/Protocol/MqttConnectReturnCode.cs | 6 +- .../MQTTnet/Protocol/MqttControlPacketType.cs | 6 +- .../Protocol/MqttDisconnectReasonCode.cs | 6 +- .../Protocol/MqttPayloadFormatIndicator.cs | 6 +- .../{MqttPropertyID.cs => MqttPropertyId.cs} | 8 +- .../MQTTnet/Protocol/MqttPubAckReasonCode.cs | 6 +- .../MQTTnet/Protocol/MqttPubCompReasonCode.cs | 6 +- .../MQTTnet/Protocol/MqttPubRecReasonCode.cs | 6 +- .../MQTTnet/Protocol/MqttPubRelReasonCode.cs | 6 +- .../Protocol/MqttQualityOfServiceLevel.cs | 6 +- Source/MQTTnet/Protocol/MqttRetainHandling.cs | 6 +- .../Protocol/MqttSubscribeReasonCode.cs | 17 +- .../Protocol/MqttSubscribeReturnCode.cs | 6 +- Source/MQTTnet/Protocol/MqttTopicValidator.cs | 21 +- .../Protocol/MqttUnsubscribeReasonCode.cs | 6 +- .../ApplicationMessageNotConsumedEventArgs.cs | 21 + .../ClientConnectedEventArgs.cs} | 8 +- .../ClientDisconnectedEventArgs.cs} | 6 +- .../ClientSubscribedTopicEventArgs.cs} | 9 +- .../ClientUnsubscribedTopicEventArgs.cs} | 8 +- .../Events/InterceptingPacketEventArgs.cs | 39 + .../Events/InterceptingPublishEventArgs.cs | 44 + .../InterceptingSubscriptionEventArgs.cs | 69 + .../InterceptingUnsubscriptionEventArgs.cs | 54 + .../LoadingRetainedMessagesEventArgs.cs | 14 + .../Events/PreparingSessionEventArgs.cs | 50 + .../Events/RetainedMessageChangedEventArgs.cs | 18 + .../Server/Events/SessionDeletedEventArgs.cs | 16 + .../Events/ValidatingConnectionEventArgs.cs | 184 ++ .../Server/GetSubscribedMessagesFilter.cs | 9 - Source/MQTTnet/Server/IMqttClientSession.cs | 15 - .../Server/IMqttRetainedMessagesManager.cs | 20 - Source/MQTTnet/Server/IMqttServer.cs | 33 - ...MqttServerApplicationMessageInterceptor.cs | 9 - .../IMqttServerCertificateCredentials.cs | 7 - .../IMqttServerClientConnectedHandler.cs | 9 - .../IMqttServerClientDisconnectedHandler.cs | 9 - ...MqttServerClientMessageQueueInterceptor.cs | 9 - ...IMqttServerClientSubscribedTopicHandler.cs | 9 - ...qttServerClientUnsubscribedTopicHandler.cs | 9 - .../Server/IMqttServerConnectionValidator.cs | 9 - Source/MQTTnet/Server/IMqttServerFactory.cs | 21 - Source/MQTTnet/Server/IMqttServerOptions.cs | 35 - .../Server/IMqttServerPersistedSession.cs | 34 - .../IMqttServerPersistedSessionsStorage.cs | 11 - .../Server/IMqttServerStartedHandler.cs | 10 - .../Server/IMqttServerStoppedHandler.cs | 10 - Source/MQTTnet/Server/IMqttServerStorage.cs | 12 - .../IMqttServerSubscriptionInterceptor.cs | 9 - .../IMqttServerUnsubscriptionInterceptor.cs | 9 - .../Server/InjectedMqttApplicationMessage.cs | 20 + .../Internal/CheckSubscriptionsResult.cs | 20 +- .../ISubscriptionChangedNotification.cs | 13 + Source/MQTTnet/Server/Internal/MqttClient.cs | 533 ++++++ .../Server/Internal/MqttClientConnection.cs | 495 ------ .../MqttClientConnectionStatistics.cs | 99 -- .../Server/Internal/MqttClientSession.cs | 60 - ...ttClientSessionApplicationMessagesQueue.cs | 65 - .../Internal/MqttClientSessionsManager.cs | 777 ++++----- .../Server/Internal/MqttClientStatistics.cs | 95 ++ .../MqttClientSubscriptionsManager.cs | 567 +++++-- .../Internal/MqttRetainedMessagesManager.cs | 78 +- .../Internal/MqttServerEventContainer.cs | 48 + .../Internal/MqttServerEventDispatcher.cs | 141 -- .../Internal/MqttServerKeepAliveMonitor.cs | 31 +- Source/MQTTnet/Server/Internal/MqttSession.cs | 161 ++ .../Server/Internal/MqttSubscription.cs | 309 ++++ .../Internal/MqttTopicFilterComparer.cs | 128 -- .../Server/Internal/MqttUnsubscribeResult.cs | 16 + .../Server/Internal/SubscribeResult.cs | 23 +- .../MQTTnet/Server/Internal/Subscription.cs | 21 - .../Internal/TopicHashMaskSubscriptions.cs | 19 + ...qttApplicationMessageInterceptorContext.cs | 24 - .../Server/MqttClientDisconnectType.cs | 6 +- ...qttClientMessageQueueInterceptorContext.cs | 25 - ...ttClientMessageQueueInterceptorDelegate.cs | 32 - .../Server/MqttConnectionValidatorContext.cs | 181 -- .../Server/MqttPendingApplicationMessage.cs | 17 - .../MqttPendingMessagesOverflowStrategy.cs | 8 - .../Server/MqttQueuedApplicationMessage.cs | 28 - .../Server/MqttRetainedMessageMatch.cs | 15 + Source/MQTTnet/Server/MqttServer.cs | 326 ++-- ...erApplicationMessageInterceptorDelegate.cs | 32 - .../MqttServerCertificateCredentials.cs | 7 - ...qttServerClientConnectedHandlerDelegate.cs | 32 - ...ServerClientDisconnectedHandlerDelegate.cs | 32 - ...verClientSubscribedTopicHandlerDelegate.cs | 44 - ...rClientUnsubscribedTopicHandlerDelegate.cs | 32 - .../MqttServerConnectionValidatorDelegate.cs | 32 - Source/MQTTnet/Server/MqttServerExtensions.cs | 216 +-- ...edApplicationMessageInterceptorDelegate.cs | 51 - Source/MQTTnet/Server/MqttServerOptions.cs | 44 - .../MqttServerStartedHandlerDelegate.cs | 32 - .../MqttServerStoppedHandlerDelegate.cs | 32 - ...ttServerSubscriptionInterceptorDelegate.cs | 32 - .../Server/MqttServerTcpEndpointOptions.cs | 11 - .../Server/MqttServerTlsTcpEndpointOptions.cs | 56 - .../MqttSubscriptionInterceptorContext.cs | 28 - .../MqttUnsubscriptionInterceptorContext.cs | 29 - .../IMqttServerCertificateCredentials.cs | 11 + .../MqttPendingMessagesOverflowStrategy.cs | 13 + .../MqttServerCertificateCredentials.cs | 11 + .../Server/Options/MqttServerOptions.cs | 39 + .../{ => Options}/MqttServerOptionsBuilder.cs | 156 +- .../MqttServerTcpEndpointBaseOptions.cs | 13 +- .../Options/MqttServerTcpEndpointOptions.cs | 14 + .../MqttServerTlsTcpEndpointOptions.cs | 28 + Source/MQTTnet/Server/PublishResponse.cs | 19 + .../Server/Status/IMqttClientStatus.cs | 45 - .../Server/Status/IMqttSessionStatus.cs | 25 - .../MQTTnet/Server/Status/MqttClientStatus.cs | 55 +- .../Server/Status/MqttSessionStatus.cs | 67 +- Source/MQTTnet/Server/SubscribeResponse.cs | 24 + Source/MQTTnet/Server/UnsubscribeResponse.cs | 23 + Source/MQTTnet/codeSigningKey.pfx | Bin 1764 -> 0 bytes .../MQTTnet.AspNetCore.Tests.csproj | 29 - .../MqttPacketSerializerTests.cs | 22 - .../SpanBasedMqttPacketWriterTests.cs | 15 - Tests/MQTTnet.Benchmarks/App.config | 6 - .../Configurations/RuntimeCompareConfig.cs | 16 - .../MQTTnet.Benchmarks.csproj | 28 - .../TopicFilterComparerBenchmark.cs | 90 - .../Client/LowLevelMqttClient_Tests.cs | 160 -- .../Extensions/MqttPacketWriterExtensions.cs | 16 - Tests/MQTTnet.Core.Tests/MQTTnet.Tests.csproj | 20 - .../Mockups/MqttPacketAsserts.cs | 18 - .../TestApplicationMessageReceivedHandler.cs | 43 - .../Mockups/TestClientWrapper.cs | 101 -- .../Mockups/TestEnvironment.cs | 305 ---- .../Mockups/TestMqttCommunicationAdapter.cs | 90 - .../TestMqttCommunicationAdapterFactory.cs | 20 - .../Mockups/TestServerStorage.cs | 22 - .../Mockups/TestServerWrapper.cs | 141 -- .../MqttApplicationMessage_Tests.cs | 32 - .../MqttPacketReader_Tests.cs | 24 - .../MqttPacketWriter_Tests.cs | 30 - Tests/MQTTnet.Core.Tests/Protocol_Tests.cs | 25 - .../Server/MqttSubscriptionsManager_Tests.cs | 114 -- .../Server/Session_Tests.cs | 174 -- .../Server/Shared_Subscriptions_Tests.cs | 27 - .../Server/TopicFilterComparer_Tests.cs | 114 -- Tests/MQTTnet.Test.BlazorApp/Client/App.razor | 10 - .../MQTTnet.Test.BlazorApp.Client.csproj | 20 - .../Client/Pages/Counter.razor | 27 - .../Client/Pages/FetchData.razor | 46 - .../Client/Pages/Index.razor | 7 - .../MQTTnet.Test.BlazorApp/Client/Program.cs | 21 - .../Client/Shared/MainLayout.razor | 15 - .../Client/Shared/NavMenu.razor | 37 - .../Client/Shared/SurveyPrompt.razor | 16 - .../Client/_Imports.razor | 9 - .../Client/wwwroot/css/app.css | 183 -- .../wwwroot/css/bootstrap/bootstrap.min.css | 7 - .../wwwroot/css/open-iconic/FONT-LICENSE | 86 - .../wwwroot/css/open-iconic/ICON-LICENSE | 21 - .../Client/wwwroot/css/open-iconic/README.md | 114 -- .../font/css/open-iconic-bootstrap.min.css | 1 - .../open-iconic/font/fonts/open-iconic.eot | Bin 28196 -> 0 bytes .../open-iconic/font/fonts/open-iconic.otf | Bin 20996 -> 0 bytes .../open-iconic/font/fonts/open-iconic.svg | 543 ------ .../open-iconic/font/fonts/open-iconic.ttf | Bin 28028 -> 0 bytes .../open-iconic/font/fonts/open-iconic.woff | Bin 14984 -> 0 bytes .../Client/wwwroot/favicon.ico | Bin 32038 -> 0 bytes .../Client/wwwroot/index.html | 24 - .../Controllers/WeatherForecastController.cs | 40 - .../MQTTnet.Test.BlazorApp.Server.csproj | 17 - .../Server/Pages/Error.cshtml | 27 - .../Server/Pages/Error.cshtml.cs | 31 - .../Server/Pages/Shared/_Layout.cshtml | 20 - .../MQTTnet.Test.BlazorApp/Server/Program.cs | 26 - .../MQTTnet.Test.BlazorApp/Server/Startup.cs | 59 - .../Server/appsettings.Development.json | 9 - .../Server/appsettings.json | 10 - .../MQTTnet.Test.BlazorApp.Shared.csproj | 7 - .../Shared/WeatherForecast.cs | 17 - .../MQTTnet.TestApp.AspNetCore2.csproj | 25 - Tests/MQTTnet.TestApp.AspNetCore2/Program.cs | 26 - Tests/MQTTnet.TestApp.AspNetCore2/Startup.cs | 95 -- .../MQTTnet.TestApp.AspNetCore2/package.json | 12 - .../MQTTnet.TestApp.AspNetCore2/tsconfig.json | 12 - .../wwwroot/Index.html | 35 - .../wwwroot/app/app.ts | 58 - .../MQTTnet.TestApp.NetCore.csproj | 30 - Tests/MQTTnet.TestApp.NetCore/Program.cs | 192 --- Tests/MQTTnet.TestApp.NetCore/ServerTest.cs | 147 -- 608 files changed, 21393 insertions(+), 18449 deletions(-) delete mode 100644 .bettercodehub.yml create mode 100644 .github/workflows/ReleaseNotes.md delete mode 100644 Build/MQTTnet.AspNetCore.nuspec delete mode 100644 Build/MQTTnet.Extensions.ManagedClient.nuspec delete mode 100644 Build/MQTTnet.Extensions.Rpc.nuspec delete mode 100644 Build/MQTTnet.Extensions.WebSocket4Net.nuspec delete mode 100644 Build/MQTTnet.nuspec delete mode 100644 Build/build.ps1 delete mode 100644 Build/codeSigningKey.pfx delete mode 100644 Build/upload.ps1 delete mode 100644 Documents/Import_CodeSigningKey.md create mode 100644 Images/nuget.png delete mode 100644 MQTTnet.noUWP.sln create mode 100644 Samples/Client/Client_Connection_Samples.cs create mode 100644 Samples/Client/Client_Publish_Samples.cs create mode 100644 Samples/Client/Client_Subscribe_Samples.cs create mode 100644 Samples/Diagnostics/Logger_Samples.cs create mode 100644 Samples/Diagnostics/PackageInspection_Samples.cs create mode 100644 Samples/Helpers/ObjectExtensions.cs create mode 100644 Samples/MQTTnet.Samples.csproj create mode 100644 Samples/ManagedClient/Managed_Client_Simple_Samples.cs create mode 100644 Samples/Program.cs create mode 100644 Samples/RpcClient/RcpClient_Samples.cs create mode 100644 Samples/Server/Server_Simple_Samples.cs create mode 100644 Source/MQTTnet.AspNetCore.Tests/MQTTnet.AspNetCore.Tests.csproj rename {Tests => Source}/MQTTnet.AspNetCore.Tests/Mockups/ConnectionContextMockup.cs (71%) rename {Tests => Source}/MQTTnet.AspNetCore.Tests/Mockups/ConnectionHandlerMockup.cs (70%) rename {Tests => Source}/MQTTnet.AspNetCore.Tests/Mockups/DuplexPipeMockup.cs (70%) rename {Tests => Source}/MQTTnet.AspNetCore.Tests/Mockups/LimitedMemoryPool.cs (62%) rename {Tests => Source}/MQTTnet.AspNetCore.Tests/Mockups/MemoryOwner.cs (74%) rename {Tests => Source}/MQTTnet.AspNetCore.Tests/MqttConnectionContextTest.cs (91%) rename {Tests => Source}/MQTTnet.AspNetCore.Tests/ReaderExtensionsTest.cs (60%) create mode 100644 Source/MQTTnet.AspTestApp/MQTTnet.AspTestApp.csproj create mode 100644 Source/MQTTnet.AspTestApp/Pages/Index.cshtml create mode 100644 Source/MQTTnet.AspTestApp/Pages/Index.cshtml.cs create mode 100644 Source/MQTTnet.AspTestApp/Pages/Shared/_Layout.cshtml create mode 100644 Source/MQTTnet.AspTestApp/Pages/_ViewImports.cshtml create mode 100644 Source/MQTTnet.AspTestApp/Pages/_ViewStart.cshtml create mode 100644 Source/MQTTnet.AspTestApp/Program.cs create mode 100644 Source/MQTTnet.AspTestApp/appsettings.Development.json create mode 100644 Source/MQTTnet.AspTestApp/appsettings.json create mode 100644 Source/MQTTnet.AspTestApp/libman.json rename Source/MQTTnet.AspnetCore/{Extensions => }/ApplicationBuilderExtensions.cs (84%) create mode 100644 Source/MQTTnet.AspnetCore/ConnectionBuilderExtensions.cs rename Source/MQTTnet.AspnetCore/{Extensions => }/ConnectionRouteBuilderExtensions.cs (73%) rename Source/MQTTnet.AspnetCore/{Extensions => }/EndpointRouterExtensions.cs (67%) delete mode 100644 Source/MQTTnet.AspnetCore/Extensions/ConnectionBuilderExtensions.cs rename Source/MQTTnet.AspnetCore/{Extensions => }/ReaderExtensions.cs (80%) rename Source/MQTTnet.AspnetCore/{Extensions => }/ServiceCollectionExtensions.cs (66%) delete mode 100644 Source/MQTTnet.AspnetCore/SpanBasedMqttPacketBodyReader.cs delete mode 100644 Source/MQTTnet.AspnetCore/SpanBasedMqttPacketWriter.cs create mode 100644 Source/MQTTnet.Benchmarks/BaseBenchmark.cs rename {Tests => Source}/MQTTnet.Benchmarks/ChannelAdapterBenchmark.cs (86%) rename {Tests => Source}/MQTTnet.Benchmarks/Configurations/AllowNonOptimized.cs (69%) rename {Tests => Source}/MQTTnet.Benchmarks/Configurations/BaseConfig.cs (69%) create mode 100644 Source/MQTTnet.Benchmarks/Configurations/RuntimeCompareConfig.cs rename {Tests => Source}/MQTTnet.Benchmarks/LoggerBenchmark.cs (88%) create mode 100644 Source/MQTTnet.Benchmarks/MQTTnet.Benchmarks.csproj create mode 100644 Source/MQTTnet.Benchmarks/MessageDeliveryBenchmark.cs rename {Tests => Source}/MQTTnet.Benchmarks/MessageProcessingBenchmark.cs (66%) rename {Tests => Source}/MQTTnet.Benchmarks/MessageProcessingMqttConnectionContextBenchmark.cs (83%) create mode 100644 Source/MQTTnet.Benchmarks/MqttPacketReaderWriterBenchmark.cs rename {Tests => Source}/MQTTnet.Benchmarks/MqttTcpChannelBenchmark.cs (77%) rename {Tests => Source}/MQTTnet.Benchmarks/Program.cs (53%) create mode 100644 Source/MQTTnet.Benchmarks/RoundtripProcessingBenchmark.cs rename {Tests => Source}/MQTTnet.Benchmarks/SerializerBenchmark.cs (76%) create mode 100644 Source/MQTTnet.Benchmarks/ServerProcessingBenchmark.cs create mode 100644 Source/MQTTnet.Benchmarks/SubscribeBenchmark.cs rename {Tests => Source}/MQTTnet.Benchmarks/TcpPipesBenchmark.cs (87%) create mode 100644 Source/MQTTnet.Benchmarks/TopicFilterComparerBenchmark.cs create mode 100644 Source/MQTTnet.Benchmarks/TopicGenerator.cs create mode 100644 Source/MQTTnet.Benchmarks/UnsubscribeBenchmark.cs rename {Tests => Source}/MQTTnet.Benchmarks/packages.config (100%) delete mode 100644 Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageProcessedHandlerDelegate.cs create mode 100644 Source/MQTTnet.Extensions.ManagedClient/ConnectingFailedEventArgs.cs delete mode 100644 Source/MQTTnet.Extensions.ManagedClient/ConnectingFailedHandlerDelegate.cs delete mode 100644 Source/MQTTnet.Extensions.ManagedClient/IApplicationMessageProcessedHandler.cs delete mode 100644 Source/MQTTnet.Extensions.ManagedClient/IConnectingFailedHandler.cs delete mode 100644 Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClient.cs delete mode 100644 Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClientOptions.cs delete mode 100644 Source/MQTTnet.Extensions.Rpc/IMqttRpcClient.cs create mode 100644 Source/MQTTnet.Extensions.Rpc/MQTTnet.Extensions.Rpc.csproj.DotSettings create mode 100644 Source/MQTTnet.Extensions.Rpc/MqttFactoryExtensions.cs delete mode 100644 Source/MQTTnet.Extensions.Rpc/Options/IMqttRpcClientOptions.cs delete mode 100644 Source/MQTTnet.Extensions.Rpc/RpcAwareApplicationMessageReceivedHandler.cs rename {Tests/MQTTnet.TestApp.NetCore => Source/MQTTnet.TestApp}/ClientFlowTest.cs (80%) rename {Tests/MQTTnet.TestApp.NetCore => Source/MQTTnet.TestApp}/ClientTest.cs (75%) create mode 100644 Source/MQTTnet.TestApp/MQTTnet.TestApp.csproj rename {Tests/MQTTnet.TestApp.NetCore => Source/MQTTnet.TestApp}/ManagedClientTest.cs (62%) create mode 100644 Source/MQTTnet.TestApp/MessageThroughputTest.cs rename {Tests/MQTTnet.TestApp.NetCore => Source/MQTTnet.TestApp}/MqttNetConsoleLogger.cs (87%) rename {Tests/MQTTnet.TestApp.NetCore => Source/MQTTnet.TestApp}/PerformanceTest.cs (93%) create mode 100644 Source/MQTTnet.TestApp/Program.cs rename {Tests/MQTTnet.TestApp.NetCore => Source/MQTTnet.TestApp}/PublicBrokerTest.cs (89%) rename {Tests/MQTTnet.TestApp.NetCore => Source/MQTTnet.TestApp}/ServerAndClientTest.cs (60%) create mode 100644 Source/MQTTnet.TestApp/ServerTest.cs rename {Tests/MQTTnet.TestApp.NetCore => Source/MQTTnet.TestApp}/Start.bat (100%) create mode 100644 Source/MQTTnet.TestApp/TopicGenerator.cs rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/BaseTestClass.cs (73%) create mode 100644 Source/MQTTnet.Tests/Client/LowLevelMqttClient_Tests.cs rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/Client/ManagedMqttClient_Tests.cs (62%) rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/Client/MqttClientOptionsBuilder_Tests.cs (59%) rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/Client/MqttClient_Tests.cs (74%) rename {Tests/MQTTnet.Core.Tests/Logger => Source/MQTTnet.Tests/Diagnostics}/Logger_Tests.cs (87%) create mode 100644 Source/MQTTnet.Tests/Diagnostics/PacketInspection_Tests.cs rename {Tests/MQTTnet.Core.Tests/Logger => Source/MQTTnet.Tests/Diagnostics}/SourceLogger_Tests.cs (66%) rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/Extension_Tests.cs (88%) create mode 100644 Source/MQTTnet.Tests/Extensions/MqttPacketWriterExtensions.cs rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/Factory/MqttFactory_Tests.cs (84%) rename Tests/MQTTnet.Core.Tests/MqttPacketSerializer_Tests.cs => Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Binary_Tests.cs (62%) create mode 100644 Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Tests.cs create mode 100644 Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V5_Tests.cs create mode 100644 Source/MQTTnet.Tests/Formatter/MqttPacketWriter_Tests.cs rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests/Internal}/AsyncLock_Tests.cs (92%) rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests/Internal}/AsyncQueue_Tests.cs (93%) rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests/Internal}/BlockingQueue_Tests.cs (92%) rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests/Internal}/CrossPlatformSocket_Tests.cs (90%) create mode 100644 Source/MQTTnet.Tests/Internal/MqttPacketBus_Tests.cs create mode 100644 Source/MQTTnet.Tests/MQTTnet.Tests.csproj rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/MQTTv5/Client_Tests.cs (76%) rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/MQTTv5/Server_Tests.cs (83%) create mode 100644 Source/MQTTnet.Tests/Mockups/MqttPacketAsserts.cs create mode 100644 Source/MQTTnet.Tests/Mockups/TestApplicationMessageReceivedHandler.cs create mode 100644 Source/MQTTnet.Tests/Mockups/TestEnvironment.cs rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/Mockups/TestLogger.cs (72%) rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/MqttApplicationMessageBuilder_Tests.cs (92%) rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/MqttPacketIdentifierProvider_Tests.cs (76%) create mode 100644 Source/MQTTnet.Tests/MqttPacketSerializationHelper.cs create mode 100644 Source/MQTTnet.Tests/MqttPacketWriter_Tests.cs rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/MqttTcpChannel_Tests.cs (90%) rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/MqttTopicValidatorSubscribe_Tests.cs (87%) rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/MqttTopicValidator_Tests.cs (78%) create mode 100644 Source/MQTTnet.Tests/Protocol_Tests.cs rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/RPC_Tests.cs (82%) rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/RoundtripTime_Tests.cs (62%) rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/Server/Assigned_Client_ID_Tests.cs (65%) rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/Server/Connection_Tests.cs (92%) rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/Server/Events_Tests.cs (75%) rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/Server/General.cs (50%) create mode 100644 Source/MQTTnet.Tests/Server/Injection_Tests.cs rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/Server/Keep_Alive_Tests.cs (88%) create mode 100644 Source/MQTTnet.Tests/Server/Load_Tests.cs create mode 100644 Source/MQTTnet.Tests/Server/MqttSubscriptionsManager_Tests.cs rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/Server/No_Local_Tests.cs (82%) rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/Server/Retain_As_Published_Tests.cs (79%) rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/Server/Retain_Handling_Tests.cs (84%) rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/Server/Retained_Messages_Tests.cs (94%) rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/Server/Security_Tests.cs (77%) rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/Server/Server_Reference_Tests.cs (68%) create mode 100644 Source/MQTTnet.Tests/Server/Session_Tests.cs create mode 100644 Source/MQTTnet.Tests/Server/Shared_Subscriptions_Tests.cs rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/Server/Status_Tests.cs (73%) create mode 100644 Source/MQTTnet.Tests/Server/Subscribe_Tests.cs rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/Server/Subscription_Identifier_Tests.cs (82%) create mode 100644 Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/Server/Topic_Alias_Tests.cs (84%) rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/Server/User_Properties_Tests.cs (78%) rename {Tests/MQTTnet.Core.Tests => Source/MQTTnet.Tests}/Server/Wildcard_Subscription_Available_Tests.cs (87%) create mode 100644 Source/MQTTnet.Tests/TopicFilterComparer_Tests.cs create mode 100644 Source/MQTTnet.Tests/TopicGenerator.cs rename Source/MQTTnet/Adapter/{MqttPacketInspectorHandler.cs => MqttPacketInspector.cs} (59%) delete mode 100644 Source/MQTTnet/Client/Connecting/IMqttClientConnectedHandler.cs create mode 100644 Source/MQTTnet/Client/Connecting/MqttClientConnectResultFactory.cs delete mode 100644 Source/MQTTnet/Client/Connecting/MqttClientConnectedHandlerDelegate.cs create mode 100644 Source/MQTTnet/Client/Connecting/MqttClientConnectingEventArgs.cs delete mode 100644 Source/MQTTnet/Client/Disconnecting/IMqttClientDisconnectedHandler.cs create mode 100644 Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectOptionsBuilder.cs delete mode 100644 Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectedHandlerDelegate.cs delete mode 100644 Source/MQTTnet/Client/IMqttClient.cs delete mode 100644 Source/MQTTnet/Client/IMqttClientFactory.cs delete mode 100644 Source/MQTTnet/Client/Options/IMqttClientCredentials.cs create mode 100644 Source/MQTTnet/Client/Options/IMqttClientCredentialsProvider.cs delete mode 100644 Source/MQTTnet/Client/Options/IMqttClientOptions.cs delete mode 100644 Source/MQTTnet/Client/Options/MqttClientCertificateValidationCallbackContext.cs create mode 100644 Source/MQTTnet/Client/Options/MqttClientCertificateValidationEventArgs.cs create mode 100644 Source/MQTTnet/Client/Options/MqttClientDefaultCertificateValidationHandler.cs create mode 100644 Source/MQTTnet/Client/Publishing/MqttClientPublishResultFactory.cs delete mode 100644 Source/MQTTnet/Client/Receiving/IMqttApplicationMessageReceivedHandler.cs rename Source/MQTTnet/{ => Client/Receiving}/MqttApplicationMessageReceivedEventArgs.cs (73%) delete mode 100644 Source/MQTTnet/Client/Receiving/MqttApplicationMessageReceivedHandlerDelegate.cs rename Source/MQTTnet/{ => Client/Receiving}/MqttApplicationMessageReceivedReasonCode.cs (63%) create mode 100644 Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResultFactory.cs create mode 100644 Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultFactory.cs delete mode 100644 Source/MQTTnet/Diagnostics/PacketInspection/IMqttPacketInspector.cs create mode 100644 Source/MQTTnet/Diagnostics/PacketInspection/InspectMqttPacketEventArgs.cs delete mode 100644 Source/MQTTnet/Diagnostics/PacketInspection/ProcessMqttPacketContext.cs delete mode 100644 Source/MQTTnet/Extensions/MqttClientOptionsBuilderExtension.cs delete mode 100644 Source/MQTTnet/Extensions/UserPropertyExtension.cs delete mode 100644 Source/MQTTnet/Formatter/IMqttDataConverter.cs delete mode 100644 Source/MQTTnet/Formatter/IMqttPacketBodyReader.cs delete mode 100644 Source/MQTTnet/Formatter/IMqttPacketWriter.cs create mode 100644 Source/MQTTnet/Formatter/MqttApplicationMessageFactory.cs create mode 100644 Source/MQTTnet/Formatter/MqttBufferReader.cs create mode 100644 Source/MQTTnet/Formatter/MqttBufferWriter.cs create mode 100644 Source/MQTTnet/Formatter/MqttConnAckPacketFactory.cs create mode 100644 Source/MQTTnet/Formatter/MqttConnectPacketFactory.cs create mode 100644 Source/MQTTnet/Formatter/MqttConnectReasonCodeConverter.cs create mode 100644 Source/MQTTnet/Formatter/MqttDisconnectPacketFactory.cs delete mode 100644 Source/MQTTnet/Formatter/MqttPacketBodyReader.cs create mode 100644 Source/MQTTnet/Formatter/MqttPacketBuffer.cs create mode 100644 Source/MQTTnet/Formatter/MqttPacketFactories.cs delete mode 100644 Source/MQTTnet/Formatter/MqttPacketWriter.cs create mode 100644 Source/MQTTnet/Formatter/MqttPubAckPacketFactory.cs create mode 100644 Source/MQTTnet/Formatter/MqttPubCompPacketFactory.cs create mode 100644 Source/MQTTnet/Formatter/MqttPubRecPacketFactory.cs create mode 100644 Source/MQTTnet/Formatter/MqttPubRelPacketFactory.cs create mode 100644 Source/MQTTnet/Formatter/MqttPublishPacketFactory.cs create mode 100644 Source/MQTTnet/Formatter/MqttSubAckPacketFactory.cs create mode 100644 Source/MQTTnet/Formatter/MqttSubscribePacketFactory.cs create mode 100644 Source/MQTTnet/Formatter/MqttUnsubAckPacketFactory.cs create mode 100644 Source/MQTTnet/Formatter/MqttUnsubscribePacketFactory.cs delete mode 100644 Source/MQTTnet/Formatter/V3/MqttV310DataConverter.cs delete mode 100644 Source/MQTTnet/Formatter/V3/MqttV310PacketFormatter.cs delete mode 100644 Source/MQTTnet/Formatter/V3/MqttV311PacketFormatter.cs create mode 100644 Source/MQTTnet/Formatter/V3/MqttV3PacketFormatter.cs delete mode 100644 Source/MQTTnet/Formatter/V5/MqttV500DataConverter.cs delete mode 100644 Source/MQTTnet/Formatter/V5/MqttV500PacketDecoder.cs delete mode 100644 Source/MQTTnet/Formatter/V5/MqttV500PacketEncoder.cs delete mode 100644 Source/MQTTnet/Formatter/V5/MqttV500PacketFormatter.cs delete mode 100644 Source/MQTTnet/Formatter/V5/MqttV500PropertiesWriter.cs create mode 100644 Source/MQTTnet/Formatter/V5/MqttV5PacketDecoder.cs create mode 100644 Source/MQTTnet/Formatter/V5/MqttV5PacketEncoder.cs create mode 100644 Source/MQTTnet/Formatter/V5/MqttV5PacketFormatter.cs rename Source/MQTTnet/Formatter/V5/{MqttV500PropertiesReader.cs => MqttV5PropertiesReader.cs} (54%) create mode 100644 Source/MQTTnet/Formatter/V5/MqttV5PropertiesWriter.cs delete mode 100644 Source/MQTTnet/IApplicationMessagePublisher.cs delete mode 100644 Source/MQTTnet/IApplicationMessageReceiver.cs delete mode 100644 Source/MQTTnet/IMqttFactory.cs create mode 100644 Source/MQTTnet/Internal/AsyncEvent.cs create mode 100644 Source/MQTTnet/Internal/AsyncEventInvocator.cs create mode 100644 Source/MQTTnet/Internal/AsyncTaskCompletionSource.cs create mode 100644 Source/MQTTnet/Internal/MqttPacketBus.cs create mode 100644 Source/MQTTnet/Internal/MqttPacketBusItem.cs create mode 100644 Source/MQTTnet/Internal/MqttPacketBusPartition.cs delete mode 100644 Source/MQTTnet/LowLevelClient/ILowLevelMqttClient.cs create mode 100644 Source/MQTTnet/MQTTnet.csproj.DotSettings delete mode 100644 Source/MQTTnet/MqttTopicFilter.cs create mode 100644 Source/MQTTnet/MqttTopicFilterCompareResult.cs create mode 100644 Source/MQTTnet/MqttTopicFilterComparer.cs delete mode 100644 Source/MQTTnet/Packets/IMqttPacketWithIdentifier.cs delete mode 100644 Source/MQTTnet/Packets/MqttAuthPacketProperties.cs delete mode 100644 Source/MQTTnet/Packets/MqttBasePacket.cs delete mode 100644 Source/MQTTnet/Packets/MqttConnAckPacketProperties.cs delete mode 100644 Source/MQTTnet/Packets/MqttConnectPacketProperties.cs delete mode 100644 Source/MQTTnet/Packets/MqttDisconnectPacketProperties.cs create mode 100644 Source/MQTTnet/Packets/MqttPacket.cs create mode 100644 Source/MQTTnet/Packets/MqttPacketWithIdentifier.cs delete mode 100644 Source/MQTTnet/Packets/MqttPubAckPacketProperties.cs delete mode 100644 Source/MQTTnet/Packets/MqttPubCompPacketProperties.cs delete mode 100644 Source/MQTTnet/Packets/MqttPubRecPacketProperties.cs delete mode 100644 Source/MQTTnet/Packets/MqttPubRelPacketProperties.cs delete mode 100644 Source/MQTTnet/Packets/MqttPublishPacketProperties.cs delete mode 100644 Source/MQTTnet/Packets/MqttSubAckPacketProperties.cs delete mode 100644 Source/MQTTnet/Packets/MqttSubscribePacketProperties.cs create mode 100644 Source/MQTTnet/Packets/MqttTopicFilter.cs delete mode 100644 Source/MQTTnet/Packets/MqttUnsubAckPacketProperties.cs delete mode 100644 Source/MQTTnet/Packets/MqttUnsubscribePacketProperties.cs delete mode 100644 Source/MQTTnet/Protocol/MqttConnectReasonCodeConverter.cs rename Source/MQTTnet/Protocol/{MqttPropertyID.cs => MqttPropertyId.cs} (78%) create mode 100644 Source/MQTTnet/Server/Events/ApplicationMessageNotConsumedEventArgs.cs rename Source/MQTTnet/Server/{MqttServerClientConnectedEventArgs.cs => Events/ClientConnectedEventArgs.cs} (75%) rename Source/MQTTnet/Server/{MqttServerClientDisconnectedEventArgs.cs => Events/ClientDisconnectedEventArgs.cs} (62%) rename Source/MQTTnet/Server/{MqttServerClientSubscribedTopicEventArgs.cs => Events/ClientSubscribedTopicEventArgs.cs} (62%) rename Source/MQTTnet/Server/{MqttServerClientUnsubscribedTopicEventArgs.cs => Events/ClientUnsubscribedTopicEventArgs.cs} (64%) create mode 100644 Source/MQTTnet/Server/Events/InterceptingPacketEventArgs.cs create mode 100644 Source/MQTTnet/Server/Events/InterceptingPublishEventArgs.cs create mode 100644 Source/MQTTnet/Server/Events/InterceptingSubscriptionEventArgs.cs create mode 100644 Source/MQTTnet/Server/Events/InterceptingUnsubscriptionEventArgs.cs create mode 100644 Source/MQTTnet/Server/Events/LoadingRetainedMessagesEventArgs.cs create mode 100644 Source/MQTTnet/Server/Events/PreparingSessionEventArgs.cs create mode 100644 Source/MQTTnet/Server/Events/RetainedMessageChangedEventArgs.cs create mode 100644 Source/MQTTnet/Server/Events/SessionDeletedEventArgs.cs create mode 100644 Source/MQTTnet/Server/Events/ValidatingConnectionEventArgs.cs delete mode 100644 Source/MQTTnet/Server/GetSubscribedMessagesFilter.cs delete mode 100644 Source/MQTTnet/Server/IMqttClientSession.cs delete mode 100644 Source/MQTTnet/Server/IMqttRetainedMessagesManager.cs delete mode 100644 Source/MQTTnet/Server/IMqttServer.cs delete mode 100644 Source/MQTTnet/Server/IMqttServerApplicationMessageInterceptor.cs delete mode 100644 Source/MQTTnet/Server/IMqttServerCertificateCredentials.cs delete mode 100644 Source/MQTTnet/Server/IMqttServerClientConnectedHandler.cs delete mode 100644 Source/MQTTnet/Server/IMqttServerClientDisconnectedHandler.cs delete mode 100644 Source/MQTTnet/Server/IMqttServerClientMessageQueueInterceptor.cs delete mode 100644 Source/MQTTnet/Server/IMqttServerClientSubscribedTopicHandler.cs delete mode 100644 Source/MQTTnet/Server/IMqttServerClientUnsubscribedTopicHandler.cs delete mode 100644 Source/MQTTnet/Server/IMqttServerConnectionValidator.cs delete mode 100644 Source/MQTTnet/Server/IMqttServerFactory.cs delete mode 100644 Source/MQTTnet/Server/IMqttServerOptions.cs delete mode 100644 Source/MQTTnet/Server/IMqttServerPersistedSession.cs delete mode 100644 Source/MQTTnet/Server/IMqttServerPersistedSessionsStorage.cs delete mode 100644 Source/MQTTnet/Server/IMqttServerStartedHandler.cs delete mode 100644 Source/MQTTnet/Server/IMqttServerStoppedHandler.cs delete mode 100644 Source/MQTTnet/Server/IMqttServerStorage.cs delete mode 100644 Source/MQTTnet/Server/IMqttServerSubscriptionInterceptor.cs delete mode 100644 Source/MQTTnet/Server/IMqttServerUnsubscriptionInterceptor.cs create mode 100644 Source/MQTTnet/Server/InjectedMqttApplicationMessage.cs create mode 100644 Source/MQTTnet/Server/Internal/ISubscriptionChangedNotification.cs create mode 100644 Source/MQTTnet/Server/Internal/MqttClient.cs delete mode 100644 Source/MQTTnet/Server/Internal/MqttClientConnection.cs delete mode 100644 Source/MQTTnet/Server/Internal/MqttClientConnectionStatistics.cs delete mode 100644 Source/MQTTnet/Server/Internal/MqttClientSession.cs delete mode 100644 Source/MQTTnet/Server/Internal/MqttClientSessionApplicationMessagesQueue.cs create mode 100644 Source/MQTTnet/Server/Internal/MqttClientStatistics.cs create mode 100644 Source/MQTTnet/Server/Internal/MqttServerEventContainer.cs delete mode 100644 Source/MQTTnet/Server/Internal/MqttServerEventDispatcher.cs create mode 100644 Source/MQTTnet/Server/Internal/MqttSession.cs create mode 100644 Source/MQTTnet/Server/Internal/MqttSubscription.cs delete mode 100644 Source/MQTTnet/Server/Internal/MqttTopicFilterComparer.cs create mode 100644 Source/MQTTnet/Server/Internal/MqttUnsubscribeResult.cs delete mode 100644 Source/MQTTnet/Server/Internal/Subscription.cs create mode 100644 Source/MQTTnet/Server/Internal/TopicHashMaskSubscriptions.cs delete mode 100644 Source/MQTTnet/Server/MqttApplicationMessageInterceptorContext.cs delete mode 100644 Source/MQTTnet/Server/MqttClientMessageQueueInterceptorContext.cs delete mode 100644 Source/MQTTnet/Server/MqttClientMessageQueueInterceptorDelegate.cs delete mode 100644 Source/MQTTnet/Server/MqttConnectionValidatorContext.cs delete mode 100644 Source/MQTTnet/Server/MqttPendingApplicationMessage.cs delete mode 100644 Source/MQTTnet/Server/MqttPendingMessagesOverflowStrategy.cs delete mode 100644 Source/MQTTnet/Server/MqttQueuedApplicationMessage.cs create mode 100644 Source/MQTTnet/Server/MqttRetainedMessageMatch.cs delete mode 100644 Source/MQTTnet/Server/MqttServerApplicationMessageInterceptorDelegate.cs delete mode 100644 Source/MQTTnet/Server/MqttServerCertificateCredentials.cs delete mode 100644 Source/MQTTnet/Server/MqttServerClientConnectedHandlerDelegate.cs delete mode 100644 Source/MQTTnet/Server/MqttServerClientDisconnectedHandlerDelegate.cs delete mode 100644 Source/MQTTnet/Server/MqttServerClientSubscribedTopicHandlerDelegate.cs delete mode 100644 Source/MQTTnet/Server/MqttServerClientUnsubscribedTopicHandlerDelegate.cs delete mode 100644 Source/MQTTnet/Server/MqttServerConnectionValidatorDelegate.cs delete mode 100644 Source/MQTTnet/Server/MqttServerMultiThreadedApplicationMessageInterceptorDelegate.cs delete mode 100644 Source/MQTTnet/Server/MqttServerOptions.cs delete mode 100644 Source/MQTTnet/Server/MqttServerStartedHandlerDelegate.cs delete mode 100644 Source/MQTTnet/Server/MqttServerStoppedHandlerDelegate.cs delete mode 100644 Source/MQTTnet/Server/MqttServerSubscriptionInterceptorDelegate.cs delete mode 100644 Source/MQTTnet/Server/MqttServerTcpEndpointOptions.cs delete mode 100644 Source/MQTTnet/Server/MqttServerTlsTcpEndpointOptions.cs delete mode 100644 Source/MQTTnet/Server/MqttSubscriptionInterceptorContext.cs delete mode 100644 Source/MQTTnet/Server/MqttUnsubscriptionInterceptorContext.cs create mode 100644 Source/MQTTnet/Server/Options/IMqttServerCertificateCredentials.cs create mode 100644 Source/MQTTnet/Server/Options/MqttPendingMessagesOverflowStrategy.cs create mode 100644 Source/MQTTnet/Server/Options/MqttServerCertificateCredentials.cs create mode 100644 Source/MQTTnet/Server/Options/MqttServerOptions.cs rename Source/MQTTnet/Server/{ => Options}/MqttServerOptionsBuilder.cs (54%) rename Source/MQTTnet/Server/{ => Options}/MqttServerTcpEndpointBaseOptions.cs (56%) create mode 100644 Source/MQTTnet/Server/Options/MqttServerTcpEndpointOptions.cs create mode 100644 Source/MQTTnet/Server/Options/MqttServerTlsTcpEndpointOptions.cs create mode 100644 Source/MQTTnet/Server/PublishResponse.cs delete mode 100644 Source/MQTTnet/Server/Status/IMqttClientStatus.cs delete mode 100644 Source/MQTTnet/Server/Status/IMqttSessionStatus.cs create mode 100644 Source/MQTTnet/Server/SubscribeResponse.cs create mode 100644 Source/MQTTnet/Server/UnsubscribeResponse.cs delete mode 100644 Source/MQTTnet/codeSigningKey.pfx delete mode 100644 Tests/MQTTnet.AspNetCore.Tests/MQTTnet.AspNetCore.Tests.csproj delete mode 100644 Tests/MQTTnet.AspNetCore.Tests/MqttPacketSerializerTests.cs delete mode 100644 Tests/MQTTnet.AspNetCore.Tests/SpanBasedMqttPacketWriterTests.cs delete mode 100644 Tests/MQTTnet.Benchmarks/App.config delete mode 100644 Tests/MQTTnet.Benchmarks/Configurations/RuntimeCompareConfig.cs delete mode 100644 Tests/MQTTnet.Benchmarks/MQTTnet.Benchmarks.csproj delete mode 100644 Tests/MQTTnet.Benchmarks/TopicFilterComparerBenchmark.cs delete mode 100644 Tests/MQTTnet.Core.Tests/Client/LowLevelMqttClient_Tests.cs delete mode 100644 Tests/MQTTnet.Core.Tests/Extensions/MqttPacketWriterExtensions.cs delete mode 100644 Tests/MQTTnet.Core.Tests/MQTTnet.Tests.csproj delete mode 100644 Tests/MQTTnet.Core.Tests/Mockups/MqttPacketAsserts.cs delete mode 100644 Tests/MQTTnet.Core.Tests/Mockups/TestApplicationMessageReceivedHandler.cs delete mode 100644 Tests/MQTTnet.Core.Tests/Mockups/TestClientWrapper.cs delete mode 100644 Tests/MQTTnet.Core.Tests/Mockups/TestEnvironment.cs delete mode 100644 Tests/MQTTnet.Core.Tests/Mockups/TestMqttCommunicationAdapter.cs delete mode 100644 Tests/MQTTnet.Core.Tests/Mockups/TestMqttCommunicationAdapterFactory.cs delete mode 100644 Tests/MQTTnet.Core.Tests/Mockups/TestServerStorage.cs delete mode 100644 Tests/MQTTnet.Core.Tests/Mockups/TestServerWrapper.cs delete mode 100644 Tests/MQTTnet.Core.Tests/MqttApplicationMessage_Tests.cs delete mode 100644 Tests/MQTTnet.Core.Tests/MqttPacketReader_Tests.cs delete mode 100644 Tests/MQTTnet.Core.Tests/MqttPacketWriter_Tests.cs delete mode 100644 Tests/MQTTnet.Core.Tests/Protocol_Tests.cs delete mode 100644 Tests/MQTTnet.Core.Tests/Server/MqttSubscriptionsManager_Tests.cs delete mode 100644 Tests/MQTTnet.Core.Tests/Server/Session_Tests.cs delete mode 100644 Tests/MQTTnet.Core.Tests/Server/Shared_Subscriptions_Tests.cs delete mode 100644 Tests/MQTTnet.Core.Tests/Server/TopicFilterComparer_Tests.cs delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Client/App.razor delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Client/MQTTnet.Test.BlazorApp.Client.csproj delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Client/Pages/Counter.razor delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Client/Pages/FetchData.razor delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Client/Pages/Index.razor delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Client/Program.cs delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Client/Shared/MainLayout.razor delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Client/Shared/NavMenu.razor delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Client/Shared/SurveyPrompt.razor delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Client/_Imports.razor delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/app.css delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/bootstrap/bootstrap.min.css delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/FONT-LICENSE delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/ICON-LICENSE delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/README.md delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.eot delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.otf delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.svg delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.woff delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/favicon.ico delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/index.html delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Server/Controllers/WeatherForecastController.cs delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Server/MQTTnet.Test.BlazorApp.Server.csproj delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Server/Pages/Error.cshtml delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Server/Pages/Error.cshtml.cs delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Server/Pages/Shared/_Layout.cshtml delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Server/Program.cs delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Server/Startup.cs delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Server/appsettings.Development.json delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Server/appsettings.json delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Shared/MQTTnet.Test.BlazorApp.Shared.csproj delete mode 100644 Tests/MQTTnet.Test.BlazorApp/Shared/WeatherForecast.cs delete mode 100644 Tests/MQTTnet.TestApp.AspNetCore2/MQTTnet.TestApp.AspNetCore2.csproj delete mode 100644 Tests/MQTTnet.TestApp.AspNetCore2/Program.cs delete mode 100644 Tests/MQTTnet.TestApp.AspNetCore2/Startup.cs delete mode 100644 Tests/MQTTnet.TestApp.AspNetCore2/package.json delete mode 100644 Tests/MQTTnet.TestApp.AspNetCore2/tsconfig.json delete mode 100644 Tests/MQTTnet.TestApp.AspNetCore2/wwwroot/Index.html delete mode 100644 Tests/MQTTnet.TestApp.AspNetCore2/wwwroot/app/app.ts delete mode 100644 Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj delete mode 100644 Tests/MQTTnet.TestApp.NetCore/Program.cs delete mode 100644 Tests/MQTTnet.TestApp.NetCore/ServerTest.cs diff --git a/.bettercodehub.yml b/.bettercodehub.yml deleted file mode 100644 index 496d2d5..0000000 --- a/.bettercodehub.yml +++ /dev/null @@ -1,3 +0,0 @@ -component_depth: 2 -languages: -- csharp diff --git a/.gitattributes b/.gitattributes index 1ff0c42..2125666 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,63 +1 @@ -############################################################################### -# Set default behavior to automatically normalize line endings. -############################################################################### -* text=auto - -############################################################################### -# Set default behavior for command prompt diff. -# -# This is need for earlier builds of msysgit that does not have it on by -# default for csharp files. -# Note: This is only used by command line -############################################################################### -#*.cs diff=csharp - -############################################################################### -# Set the merge driver for project and solution files -# -# Merging from the command prompt will add diff markers to the files if there -# are conflicts (Merging from VS is not affected by the settings below, in VS -# the diff markers are never inserted). Diff markers may cause the following -# file extensions to fail to load in VS. An alternative would be to treat -# these files as binary and thus will always conflict and require user -# intervention with every merge. To do so, just uncomment the entries below -############################################################################### -#*.sln merge=binary -#*.csproj merge=binary -#*.vbproj merge=binary -#*.vcxproj merge=binary -#*.vcproj merge=binary -#*.dbproj merge=binary -#*.fsproj merge=binary -#*.lsproj merge=binary -#*.wixproj merge=binary -#*.modelproj merge=binary -#*.sqlproj merge=binary -#*.wwaproj merge=binary - -############################################################################### -# behavior for image files -# -# image files are treated as binary by default. -############################################################################### -#*.jpg binary -#*.png binary -#*.gif binary - -############################################################################### -# diff behavior for common document formats -# -# Convert binary document formats to text before diffing them. This feature -# is only available from the command line. Turn it on by uncommenting the -# entries below. -############################################################################### -#*.doc diff=astextplain -#*.DOC diff=astextplain -#*.docx diff=astextplain -#*.DOCX diff=astextplain -#*.dot diff=astextplain -#*.DOT diff=astextplain -#*.pdf diff=astextplain -#*.PDF diff=astextplain -#*.rtf diff=astextplain -#*.RTF diff=astextplain +* text=auto \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 24dc74f..6ed785a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,16 +7,25 @@ assignees: '' --- +### Verification + +Before opening a bug make sure that the following conditions are met. + + +1. Performance issues are also appearing in RELEASE mode of your application. +2. An increased memory consumption or high CPU load also happens when your application runs in RELEASE mode AND logging is DISABLED. +3. Client issues with not received messages, connection drops etc. can be reproduced via another client application like "MQTTnetApp" (https://github.com/chkr1011/MQTTnetApp) or similar. +4. The bug also appears in the VERY LATEST version of this library. There is no support for older version (due to limited resources) but pull requests for older versions are welcome. + ### Describe the bug A clear and concise description of what the bug is. -### Which project is your bug related to? - +### Which component is your bug related to? + - Client - ManagedClient -- MQTTnet.Server standalone +- RcpClient - Server -- Generic ### To Reproduce Steps to reproduce the behavior: @@ -39,7 +48,9 @@ Include debugging or logging information here: \\ Put your logging output here. ``` ### Code example - Please provide full code examples below where possible to make it easier for the developers to check your issues. +Please provide full code examples below where possible to make it easier for the developers to check your issues. + +**Ideally a Unit Test (which shows the error) is provided so that the behavior can be reproduced easily.** ```csharp \\ Put your code here. diff --git a/.github/workflows/ReleaseNotes.md b/.github/workflows/ReleaseNotes.md new file mode 100644 index 0000000..f6ddfad --- /dev/null +++ b/.github/workflows/ReleaseNotes.md @@ -0,0 +1,27 @@ +We have joined the .NET Foundation! + +Version 4 comes with a new API so a lot of breaking changes should be expected. +Checkout the upgrade guide (https://github.com/dotnet/MQTTnet/wiki/Upgrading-guide) for an overview of the changes. +Checkout the new samples (https://github.com/dotnet/MQTTnet/tree/feature/master/Samples) how to use the new API. The wiki only remains for version 3 of this library. + +* [Core] Improved memory management when working with large payloads. +* [Core] Added support for .NET 6.0. +* [Core] nuget packages are now created by MSBuild including more information (i.e. commit hash). +* [Client] Exposed socket linger state in options. +* [Client] The OS will now choose the best TLS version to use. It is no longer fixed to 1.3 etc. (thanks to @patagonaa, #1271). +* [Client] Added support for _ServerKeepAlive_ (MQTTv5). +* [Client] Exposed user properties and reason string in subscribe result. +* [Client] Exposed user properties and reason string in unsubscribe result. +* [Client] Migrated application message handler to a regular .NET event (BREAKING CHANGE!). +* [Client] The will message is longer a regular application message due to not supported properties by the will message (BREAKING CHANGE!). +* [Client] Timeouts are no longer handled inside the library. Each method (Connect, Publish etc.) supports a cancellation token so that custom timeouts can and must be used (BREAKING CHANGE!). +* [Server] Exposed socket linger state in options. +* [Server] Added support for returning individual subscription errors (#80 thanks to @jimch) +* [Server] Improved topic filter comparisons (support for $). +* [Server] Added more MQTTv5 response information to all interceptors (BREAKING CHANGE!). +* [Server] Improved session management for MQTT v5 (#1294, thanks to @logicaloud). +* [Server] All interceptors and events are migrated from interfaces to simple events. All existing APIs are availble but must be migrated to corresponding events (BREAKING CHANGE!). +* [Server] Removed all interceptor and event interfaces including the delegate implementations etc. (BREAKING CHANGE!). +* [Server] Renamed a lot of classes and adjusted namespaces (BREAKING CHANGE!). +* [Server] Introduced a new queueing approach for internal message process (packet bus). +* [Server] For security reasons the default endpoint (1883) is no longer enabled by default (BREAKING CHANGE!). \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36d7ecd..f8767c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,34 +1,68 @@ name: CI -on: [push] +on: [push, pull_request] + +env: + ASSEMBLY_VERSION: "4.0.0.${{github.run_number}}" + NUGET_VERSION: "4.0.0.${{github.run_number}}" jobs: build: - runs-on: windows-latest - strategy: - matrix: - dotnet-version: ['6.0.x'] + runs-on: windows-2022 steps: - - uses: actions/checkout@v2 - - name: Setup .NET Core SDK ${{ matrix.dotnet-version }} - uses: actions/setup-dotnet@v1.7.2 + - name: Setup Windows SDK + uses: GuillaumeFalourd/setup-windows10-sdk-action@v1 + with: + sdk-version: 18362 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v1.9.0 with: - dotnet-version: ${{ matrix.dotnet-version }} + dotnet-version: | + 3.1.x + 6.0.x + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v1.1 - - name: Build - shell: pwsh + - name: Checkout Code + uses: actions/checkout@v2 + + - name: Setup Signing Certificate run: | - Set-Location -Path "Build" - .\build.ps1 1.2.3 4.5.6-rc1 - - -# - name: Install dependencies -# run: dotnet restore MQTTnet.sln -# - name: Build -# run: dotnet build --configuration Release --no-restore MQTTnet.sln -# - name: Test MQTTnet -# run: dotnet test --no-restore --verbosity normal Tests\MQTTnet.Core.Tests\MQTTnet.Tests.csproj -# - name: Test AspNetCore -# run: dotnet test --no-restore --verbosity normal Tests\MQTTnet.AspNetCore.Tests\MQTTnet.AspNetCore.Tests.csproj \ No newline at end of file + $secret = '${{ secrets.SNC_BASE64 }}' + $decoded = [System.Convert]::FromBase64CharArray($secret, 0, $secret.Length) + Set-Content -Path ${{ github.workspace }}\certificate.snk -Value $decoded -AsByteStream + + - name: Restore nuget packages + run: msbuild MQTTnet.sln /t:Restore /p:Configuration="Release" /verbosity:m + + - name: Build solution + run: msbuild MQTTnet.sln /t:Build /p:Configuration="Release" /verbosity:m /p:FileVersion=${{ env.ASSEMBLY_VERSION }} /p:AssemblyVersion=${{ env.ASSEMBLY_VERSION }} /p:PackageVersion=${{ env.NUGET_VERSION }} /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=${{ github.workspace }}\certificate.snk + + - name: Collect nuget Packages + uses: actions/upload-artifact@v2 + with: + name: nuget Packages + path: | + **\*.nupkg + **\*.snupkg + + - name: Setup VSTest + uses: darenm/Setup-VSTest@v1 + + - name: Core Tests + run: vstest.console.exe Source\MQTTnet.Tests\bin\Release\net6.0\MQTTnet.Tests.dll + + - name: ASP.NET Tests + run: vstest.console.exe Source\MQTTnet.AspNetCore.Tests\bin\Release\netcoreapp3.1\MQTTnet.AspNetCore.Tests.dll + + - name: Publish MyGet nugets + if: ${{ github.event_name == 'push' }} + run: dotnet nuget push **/*.nupkg -k ${{ secrets.MYGET_API_KEY }} -s https://www.myget.org/F/mqttnet/api/v3/index.json --skip-duplicate + +# - name: Publish nugets +# if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} +# run: dotnet nuget push **/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json --skip-duplicate \ No newline at end of file diff --git a/Build/MQTTnet.AspNetCore.nuspec b/Build/MQTTnet.AspNetCore.nuspec deleted file mode 100644 index 68fa9db..0000000 --- a/Build/MQTTnet.AspNetCore.nuspec +++ /dev/null @@ -1,49 +0,0 @@ - - - - MQTTnet.AspNetCore - 0.0.0 - The contributors of MQTTnet - Christian Kratky - LICENSE - https://github.com/chkr1011/MQTTnet - https://raw.githubusercontent.com/chkr1011/MQTTnet/master/Images/Logo_128x128.png - images\Logo_128x128.png - true - This is a support library to integrate MQTTnet into AspNetCore. - For release notes please go to MQTTnet release notes (https://www.nuget.org/packages/MQTTnet/). - Copyright Christian Kratky 2016-2021 - 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Build/MQTTnet.Extensions.ManagedClient.nuspec b/Build/MQTTnet.Extensions.ManagedClient.nuspec deleted file mode 100644 index 8c43ed6..0000000 --- a/Build/MQTTnet.Extensions.ManagedClient.nuspec +++ /dev/null @@ -1,52 +0,0 @@ - - - - MQTTnet.Extensions.ManagedClient - 0.0.0 - The contributors of MQTTnet - Christian Kratky - LICENSE - https://github.com/chkr1011/MQTTnet - https://raw.githubusercontent.com/chkr1011/MQTTnet/master/Images/Logo_128x128.png - images\Logo_128x128.png - true - This is an extension library which provides a managed MQTT client with additional features using MQTTnet. - For release notes please go to MQTTnet release notes (https://www.nuget.org/packages/MQTTnet/). - Copyright Christian Kratky 2016-2021 - 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Build/MQTTnet.Extensions.Rpc.nuspec b/Build/MQTTnet.Extensions.Rpc.nuspec deleted file mode 100644 index d038413..0000000 --- a/Build/MQTTnet.Extensions.Rpc.nuspec +++ /dev/null @@ -1,52 +0,0 @@ - - - - MQTTnet.Extensions.Rpc - 0.0.0 - The contributors of MQTTnet - Christian Kratky - LICENSE - https://github.com/chkr1011/MQTTnet - https://raw.githubusercontent.com/chkr1011/MQTTnet/master/Images/Logo_128x128.png - images\Logo_128x128.png - true - This is an extension library which allows executing synchronous device calls including a response using MQTTnet. - For release notes please go to MQTTnet release notes (https://www.nuget.org/packages/MQTTnet/). - Copyright Christian Kratky 2016-2021 - 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Build/MQTTnet.Extensions.WebSocket4Net.nuspec b/Build/MQTTnet.Extensions.WebSocket4Net.nuspec deleted file mode 100644 index 21baa84..0000000 --- a/Build/MQTTnet.Extensions.WebSocket4Net.nuspec +++ /dev/null @@ -1,53 +0,0 @@ - - - - MQTTnet.Extensions.WebSocket4Net - 0.0.0 - The contributors of MQTTnet - Christian Kratky - LICENSE - https://github.com/chkr1011/MQTTnet - https://raw.githubusercontent.com/chkr1011/MQTTnet/master/Images/Logo_128x128.png - images\Logo_128x128.png - true - This is an extension library which allows using _WebSocket4Net_ as transport for MQTTnet clients. - For release notes please go to MQTTnet release notes (https://www.nuget.org/packages/MQTTnet/). - Copyright Christian Kratky 2016-2021 - 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Build/MQTTnet.nuspec b/Build/MQTTnet.nuspec deleted file mode 100644 index a5242c4..0000000 --- a/Build/MQTTnet.nuspec +++ /dev/null @@ -1,78 +0,0 @@ - - - - MQTTnet - 0.0.0 - The contributors of MQTTnet - Christian Kratky - LICENSE - https://github.com/chkr1011/MQTTnet - https://raw.githubusercontent.com/chkr1011/MQTTnet/master/Images/Logo_128x128.png - images\Logo_128x128.png - true - MQTTnet is a high performance .NET library for MQTT based communication. It provides a MQTT client and a MQTT server (broker) and supports v3.1.0, v3.1.1 and v5.0.0 of the MQTT protocol. - -* [Client] Increased delay for keep alive checks do decrease CPU load. -* [Core] Decreased object allocations (#1324, thanks to @gfoidl). -* [Core] Decreased object allocations when logging is not active (thanks to @gfoidl, @Tymoniden). -* [Client] Fixed issue in _MqttApplicationMessageBuilder.WithPayload_ (#1322, thanks to @gfoidl). -* [Client] Adjusted default SslProtocol values to Tls12 and Tls13 (#1347). -* [Extensions.WebSocket4Net] Adjusted default SslProtocol values to Tls12 and Tls13 (#1347). - -Git commit: $gitCommit - - Copyright Christian Kratky 2016-2021 - 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Build/build.ps1 b/Build/build.ps1 deleted file mode 100644 index 7af37d8..0000000 --- a/Build/build.ps1 +++ /dev/null @@ -1,99 +0,0 @@ -param([string]$assemblyVersion, [string]$nugetVersion) - -if ([string]::IsNullOrEmpty($assemblyVersion)) {$assemblyVersion = "0.0.1"} -if ([string]::IsNullOrEmpty($nugetVersion)) {$nugetVersion = "0.0.1"} - -$vswhere = ${Env:\ProgramFiles(x86)} + '\Microsoft Visual Studio\Installer\vswhere' -$msbuild = &$vswhere -products * -requires Microsoft.Component.MSBuild -latest -find MSBuild\**\Bin\MSBuild.exe -$vstest = &$vswhere -products * -latest -find **\TestWindow\**\vstest.console.exe - -Write-Host -Write-Host "Assembly version = $assemblyVersion" -Write-Host "Nuget version = $nugetVersion" -Write-Host "MSBuild path = $msbuild" -Write-Host - -Invoke-WebRequest -Uri "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" -OutFile "nuget.exe" - -.\nuget.exe restore ..\MQTTnet.sln - -# Build and execute tests -&$msbuild ..\Tests\MQTTnet.Core.Tests\MQTTnet.Tests.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netcoreapp3.1" /verbosity:m -&$msbuild ..\Tests\MQTTnet.AspNetCore.Tests\MQTTnet.AspNetCore.Tests.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netcoreapp3.1" /verbosity:m - -&$vstest ..\Tests\MQTTnet.Core.Tests\bin\Release\netcoreapp3.1\MQTTnet.Tests.dll -&$vstest ..\Tests\MQTTnet.AspNetCore.Tests\bin\Release\netcoreapp3.1\MQTTnet.AspNetCore.Tests.dll - -# Build the core library -&$msbuild ..\Source\MQTTnet\MQTTnet.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net452" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet\MQTTnet.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net461" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet\MQTTnet.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard1.3" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet\MQTTnet.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard2.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet\MQTTnet.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard2.1" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet\MQTTnet.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netcoreapp3.1" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet\MQTTnet.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="uap10.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet\MQTTnet.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net5.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" - -# Build the ASP.NET Core 2.0 extension -&$msbuild ..\Source\MQTTnet.AspNetCore\MQTTnet.AspNetCore.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard2.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet.AspNetCore\MQTTnet.AspNetCore.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netcoreapp3.1" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet.AspNetCore\MQTTnet.AspNetCore.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net5.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" - -# Build the RPC extension -&$msbuild ..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net452" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net461" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard1.3" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard2.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard2.1" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="uap10.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet.Extensions.Rpc\MQTTnet.Extensions.Rpc.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net5.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" - -# Build the Managed Client extension -&$msbuild ..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net452" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net461" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard1.3" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard2.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard2.1" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="uap10.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet.Extensions.ManagedClient\MQTTnet.Extensions.ManagedClient.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net5.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" - -# Build the WebSocket4Net extension -&$msbuild ..\Source\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net452" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net461" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard1.3" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard2.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="netstandard2.1" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="uap10.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" -&$msbuild ..\Source\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj /t:Build /p:Configuration="Release" /p:TargetFramework="net5.0" /p:FileVersion=$assemblyVersion /p:AssemblyVersion=$assemblyVersion /verbosity:m /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=".\..\..\Build\codeSigningKey.pfx" - -# Create NuGet packages. - -Remove-Item .\NuGet -Force -Recurse -ErrorAction SilentlyContinue - -$gitCommit = git log -1 --format=%h - -Copy-Item MQTTnet.nuspec -Destination MQTTnet.nuspec.old -Force -(Get-Content MQTTnet.nuspec) -replace '\$gitCommit', $gitCommit | Set-Content MQTTnet.nuspec -Copy-Item MQTTnet.AspNetCore.nuspec -Destination MQTTnet.AspNetCore.nuspec.old -Force -(Get-Content MQTTnet.AspNetCore.nuspec) -replace '\$nugetVersion', $nugetVersion | Set-Content MQTTnet.AspNetCore.nuspec -Copy-Item MQTTnet.Extensions.Rpc.nuspec -Destination MQTTnet.Extensions.Rpc.nuspec.old -Force -(Get-Content MQTTnet.Extensions.Rpc.nuspec) -replace '\$nugetVersion', $nugetVersion | Set-Content MQTTnet.Extensions.Rpc.nuspec -Copy-Item MQTTnet.Extensions.ManagedClient.nuspec -Destination MQTTnet.Extensions.ManagedClient.nuspec.old -Force -(Get-Content MQTTnet.Extensions.ManagedClient.nuspec) -replace '\$nugetVersion', $nugetVersion | Set-Content MQTTnet.Extensions.ManagedClient.nuspec -Copy-Item MQTTnet.Extensions.WebSocket4Net.nuspec -Destination MQTTnet.Extensions.WebSocket4Net.nuspec.old -Force -(Get-Content MQTTnet.Extensions.WebSocket4Net.nuspec) -replace '\$nugetVersion', $nugetVersion | Set-Content MQTTnet.Extensions.WebSocket4Net.nuspec - -New-Item -ItemType Directory -Force -Path .\NuGet -.\nuget.exe pack MQTTnet.nuspec -Verbosity detailed -Symbols -SymbolPackageFormat snupkg -OutputDir "NuGet" -Version $nugetVersion -.\nuget.exe pack MQTTnet.AspNetCore.nuspec -Verbosity detailed -Symbols -SymbolPackageFormat snupkg -OutputDir "NuGet" -Version $nugetVersion -.\nuget.exe pack MQTTnet.Extensions.Rpc.nuspec -Verbosity detailed -Symbols -SymbolPackageFormat snupkg -OutputDir "NuGet" -Version $nugetVersion -.\nuget.exe pack MQTTnet.Extensions.ManagedClient.nuspec -Verbosity detailed -Symbols -SymbolPackageFormat snupkg -OutputDir "NuGet" -Version $nugetVersion -.\nuget.exe pack MQTTnet.Extensions.WebSocket4Net.nuspec -Verbosity detailed -Symbols -SymbolPackageFormat snupkg -OutputDir "NuGet" -Version $nugetVersion - -Move-Item MQTTnet.nuspec.old -Destination MQTTnet.nuspec -Force -Move-Item MQTTnet.AspNetCore.nuspec.old -Destination MQTTnet.AspNetCore.nuspec -Force -Move-Item MQTTnet.Extensions.Rpc.nuspec.old -Destination MQTTnet.Extensions.Rpc.nuspec -Force -Move-Item MQTTnet.Extensions.ManagedClient.nuspec.old -Destination MQTTnet.Extensions.ManagedClient.nuspec -Force -Move-Item MQTTnet.Extensions.WebSocket4Net.nuspec.old -Destination MQTTnet.Extensions.WebSocket4Net.nuspec -Force - -Remove-Item "nuget.exe" -Force -Recurse -ErrorAction SilentlyContinue \ No newline at end of file diff --git a/Build/codeSigningKey.pfx b/Build/codeSigningKey.pfx deleted file mode 100644 index 9374a5dbc11acc2a3a2c507ad3281f755fcbbf45..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1764 zcmZXTcU0477sr1Y{u02E3Rsy6RF6!P5EC4TViG6<5|(8)6bdht4Pn**2~%VhDG)+r zRiF|;Rs(232}_WnBFNAn7@#7v0YUKX&z|>fPfyQ1_qpHuxzByh^T)kd7AS(l5Lgz- z1*NnSO%pfw!BDUw7MO-)fhiD9z_Jic|0p5Kkt{?hgl|D!0LuKYYp*m6Uc`boVOj7x ztTHJ5e+)goABkoX7*ju@%aL$6Y6!`KXGW>@w~Y-`14Vm$%t|J4kDqTZ6P+L4WA}$u z7G|cVzWO=rPM5wfIP-qtyt0lB$q9RZfl;2jH`j5_?H+F9>;~s$TY3gKglNc)7o8NO ze7LbE7vwGTl9VM=d7UKxRL#pd*=>j^2Jg!@OuS4?H+PszXH25Hvpu&B?YF0YC?2ia z+c&>@k2C6vei%8?-AgF9bj+B4I@jH@=B!s{pvnx1wN+0Re(2V7_ge?B>q zdHFIZ!gba3v|lxO;k{Ktous{`Tpu|>`6?-G4*zzsWxE>Vm%$<#P%2#|JnccIM@Gqd zu%^q)oXAITT6lZCy11?0+W2A<2)kju-CK_iCw7+hx$(4#6%wtm4UD_1$L&-yeOC1D zmu3aaXob)(G#136n~%L@2{jtY#{_RJg60lcTq+bM*J^Wp1$|~~>_y@66^1u&x1eXS zRL|PzXw*-N3Vxa7wlGgKJJiPOnwGfS+>U|9#YTA=tch(b{C>^w`GnH-F`6bV>NAP6 z(cu++Wu*^^YW(6UY~b)Kfu>97t4@NW-IbZWvAa&ROV=VtB^)iEv1e!4sc#)eUr1Nv z5v%-6b!#dA#_&$CS7%yR^_&pYgk7;{oG&UcmcgTazem_<<^YeCsRNmfSkStrT?e zS||EX*T5ariA5od^Pu_9SyT4-4idp#zIeEDdB8tyZZL-GpsZyp{I?z5?=WEz_0kH0xwYk_S$89R1TJ>r>J#A#5eptXD0belP2bQGR8iAa&O zHyoOzGlQiC{?4rp>Up{Euzr-RTR=2uUg|4vUU{gha4rG~C3YjItMBMNlp`7{q0 zWZ5(p98}3`bh?R*UvIrI7WnzB8Ykd<@d5MU)`l~za(|88==QO@{Kfm78_oTOp>}l2 zYgtdWF#dj^N|);q&cXzZ{ktJGdv$DwlyQ=;!>1b8+Pv0%5*l!MiRnRK6;yIwM5w=E z!aGh-ZaCyO2JWn-^J7$tVwc2uk2>|M6PFrc_-77iW?)aEi&C&)Nho#UYYz4-_6QaQ z?$O?lfGZ;9Ruv;2rrE~ZwaktU*j$VFvWM4^kCgM2S4msW`lZOXqIU#UV`58>!D@UO G;6DJc77}Ox diff --git a/Build/upload.ps1 b/Build/upload.ps1 deleted file mode 100644 index 794fe87..0000000 --- a/Build/upload.ps1 +++ /dev/null @@ -1,13 +0,0 @@ -param([string]$apiKey) - -Invoke-WebRequest -Uri "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" -OutFile "nuget.exe" - -$files = Get-ChildItem -Path ".\NuGet" -Filter "*.nupkg" -foreach ($file in $files) -{ - Write-Host "Uploading: " $file - - .\nuget.exe push $file.Fullname $apiKey -Source https://api.nuget.org/v3/index.json -} - -Remove-Item "nuget.exe" -Force -Recurse -ErrorAction SilentlyContinue \ No newline at end of file diff --git a/Documents/Import_CodeSigningKey.md b/Documents/Import_CodeSigningKey.md deleted file mode 100644 index f14add9..0000000 --- a/Documents/Import_CodeSigningKey.md +++ /dev/null @@ -1,9 +0,0 @@ -# Import codeSigningKey.pfx -In order to import the key for code signing on a new developer machine use the following command within the VS developer command line: - -sn –i .\codeSigningKey.pfx VS_KEY_62CA73019ED23333 - -The container name may be different. - -# Check if the assembly has a strong name -sn -vf MQTTnet.dll \ No newline at end of file diff --git a/Images/nuget.png b/Images/nuget.png new file mode 100644 index 0000000000000000000000000000000000000000..b725537a9ec336b3754e416fa695b36402885761 GIT binary patch literal 4143 zcmb7HWmweB*Z#pnEZs^8N=ph#F0phkpeQ9shjhpSOLs_jFQA|g(%ma9jVQ=2-6HF{ zbP4}`zP(@Hxvn$!IoHgGxz3z(-!ln@P)#awR&oFUsI;}zp5ih5KLe5A=K)2VaXb+D zs%RU7@FN&xACHg8ytK@G@p$>45!5U39^f4oKXp^TXP%CJFdH8S00x7Jx_G$y+SzzH zh8*9#x~ z#-=ibB1@VM`bFWgyN?G1uvaC!@&{MTM=PZj{nT9u3jw{SpYoOAw}J!`T*a!vQIY ze*&D`a){V^KDd2Fq9Ggbv2GqRznHcBnXi&-8>Fx?ngkFZGJ*R_$Z3EG;jxDvXdyo)N;+9shM_JVBZ6k9_$4x*?WiW}r#&p`SRc zIgQJC-8*Y6m)o1hy4*T8zvNWy^W!|5N<`z-pNFHgOq5LZP^{o+CJ}+d9=}r%NPq(D zqahN2`iFSB90b`BAWV}@1ng6|fc%R8Psp(5Lb?gKf-ScJ=q(xW#Ce&!sa~rVS&G1jHPnIezZY~Q{E9%#1}tF z$onK5S?)gFCN=-fF!X>Kmdaix!ykt#lt#Cpu`LVn%wbrv+~=4Q0|@ETQU zbS(8QI&Xd37{cWE>HzC3EXTo@E?o|XyVITvDLS;HrhO&#$9`WV+LW#)9+W;=Uw>C) zu3nmJr83vkwcJQxF5>;HieCDSAj_`>U%LfS(OL^lOYep)$=Or|jiqlNsV_DtPaGTo zwPe=sznxHYdT-uPXZyNv-uIqjS9$gfEpk^Wq;q);#^{b+(VLjAx~&Ua_SkMy2T}68 zx$voEWTXYVJgo}BJ%1?(WSi@kG6|tX7Iw2upPQAR{BaWe0vaC^0kZO_i+HWS5c=O(Z!m>*w0TU^XoFw^7v(w)=p z!2AO~L@w&N_;o*u2n5yRarqnxU7V-7r*oZYkho(u;fTMe-P6?@$4n;JC z9SyS&t}yF435c|JJVA+XNrLK3BM`HGWB#k(t9g%VZ*ZyQ*{k$_FOG^{wdkO33MB8H zPTuuS0E8Wz^v%(lA>l7{_bJ4h;l&EZ1O$T$xafA8 zE4l4lu}yk{EKVhl$K#db6FhuE)fZ=3yt<%oKh3i{eD~6xoh7hK>G`yqTyE$3j~t0K zgOeNp853<)r02i!47EF=+snB>x#9EJ!YYNx!N%B@oFztCr-k- zPNd~w*hchmNGz4_SdQ3cqarff9gVbg$-X1){@*b&9{KX2ta{A1mr-qXfl?DjgzA;B z!uD6Dds#JCxnjCI#6zRIgWcmqh9NjEd?UGJSJD%GwF-QHp*@o!6m}jpcSc*d>RDda z`1PR$RZx$;m35YBA6m)#r@-1682jVh`|ATIsm&jW4E!JD;qxw6ry8_7!x~j?`N`Q*MuW+1>q2Y4pY@$rzLvB0! zU09UJv$FvjOv?B7_59&$(L_~l#Qc0ZJF;kLoWn~+I=_%zs9sA;#>K)4eA+Q~#^_%$ z%Z!O6cEzQK7N;7FmlSu48n?&WUjoBpI*E#xT2H25zM5{qpuuik*VT1A{HZ~D`H{4Y zTlF=Ai%q;ozl#+y&7Qr97L}28ZfL%OP$7QOf#t4yt!Tu8u0KbVt*usTlgwB8<$9aJ zw`}|cCa+#-n0a`_g&a#RafScu+4p{HP5Ethc1`G@2H6xBuV}cYv`5xUUN@C{^!@cp zw=BlxV!CC{XCA`P6oFutHg|Fb#gI;mgB4U;YJ_LEvX)PyzLm8-k6B>g)^wrf_QEUl zRvZtnxX&~P7YP-8%kudeL5ye5wohpfODUSk&BJ-^xM^DAW|YqOHJUcA?BPQ_xsU?z z?Ts|7@0?Ion=C@^o{`;f8lFE1%E~fY`$A5>LnV!RF%$ zW=~h>_B~d22A4X5dxAT3qqmaIn*m_#@K8p=mgwSW$EYZCbLMC1WupXC>4=gPezO1P z)4M*zrI0B~YzGWdR*pWwBuPnTSY3(GZ+*MixB5Hcg?Mj)Y$oRojz&6g%ktckVJ3&7+GNh~`L>I6_np4G z$iMw6DF&q}W$y;(Y6Eyf#ejNIZqBLEwLaGc)Pd=1-DM2gU%KR+Ep(NXmRsgQMB0}9 z6FNFmR#v=#l{Hk73>1UE<>tasJ)-J4gofu~_OnC+TK`ub4Lm?N4UXmJ1jBUq*Cwm* z)UOqq3Tx>igfmp2*SI<^;O?y5q5W)6ER34-G+g?{6NmtUIFj>b<&HWK8Xex!*a893 z?ja=;fnau~d2%du^m=d=lrB7#o>0xY=6vdSt49U+OhyLc(rip-Vww~R6l^KT^WKdA zGR7q%PM3Y((B!E~pcMy9>2~R7fGSWB4=U$`zulXy_<4uZ0AGBj!ptW@uago5ltPOR z?M5E4P?j%{I=A43gmC_mpDmHPW|D?MO;<>ltZ`ArlMr-8L+j64q)=sKQ}1;~9&Xe9 z>-6nm?ll(Erft=2diV&0{EO||HTG{E5GQT!foJwQQhixm#FX%V5);`zoUdW*J;|9Y zo8LNc(+EB0-ubO;;GJ?q1Le!M6r5(t526kqX+a3-F@df3^d_pBIIZ`@IZHhN}c=7~B$HuvX*C2(5tJfQM zmlzXrXQTpJ$m0phId1x@|GXwxx;(d=`g^_0j8+4>T0A*ZOBIBI1}#2~+`HY%EYJ3J zA4z$Uy_!*t?bRK=Jdv7=5q+GA*mK?=9~Wm!X67U*%#bs)|F@ zx-cB>G!-8jki9#l9qUa8m(c3LRU9kbrgyh3Iy0s!e0WMHYa6}wX6imbhU5;YJp4Rf z3Y>oE@KJAG5c9W}cRVmPV&jHkTiL8cA=OO!o~&1}Sjc^{Uy*P5ab^TEP2amLUKTkA zK}=2Ke!FAUnNko#s4qvcBX!f{$aNFsM`P!g@}YO(V={jC=2J@iy60*~oQT){czYzp z3>UD@3T!$A27ZIx=ZbIgPAH5Ykqg7>pML_1%6glPjR9$Y%*HS&XZzWXAd}#Ix?fu< zZR=E%Y3OwF`{^>SIcRJRHE4~lyXjDwk5E81!0?HJ%`heX?u7Rb=ykJ;o+1qTq@dmS z&(G*%zjp&q13AnM0TwKVX)`iiD+s8Ze z8}7@PZFL6{H8<2~Yb);|gQCG{>YPYpi}9H6)hrJW3z(HF%2r+L((#S)Jy+Do_d=o) zWlq=$WWJ1F6L*XrDD~_B_xyZ{ierF_5>*#3fCyx45bwJ%25^{tsy)lm)DU<1bAU#SM#>!D0P8R&RD)@U-;A8+WpB zaT#;>pjW-Q!nj0X{G=Fc>9MdpssmVC0&?=!Kun>O(JS-)KX3S=BJg!5)8R}voJc+^ zk!uy4{&n@5q=}7aN3`2k*VNR!V6WK?{|2_-n_nwY_&Hw@r{CGo*tpCL=!00P=u~5A zWrJmwug)|p=E}%uG~d6NGquzYQ74u(sYQqhAT|ccy6Wsr>?}tX02QxC4;c7p4~DO1H?KO7w2FD=B$NZ?lh z>2~!VF~fTqGQn4>#}2CsP?`CbkXIUX5K=k-Egp|810kZ+HaX`$Bm#1fIU|uU2!1KP zKTJ_3>;l|LDS2~*Ayo&U6$3p^C!MT#Z^OM%4xGdV@OwS^og!8SLXur$&@_nvy9N>t zJM||~2dK-3l$3!vSs@mrLG-}Lk;5FwAQGP;ZI%fmufmqFp1U~Y%RzR)YS z&pd!q0E*6Y1U&Vpo#Qbx5yGx+C5NsgWn*3Rt(R&&GSmRPfjM`*@{Cqc5n(6(9e`r- zYrEMkCMIs`lnofHh<+Fd0^IEk&p<+z2T;tblh zXArS00;(!(*De@NBvFoeo*V(?OnG1-g}P%NSytc#SZuHq6~>TPZSzxR)LqB*_$|o)ApqZUlB>$S~J(Pr~L&lbV+zMfgeSg(S-HUq;IaP zp@X}^R+T|P0K?>?XT+)vv8|Yh+hB1NFWIm0yxeg%8q}IX_$8e9mXwAy WKD3-Wh5-M$2ej3pYPBlXk^ci< + Required + Required + Required + Required + False + False + False + False + False + False NEVER + False + NEVER + False + False False False False + True + True + CHOP_IF_LONG + CHOP_IF_LONG + CHOP_IF_LONG + CHOP_IF_LONG + 180 + CHOP_IF_LONG + True + OnDifferentLines + OnDifferentLines + <?xml version="1.0" encoding="utf-16"?> +<Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns"> + <TypePattern DisplayName="Non-reorderable types"> + <TypePattern.Match> + <Or> + <And> + <Kind Is="Interface" /> + <Or> + <HasAttribute Name="System.Runtime.InteropServices.InterfaceTypeAttribute" /> + <HasAttribute Name="System.Runtime.InteropServices.ComImport" /> + </Or> + </And> + <Kind Is="Struct" /> + <HasAttribute Name="JetBrains.Annotations.NoReorderAttribute" /> + <HasAttribute Name="JetBrains.Annotations.NoReorder" /> + </Or> + </TypePattern.Match> + </TypePattern> + <TypePattern DisplayName="xUnit.net Test Classes" RemoveRegions="All"> + <TypePattern.Match> + <And> + <Kind Is="Class" /> + <HasMember> + <And> + <Kind Is="Method" /> + <HasAttribute Name="Xunit.FactAttribute" Inherited="True" /> + <HasAttribute Name="Xunit.TheoryAttribute" Inherited="True" /> + </And> + </HasMember> + </And> + </TypePattern.Match> + <Entry DisplayName="Setup/Teardown Methods"> + <Entry.Match> + <Or> + <Kind Is="Constructor" /> + <And> + <Kind Is="Method" /> + <ImplementsInterface Name="System.IDisposable" /> + </And> + </Or> + </Entry.Match> + <Entry.SortBy> + <Kind Order="Constructor" /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="All other members" /> + <Entry Priority="100" DisplayName="Test Methods"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <HasAttribute Name="Xunit.FactAttribute" /> + <HasAttribute Name="Xunit.TheoryAttribute" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + </TypePattern> + <TypePattern DisplayName="NUnit Test Fixtures" RemoveRegions="All"> + <TypePattern.Match> + <And> + <Kind Is="Class" /> + <Or> + <HasAttribute Name="NUnit.Framework.TestFixtureAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.TestFixtureSourceAttribute" Inherited="True" /> + <HasMember> + <And> + <Kind Is="Method" /> + <HasAttribute Name="NUnit.Framework.TestAttribute" /> + <HasAttribute Name="NUnit.Framework.TestCaseAttribute" /> + <HasAttribute Name="NUnit.Framework.TestCaseSourceAttribute" /> + </And> + </HasMember> + </Or> + </And> + </TypePattern.Match> + <Entry DisplayName="Setup/Teardown Methods"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <Or> + <HasAttribute Name="NUnit.Framework.SetUpAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.TearDownAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.TestFixtureSetUpAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.TestFixtureTearDownAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.OneTimeSetUpAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.OneTimeTearDownAttribute" Inherited="True" /> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="All other members" /> + <Entry Priority="100" DisplayName="Test Methods"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <HasAttribute Name="NUnit.Framework.TestAttribute" /> + <HasAttribute Name="NUnit.Framework.TestCaseAttribute" /> + <HasAttribute Name="NUnit.Framework.TestCaseSourceAttribute" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + </TypePattern> + <TypePattern DisplayName="Default Pattern"> + <Entry Priority="100" DisplayName="Public Delegates"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Kind Is="Delegate" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + <Entry Priority="100" DisplayName="Public Enums"> + <Entry.Match> + <And> + <Access Is="Public" /> + <Kind Is="Enum" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Static Fields and Constants"> + <Entry.Match> + <Or> + <Kind Is="Constant" /> + <And> + <Kind Is="Field" /> + <Static /> + </And> + </Or> + </Entry.Match> + <Entry.SortBy> + <Kind Order="Constant Field" /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Fields"> + <Entry.Match> + <And> + <Kind Is="Field" /> + <Not> + <Static /> + </Not> + </And> + </Entry.Match> + <Entry.SortBy> + <Readonly /> + <Name /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Constructors"> + <Entry.Match> + <Kind Is="Constructor" /> + </Entry.Match> + <Entry.SortBy> + <Static /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Events"> + <Entry.Match> + <Kind Is="Event" /> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Properties, Indexers"> + <Entry.Match> + <Or> + <Kind Is="Property" /> + <Kind Is="Indexer" /> + </Or> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="All other members"> + <Entry.SortBy> + <Access Order="Public Internal Protected Private" /> + <Name /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Nested Types"> + <Entry.Match> + <Kind Is="Type" /> + </Entry.Match> + </Entry> + </TypePattern> +</Patterns> True True True True True True + True True True True True + True True \ No newline at end of file diff --git a/README.md b/README.md index 6cecfa5..6fd25db 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,19 @@

- +

[![NuGet Badge](https://buildstats.info/nuget/MQTTnet)](https://www.nuget.org/packages/MQTTnet) -[![BCH compliance](https://bettercodehub.com/edge/badge/chkr1011/MQTTnet?branch=master)](https://bettercodehub.com/) +[![CI](https://github.com/dotnet/MQTTnet/actions/workflows/ci.yml/badge.svg)](https://github.com/dotnet/MQTTnet/actions/workflows/ci.yml) +[![MyGet](https://img.shields.io/myget/mqttnet/v/mqttnet?color=orange&label=MyGet-Preview)](https://www.myget.org/feed/mqttnet/package/nuget/MQTTnet) +![Size](https://img.shields.io/github/repo-size/dotnet/MQTTnet.svg) [![Join the chat at https://gitter.im/MQTTnet/community](https://badges.gitter.im/MQTTnet/community.svg)](https://gitter.im/MQTTnet/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://raw.githubusercontent.com/chkr1011/MQTTnet/master/LICENSE) +[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://raw.githubusercontent.com/dotnet/MQTTnet/master/LICENSE) # MQTTnet -MQTTnet is a high performance .NET library for MQTT based communication. It provides a MQTT client and a MQTT server (broker). The implementation is based on the documentation from . +MQTTnet is a high performance .NET library for MQTT based communication. It provides a MQTT client and a MQTT server (broker) and supports the MQTT protocol up to version 5. ## Features @@ -21,11 +23,11 @@ MQTTnet is a high performance .NET library for MQTT based communication. It prov * TLS support for client and server (but not UWP servers) * Extensible communication channels (e.g. In-Memory, TCP, TCP+TLS, WS) * Lightweight (only the low level implementation of MQTT, no overhead) -* Performance optimized (processing ~70.000 messages / second)* +* Performance optimized (processing ~150.000 messages / second)* * Uniform API across all supported versions of the MQTT protocol * Interfaces included for mocking and testing * Access to internal trace messages -* Unit tested (~250 tests) +* Unit tested (~636 tests) * No external dependencies \* Tested on local machine (Intel i7 8700K) with MQTTnet client and server running in the same process using the TCP channel. The app for verification is part of this repository and stored in _/Tests/MQTTnet.TestApp.NetCore_. @@ -51,63 +53,20 @@ MQTTnet is a high performance .NET library for MQTT based communication. It prov * Validate subscriptions and deny subscribing of certain topics depending on requesting clients * Connect clients with different protocol versions at the same time. -## MQTTnet Server - -_MQTTnet Server_ is a standalone cross platform MQTT server (like mosquitto) basing on this library. It has the following features. -* Running portable (no installation required) -* Runs under Windows, Linux, macOS, Raspberry Pi -* Python scripting support for manipulating messages, validation of clients, building business logic etc. -* Supports WebSocket and TCP (with and without TLS) connections -* Provides a HTTP based API (including Swagger endpoint) -* Extensive configuration parameters and customization supported - ## Supported frameworks -* .NET 5.0+ -* .NET Standard 1.3+ -* .NET Core 1.1+ -* .NET Core App 1.1+ -* .NET Framework 4.5.2+ -* Mono 5.2+ -* Universal Windows Platform (UWP) 10.0.10240+ (Windows 10 IoT Core) -* Xamarin.Android 7.5+ -* Xamarin.iOS 10.14+ -* Blazor WebAssembly 3.2.0+ - -## Supported platforms - -* x86 -* x64 -* AnyCPU -* ARM - -## Supported OS - -* Windows -* Windows 10 IoT Core -* Linux (Ubuntu, Raspbian etc.) -* macOS -* Android -* iOS - -## Supported MQTT versions - -* 5.0.0 -* 3.1.1 -* 3.1.0 - -## Nuget - -This library is available as a nuget package: - -## Examples - -Please find examples and the documentation at the Wiki of this repository (). - -## Contributions - -If you want to contribute to this project just create a pull request. But only pull requests which are matching the code style of this library will be accepted. Before creating a pull request please have a look at the library to get an overview of the required style. -Also additions and updates in the Wiki are welcome. +| Framwork | Version | +| ------------------ | ----------- | +|.NET | 5.0+ | +|.NET Framework | 4.5.2+ | +|.NET Standard | 1.3+ | +|.NET Core | 1.1+ | +|.NET Core App | 1.1+ | +| Mono | 5.2+ | +| UWP | 10.0.10240+ | +| Xamarin.Android | 7.5+ | +| Xamarin.iOS | 10.14+ | +| Blazor WebAssembly | 3.2.0+ | ## References @@ -118,35 +77,20 @@ This library is used in the following projects: * MQTT Client Rx (Wrapper for Reactive Extensions, ) * MQTT Client Rx (Managed Client Wrapper for Reactive Extensions, ) * MQTT Tester (MQTT client test app for [Android](https://play.google.com/store/apps/details?id=com.liveowl.mqtttester) and [iOS](https://itunes.apple.com/us/app/mqtt-tester/id1278621826?mt=8)) -* MQTTnet App (Cross platform client application for MQTT debugging, inspection etc., ) +* MQTTnet App (Cross platform client application for MQTT debugging, inspection etc., ) * Wirehome.Core (Open Source Home Automation system for .NET Core, ) * SparkplugNet (Sparkplug library for .Net, ) * Silverback (Framework to build event-driven applications - support for MQTT, Kafka & RabbitMQ) -Further projects using this project can be found under https://github.com/chkr1011/MQTTnet/network/dependents. +Further projects using this project can be found under https://github.com/dotnet/MQTTnet/network/dependents. If you use this library and want to see your project here please create a pull request. -## License - -MIT License - -MQTTnet Copyright (c) 2016-2021 Christian Kratky +## Code of Conduct -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our community. +For more information see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct). -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +## .NET Foundation -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +This project is supported by the [.NET Foundation](https://dotnetfoundation.org). \ No newline at end of file diff --git a/Samples/Client/Client_Connection_Samples.cs b/Samples/Client/Client_Connection_Samples.cs new file mode 100644 index 0000000..97cb466 --- /dev/null +++ b/Samples/Client/Client_Connection_Samples.cs @@ -0,0 +1,271 @@ +// 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.Client; +using MQTTnet.Formatter; +using MQTTnet.Samples.Helpers; + +namespace MQTTnet.Samples.Client; + +public static class Client_Connection_Samples +{ + public static async Task Connect_Client() + { + /* + * This sample creates a simple MQTT client and connects to a public broker. + * + * Always dispose the client when it is no longer used. + * The default version of MQTT is 3.1.1. + */ + + var mqttFactory = new MqttFactory(); + + using (var mqttClient = mqttFactory.CreateMqttClient()) + { + // Use builder classes where possible in this project. + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); + + // This will throw an exception if the server is not available. + // The result from this message returns additional data which was sent + // from the server. Please refer to the MQTT protocol specification for details. + var response = await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + + Console.WriteLine("The MQTT client is connected."); + + response.DumpToConsole(); + + // Send a clean disconnect to the server by calling _DisconnectAsync_. Without this the TCP connection + // gets dropped and the server will handle this as a non clean disconnect (see MQTT spec for details). + var mqttClientDisconnectOptions = mqttFactory.CreateClientDisconnectOptionsBuilder().Build(); + + await mqttClient.DisconnectAsync(mqttClientDisconnectOptions, CancellationToken.None); + } + } + + public static async Task Connect_Client_Timeout() + { + /* + * This sample creates a simple MQTT client and connects to an invalid broker using a timeout. + * + * This is a modified version of the sample _Connect_Client_! See other sample for more details. + */ + + var mqttFactory = new MqttFactory(); + + using (var mqttClient = mqttFactory.CreateMqttClient()) + { + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1").Build(); + + try + { + using (var timeoutToken = new CancellationTokenSource(TimeSpan.FromSeconds(1))) + { + await mqttClient.ConnectAsync(mqttClientOptions, timeoutToken.Token); + } + } + catch (OperationCanceledException) + { + Console.WriteLine("Timeout while connecting."); + } + } + } + + public static async Task Connect_Client_Using_MQTTv5() + { + /* + * This sample creates a simple MQTT client and connects to a public broker using MQTTv5. + * + * This is a modified version of the sample _Connect_Client_! See other sample for more details. + */ + + var mqttFactory = new MqttFactory(); + + using (var mqttClient = mqttFactory.CreateMqttClient()) + { + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").WithProtocolVersion(MqttProtocolVersion.V500).Build(); + + // In MQTTv5 the response contains much more information. + var response = await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + + Console.WriteLine("The MQTT client is connected."); + + response.DumpToConsole(); + } + } + + public static async Task Connect_Client_Using_TLS_1_2() + { + /* + * This sample creates a simple MQTT client and connects to a public broker using TLS 1.2 encryption. + * + * This is a modified version of the sample _Connect_Client_! See other sample for more details. + */ + + var mqttFactory = new MqttFactory(); + + using (var mqttClient = mqttFactory.CreateMqttClient()) + { + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("mqtt.fluux.io") + .WithTls( + o => + { + o.SslProtocol = SslProtocols.Tls12; + }) + .Build(); + + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + + Console.WriteLine("The MQTT client is connected."); + } + } + + public static async Task Connect_Client_Using_WebSockets() + { + /* + * This sample creates a simple MQTT client and connects to a public broker using a WebSocket connection. + * + * This is a modified version of the sample _Connect_Client_! See other sample for more details. + */ + + var mqttFactory = new MqttFactory(); + + using (var mqttClient = mqttFactory.CreateMqttClient()) + { + var mqttClientOptions = new MqttClientOptionsBuilder().WithWebSocketServer("broker.hivemq.com:8000/mqtt").Build(); + + var response = await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + + Console.WriteLine("The MQTT client is connected."); + + response.DumpToConsole(); + } + } + + public static async Task Connect_Client_With_TLS_Encryption() + { + /* + * This sample creates a simple MQTT client and connects to a public broker with enabled TLS encryption. + * + * This is a modified version of the sample _Connect_Client_! See other sample for more details. + */ + + var mqttFactory = new MqttFactory(); + + using (var mqttClient = mqttFactory.CreateMqttClient()) + { + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("test.mosquitto.org", 8883) + .WithTls( + o => + { + o.SslProtocol = SslProtocols.Tls12; // The default value is determined by the OS. Set manually to force version. + }) + .Build(); + + // In MQTTv5 the response contains much more information. + var response = await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + + Console.WriteLine("The MQTT client is connected."); + + response.DumpToConsole(); + } + } + + public static async Task Ping_Server() + { + /* + * This sample sends a PINGREQ packet to the server and waits for a reply. + * + * This is only supported in METTv5.0.0+. + */ + + var mqttFactory = new MqttFactory(); + + using (var mqttClient = mqttFactory.CreateMqttClient()) + { + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); + + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + + // This will throw an exception if the server does not reply. + await mqttClient.PingAsync(CancellationToken.None); + + Console.WriteLine("The MQTT server replied to the ping request."); + } + } + + public static async Task Reconnect_Using_Event() + { + /* + * This sample shows how to reconnect when the connection was dropped. + * This approach uses one of the events from the client. + * This approach has a risk of dead locks! Consider using the timer approach (see sample). + */ + + var mqttFactory = new MqttFactory(); + + using (var mqttClient = mqttFactory.CreateMqttClient()) + { + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); + + mqttClient.DisconnectedAsync += async e => + { + if (e.ClientWasConnected) + { + // Use the current options as the new options. + await mqttClient.ConnectAsync(mqttClient.Options); + } + }; + + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + } + } + + public static void Reconnect_Using_Timer() + { + /* + * This sample shows how to reconnect when the connection was dropped. + * This approach uses a custom Task/Thread which will monitor the connection status. + * This is the recommended way but requires more custom code! + */ + + var mqttFactory = new MqttFactory(); + + using (var mqttClient = mqttFactory.CreateMqttClient()) + { + var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("broker.hivemq.com").Build(); + + _ = Task.Run( + async () => + { + // User proper cancellation and no while(true). + while (true) + { + try + { + // This code will also do the very first connect! So no call to _ConnectAsync_ is required + // in the first place. + if (!mqttClient.IsConnected) + { + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + + // Subscribe to topics when session is clean etc. + + Console.WriteLine("The MQTT client is connected."); + } + } + catch + { + // Handle the exception properly (logging etc.). + } + finally + { + // Check the connection state every 5 seconds and perform a reconnect if required. + await Task.Delay(TimeSpan.FromSeconds(5)); + } + } + }); + } + } +} \ No newline at end of file diff --git a/Samples/Client/Client_Publish_Samples.cs b/Samples/Client/Client_Publish_Samples.cs new file mode 100644 index 0000000..7bfe72b --- /dev/null +++ b/Samples/Client/Client_Publish_Samples.cs @@ -0,0 +1,42 @@ +// 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.Client; + +namespace MQTTnet.Samples.Client; + +public static class Client_Publish_Samples +{ + public static async Task Publish_Application_Message() + { + /* + * This sample pushes a simple application message including a topic and a payload. + * + * Always use builders where they exist. Builders (in this project) are designed to be + * backward compatible. Creating an _MqttApplicationMessage_ via its constructor is also + * supported but the class might change often in future releases where the builder does not + * or at least provides backward compatibility where possible. + */ + + var mqttFactory = new MqttFactory(); + + using (var mqttClient = mqttFactory.CreateMqttClient()) + { + var mqttClientOptions = new MqttClientOptionsBuilder() + .WithTcpServer("broker.hivemq.com") + .Build(); + + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + + var applicationMessage = new MqttApplicationMessageBuilder() + .WithTopic("samples/temperature/living_room") + .WithPayload("19.5") + .Build(); + + await mqttClient.PublishAsync(applicationMessage, CancellationToken.None); + + Console.WriteLine("MQTT application message is published."); + } + } +} \ No newline at end of file diff --git a/Samples/Client/Client_Subscribe_Samples.cs b/Samples/Client/Client_Subscribe_Samples.cs new file mode 100644 index 0000000..7cedf05 --- /dev/null +++ b/Samples/Client/Client_Subscribe_Samples.cs @@ -0,0 +1,80 @@ +// 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.Client; +using MQTTnet.Samples.Helpers; + +namespace MQTTnet.Samples.Client; + +public static class Client_Subscribe_Samples +{ + public static async Task Handle_Received_Application_Message() + { + /* + * This sample subscribes to a topic and processes the received message. + */ + + var mqttFactory = new MqttFactory(); + + using (var mqttClient = mqttFactory.CreateMqttClient()) + { + var mqttClientOptions = new MqttClientOptionsBuilder() + .WithTcpServer("broker.hivemq.com") + .Build(); + + // Setup message handling before connecting so that queued messages + // are also handled properly. When there is no event handler attached all + // received messages get lost. + mqttClient.ApplicationMessageReceivedAsync += e => + { + Console.WriteLine("Received application message."); + e.DumpToConsole(); + + return Task.CompletedTask; + }; + + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + + var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder() + .WithTopicFilter(f => { f.WithTopic("mqttnet/samples/topic/2"); }) + .Build(); + + await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None); + + Console.WriteLine("MQTT client subscribed to topic."); + + Console.WriteLine("Press enter to exit."); + Console.ReadLine(); + } + } + + public static async Task Subscribe_Topic() + { + /* + * This sample subscribes to a topic. + */ + + var mqttFactory = new MqttFactory(); + + using (var mqttClient = mqttFactory.CreateMqttClient()) + { + var mqttClientOptions = new MqttClientOptionsBuilder() + .WithTcpServer("broker.hivemq.com") + .Build(); + + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + + var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder() + .WithTopicFilter(f => { f.WithTopic("mqttnet/samples/topic/1"); }) + .Build(); + + var response = await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None); + + Console.WriteLine("MQTT client subscribed to topic."); + + // The response contains additional data sent by the server after subscribing. + response.DumpToConsole(); + } + } +} \ No newline at end of file diff --git a/Samples/Diagnostics/Logger_Samples.cs b/Samples/Diagnostics/Logger_Samples.cs new file mode 100644 index 0000000..313d341 --- /dev/null +++ b/Samples/Diagnostics/Logger_Samples.cs @@ -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 System.Text; +using MQTTnet.Diagnostics; + +namespace MQTTnet.Samples.Diagnostics; + +public static class Logger_Samples +{ + public static async Task Create_Custom_Logger() + { + /* + * This sample covers the creation of a custom logger which can be used to forward MQTTnet log messages + * to other loggers like Microsoft logger or Serilog or log4net etc. + */ + + var mqttFactory = new MqttFactory(new MyLogger()); + + var mqttClientOptions = mqttFactory.CreateClientOptionsBuilder() + .WithTcpServer("broker.hivemq.com") + .Build(); + + using (var mqttClient = mqttFactory.CreateMqttClient()) + { + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + + Console.WriteLine("MQTT client is connected."); + + var mqttClientDisconnectOptions = mqttFactory.CreateClientDisconnectOptionsBuilder() + .Build(); + + await mqttClient.DisconnectAsync(mqttClientDisconnectOptions, CancellationToken.None); + } + } + + public static async Task Use_Event_Logger() + { + /* + * This sample shows how to get logs from the library. + * + * ATTENTION: Only use the logger for debugging etc. The performance is heavily decreased when a logger is used. + */ + + // The logger ID is optional but can be set do distinguish different logger instances. + var mqttEventLogger = new MqttNetEventLogger("MyCustomLogger"); + + mqttEventLogger.LogMessagePublished += (sender, args) => + { + var output = new StringBuilder(); + output.AppendLine($">> [{args.LogMessage.Timestamp:O}] [{args.LogMessage.ThreadId}] [{args.LogMessage.Source}] [{args.LogMessage.Level}]: {args.LogMessage.Message}"); + if (args.LogMessage.Exception != null) + { + output.AppendLine(args.LogMessage.Exception.ToString()); + } + + Console.Write(output); + }; + + var mqttFactory = new MqttFactory(mqttEventLogger); + + var mqttClientOptions = mqttFactory.CreateClientOptionsBuilder() + .WithTcpServer("broker.hivemq.com") + .Build(); + + using (var mqttClient = mqttFactory.CreateMqttClient()) + { + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + + Console.WriteLine("MQTT client is connected."); + + var mqttClientDisconnectOptions = mqttFactory.CreateClientDisconnectOptionsBuilder() + .Build(); + + await mqttClient.DisconnectAsync(mqttClientDisconnectOptions, CancellationToken.None); + } + } + + sealed class MyLogger : IMqttNetLogger + { + public bool IsEnabled { get; set; } = true; + + public void Publish(MqttNetLogLevel logLevel, string source, string message, object[] parameters, Exception exception) + { + // Forward the log message to other loggers. + // 1. Convert log level to matching log level in target logger. + // 2. Call target logger and pass data accordingly. + } + } +} \ No newline at end of file diff --git a/Samples/Diagnostics/PackageInspection_Samples.cs b/Samples/Diagnostics/PackageInspection_Samples.cs new file mode 100644 index 0000000..58c7a54 --- /dev/null +++ b/Samples/Diagnostics/PackageInspection_Samples.cs @@ -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 MQTTnet.Diagnostics; + +namespace MQTTnet.Samples.Diagnostics; + +public static class PackageInspection_Samples +{ + public static async Task Inspect_Outgoing_Package() + { + /* + * This sample covers the inspection of outgoing packages from the client. + */ + + var mqttFactory = new MqttFactory(); + + using (var mqttClient = mqttFactory.CreateMqttClient()) + { + var mqttClientOptions = mqttFactory.CreateClientOptionsBuilder() + .WithTcpServer("broker.hivemq.com") + .Build(); + + mqttClient.InspectPackage += OnInspectPackage; + + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + + Console.WriteLine("MQTT client is connected."); + + var mqttClientDisconnectOptions = mqttFactory.CreateClientDisconnectOptionsBuilder() + .Build(); + + await mqttClient.DisconnectAsync(mqttClientDisconnectOptions, CancellationToken.None); + } + } + + static Task OnInspectPackage(InspectMqttPacketEventArgs eventArgs) + { + if (eventArgs.Direction == MqttPacketFlowDirection.Inbound) + { + Console.WriteLine($"IN: {Convert.ToBase64String(eventArgs.Buffer)}"); + } + else + { + Console.WriteLine($"OUT: {Convert.ToBase64String(eventArgs.Buffer)}"); + } + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Samples/Helpers/ObjectExtensions.cs b/Samples/Helpers/ObjectExtensions.cs new file mode 100644 index 0000000..4384fa9 --- /dev/null +++ b/Samples/Helpers/ObjectExtensions.cs @@ -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.Text.Json; + +namespace MQTTnet.Samples.Helpers; + +internal static class ObjectExtensions +{ + public static TObject DumpToConsole(this TObject @object) + { + var output = "NULL"; + if (@object != null) + { + output = JsonSerializer.Serialize(@object, new JsonSerializerOptions + { + WriteIndented = true + }); + } + + Console.WriteLine($"[{@object?.GetType().Name}]:\r\n{output}"); + return @object; + } +} \ No newline at end of file diff --git a/Samples/MQTTnet.Samples.csproj b/Samples/MQTTnet.Samples.csproj new file mode 100644 index 0000000..f711e00 --- /dev/null +++ b/Samples/MQTTnet.Samples.csproj @@ -0,0 +1,26 @@ + + + + Exe + net6.0 + enable + enable + false + true + false + false + + + + + + + + + + + + + + + diff --git a/Samples/ManagedClient/Managed_Client_Simple_Samples.cs b/Samples/ManagedClient/Managed_Client_Simple_Samples.cs new file mode 100644 index 0000000..39a6892 --- /dev/null +++ b/Samples/ManagedClient/Managed_Client_Simple_Samples.cs @@ -0,0 +1,44 @@ +using MQTTnet.Client; +using MQTTnet.Extensions.ManagedClient; + +namespace MQTTnet.Samples.ManagedClient; + +public sealed class Managed_Client_Simple_Samples +{ + public static async Task Connect_Client() + { + /* + * This sample creates a simple managed MQTT client and connects to a public broker. + * + * The managed client extends the existing _MqttClient_. It adds the following features. + * - Reconnecting when connection is lost. + * - Storing pending messages in an internal queue so that an enqueue is possible while the client remains not connected. + */ + + var mqttFactory = new MqttFactory(); + + using (var managedMqttClient = mqttFactory.CreateManagedMqttClient()) + { + var mqttClientOptions = new MqttClientOptionsBuilder() + .WithTcpServer("broker.hivemq.com") + .Build(); + + var managedMqttClientOptions = new ManagedMqttClientOptionsBuilder() + .WithClientOptions(mqttClientOptions) + .Build(); + + await managedMqttClient.StartAsync(managedMqttClientOptions); + + // The application message is not sent. It is stored in an internal queue and + // will be sent when the client is connected. + await managedMqttClient.EnqueueAsync("Topic", "Payload"); + + Console.WriteLine("The managed MQTT client is connected."); + + // Wait until the queue is fully processed. + SpinWait.SpinUntil(() => managedMqttClient.PendingApplicationMessagesCount == 0, 10000); + + Console.WriteLine($"Pending messages = {managedMqttClient.PendingApplicationMessagesCount}"); + } + } +} \ No newline at end of file diff --git a/Samples/Program.cs b/Samples/Program.cs new file mode 100644 index 0000000..fdfe0e5 --- /dev/null +++ b/Samples/Program.cs @@ -0,0 +1,48 @@ +// 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.Reflection; + +Console.WriteLine("Welcome to MQTTnet samples!"); +Console.WriteLine(); + +var sampleClasses = Assembly.GetExecutingAssembly().GetExportedTypes().OrderBy(c => c.Name).ToList(); + +var index = 0; +foreach (var sampleClass in sampleClasses) +{ + Console.WriteLine($"{index} = {sampleClass.Name}"); + index++; +} + +Console.Write("Please choose sample class (press Enter to continue): "); +var input = Console.ReadLine(); +var selectedIndex = int.Parse(input ?? "0"); +var selectedSampleClass = sampleClasses[selectedIndex]; +var sampleMethods = selectedSampleClass.GetMethods(BindingFlags.Static | BindingFlags.Public).OrderBy(m => m.Name).ToList(); + +index = 0; +foreach (var sampleMethod in sampleMethods) +{ + Console.WriteLine($"{index} = {sampleMethod.Name}"); + index++; +} + +Console.Write("Please choose sample (press Enter to continue): "); +input = Console.ReadLine(); +selectedIndex = int.Parse(input ?? "0"); +var selectedSampleMethod = sampleMethods[selectedIndex]; + +Console.WriteLine("Executing sample..."); +Console.WriteLine(); + +try +{ + var task = selectedSampleMethod.Invoke(null, null) as Task; + task?.Wait(); +} +catch (Exception exception) +{ + Console.WriteLine(exception.ToString()); +} \ No newline at end of file diff --git a/Samples/RpcClient/RcpClient_Samples.cs b/Samples/RpcClient/RcpClient_Samples.cs new file mode 100644 index 0000000..21f2b95 --- /dev/null +++ b/Samples/RpcClient/RcpClient_Samples.cs @@ -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 MQTTnet.Client; +using MQTTnet.Extensions.Rpc; +using MQTTnet.Protocol; + +namespace MQTTnet.Samples.RpcClient; + +public static class RcpClient_Samples +{ + /* + * The extension MQTTnet.Extensions.Rpc (available as nuget) allows sending a request and waiting for the matching reply. + * This is done via defining a pattern which uses the topic to correlate the request and the response. + * From client usage it is possible to define a timeout. + */ + + public static async Task Send_Request() + { + var mqttFactory = new MqttFactory(); + + // The RPC client is an addon for the existing client. So we need a regular client + // which is wrapped later. + + using (var mqttClient = mqttFactory.CreateMqttClient()) + { + var mqttClientOptions = new MqttClientOptionsBuilder() + .WithTcpServer("broker.hivemq.com") + .Build(); + + await mqttClient.ConnectAsync(mqttClientOptions); + + using (var mqttRpcClient = mqttFactory.CreateMqttRpcClient(mqttClient)) + { + // Access to a fully featured application message is not supported for RCP calls! + // The method will throw an exception when the response was not received in time. + await mqttRpcClient.ExecuteAsync(TimeSpan.FromSeconds(2), "ping", "", MqttQualityOfServiceLevel.AtMostOnce); + } + + Console.WriteLine("The RPC call was successful."); + } + } + + /* + * The device must respond to the request using the correct topic. The following C code shows how a + * smart device like an ESP8266 must respond to the above sample. + * + // If using the MQTT client PubSubClient it must be ensured + // that the request topic for each method is subscribed like the following. + mqttClient.subscribe("MQTTnet.RPC/+/ping"); + mqttClient.subscribe("MQTTnet.RPC/+/do_something"); + + // It is not allowed to change the structure of the topic. + // Otherwise RPC will not work. + // So method names can be separated using an _ or . but no +, # or /. + // If it is required to distinguish between devices + // own rules can be defined like the following: + mqttClient.subscribe("MQTTnet.RPC/+/deviceA.ping"); + mqttClient.subscribe("MQTTnet.RPC/+/deviceB.ping"); + mqttClient.subscribe("MQTTnet.RPC/+/deviceC.getTemperature"); + + // Within the callback of the MQTT client the topic must be checked + // if it belongs to MQTTnet RPC. The following code shows one + // possible way of doing this. + void mqtt_Callback(char *topic, byte *payload, unsigned int payloadLength) + { + String topicString = String(topic); + + if (topicString.startsWith("MQTTnet.RPC/")) { + String responseTopic = topicString + String("/response"); + + if (topicString.endsWith("/deviceA.ping")) { + mqtt_publish(responseTopic, "pong", false); + return; + } + } + } + + // Important notes: + // ! Do not send response message with the _retain_ flag set to true. + // ! All required data for a RPC call and the result must be placed into the payload. + */ +} \ No newline at end of file diff --git a/Samples/Server/Server_Simple_Samples.cs b/Samples/Server/Server_Simple_Samples.cs new file mode 100644 index 0000000..4017c5a --- /dev/null +++ b/Samples/Server/Server_Simple_Samples.cs @@ -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.Protocol; +using MQTTnet.Server; + +namespace MQTTnet.Samples.Server; + +public static class Server_Simple_Samples +{ + public static async Task Run_Minimal_Server() + { + /* + * This sample starts a simple MQTT server which will accept any TCP connection. + */ + + var mqttFactory = new MqttFactory(); + + // The port for the default endpoint is 1883. + // The default endpoint is NOT encrypted! + // Use the builder classes where possible. + var mqttServerOptions = new MqttServerOptionsBuilder() + .WithDefaultEndpoint() + .Build(); + + // The port can be changed using the following API (not used in this example). + new MqttServerOptionsBuilder() + .WithDefaultEndpoint() + .WithDefaultEndpointPort(1234) + .Build(); + + using (var mqttServer = mqttFactory.CreateMqttServer(mqttServerOptions)) + { + await mqttServer.StartAsync(); + + Console.WriteLine("Press Enter to exit."); + Console.ReadLine(); + + // Stop and dispose the MQTT server if it is no longer needed! + await mqttServer.StopAsync(); + } + } + + public static async Task Validating_Connections() + { + /* + * This sample starts a simple MQTT server which will check for valid credentials and client ID. + * + * See _Run_Minimal_Server_ for more information. + */ + + var mqttFactory = new MqttFactory(); + + var mqttServerOptions = new MqttServerOptionsBuilder() + .WithDefaultEndpoint() + .Build(); + + using (var mqttServer = mqttFactory.CreateMqttServer(mqttServerOptions)) + { + // Setup connection validation before starting the server so that there is + // no change to connect without valid credentials. + mqttServer.ValidatingConnectionAsync += e => + { + if (e.ClientId != "ValidClientId") + { + e.ReasonCode = MqttConnectReasonCode.ClientIdentifierNotValid; + } + + if (e.Username != "ValidUser") + { + e.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword; + } + + if (e.Password != "SecretPassword") + { + e.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword; + } + + return Task.CompletedTask; + }; + + await mqttServer.StartAsync(); + + Console.WriteLine("Press Enter to exit."); + Console.ReadLine(); + + await mqttServer.StopAsync(); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.AspNetCore.Tests/MQTTnet.AspNetCore.Tests.csproj b/Source/MQTTnet.AspNetCore.Tests/MQTTnet.AspNetCore.Tests.csproj new file mode 100644 index 0000000..830c7c0 --- /dev/null +++ b/Source/MQTTnet.AspNetCore.Tests/MQTTnet.AspNetCore.Tests.csproj @@ -0,0 +1,35 @@ + + + + netcoreapp3.1 + false + true + false + false + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/MQTTnet.AspNetCore.Tests/Mockups/ConnectionContextMockup.cs b/Source/MQTTnet.AspNetCore.Tests/Mockups/ConnectionContextMockup.cs similarity index 71% rename from Tests/MQTTnet.AspNetCore.Tests/Mockups/ConnectionContextMockup.cs rename to Source/MQTTnet.AspNetCore.Tests/Mockups/ConnectionContextMockup.cs index 296959e..0ec946d 100644 --- a/Tests/MQTTnet.AspNetCore.Tests/Mockups/ConnectionContextMockup.cs +++ b/Source/MQTTnet.AspNetCore.Tests/Mockups/ConnectionContextMockup.cs @@ -1,4 +1,8 @@ -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; using System.IO.Pipelines; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Features; diff --git a/Tests/MQTTnet.AspNetCore.Tests/Mockups/ConnectionHandlerMockup.cs b/Source/MQTTnet.AspNetCore.Tests/Mockups/ConnectionHandlerMockup.cs similarity index 70% rename from Tests/MQTTnet.AspNetCore.Tests/Mockups/ConnectionHandlerMockup.cs rename to Source/MQTTnet.AspNetCore.Tests/Mockups/ConnectionHandlerMockup.cs index 9adcebd..5f372c9 100644 --- a/Tests/MQTTnet.AspNetCore.Tests/Mockups/ConnectionHandlerMockup.cs +++ b/Source/MQTTnet.AspNetCore.Tests/Mockups/ConnectionHandlerMockup.cs @@ -1,9 +1,14 @@ -using Microsoft.AspNetCore.Connections; +// 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 Microsoft.AspNetCore.Connections; using MQTTnet.Adapter; using MQTTnet.Formatter; using MQTTnet.Server; using System; using System.Threading.Tasks; +using MQTTnet.Diagnostics; namespace MQTTnet.AspNetCore.Tests.Mockups { @@ -12,16 +17,11 @@ namespace MQTTnet.AspNetCore.Tests.Mockups public TaskCompletionSource Context { get; } = new TaskCompletionSource(); public Func ClientHandler { get; set; } - public ConnectionHandlerMockup() - { - } - public async Task OnConnectedAsync(ConnectionContext connection) { try { - var writer = new SpanBasedMqttPacketWriter(); - var formatter = new MqttPacketFormatterAdapter(writer); + var formatter = new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)); var context = new MqttConnectionContext(formatter, connection); Context.TrySetResult(context); @@ -33,7 +33,7 @@ namespace MQTTnet.AspNetCore.Tests.Mockups } } - public Task StartAsync(IMqttServerOptions options) + public Task StartAsync(MqttServerOptions options, IMqttNetLogger logger) { return Task.CompletedTask; } diff --git a/Tests/MQTTnet.AspNetCore.Tests/Mockups/DuplexPipeMockup.cs b/Source/MQTTnet.AspNetCore.Tests/Mockups/DuplexPipeMockup.cs similarity index 70% rename from Tests/MQTTnet.AspNetCore.Tests/Mockups/DuplexPipeMockup.cs rename to Source/MQTTnet.AspNetCore.Tests/Mockups/DuplexPipeMockup.cs index 306749b..24f8552 100644 --- a/Tests/MQTTnet.AspNetCore.Tests/Mockups/DuplexPipeMockup.cs +++ b/Source/MQTTnet.AspNetCore.Tests/Mockups/DuplexPipeMockup.cs @@ -1,4 +1,8 @@ -using System.IO.Pipelines; +// 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.IO.Pipelines; namespace MQTTnet.AspNetCore.Tests.Mockups { diff --git a/Tests/MQTTnet.AspNetCore.Tests/Mockups/LimitedMemoryPool.cs b/Source/MQTTnet.AspNetCore.Tests/Mockups/LimitedMemoryPool.cs similarity index 62% rename from Tests/MQTTnet.AspNetCore.Tests/Mockups/LimitedMemoryPool.cs rename to Source/MQTTnet.AspNetCore.Tests/Mockups/LimitedMemoryPool.cs index ac5c23c..c2f4050 100644 --- a/Tests/MQTTnet.AspNetCore.Tests/Mockups/LimitedMemoryPool.cs +++ b/Source/MQTTnet.AspNetCore.Tests/Mockups/LimitedMemoryPool.cs @@ -1,4 +1,8 @@ -using System.Buffers; +// 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.Buffers; namespace MQTTnet.AspNetCore.Tests.Mockups { diff --git a/Tests/MQTTnet.AspNetCore.Tests/Mockups/MemoryOwner.cs b/Source/MQTTnet.AspNetCore.Tests/Mockups/MemoryOwner.cs similarity index 74% rename from Tests/MQTTnet.AspNetCore.Tests/Mockups/MemoryOwner.cs rename to Source/MQTTnet.AspNetCore.Tests/Mockups/MemoryOwner.cs index 1b7b02f..55e4e31 100644 --- a/Tests/MQTTnet.AspNetCore.Tests/Mockups/MemoryOwner.cs +++ b/Source/MQTTnet.AspNetCore.Tests/Mockups/MemoryOwner.cs @@ -1,4 +1,8 @@ -using System; +// 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.Buffers; namespace MQTTnet.AspNetCore.Tests.Mockups diff --git a/Tests/MQTTnet.AspNetCore.Tests/MqttConnectionContextTest.cs b/Source/MQTTnet.AspNetCore.Tests/MqttConnectionContextTest.cs similarity index 91% rename from Tests/MQTTnet.AspNetCore.Tests/MqttConnectionContextTest.cs rename to Source/MQTTnet.AspNetCore.Tests/MqttConnectionContextTest.cs index 710f226..190ff48 100644 --- a/Tests/MQTTnet.AspNetCore.Tests/MqttConnectionContextTest.cs +++ b/Source/MQTTnet.AspNetCore.Tests/MqttConnectionContextTest.cs @@ -1,21 +1,23 @@ -using Microsoft.AspNetCore.Builder; +// 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 Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; -using System.Linq; +using System.Net; using System.Threading; using System.Threading.Tasks; using MQTTnet.Adapter; using MQTTnet.AspNetCore.Tests.Mockups; -using MQTTnet.Client.Options; using MQTTnet.Exceptions; using MQTTnet.Formatter; using MQTTnet.Packets; -using System.Net; -using MQTTnet.AspNetCore.Extensions; +using MQTTnet.Client; using MQTTnet.Protocol; using MQTTnet.Tests.Extensions; @@ -27,7 +29,7 @@ namespace MQTTnet.AspNetCore.Tests [TestMethod] public async Task TestReceivePacketAsyncThrowsWhenReaderCompleted() { - var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311); + var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311, new MqttBufferWriter(4096, 65535)); var pipe = new DuplexPipeMockup(); var connection = new DefaultConnectionContext(); connection.Transport = pipe; @@ -41,7 +43,7 @@ namespace MQTTnet.AspNetCore.Tests [TestMethod] public async Task TestCorruptedConnectPacket() { - var writer = new MqttPacketWriter(); + var writer = new MqttBufferWriter(4096, 65535); var serializer = new MqttPacketFormatterAdapter(writer); var pipe = new DuplexPipeMockup(); var connection = new DefaultConnectionContext(); @@ -80,7 +82,7 @@ namespace MQTTnet.AspNetCore.Tests [TestMethod] public async Task TestLargePacket() { - var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311); + var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311, new MqttBufferWriter(4096, 65535)); var pipe = new DuplexPipeMockup(); var connection = new DefaultConnectionContext(); connection.Transport = pipe; @@ -114,6 +116,7 @@ namespace MQTTnet.AspNetCore.Tests services.AddSingleton(mockup); }) .Build()) + using (var client = new MqttFactory().CreateMqttClient()) { host.Start(); diff --git a/Tests/MQTTnet.AspNetCore.Tests/ReaderExtensionsTest.cs b/Source/MQTTnet.AspNetCore.Tests/ReaderExtensionsTest.cs similarity index 60% rename from Tests/MQTTnet.AspNetCore.Tests/ReaderExtensionsTest.cs rename to Source/MQTTnet.AspNetCore.Tests/ReaderExtensionsTest.cs index a98e98e..056a6ab 100644 --- a/Tests/MQTTnet.AspNetCore.Tests/ReaderExtensionsTest.cs +++ b/Source/MQTTnet.AspNetCore.Tests/ReaderExtensionsTest.cs @@ -1,7 +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. + #if NETCOREAPP3_1 using System.Buffers; using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.AspNetCore.Extensions; +using MQTTnet.AspNetCore; using MQTTnet.Formatter; using MQTTnet.Packets; @@ -13,35 +17,33 @@ namespace MQTTnet.AspNetCore.Tests [TestMethod] public void TestTryDeserialize() { - var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311); + var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311, new MqttBufferWriter(4096, 65535)); - var buffer = serializer.Encode(new MqttPublishPacket() {Topic = "a", Payload = new byte[5]}); + var buffer = serializer.Encode(new MqttPublishPacket {Topic = "a", Payload = new byte[5]}).Join(); var sequence = new ReadOnlySequence(buffer.Array, buffer.Offset, buffer.Count); var part = sequence; - MqttBasePacket packet; + MqttPacket packet; var consumed = part.Start; var observed = part.Start; var result = false; var read = 0; - - var reader = new SpanBasedMqttPacketBodyReader(); - + part = sequence.Slice(sequence.Start, 0); // empty message should fail - result = serializer.TryDecode(reader, part, out packet, out consumed, out observed, out read); + result = serializer.TryDecode(part, out packet, out consumed, out observed, out read); Assert.IsFalse(result); part = sequence.Slice(sequence.Start, 1); // partial fixed header should fail - result = serializer.TryDecode(reader, part, out packet, out consumed, out observed, out read); + result = serializer.TryDecode(part, out packet, out consumed, out observed, out read); Assert.IsFalse(result); part = sequence.Slice(sequence.Start, 4); // partial body should fail - result = serializer.TryDecode(reader, part, out packet, out consumed, out observed, out read); + result = serializer.TryDecode(part, out packet, out consumed, out observed, out read); Assert.IsFalse(result); part = sequence; // complete msg should work - result = serializer.TryDecode(reader, part, out packet, out consumed, out observed, out read); + result = serializer.TryDecode(part, out packet, out consumed, out observed, out read); Assert.IsTrue(result); } } diff --git a/Source/MQTTnet.AspTestApp/MQTTnet.AspTestApp.csproj b/Source/MQTTnet.AspTestApp/MQTTnet.AspTestApp.csproj new file mode 100644 index 0000000..973f5d7 --- /dev/null +++ b/Source/MQTTnet.AspTestApp/MQTTnet.AspTestApp.csproj @@ -0,0 +1,24 @@ + + + + net6.0 + enable + enable + false + true + false + false + + + + + + + + + + + + + + diff --git a/Source/MQTTnet.AspTestApp/Pages/Index.cshtml b/Source/MQTTnet.AspTestApp/Pages/Index.cshtml new file mode 100644 index 0000000..88d6dbe --- /dev/null +++ b/Source/MQTTnet.AspTestApp/Pages/Index.cshtml @@ -0,0 +1,34 @@ +@page +@model IndexModel +@{ + ViewData["Title"] = "Home page"; +} + +
+

MQTTnet ASP.NET Core Test App

+
+ + + + diff --git a/Source/MQTTnet.AspTestApp/Pages/Index.cshtml.cs b/Source/MQTTnet.AspTestApp/Pages/Index.cshtml.cs new file mode 100644 index 0000000..558db0f --- /dev/null +++ b/Source/MQTTnet.AspTestApp/Pages/Index.cshtml.cs @@ -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 Microsoft.AspNetCore.Mvc.RazorPages; + +namespace MQTTnet.AspTestApp.Pages +{ + public class IndexModel : PageModel + { + private readonly ILogger _logger; + + public IndexModel(ILogger logger) + { + _logger = logger; + } + + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.AspTestApp/Pages/Shared/_Layout.cshtml b/Source/MQTTnet.AspTestApp/Pages/Shared/_Layout.cshtml new file mode 100644 index 0000000..4546984 --- /dev/null +++ b/Source/MQTTnet.AspTestApp/Pages/Shared/_Layout.cshtml @@ -0,0 +1,14 @@ + + + + + + @ViewData["Title"] - MQTTnet + + + + @RenderBody() + + @await RenderSectionAsync("Scripts", required: false) + + \ No newline at end of file diff --git a/Source/MQTTnet.AspTestApp/Pages/_ViewImports.cshtml b/Source/MQTTnet.AspTestApp/Pages/_ViewImports.cshtml new file mode 100644 index 0000000..c8fbf31 --- /dev/null +++ b/Source/MQTTnet.AspTestApp/Pages/_ViewImports.cshtml @@ -0,0 +1,2 @@ +@namespace MQTTnet.AspTestApp.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/Source/MQTTnet.AspTestApp/Pages/_ViewStart.cshtml b/Source/MQTTnet.AspTestApp/Pages/_ViewStart.cshtml new file mode 100644 index 0000000..a5f1004 --- /dev/null +++ b/Source/MQTTnet.AspTestApp/Pages/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/Source/MQTTnet.AspTestApp/Program.cs b/Source/MQTTnet.AspTestApp/Program.cs new file mode 100644 index 0000000..18a9ef3 --- /dev/null +++ b/Source/MQTTnet.AspTestApp/Program.cs @@ -0,0 +1,73 @@ +// 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; +using MQTTnet.AspNetCore; +using MQTTnet.Server; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddRazorPages(); + +// Setup MQTT stuff. +builder.Services.AddMqttServer(); +builder.Services.AddConnections(); + +var app = builder.Build(); + +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error"); +} + +app.UseStaticFiles(); + +app.UseRouting(); + +app.UseAuthorization(); + +app.MapRazorPages(); + +// Setup MQTT stuff. +app.UseEndpoints(endpoints => +{ + endpoints.MapMqtt("/mqtt"); +}); + +app.UseMqttServer(server => +{ + server.StartedAsync += args => + { + _ = Task.Run(async () => + { + var mqttApplicationMessage = new MqttApplicationMessageBuilder() + .WithPayload($"Test application message from MQTTnet server.") + .WithTopic("message") + .Build(); + + while (true) + { + try + { + await server.InjectApplicationMessage(new InjectedMqttApplicationMessage(mqttApplicationMessage) + { + SenderClientId = "server" + }); + } + catch (Exception e) + { + Console.WriteLine(e); + } + finally + { + await Task.Delay(TimeSpan.FromSeconds(5)); + } + } + }); + + return Task.CompletedTask; + }; +}); + +app.Run(); diff --git a/Source/MQTTnet.AspTestApp/appsettings.Development.json b/Source/MQTTnet.AspTestApp/appsettings.Development.json new file mode 100644 index 0000000..770d3e9 --- /dev/null +++ b/Source/MQTTnet.AspTestApp/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Source/MQTTnet.AspTestApp/appsettings.json b/Source/MQTTnet.AspTestApp/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/Source/MQTTnet.AspTestApp/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Source/MQTTnet.AspTestApp/libman.json b/Source/MQTTnet.AspTestApp/libman.json new file mode 100644 index 0000000..ceee271 --- /dev/null +++ b/Source/MQTTnet.AspTestApp/libman.json @@ -0,0 +1,5 @@ +{ + "version": "1.0", + "defaultProvider": "cdnjs", + "libraries": [] +} \ No newline at end of file diff --git a/Source/MQTTnet.AspnetCore/Extensions/ApplicationBuilderExtensions.cs b/Source/MQTTnet.AspnetCore/ApplicationBuilderExtensions.cs similarity index 84% rename from Source/MQTTnet.AspnetCore/Extensions/ApplicationBuilderExtensions.cs rename to Source/MQTTnet.AspnetCore/ApplicationBuilderExtensions.cs index d9e7ed8..d7bc3e3 100644 --- a/Source/MQTTnet.AspnetCore/Extensions/ApplicationBuilderExtensions.cs +++ b/Source/MQTTnet.AspnetCore/ApplicationBuilderExtensions.cs @@ -1,9 +1,13 @@ -using System; +// 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 Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using MQTTnet.Server; -namespace MQTTnet.AspNetCore.Extensions +namespace MQTTnet.AspNetCore { public static class ApplicationBuilderExtensions { @@ -36,9 +40,9 @@ namespace MQTTnet.AspNetCore.Extensions return app; } - public static IApplicationBuilder UseMqttServer(this IApplicationBuilder app, Action configure) + public static IApplicationBuilder UseMqttServer(this IApplicationBuilder app, Action configure) { - var server = app.ApplicationServices.GetRequiredService(); + var server = app.ApplicationServices.GetRequiredService(); configure(server); diff --git a/Source/MQTTnet.AspnetCore/AspNetMqttServerOptionsBuilder.cs b/Source/MQTTnet.AspnetCore/AspNetMqttServerOptionsBuilder.cs index e0ae333..a95e001 100644 --- a/Source/MQTTnet.AspnetCore/AspNetMqttServerOptionsBuilder.cs +++ b/Source/MQTTnet.AspnetCore/AspNetMqttServerOptionsBuilder.cs @@ -1,13 +1,17 @@ -using MQTTnet.Server; +// 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.Server; using System; namespace MQTTnet.AspNetCore { - public class AspNetMqttServerOptionsBuilder : MqttServerOptionsBuilder + public sealed class AspNetMqttServerOptionsBuilder : MqttServerOptionsBuilder { public AspNetMqttServerOptionsBuilder(IServiceProvider serviceProvider) { - ServiceProvider = serviceProvider; + ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); } public IServiceProvider ServiceProvider { get; } diff --git a/Source/MQTTnet.AspnetCore/Client/MqttClientConnectionContextFactory.cs b/Source/MQTTnet.AspnetCore/Client/MqttClientConnectionContextFactory.cs index 9358133..f8e38f4 100644 --- a/Source/MQTTnet.AspnetCore/Client/MqttClientConnectionContextFactory.cs +++ b/Source/MQTTnet.AspnetCore/Client/MqttClientConnectionContextFactory.cs @@ -1,15 +1,20 @@ -using MQTTnet.Adapter; +// 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.AspNetCore.Client.Tcp; -using MQTTnet.Client.Options; using MQTTnet.Formatter; using System; using System.Net; +using MQTTnet.Client; +using MQTTnet.Diagnostics; namespace MQTTnet.AspNetCore.Client { - public class MqttClientConnectionContextFactory : IMqttClientAdapterFactory + public sealed class MqttClientConnectionContextFactory : IMqttClientAdapterFactory { - public IMqttChannelAdapter CreateClientAdapter(IMqttClientOptions options) + public IMqttChannelAdapter CreateClientAdapter(MqttClientOptions options, MqttPacketInspector packetInspector, IMqttNetLogger logger) { if (options == null) throw new ArgumentNullException(nameof(options)); @@ -19,9 +24,8 @@ namespace MQTTnet.AspNetCore.Client { var endpoint = new DnsEndPoint(tcpOptions.Server, tcpOptions.GetPort()); var tcpConnection = new TcpConnection(endpoint); - - var writer = new SpanBasedMqttPacketWriter(); - var formatter = new MqttPacketFormatterAdapter(options.ProtocolVersion, writer); + + var formatter = new MqttPacketFormatterAdapter(options.ProtocolVersion, new MqttBufferWriter(4096, 65535)); return new MqttConnectionContext(formatter, tcpConnection); } default: diff --git a/Source/MQTTnet.AspnetCore/Client/Tcp/BufferExtensions.cs b/Source/MQTTnet.AspnetCore/Client/Tcp/BufferExtensions.cs index 5911a3a..6273659 100644 --- a/Source/MQTTnet.AspnetCore/Client/Tcp/BufferExtensions.cs +++ b/Source/MQTTnet.AspnetCore/Client/Tcp/BufferExtensions.cs @@ -1,4 +1,8 @@ -using System; +// 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.Runtime.InteropServices; namespace MQTTnet.AspNetCore.Client.Tcp diff --git a/Source/MQTTnet.AspnetCore/Client/Tcp/DuplexPipe.cs b/Source/MQTTnet.AspnetCore/Client/Tcp/DuplexPipe.cs index e234da5..84bbf99 100644 --- a/Source/MQTTnet.AspnetCore/Client/Tcp/DuplexPipe.cs +++ b/Source/MQTTnet.AspnetCore/Client/Tcp/DuplexPipe.cs @@ -1,4 +1,8 @@ -using System.IO.Pipelines; +// 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.IO.Pipelines; namespace MQTTnet.AspNetCore.Client.Tcp { diff --git a/Source/MQTTnet.AspnetCore/Client/Tcp/SocketAwaitable.cs b/Source/MQTTnet.AspnetCore/Client/Tcp/SocketAwaitable.cs index dbc2612..6a53a7a 100644 --- a/Source/MQTTnet.AspnetCore/Client/Tcp/SocketAwaitable.cs +++ b/Source/MQTTnet.AspnetCore/Client/Tcp/SocketAwaitable.cs @@ -1,4 +1,8 @@ -using System; +// 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.Diagnostics; using System.IO.Pipelines; using System.Net.Sockets; diff --git a/Source/MQTTnet.AspnetCore/Client/Tcp/SocketReceiver.cs b/Source/MQTTnet.AspnetCore/Client/Tcp/SocketReceiver.cs index 7d11fa2..a10708e 100644 --- a/Source/MQTTnet.AspnetCore/Client/Tcp/SocketReceiver.cs +++ b/Source/MQTTnet.AspnetCore/Client/Tcp/SocketReceiver.cs @@ -1,4 +1,8 @@ -using System; +// 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.IO.Pipelines; using System.Net.Sockets; diff --git a/Source/MQTTnet.AspnetCore/Client/Tcp/SocketSender.cs b/Source/MQTTnet.AspnetCore/Client/Tcp/SocketSender.cs index f5bb0b4..d4a5dcc 100644 --- a/Source/MQTTnet.AspnetCore/Client/Tcp/SocketSender.cs +++ b/Source/MQTTnet.AspnetCore/Client/Tcp/SocketSender.cs @@ -1,4 +1,8 @@ -using System; +// 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.Buffers; using System.Collections.Generic; using System.Diagnostics; diff --git a/Source/MQTTnet.AspnetCore/Client/Tcp/TcpConnection.cs b/Source/MQTTnet.AspnetCore/Client/Tcp/TcpConnection.cs index 7814567..845a67d 100644 --- a/Source/MQTTnet.AspnetCore/Client/Tcp/TcpConnection.cs +++ b/Source/MQTTnet.AspnetCore/Client/Tcp/TcpConnection.cs @@ -1,4 +1,8 @@ -using Microsoft.AspNetCore.Connections; +// 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 Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Features; using MQTTnet.Exceptions; using System; @@ -40,7 +44,7 @@ namespace MQTTnet.AspNetCore.Client.Tcp _sender = new SocketSender(_socket, PipeScheduler.ThreadPool); _receiver = new SocketReceiver(_socket, PipeScheduler.ThreadPool); } -#if NETCOREAPP3_1 || NET5_0 +#if NETCOREAPP3_1 || NET5_0_OR_GREATER public override ValueTask DisposeAsync() #else public Task DisposeAsync() @@ -53,7 +57,7 @@ namespace MQTTnet.AspNetCore.Client.Tcp _socket?.Dispose(); -#if NETCOREAPP3_1 || NET5_0 +#if NETCOREAPP3_1 || NET5_0_OR_GREATER return base.DisposeAsync(); } diff --git a/Source/MQTTnet.AspnetCore/ConnectionBuilderExtensions.cs b/Source/MQTTnet.AspnetCore/ConnectionBuilderExtensions.cs new file mode 100644 index 0000000..9ea8922 --- /dev/null +++ b/Source/MQTTnet.AspnetCore/ConnectionBuilderExtensions.cs @@ -0,0 +1,16 @@ +// 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 Microsoft.AspNetCore.Connections; + +namespace MQTTnet.AspNetCore +{ + public static class ConnectionBuilderExtensions + { + public static IConnectionBuilder UseMqtt(this IConnectionBuilder builder) + { + return builder.UseConnectionHandler(); + } + } +} diff --git a/Source/MQTTnet.AspnetCore/Extensions/ConnectionRouteBuilderExtensions.cs b/Source/MQTTnet.AspnetCore/ConnectionRouteBuilderExtensions.cs similarity index 73% rename from Source/MQTTnet.AspnetCore/Extensions/ConnectionRouteBuilderExtensions.cs rename to Source/MQTTnet.AspnetCore/ConnectionRouteBuilderExtensions.cs index 0595b98..cbbe9f3 100644 --- a/Source/MQTTnet.AspnetCore/Extensions/ConnectionRouteBuilderExtensions.cs +++ b/Source/MQTTnet.AspnetCore/ConnectionRouteBuilderExtensions.cs @@ -1,11 +1,15 @@ -using Microsoft.AspNetCore.Http; +// 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 Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Connections; #if NETCOREAPP3_1 using System; #endif -namespace MQTTnet.AspNetCore.Extensions +namespace MQTTnet.AspNetCore { public static class ConnectionRouteBuilderExtensions { diff --git a/Source/MQTTnet.AspnetCore/Extensions/EndpointRouterExtensions.cs b/Source/MQTTnet.AspnetCore/EndpointRouterExtensions.cs similarity index 67% rename from Source/MQTTnet.AspnetCore/Extensions/EndpointRouterExtensions.cs rename to Source/MQTTnet.AspnetCore/EndpointRouterExtensions.cs index 7cbed8c..f5c7241 100644 --- a/Source/MQTTnet.AspnetCore/Extensions/EndpointRouterExtensions.cs +++ b/Source/MQTTnet.AspnetCore/EndpointRouterExtensions.cs @@ -1,5 +1,9 @@ - -#if NETCOREAPP3_1 || NET5_0 +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +#if NETCOREAPP3_1 || NET5_0_OR_GREATER using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; diff --git a/Source/MQTTnet.AspnetCore/Extensions/ConnectionBuilderExtensions.cs b/Source/MQTTnet.AspnetCore/Extensions/ConnectionBuilderExtensions.cs deleted file mode 100644 index f62f870..0000000 --- a/Source/MQTTnet.AspnetCore/Extensions/ConnectionBuilderExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.AspNetCore.Connections; - -namespace MQTTnet.AspNetCore.Extensions -{ - public static class ConnectionBuilderExtensions - { - public static IConnectionBuilder UseMqtt(this IConnectionBuilder builder) - { - return builder.UseConnectionHandler(); - } - } -} diff --git a/Source/MQTTnet.AspnetCore/MQTTnet.AspNetCore.csproj b/Source/MQTTnet.AspnetCore/MQTTnet.AspNetCore.csproj index f2a891d..94ccb9f 100644 --- a/Source/MQTTnet.AspnetCore/MQTTnet.AspNetCore.csproj +++ b/Source/MQTTnet.AspnetCore/MQTTnet.AspNetCore.csproj @@ -1,36 +1,71 @@  - - netstandard2.0;netcoreapp2.1;netcoreapp3.1;net5.0 - - - - - 7.2 - true - true - true - snupkg - - - - - - - - RELEASE;NETSTANDARD2_0 - - - - - - - - - - - - - + + netstandard2.0;netcoreapp2.1;netcoreapp3.1;net5.0;net6.0 + + MQTTnet.AspNetCore + MQTTnet.AspNetCore + True + The contributors of MQTTnet + MQTTnet + MQTTnet is a high performance .NET library for MQTT based communication. It provides a MQTT client and a MQTT server (broker) and supports v3.1.0, v3.1.1 and v5.0.0 of the MQTT protocol. + The contributors of MQTTnet + MQTTnet.AspNetCore + false + false + true + true + snupkg + Christian Kratky 2016-2022 + https://github.com/dotnet/MQTTnet + https://github.com/dotnet/MQTTnet.git + git + 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 + en-US + false + false + nuget.png + true + true + LICENSE + For release notes please go to MQTTnet release notes (https://www.nuget.org/packages/MQTTnet/). + true + + true + + + + + True + \ + + + + + + True + \ + + + + + + + + + RELEASE;NETSTANDARD2_0 + + + + + + + + + + + + + diff --git a/Source/MQTTnet.AspnetCore/MqttConnectionContext.cs b/Source/MQTTnet.AspnetCore/MqttConnectionContext.cs index e8a62e4..1268d25 100644 --- a/Source/MQTTnet.AspnetCore/MqttConnectionContext.cs +++ b/Source/MQTTnet.AspnetCore/MqttConnectionContext.cs @@ -1,4 +1,8 @@ -using Microsoft.AspNetCore.Connections; +// 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 Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Connections.Features; using MQTTnet.Adapter; using MQTTnet.AspNetCore.Client.Tcp; @@ -10,7 +14,6 @@ using System.IO.Pipelines; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; -using MQTTnet.AspNetCore.Extensions; using MQTTnet.Internal; namespace MQTTnet.AspNetCore @@ -18,7 +21,6 @@ namespace MQTTnet.AspNetCore public sealed class MqttConnectionContext : IMqttChannelAdapter { readonly AsyncLock _writerLock = new AsyncLock(); - readonly SpanBasedMqttPacketBodyReader _reader; PipeReader _input; PipeWriter _output; @@ -33,8 +35,6 @@ namespace MQTTnet.AspNetCore _input = Connection.Transport.Input; _output = Connection.Transport.Output; } - - _reader = new SpanBasedMqttPacketBodyReader(); } public string Endpoint @@ -73,7 +73,7 @@ namespace MQTTnet.AspNetCore IHttpContextFeature Http => Connection.Features.Get(); - public async Task ConnectAsync(TimeSpan timeout, CancellationToken cancellationToken) + public async Task ConnectAsync(CancellationToken cancellationToken) { if (Connection is TcpConnection tcp && !tcp.IsConnected) { @@ -84,7 +84,7 @@ namespace MQTTnet.AspNetCore _output = Connection.Transport.Output; } - public Task DisconnectAsync(TimeSpan timeout, CancellationToken cancellationToken) + public Task DisconnectAsync(CancellationToken cancellationToken) { _input?.Complete(); _output?.Complete(); @@ -92,7 +92,7 @@ namespace MQTTnet.AspNetCore return Task.CompletedTask; } - public async Task ReceivePacketAsync(CancellationToken cancellationToken) + public async Task ReceivePacketAsync(CancellationToken cancellationToken) { var input = Connection.Transport.Input; @@ -120,7 +120,7 @@ namespace MQTTnet.AspNetCore { if (!buffer.IsEmpty) { - if (PacketFormatterAdapter.TryDecode(_reader, buffer, out var packet, out consumed, out observed, out var received)) + if (PacketFormatterAdapter.TryDecode(buffer, out var packet, out consumed, out observed, out var received)) { BytesReceived += received; return packet; @@ -167,13 +167,14 @@ namespace MQTTnet.AspNetCore BytesSent = 0; } - public async Task SendPacketAsync(MqttBasePacket packet, CancellationToken cancellationToken) + public async Task SendPacketAsync(MqttPacket packet, CancellationToken cancellationToken) { var formatter = PacketFormatterAdapter; + using (await _writerLock.WaitAsync(cancellationToken).ConfigureAwait(false)) { var buffer = formatter.Encode(packet); - var msg = buffer.AsMemory(); + var msg = buffer.Join().AsMemory(); var output = _output; var result = await output.WriteAsync(msg, cancellationToken).ConfigureAwait(false); if (result.IsCompleted) @@ -181,7 +182,7 @@ namespace MQTTnet.AspNetCore BytesSent += msg.Length; } - PacketFormatterAdapter.FreeBuffer(); + PacketFormatterAdapter.Cleanup(); } } diff --git a/Source/MQTTnet.AspnetCore/MqttConnectionHandler.cs b/Source/MQTTnet.AspnetCore/MqttConnectionHandler.cs index 8cc1022..b86de30 100644 --- a/Source/MQTTnet.AspnetCore/MqttConnectionHandler.cs +++ b/Source/MQTTnet.AspnetCore/MqttConnectionHandler.cs @@ -1,15 +1,22 @@ -using Microsoft.AspNetCore.Connections; +// 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 Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; using MQTTnet.Adapter; using MQTTnet.Server; using System; using System.Threading.Tasks; +using MQTTnet.Diagnostics; using MQTTnet.Formatter; namespace MQTTnet.AspNetCore { - public class MqttConnectionHandler : ConnectionHandler, IMqttServerAdapter + public sealed class MqttConnectionHandler : ConnectionHandler, IMqttServerAdapter { + MqttServerOptions _serverOptions; + public Func ClientHandler { get; set; } public override async Task OnConnectedAsync(ConnectionContext connection) @@ -20,9 +27,8 @@ namespace MQTTnet.AspNetCore { transferFormatFeature.ActiveFormat = TransferFormat.Binary; } - - var writer = new SpanBasedMqttPacketWriter(); - var formatter = new MqttPacketFormatterAdapter(writer); + + var formatter = new MqttPacketFormatterAdapter(new MqttBufferWriter(_serverOptions.WriterBufferSize, _serverOptions.WriterBufferSizeMax)); using (var adapter = new MqttConnectionContext(formatter, connection)) { var clientHandler = ClientHandler; @@ -33,8 +39,10 @@ namespace MQTTnet.AspNetCore } } - public Task StartAsync(IMqttServerOptions options) + public Task StartAsync(MqttServerOptions options, IMqttNetLogger logger) { + _serverOptions = options; + return Task.CompletedTask; } diff --git a/Source/MQTTnet.AspnetCore/MqttHostedServer.cs b/Source/MQTTnet.AspnetCore/MqttHostedServer.cs index 3a9c882..81d0eee 100644 --- a/Source/MQTTnet.AspnetCore/MqttHostedServer.cs +++ b/Source/MQTTnet.AspnetCore/MqttHostedServer.cs @@ -1,27 +1,27 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using MQTTnet.Adapter; -using MQTTnet.Diagnostics.Logger; +using MQTTnet.Diagnostics; using MQTTnet.Server; namespace MQTTnet.AspNetCore { public sealed class MqttHostedServer : MqttServer, IHostedService { - readonly IMqttServerOptions _options; - - public MqttHostedServer(IMqttServerOptions options, IEnumerable adapters, IMqttNetLogger logger) - : base(adapters, logger) + public MqttHostedServer(MqttServerOptions options, IEnumerable adapters, IMqttNetLogger logger) + : base(options, adapters, logger) { - _options = options ?? throw new ArgumentNullException(nameof(options)); } public Task StartAsync(CancellationToken cancellationToken) { - _ = StartAsync(_options); + _ = StartAsync(); return Task.CompletedTask; } @@ -30,4 +30,4 @@ namespace MQTTnet.AspNetCore return StopAsync(); } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.AspnetCore/MqttSubProtocolSelector.cs b/Source/MQTTnet.AspnetCore/MqttSubProtocolSelector.cs index 6386b4f..6f318b1 100644 --- a/Source/MQTTnet.AspnetCore/MqttSubProtocolSelector.cs +++ b/Source/MQTTnet.AspnetCore/MqttSubProtocolSelector.cs @@ -1,4 +1,8 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Http; diff --git a/Source/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs b/Source/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs index 270305d..a83eb02 100644 --- a/Source/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs +++ b/Source/MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs @@ -1,4 +1,8 @@ -using Microsoft.AspNetCore.Http; +// 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 Microsoft.AspNetCore.Http; using MQTTnet.Adapter; using MQTTnet.Formatter; using MQTTnet.Implementations; @@ -6,23 +10,19 @@ using MQTTnet.Server; using System; using System.Net.WebSockets; using System.Threading.Tasks; -using MQTTnet.Diagnostics.Logger; +using MQTTnet.Diagnostics; namespace MQTTnet.AspNetCore { public sealed class MqttWebSocketServerAdapter : IMqttServerAdapter { - readonly IMqttNetLogger _rootLogger; - - public MqttWebSocketServerAdapter(IMqttNetLogger logger) - { - _rootLogger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - + IMqttNetLogger _logger = new MqttNetNullLogger(); + public Func ClientHandler { get; set; } - public Task StartAsync(IMqttServerOptions options) + public Task StartAsync(MqttServerOptions options, IMqttNetLogger logger) { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); return Task.CompletedTask; } @@ -45,11 +45,10 @@ namespace MQTTnet.AspNetCore var clientHandler = ClientHandler; if (clientHandler != null) { - var writer = new SpanBasedMqttPacketWriter(); - var formatter = new MqttPacketFormatterAdapter(writer); + var formatter = new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)); var channel = new MqttWebSocketChannel(webSocket, endpoint, isSecureConnection, clientCertificate); - using (var channelAdapter = new MqttChannelAdapter(channel, formatter, null, _rootLogger)) + using (var channelAdapter = new MqttChannelAdapter(channel, formatter, null, _logger)) { await clientHandler(channelAdapter).ConfigureAwait(false); } diff --git a/Source/MQTTnet.AspnetCore/Extensions/ReaderExtensions.cs b/Source/MQTTnet.AspnetCore/ReaderExtensions.cs similarity index 80% rename from Source/MQTTnet.AspnetCore/Extensions/ReaderExtensions.cs rename to Source/MQTTnet.AspnetCore/ReaderExtensions.cs index 0560bea..d3b00ab 100644 --- a/Source/MQTTnet.AspnetCore/Extensions/ReaderExtensions.cs +++ b/Source/MQTTnet.AspnetCore/ReaderExtensions.cs @@ -1,18 +1,21 @@ -using System; +// 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.Buffers; using MQTTnet.Adapter; using MQTTnet.Exceptions; using MQTTnet.Formatter; using MQTTnet.Packets; -namespace MQTTnet.AspNetCore.Extensions +namespace MQTTnet.AspNetCore { public static class ReaderExtensions { public static bool TryDecode(this MqttPacketFormatterAdapter formatter, - SpanBasedMqttPacketBodyReader reader, in ReadOnlySequence input, - out MqttBasePacket packet, + out MqttPacket packet, out SequencePosition consumed, out SequencePosition observed, out int bytesRead) @@ -30,7 +33,7 @@ namespace MQTTnet.AspNetCore.Extensions return false; } - var fixedheader = copy.First.Span[0]; + var fixedHeader = copy.First.Span[0]; if (!TryReadBodyLength(ref copy, out int headerLength, out var bodyLength)) { return false; @@ -42,10 +45,9 @@ namespace MQTTnet.AspNetCore.Extensions } var bodySlice = copy.Slice(0, bodyLength); - var buffer = bodySlice.GetMemory(); - reader.SetBuffer(buffer); - - var receivedMqttPacket = new ReceivedMqttPacket(fixedheader, reader, buffer.Length + 2); + var buffer = bodySlice.GetMemory().ToArray(); + + var receivedMqttPacket = new ReceivedMqttPacket(fixedHeader, new ArraySegment(buffer, 0, buffer.Length), buffer.Length + 2); if (formatter.ProtocolVersion == MqttProtocolVersion.Unknown) { @@ -59,7 +61,7 @@ namespace MQTTnet.AspNetCore.Extensions return true; } - private static ReadOnlyMemory GetMemory(this in ReadOnlySequence input) + static ReadOnlyMemory GetMemory(this in ReadOnlySequence input) { if (input.IsSingleSegment) { @@ -70,7 +72,7 @@ namespace MQTTnet.AspNetCore.Extensions return input.ToArray(); } - private static bool TryReadBodyLength(ref ReadOnlySequence input, out int headerLength, out int bodyLength) + static bool TryReadBodyLength(ref ReadOnlySequence input, out int headerLength, out int bodyLength) { // Alorithm taken from https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/errata01/os/mqtt-v3.1.1-errata01-os-complete.html. var multiplier = 1; @@ -88,6 +90,7 @@ namespace MQTTnet.AspNetCore.Extensions { return false; } + encodedByte = temp[index]; index++; diff --git a/Source/MQTTnet.AspnetCore/Extensions/ServiceCollectionExtensions.cs b/Source/MQTTnet.AspnetCore/ServiceCollectionExtensions.cs similarity index 66% rename from Source/MQTTnet.AspnetCore/Extensions/ServiceCollectionExtensions.cs rename to Source/MQTTnet.AspnetCore/ServiceCollectionExtensions.cs index 683fe1c..8933bca 100644 --- a/Source/MQTTnet.AspnetCore/Extensions/ServiceCollectionExtensions.cs +++ b/Source/MQTTnet.AspnetCore/ServiceCollectionExtensions.cs @@ -1,17 +1,33 @@ -using System; +// 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 Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using MQTTnet.Adapter; using MQTTnet.Diagnostics; -using MQTTnet.Diagnostics.Logger; using MQTTnet.Implementations; using MQTTnet.Server; -namespace MQTTnet.AspNetCore.Extensions +namespace MQTTnet.AspNetCore { public static class ServiceCollectionExtensions { - public static IServiceCollection AddHostedMqttServer(this IServiceCollection services, IMqttServerOptions options) + public static IServiceCollection AddMqttServer(this IServiceCollection serviceCollection, Action configure = null) + { + if (serviceCollection is null) + { + throw new ArgumentNullException(nameof(serviceCollection)); + } + + serviceCollection.AddMqttConnectionHandler(); + serviceCollection.AddHostedMqttServer(configure); + + return serviceCollection; + } + + public static IServiceCollection AddHostedMqttServer(this IServiceCollection services, MqttServerOptions options) { if (options == null) throw new ArgumentNullException(nameof(options)); @@ -22,13 +38,13 @@ namespace MQTTnet.AspNetCore.Extensions return services; } - public static IServiceCollection AddHostedMqttServer(this IServiceCollection services, Action configure) + public static IServiceCollection AddHostedMqttServer(this IServiceCollection services, Action configure = null) { - services.AddSingleton(s => + services.AddSingleton(s => { - var builder = new MqttServerOptionsBuilder(); - configure(builder); - return builder.Build(); + var serverOptionsBuilder = new MqttServerOptionsBuilder(); + configure?.Invoke(serverOptionsBuilder); + return serverOptionsBuilder.Build(); }); services.AddHostedMqttServer(); @@ -38,7 +54,7 @@ namespace MQTTnet.AspNetCore.Extensions public static IServiceCollection AddHostedMqttServerWithServices(this IServiceCollection services, Action configure) { - services.AddSingleton(s => + services.AddSingleton(s => { var builder = new AspNetMqttServerOptionsBuilder(s); configure(builder); @@ -50,24 +66,14 @@ namespace MQTTnet.AspNetCore.Extensions return services; } - public static IServiceCollection AddHostedMqttServer(this IServiceCollection services) - where TOptions : class, IMqttServerOptions - { - services.AddSingleton(); - - services.AddHostedMqttServer(); - - return services; - } - - private static IServiceCollection AddHostedMqttServer(this IServiceCollection services) + static IServiceCollection AddHostedMqttServer(this IServiceCollection services) { var logger = new MqttNetEventLogger(); services.AddSingleton(logger); services.AddSingleton(); services.AddSingleton(s => s.GetService()); - services.AddSingleton(s => s.GetService()); + services.AddSingleton(s => s.GetService()); return services; } diff --git a/Source/MQTTnet.AspnetCore/SpanBasedMqttPacketBodyReader.cs b/Source/MQTTnet.AspnetCore/SpanBasedMqttPacketBodyReader.cs deleted file mode 100644 index 2f921db..0000000 --- a/Source/MQTTnet.AspnetCore/SpanBasedMqttPacketBodyReader.cs +++ /dev/null @@ -1,129 +0,0 @@ -using MQTTnet.Exceptions; -using MQTTnet.Formatter; -using System; -using System.Buffers.Binary; -using System.Text; - -namespace MQTTnet.AspNetCore -{ - public class SpanBasedMqttPacketBodyReader : IMqttPacketBodyReader - { - ReadOnlyMemory _buffer; - - int _offset; - - public void SetBuffer(ReadOnlyMemory buffer) - { - _buffer = buffer; - _offset = 0; - } - - public int Length => _buffer.Length; - - public bool EndOfStream => _buffer.Length.Equals(_offset); - - public int Offset => _offset; - - public byte ReadByte() - { - return _buffer.Span[_offset++]; - } - - public byte[] ReadRemainingData() - { - return _buffer.Slice(_offset).ToArray(); - } - - public byte[] ReadWithLengthPrefix() - { - return ReadSegmentWithLengthPrefix().ToArray(); - } - - public unsafe string ReadStringWithLengthPrefix() - { - var buffer = ReadSegmentWithLengthPrefix(); - if (buffer.Length == 0) - { - return string.Empty; - } - - fixed (byte* bytes = &buffer.GetPinnableReference()) - { - var result = Encoding.UTF8.GetString(bytes, buffer.Length); - return result; - } - } - - public ushort ReadTwoByteInteger() - { - var result = BinaryPrimitives.ReadUInt16BigEndian(_buffer.Span.Slice(_offset)); - _offset += 2; - return result; - } - - public uint ReadFourByteInteger() - { - var result = BinaryPrimitives.ReadUInt32BigEndian(_buffer.Span.Slice(_offset)); - _offset += 4; - return result; - } - - public uint ReadVariableLengthInteger() - { - var multiplier = 1; - var value = 0U; - byte encodedByte; - - do - { - encodedByte = ReadByte(); - value += (uint)((encodedByte & 127) * multiplier); - - if (multiplier > 2097152) - { - throw new MqttProtocolViolationException("Variable length integer is invalid."); - } - - multiplier *= 128; - } while ((encodedByte & 128) != 0); - - return value; - } - - public bool ReadBoolean() - { - var buffer = ReadByte(); - - if (buffer == 0) - { - return false; - } - - if (buffer == 1) - { - return true; - } - - throw new MqttProtocolViolationException("Boolean values can be 0 or 1 only."); - } - - public void Seek(int position) - { - _offset = position; - } - - ReadOnlySpan ReadSegmentWithLengthPrefix() - { - var span = _buffer.Span; - var length = BinaryPrimitives.ReadUInt16BigEndian(span.Slice(_offset)); - - if (Length < _offset + length) - { - throw new MqttProtocolViolationException($"Expected at least {_offset + 2 + length} bytes but there are only {Length} bytes"); - } - var result = span.Slice(_offset + 2, length); - _offset += 2 + length; - return result; - } - } -} diff --git a/Source/MQTTnet.AspnetCore/SpanBasedMqttPacketWriter.cs b/Source/MQTTnet.AspnetCore/SpanBasedMqttPacketWriter.cs deleted file mode 100644 index 62f678b..0000000 --- a/Source/MQTTnet.AspnetCore/SpanBasedMqttPacketWriter.cs +++ /dev/null @@ -1,144 +0,0 @@ -using MQTTnet.Formatter; -using System; -using System.Buffers; -using System.Buffers.Binary; -using System.Text; - -namespace MQTTnet.AspNetCore -{ - public class SpanBasedMqttPacketWriter : IMqttPacketWriter - { - readonly ArrayPool _pool = ArrayPool.Create(); - - public SpanBasedMqttPacketWriter() - { - Reset(0); - } - - byte[] _buffer; - int _position; - - public int Length { get; set; } - - public void FreeBuffer() - { - _pool.Return(_buffer); - } - - public byte[] GetBuffer() - { - return _buffer; - } - - public void Reset(int v) - { - _buffer = _pool.Rent(1500); - Length = v; - _position = v; - } - - public void Seek(int v) - { - _position = v; - } - - public void Write(byte value) - { - GrowIfNeeded(1); - _buffer[_position] = value; - Commit(1); - } - - public void Write(ushort value) - { - GrowIfNeeded(2); - - BinaryPrimitives.WriteUInt16BigEndian(_buffer.AsSpan(_position), value); - Commit(2); - } - - public void Write(IMqttPacketWriter propertyWriter) - { - if (propertyWriter == null) throw new ArgumentNullException(nameof(propertyWriter)); - - GrowIfNeeded(propertyWriter.Length); - Write(propertyWriter.GetBuffer(), 0, propertyWriter.Length); - } - - public void Write(byte[] payload, int start, int length) - { - GrowIfNeeded(length); - - payload.AsSpan(start, length).CopyTo(_buffer.AsSpan(_position)); - Commit(length); - } - - public void WriteVariableLengthInteger(uint value) - { - GrowIfNeeded(4); - - var x = value; - do - { - var encodedByte = x % 128; - x = x / 128; - if (x > 0) - { - encodedByte = encodedByte | 128; - } - - _buffer[_position] = (byte)encodedByte; - Commit(1); - } while (x > 0); - } - - public void WriteWithLengthPrefix(string value) - { - var bytesLength = Encoding.UTF8.GetByteCount(value ?? string.Empty); - GrowIfNeeded(bytesLength + 2); - - Write((ushort)bytesLength); - Encoding.UTF8.GetBytes(value ?? string.Empty, 0, value?.Length ?? 0, _buffer, _position); - Commit(bytesLength); - } - - public void WriteWithLengthPrefix(byte[] payload) - { - GrowIfNeeded(payload.Length + 2); - - Write((ushort)payload.Length); - payload.CopyTo(_buffer, _position); - Commit(payload.Length); - } - - void Commit(int count) - { - if (_position == Length) - { - Length += count; - } - - _position += count; - } - - void GrowIfNeeded(int requiredAdditional) - { - var requiredTotal = _position + requiredAdditional; - if (_buffer.Length >= requiredTotal) - { - return; - } - - var newBufferLength = _buffer.Length; - while (newBufferLength < requiredTotal) - { - newBufferLength *= 2; - } - - var newBuffer = _pool.Rent(newBufferLength); - Array.Copy(_buffer, newBuffer, _buffer.Length); - _pool.Return(_buffer); - _buffer = newBuffer; - } - } -} diff --git a/Source/MQTTnet.Benchmarks/BaseBenchmark.cs b/Source/MQTTnet.Benchmarks/BaseBenchmark.cs new file mode 100644 index 0000000..a33ad7a --- /dev/null +++ b/Source/MQTTnet.Benchmarks/BaseBenchmark.cs @@ -0,0 +1,6 @@ +namespace MQTTnet.Benchmarks +{ + public abstract class BaseBenchmark + { + } +} \ No newline at end of file diff --git a/Tests/MQTTnet.Benchmarks/ChannelAdapterBenchmark.cs b/Source/MQTTnet.Benchmarks/ChannelAdapterBenchmark.cs similarity index 86% rename from Tests/MQTTnet.Benchmarks/ChannelAdapterBenchmark.cs rename to Source/MQTTnet.Benchmarks/ChannelAdapterBenchmark.cs index 34a05a9..96c6419 100644 --- a/Tests/MQTTnet.Benchmarks/ChannelAdapterBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/ChannelAdapterBenchmark.cs @@ -1,11 +1,15 @@ -using BenchmarkDotNet.Attributes; +// 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 BenchmarkDotNet.Attributes; using MQTTnet.Adapter; using MQTTnet.Internal; using MQTTnet.Packets; using System; using System.IO; using System.Threading; -using MQTTnet.Diagnostics.Logger; +using MQTTnet.Diagnostics; using MQTTnet.Formatter; namespace MQTTnet.Benchmarks @@ -26,9 +30,9 @@ namespace MQTTnet.Benchmarks Topic = "A" }; - var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311); + var serializer = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311, new MqttBufferWriter(4096, 65535)); - var serializedPacket = Join(serializer.Encode(_packet)); + var serializedPacket = Join(serializer.Encode(_packet).Join()); _iterations = 10000; diff --git a/Tests/MQTTnet.Benchmarks/Configurations/AllowNonOptimized.cs b/Source/MQTTnet.Benchmarks/Configurations/AllowNonOptimized.cs similarity index 69% rename from Tests/MQTTnet.Benchmarks/Configurations/AllowNonOptimized.cs rename to Source/MQTTnet.Benchmarks/Configurations/AllowNonOptimized.cs index c40eb09..2ae1e19 100644 --- a/Tests/MQTTnet.Benchmarks/Configurations/AllowNonOptimized.cs +++ b/Source/MQTTnet.Benchmarks/Configurations/AllowNonOptimized.cs @@ -1,4 +1,8 @@ -using BenchmarkDotNet.Jobs; +// 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 BenchmarkDotNet.Jobs; using BenchmarkDotNet.Validators; namespace MQTTnet.Benchmarks.Configurations diff --git a/Tests/MQTTnet.Benchmarks/Configurations/BaseConfig.cs b/Source/MQTTnet.Benchmarks/Configurations/BaseConfig.cs similarity index 69% rename from Tests/MQTTnet.Benchmarks/Configurations/BaseConfig.cs rename to Source/MQTTnet.Benchmarks/Configurations/BaseConfig.cs index ff5b148..15324a6 100644 --- a/Tests/MQTTnet.Benchmarks/Configurations/BaseConfig.cs +++ b/Source/MQTTnet.Benchmarks/Configurations/BaseConfig.cs @@ -1,4 +1,8 @@ -using BenchmarkDotNet.Configs; +// 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 BenchmarkDotNet.Configs; using System.Linq; namespace MQTTnet.Benchmarks.Configurations diff --git a/Source/MQTTnet.Benchmarks/Configurations/RuntimeCompareConfig.cs b/Source/MQTTnet.Benchmarks/Configurations/RuntimeCompareConfig.cs new file mode 100644 index 0000000..cd01b72 --- /dev/null +++ b/Source/MQTTnet.Benchmarks/Configurations/RuntimeCompareConfig.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Toolchains.CsProj; + +namespace MQTTnet.Benchmarks.Configurations +{ + public class RuntimeCompareConfig : BaseConfig + { + public RuntimeCompareConfig() + { + AddJob(Job.Default.WithRuntime(ClrRuntime.Net48)); + AddJob(Job.Default.WithRuntime(CoreRuntime.Core50).WithToolchain(CsProjCoreToolchain.NetCoreApp50)); + } + } +} diff --git a/Tests/MQTTnet.Benchmarks/LoggerBenchmark.cs b/Source/MQTTnet.Benchmarks/LoggerBenchmark.cs similarity index 88% rename from Tests/MQTTnet.Benchmarks/LoggerBenchmark.cs rename to Source/MQTTnet.Benchmarks/LoggerBenchmark.cs index f31f89e..f7814e8 100644 --- a/Tests/MQTTnet.Benchmarks/LoggerBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/LoggerBenchmark.cs @@ -1,10 +1,14 @@ -using BenchmarkDotNet.Attributes; +// 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 BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; -using MQTTnet.Diagnostics.Logger; +using MQTTnet.Diagnostics; namespace MQTTnet.Benchmarks { - [SimpleJob(RuntimeMoniker.Net461)] + [SimpleJob(RuntimeMoniker.NetCoreApp50)] [RPlotExporter] [MemoryDiagnoser] public class LoggerBenchmark diff --git a/Source/MQTTnet.Benchmarks/MQTTnet.Benchmarks.csproj b/Source/MQTTnet.Benchmarks/MQTTnet.Benchmarks.csproj new file mode 100644 index 0000000..10a38c1 --- /dev/null +++ b/Source/MQTTnet.Benchmarks/MQTTnet.Benchmarks.csproj @@ -0,0 +1,39 @@ + + + + Exe + Full + net5.0;net6.0 + 7.2 + false + false + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Source/MQTTnet.Benchmarks/MessageDeliveryBenchmark.cs b/Source/MQTTnet.Benchmarks/MessageDeliveryBenchmark.cs new file mode 100644 index 0000000..e904e02 --- /dev/null +++ b/Source/MQTTnet.Benchmarks/MessageDeliveryBenchmark.cs @@ -0,0 +1,234 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using MQTTnet.Client; +using MQTTnet.Server; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + + +namespace MQTTnet.Benchmarks +{ + /// + /// Create a number of topics, publish, subscribe, and wait for response + /// + [MemoryDiagnoser] + public class MessageDeliveryBenchmark + { + List _topicPublishMessages; + + [Params(1, 5)] + public int NumTopicsPerPublisher; + + [Params(1000, 10000)] + public int NumPublishers; + + [Params(10)] + public int NumSubscribers; + + [Params(5, 10, 20, 50)] + public int NumSubscribedTopicsPerSubscriber; + + object _lockMsgCount; + int _messagesReceivedCount; + int _messagesExpectedCount; + CancellationTokenSource _cancellationTokenSource; + + MqttServer _mqttServer; + List _mqttSubscriberClients; + Dictionary _mqttPublisherClientsByPublisherName; + + Dictionary> _topicsByPublisher; + Dictionary _publisherByTopic; + List _allSubscribedTopics; // Keep track of the subset of topics that are subscribed + + + [GlobalSetup] + public void Setup() + { + _lockMsgCount = new object(); + + Dictionary> singleWildcardTopicsByPublisher; + Dictionary> multiWildcardTopicsByPublisher; + + TopicGenerator.Generate(NumPublishers, NumTopicsPerPublisher, out _topicsByPublisher, out singleWildcardTopicsByPublisher, out multiWildcardTopicsByPublisher); + + var topics = _topicsByPublisher.First().Value; + _topicPublishMessages = new List(); + // Prepare messages, same for each publisher + foreach (var topic in topics) + { + var message = new MqttApplicationMessageBuilder() + .WithTopic(topic) + .WithPayload(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }) + .Build(); + _topicPublishMessages.Add(message); + } + + // Create server + var factory = new MqttFactory(); + var serverOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build(); + _mqttServer = factory.CreateMqttServer(serverOptions); + _mqttServer.StartAsync().GetAwaiter().GetResult(); + + // Create publisher clients + _mqttPublisherClientsByPublisherName = new Dictionary(); + foreach (var pt in _topicsByPublisher) + { + var publisherName = pt.Key; + var mqttClient = factory.CreateMqttClient(); + var publisherOptions = new MqttClientOptionsBuilder() + .WithTcpServer("localhost") + .WithClientId(publisherName) + .WithKeepAlivePeriod(TimeSpan.FromSeconds(30)) + .Build(); + mqttClient.ConnectAsync(publisherOptions).GetAwaiter().GetResult(); + _mqttPublisherClientsByPublisherName.Add(publisherName, mqttClient); + } + + // Create subscriber clients + _mqttSubscriberClients = new List(); + for (var i = 0; i < NumSubscribers; i++) + { + var mqttSubscriberClient = factory.CreateMqttClient(); + _mqttSubscriberClients.Add(mqttSubscriberClient); + + var subscriberOptions = new MqttClientOptionsBuilder() + .WithTcpServer("localhost") + .WithClientId("subscriber" + i) + .Build(); + mqttSubscriberClient.ApplicationMessageReceivedAsync += r => + { + // count messages and signal cancellation when expected message count is reached + lock (_lockMsgCount) + { + ++_messagesReceivedCount; + if (_messagesReceivedCount == _messagesExpectedCount) + { + _cancellationTokenSource.Cancel(); + } + } + return Task.CompletedTask; + }; + mqttSubscriberClient.ConnectAsync(subscriberOptions).GetAwaiter().GetResult(); + } + + + List allTopics = new List(); + _publisherByTopic = new Dictionary(); + foreach (var t in _topicsByPublisher) + { + foreach (var topic in t.Value) + { + _publisherByTopic.Add(topic, t.Key); + allTopics.Add(topic); + } + } + + // Subscribe to NumSubscribedTopics topics spread across all topics + _allSubscribedTopics = new List(); + + var totalNumTopics = NumPublishers * NumTopicsPerPublisher; + int topicIndexStep = totalNumTopics / (NumSubscribedTopicsPerSubscriber * NumSubscribers); + if (topicIndexStep * NumSubscribedTopicsPerSubscriber * NumSubscribers != totalNumTopics) + { + throw new System.Exception( + String.Format("The total number of topics must be divisible by the number of subscribed topics across all subscribers. Total number of topics: {0}, topic step: {1}", + totalNumTopics, topicIndexStep + )); + } + + var topicIndex = 0; + foreach (var mqttSubscriber in _mqttSubscriberClients) + { + for (var i = 0; i < NumSubscribedTopicsPerSubscriber; ++i, topicIndex += topicIndexStep) + { + var topic = allTopics[topicIndex]; + _allSubscribedTopics.Add(topic); + var subOptions = new Client.MqttClientSubscribeOptionsBuilder().WithTopicFilter( + new Packets.MqttTopicFilter() { Topic = topic }) + .Build(); + mqttSubscriber.SubscribeAsync(subOptions).GetAwaiter().GetResult(); + } + } + + Task.Delay(1000).GetAwaiter().GetResult(); + } + + /// + /// Publish messages and wait for messages sent to subscribers + /// + [Benchmark] + public void DeliverMessages() + { + // There should be one message received per publish for each subscribed topic + _messagesExpectedCount = NumSubscribedTopicsPerSubscriber * NumSubscribers; + + // Loop for a while and exchange messages + + _messagesReceivedCount = 0; + + _cancellationTokenSource = new CancellationTokenSource(); + + // same payload for all messages + var payload = new byte[] { 1, 2, 3, 4 }; + + var tasks = new List(); + + // publish a message for each subscribed topic + foreach (var topic in _allSubscribedTopics) + { + var message = new MqttApplicationMessageBuilder() + .WithTopic(topic) + .WithPayload(payload) + .Build(); + // pick the correct publisher + var publisherName = _publisherByTopic[topic]; + var publisherClient = _mqttPublisherClientsByPublisherName[publisherName]; + _ = publisherClient.PublishAsync(message); + } + + // Wait one message per publish to be received by subscriber (in the subscriber's application message handler) + try + { + Task.Delay(30000, _cancellationTokenSource.Token).GetAwaiter().GetResult(); + } + catch + { + + } + + _cancellationTokenSource.Dispose(); + + if (_messagesReceivedCount < _messagesExpectedCount) + { + throw new Exception(string.Format("Messages Received Count mismatch, expected {0}, received {1}", _messagesExpectedCount, _messagesReceivedCount)); + } + } + + [GlobalCleanup] + public void Cleanup() + { + foreach (var mp in _mqttPublisherClientsByPublisherName) + { + var mqttPublisherClient = mp.Value; + mqttPublisherClient.DisconnectAsync().GetAwaiter().GetResult(); + mqttPublisherClient.Dispose(); + } + _mqttPublisherClientsByPublisherName.Clear(); + + foreach (var mqttSubscriber in _mqttSubscriberClients) + { + mqttSubscriber.DisconnectAsync().GetAwaiter().GetResult(); + mqttSubscriber.Dispose(); + } + _mqttSubscriberClients.Clear(); + + _mqttServer.StopAsync().GetAwaiter().GetResult(); + _mqttServer.Dispose(); + _mqttServer = null; + } + } +} diff --git a/Tests/MQTTnet.Benchmarks/MessageProcessingBenchmark.cs b/Source/MQTTnet.Benchmarks/MessageProcessingBenchmark.cs similarity index 66% rename from Tests/MQTTnet.Benchmarks/MessageProcessingBenchmark.cs rename to Source/MQTTnet.Benchmarks/MessageProcessingBenchmark.cs index 91718f1..175e6a2 100644 --- a/Tests/MQTTnet.Benchmarks/MessageProcessingBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/MessageProcessingBenchmark.cs @@ -1,29 +1,34 @@ -using BenchmarkDotNet.Attributes; +// 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 BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; using MQTTnet.Client; -using MQTTnet.Client.Options; using MQTTnet.Server; +using MqttClient = MQTTnet.Client.MqttClient; namespace MQTTnet.Benchmarks { - [SimpleJob(RuntimeMoniker.Net461)] + [SimpleJob(RuntimeMoniker.NetCoreApp50)] [RPlotExporter, RankColumn] [MemoryDiagnoser] public class MessageProcessingBenchmark { - IMqttServer _mqttServer; - IMqttClient _mqttClient; + MqttServer _mqttServer; + MqttClient _mqttClient; MqttApplicationMessage _message; [GlobalSetup] public void Setup() { + var serverOptions = new MqttServerOptionsBuilder().Build(); + var factory = new MqttFactory(); - _mqttServer = factory.CreateMqttServer(); + _mqttServer = factory.CreateMqttServer(serverOptions); _mqttClient = factory.CreateMqttClient(); - var serverOptions = new MqttServerOptionsBuilder().Build(); - _mqttServer.StartAsync(serverOptions).GetAwaiter().GetResult(); + _mqttServer.StartAsync().GetAwaiter().GetResult(); var clientOptions = new MqttClientOptionsBuilder() .WithTcpServer("localhost").Build(); diff --git a/Tests/MQTTnet.Benchmarks/MessageProcessingMqttConnectionContextBenchmark.cs b/Source/MQTTnet.Benchmarks/MessageProcessingMqttConnectionContextBenchmark.cs similarity index 83% rename from Tests/MQTTnet.Benchmarks/MessageProcessingMqttConnectionContextBenchmark.cs rename to Source/MQTTnet.Benchmarks/MessageProcessingMqttConnectionContextBenchmark.cs index 651f400..6b65a9a 100644 --- a/Tests/MQTTnet.Benchmarks/MessageProcessingMqttConnectionContextBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/MessageProcessingMqttConnectionContextBenchmark.cs @@ -1,19 +1,24 @@ -using BenchmarkDotNet.Attributes; +// 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 BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; using MQTTnet.Client; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; using MQTTnet.AspNetCore.Client; -using MQTTnet.AspNetCore.Extensions; -using MQTTnet.Client.Options; -using MQTTnet.Diagnostics.Logger; +using MQTTnet.AspNetCore; +using MQTTnet.Diagnostics; namespace MQTTnet.Benchmarks { + [SimpleJob(RuntimeMoniker.NetCoreApp50)] [MemoryDiagnoser] public class MessageProcessingMqttConnectionContextBenchmark { IWebHost _host; - IMqttClient _mqttClient; + MqttClient _mqttClient; MqttApplicationMessage _message; [GlobalSetup] diff --git a/Source/MQTTnet.Benchmarks/MqttPacketReaderWriterBenchmark.cs b/Source/MQTTnet.Benchmarks/MqttPacketReaderWriterBenchmark.cs new file mode 100644 index 0000000..abe0ab0 --- /dev/null +++ b/Source/MQTTnet.Benchmarks/MqttPacketReaderWriterBenchmark.cs @@ -0,0 +1,93 @@ +using System; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using MQTTnet.AspNetCore; +using MQTTnet.Formatter; +using MQTTnet.Tests.Mockups; + +namespace MQTTnet.Benchmarks +{ + [SimpleJob(RuntimeMoniker.NetCoreApp50)] + [MemoryDiagnoser] + public class MqttPacketReaderWriterBenchmark + { + readonly byte[] _demoPayload = new byte[1024]; + + byte[] _readPayload; + + [GlobalCleanup] + public void GlobalCleanup() + { + } + + [GlobalSetup] + public void GlobalSetup() + { + TestEnvironment.EnableLogger = false; + + var writer = new MqttBufferWriter(4096, 65535); + writer.WriteString("A relative short string."); + writer.WriteBinaryData(_demoPayload); + writer.WriteByte(0x01); + writer.WriteByte(0x02); + writer.WriteVariableByteInteger(5647382); + writer.WriteString("A relative short string."); + writer.WriteVariableByteInteger(8574489); + writer.WriteBinaryData(_demoPayload); + writer.WriteByte(2); + writer.WriteByte(0x02); + writer.WriteString("fjgffiogfhgfhoihgoireghreghreguhreguireoghreouighreouighreughreguiorehreuiohruiorehreuioghreug"); + writer.WriteBinaryData(_demoPayload); + + _readPayload = new ArraySegment(writer.GetBuffer(), 0, writer.Length).ToArray(); + } + + [Benchmark] + public void Read_100_000_Messages() + { + var reader = new MqttBufferReader(); + reader.SetBuffer(_readPayload, 0, _readPayload.Length); + + for (var i = 0; i < 100000; i++) + { + reader.Seek(0); + + reader.ReadString(); + reader.ReadBinaryData(); + reader.ReadByte(); + reader.ReadByte(); + reader.ReadVariableByteInteger(); + reader.ReadString(); + reader.ReadVariableByteInteger(); + reader.ReadBinaryData(); + reader.ReadByte(); + reader.ReadByte(); + reader.ReadString(); + reader.ReadBinaryData(); + } + } + + [Benchmark] + public void Write_100_000_Messages() + { + var writer = new MqttBufferWriter(4096, 65535); + + for (var i = 0; i < 100000; i++) + { + writer.WriteString("A relative short string."); + writer.WriteByte(0x01); + writer.WriteByte(0x02); + writer.WriteVariableByteInteger(5647382); + writer.WriteString("A relative short string."); + writer.WriteVariableByteInteger(8574589); + writer.WriteBinaryData(_demoPayload); + writer.WriteByte(2); + writer.WriteByte(0x02); + writer.WriteString("fjgffiogfhgfhoihgoireghreghreguhreguireoghreouighreouighreughreguiorehreuiohruiorehreuioghreug"); + writer.WriteBinaryData(_demoPayload); + + writer.Reset(0); + } + } + } +} \ No newline at end of file diff --git a/Tests/MQTTnet.Benchmarks/MqttTcpChannelBenchmark.cs b/Source/MQTTnet.Benchmarks/MqttTcpChannelBenchmark.cs similarity index 77% rename from Tests/MQTTnet.Benchmarks/MqttTcpChannelBenchmark.cs rename to Source/MQTTnet.Benchmarks/MqttTcpChannelBenchmark.cs index 32ca9e7..5a84403 100644 --- a/Tests/MQTTnet.Benchmarks/MqttTcpChannelBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/MqttTcpChannelBenchmark.cs @@ -1,18 +1,24 @@ -using BenchmarkDotNet.Attributes; +// 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 BenchmarkDotNet.Attributes; using MQTTnet.Channel; -using MQTTnet.Client.Options; using MQTTnet.Implementations; using MQTTnet.Server; using System.Threading; using System.Threading.Tasks; -using MQTTnet.Diagnostics.Logger; +using BenchmarkDotNet.Jobs; +using MQTTnet.Client; +using MQTTnet.Diagnostics; namespace MQTTnet.Benchmarks { + [SimpleJob(RuntimeMoniker.NetCoreApp50)] [MemoryDiagnoser] public sealed class MqttTcpChannelBenchmark { - IMqttServer _mqttServer; + MqttServer _mqttServer; IMqttChannel _serverChannel; IMqttChannel _clientChannel; @@ -20,7 +26,7 @@ namespace MQTTnet.Benchmarks public void Setup() { var factory = new MqttFactory(); - var tcpServer = new MqttTcpServerAdapter(new MqttNetEventLogger()); + var tcpServer = new MqttTcpServerAdapter(); tcpServer.ClientHandler += args => { _serverChannel = @@ -31,10 +37,11 @@ namespace MQTTnet.Benchmarks return Task.CompletedTask; }; - _mqttServer = factory.CreateMqttServer(new[] { tcpServer }, new MqttNetEventLogger()); - var serverOptions = new MqttServerOptionsBuilder().Build(); - _mqttServer.StartAsync(serverOptions).GetAwaiter().GetResult(); + _mqttServer = factory.CreateMqttServer(serverOptions, new[] { tcpServer }, new MqttNetEventLogger()); + + + _mqttServer.StartAsync().GetAwaiter().GetResult(); var clientOptions = new MqttClientOptionsBuilder() .WithTcpServer("localhost").Build(); diff --git a/Tests/MQTTnet.Benchmarks/Program.cs b/Source/MQTTnet.Benchmarks/Program.cs similarity index 53% rename from Tests/MQTTnet.Benchmarks/Program.cs rename to Source/MQTTnet.Benchmarks/Program.cs index b2d1919..13921d8 100644 --- a/Tests/MQTTnet.Benchmarks/Program.cs +++ b/Source/MQTTnet.Benchmarks/Program.cs @@ -1,7 +1,11 @@ -using System; +// 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 BenchmarkDotNet.Running; using MQTTnet.Benchmarks.Configurations; -using MQTTnet.Diagnostics.Runtime; +using MQTTnet.Diagnostics; namespace MQTTnet.Benchmarks { @@ -9,7 +13,8 @@ namespace MQTTnet.Benchmarks { public static void Main(string[] args) { - Console.WriteLine($"MQTTnet - BenchmarkApp.{TargetFrameworkProvider.TargetFramework}"); + Console.WriteLine($"MQTTnet - Benchmarks ({TargetFrameworkProvider.TargetFramework})"); + Console.WriteLine("--------------------------------------------------------"); Console.WriteLine("1 = MessageProcessingBenchmark"); Console.WriteLine("2 = SerializerBenchmark"); Console.WriteLine("3 = LoggerBenchmark"); @@ -18,6 +23,12 @@ namespace MQTTnet.Benchmarks Console.WriteLine("6 = MqttTcpChannelBenchmark"); Console.WriteLine("7 = TcpPipesBenchmark"); Console.WriteLine("8 = MessageProcessingMqttConnectionContextBenchmark"); + Console.WriteLine("9 = ServerProcessingBenchmark"); + Console.WriteLine("a = MqttPacketReaderWriterBenchmark"); + Console.WriteLine("b = RoundtripBenchmark"); + Console.WriteLine("c = SubscribeBenchmark"); + Console.WriteLine("d = UnsubscribeBenchmark"); + Console.WriteLine("e = MessageDeliveryBenchmark"); var pressedKey = Console.ReadKey(true); switch (pressedKey.KeyChar) @@ -44,7 +55,25 @@ namespace MQTTnet.Benchmarks BenchmarkRunner.Run(); break; case '8': - BenchmarkRunner.Run(new RuntimeCompareConfig()/*new AllowNonOptimized()*/); + BenchmarkRunner.Run(new RuntimeCompareConfig()); + break; + case '9': + BenchmarkRunner.Run(); + break; + case 'a': + BenchmarkRunner.Run(typeof(MqttPacketReaderWriterBenchmark)); + break; + case 'b': + BenchmarkRunner.Run(); + break; + case 'c': + BenchmarkRunner.Run(); + break; + case 'd': + BenchmarkRunner.Run(); + break; + case 'e': + BenchmarkRunner.Run(); break; } diff --git a/Source/MQTTnet.Benchmarks/RoundtripProcessingBenchmark.cs b/Source/MQTTnet.Benchmarks/RoundtripProcessingBenchmark.cs new file mode 100644 index 0000000..c944ac5 --- /dev/null +++ b/Source/MQTTnet.Benchmarks/RoundtripProcessingBenchmark.cs @@ -0,0 +1,30 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using MQTTnet.Tests.Mockups; +using MQTTnet.Tests.Server; + +namespace MQTTnet.Benchmarks +{ + [SimpleJob(RuntimeMoniker.NetCoreApp50)] + [RPlotExporter, RankColumn] + [MemoryDiagnoser] + public class RoundtripProcessingBenchmark + { + [GlobalSetup] + public void GlobalSetup() + { + TestEnvironment.EnableLogger = false; + } + + [GlobalCleanup] + public void GlobalCleanup() + { + } + + [Benchmark] + public void Handle_100_000_Messages_In_Receiving_Client() + { + new Load_Tests().Handle_100_000_Messages_In_Receiving_Client().GetAwaiter().GetResult(); + } + } +} \ No newline at end of file diff --git a/Tests/MQTTnet.Benchmarks/SerializerBenchmark.cs b/Source/MQTTnet.Benchmarks/SerializerBenchmark.cs similarity index 76% rename from Tests/MQTTnet.Benchmarks/SerializerBenchmark.cs rename to Source/MQTTnet.Benchmarks/SerializerBenchmark.cs index e2b2bcd..9bb94c4 100644 --- a/Tests/MQTTnet.Benchmarks/SerializerBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/SerializerBenchmark.cs @@ -1,4 +1,8 @@ -using BenchmarkDotNet.Attributes; +// 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 BenchmarkDotNet.Attributes; using MQTTnet.Packets; using System; using System.Security.Cryptography.X509Certificates; @@ -9,29 +13,31 @@ using MQTTnet.Channel; using MQTTnet.Formatter; using MQTTnet.Formatter.V3; using BenchmarkDotNet.Jobs; -using MQTTnet.Diagnostics.Logger; +using MQTTnet.Diagnostics; namespace MQTTnet.Benchmarks { - [SimpleJob(RuntimeMoniker.Net461)] + [SimpleJob(RuntimeMoniker.NetCoreApp50)] [RPlotExporter] [MemoryDiagnoser] - public class SerializerBenchmark + public class SerializerBenchmark : BaseBenchmark { - MqttBasePacket _packet; + MqttPacket _packet; ArraySegment _serializedPacket; IMqttPacketFormatter _serializer; + MqttBufferWriter _bufferWriter; [GlobalSetup] - public void Setup() + public void GlobalSetup() { _packet = new MqttPublishPacket { Topic = "A" }; - _serializer = new MqttV311PacketFormatter(new MqttPacketWriter()); - _serializedPacket = _serializer.Encode(_packet); + _bufferWriter = new MqttBufferWriter(4096, 65535); + _serializer = new MqttV3PacketFormatter(_bufferWriter, MqttProtocolVersion.V311); + _serializedPacket = _serializer.Encode(_packet).Join(); } [Benchmark] @@ -40,7 +46,7 @@ namespace MQTTnet.Benchmarks for (var i = 0; i < 10000; i++) { _serializer.Encode(_packet); - _serializer.FreeBuffer(); + _bufferWriter.Cleanup(); } } @@ -48,8 +54,7 @@ namespace MQTTnet.Benchmarks public void Deserialize_10000_Messages() { var channel = new BenchmarkMqttChannel(_serializedPacket); - var fixedHeader = new byte[2]; - var reader = new MqttChannelAdapter(channel, new MqttPacketFormatterAdapter(new MqttPacketWriter()), null, new MqttNetEventLogger()); + var reader = new MqttChannelAdapter(channel, new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)), null, new MqttNetEventLogger()); for (var i = 0; i < 10000; i++) { diff --git a/Source/MQTTnet.Benchmarks/ServerProcessingBenchmark.cs b/Source/MQTTnet.Benchmarks/ServerProcessingBenchmark.cs new file mode 100644 index 0000000..453a7ac --- /dev/null +++ b/Source/MQTTnet.Benchmarks/ServerProcessingBenchmark.cs @@ -0,0 +1,36 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using MQTTnet.Tests.Mockups; +using MQTTnet.Tests.Server; + +namespace MQTTnet.Benchmarks +{ + [SimpleJob(RuntimeMoniker.NetCoreApp50)] + [RPlotExporter, RankColumn] + [MemoryDiagnoser] + public class ServerProcessingBenchmark + { + [GlobalSetup] + public void GlobalSetup() + { + TestEnvironment.EnableLogger = false; + } + + [GlobalCleanup] + public void GlobalCleanup() + { + } + + [Benchmark] + public void Handle_100_000_Messages_In_Server_MqttClient() + { + new Load_Tests().Handle_100_000_Messages_In_Server().GetAwaiter().GetResult(); + } + + //[Benchmark] + public void Handle_100_000_Messages_In_Server_LowLevelMqttClient() + { + new Load_Tests().Handle_100_000_Messages_In_Low_Level_Client().GetAwaiter().GetResult(); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Benchmarks/SubscribeBenchmark.cs b/Source/MQTTnet.Benchmarks/SubscribeBenchmark.cs new file mode 100644 index 0000000..b5acf81 --- /dev/null +++ b/Source/MQTTnet.Benchmarks/SubscribeBenchmark.cs @@ -0,0 +1,61 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using MQTTnet.Client; +using MQTTnet.Server; +using System.Collections.Generic; +using System.Linq; + +namespace MQTTnet.Benchmarks +{ + [MemoryDiagnoser] + public class SubscribeBenchmark + { + MqttServer _mqttServer; + MQTTnet.Client.MqttClient _mqttClient; + + const int NumPublishers = 1; + const int NumTopicsPerPublisher = 10000; + + List _topics; + + [GlobalSetup] + public void Setup() + { + TopicGenerator.Generate(NumPublishers, NumTopicsPerPublisher, out var topicsByPublisher, out var singleWildcardTopicsByPublisher, out var multiWildcardTopicsByPublisher); + _topics = topicsByPublisher.Values.First(); + + var serverOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build(); + + var factory = new MqttFactory(); + _mqttServer = factory.CreateMqttServer(serverOptions); + _mqttClient = factory.CreateMqttClient(); + + _mqttServer.StartAsync().GetAwaiter().GetResult(); + + var clientOptions = new MqttClientOptionsBuilder() + .WithTcpServer("localhost").Build(); + + _mqttClient.ConnectAsync(clientOptions).GetAwaiter().GetResult(); + } + + [GlobalCleanup] + public void Cleanup() + { + _mqttClient.DisconnectAsync().GetAwaiter().GetResult(); + _mqttServer.StopAsync().GetAwaiter().GetResult(); + _mqttServer.Dispose(); + } + + [Benchmark] + public void Subscribe_10000_Topics() + { + foreach (var topic in _topics) + { + var subscribeOptions = new MqttClientSubscribeOptionsBuilder() + .WithTopicFilter(topic, Protocol.MqttQualityOfServiceLevel.AtMostOnce) + .Build(); + _mqttClient.SubscribeAsync(subscribeOptions).GetAwaiter().GetResult(); + } + } + } +} diff --git a/Tests/MQTTnet.Benchmarks/TcpPipesBenchmark.cs b/Source/MQTTnet.Benchmarks/TcpPipesBenchmark.cs similarity index 87% rename from Tests/MQTTnet.Benchmarks/TcpPipesBenchmark.cs rename to Source/MQTTnet.Benchmarks/TcpPipesBenchmark.cs index 636d693..c2f419c 100644 --- a/Tests/MQTTnet.Benchmarks/TcpPipesBenchmark.cs +++ b/Source/MQTTnet.Benchmarks/TcpPipesBenchmark.cs @@ -1,13 +1,19 @@ -using System.IO.Pipelines; +// 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.IO.Pipelines; using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; using MQTTnet.AspNetCore.Client.Tcp; namespace MQTTnet.Benchmarks { + [SimpleJob(RuntimeMoniker.NetCoreApp50)] [MemoryDiagnoser] public class TcpPipesBenchmark { diff --git a/Source/MQTTnet.Benchmarks/TopicFilterComparerBenchmark.cs b/Source/MQTTnet.Benchmarks/TopicFilterComparerBenchmark.cs new file mode 100644 index 0000000..c3f95f3 --- /dev/null +++ b/Source/MQTTnet.Benchmarks/TopicFilterComparerBenchmark.cs @@ -0,0 +1,277 @@ +// 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 BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; + +namespace MQTTnet.Benchmarks +{ + [SimpleJob(RuntimeMoniker.NetCoreApp50)] + [RPlotExporter] + [MemoryDiagnoser] + public class TopicFilterComparerBenchmark + { + static readonly char[] TopicLevelSeparator = { '/' }; + + readonly string _longTopic = + "AAAAAAAAAAAAAssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssshhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; + + [Benchmark] + public void MqttTopicFilterComparer_10000_LoopMethod() + { + for (var i = 0; i < 100000; i++) + { + MqttTopicFilterComparer.Compare("sport/tennis/player1", "sport/#"); + MqttTopicFilterComparer.Compare("sport/tennis/player1/ranking", "sport/#/ranking"); + MqttTopicFilterComparer.Compare("sport/tennis/player1/score/wimbledon", "sport/+/player1/#"); + MqttTopicFilterComparer.Compare("sport/tennis/player1", "sport/tennis/+"); + MqttTopicFilterComparer.Compare("/finance", "+/+"); + MqttTopicFilterComparer.Compare("/finance", "/+"); + MqttTopicFilterComparer.Compare("/finance", "+"); + MqttTopicFilterComparer.Compare(_longTopic, _longTopic); + } + } + + [Benchmark] + public void MqttTopicFilterComparer_10000_LoopMethod_Without_Pointer() + { + for (var i = 0; i < 100000; i++) + { + MqttTopicFilterComparerWithoutPointer.Compare("sport/tennis/player1", "sport/#"); + MqttTopicFilterComparerWithoutPointer.Compare("sport/tennis/player1/ranking", "sport/#/ranking"); + MqttTopicFilterComparerWithoutPointer.Compare("sport/tennis/player1/score/wimbledon", "sport/+/player1/#"); + MqttTopicFilterComparerWithoutPointer.Compare("sport/tennis/player1", "sport/tennis/+"); + MqttTopicFilterComparerWithoutPointer.Compare("/finance", "+/+"); + MqttTopicFilterComparerWithoutPointer.Compare("/finance", "/+"); + MqttTopicFilterComparerWithoutPointer.Compare("/finance", "+"); + MqttTopicFilterComparerWithoutPointer.Compare(_longTopic, _longTopic); + } + } + + [Benchmark] + public void MqttTopicFilterComparer_10000_StringSplitMethod() + { + for (var i = 0; i < 100000; i++) + { + LegacyMethodByStringSplit("sport/tennis/player1", "sport/#"); + LegacyMethodByStringSplit("sport/tennis/player1/ranking", "sport/#/ranking"); + LegacyMethodByStringSplit("sport/tennis/player1/score/wimbledon", "sport/+/player1/#"); + LegacyMethodByStringSplit("sport/tennis/player1", "sport/tennis/+"); + LegacyMethodByStringSplit("/finance", "+/+"); + LegacyMethodByStringSplit("/finance", "/+"); + LegacyMethodByStringSplit("/finance", "+"); + MqttTopicFilterComparer.Compare(_longTopic, _longTopic); + } + } + + [GlobalSetup] + public void Setup() + { + } + + static bool LegacyMethodByStringSplit(string topic, string filter) + { + if (topic == null) + { + throw new ArgumentNullException(nameof(topic)); + } + + if (filter == null) + { + throw new ArgumentNullException(nameof(filter)); + } + + if (string.Equals(topic, filter, StringComparison.Ordinal)) + { + return true; + } + + var fragmentsTopic = topic.Split(TopicLevelSeparator, StringSplitOptions.None); + var fragmentsFilter = filter.Split(TopicLevelSeparator, StringSplitOptions.None); + + // # > In either case it MUST be the last character specified in the Topic Filter [MQTT-4.7.1-2]. + for (var i = 0; i < fragmentsFilter.Length; i++) + { + if (fragmentsFilter[i] == "+") + { + continue; + } + + if (fragmentsFilter[i] == "#") + { + return true; + } + + if (i >= fragmentsTopic.Length) + { + return false; + } + + if (!string.Equals(fragmentsFilter[i], fragmentsTopic[i], StringComparison.Ordinal)) + { + return false; + } + } + + return fragmentsTopic.Length == fragmentsFilter.Length; + } + + public static class MqttTopicFilterComparerWithoutPointer + { + public const char LevelSeparator = '/'; + public const char MultiLevelWildcard = '#'; + public const char SingleLevelWildcard = '+'; + public const char ReservedTopicPrefix = '$'; + + public static MqttTopicFilterCompareResult Compare(string topic, string filter) + { + if (string.IsNullOrEmpty(topic)) + { + return MqttTopicFilterCompareResult.TopicInvalid; + } + + if (string.IsNullOrEmpty(filter)) + { + return MqttTopicFilterCompareResult.FilterInvalid; + } + + var filterOffset = 0; + var filterLength = filter.Length; + + var topicOffset = 0; + var topicLength = topic.Length; + + var topicPointer = topic; + var filterPointer = filter; + + var isMultiLevelFilter = filterPointer[filterLength - 1] == MultiLevelWildcard; + var isReservedTopic = topicPointer[0] == ReservedTopicPrefix; + + if (isReservedTopic && filterLength == 1 && isMultiLevelFilter) + { + // It is not allowed to receive i.e. '$foo/bar' with filter '#'. + return MqttTopicFilterCompareResult.NoMatch; + } + + if (isReservedTopic && filterPointer[0] == SingleLevelWildcard) + { + // It is not allowed to receive i.e. '$SYS/monitor/Clients' with filter '+/monitor/Clients'. + return MqttTopicFilterCompareResult.NoMatch; + } + + if (filterLength == 1 && isMultiLevelFilter) + { + // Filter '#' matches basically everything. + return MqttTopicFilterCompareResult.IsMatch; + } + + // Go through the filter char by char. + while (filterOffset < filterLength && topicOffset < topicLength) + { + // Check if the current char is a multi level wildcard. The char is only allowed + // at the very las position. + if (filterPointer[filterOffset] == MultiLevelWildcard && filterOffset != filterLength - 1) + { + return MqttTopicFilterCompareResult.FilterInvalid; + } + + if (filterPointer[filterOffset] == topicPointer[topicOffset]) + { + if (topicOffset == topicLength - 1) + { + // Check for e.g. "foo" matching "foo/#" + if (filterOffset == filterLength - 3 && filterPointer[filterOffset + 1] == LevelSeparator && isMultiLevelFilter) + { + return MqttTopicFilterCompareResult.IsMatch; + } + + // Check for e.g. "foo/" matching "foo/#" + if (filterOffset == filterLength - 2 && filterPointer[filterOffset] == LevelSeparator && isMultiLevelFilter) + { + return MqttTopicFilterCompareResult.IsMatch; + } + } + + filterOffset++; + topicOffset++; + + // Check if the end was reached and i.e. "foo/bar" matches "foo/bar" + if (filterOffset == filterLength && topicOffset == topicLength) + { + return MqttTopicFilterCompareResult.IsMatch; + } + + var endOfTopic = topicOffset == topicLength; + + if (endOfTopic && filterOffset == filterLength - 1 && filterPointer[filterOffset] == SingleLevelWildcard) + { + if (filterOffset > 0 && filterPointer[filterOffset - 1] != LevelSeparator) + { + return MqttTopicFilterCompareResult.FilterInvalid; + } + + return MqttTopicFilterCompareResult.IsMatch; + } + } + else + { + if (filterPointer[filterOffset] == SingleLevelWildcard) + { + // Check for invalid "+foo" or "a/+foo" subscription + if (filterOffset > 0 && filterPointer[filterOffset - 1] != LevelSeparator) + { + return MqttTopicFilterCompareResult.FilterInvalid; + } + + // Check for bad "foo+" or "foo+/a" subscription + if (filterOffset < filterLength - 1 && filterPointer[filterOffset + 1] != LevelSeparator) + { + return MqttTopicFilterCompareResult.FilterInvalid; + } + + filterOffset++; + while (topicOffset < topicLength && topicPointer[topicOffset] != LevelSeparator) + { + topicOffset++; + } + + if (topicOffset == topicLength && filterOffset == filterLength) + { + return MqttTopicFilterCompareResult.IsMatch; + } + } + else if (filterPointer[filterOffset] == MultiLevelWildcard) + { + if (filterOffset > 0 && filterPointer[filterOffset - 1] != LevelSeparator) + { + return MqttTopicFilterCompareResult.FilterInvalid; + } + + if (filterOffset + 1 != filterLength) + { + return MqttTopicFilterCompareResult.FilterInvalid; + } + + return MqttTopicFilterCompareResult.IsMatch; + } + else + { + // Check for e.g. "foo/bar" matching "foo/+/#". + if (filterOffset > 0 && filterOffset + 2 == filterLength && topicOffset == topicLength && filterPointer[filterOffset - 1] == SingleLevelWildcard && + filterPointer[filterOffset] == LevelSeparator && isMultiLevelFilter) + { + return MqttTopicFilterCompareResult.IsMatch; + } + + return MqttTopicFilterCompareResult.NoMatch; + } + } + } + + return MqttTopicFilterCompareResult.NoMatch; + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Benchmarks/TopicGenerator.cs b/Source/MQTTnet.Benchmarks/TopicGenerator.cs new file mode 100644 index 0000000..cb17fee --- /dev/null +++ b/Source/MQTTnet.Benchmarks/TopicGenerator.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MQTTnet.Benchmarks +{ + public class TopicGenerator + { + public static void Generate( + int numPublishers, int numTopicsPerPublisher, + out Dictionary> topicsByPublisher, + out Dictionary> singleWildcardTopicsByPublisher, + out Dictionary> multiWildcardTopicsByPublisher + ) + { + topicsByPublisher = new Dictionary>(); + singleWildcardTopicsByPublisher = new Dictionary>(); + multiWildcardTopicsByPublisher = new Dictionary>(); + + // Find some reasonable distribution across three topic levels + var topicsPerLevel = (int)Math.Pow(numTopicsPerPublisher, (1.0 / 3.0)); + if (topicsPerLevel <= 0) + { + topicsPerLevel = 1; + } + + int numLevel1Topics = topicsPerLevel; + int numLevel2Topics = topicsPerLevel; + + var maxNumLevel3Topics = 1 + (int)((double)numTopicsPerPublisher / numLevel1Topics / numLevel2Topics); + if (maxNumLevel3Topics <= 0) + { + maxNumLevel3Topics = 1; + } + + for (var p = 0; p < numPublishers; ++p) + { + int publisherTopicCount = 0; + var publisherName = "pub" + p; + for (var l1 = 0; l1 < numLevel1Topics; ++l1) + { + for (var l2 = 0; l2 < numLevel2Topics; ++l2) + { + for (var l3 = 0; l3 < maxNumLevel3Topics; ++l3) + { + if (publisherTopicCount >= numTopicsPerPublisher) + break; + + var topic = string.Format("{0}/building{1}/level{2}/sensor{3}", publisherName, l1 + 1, l2 + 1, l3 + 1); + AddPublisherTopic(publisherName, topic, topicsByPublisher); + + if (l2 == 0) + { + var singleWildcardTopic = string.Format("{0}/building{1}/+/sensor{2}", publisherName, l1 + 1, l3 + 1); + AddPublisherTopic(publisherName, singleWildcardTopic, singleWildcardTopicsByPublisher); + } + if ((l1 == 0) && (l3 == 0)) + { + var multiWildcardTopic = string.Format("{0}/+/level{1}/+", publisherName, l2 + 1); + AddPublisherTopic(publisherName, multiWildcardTopic, multiWildcardTopicsByPublisher); + } + + ++publisherTopicCount; + } + } + } + } + } + + static void AddPublisherTopic(string publisherName, string topic, Dictionary> topicsByPublisher) + { + List topicList; + if (!topicsByPublisher.TryGetValue(publisherName, out topicList)) + { + topicList = new List(); + topicsByPublisher.Add(publisherName, topicList); + } + topicList.Add(topic); + } + } +} diff --git a/Source/MQTTnet.Benchmarks/UnsubscribeBenchmark.cs b/Source/MQTTnet.Benchmarks/UnsubscribeBenchmark.cs new file mode 100644 index 0000000..129d8d9 --- /dev/null +++ b/Source/MQTTnet.Benchmarks/UnsubscribeBenchmark.cs @@ -0,0 +1,61 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using MQTTnet.Client; +using MQTTnet.Server; +using System.Collections.Generic; +using System.Linq; + +namespace MQTTnet.Benchmarks +{ + [MemoryDiagnoser] + public class UnsubscribeBenchmark + { + MqttServer _mqttServer; + MQTTnet.Client.MqttClient _mqttClient; + + const int NumPublishers = 1; + const int NumTopicsPerPublisher = 10000; + + List _topics; + + [GlobalSetup] + public void Setup() + { + TopicGenerator.Generate(NumPublishers, NumTopicsPerPublisher, out var topicsByPublisher, out var singleWildcardTopicsByPublisher, out var multiWildcardTopicsByPublisher); + _topics = topicsByPublisher.Values.First(); + + var serverOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build(); + + var factory = new MqttFactory(); + _mqttServer = factory.CreateMqttServer(serverOptions); + _mqttClient = factory.CreateMqttClient(); + + _mqttServer.StartAsync().GetAwaiter().GetResult(); + + var clientOptions = new MqttClientOptionsBuilder() + .WithTcpServer("localhost").Build(); + + _mqttClient.ConnectAsync(clientOptions).GetAwaiter().GetResult(); + + foreach (var topic in _topics) + { + var subscribeOptions = new MqttClientSubscribeOptionsBuilder() + .WithTopicFilter(topic, Protocol.MqttQualityOfServiceLevel.AtMostOnce) + .Build(); + _mqttClient.SubscribeAsync(subscribeOptions).GetAwaiter().GetResult(); + } + } + + [Benchmark] + public void Unsubscribe_10000_Topics() + { + foreach (var topic in _topics) + { + var unsubscribeOptions = new MqttClientUnsubscribeOptionsBuilder() + .WithTopicFilter(topic) + .Build(); + _mqttClient.UnsubscribeAsync(unsubscribeOptions).GetAwaiter().GetResult(); + } + } + } +} diff --git a/Tests/MQTTnet.Benchmarks/packages.config b/Source/MQTTnet.Benchmarks/packages.config similarity index 100% rename from Tests/MQTTnet.Benchmarks/packages.config rename to Source/MQTTnet.Benchmarks/packages.config diff --git a/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageProcessedEventArgs.cs b/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageProcessedEventArgs.cs index 6eb97d2..2efaa49 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageProcessedEventArgs.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageProcessedEventArgs.cs @@ -1,8 +1,12 @@ -using System; +// 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.Extensions.ManagedClient { - public class ApplicationMessageProcessedEventArgs : EventArgs + public sealed class ApplicationMessageProcessedEventArgs : EventArgs { public ApplicationMessageProcessedEventArgs(ManagedMqttApplicationMessage applicationMessage, Exception exception) { @@ -11,9 +15,10 @@ namespace MQTTnet.Extensions.ManagedClient } public ManagedMqttApplicationMessage ApplicationMessage { get; } + + /// + /// Then this is _null_ the message was processed successfully without any error. + /// public Exception Exception { get; } - - public bool HasFailed => Exception != null; - public bool HasSucceeded => Exception == null; } } diff --git a/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageProcessedHandlerDelegate.cs b/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageProcessedHandlerDelegate.cs deleted file mode 100644 index e976722..0000000 --- a/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageProcessedHandlerDelegate.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace MQTTnet.Extensions.ManagedClient -{ - public class ApplicationMessageProcessedHandlerDelegate : IApplicationMessageProcessedHandler - { - private readonly Func _handler; - - public ApplicationMessageProcessedHandlerDelegate(Action handler) - { - if (handler == null) throw new ArgumentNullException(nameof(handler)); - - _handler = context => - { - handler(context); - return Task.FromResult(0); - }; - } - - public ApplicationMessageProcessedHandlerDelegate(Func handler) - { - _handler = handler ?? throw new ArgumentNullException(nameof(handler)); - } - - public Task HandleApplicationMessageProcessedAsync(ApplicationMessageProcessedEventArgs eventArgs) - { - return _handler(eventArgs); - } - } -} diff --git a/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageSkippedEventArgs.cs b/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageSkippedEventArgs.cs index 0f1613d..7515334 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageSkippedEventArgs.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageSkippedEventArgs.cs @@ -1,4 +1,8 @@ -using System; +// 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.Extensions.ManagedClient { diff --git a/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageSkippedHandlerDelegate.cs b/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageSkippedHandlerDelegate.cs index 978f30b..c20e8ef 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageSkippedHandlerDelegate.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ApplicationMessageSkippedHandlerDelegate.cs @@ -1,4 +1,8 @@ -using System; +// 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.Threading.Tasks; namespace MQTTnet.Extensions.ManagedClient diff --git a/Source/MQTTnet.Extensions.ManagedClient/ConnectingFailedEventArgs.cs b/Source/MQTTnet.Extensions.ManagedClient/ConnectingFailedEventArgs.cs new file mode 100644 index 0000000..5fd7bdd --- /dev/null +++ b/Source/MQTTnet.Extensions.ManagedClient/ConnectingFailedEventArgs.cs @@ -0,0 +1,21 @@ +using System; +using MQTTnet.Client; + +namespace MQTTnet.Extensions.ManagedClient +{ + public sealed class ConnectingFailedEventArgs : EventArgs + { + public ConnectingFailedEventArgs(MqttClientConnectResult connectResult, Exception exception) + { + ConnectResult = connectResult; + Exception = exception; + } + + /// + /// This is null when the connection was failing and the server was not reachable. + /// + public MqttClientConnectResult ConnectResult { get; } + + public Exception Exception { get; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/ConnectingFailedHandlerDelegate.cs b/Source/MQTTnet.Extensions.ManagedClient/ConnectingFailedHandlerDelegate.cs deleted file mode 100644 index 85a796b..0000000 --- a/Source/MQTTnet.Extensions.ManagedClient/ConnectingFailedHandlerDelegate.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace MQTTnet.Extensions.ManagedClient -{ - public class ConnectingFailedHandlerDelegate : IConnectingFailedHandler - { - private readonly Func _handler; - - public ConnectingFailedHandlerDelegate(Action handler) - { - if (handler == null) throw new ArgumentNullException(nameof(handler)); - - _handler = eventArgs => - { - handler(eventArgs); - return Task.FromResult(0); - }; - } - - public ConnectingFailedHandlerDelegate(Func handler) - { - _handler = handler ?? throw new ArgumentNullException(nameof(handler)); - } - - public Task HandleConnectingFailedAsync(ManagedProcessFailedEventArgs eventArgs) - { - return _handler(eventArgs); - } - } -} diff --git a/Source/MQTTnet.Extensions.ManagedClient/IApplicationMessageProcessedHandler.cs b/Source/MQTTnet.Extensions.ManagedClient/IApplicationMessageProcessedHandler.cs deleted file mode 100644 index 08ed197..0000000 --- a/Source/MQTTnet.Extensions.ManagedClient/IApplicationMessageProcessedHandler.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; - -namespace MQTTnet.Extensions.ManagedClient -{ - public interface IApplicationMessageProcessedHandler - { - Task HandleApplicationMessageProcessedAsync(ApplicationMessageProcessedEventArgs eventArgs); - } -} diff --git a/Source/MQTTnet.Extensions.ManagedClient/IApplicationMessageSkippedHandler.cs b/Source/MQTTnet.Extensions.ManagedClient/IApplicationMessageSkippedHandler.cs index e664cd2..fb51f61 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/IApplicationMessageSkippedHandler.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/IApplicationMessageSkippedHandler.cs @@ -1,4 +1,8 @@ -using System.Threading.Tasks; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; namespace MQTTnet.Extensions.ManagedClient { diff --git a/Source/MQTTnet.Extensions.ManagedClient/IConnectingFailedHandler.cs b/Source/MQTTnet.Extensions.ManagedClient/IConnectingFailedHandler.cs deleted file mode 100644 index aabdbfb..0000000 --- a/Source/MQTTnet.Extensions.ManagedClient/IConnectingFailedHandler.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; - -namespace MQTTnet.Extensions.ManagedClient -{ - public interface IConnectingFailedHandler - { - Task HandleConnectingFailedAsync(ManagedProcessFailedEventArgs eventArgs); - } -} diff --git a/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClient.cs b/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClient.cs deleted file mode 100644 index cc2490c..0000000 --- a/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClient.cs +++ /dev/null @@ -1,51 +0,0 @@ -using MQTTnet.Client.Connecting; -using MQTTnet.Client.Disconnecting; -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MQTTnet.Client; - -namespace MQTTnet.Extensions.ManagedClient -{ - public interface IManagedMqttClient : IApplicationMessageReceiver, IApplicationMessagePublisher, IDisposable - { - /// - /// Gets the internally used MQTT client. - /// This property should be used with caution because manipulating the internal client might break the managed client. - /// - IMqttClient InternalClient { get; } - - bool IsStarted { get; } - - bool IsConnected { get; } - - int PendingApplicationMessagesCount { get; } - - IManagedMqttClientOptions Options { get; } - - IMqttClientConnectedHandler ConnectedHandler { get; set; } - - IMqttClientDisconnectedHandler DisconnectedHandler { get; set; } - - IApplicationMessageProcessedHandler ApplicationMessageProcessedHandler { get; set; } - - IApplicationMessageSkippedHandler ApplicationMessageSkippedHandler { get; set; } - - IConnectingFailedHandler ConnectingFailedHandler { get; set; } - - ISynchronizingSubscriptionsFailedHandler SynchronizingSubscriptionsFailedHandler { get; set; } - - Task StartAsync(IManagedMqttClientOptions options); - - Task StopAsync(); - - Task PingAsync(CancellationToken cancellationToken); - - Task SubscribeAsync(IEnumerable topicFilters); - - Task UnsubscribeAsync(IEnumerable topics); - - Task PublishAsync(ManagedMqttApplicationMessage applicationMessages); - } -} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClientOptions.cs b/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClientOptions.cs deleted file mode 100644 index 74028da..0000000 --- a/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClientOptions.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using MQTTnet.Client.Options; -using MQTTnet.Server; - -namespace MQTTnet.Extensions.ManagedClient -{ - public interface IManagedMqttClientOptions - { - IMqttClientOptions ClientOptions { get; } - - TimeSpan AutoReconnectDelay { get; } - - TimeSpan ConnectionCheckInterval { get; } - - IManagedMqttClientStorage Storage { get; } - - int MaxPendingMessages { get; } - - MqttPendingMessagesOverflowStrategy PendingMessagesOverflowStrategy { get; } - - /// - /// Defines the maximum amount of topic filters which will be sent in a SUBSCRIBE/UNSUBSCRIBE packet. - /// Amazon AWS limits this number to 8. The default is int.MaxValue. - /// - int MaxTopicFiltersInSubscribeUnsubscribePackets { get; } - } -} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClientStorage.cs b/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClientStorage.cs index dc44001..09bd410 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClientStorage.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/IManagedMqttClientStorage.cs @@ -1,4 +1,8 @@ -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; using System.Threading.Tasks; namespace MQTTnet.Extensions.ManagedClient diff --git a/Source/MQTTnet.Extensions.ManagedClient/ISynchronizingSubscriptionsFailedHandler.cs b/Source/MQTTnet.Extensions.ManagedClient/ISynchronizingSubscriptionsFailedHandler.cs index f76f18f..5287e6a 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ISynchronizingSubscriptionsFailedHandler.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ISynchronizingSubscriptionsFailedHandler.cs @@ -1,4 +1,8 @@ -using System.Threading.Tasks; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; namespace MQTTnet.Extensions.ManagedClient { diff --git a/Source/MQTTnet.Extensions.ManagedClient/MQTTnet.Extensions.ManagedClient.csproj b/Source/MQTTnet.Extensions.ManagedClient/MQTTnet.Extensions.ManagedClient.csproj index b53859d..f47aea0 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/MQTTnet.Extensions.ManagedClient.csproj +++ b/Source/MQTTnet.Extensions.ManagedClient/MQTTnet.Extensions.ManagedClient.csproj @@ -1,40 +1,72 @@ - - netstandard1.3;netstandard2.0;netstandard2.1;net5.0 - $(TargetFrameworks);net452;net461 - $(TargetFrameworks);uap10.0 - - - - - False - false - false - true - true - snupkg - + + netstandard1.3;netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0 + $(TargetFrameworks);net452;net461 + $(TargetFrameworks);uap10.0 + + MQTTnet.Extensions.ManagedClient + MQTTnet.Extensions.ManagedClient + True + The contributors of MQTTnet + MQTTnet + This is an extension library which provides a managed MQTT client with additional features using MQTTnet. + The contributors of MQTTnet + MQTTnet.Extensions.ManagedClient + false + false + true + true + snupkg + Christian Kratky 2016-2022 + https://github.com/dotnet/MQTTnet + https://github.com/dotnet/MQTTnet.git + git + 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 + en-US + false + false + nuget.png + true + true + LICENSE + For release notes please go to MQTTnet release notes (https://www.nuget.org/packages/MQTTnet/). + true + - - false - UAP,Version=v10.0 - UAP - 10.0.18362.0 - 10.0.10240.0 - .NETCore - v5.0 - $(DefineConstants);WINDOWS_UWP - en - $(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets - + + false + UAP,Version=v10.0 + UAP + 10.0.18362.0 + 10.0.10240.0 + .NETCore + v5.0 + $(DefineConstants);WINDOWS_UWP + en + $(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets + - - - + + + True + \ + + - - - + + + True + \ + + + + + + + + + + diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttApplicationMessage.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttApplicationMessage.cs index bba73b0..7ef38eb 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttApplicationMessage.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttApplicationMessage.cs @@ -1,4 +1,8 @@ -using System; +// 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.Extensions.ManagedClient { diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttApplicationMessageBuilder.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttApplicationMessageBuilder.cs index bf67f70..96d0a42 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttApplicationMessageBuilder.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttApplicationMessageBuilder.cs @@ -1,4 +1,8 @@ -using System; +// 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.Extensions.ManagedClient { diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs index 6e1c1f5..4d3fed2 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClient.cs @@ -1,162 +1,155 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using MQTTnet.Client; -using MQTTnet.Client.Connecting; -using MQTTnet.Client.Disconnecting; -using MQTTnet.Client.Publishing; -using MQTTnet.Client.Receiving; using MQTTnet.Diagnostics; using MQTTnet.Exceptions; using MQTTnet.Internal; +using MQTTnet.Packets; using MQTTnet.Protocol; using MQTTnet.Server; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MQTTnet.Diagnostics.Logger; +using MqttClient = MQTTnet.Client.MqttClient; namespace MQTTnet.Extensions.ManagedClient { - public class ManagedMqttClient : Disposable, IManagedMqttClient + public sealed class ManagedMqttClient : Disposable { + readonly AsyncEvent _applicationMessageProcessedEvent = new AsyncEvent(); + readonly AsyncEvent _connectingFailedEvent = new AsyncEvent(); + readonly AsyncEvent _connectionStateChangedEvent = new AsyncEvent(); + + readonly MqttNetSourceLogger _logger; readonly BlockingQueue _messageQueue = new BlockingQueue(); + readonly AsyncLock _messageQueueLock = new AsyncLock(); + /// - /// The subscriptions are managed in 2 separate buckets: - /// and are processed during normal operation - /// and are moved to the when they get processed. They can be accessed by - /// any thread and are therefore mutex'ed. get sent to the broker - /// at reconnect and are solely owned by . + /// The subscriptions are managed in 2 separate buckets: + /// + /// and + /// + /// are processed during normal operation + /// and are moved to the + /// + /// when they get processed. They can be accessed by + /// any thread and are therefore mutex'ed. + /// + /// get sent to the broker + /// at reconnect and are solely owned by + /// + /// . /// readonly Dictionary _reconnectSubscriptions = new Dictionary(); readonly Dictionary _subscriptions = new Dictionary(); - readonly HashSet _unsubscriptions = new HashSet(); readonly SemaphoreSlim _subscriptionsQueuedSignal = new SemaphoreSlim(0); - - readonly MqttNetSourceLogger _logger; - - readonly AsyncLock _messageQueueLock = new AsyncLock(); + readonly HashSet _unsubscriptions = new HashSet(); CancellationTokenSource _connectionCancellationToken; - CancellationTokenSource _publishingCancellationToken; Task _maintainConnectionTask; + CancellationTokenSource _publishingCancellationToken; ManagedMqttClientStorageManager _storageManager; - public ManagedMqttClient(IMqttClient mqttClient, IMqttNetLogger logger) + public ManagedMqttClient(MqttClient mqttClient, IMqttNetLogger logger) { InternalClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient)); - if (logger == null) throw new ArgumentNullException(nameof(logger)); + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + _logger = logger.WithSource(nameof(ManagedMqttClient)); } - public bool IsConnected => InternalClient.IsConnected; - - public bool IsStarted => _connectionCancellationToken != null; - - public IMqttClient InternalClient { get; } - - public int PendingApplicationMessagesCount => _messageQueue.Count; - - public IManagedMqttClientOptions Options { get; private set; } - - public IMqttClientConnectedHandler ConnectedHandler + public event Func ApplicationMessageProcessedAsync { - get => InternalClient.ConnectedHandler; - set => InternalClient.ConnectedHandler = value; + add => _applicationMessageProcessedEvent.AddHandler(value); + remove => _applicationMessageProcessedEvent.RemoveHandler(value); } - public IMqttClientDisconnectedHandler DisconnectedHandler + public event Func ApplicationMessageReceivedAsync { - get => InternalClient.DisconnectedHandler; - set => InternalClient.DisconnectedHandler = value; + add => InternalClient.ApplicationMessageReceivedAsync += value; + remove => InternalClient.ApplicationMessageReceivedAsync -= value; } - public IMqttApplicationMessageReceivedHandler ApplicationMessageReceivedHandler + public event Func ConnectedAsync { - get => InternalClient.ApplicationMessageReceivedHandler; - set => InternalClient.ApplicationMessageReceivedHandler = value; + add => InternalClient.ConnectedAsync += value; + remove => InternalClient.ConnectedAsync -= value; } - public IApplicationMessageProcessedHandler ApplicationMessageProcessedHandler { get; set; } - - public IApplicationMessageSkippedHandler ApplicationMessageSkippedHandler { get; set; } - - public IConnectingFailedHandler ConnectingFailedHandler { get; set; } - - public ISynchronizingSubscriptionsFailedHandler SynchronizingSubscriptionsFailedHandler { get; set; } + public event Func ConnectingFailedAsync + { + add => _connectingFailedEvent.AddHandler(value); + remove => _connectingFailedEvent.RemoveHandler(value); + } - public async Task StartAsync(IManagedMqttClientOptions options) + public event Func ConnectionStateChangedAsync { - ThrowIfDisposed(); + add => _connectionStateChangedEvent.AddHandler(value); + remove => _connectionStateChangedEvent.RemoveHandler(value); + } - if (options == null) throw new ArgumentNullException(nameof(options)); - if (options.ClientOptions == null) throw new ArgumentException("The client options are not set.", nameof(options)); + public event Func DisconnectedAsync + { + add => InternalClient.DisconnectedAsync += value; + remove => InternalClient.DisconnectedAsync -= value; + } - if (!_maintainConnectionTask?.IsCompleted ?? false) throw new InvalidOperationException("The managed client is already started."); + public IApplicationMessageSkippedHandler ApplicationMessageSkippedHandler { get; set; } - Options = options; + public MqttClient InternalClient { get; } - if (options.Storage != null) - { - _storageManager = new ManagedMqttClientStorageManager(options.Storage); - var messages = await _storageManager.LoadQueuedMessagesAsync().ConfigureAwait(false); + public bool IsConnected => InternalClient.IsConnected; - foreach (var message in messages) - { - _messageQueue.Enqueue(message); - } - } + public bool IsStarted => _connectionCancellationToken != null; - var cancellationTokenSource = new CancellationTokenSource(); - var cancellationToken = cancellationTokenSource.Token; - _connectionCancellationToken = cancellationTokenSource; + public ManagedMqttClientOptions Options { get; private set; } - _maintainConnectionTask = Task.Run(() => MaintainConnectionAsync(cancellationToken), cancellationToken); - _maintainConnectionTask.RunInBackground(_logger); + public int PendingApplicationMessagesCount => _messageQueue.Count; - _logger.Info("Started"); - } + public ISynchronizingSubscriptionsFailedHandler SynchronizingSubscriptionsFailedHandler { get; set; } - public async Task StopAsync() + public async Task EnqueueAsync(MqttApplicationMessage applicationMessage) { ThrowIfDisposed(); - StopPublishing(); - StopMaintainingConnection(); - - _messageQueue.Clear(); - - if (_maintainConnectionTask != null) + if (applicationMessage == null) { - await Task.WhenAny(_maintainConnectionTask); - _maintainConnectionTask = null; + throw new ArgumentNullException(nameof(applicationMessage)); } - } - public Task PingAsync(CancellationToken cancellationToken) - { - return InternalClient.PingAsync(cancellationToken); + var managedMqttApplicationMessage = new ManagedMqttApplicationMessageBuilder().WithApplicationMessage(applicationMessage); + await EnqueueAsync(managedMqttApplicationMessage.Build()).ConfigureAwait(false); } - public async Task PublishAsync(MqttApplicationMessage applicationMessage, CancellationToken cancellationToken) + public async Task EnqueueAsync(ManagedMqttApplicationMessage applicationMessage) { ThrowIfDisposed(); - if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); - - await PublishAsync(new ManagedMqttApplicationMessageBuilder().WithApplicationMessage(applicationMessage).Build()).ConfigureAwait(false); - return new MqttClientPublishResult(); - } - - public async Task PublishAsync(ManagedMqttApplicationMessage applicationMessage) - { - ThrowIfDisposed(); + if (applicationMessage == null) + { + throw new ArgumentNullException(nameof(applicationMessage)); + } - if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); - if (Options == null) throw new InvalidOperationException("call StartAsync before publishing messages"); + if (Options == null) + { + throw new InvalidOperationException("call StartAsync before publishing messages"); + } MqttTopicValidator.ThrowIfInvalid(applicationMessage.ApplicationMessage.Topic); @@ -210,11 +203,77 @@ namespace MQTTnet.Extensions.ManagedClient } } - public Task SubscribeAsync(IEnumerable topicFilters) + public Task PingAsync(CancellationToken cancellationToken) + { + return InternalClient.PingAsync(cancellationToken); + } + + public async Task StartAsync(ManagedMqttClientOptions options) { ThrowIfDisposed(); - if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (options.ClientOptions == null) + { + throw new ArgumentException("The client options are not set.", nameof(options)); + } + + if (!_maintainConnectionTask?.IsCompleted ?? false) + { + throw new InvalidOperationException("The managed client is already started."); + } + + Options = options; + + if (options.Storage != null) + { + _storageManager = new ManagedMqttClientStorageManager(options.Storage); + var messages = await _storageManager.LoadQueuedMessagesAsync().ConfigureAwait(false); + + foreach (var message in messages) + { + _messageQueue.Enqueue(message); + } + } + + var cancellationTokenSource = new CancellationTokenSource(); + var cancellationToken = cancellationTokenSource.Token; + _connectionCancellationToken = cancellationTokenSource; + + _maintainConnectionTask = Task.Run(() => MaintainConnectionAsync(cancellationToken), cancellationToken); + _maintainConnectionTask.RunInBackground(_logger); + + _logger.Info("Started"); + } + + public async Task StopAsync() + { + ThrowIfDisposed(); + + StopPublishing(); + StopMaintainingConnection(); + + _messageQueue.Clear(); + + if (_maintainConnectionTask != null) + { + await Task.WhenAny(_maintainConnectionTask); + _maintainConnectionTask = null; + } + } + + public Task SubscribeAsync(ICollection topicFilters) + { + ThrowIfDisposed(); + + if (topicFilters == null) + { + throw new ArgumentNullException(nameof(topicFilters)); + } foreach (var topicFilter in topicFilters) { @@ -235,11 +294,14 @@ namespace MQTTnet.Extensions.ManagedClient return Task.FromResult(0); } - public Task UnsubscribeAsync(IEnumerable topics) + public Task UnsubscribeAsync(ICollection topics) { ThrowIfDisposed(); - if (topics == null) throw new ArgumentNullException(nameof(topics)); + if (topics == null) + { + throw new ArgumentNullException(nameof(topics)); + } lock (_subscriptions) { @@ -277,6 +339,23 @@ namespace MQTTnet.Extensions.ManagedClient base.Dispose(disposing); } + static TimeSpan GetRemainingTime(DateTime endTime) + { + var remainingTime = endTime - DateTime.UtcNow; + return remainingTime < TimeSpan.Zero ? TimeSpan.Zero : remainingTime; + } + + async Task HandleSubscriptionExceptionAsync(Exception exception) + { + _logger.Warning(exception, "Synchronizing subscriptions failed."); + + var synchronizingSubscriptionsFailedHandler = SynchronizingSubscriptionsFailedHandler; + if (SynchronizingSubscriptionsFailedHandler != null) + { + await synchronizingSubscriptionsFailedHandler.HandleSynchronizingSubscriptionsFailedAsync(new ManagedProcessFailedEventArgs(exception)).ConfigureAwait(false); + } + } + async Task MaintainConnectionAsync(CancellationToken cancellationToken) { try @@ -299,9 +378,9 @@ namespace MQTTnet.Extensions.ManagedClient { try { - using (var disconnectTimeout = new CancellationTokenSource(Options.ClientOptions.CommunicationTimeout)) + using (var disconnectTimeout = new CancellationTokenSource(Options.ClientOptions.Timeout)) { - await InternalClient.DisconnectAsync(disconnectTimeout.Token).ConfigureAwait(false); + await InternalClient.DisconnectAsync(new MqttClientDisconnectOptions(), disconnectTimeout.Token).ConfigureAwait(false); } } catch (OperationCanceledException) @@ -326,49 +405,6 @@ namespace MQTTnet.Extensions.ManagedClient } } - async Task TryMaintainConnectionAsync(CancellationToken cancellationToken) - { - try - { - var connectionState = await ReconnectIfRequiredAsync(cancellationToken).ConfigureAwait(false); - if (connectionState == ReconnectionResult.NotConnected) - { - StopPublishing(); - await Task.Delay(Options.AutoReconnectDelay, cancellationToken).ConfigureAwait(false); - return; - } - - if (connectionState == ReconnectionResult.Reconnected) - { - await PublishReconnectSubscriptionsAsync().ConfigureAwait(false); - StartPublishing(); - return; - } - - if (connectionState == ReconnectionResult.Recovered) - { - StartPublishing(); - return; - } - - if (connectionState == ReconnectionResult.StillConnected) - { - await PublishSubscriptionsAsync(Options.ConnectionCheckInterval, cancellationToken).ConfigureAwait(false); - } - } - catch (OperationCanceledException) - { - } - catch (MqttCommunicationException exception) - { - _logger.Warning(exception, "Communication error while maintaining connection."); - } - catch (Exception exception) - { - _logger.Error(exception, "Error exception while maintaining connection."); - } - } - async Task PublishQueuedMessagesAsync(CancellationToken cancellationToken) { try @@ -407,68 +443,40 @@ namespace MQTTnet.Extensions.ManagedClient } } - async Task TryPublishQueuedMessageAsync(ManagedMqttApplicationMessage message) + async Task PublishReconnectSubscriptionsAsync() { - Exception transmitException = null; + _logger.Info("Publishing subscriptions at reconnect"); + try { - await InternalClient.PublishAsync(message.ApplicationMessage).ConfigureAwait(false); - - using (await _messageQueueLock.WaitAsync(CancellationToken.None).ConfigureAwait(false)) //lock to avoid conflict with this.PublishAsync + if (_reconnectSubscriptions.Any()) { - // While publishing this message, this.PublishAsync could have booted this - // message off the queue to make room for another (when using a cap - // with the DropOldestQueuedMessage strategy). If the first item - // in the queue is equal to this message, then it's safe to remove - // it from the queue. If not, that means this.PublishAsync has already - // removed it, in which case we don't want to do anything. - _messageQueue.RemoveFirst(i => i.Id.Equals(message.Id)); - - if (_storageManager != null) - { - await _storageManager.RemoveAsync(message).ConfigureAwait(false); - } - } - } - catch (MqttCommunicationException exception) - { - transmitException = exception; + var subscriptions = _reconnectSubscriptions.Select( + i => new MqttTopicFilter + { + Topic = i.Key, + QualityOfServiceLevel = i.Value + }); - _logger.Warning(exception, "Publishing application message ({0}) failed.", message.Id); + var topicFilters = new List(); - if (message.ApplicationMessage.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce) - { - //If QoS 0, we don't want this message to stay on the queue. - //If QoS 1 or 2, it's possible that, when using a cap, this message - //has been booted off the queue by this.PublishAsync, in which case this - //thread will not continue to try to publish it. While this does - //contradict the expected behavior of QoS 1 and 2, that's also true - //for the usage of a message queue cap, so it's still consistent - //with prior behavior in that way. - using (await _messageQueueLock.WaitAsync(CancellationToken.None).ConfigureAwait(false)) //lock to avoid conflict with this.PublishAsync + foreach (var sub in subscriptions) { - _messageQueue.RemoveFirst(i => i.Id.Equals(message.Id)); + topicFilters.Add(sub); - if (_storageManager != null) + if (topicFilters.Count == Options.MaxTopicFiltersInSubscribeUnsubscribePackets) { - await _storageManager.RemoveAsync(message).ConfigureAwait(false); + await SendSubscribeUnsubscribe(topicFilters, null).ConfigureAwait(false); + topicFilters.Clear(); } } + + await SendSubscribeUnsubscribe(topicFilters, null).ConfigureAwait(false); } } catch (Exception exception) { - transmitException = exception; - _logger.Error(exception, "Error while publishing application message ({0}).", message.Id); - } - finally - { - var eventHandler = ApplicationMessageProcessedHandler; - if (eventHandler != null) - { - var eventArguments = new ApplicationMessageProcessedEventArgs(message, transmitException); - await eventHandler.HandleApplicationMessageProcessedAsync(eventArguments).ConfigureAwait(false); - } + await HandleSubscriptionExceptionAsync(exception).ConfigureAwait(false); } } @@ -483,14 +491,16 @@ namespace MQTTnet.Extensions.ManagedClient lock (_subscriptions) { - subscriptions = _subscriptions.Select(i => new MqttTopicFilter - { - Topic = i.Key, - QualityOfServiceLevel = i.Value - }).ToList(); - + subscriptions = _subscriptions.Select( + i => new MqttTopicFilter + { + Topic = i.Key, + QualityOfServiceLevel = i.Value + }) + .ToList(); + _subscriptions.Clear(); - + unsubscriptions = new HashSet(_unsubscriptions); _unsubscriptions.Clear(); } @@ -516,7 +526,7 @@ namespace MQTTnet.Extensions.ManagedClient foreach (var subscription in subscriptions) { addedTopicFilters.Add(subscription); - + if (addedTopicFilters.Count == Options.MaxTopicFiltersInSubscribeUnsubscribePackets) { await SendSubscribeUnsubscribe(addedTopicFilters, null).ConfigureAwait(false); @@ -530,7 +540,7 @@ namespace MQTTnet.Extensions.ManagedClient foreach (var unSub in unsubscriptions) { removedTopicFilters.Add(unSub); - + if (removedTopicFilters.Count == Options.MaxTopicFiltersInSubscribeUnsubscribePackets) { await SendSubscribeUnsubscribe(null, removedTopicFilters).ConfigureAwait(false); @@ -542,54 +552,61 @@ namespace MQTTnet.Extensions.ManagedClient } } - async Task SendSubscribeUnsubscribe(List addedSubscriptions, List removedSubscriptions) + async Task ReconnectIfRequiredAsync(CancellationToken cancellationToken) { + if (InternalClient.IsConnected) + { + return ReconnectionResult.StillConnected; + } + + MqttClientConnectResult connectResult = null; try { - if (removedSubscriptions != null && removedSubscriptions.Any()) + using (var connectTimeout = new CancellationTokenSource(Options.ClientOptions.Timeout)) { - await InternalClient.UnsubscribeAsync(removedSubscriptions.ToArray()).ConfigureAwait(false); + connectResult = await InternalClient.ConnectAsync(Options.ClientOptions, connectTimeout.Token).ConfigureAwait(false); } - if (addedSubscriptions != null && addedSubscriptions.Any()) + if (connectResult.ResultCode != MqttClientConnectResultCode.Success) { - await InternalClient.SubscribeAsync(addedSubscriptions.ToArray()).ConfigureAwait(false); + throw new MqttCommunicationException($"Client connected but server denied connection with reason '{connectResult.ResultCode}'."); } + + return connectResult.IsSessionPresent ? ReconnectionResult.Recovered : ReconnectionResult.Reconnected; } catch (Exception exception) { - await HandleSubscriptionExceptionAsync(exception).ConfigureAwait(false); + await _connectingFailedEvent.InvokeAsync(new ConnectingFailedEventArgs(connectResult, exception)); + return ReconnectionResult.NotConnected; } } - async Task PublishReconnectSubscriptionsAsync() + async Task SendSubscribeUnsubscribe(List addedSubscriptions, List removedSubscriptions) { - _logger.Info("Publishing subscriptions at reconnect"); - try { - if (_reconnectSubscriptions.Any()) + if (removedSubscriptions != null && removedSubscriptions.Any()) { - var subscriptions = _reconnectSubscriptions.Select(i => new MqttTopicFilter + var unsubscribeOptionsBuilder = new MqttClientUnsubscribeOptionsBuilder(); + + foreach (var removedSubscription in removedSubscriptions) { - Topic = i.Key, - QualityOfServiceLevel = i.Value - }); - - var topicFilters = new List(); - - foreach (var sub in subscriptions) + unsubscribeOptionsBuilder.WithTopicFilter(removedSubscription); + } + + await InternalClient.UnsubscribeAsync(unsubscribeOptionsBuilder.Build()).ConfigureAwait(false); + } + + if (addedSubscriptions != null && addedSubscriptions.Any()) + { + var subscribeOptionsBuilder = new MqttClientSubscribeOptionsBuilder(); + + foreach (var addedSubscription in addedSubscriptions) { - topicFilters.Add(sub); - - if (topicFilters.Count == Options.MaxTopicFiltersInSubscribeUnsubscribePackets) - { - await SendSubscribeUnsubscribe(topicFilters, null).ConfigureAwait(false); - topicFilters.Clear(); - } + subscribeOptionsBuilder.WithTopicFilter(addedSubscription); } - await SendSubscribeUnsubscribe(topicFilters, null).ConfigureAwait(false); + await InternalClient.SubscribeAsync(subscribeOptionsBuilder.Build()).ConfigureAwait(false); } } catch (Exception exception) @@ -598,53 +615,28 @@ namespace MQTTnet.Extensions.ManagedClient } } - async Task HandleSubscriptionExceptionAsync(Exception exception) + void StartPublishing() { - _logger.Warning(exception, "Synchronizing subscriptions failed."); + StopPublishing(); - var synchronizingSubscriptionsFailedHandler = SynchronizingSubscriptionsFailedHandler; - if (SynchronizingSubscriptionsFailedHandler != null) - { - await synchronizingSubscriptionsFailedHandler.HandleSynchronizingSubscriptionsFailedAsync(new ManagedProcessFailedEventArgs(exception)).ConfigureAwait(false); - } + var cancellationTokenSource = new CancellationTokenSource(); + var cancellationToken = cancellationTokenSource.Token; + _publishingCancellationToken = cancellationTokenSource; + + Task.Run(() => PublishQueuedMessagesAsync(cancellationToken), cancellationToken).RunInBackground(_logger); } - async Task ReconnectIfRequiredAsync(CancellationToken cancellationToken) + void StopMaintainingConnection() { - if (InternalClient.IsConnected) - { - return ReconnectionResult.StillConnected; - } - try { - var result = await InternalClient.ConnectAsync(Options.ClientOptions, cancellationToken).ConfigureAwait(false); - return result.IsSessionPresent ? ReconnectionResult.Recovered : ReconnectionResult.Reconnected; - } - catch (Exception exception) - { - var connectingFailedHandler = ConnectingFailedHandler; - if (connectingFailedHandler != null) - { - await connectingFailedHandler.HandleConnectingFailedAsync(new ManagedProcessFailedEventArgs(exception)).ConfigureAwait(false); - } - - return ReconnectionResult.NotConnected; + _connectionCancellationToken?.Cancel(false); } - } - - void StartPublishing() - { - if (_publishingCancellationToken != null) + finally { - StopPublishing(); + _connectionCancellationToken?.Dispose(); + _connectionCancellationToken = null; } - - var cancellationTokenSource = new CancellationTokenSource(); - var cancellationToken = cancellationTokenSource.Token; - _publishingCancellationToken = cancellationTokenSource; - - Task.Run(() => PublishQueuedMessagesAsync(cancellationToken), cancellationToken).RunInBackground(_logger); } void StopPublishing() @@ -660,23 +652,112 @@ namespace MQTTnet.Extensions.ManagedClient } } - void StopMaintainingConnection() + async Task TryMaintainConnectionAsync(CancellationToken cancellationToken) { try { - _connectionCancellationToken?.Cancel(false); + var oldConnectionState = InternalClient.IsConnected; + var connectionState = await ReconnectIfRequiredAsync(cancellationToken).ConfigureAwait(false); + + if (connectionState == ReconnectionResult.NotConnected) + { + StopPublishing(); + await Task.Delay(Options.AutoReconnectDelay, cancellationToken).ConfigureAwait(false); + } + else if (connectionState == ReconnectionResult.Reconnected) + { + await PublishReconnectSubscriptionsAsync().ConfigureAwait(false); + StartPublishing(); + } + else if (connectionState == ReconnectionResult.Recovered) + { + StartPublishing(); + } + else if (connectionState == ReconnectionResult.StillConnected) + { + await PublishSubscriptionsAsync(Options.ConnectionCheckInterval, cancellationToken).ConfigureAwait(false); + } + + if (oldConnectionState != InternalClient.IsConnected) + { + await _connectionStateChangedEvent.InvokeAsync(EventArgs.Empty).ConfigureAwait(false); + } } - finally + catch (OperationCanceledException) { - _connectionCancellationToken?.Dispose(); - _connectionCancellationToken = null; + } + catch (MqttCommunicationException exception) + { + _logger.Warning(exception, "Communication error while maintaining connection."); + } + catch (Exception exception) + { + _logger.Error(exception, "Error exception while maintaining connection."); } } - static TimeSpan GetRemainingTime(DateTime endTime) + async Task TryPublishQueuedMessageAsync(ManagedMqttApplicationMessage message) { - var remainingTime = endTime - DateTime.UtcNow; - return remainingTime < TimeSpan.Zero ? TimeSpan.Zero : remainingTime; + Exception transmitException = null; + try + { + await InternalClient.PublishAsync(message.ApplicationMessage).ConfigureAwait(false); + + using (await _messageQueueLock.WaitAsync(CancellationToken.None).ConfigureAwait(false)) //lock to avoid conflict with this.PublishAsync + { + // While publishing this message, this.PublishAsync could have booted this + // message off the queue to make room for another (when using a cap + // with the DropOldestQueuedMessage strategy). If the first item + // in the queue is equal to this message, then it's safe to remove + // it from the queue. If not, that means this.PublishAsync has already + // removed it, in which case we don't want to do anything. + _messageQueue.RemoveFirst(i => i.Id.Equals(message.Id)); + + if (_storageManager != null) + { + await _storageManager.RemoveAsync(message).ConfigureAwait(false); + } + } + } + catch (MqttCommunicationException exception) + { + transmitException = exception; + + _logger.Warning(exception, "Publishing application message ({0}) failed.", message.Id); + + if (message.ApplicationMessage.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce) + { + //If QoS 0, we don't want this message to stay on the queue. + //If QoS 1 or 2, it's possible that, when using a cap, this message + //has been booted off the queue by this.PublishAsync, in which case this + //thread will not continue to try to publish it. While this does + //contradict the expected behavior of QoS 1 and 2, that's also true + //for the usage of a message queue cap, so it's still consistent + //with prior behavior in that way. + using (await _messageQueueLock.WaitAsync(CancellationToken.None).ConfigureAwait(false)) //lock to avoid conflict with this.PublishAsync + { + _messageQueue.RemoveFirst(i => i.Id.Equals(message.Id)); + + if (_storageManager != null) + { + await _storageManager.RemoveAsync(message).ConfigureAwait(false); + } + } + } + } + catch (Exception exception) + { + transmitException = exception; + _logger.Error(exception, "Error while publishing application message ({0}).", message.Id); + } + finally + { + if (_applicationMessageProcessedEvent.HasHandlers) + { + var eventArgs = new ApplicationMessageProcessedEventArgs(message, transmitException); + await _applicationMessageProcessedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + } + } } } } \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientExtensions.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientExtensions.cs index d92e5c2..d14538c 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientExtensions.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientExtensions.cs @@ -1,232 +1,78 @@ -using MQTTnet.Client.Connecting; -using MQTTnet.Client.Disconnecting; -using MQTTnet.Client.Publishing; -using MQTTnet.Client.Receiving; -using MQTTnet.Protocol; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + using System; using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; +using MQTTnet.Packets; +using MQTTnet.Protocol; namespace MQTTnet.Extensions.ManagedClient { public static class ManagedMqttClientExtensions { - public static IManagedMqttClient UseConnectedHandler(this IManagedMqttClient client, Func handler) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - - if (handler == null) + public static Task EnqueueAsync( + this ManagedMqttClient managedMqttClient, + string topic, + string payload = null, + MqttQualityOfServiceLevel qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce, + bool retain = false) + { + if (managedMqttClient == null) { - return client.UseConnectedHandler((IMqttClientConnectedHandler)null); + throw new ArgumentNullException(nameof(managedMqttClient)); } - return client.UseConnectedHandler(new MqttClientConnectedHandlerDelegate(handler)); - } - - public static IManagedMqttClient UseConnectedHandler(this IManagedMqttClient client, Action handler) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - - if (handler == null) + if (topic == null) { - return client.UseConnectedHandler((IMqttClientConnectedHandler)null); + throw new ArgumentNullException(nameof(topic)); } - return client.UseConnectedHandler(new MqttClientConnectedHandlerDelegate(handler)); - } - - public static IManagedMqttClient UseConnectedHandler(this IManagedMqttClient client, IMqttClientConnectedHandler handler) - { - if (client == null) throw new ArgumentNullException(nameof(client)); + var applicationMessage = new MqttApplicationMessageBuilder().WithTopic(topic) + .WithPayload(payload) + .WithRetainFlag(retain) + .WithQualityOfServiceLevel(qualityOfServiceLevel) + .Build(); - client.ConnectedHandler = handler; - return client; + return managedMqttClient.EnqueueAsync(applicationMessage); } - public static IManagedMqttClient UseDisconnectedHandler(this IManagedMqttClient client, Func handler) + public static Task SubscribeAsync( + this ManagedMqttClient managedMqttClient, + string topic, + MqttQualityOfServiceLevel qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce) { - if (client == null) throw new ArgumentNullException(nameof(client)); - - if (handler == null) + if (managedMqttClient == null) { - return client.UseDisconnectedHandler((IMqttClientDisconnectedHandler)null); + throw new ArgumentNullException(nameof(managedMqttClient)); } - return client.UseDisconnectedHandler(new MqttClientDisconnectedHandlerDelegate(handler)); - } - - public static IManagedMqttClient UseDisconnectedHandler(this IManagedMqttClient client, Action handler) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - - if (handler == null) + if (topic == null) { - return client.UseDisconnectedHandler((IMqttClientDisconnectedHandler)null); + throw new ArgumentNullException(nameof(topic)); } - return client.UseDisconnectedHandler(new MqttClientDisconnectedHandlerDelegate(handler)); - } - - public static IManagedMqttClient UseDisconnectedHandler(this IManagedMqttClient client, IMqttClientDisconnectedHandler handler) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - - client.DisconnectedHandler = handler; - return client; + return managedMqttClient.SubscribeAsync( + new List + { + new MqttTopicFilterBuilder().WithTopic(topic).WithQualityOfServiceLevel(qualityOfServiceLevel).Build() + }); } - public static TReceiver UseApplicationMessageReceivedHandler(this TReceiver receiver, Func handler) - where TReceiver : IApplicationMessageReceiver + public static Task UnsubscribeAsync(this ManagedMqttClient managedMqttClient, string topic) { - if (receiver == null) throw new ArgumentNullException(nameof(receiver)); - - if (handler == null) + if (managedMqttClient == null) { - return receiver.UseApplicationMessageReceivedHandler((IMqttApplicationMessageReceivedHandler)null); + throw new ArgumentNullException(nameof(managedMqttClient)); } - return receiver.UseApplicationMessageReceivedHandler(new MqttApplicationMessageReceivedHandlerDelegate(handler)); - } - - public static TReceiver UseApplicationMessageReceivedHandler(this TReceiver receiver, Action handler) - where TReceiver : IApplicationMessageReceiver - { - if (receiver == null) throw new ArgumentNullException(nameof(receiver)); - - if (handler == null) + if (topic == null) { - return receiver.UseApplicationMessageReceivedHandler((IMqttApplicationMessageReceivedHandler)null); + throw new ArgumentNullException(nameof(topic)); } - return receiver.UseApplicationMessageReceivedHandler(new MqttApplicationMessageReceivedHandlerDelegate(handler)); - } - - public static TReceiver UseApplicationMessageReceivedHandler(this TReceiver receiver, IMqttApplicationMessageReceivedHandler handler) - where TReceiver : IApplicationMessageReceiver - { - if (receiver == null) throw new ArgumentNullException(nameof(receiver)); - - receiver.ApplicationMessageReceivedHandler = handler; - return receiver; - } - - public static Task SubscribeAsync(this IManagedMqttClient client, params MqttTopicFilter[] topicFilters) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - - return client.SubscribeAsync(topicFilters); - } - - public static Task SubscribeAsync(this IManagedMqttClient client, string topic, MqttQualityOfServiceLevel qualityOfServiceLevel) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); - - return client.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic(topic).WithQualityOfServiceLevel(qualityOfServiceLevel).Build()); - } - - public static Task SubscribeAsync(this IManagedMqttClient client, string topic) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); - - return client.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic(topic).Build()); - } - - public static Task UnsubscribeAsync(this IManagedMqttClient client, params string[] topicFilters) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - - return client.UnsubscribeAsync(topicFilters); - } - - public static async Task PublishAsync(this IApplicationMessagePublisher publisher, IEnumerable applicationMessages) - { - if (publisher == null) throw new ArgumentNullException(nameof(publisher)); - if (applicationMessages == null) throw new ArgumentNullException(nameof(applicationMessages)); - - foreach (var applicationMessage in applicationMessages) - { - await publisher.PublishAsync(applicationMessage).ConfigureAwait(false); - } - } - - public static Task PublishAsync(this IApplicationMessagePublisher publisher, MqttApplicationMessage applicationMessage) - { - if (publisher == null) throw new ArgumentNullException(nameof(publisher)); - if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); - - return publisher.PublishAsync(applicationMessage, CancellationToken.None); - } - - public static async Task PublishAsync(this IApplicationMessagePublisher publisher, params MqttApplicationMessage[] applicationMessages) - { - if (publisher == null) throw new ArgumentNullException(nameof(publisher)); - if (applicationMessages == null) throw new ArgumentNullException(nameof(applicationMessages)); - - foreach (var applicationMessage in applicationMessages) - { - await publisher.PublishAsync(applicationMessage, CancellationToken.None).ConfigureAwait(false); - } - } - - public static Task PublishAsync(this IApplicationMessagePublisher publisher, string topic) - { - if (publisher == null) throw new ArgumentNullException(nameof(publisher)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); - - return publisher.PublishAsync(builder => builder - .WithTopic(topic)); - } - - public static Task PublishAsync(this IApplicationMessagePublisher publisher, string topic, string payload) - { - if (publisher == null) throw new ArgumentNullException(nameof(publisher)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); - - return publisher.PublishAsync(builder => builder - .WithTopic(topic) - .WithPayload(payload)); - } - - public static Task PublishAsync(this IApplicationMessagePublisher publisher, string topic, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel) - { - if (publisher == null) throw new ArgumentNullException(nameof(publisher)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); - - return publisher.PublishAsync(builder => builder - .WithTopic(topic) - .WithPayload(payload) - .WithQualityOfServiceLevel(qualityOfServiceLevel)); - } - - public static Task PublishAsync(this IApplicationMessagePublisher publisher, string topic, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel, bool retain) - { - if (publisher == null) throw new ArgumentNullException(nameof(publisher)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); - - return publisher.PublishAsync(builder => builder - .WithTopic(topic) - .WithPayload(payload) - .WithQualityOfServiceLevel(qualityOfServiceLevel) - .WithRetainFlag(retain)); - } - - public static Task PublishAsync(this IApplicationMessagePublisher publisher, Func builder, CancellationToken cancellationToken) - { - if (publisher == null) throw new ArgumentNullException(nameof(publisher)); - - var message = builder(new MqttApplicationMessageBuilder()).Build(); - return publisher.PublishAsync(message, cancellationToken); - } - - public static Task PublishAsync(this IApplicationMessagePublisher publisher, Func builder) - { - if (publisher == null) throw new ArgumentNullException(nameof(publisher)); - - var message = builder(new MqttApplicationMessageBuilder()).Build(); - return publisher.PublishAsync(message, CancellationToken.None); + return managedMqttClient.UnsubscribeAsync(new List { topic }); } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptions.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptions.cs index f948055..dcd0c11 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptions.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptions.cs @@ -1,12 +1,16 @@ +// 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.Client.Options; +using MQTTnet.Client; using MQTTnet.Server; namespace MQTTnet.Extensions.ManagedClient { - public sealed class ManagedMqttClientOptions : IManagedMqttClientOptions + public sealed class ManagedMqttClientOptions { - public IMqttClientOptions ClientOptions { get; set; } + public MqttClientOptions ClientOptions { get; set; } public TimeSpan AutoReconnectDelay { get; set; } = TimeSpan.FromSeconds(5); @@ -18,6 +22,10 @@ namespace MQTTnet.Extensions.ManagedClient public MqttPendingMessagesOverflowStrategy PendingMessagesOverflowStrategy { get; set; } = MqttPendingMessagesOverflowStrategy.DropNewMessage; + /// + /// Defines the maximum amount of topic filters which will be sent in a SUBSCRIBE/UNSUBSCRIBE packet. + /// Amazon AWS limits this number to 8. The default is int.MaxValue. + /// public int MaxTopicFiltersInSubscribeUnsubscribePackets { get; set; } = int.MaxValue; } } diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptionsBuilder.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptionsBuilder.cs index dec105f..72c192b 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptionsBuilder.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientOptionsBuilder.cs @@ -1,5 +1,9 @@ +// 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.Client.Options; +using MQTTnet.Client; using MQTTnet.Server; namespace MQTTnet.Extensions.ManagedClient @@ -33,7 +37,7 @@ namespace MQTTnet.Extensions.ManagedClient return this; } - public ManagedMqttClientOptionsBuilder WithClientOptions(IMqttClientOptions value) + public ManagedMqttClientOptionsBuilder WithClientOptions(MqttClientOptions value) { if (_clientOptionsBuilder != null) { diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientStorageManager.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientStorageManager.cs index 772f102..1b812d8 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientStorageManager.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedMqttClientStorageManager.cs @@ -1,4 +1,8 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; diff --git a/Source/MQTTnet.Extensions.ManagedClient/ManagedProcessFailedEventArgs.cs b/Source/MQTTnet.Extensions.ManagedClient/ManagedProcessFailedEventArgs.cs index bb0cf68..73eee45 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ManagedProcessFailedEventArgs.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ManagedProcessFailedEventArgs.cs @@ -1,4 +1,8 @@ -using System; +// 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.Extensions.ManagedClient { diff --git a/Source/MQTTnet.Extensions.ManagedClient/MqttFactoryExtensions.cs b/Source/MQTTnet.Extensions.ManagedClient/MqttFactoryExtensions.cs index 10256c5..be9ae2a 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/MqttFactoryExtensions.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/MqttFactoryExtensions.cs @@ -1,18 +1,28 @@ -using System; -using MQTTnet.Diagnostics.Logger; +// 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.Client; +using MQTTnet.Diagnostics; namespace MQTTnet.Extensions.ManagedClient { public static class MqttFactoryExtensions { - public static IManagedMqttClient CreateManagedMqttClient(this IMqttFactory factory) + public static ManagedMqttClient CreateManagedMqttClient(this MqttFactory factory, MqttClient mqttClient = null) { if (factory == null) throw new ArgumentNullException(nameof(factory)); - return new ManagedMqttClient(factory.CreateMqttClient(), factory.DefaultLogger); + if (mqttClient == null) + { + return new ManagedMqttClient(factory.CreateMqttClient(), factory.DefaultLogger); + } + + return new ManagedMqttClient(mqttClient, factory.DefaultLogger); } - - public static IManagedMqttClient CreateManagedMqttClient(this IMqttFactory factory, IMqttNetLogger logger) + + public static ManagedMqttClient CreateManagedMqttClient(this MqttFactory factory, IMqttNetLogger logger) { if (factory == null) throw new ArgumentNullException(nameof(factory)); if (logger == null) throw new ArgumentNullException(nameof(logger)); diff --git a/Source/MQTTnet.Extensions.ManagedClient/ReconnectionResult.cs b/Source/MQTTnet.Extensions.ManagedClient/ReconnectionResult.cs index 092662f..9bee012 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/ReconnectionResult.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/ReconnectionResult.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Extensions.ManagedClient +// 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.Extensions.ManagedClient { public enum ReconnectionResult { diff --git a/Source/MQTTnet.Extensions.ManagedClient/SynchronizingSubscriptionsFailedHandlerDelegate.cs b/Source/MQTTnet.Extensions.ManagedClient/SynchronizingSubscriptionsFailedHandlerDelegate.cs index 72d6e4a..2a4b6b4 100644 --- a/Source/MQTTnet.Extensions.ManagedClient/SynchronizingSubscriptionsFailedHandlerDelegate.cs +++ b/Source/MQTTnet.Extensions.ManagedClient/SynchronizingSubscriptionsFailedHandlerDelegate.cs @@ -1,4 +1,8 @@ -using System; +// 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.Threading.Tasks; namespace MQTTnet.Extensions.ManagedClient diff --git a/Source/MQTTnet.Extensions.Rpc/IMqttRpcClient.cs b/Source/MQTTnet.Extensions.Rpc/IMqttRpcClient.cs deleted file mode 100644 index 2941fe8..0000000 --- a/Source/MQTTnet.Extensions.Rpc/IMqttRpcClient.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using MQTTnet.Protocol; - -namespace MQTTnet.Extensions.Rpc -{ - public interface IMqttRpcClient : IDisposable - { - Task ExecuteAsync(TimeSpan timeout, string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel); - - Task ExecuteAsync(string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, CancellationToken cancellationToken); - } -} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.Rpc/MQTTnet.Extensions.Rpc.csproj b/Source/MQTTnet.Extensions.Rpc/MQTTnet.Extensions.Rpc.csproj index b53859d..6f36766 100644 --- a/Source/MQTTnet.Extensions.Rpc/MQTTnet.Extensions.Rpc.csproj +++ b/Source/MQTTnet.Extensions.Rpc/MQTTnet.Extensions.Rpc.csproj @@ -1,40 +1,72 @@ - - netstandard1.3;netstandard2.0;netstandard2.1;net5.0 - $(TargetFrameworks);net452;net461 - $(TargetFrameworks);uap10.0 - - - - - False - false - false - true - true - snupkg - + + netstandard1.3;netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0 + $(TargetFrameworks);net452;net461 + $(TargetFrameworks);uap10.0 + + MQTTnet.Extensions.Rpc + MQTTnet.Extensions.Rpc + True + The contributors of MQTTnet + MQTTnet + This is an extension library which allows executing synchronous device calls including a response using MQTTnet. + The contributors of MQTTnet + MQTTnet.Extensions.Rpc + false + false + true + true + snupkg + Christian Kratky 2016-2022 + https://github.com/dotnet/MQTTnet + https://github.com/dotnet/MQTTnet.git + git + 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 + en-US + false + false + nuget.png + true + true + LICENSE + For release notes please go to MQTTnet release notes (https://www.nuget.org/packages/MQTTnet/). + true + - - false - UAP,Version=v10.0 - UAP - 10.0.18362.0 - 10.0.10240.0 - .NETCore - v5.0 - $(DefineConstants);WINDOWS_UWP - en - $(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets - + + false + UAP,Version=v10.0 + UAP + 10.0.18362.0 + 10.0.10240.0 + .NETCore + v5.0 + $(DefineConstants);WINDOWS_UWP + en + $(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets + - - - + + + True + \ + + - - - + + + True + \ + + + + + + + + + + diff --git a/Source/MQTTnet.Extensions.Rpc/MQTTnet.Extensions.Rpc.csproj.DotSettings b/Source/MQTTnet.Extensions.Rpc/MQTTnet.Extensions.Rpc.csproj.DotSettings new file mode 100644 index 0000000..1b491ba --- /dev/null +++ b/Source/MQTTnet.Extensions.Rpc/MQTTnet.Extensions.Rpc.csproj.DotSettings @@ -0,0 +1,3 @@ + + True + True \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.Rpc/MqttFactoryExtensions.cs b/Source/MQTTnet.Extensions.Rpc/MqttFactoryExtensions.cs new file mode 100644 index 0000000..42028cc --- /dev/null +++ b/Source/MQTTnet.Extensions.Rpc/MqttFactoryExtensions.cs @@ -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 MQTTnet.Client; + +namespace MQTTnet.Extensions.Rpc +{ + public static class MqttFactoryExtensions + { + public static MqttRpcClient CreateMqttRpcClient(this MqttFactory factory, MqttClient mqttClient) + { + return factory.CreateMqttRpcClient(mqttClient, new MqttRpcClientOptions + { + TopicGenerationStrategy = new DefaultMqttRpcClientTopicGenerationStrategy() + }); + } + + public static MqttRpcClient CreateMqttRpcClient(this MqttFactory factory, MqttClient mqttClient, MqttRpcClientOptions rpcClientOptions) + { + if (factory == null) throw new ArgumentNullException(nameof(factory)); + + if (mqttClient == null) + { + throw new ArgumentNullException(nameof(mqttClient)); + } + + if (rpcClientOptions == null) + { + throw new ArgumentNullException(nameof(rpcClientOptions)); + } + + return new MqttRpcClient(mqttClient, rpcClientOptions); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.Rpc/MqttRpcClient.cs b/Source/MQTTnet.Extensions.Rpc/MqttRpcClient.cs index 7a1cfef..cf0a6f7 100644 --- a/Source/MQTTnet.Extensions.Rpc/MqttRpcClient.cs +++ b/Source/MQTTnet.Extensions.Rpc/MqttRpcClient.cs @@ -1,39 +1,30 @@ +// 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.Client; using MQTTnet.Exceptions; -using MQTTnet.Extensions.Rpc.Options; -using MQTTnet.Extensions.Rpc.Options.TopicGeneration; using MQTTnet.Protocol; using System; using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; -using MQTTnet.Client.Subscribing; using MQTTnet.Implementations; namespace MQTTnet.Extensions.Rpc { - public sealed class MqttRpcClient : IMqttRpcClient + public sealed class MqttRpcClient : IDisposable { readonly ConcurrentDictionary> _waitingCalls = new ConcurrentDictionary>(); - readonly IMqttClient _mqttClient; - readonly IMqttRpcClientOptions _options; - readonly RpcAwareApplicationMessageReceivedHandler _applicationMessageReceivedHandler; - - [Obsolete("Use MqttRpcClient(IMqttClient mqttClient, IMqttRpcClientOptions options).")] - public MqttRpcClient(IMqttClient mqttClient) : this(mqttClient, new MqttRpcClientOptions()) - { - } - - public MqttRpcClient(IMqttClient mqttClient, IMqttRpcClientOptions options) + readonly MqttClient _mqttClient; + readonly MqttRpcClientOptions _options; + + public MqttRpcClient(MqttClient mqttClient, MqttRpcClientOptions options) { _mqttClient = mqttClient ?? throw new ArgumentNullException(nameof(mqttClient)); _options = options ?? throw new ArgumentNullException(nameof(options)); - _applicationMessageReceivedHandler = new RpcAwareApplicationMessageReceivedHandler( - mqttClient.ApplicationMessageReceivedHandler, - HandleApplicationMessageReceivedAsync); - - _mqttClient.ApplicationMessageReceivedHandler = _applicationMessageReceivedHandler; + _mqttClient.ApplicationMessageReceivedAsync += HandleApplicationMessageReceivedAsync; } public async Task ExecuteAsync(TimeSpan timeout, string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel) @@ -59,12 +50,7 @@ namespace MQTTnet.Extensions.Rpc public async Task ExecuteAsync(string methodName, byte[] payload, MqttQualityOfServiceLevel qualityOfServiceLevel, CancellationToken cancellationToken) { if (methodName == null) throw new ArgumentNullException(nameof(methodName)); - - if (!(_mqttClient.ApplicationMessageReceivedHandler is RpcAwareApplicationMessageReceivedHandler)) - { - throw new InvalidOperationException("The application message received handler was modified."); - } - + var topicNames = _options.TopicGenerationStrategy.CreateRpcTopics(new TopicGenerationContext { MethodName = methodName, @@ -121,7 +107,7 @@ namespace MQTTnet.Extensions.Rpc finally { _waitingCalls.TryRemove(responseTopic, out _); - + await _mqttClient.UnsubscribeAsync(responseTopic).ConfigureAwait(false); } } @@ -147,7 +133,7 @@ namespace MQTTnet.Extensions.Rpc public void Dispose() { - _mqttClient.ApplicationMessageReceivedHandler = _applicationMessageReceivedHandler.OriginalHandler; + _mqttClient.ApplicationMessageReceivedAsync -= HandleApplicationMessageReceivedAsync; foreach (var tcs in _waitingCalls) { diff --git a/Source/MQTTnet.Extensions.Rpc/MqttRpcClientExtensions.cs b/Source/MQTTnet.Extensions.Rpc/MqttRpcClientExtensions.cs index 005c774..86fd1ef 100644 --- a/Source/MQTTnet.Extensions.Rpc/MqttRpcClientExtensions.cs +++ b/Source/MQTTnet.Extensions.Rpc/MqttRpcClientExtensions.cs @@ -1,4 +1,8 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; using System.Text; using System.Threading.Tasks; using MQTTnet.Protocol; @@ -7,7 +11,7 @@ namespace MQTTnet.Extensions.Rpc { public static class MqttRpcClientExtensions { - public static Task ExecuteAsync(this IMqttRpcClient client, TimeSpan timeout, string methodName, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel) + public static Task ExecuteAsync(this MqttRpcClient client, TimeSpan timeout, string methodName, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel) { if (client == null) throw new ArgumentNullException(nameof(client)); diff --git a/Source/MQTTnet.Extensions.Rpc/Options/IMqttRpcClientOptions.cs b/Source/MQTTnet.Extensions.Rpc/Options/IMqttRpcClientOptions.cs deleted file mode 100644 index 6b7afa9..0000000 --- a/Source/MQTTnet.Extensions.Rpc/Options/IMqttRpcClientOptions.cs +++ /dev/null @@ -1,9 +0,0 @@ -using MQTTnet.Extensions.Rpc.Options.TopicGeneration; - -namespace MQTTnet.Extensions.Rpc.Options -{ - public interface IMqttRpcClientOptions - { - IMqttRpcClientTopicGenerationStrategy TopicGenerationStrategy { get; set; } - } -} \ No newline at end of file diff --git a/Source/MQTTnet.Extensions.Rpc/Options/MqttRpcClientOptions.cs b/Source/MQTTnet.Extensions.Rpc/Options/MqttRpcClientOptions.cs index 617653a..bb89170 100644 --- a/Source/MQTTnet.Extensions.Rpc/Options/MqttRpcClientOptions.cs +++ b/Source/MQTTnet.Extensions.Rpc/Options/MqttRpcClientOptions.cs @@ -1,8 +1,10 @@ -using MQTTnet.Extensions.Rpc.Options.TopicGeneration; +// 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.Extensions.Rpc.Options +namespace MQTTnet.Extensions.Rpc { - public sealed class MqttRpcClientOptions : IMqttRpcClientOptions + public sealed class MqttRpcClientOptions { public IMqttRpcClientTopicGenerationStrategy TopicGenerationStrategy { get; set; } = new DefaultMqttRpcClientTopicGenerationStrategy(); } diff --git a/Source/MQTTnet.Extensions.Rpc/Options/MqttRpcClientOptionsBuilder.cs b/Source/MQTTnet.Extensions.Rpc/Options/MqttRpcClientOptionsBuilder.cs index 9f0e986..dc53ddc 100644 --- a/Source/MQTTnet.Extensions.Rpc/Options/MqttRpcClientOptionsBuilder.cs +++ b/Source/MQTTnet.Extensions.Rpc/Options/MqttRpcClientOptionsBuilder.cs @@ -1,7 +1,10 @@ -using MQTTnet.Extensions.Rpc.Options.TopicGeneration; +// 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.Extensions.Rpc.Options +namespace MQTTnet.Extensions.Rpc { public sealed class MqttRpcClientOptionsBuilder { @@ -14,7 +17,7 @@ namespace MQTTnet.Extensions.Rpc.Options return this; } - public IMqttRpcClientOptions Build() + public MqttRpcClientOptions Build() { return new MqttRpcClientOptions { diff --git a/Source/MQTTnet.Extensions.Rpc/Options/MqttRpcTopicPair.cs b/Source/MQTTnet.Extensions.Rpc/Options/MqttRpcTopicPair.cs index 56f768a..4cbb166 100644 --- a/Source/MQTTnet.Extensions.Rpc/Options/MqttRpcTopicPair.cs +++ b/Source/MQTTnet.Extensions.Rpc/Options/MqttRpcTopicPair.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Extensions.Rpc.Options +// 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.Extensions.Rpc { public sealed class MqttRpcTopicPair { diff --git a/Source/MQTTnet.Extensions.Rpc/Options/TopicGeneration/DefaultMqttRpcClientTopicGenerationStrategy.cs b/Source/MQTTnet.Extensions.Rpc/Options/TopicGeneration/DefaultMqttRpcClientTopicGenerationStrategy.cs index 39bbde7..11c087d 100644 --- a/Source/MQTTnet.Extensions.Rpc/Options/TopicGeneration/DefaultMqttRpcClientTopicGenerationStrategy.cs +++ b/Source/MQTTnet.Extensions.Rpc/Options/TopicGeneration/DefaultMqttRpcClientTopicGenerationStrategy.cs @@ -1,6 +1,10 @@ -using System; +// 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.Extensions.Rpc.Options.TopicGeneration +using System; + +namespace MQTTnet.Extensions.Rpc { public sealed class DefaultMqttRpcClientTopicGenerationStrategy : IMqttRpcClientTopicGenerationStrategy { diff --git a/Source/MQTTnet.Extensions.Rpc/Options/TopicGeneration/IMqttRpcClientTopicGenerationStrategy.cs b/Source/MQTTnet.Extensions.Rpc/Options/TopicGeneration/IMqttRpcClientTopicGenerationStrategy.cs index 19c78d3..487daa1 100644 --- a/Source/MQTTnet.Extensions.Rpc/Options/TopicGeneration/IMqttRpcClientTopicGenerationStrategy.cs +++ b/Source/MQTTnet.Extensions.Rpc/Options/TopicGeneration/IMqttRpcClientTopicGenerationStrategy.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Extensions.Rpc.Options.TopicGeneration +// 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.Extensions.Rpc { public interface IMqttRpcClientTopicGenerationStrategy { diff --git a/Source/MQTTnet.Extensions.Rpc/Options/TopicGeneration/TopicGenerationContext.cs b/Source/MQTTnet.Extensions.Rpc/Options/TopicGeneration/TopicGenerationContext.cs index 39110bb..fe2001c 100644 --- a/Source/MQTTnet.Extensions.Rpc/Options/TopicGeneration/TopicGenerationContext.cs +++ b/Source/MQTTnet.Extensions.Rpc/Options/TopicGeneration/TopicGenerationContext.cs @@ -1,7 +1,11 @@ -using MQTTnet.Client; +// 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.Client; using MQTTnet.Protocol; -namespace MQTTnet.Extensions.Rpc.Options.TopicGeneration +namespace MQTTnet.Extensions.Rpc { public sealed class TopicGenerationContext { @@ -17,8 +21,8 @@ namespace MQTTnet.Extensions.Rpc.Options.TopicGeneration /// public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } - public IMqttClient MqttClient { get; set; } + public MqttClient MqttClient { get; set; } - public IMqttRpcClientOptions Options { get; set; } + public MqttRpcClientOptions Options { get; set; } } } diff --git a/Source/MQTTnet.Extensions.Rpc/RpcAwareApplicationMessageReceivedHandler.cs b/Source/MQTTnet.Extensions.Rpc/RpcAwareApplicationMessageReceivedHandler.cs deleted file mode 100644 index 5d43eb6..0000000 --- a/Source/MQTTnet.Extensions.Rpc/RpcAwareApplicationMessageReceivedHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Threading.Tasks; -using MQTTnet.Client.Receiving; - -namespace MQTTnet.Extensions.Rpc -{ - public sealed class RpcAwareApplicationMessageReceivedHandler : IMqttApplicationMessageReceivedHandler - { - readonly Func _handleReceivedApplicationMessageAsync; - - public RpcAwareApplicationMessageReceivedHandler( - IMqttApplicationMessageReceivedHandler originalHandler, - Func handleReceivedApplicationMessageAsync) - { - OriginalHandler = originalHandler; - _handleReceivedApplicationMessageAsync = handleReceivedApplicationMessageAsync ?? throw new ArgumentNullException(nameof(handleReceivedApplicationMessageAsync)); - } - - public IMqttApplicationMessageReceivedHandler OriginalHandler { get; } - - public async Task HandleApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs) - { - // First try to check if there is a pending RPC call! - await _handleReceivedApplicationMessageAsync(eventArgs).ConfigureAwait(false); - - if (OriginalHandler != null) - { - await OriginalHandler.HandleApplicationMessageReceivedAsync(eventArgs).ConfigureAwait(false); - } - } - } -} diff --git a/Source/MQTTnet.Extensions.WebSocket4Net/MQTTnet.Extensions.WebSocket4Net.csproj b/Source/MQTTnet.Extensions.WebSocket4Net/MQTTnet.Extensions.WebSocket4Net.csproj index 9a5cf52..e8d3d0b 100644 --- a/Source/MQTTnet.Extensions.WebSocket4Net/MQTTnet.Extensions.WebSocket4Net.csproj +++ b/Source/MQTTnet.Extensions.WebSocket4Net/MQTTnet.Extensions.WebSocket4Net.csproj @@ -1,44 +1,76 @@ - - netstandard1.3;netstandard2.0;netstandard2.1;net5.0 - $(TargetFrameworks);net452;net461 - $(TargetFrameworks);uap10.0 - - - - - False - false - false - true - true - snupkg - - - - false - UAP,Version=v10.0 - UAP - 10.0.18362.0 - 10.0.10240.0 - .NETCore - v5.0 - $(DefineConstants);WINDOWS_UWP - en - $(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets - - - - - - - - - - - - - + + netstandard1.3;netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0 + $(TargetFrameworks);net452;net461 + $(TargetFrameworks);uap10.0 + + MQTTnet.Extensions.WebSocket4Net + MQTTnet.Extensions.WebSocket4Net + True + The contributors of MQTTnet + MQTTnet + This is an extension library which allows using _WebSocket4Net_ as transport for MQTTnet clients. + The contributors of MQTTnet + MQTTnet.Extensions.WebSocket4Net + false + false + true + true + snupkg + Christian Kratky 2016-2022 + https://github.com/dotnet/MQTTnet + https://github.com/dotnet/MQTTnet.git + git + 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 + en-US + false + false + nuget.png + true + true + LICENSE + For release notes please go to MQTTnet release notes (https://www.nuget.org/packages/MQTTnet/). + true + + + + false + UAP,Version=v10.0 + UAP + 10.0.18362.0 + 10.0.10240.0 + .NETCore + v5.0 + $(DefineConstants);WINDOWS_UWP + en + $(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets + + + + + True + \ + + + + + + True + \ + + + + + + + + + + + + + + diff --git a/Source/MQTTnet.Extensions.WebSocket4Net/MqttFactoryExtensions.cs b/Source/MQTTnet.Extensions.WebSocket4Net/MqttFactoryExtensions.cs index 327074d..d23286f 100644 --- a/Source/MQTTnet.Extensions.WebSocket4Net/MqttFactoryExtensions.cs +++ b/Source/MQTTnet.Extensions.WebSocket4Net/MqttFactoryExtensions.cs @@ -1,14 +1,18 @@ -using System; +// 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.Extensions.WebSocket4Net { public static class MqttFactoryExtensions { - public static IMqttFactory UseWebSocket4Net(this IMqttFactory mqttFactory) + public static MqttFactory UseWebSocket4Net(this MqttFactory mqttFactory) { if (mqttFactory == null) throw new ArgumentNullException(nameof(mqttFactory)); - return mqttFactory.UseClientAdapterFactory(new WebSocket4NetMqttClientAdapterFactory(mqttFactory.DefaultLogger)); + return mqttFactory.UseClientAdapterFactory(new WebSocket4NetMqttClientAdapterFactory()); } } } diff --git a/Source/MQTTnet.Extensions.WebSocket4Net/WebSocket4NetMqttChannel.cs b/Source/MQTTnet.Extensions.WebSocket4Net/WebSocket4NetMqttChannel.cs index 930ab7e..a248be4 100644 --- a/Source/MQTTnet.Extensions.WebSocket4Net/WebSocket4NetMqttChannel.cs +++ b/Source/MQTTnet.Extensions.WebSocket4Net/WebSocket4NetMqttChannel.cs @@ -1,3 +1,7 @@ +// 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.Concurrent; using System.Collections.Generic; @@ -8,9 +12,8 @@ using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using MQTTnet.Channel; -using MQTTnet.Client.Options; +using MQTTnet.Client; using MQTTnet.Exceptions; -using MQTTnet.Internal; using SuperSocket.ClientEngine; using WebSocket4Net; @@ -20,12 +23,12 @@ namespace MQTTnet.Extensions.WebSocket4Net { readonly BlockingCollection _receiveBuffer = new BlockingCollection(); - readonly IMqttClientOptions _clientOptions; + readonly MqttClientOptions _clientOptions; readonly MqttClientWebSocketOptions _webSocketOptions; WebSocket _webSocket; - public WebSocket4NetMqttChannel(IMqttClientOptions clientOptions, MqttClientWebSocketOptions webSocketOptions) + public WebSocket4NetMqttChannel(MqttClientOptions clientOptions, MqttClientWebSocketOptions webSocketOptions) { _clientOptions = clientOptions ?? throw new ArgumentNullException(nameof(clientOptions)); _webSocketOptions = webSocketOptions ?? throw new ArgumentNullException(nameof(webSocketOptions)); @@ -210,25 +213,36 @@ namespace MQTTnet.Extensions.WebSocket4Net _webSocket.Open(); #pragma warning restore AsyncFixer02 // Long-running or blocking operations inside an async method - var exception = await MqttTaskTimeout.WaitAsync(c => - { - c.Register(() => taskCompletionSource.TrySetCanceled()); - return taskCompletionSource.Task; - }, _clientOptions.CommunicationTimeout, cancellationToken).ConfigureAwait(false); - - if (exception != null) + using (var timeoutCts = new CancellationTokenSource(_clientOptions.Timeout)) + using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken)) { - if (exception is AuthenticationException authenticationException) + using (linkedCts.Token.Register(() => taskCompletionSource.TrySetCanceled())) { - throw new MqttCommunicationException(authenticationException.InnerException); + try + { + await taskCompletionSource.Task.ConfigureAwait(false); + } + catch (Exception exception) + { + var timeoutReached = timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested; + if (timeoutReached) + { + throw new MqttCommunicationTimedOutException(exception); + } + + if (exception is AuthenticationException authenticationException) + { + throw new MqttCommunicationException(authenticationException.InnerException); + } + + if (exception is OperationCanceledException) + { + throw new MqttCommunicationTimedOutException(); + } + + throw new MqttCommunicationException(exception); + } } - - if (exception is OperationCanceledException) - { - throw new MqttCommunicationTimedOutException(); - } - - throw new MqttCommunicationException(exception); } } catch (OperationCanceledException) diff --git a/Source/MQTTnet.Extensions.WebSocket4Net/WebSocket4NetMqttClientAdapterFactory.cs b/Source/MQTTnet.Extensions.WebSocket4Net/WebSocket4NetMqttClientAdapterFactory.cs index e4e72cc..69ece62 100644 --- a/Source/MQTTnet.Extensions.WebSocket4Net/WebSocket4NetMqttClientAdapterFactory.cs +++ b/Source/MQTTnet.Extensions.WebSocket4Net/WebSocket4NetMqttClientAdapterFactory.cs @@ -1,23 +1,19 @@ -using MQTTnet.Adapter; -using MQTTnet.Client.Options; -using MQTTnet.Diagnostics; +// 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.Formatter; using MQTTnet.Implementations; using System; -using MQTTnet.Diagnostics.Logger; +using MQTTnet.Client; +using MQTTnet.Diagnostics; namespace MQTTnet.Extensions.WebSocket4Net { public sealed class WebSocket4NetMqttClientAdapterFactory : IMqttClientAdapterFactory { - readonly IMqttNetLogger _logger; - - public WebSocket4NetMqttClientAdapterFactory(IMqttNetLogger logger) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - public IMqttChannelAdapter CreateClientAdapter(IMqttClientOptions options) + public IMqttChannelAdapter CreateClientAdapter(MqttClientOptions options, MqttPacketInspector packetInspector, IMqttNetLogger logger) { if (options == null) throw new ArgumentNullException(nameof(options)); @@ -27,18 +23,18 @@ namespace MQTTnet.Extensions.WebSocket4Net { return new MqttChannelAdapter( new MqttTcpChannel(options), - new MqttPacketFormatterAdapter(options.ProtocolVersion), - options.PacketInspector, - _logger); + new MqttPacketFormatterAdapter(options.ProtocolVersion, new MqttBufferWriter(4096, 65535)), + packetInspector, + logger); } case MqttClientWebSocketOptions webSocketOptions: { return new MqttChannelAdapter( new WebSocket4NetMqttChannel(options, webSocketOptions), - new MqttPacketFormatterAdapter(options.ProtocolVersion), - options.PacketInspector, - _logger); + new MqttPacketFormatterAdapter(options.ProtocolVersion, new MqttBufferWriter(4068, 65535)), + packetInspector, + logger); } default: diff --git a/Tests/MQTTnet.TestApp.NetCore/ClientFlowTest.cs b/Source/MQTTnet.TestApp/ClientFlowTest.cs similarity index 80% rename from Tests/MQTTnet.TestApp.NetCore/ClientFlowTest.cs rename to Source/MQTTnet.TestApp/ClientFlowTest.cs index 8c0c6d9..457fc88 100644 --- a/Tests/MQTTnet.TestApp.NetCore/ClientFlowTest.cs +++ b/Source/MQTTnet.TestApp/ClientFlowTest.cs @@ -1,10 +1,13 @@ -using System; +// 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.Threading.Tasks; using MQTTnet.Client; -using MQTTnet.Client.Options; -using MQTTnet.Diagnostics.Logger; +using MQTTnet.Diagnostics; -namespace MQTTnet.TestApp.NetCore +namespace MQTTnet.TestApp { public static class ClientFlowTest { @@ -32,7 +35,7 @@ namespace MQTTnet.TestApp.NetCore Console.WriteLine("AFTER SUBSCRIBE"); Console.WriteLine("BEFORE PUBLISH"); - await client.PublishAsync("test/topic", "payload"); + await client.PublishStringAsync("test/topic", "payload"); Console.WriteLine("AFTER PUBLISH"); await Task.Delay(1000); diff --git a/Tests/MQTTnet.TestApp.NetCore/ClientTest.cs b/Source/MQTTnet.TestApp/ClientTest.cs similarity index 75% rename from Tests/MQTTnet.TestApp.NetCore/ClientTest.cs rename to Source/MQTTnet.TestApp/ClientTest.cs index 1f17740..33469ac 100644 --- a/Tests/MQTTnet.TestApp.NetCore/ClientTest.cs +++ b/Source/MQTTnet.TestApp/ClientTest.cs @@ -1,15 +1,16 @@ -using MQTTnet.Client; -using MQTTnet.Client.Connecting; -using MQTTnet.Client.Disconnecting; -using MQTTnet.Client.Options; -using MQTTnet.Client.Receiving; -using MQTTnet.Protocol; +// 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.Client; using System; using System.Text; using System.Threading.Tasks; -using MQTTnet.Diagnostics.Logger; +using MQTTnet.Diagnostics; +using MQTTnet.Implementations; +using MQTTnet.Protocol; -namespace MQTTnet.TestApp.NetCore +namespace MQTTnet.TestApp { public static class ClientTest { @@ -30,7 +31,7 @@ namespace MQTTnet.TestApp.NetCore } }; - client.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(e => + client.ApplicationMessageReceivedAsync += e => { Console.WriteLine("### RECEIVED APPLICATION MESSAGE ###"); Console.WriteLine($"+ Topic = {e.ApplicationMessage.Topic}"); @@ -38,18 +39,20 @@ namespace MQTTnet.TestApp.NetCore Console.WriteLine($"+ QoS = {e.ApplicationMessage.QualityOfServiceLevel}"); Console.WriteLine($"+ Retain = {e.ApplicationMessage.Retain}"); Console.WriteLine(); - }); + + return PlatformAbstractionLayer.CompletedTask; + }; - client.ConnectedHandler = new MqttClientConnectedHandlerDelegate(async e => + client.ConnectedAsync += async e => { Console.WriteLine("### CONNECTED WITH SERVER ###"); - await client.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("#").Build()); + await client.SubscribeAsync("#"); Console.WriteLine("### SUBSCRIBED ###"); - }); + }; - client.DisconnectedHandler = new MqttClientDisconnectedHandlerDelegate(async e => + client.DisconnectedAsync += async e => { Console.WriteLine("### DISCONNECTED FROM SERVER ###"); await Task.Delay(TimeSpan.FromSeconds(5)); @@ -62,7 +65,7 @@ namespace MQTTnet.TestApp.NetCore { Console.WriteLine("### RECONNECTING FAILED ###"); } - }); + }; try { @@ -79,12 +82,12 @@ namespace MQTTnet.TestApp.NetCore { Console.ReadLine(); - await client.SubscribeAsync(new MqttTopicFilter { Topic = "test", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }); + await client.SubscribeAsync("test"); var applicationMessage = new MqttApplicationMessageBuilder() .WithTopic("A/B/C") .WithPayload("Hello World") - .WithAtLeastOnceQoS() + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) .Build(); await client.PublishAsync(applicationMessage); diff --git a/Source/MQTTnet.TestApp/MQTTnet.TestApp.csproj b/Source/MQTTnet.TestApp/MQTTnet.TestApp.csproj new file mode 100644 index 0000000..d43a14f --- /dev/null +++ b/Source/MQTTnet.TestApp/MQTTnet.TestApp.csproj @@ -0,0 +1,32 @@ + + + + Exe + Full + net5.0;net6.0 + $(TargetFrameworks);net452;net461 + false + false + false + true + + + + + + + + + + + + + + + + + Always + + + + diff --git a/Tests/MQTTnet.TestApp.NetCore/ManagedClientTest.cs b/Source/MQTTnet.TestApp/ManagedClientTest.cs similarity index 62% rename from Tests/MQTTnet.TestApp.NetCore/ManagedClientTest.cs rename to Source/MQTTnet.TestApp/ManagedClientTest.cs index 663d6ec..88125ff 100644 --- a/Tests/MQTTnet.TestApp.NetCore/ManagedClientTest.cs +++ b/Source/MQTTnet.TestApp/ManagedClientTest.cs @@ -1,14 +1,18 @@ -using System; +// 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.Threading.Tasks; using System.IO; using Newtonsoft.Json; using System.Collections.Generic; -using MQTTnet.Client.Options; -using MQTTnet.Client.Receiving; +using MQTTnet.Client; using MQTTnet.Extensions.ManagedClient; +using MQTTnet.Implementations; using MQTTnet.Protocol; -namespace MQTTnet.TestApp.NetCore +namespace MQTTnet.TestApp { public static class ManagedClientTest { @@ -35,20 +39,21 @@ namespace MQTTnet.TestApp.NetCore try { var managedClient = new MqttFactory().CreateManagedMqttClient(); - managedClient.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(e => + managedClient.ApplicationMessageReceivedAsync += e => { Console.WriteLine(">> RECEIVED: " + e.ApplicationMessage.Topic); - }); + return PlatformAbstractionLayer.CompletedTask; + }; await managedClient.StartAsync(options); - await managedClient.PublishAsync(builder => builder.WithTopic("Step").WithPayload("1")); - await managedClient.PublishAsync(builder => builder.WithTopic("Step").WithPayload("2").WithAtLeastOnceQoS()); + await managedClient.EnqueueAsync(topic: "Step", payload: "1"); + await managedClient.EnqueueAsync(topic: "Step", payload: "2", MqttQualityOfServiceLevel.AtLeastOnce); - await managedClient.SubscribeAsync(new MqttTopicFilter { Topic = "xyz", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }); - await managedClient.SubscribeAsync(new MqttTopicFilter { Topic = "abc", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }); + await managedClient.SubscribeAsync(topic: "xyz", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtMostOnce); + await managedClient.SubscribeAsync(topic: "abc", qualityOfServiceLevel: MqttQualityOfServiceLevel.AtMostOnce); - await managedClient.PublishAsync(builder => builder.WithTopic("Step").WithPayload("3")); + await managedClient.EnqueueAsync(topic: "Step", payload: "3"); Console.WriteLine("Managed client started."); Console.ReadLine(); @@ -60,16 +65,22 @@ namespace MQTTnet.TestApp.NetCore } - public class RandomPassword : IMqttClientCredentials + public sealed class RandomPassword : IMqttClientCredentialsProvider { - public byte[] Password => Guid.NewGuid().ToByteArray(); + public string GetUserName(MqttClientOptions clientOptions) + { + return "the_static_user"; + } - public string Username => "the_static_user"; + public byte[] GetPassword(MqttClientOptions clientOptions) + { + return Guid.NewGuid().ToByteArray(); + } } public class ClientRetainedMessageHandler : IManagedMqttClientStorage { - private const string Filename = @"RetainedMessages.json"; + const string Filename = @"RetainedMessages.json"; public Task SaveQueuedMessagesAsync(IList messages) { diff --git a/Source/MQTTnet.TestApp/MessageThroughputTest.cs b/Source/MQTTnet.TestApp/MessageThroughputTest.cs new file mode 100644 index 0000000..311eaea --- /dev/null +++ b/Source/MQTTnet.TestApp/MessageThroughputTest.cs @@ -0,0 +1,344 @@ +using MQTTnet.Client; +using MQTTnet.Server; +using System.Collections.Generic; +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Linq; + +namespace MQTTnet.TestApp +{ + /// + /// Connect a number of publisher clients and one subscriber client, then publish messages and measure + /// the number of messages per second that can be exchanged between publishers and subscriber. + /// Measurements are performed for subscriptions containing no wildcard, a single wildcard or multiple wildcards. + /// + public class MessageThroughputTest + { + // Change these constants to suit + const int NumPublishers = 5000; + const int NumTopicsPerPublisher = 10; + + // Note: Other code changes may be required when changing this constant: + const int NumSubscribers = 1; // Fixed + + // Number of publish calls before a response for all published messages is awaited. + // This must be limited to a reasonable value that the server or TCP pipeline can handle. + const int NumPublishCallsPerBatch = 250; + + // Message counters are set/reset in the PublishAllAsync loop + int _messagesReceivedCount; + int _messagesExpectedCount; + + CancellationTokenSource _cancellationTokenSource; + + MqttServer _mqttServer; + Dictionary _mqttPublisherClientsByPublisherName; + List _mqttSubscriberClients; + + Dictionary> _topicsByPublisher; + Dictionary> _singleWildcardTopicsByPublisher; + Dictionary> _multiWildcardTopicsByPublisher; + + public async Task Run() + { + try + { + Console.WriteLine(); + Console.WriteLine("Begin message throughput test"); + Console.WriteLine(); + + Console.WriteLine("Number of publishers: " + NumPublishers); + Console.WriteLine("Number of published topics (total): " + NumPublishers * NumTopicsPerPublisher); + Console.WriteLine("Number of subscribers: " + NumSubscribers); + + await Setup(); + + await Subscribe_to_No_Wildcard_Topics(); + await Subscribe_to_Single_Wildcard_Topics(); + await Subscribe_to_Multi_Wildcard_Topics(); + + Console.WriteLine(); + Console.WriteLine("End message throughput test"); + } + catch (Exception ex) + { + ConsoleWriteLineError(ex.Message); + } + finally + { + await Cleanup(); + } + } + + public async Task Setup() + { + new TopicGenerator().Generate(NumPublishers, NumTopicsPerPublisher, out _topicsByPublisher, out _singleWildcardTopicsByPublisher, out _multiWildcardTopicsByPublisher); + + var serverOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build(); + var factory = new MqttFactory(); + _mqttServer = factory.CreateMqttServer(serverOptions); + await _mqttServer.StartAsync(); + + Console.WriteLine(); + Console.WriteLine("Begin connect " + NumPublishers + " publisher(s)..."); + + var stopWatch = new System.Diagnostics.Stopwatch(); + stopWatch.Start(); + _mqttPublisherClientsByPublisherName = new Dictionary(); + foreach (var pt in _topicsByPublisher) + { + var publisherName = pt.Key; + var mqttClient = factory.CreateMqttClient(); + var publisherOptions = new MqttClientOptionsBuilder() + .WithTcpServer("localhost") + .WithClientId(publisherName) + .Build(); + await mqttClient.ConnectAsync(publisherOptions); + _mqttPublisherClientsByPublisherName.Add(publisherName, mqttClient); + } + stopWatch.Stop(); + + Console.Write(string.Format("{0} publisher(s) connected in {1:0.000} seconds, ", NumPublishers, stopWatch.ElapsedMilliseconds / 1000.0)); + Console.WriteLine(string.Format("connections per second: {0:0.000}", NumPublishers / (stopWatch.ElapsedMilliseconds / 1000.0))); + + _mqttSubscriberClients = new List(); + for (var i = 0; i < NumSubscribers; ++i) + { + var mqttClient = factory.CreateMqttClient(); + var subsriberOptions = new MqttClientOptionsBuilder() + .WithTcpServer("localhost") + .WithClientId("sub" + i) + .Build(); + await mqttClient.ConnectAsync(subsriberOptions); + mqttClient.ApplicationMessageReceivedAsync += HandleApplicationMessageReceivedAsync; + _mqttSubscriberClients.Add(mqttClient); + } + } + + Task HandleApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs) + { + // count messages and signal cancellation when expected message count is reached + ++_messagesReceivedCount; + if (_messagesReceivedCount == _messagesExpectedCount) + { + _cancellationTokenSource.Cancel(); + } + return Implementations.PlatformAbstractionLayer.CompletedTask; + } + + public async Task Cleanup() + { + foreach (var mqttClient in _mqttSubscriberClients) + { + await mqttClient.DisconnectAsync(); + mqttClient.ApplicationMessageReceivedAsync -= HandleApplicationMessageReceivedAsync; + mqttClient.Dispose(); + } + + foreach (var pub in _mqttPublisherClientsByPublisherName) + { + var mqttClient = pub.Value; + await mqttClient.DisconnectAsync(); + mqttClient.Dispose(); + } + + await _mqttServer.StopAsync(); + _mqttServer.Dispose(); + } + + + /// + /// Measure no-wildcard topic subscription message exchange performance + /// + public Task Subscribe_to_No_Wildcard_Topics() + { + return ProcessMessages(_topicsByPublisher, "no wildcards"); + } + + /// + /// Measure single-wildcard topic subscription message exchange performance + /// + public Task Subscribe_to_Single_Wildcard_Topics() + { + return ProcessMessages(_singleWildcardTopicsByPublisher, "single wildcard"); + } + + /// + /// Measure multi-wildcard topic subscription message exchange performance + /// + public Task Subscribe_to_Multi_Wildcard_Topics() + { + return ProcessMessages(_multiWildcardTopicsByPublisher, "multi wildcard"); + } + + + /// + /// Subcribe to all topics, then run message exchange + /// + public async Task ProcessMessages(Dictionary> topicsByPublisher, string topicTypeDescription) + { + var numTopics = CountTopics(topicsByPublisher); + + Console.WriteLine(); + Console.Write(string.Format("Subscribing to {0} topics ", numTopics)); + ConsoleWriteInfo(string.Format("({0})", topicTypeDescription)); + Console.WriteLine(string.Format(" for {0} subscriber(s)...", NumSubscribers)); + + var stopWatch = new System.Diagnostics.Stopwatch(); + stopWatch.Start(); + + // each subscriber subscribes to all topics, one call per topic + foreach (var subscriber in _mqttSubscriberClients) + { + foreach (var tp in topicsByPublisher) + { + var topics = tp.Value; + foreach (var topic in topics) + { + var topicFilter = new Packets.MqttTopicFilter() { Topic = topic }; + await subscriber.SubscribeAsync(topicFilter).ConfigureAwait(false); + } + } + } + + stopWatch.Stop(); + + Console.Write(string.Format("{0} subscriber(s) subscribed in {1:0.000} seconds, ", NumSubscribers, stopWatch.ElapsedMilliseconds / 1000.0)); + Console.WriteLine(string.Format("subscribe calls per second: {0:0.000}", numTopics / (stopWatch.ElapsedMilliseconds / 1000.0))); + + await PublishAllAsync(); + + Console.WriteLine(string.Format("Unsubscribing {0} topics ({1}) for {2} subscriber(s)...", numTopics, topicTypeDescription, NumSubscribers)); + + stopWatch.Restart(); + + // unsubscribe to all topics, one call per topic + foreach (var subscriber in _mqttSubscriberClients) + { + foreach (var tp in topicsByPublisher) + { + var topics = tp.Value; + foreach (var topic in topics) + { + var topicFilter = new Packets.MqttTopicFilter() { Topic = topic }; + + MqttClientUnsubscribeOptions options = + new MqttClientUnsubscribeOptionsBuilder() + .WithTopicFilter(topicFilter) + .Build(); + await subscriber.UnsubscribeAsync(options).ConfigureAwait(false); + } + } + } + + stopWatch.Stop(); + + Console.Write(string.Format("{0} subscriber(s) unsubscribed in {1:0.000} seconds, ", NumSubscribers, stopWatch.ElapsedMilliseconds / 1000.0)); + Console.WriteLine(string.Format("unsubscribe calls per second: {0:0.000}", numTopics / (stopWatch.ElapsedMilliseconds / 1000.0))); + } + + + /// + /// Publish messages in batches of NumPublishCallsPerBatch, wait for messages sent to subscriber + /// + async Task PublishAllAsync() + { + Console.WriteLine("Begin message exchange..."); + + int publisherIndexCounter = 0; // index to loop around all publishers to publish + int topicIndexCounter = 0; // index to loop around all topics to publish + int totalNumMessagesReceived = 0; + + var publisherNames = _topicsByPublisher.Keys.ToList(); + + // There should be one message received per publish + _messagesExpectedCount = NumPublishCallsPerBatch; + + var stopWatch = new System.Diagnostics.Stopwatch(); + stopWatch.Start(); + + // Loop for a while and exchange messages + + while (stopWatch.ElapsedMilliseconds < 10000) + { + _messagesReceivedCount = 0; + + _cancellationTokenSource = new CancellationTokenSource(); + + for (var publishCallCount = 0; publishCallCount < NumPublishCallsPerBatch; ++publishCallCount) + { + // pick a publisher + var publisherName = publisherNames[publisherIndexCounter % publisherNames.Count]; + var publisherTopics = _topicsByPublisher[publisherName]; + // pick a publisher topic + var topic = publisherTopics[topicIndexCounter % publisherTopics.Count]; + var message = new MqttApplicationMessageBuilder() + .WithTopic(topic) + .Build(); + await _mqttPublisherClientsByPublisherName[publisherName].PublishAsync(message).ConfigureAwait(false); + ++topicIndexCounter; + ++publisherIndexCounter; + } + + // Wait for at least one message per publish to be received by subscriber (in the subscriber's application message handler), + // then loop around to send another batch + try + { + await Task.Delay(30000, _cancellationTokenSource.Token).ConfigureAwait(false); + } + catch + { + + } + + _cancellationTokenSource.Dispose(); + + if (_messagesReceivedCount < _messagesExpectedCount) + { + ConsoleWriteLineError(string.Format("Messages Received Count mismatch, expected {0}, received {1}", _messagesExpectedCount, _messagesReceivedCount)); + return; + } + + totalNumMessagesReceived += _messagesReceivedCount; + } + + stopWatch.Stop(); + + System.Console.Write(string.Format("{0} messages published and received in {1:0.000} seconds, ", totalNumMessagesReceived, stopWatch.ElapsedMilliseconds / 1000.0)); + ConsoleWriteLineSuccess(string.Format("messages per second: {0}", (int)(totalNumMessagesReceived / (stopWatch.ElapsedMilliseconds / 1000.0)))); + } + + int CountTopics(Dictionary> topicsByPublisher) + { + var count = 0; + foreach (var tp in topicsByPublisher) + { + count += tp.Value.Count; + } + return count; + } + + void ConsoleWriteLineError(string message) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(message); + Console.ResetColor(); + } + + void ConsoleWriteLineSuccess(string message) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine(message); + Console.ResetColor(); + } + + void ConsoleWriteInfo(string message) + { + Console.ForegroundColor = ConsoleColor.White; + Console.Write(message); + Console.ResetColor(); + } + + } +} diff --git a/Tests/MQTTnet.TestApp.NetCore/MqttNetConsoleLogger.cs b/Source/MQTTnet.TestApp/MqttNetConsoleLogger.cs similarity index 87% rename from Tests/MQTTnet.TestApp.NetCore/MqttNetConsoleLogger.cs rename to Source/MQTTnet.TestApp/MqttNetConsoleLogger.cs index 1c1d6ed..040e2c7 100644 --- a/Tests/MQTTnet.TestApp.NetCore/MqttNetConsoleLogger.cs +++ b/Source/MQTTnet.TestApp/MqttNetConsoleLogger.cs @@ -1,8 +1,12 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; using System.Text; -using MQTTnet.Diagnostics.Logger; +using MQTTnet.Diagnostics; -namespace MQTTnet.TestApp.NetCore +namespace MQTTnet.TestApp { public static class MqttNetConsoleLogger { diff --git a/Tests/MQTTnet.TestApp.NetCore/PerformanceTest.cs b/Source/MQTTnet.TestApp/PerformanceTest.cs similarity index 93% rename from Tests/MQTTnet.TestApp.NetCore/PerformanceTest.cs rename to Source/MQTTnet.TestApp/PerformanceTest.cs index b41d199..e4189b2 100644 --- a/Tests/MQTTnet.TestApp.NetCore/PerformanceTest.cs +++ b/Source/MQTTnet.TestApp/PerformanceTest.cs @@ -1,15 +1,19 @@ -using System; +// 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.Diagnostics; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using MQTTnet.Client; -using MQTTnet.Client.Options; using MQTTnet.Protocol; using MQTTnet.Server; +using MqttClient = MQTTnet.Client.MqttClient; -namespace MQTTnet.TestApp.NetCore +namespace MQTTnet.TestApp { public static class PerformanceTest { @@ -58,22 +62,25 @@ namespace MQTTnet.TestApp.NetCore { try { - var mqttServer = new MqttFactory().CreateMqttServer(); - await mqttServer.StartAsync(new MqttServerOptions()).ConfigureAwait(false); + var mqttFactory = new MqttFactory(); + var mqttServerOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build(); + var mqttServer = mqttFactory.CreateMqttServer(mqttServerOptions); + await mqttServer.StartAsync().ConfigureAwait(false); var options = new MqttClientOptions { ChannelOptions = new MqttClientTcpOptions { Server = "127.0.0.1" - }, - CleanSession = true + } }; - var client = new MqttFactory().CreateMqttClient(); + var client = mqttFactory.CreateMqttClient(); await client.ConnectAsync(options).ConfigureAwait(false); - var message = CreateMessage(); + var message = new MqttApplicationMessageBuilder().WithTopic("t") + .Build(); + var stopwatch = new Stopwatch(); for (var i = 0; i < 10; i++) @@ -96,12 +103,12 @@ namespace MQTTnet.TestApp.NetCore } } - private static Task RunClientsAsync(int msgChunkSize, TimeSpan interval, bool concurrent) + static Task RunClientsAsync(int msgChunkSize, TimeSpan interval, bool concurrent) { return Task.WhenAll(Enumerable.Range(0, 3).Select(i => Task.Run(() => RunClientAsync(msgChunkSize, interval, concurrent)))); } - private static async Task RunClientAsync(int msgChunkSize, TimeSpan interval, bool concurrent) + static async Task RunClientAsync(int msgChunkSize, TimeSpan interval, bool concurrent) { try { @@ -109,8 +116,7 @@ namespace MQTTnet.TestApp.NetCore { ChannelOptions = new MqttClientTcpOptions { Server = "localhost" }, ClientId = "Client1", - CleanSession = true, - CommunicationTimeout = TimeSpan.FromMinutes(10) + CleanSession = true }; var client = new MqttFactory().CreateMqttClient(); @@ -156,9 +162,12 @@ namespace MQTTnet.TestApp.NetCore } else { - await client.PublishAsync(msgs); - msgCount += msgs.Count; - //send multiple + foreach (var msg in msgs) + { + await client.PublishAsync(msg); + msgCount += msgs.Count; + //send multiple + } } var now = DateTime.Now; @@ -178,7 +187,7 @@ namespace MQTTnet.TestApp.NetCore } } - private static MqttApplicationMessage CreateMessage() + static MqttApplicationMessage CreateMessage() { //const string Payload = "###############################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################" //const string Payload = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; @@ -192,7 +201,7 @@ namespace MQTTnet.TestApp.NetCore }; } - private static Task PublishSingleMessage(IMqttClient client, MqttApplicationMessage applicationMessage, ref int count) + static Task PublishSingleMessage(MqttClient client, MqttApplicationMessage applicationMessage, ref int count) { Interlocked.Increment(ref count); return Task.Run(() => client.PublishAsync(applicationMessage)); @@ -202,8 +211,8 @@ namespace MQTTnet.TestApp.NetCore { try { - var mqttServer = new MqttFactory().CreateMqttServer(); - await mqttServer.StartAsync(new MqttServerOptions()); + var mqttServer = new MqttFactory().CreateMqttServer(new MqttServerOptions()); + await mqttServer.StartAsync(); var options = new MqttClientOptions { @@ -253,8 +262,8 @@ namespace MQTTnet.TestApp.NetCore { try { - var mqttServer = new MqttFactory().CreateMqttServer(); - await mqttServer.StartAsync(new MqttServerOptions()); + var mqttServer = new MqttFactory().CreateMqttServer(new MqttServerOptions()); + await mqttServer.StartAsync(); var options = new MqttClientOptions { diff --git a/Source/MQTTnet.TestApp/Program.cs b/Source/MQTTnet.TestApp/Program.cs new file mode 100644 index 0000000..f1a3258 --- /dev/null +++ b/Source/MQTTnet.TestApp/Program.cs @@ -0,0 +1,95 @@ +// 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; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MQTTnet.TestApp +{ + public static class Program + { + public static void Main() + { + Console.WriteLine($"MQTTnet - TestApp.{TargetFrameworkProvider.TargetFramework}"); + Console.WriteLine("1 = Start client"); + Console.WriteLine("2 = Start server"); + Console.WriteLine("3 = Start performance test"); + Console.WriteLine("4 = Start managed client"); + Console.WriteLine("5 = Start public broker test"); + Console.WriteLine("6 = Start server & client"); + Console.WriteLine("7 = Client flow test"); + Console.WriteLine("8 = Start performance test (client only)"); + Console.WriteLine("9 = Start server (no trace)"); + Console.WriteLine("a = Start QoS 2 benchmark"); + Console.WriteLine("b = Start QoS 1 benchmark"); + Console.WriteLine("c = Start QoS 0 benchmark"); + Console.WriteLine("d = Start server with logging"); + Console.WriteLine("e = Start Message Throughput Test"); + + var pressedKey = Console.ReadKey(true); + if (pressedKey.KeyChar == '1') + { + Task.Run(ClientTest.RunAsync); + } + else if (pressedKey.KeyChar == '2') + { + Task.Run(ServerTest.RunAsync); + } + else if (pressedKey.KeyChar == '3') + { + Task.Run(PerformanceTest.RunClientAndServer); + } + else if (pressedKey.KeyChar == '4') + { + Task.Run(ManagedClientTest.RunAsync); + } + else if (pressedKey.KeyChar == '5') + { + Task.Run(PublicBrokerTest.RunAsync); + } + else if (pressedKey.KeyChar == '6') + { + Task.Run(ServerAndClientTest.RunAsync); + } + else if (pressedKey.KeyChar == '7') + { + Task.Run(ClientFlowTest.RunAsync); + } + else if (pressedKey.KeyChar == '8') + { + PerformanceTest.RunClientOnly(); + return; + } + else if (pressedKey.KeyChar == '9') + { + ServerTest.RunEmptyServer(); + return; + } + else if (pressedKey.KeyChar == 'a') + { + Task.Run(PerformanceTest.RunQoS2Test); + } + else if (pressedKey.KeyChar == 'b') + { + Task.Run(PerformanceTest.RunQoS1Test); + } + else if (pressedKey.KeyChar == 'c') + { + Task.Run(PerformanceTest.RunQoS0Test); + } + else if (pressedKey.KeyChar == 'd') + { + Task.Run(ServerTest.RunEmptyServerWithLogging); + } + else if (pressedKey.KeyChar == 'e') + { + Task.Run(new MessageThroughputTest().Run); + } + + Thread.Sleep(Timeout.Infinite); + } + } +} \ No newline at end of file diff --git a/Tests/MQTTnet.TestApp.NetCore/PublicBrokerTest.cs b/Source/MQTTnet.TestApp/PublicBrokerTest.cs similarity index 89% rename from Tests/MQTTnet.TestApp.NetCore/PublicBrokerTest.cs rename to Source/MQTTnet.TestApp/PublicBrokerTest.cs index bb55f30..65aaa31 100644 --- a/Tests/MQTTnet.TestApp.NetCore/PublicBrokerTest.cs +++ b/Source/MQTTnet.TestApp/PublicBrokerTest.cs @@ -1,16 +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.Client; using System; -using System.IO; -using System.Net; using System.Security.Authentication; using System.Threading; using System.Threading.Tasks; -using MQTTnet.Client.Options; -using MQTTnet.Client.Receiving; using MQTTnet.Formatter; +using MQTTnet.Implementations; using MQTTnet.Protocol; -namespace MQTTnet.TestApp.NetCore +namespace MQTTnet.TestApp { public static class PublicBrokerTest { @@ -27,7 +28,7 @@ namespace MQTTnet.TestApp.NetCore UseTls = true, SslProtocol = SslProtocols.Tls13, // Don't use this in production code. This handler simply allows any invalid certificate to work. - CertificateValidationHandler = (w) => true + CertificateValidationHandler = w => true }; #endif @@ -37,7 +38,7 @@ namespace MQTTnet.TestApp.NetCore UseTls = true, SslProtocol = SslProtocols.Tls12, // Don't use this in production code. This handler simply allows any invalid certificate to work. - CertificateValidationHandler = (w) => true + CertificateValidationHandler = w => true }; // mqtt.eclipseprojects.io @@ -129,7 +130,7 @@ namespace MQTTnet.TestApp.NetCore Console.ReadLine(); } - private static async Task ExecuteTestAsync(string name, IMqttClientOptions options) + static async Task ExecuteTestAsync(string name, MqttClientOptions options) { try { @@ -140,11 +141,15 @@ namespace MQTTnet.TestApp.NetCore var topic = Guid.NewGuid().ToString(); MqttApplicationMessage receivedMessage = null; - client.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(e => receivedMessage = e.ApplicationMessage); + client.ApplicationMessageReceivedAsync += e => + { + receivedMessage = e.ApplicationMessage; + return PlatformAbstractionLayer.CompletedTask; + }; await client.ConnectAsync(options); await client.SubscribeAsync(topic, MqttQualityOfServiceLevel.AtLeastOnce); - await client.PublishAsync(topic, "Hello_World", MqttQualityOfServiceLevel.AtLeastOnce); + await client.PublishStringAsync(topic, "Hello_World", MqttQualityOfServiceLevel.AtLeastOnce); SpinWait.SpinUntil(() => receivedMessage != null, 5000); @@ -164,7 +169,7 @@ namespace MQTTnet.TestApp.NetCore } } - private static void Write(string message, ConsoleColor color) + static void Write(string message, ConsoleColor color) { Console.ForegroundColor = color; Console.Write(message); diff --git a/Tests/MQTTnet.TestApp.NetCore/ServerAndClientTest.cs b/Source/MQTTnet.TestApp/ServerAndClientTest.cs similarity index 60% rename from Tests/MQTTnet.TestApp.NetCore/ServerAndClientTest.cs rename to Source/MQTTnet.TestApp/ServerAndClientTest.cs index 2898639..062160c 100644 --- a/Tests/MQTTnet.TestApp.NetCore/ServerAndClientTest.cs +++ b/Source/MQTTnet.TestApp/ServerAndClientTest.cs @@ -1,11 +1,14 @@ -using System.Threading; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading; using System.Threading.Tasks; using MQTTnet.Client; -using MQTTnet.Client.Options; -using MQTTnet.Diagnostics.Logger; +using MQTTnet.Diagnostics; using MQTTnet.Server; -namespace MQTTnet.TestApp.NetCore +namespace MQTTnet.TestApp { public static class ServerAndClientTest { @@ -15,11 +18,10 @@ namespace MQTTnet.TestApp.NetCore MqttNetConsoleLogger.ForwardToConsole(logger); var factory = new MqttFactory(logger); - var server = factory.CreateMqttServer(); + var server = factory.CreateMqttServer( new MqttServerOptionsBuilder().Build()); var client = factory.CreateMqttClient(); - var serverOptions = new MqttServerOptionsBuilder().Build(); - await server.StartAsync(serverOptions); + await server.StartAsync(); var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("localhost").Build(); await client.ConnectAsync(clientOptions); diff --git a/Source/MQTTnet.TestApp/ServerTest.cs b/Source/MQTTnet.TestApp/ServerTest.cs new file mode 100644 index 0000000..341b3dd --- /dev/null +++ b/Source/MQTTnet.TestApp/ServerTest.cs @@ -0,0 +1,198 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using MQTTnet.Diagnostics; +using MQTTnet.Extensions.ManagedClient; +using MQTTnet.Implementations; +using MQTTnet.Protocol; +using MQTTnet.Server; +using Newtonsoft.Json; + +namespace MQTTnet.TestApp +{ + public static class ServerTest + { + public static void RunEmptyServer() + { + var mqttServer = new MqttFactory().CreateMqttServer(new MqttServerOptions()); + mqttServer.StartAsync().GetAwaiter().GetResult(); + + Console.WriteLine("Press any key to exit."); + Console.ReadLine(); + } + + public static void RunEmptyServerWithLogging() + { + var logger = new MqttNetEventLogger(); + MqttNetConsoleLogger.ForwardToConsole(logger); + + var mqttFactory = new MqttFactory(logger); + var mqttServer = mqttFactory.CreateMqttServer(new MqttServerOptions()); + mqttServer.StartAsync().GetAwaiter().GetResult(); + + Console.WriteLine("Press any key to exit."); + Console.ReadLine(); + } + + public static async Task RunAsync() + { + try + { + var options = new MqttServerOptions(); + + // Extend the timestamp for all messages from clients. + // Protect several topics from being subscribed from every client. + + //var certificate = new X509Certificate(@"C:\certs\test\test.cer", ""); + //options.TlsEndpointOptions.Certificate = certificate.Export(X509ContentType.Cert); + //options.ConnectionBacklog = 5; + //options.DefaultEndpointOptions.IsEnabled = true; + //options.TlsEndpointOptions.IsEnabled = false; + + var mqttServer = new MqttFactory().CreateMqttServer(options); + + const string Filename = "C:\\MQTT\\RetainedMessages.json"; + + mqttServer.RetainedMessageChangedAsync += e => + { + var directory = Path.GetDirectoryName(Filename); + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + File.WriteAllText(Filename, JsonConvert.SerializeObject(e.StoredRetainedMessages)); + return Task.FromResult(0); + }; + + mqttServer.RetainedMessagesClearedAsync += e => + { + File.Delete(Filename); + return Task.FromResult(0); + }; + + mqttServer.LoadingRetainedMessageAsync += e => + { + List retainedMessages; + if (File.Exists(Filename)) + { + var json = File.ReadAllText(Filename); + retainedMessages = JsonConvert.DeserializeObject>(json); + } + else + { + retainedMessages = new List(); + } + + e.LoadedRetainedMessages = retainedMessages; + + return Task.FromResult(0); + }; + + mqttServer.InterceptingPublishAsync += e => + { + if (MqttTopicFilterComparer.Compare(e.ApplicationMessage.Topic, "/myTopic/WithTimestamp/#") == MqttTopicFilterCompareResult.IsMatch) + { + // Replace the payload with the timestamp. But also extending a JSON + // based payload with the timestamp is a suitable use case. + e.ApplicationMessage.Payload = Encoding.UTF8.GetBytes(DateTime.Now.ToString("O")); + } + + if (e.ApplicationMessage.Topic == "not_allowed_topic") + { + e.ProcessPublish = false; + e.CloseConnection = true; + } + + return PlatformAbstractionLayer.CompletedTask; + }; + + mqttServer.ValidatingConnectionAsync += e => + { + if (e.ClientId == "SpecialClient") + { + if (e.Username != "USER" || e.Password != "PASS") + { + e.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword; + } + } + + return PlatformAbstractionLayer.CompletedTask; + }; + + mqttServer.InterceptingSubscriptionAsync += e => + { + if (e.TopicFilter.Topic.StartsWith("admin/foo/bar") && e.ClientId != "theAdmin") + { + e.Response.ReasonCode = MqttSubscribeReasonCode.ImplementationSpecificError; + } + + if (e.TopicFilter.Topic.StartsWith("the/secret/stuff") && e.ClientId != "Imperator") + { + e.Response.ReasonCode = MqttSubscribeReasonCode.ImplementationSpecificError; + e.CloseConnection = true; + } + + return PlatformAbstractionLayer.CompletedTask; + }; + + mqttServer.InterceptingPublishAsync += e => + { + MqttNetConsoleLogger.PrintToConsole( + $"'{e.ClientId}' reported '{e.ApplicationMessage.Topic}' > '{Encoding.UTF8.GetString(e.ApplicationMessage.Payload ?? new byte[0])}'", + ConsoleColor.Magenta); + + return PlatformAbstractionLayer.CompletedTask; + }; + + //options.ApplicationMessageInterceptor = c => + //{ + // if (c.ApplicationMessage.Payload == null || c.ApplicationMessage.Payload.Length == 0) + // { + // return; + // } + + // try + // { + // var content = JObject.Parse(Encoding.UTF8.GetString(c.ApplicationMessage.Payload)); + // var timestampProperty = content.Property("timestamp"); + // if (timestampProperty != null && timestampProperty.Value.Type == JTokenType.Null) + // { + // timestampProperty.Value = DateTime.Now.ToString("O"); + // c.ApplicationMessage.Payload = Encoding.UTF8.GetBytes(content.ToString()); + // } + // } + // catch (Exception) + // { + // } + //}; + + mqttServer.ClientConnectedAsync += e => + { + Console.Write("Client disconnected event fired."); + return PlatformAbstractionLayer.CompletedTask; + }; + + await mqttServer.StartAsync(); + + Console.WriteLine("Press any key to exit."); + Console.ReadLine(); + + await mqttServer.StopAsync(); + } + catch (Exception e) + { + Console.WriteLine(e); + } + + Console.ReadLine(); + } + } +} diff --git a/Tests/MQTTnet.TestApp.NetCore/Start.bat b/Source/MQTTnet.TestApp/Start.bat similarity index 100% rename from Tests/MQTTnet.TestApp.NetCore/Start.bat rename to Source/MQTTnet.TestApp/Start.bat diff --git a/Source/MQTTnet.TestApp/TopicGenerator.cs b/Source/MQTTnet.TestApp/TopicGenerator.cs new file mode 100644 index 0000000..495c18e --- /dev/null +++ b/Source/MQTTnet.TestApp/TopicGenerator.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MQTTnet.TestApp +{ + public class TopicGenerator + { + public void Generate( + int numPublishers, int numTopicsPerPublisher, + out Dictionary> topicsByPublisher, + out Dictionary> singleWildcardTopicsByPublisher, + out Dictionary> multiWildcardTopicsByPublisher + ) + { + topicsByPublisher = new Dictionary>(); + singleWildcardTopicsByPublisher = new Dictionary>(); + multiWildcardTopicsByPublisher = new Dictionary>(); + + // Find some reasonable distribution across three topic levels + var topicsPerLevel = (int)Math.Pow(numTopicsPerPublisher, (1.0 / 3.0)); + + int numLevel1Topics = topicsPerLevel; + if (numLevel1Topics <= 0) + { + numLevel1Topics = 1; + } + int numLevel2Topics = topicsPerLevel; + if (numLevel2Topics <= 0) + { + numLevel2Topics = 1; + } + var maxNumLevel3Topics = 1 + (int)((double)numTopicsPerPublisher / numLevel1Topics / numLevel2Topics); + if (maxNumLevel3Topics <= 0) + { + maxNumLevel3Topics = 1; + } + for (var p = 0; p < numPublishers; ++p) + { + int publisherTopicCount = 0; + + var publisherName = "pub" + p; + for (var l1 = 0; l1 < numLevel1Topics; ++l1) + { + for (var l2 = 0; l2 < numLevel2Topics; ++l2) + { + for (var l3 = 0; l3 < maxNumLevel3Topics; ++l3) + { + if (publisherTopicCount >= numTopicsPerPublisher) + break; + + var topic = string.Format("{0}/building{1}/level{2}/sensor{3}", publisherName, l1 + 1, l2 + 1, l3 + 1); + AddPublisherTopic(publisherName, topic, topicsByPublisher); + + if (l2 == 0) + { + var singleWildcardTopic = string.Format("{0}/building{1}/+/sensor{2}", publisherName, l1 + 1, l3 + 1); + AddPublisherTopic(publisherName, singleWildcardTopic, singleWildcardTopicsByPublisher); + if (l1 == 0) + { + var multiWildcardTopic = string.Format("{0}/+/level{1}/+", publisherName, l3 + 1); + AddPublisherTopic(publisherName, multiWildcardTopic, multiWildcardTopicsByPublisher); + } + } + + ++publisherTopicCount; + } + } + } + } + } + + void AddPublisherTopic(string publisherName, string topic, Dictionary> topicsByPublisher) + { + List topicList; + if (!topicsByPublisher.TryGetValue(publisherName, out topicList)) + { + topicList = new List(); + topicsByPublisher.Add(publisherName, topicList); + } + topicList.Add(topic); + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/BaseTestClass.cs b/Source/MQTTnet.Tests/BaseTestClass.cs similarity index 73% rename from Tests/MQTTnet.Core.Tests/BaseTestClass.cs rename to Source/MQTTnet.Tests/BaseTestClass.cs index 80cfe24..8e5248e 100644 --- a/Tests/MQTTnet.Core.Tests/BaseTestClass.cs +++ b/Source/MQTTnet.Tests/BaseTestClass.cs @@ -1,4 +1,8 @@ -using System; +// 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.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Formatter; diff --git a/Source/MQTTnet.Tests/Client/LowLevelMqttClient_Tests.cs b/Source/MQTTnet.Tests/Client/LowLevelMqttClient_Tests.cs new file mode 100644 index 0000000..6534bf3 --- /dev/null +++ b/Source/MQTTnet.Tests/Client/LowLevelMqttClient_Tests.cs @@ -0,0 +1,216 @@ +// 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.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Client; +using MQTTnet.Exceptions; +using MQTTnet.LowLevelClient; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Tests.Client +{ + [TestClass] + public sealed class LowLevelMqttClient_Tests : BaseTestClass + { + [TestMethod] + public async Task Authenticate() + { + using (var testEnvironment = CreateTestEnvironment()) + { + await testEnvironment.StartServer(); + + var factory = new MqttFactory(); + var lowLevelClient = factory.CreateLowLevelMqttClient(); + + await lowLevelClient.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build(), CancellationToken.None); + + var receivedPacket = await Authenticate(lowLevelClient).ConfigureAwait(false); + + await lowLevelClient.DisconnectAsync(CancellationToken.None).ConfigureAwait(false); + + Assert.IsNotNull(receivedPacket); + Assert.AreEqual(MqttConnectReturnCode.ConnectionAccepted, receivedPacket.ReturnCode); + } + } + + [TestMethod] + public async Task Connect_And_Disconnect() + { + using (var testEnvironment = CreateTestEnvironment()) + { + await testEnvironment.StartServer(); + + var lowLevelClient = testEnvironment.Factory.CreateLowLevelMqttClient(); + + await lowLevelClient.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build(), CancellationToken.None); + + await lowLevelClient.DisconnectAsync(CancellationToken.None); + } + } + + [TestMethod] + [ExpectedException(typeof(MqttCommunicationException))] + public async Task Connect_To_Not_Existing_Broker() + { + var client = new MqttFactory().CreateLowLevelMqttClient(); + var options = new MqttClientOptionsBuilder().WithTcpServer("localhost").Build(); + + await client.ConnectAsync(options, CancellationToken.None).ConfigureAwait(false); + } + + [TestMethod] + [ExpectedException(typeof(MqttCommunicationException))] + public async Task Connect_To_Wrong_Host() + { + var client = new MqttFactory().CreateLowLevelMqttClient(); + var options = new MqttClientOptionsBuilder().WithTcpServer("123.456.789.10").Build(); + + await client.ConnectAsync(options, CancellationToken.None).ConfigureAwait(false); + } + + [TestMethod] + public async Task Loose_Connection() + { + using (var testEnvironment = CreateTestEnvironment()) + { + testEnvironment.ServerPort = 8364; + var server = await testEnvironment.StartServer(); + + var client = await testEnvironment.ConnectLowLevelClient(o => o.WithTimeout(TimeSpan.Zero)); + + await Authenticate(client).ConfigureAwait(false); + + await server.StopAsync(); + + await Task.Delay(2000); + + try + { + await client.SendAsync(MqttPingReqPacket.Instance, CancellationToken.None).ConfigureAwait(false); + await Task.Delay(2000); + await client.SendAsync(MqttPingReqPacket.Instance, CancellationToken.None).ConfigureAwait(false); + } + catch (MqttCommunicationException exception) + { + Assert.IsTrue(exception.InnerException is SocketException); + return; + } + catch + { + Assert.Fail("Wrong exception type thrown."); + } + + Assert.Fail("This MUST fail"); + } + } + + [TestMethod] + public async Task Maintain_IsConnected_Property() + { + using (var testEnvironment = CreateTestEnvironment()) + { + var server = await testEnvironment.StartServer(); + + using (var lowLevelClient = testEnvironment.CreateLowLevelClient()) + { + Assert.IsFalse(lowLevelClient.IsConnected); + + var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).WithTimeout(TimeSpan.FromSeconds(1)).Build(); + + await lowLevelClient.ConnectAsync(clientOptions, CancellationToken.None); + + Assert.IsTrue(lowLevelClient.IsConnected); + + await server.StopAsync(); + server.Dispose(); + + await LongTestDelay(); + + Assert.IsTrue(lowLevelClient.IsConnected); + + try + { + using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5))) + { + await lowLevelClient.SendAsync(MqttPingReqPacket.Instance, CancellationToken.None); + await LongTestDelay(); + + await lowLevelClient.ReceiveAsync(timeout.Token); + } + } + catch + { + } + + Assert.IsFalse(lowLevelClient.IsConnected); + } + } + } + + [TestMethod] + public async Task Subscribe() + { + using (var testEnvironment = CreateTestEnvironment()) + { + await testEnvironment.StartServer(); + + var factory = new MqttFactory(); + var lowLevelClient = factory.CreateLowLevelMqttClient(); + + await lowLevelClient.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build(), CancellationToken.None); + + await Authenticate(lowLevelClient).ConfigureAwait(false); + + var receivedPacket = await Subscribe(lowLevelClient, "a").ConfigureAwait(false); + + await lowLevelClient.DisconnectAsync(CancellationToken.None).ConfigureAwait(false); + + Assert.IsNotNull(receivedPacket); + Assert.AreEqual(MqttSubscribeReasonCode.GrantedQoS0, receivedPacket.ReasonCodes[0]); + } + } + + async Task Authenticate(LowLevelMqttClient client) + { + await client.SendAsync( + new MqttConnectPacket + { + CleanSession = true, + ClientId = TestContext.TestName, + Username = "user", + Password = Encoding.UTF8.GetBytes("pass") + }, + CancellationToken.None) + .ConfigureAwait(false); + + return await client.ReceiveAsync(CancellationToken.None).ConfigureAwait(false) as MqttConnAckPacket; + } + + async Task Subscribe(LowLevelMqttClient client, string topic) + { + await client.SendAsync( + new MqttSubscribePacket + { + PacketIdentifier = 1, + TopicFilters = + { + new MqttTopicFilter + { + Topic = topic + } + } + }, + CancellationToken.None) + .ConfigureAwait(false); + + return await client.ReceiveAsync(CancellationToken.None).ConfigureAwait(false) as MqttSubAckPacket; + } + } +} \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/Client/ManagedMqttClient_Tests.cs b/Source/MQTTnet.Tests/Client/ManagedMqttClient_Tests.cs similarity index 62% rename from Tests/MQTTnet.Core.Tests/Client/ManagedMqttClient_Tests.cs rename to Source/MQTTnet.Tests/Client/ManagedMqttClient_Tests.cs index 7f29468..e64931a 100644 --- a/Tests/MQTTnet.Core.Tests/Client/ManagedMqttClient_Tests.cs +++ b/Source/MQTTnet.Tests/Client/ManagedMqttClient_Tests.cs @@ -1,25 +1,63 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; -using MQTTnet.Client.Connecting; -using MQTTnet.Client.Options; -using MQTTnet.Client.Receiving; using MQTTnet.Diagnostics; -using MQTTnet.Diagnostics.Logger; using MQTTnet.Extensions.ManagedClient; +using MQTTnet.Implementations; +using MQTTnet.Internal; +using MQTTnet.Protocol; using MQTTnet.Server; using MQTTnet.Tests.Mockups; +using MqttClient = MQTTnet.Client.MqttClient; namespace MQTTnet.Tests.Client { [TestClass] - public class ManagedMqttClient_Tests + public sealed class ManagedMqttClient_Tests : BaseTestClass { - public TestContext TestContext { get; set; } + [TestMethod] + public async Task Connect_To_Invalid_Server() + { + using (var testEnvironment = CreateTestEnvironment()) + { + testEnvironment.IgnoreClientLogErrors = true; + + var mqttClient = testEnvironment.CreateClient(); + + var managedClient = testEnvironment.Factory.CreateManagedMqttClient(mqttClient); + + ConnectingFailedEventArgs connectingFailedEventArgs = null; + + managedClient.ConnectingFailedAsync += args => + { + connectingFailedEventArgs = args; + return PlatformAbstractionLayer.CompletedTask; + }; + + await managedClient.StartAsync( + new ManagedMqttClientOptions + { + ClientOptions = testEnvironment.Factory.CreateClientOptionsBuilder().WithTimeout(TimeSpan.FromSeconds(2)).WithTcpServer("wrong_server", 1234).Build() + }); + + await managedClient.EnqueueAsync("test_topic_2"); + + SpinWait.SpinUntil(() => connectingFailedEventArgs != null, 10000); + + // The wrong server must be reported in general. + Assert.IsNotNull(connectingFailedEventArgs); + Assert.IsNotNull(connectingFailedEventArgs.Exception); + Assert.IsNull(connectingFailedEventArgs.ConnectResult); + } + } [TestMethod] public async Task Drop_New_Messages_On_Full_Queue() @@ -28,23 +66,21 @@ namespace MQTTnet.Tests.Client var managedClient = factory.CreateManagedMqttClient(); try { - var clientOptions = new ManagedMqttClientOptionsBuilder() - .WithMaxPendingMessages(5) + var clientOptions = new ManagedMqttClientOptionsBuilder().WithMaxPendingMessages(5) .WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy.DropNewMessage); clientOptions.WithClientOptions(o => o.WithTcpServer("localhost")); await managedClient.StartAsync(clientOptions.Build()); - await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "1" }); - await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "2" }); - await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "3" }); - await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "4" }); - await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "5" }); - - await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "6" }); - await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "7" }); - await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "8" }); + await managedClient.EnqueueAsync("1"); + await managedClient.EnqueueAsync("2"); + await managedClient.EnqueueAsync("3"); + await managedClient.EnqueueAsync("4"); + await managedClient.EnqueueAsync("5"); + await managedClient.EnqueueAsync("6"); + await managedClient.EnqueueAsync("7"); + await managedClient.EnqueueAsync("8"); Assert.AreEqual(5, managedClient.PendingApplicationMessagesCount); } @@ -57,30 +93,41 @@ namespace MQTTnet.Tests.Client [TestMethod] public async Task ManagedClients_Will_Message_Send() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { + await testEnvironment.StartServer(); + var receivedMessagesCount = 0; - await testEnvironment.StartServer(); + var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort) + .WithWillTopic("My/last/will") + .WithWillQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce) + .Build(); - var willMessage = new MqttApplicationMessageBuilder().WithTopic("My/last/will").WithAtMostOnceQoS().Build(); - var clientOptions = new MqttClientOptionsBuilder() - .WithTcpServer("localhost", testEnvironment.ServerPort) - .WithWillMessage(willMessage); var dyingClient = testEnvironment.CreateClient(); var dyingManagedClient = new ManagedMqttClient(dyingClient, testEnvironment.ClientLogger); - await dyingManagedClient.StartAsync(new ManagedMqttClientOptionsBuilder() - .WithClientOptions(clientOptions) - .Build()); - var recievingClient = await testEnvironment.ConnectClient(); - await recievingClient.SubscribeAsync("My/last/will"); + await dyingManagedClient.StartAsync(new ManagedMqttClientOptionsBuilder().WithClientOptions(clientOptions).Build()); + + // Wait until the managed client is fully set up and running. + await Task.Delay(1000); + + var receivingClient = await testEnvironment.ConnectClient(); - recievingClient.UseApplicationMessageReceivedHandler(context => Interlocked.Increment(ref receivedMessagesCount)); + receivingClient.ApplicationMessageReceivedAsync += e => + { + Interlocked.Increment(ref receivedMessagesCount); + return PlatformAbstractionLayer.CompletedTask; + }; + + await receivingClient.SubscribeAsync("My/last/will"); + // Disposing the client will not sent a DISCONNECT packet so that connection is terminated + // which will lead to the will publish. dyingManagedClient.Dispose(); - await Task.Delay(1000); + // Wait for arrival of the will message at the receiver. + await Task.Delay(5000); Assert.AreEqual(1, receivedMessagesCount); } @@ -89,19 +136,16 @@ namespace MQTTnet.Tests.Client [TestMethod] public async Task Start_Stop() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { var server = await testEnvironment.StartServer(); var managedClient = new ManagedMqttClient(testEnvironment.CreateClient(), new MqttNetEventLogger()); - var clientOptions = new MqttClientOptionsBuilder() - .WithTcpServer("localhost", testEnvironment.ServerPort); + var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort); var connected = GetConnectedTask(managedClient); - await managedClient.StartAsync(new ManagedMqttClientOptionsBuilder() - .WithClientOptions(clientOptions) - .Build()); + await managedClient.StartAsync(new ManagedMqttClientOptionsBuilder().WithClientOptions(clientOptions).Build()); await connected; @@ -109,14 +153,14 @@ namespace MQTTnet.Tests.Client await Task.Delay(500); - Assert.AreEqual(0, (await server.GetClientStatusAsync()).Count); + Assert.AreEqual(0, (await server.GetClientsAsync()).Count); } } [TestMethod] public async Task Storage_Queue_Drains() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { testEnvironment.IgnoreClientLogErrors = true; testEnvironment.IgnoreServerLogErrors = true; @@ -124,23 +168,19 @@ namespace MQTTnet.Tests.Client await testEnvironment.StartServer(); var managedClient = new ManagedMqttClient(testEnvironment.CreateClient(), new MqttNetEventLogger()); - var clientOptions = new MqttClientOptionsBuilder() - .WithTcpServer("localhost", testEnvironment.ServerPort); + var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort); var storage = new ManagedMqttClientTestStorage(); var connected = GetConnectedTask(managedClient); - await managedClient.StartAsync(new ManagedMqttClientOptionsBuilder() - .WithClientOptions(clientOptions) - .WithStorage(storage) - .WithAutoReconnectDelay(System.TimeSpan.FromSeconds(5)) - .Build()); + await managedClient.StartAsync( + new ManagedMqttClientOptionsBuilder().WithClientOptions(clientOptions).WithStorage(storage).WithAutoReconnectDelay(TimeSpan.FromSeconds(5)).Build()); await connected; await testEnvironment.Server.StopAsync(); - await managedClient.PublishAsync(new MqttApplicationMessage { Topic = "1" }); + await managedClient.EnqueueAsync("1"); //Message should have been added to the storage queue in PublishAsync, //and we are awaiting PublishAsync, so the message should already be @@ -149,8 +189,7 @@ namespace MQTTnet.Tests.Client connected = GetConnectedTask(managedClient); - await testEnvironment.Server.StartAsync(new MqttServerOptionsBuilder() - .WithDefaultEndpointPort(testEnvironment.ServerPort).Build()); + await testEnvironment.Server.StartAsync(); await connected; @@ -166,7 +205,7 @@ namespace MQTTnet.Tests.Client [TestMethod] public async Task Subscriptions_And_Unsubscriptions_Are_Made_And_Reestablished_At_Reconnect() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { var unmanagedClient = testEnvironment.CreateClient(); var managedClient = await CreateManagedClientAsync(testEnvironment, unmanagedClient); @@ -190,9 +229,9 @@ namespace MQTTnet.Tests.Client async Task PublishMessages() { - await sendingClient.PublishAsync("keptSubscribed", new byte[] { 1 }); - await sendingClient.PublishAsync("subscribedThenUnsubscribed", new byte[] { 1 }); - await sendingClient.PublishAsync("unsubscribedThenSubscribed", new byte[] { 1 }); + await sendingClient.PublishBinaryAsync("keptSubscribed", new byte[] { 1 }); + await sendingClient.PublishBinaryAsync("subscribedThenUnsubscribed", new byte[] { 1 }); + await sendingClient.PublishBinaryAsync("unsubscribedThenSubscribed", new byte[] { 1 }); } await PublishMessages(); @@ -225,83 +264,21 @@ namespace MQTTnet.Tests.Client } } - // This case also serves as a regression test for the previous behavior which re-published - // each and every existing subscriptions with every new subscription that was made - // (causing performance problems and having the visible symptom of retained messages being received again) - [TestMethod] - public async Task Subscriptions_Subscribe_Only_New_Subscriptions() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - var managedClient = await CreateManagedClientAsync(testEnvironment); - - var sendingClient = await testEnvironment.ConnectClient(); - - await managedClient.SubscribeAsync("topic"); - - //wait a bit for the subscription to become established - await Task.Delay(500); - - await sendingClient.PublishAsync(new MqttApplicationMessage { Topic = "topic", Payload = new byte[] { 1 }, Retain = true }); - - var messages = await SetupReceivingOfMessages(managedClient, 1); - - Assert.AreEqual(1, messages.Count); - Assert.AreEqual("topic", messages.Single().Topic); - - await managedClient.SubscribeAsync("anotherTopic"); - - await Task.Delay(500); - - // The subscription of the other topic must not trigger a re-subscription of the existing topic - // (and thus renewed receiving of the retained message) - Assert.AreEqual(1, messages.Count); - } - } - - // This case also serves as a regression test for the previous behavior - // that subscriptions were only published at the ConnectionCheckInterval - [TestMethod] - public async Task Subscriptions_Are_Published_Immediately() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - // Use a long connection check interval to verify that the subscriptions - // do not depend on the connection check interval anymore - var connectionCheckInterval = TimeSpan.FromSeconds(10); - var managedClient = await CreateManagedClientAsync(testEnvironment, null, connectionCheckInterval); - var sendingClient = await testEnvironment.ConnectClient(); - - await sendingClient.PublishAsync(new MqttApplicationMessage { Topic = "topic", Payload = new byte[] { 1 }, Retain = true }); - - var subscribeTime = DateTime.UtcNow; - - var messagesTask = SetupReceivingOfMessages(managedClient, 1); - - await managedClient.SubscribeAsync("topic"); - - var messages = await messagesTask; - - var elapsed = DateTime.UtcNow - subscribeTime; - Assert.IsTrue(elapsed < TimeSpan.FromSeconds(1), $"Subscriptions must be activated immediately, this one took {elapsed}"); - Assert.AreEqual(messages.Single().Topic, "topic"); - } - } - [TestMethod] public async Task Subscriptions_Are_Cleared_At_Logout() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { await testEnvironment.StartServer().ConfigureAwait(false); var sendingClient = await testEnvironment.ConnectClient().ConfigureAwait(false); - await sendingClient.PublishAsync(new MqttApplicationMessage - { - Topic = "topic", - Payload = new byte[] { 1 }, - Retain = true - }); + await sendingClient.PublishAsync( + new MqttApplicationMessage + { + Topic = "topic", + Payload = new byte[] { 1 }, + Retain = true + }); // Wait a bit for the retained message to be available await Task.Delay(1000); @@ -310,22 +287,19 @@ namespace MQTTnet.Tests.Client // Now use the managed client and check if subscriptions get cleared properly. - var clientOptions = new MqttClientOptionsBuilder() - .WithTcpServer("127.0.0.1", testEnvironment.ServerPort); + var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort); var receivedManagedMessages = new List(); var managedClient = new ManagedMqttClient(testEnvironment.CreateClient(), new MqttNetEventLogger()); - managedClient.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(c => + managedClient.ApplicationMessageReceivedAsync += e => { - receivedManagedMessages.Add(c.ApplicationMessage); - }); + receivedManagedMessages.Add(e.ApplicationMessage); + return PlatformAbstractionLayer.CompletedTask; + }; await managedClient.SubscribeAsync("topic"); - await managedClient.StartAsync(new ManagedMqttClientOptionsBuilder() - .WithClientOptions(clientOptions) - .WithAutoReconnectDelay(TimeSpan.FromSeconds(1)) - .Build()); + await managedClient.StartAsync(new ManagedMqttClientOptionsBuilder().WithClientOptions(clientOptions).WithAutoReconnectDelay(TimeSpan.FromSeconds(1)).Build()); await Task.Delay(1000); @@ -335,10 +309,7 @@ namespace MQTTnet.Tests.Client await Task.Delay(500); - await managedClient.StartAsync(new ManagedMqttClientOptionsBuilder() - .WithClientOptions(clientOptions) - .WithAutoReconnectDelay(TimeSpan.FromSeconds(1)) - .Build()); + await managedClient.StartAsync(new ManagedMqttClientOptionsBuilder().WithClientOptions(clientOptions).WithAutoReconnectDelay(TimeSpan.FromSeconds(1)).Build()); await Task.Delay(1000); @@ -353,79 +324,80 @@ namespace MQTTnet.Tests.Client } } + // This case also serves as a regression test for the previous behavior + // that subscriptions were only published at the ConnectionCheckInterval [TestMethod] - public async Task Manage_Session_MaxParallel_Subscribe() + public async Task Subscriptions_Are_Published_Immediately() { - using (var testEnvironment = new TestEnvironment()) + using (var testEnvironment = CreateTestEnvironment()) { - testEnvironment.IgnoreClientLogErrors = true; - - var serverOptions = new MqttServerOptionsBuilder(); - await testEnvironment.StartServer(serverOptions); - - var options = new MqttClientOptionsBuilder().WithClientId("1").WithKeepAlivePeriod(TimeSpan.FromSeconds(5)); - - var hasReceive = false; - - void OnReceive() - { - if (!hasReceive) - { - hasReceive = true; - } - } + // Use a long connection check interval to verify that the subscriptions + // do not depend on the connection check interval anymore + var connectionCheckInterval = TimeSpan.FromSeconds(10); + var receivingClient = await CreateManagedClientAsync(testEnvironment, null, connectionCheckInterval); + var sendingClient = await testEnvironment.ConnectClient(); - var clients = await Task.WhenAll(Enumerable.Range(0, 25) - .Select(i => TryConnect_Subscribe(testEnvironment, options, OnReceive))); + await sendingClient.PublishAsync(new MqttApplicationMessage { Topic = "topic", Payload = new byte[] { 1 }, Retain = true }); - var connectedClients = clients.Where(c => c?.IsConnected ?? false).ToList(); + var subscribeTime = DateTime.UtcNow; - Assert.AreEqual(1, connectedClients.Count); + var messagesTask = SetupReceivingOfMessages(receivingClient, 1); - await Task.Delay(10000); // over 1.5T + await receivingClient.SubscribeAsync("topic"); - var option2 = new MqttClientOptionsBuilder().WithClientId("2").WithKeepAlivePeriod(TimeSpan.FromSeconds(10)); - var sendClient = await testEnvironment.ConnectClient(option2); - await sendClient.PublishAsync("aaa", "1"); - - await Task.Delay(3000); + var messages = await messagesTask; - Assert.AreEqual(true, hasReceive); + var elapsed = DateTime.UtcNow - subscribeTime; + Assert.IsTrue(elapsed < TimeSpan.FromSeconds(1), $"Subscriptions must be activated immediately, this one took {elapsed}"); + Assert.AreEqual(messages.Single().Topic, "topic"); } } - private async Task TryConnect_Subscribe(TestEnvironment testEnvironment, MqttClientOptionsBuilder options, Action onReceive) + // This case also serves as a regression test for the previous behavior which re-published + // each and every existing subscriptions with every new subscription that was made + // (causing performance problems and having the visible symptom of retained messages being received again) + [TestMethod] + public async Task Subscriptions_Subscribe_Only_New_Subscriptions() { - - try - { - var sendClient = await testEnvironment.ConnectClient(options); - sendClient.ApplicationMessageReceivedHandler = new MQTTnet.Client.Receiving.MqttApplicationMessageReceivedHandlerDelegate(e => - { - onReceive(); - }); - await sendClient.SubscribeAsync("aaa"); - return sendClient; - } - catch (System.Exception) + using (var testEnvironment = CreateTestEnvironment()) { - return null; + var managedClient = await CreateManagedClientAsync(testEnvironment); + + var sendingClient = await testEnvironment.ConnectClient(); + + await managedClient.SubscribeAsync("topic"); + + //wait a bit for the subscription to become established + await Task.Delay(500); + + await sendingClient.PublishAsync(new MqttApplicationMessage { Topic = "topic", Payload = new byte[] { 1 }, Retain = true }); + + var messages = await SetupReceivingOfMessages(managedClient, 1); + + Assert.AreEqual(1, messages.Count); + Assert.AreEqual("topic", messages.Single().Topic); + + await managedClient.SubscribeAsync("anotherTopic"); + + await Task.Delay(500); + + // The subscription of the other topic must not trigger a re-subscription of the existing topic + // (and thus renewed receiving of the retained message) + Assert.AreEqual(1, messages.Count); } } async Task CreateManagedClientAsync( TestEnvironment testEnvironment, - IMqttClient underlyingClient = null, - TimeSpan? connectionCheckInterval = null) + MqttClient underlyingClient = null, + TimeSpan? connectionCheckInterval = null, + string host = "localhost") { await testEnvironment.StartServer(); - var clientOptions = new MqttClientOptionsBuilder() - .WithTcpServer("localhost", testEnvironment.ServerPort); + var clientOptions = new MqttClientOptionsBuilder().WithTcpServer(host, testEnvironment.ServerPort); - var managedOptions = new ManagedMqttClientOptionsBuilder() - .WithClientOptions(clientOptions) - .Build(); + var managedOptions = new ManagedMqttClientOptionsBuilder().WithClientOptions(clientOptions).Build(); // Use a short connection check interval so that subscription operations are performed quickly // in order to verify against a previous implementation that performed subscriptions only @@ -444,37 +416,49 @@ namespace MQTTnet.Tests.Client } /// - /// Returns a task that will finish when the has connected + /// Returns a task that will finish when the + /// + /// has connected /// Task GetConnectedTask(ManagedMqttClient managedClient) { - TaskCompletionSource connected = new TaskCompletionSource(); - managedClient.ConnectedHandler = new MqttClientConnectedHandlerDelegate(e => + var connected = new TaskCompletionSource(); + + managedClient.ConnectedAsync += e => { - managedClient.ConnectedHandler = null; - connected.SetResult(true); - }); + connected.TrySetResult(true); + return PlatformAbstractionLayer.CompletedTask; + }; + return connected.Task; } /// - /// Returns a task that will return the messages received on - /// when have been received + /// Returns a task that will return the messages received on + /// + /// when + /// + /// have been received /// Task> SetupReceivingOfMessages(ManagedMqttClient managedClient, int expectedNumberOfMessages) { var receivedMessages = new List(); - var result = new TaskCompletionSource>(TaskCreationOptions.RunContinuationsAsynchronously); + var result = new AsyncTaskCompletionSource>(); - managedClient.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(r => + managedClient.ApplicationMessageReceivedAsync += e => { - receivedMessages.Add(r.ApplicationMessage); + receivedMessages.Add(e.ApplicationMessage); if (receivedMessages.Count == expectedNumberOfMessages) { result.TrySetResult(receivedMessages); } - }); + + return PlatformAbstractionLayer.CompletedTask; + }; return result.Task; } @@ -484,6 +468,11 @@ namespace MQTTnet.Tests.Client { IList _messages; + public int GetMessageCount() + { + return _messages.Count; + } + public Task> LoadQueuedMessagesAsync() { if (_messages == null) @@ -499,10 +488,5 @@ namespace MQTTnet.Tests.Client _messages = messages; return Task.FromResult(0); } - - public int GetMessageCount() - { - return _messages.Count; - } } -} +} \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/Client/MqttClientOptionsBuilder_Tests.cs b/Source/MQTTnet.Tests/Client/MqttClientOptionsBuilder_Tests.cs similarity index 59% rename from Tests/MQTTnet.Core.Tests/Client/MqttClientOptionsBuilder_Tests.cs rename to Source/MQTTnet.Tests/Client/MqttClientOptionsBuilder_Tests.cs index 56748bb..59db109 100644 --- a/Tests/MQTTnet.Core.Tests/Client/MqttClientOptionsBuilder_Tests.cs +++ b/Source/MQTTnet.Tests/Client/MqttClientOptionsBuilder_Tests.cs @@ -1,7 +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. + using System.Linq; using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Client.Options; +using MQTTnet.Client; using MQTTnet.Extensions; namespace MQTTnet.Tests.Client @@ -15,8 +19,9 @@ namespace MQTTnet.Tests.Client var options = new MqttClientOptionsBuilder() .WithConnectionUri("mqtt://user:password@127.0.0.1") .Build(); - Assert.AreEqual("user", options.Credentials.Username); - Assert.IsTrue(Encoding.UTF8.GetBytes("password").SequenceEqual(options.Credentials.Password)); + + Assert.AreEqual("user", options.Credentials.GetUserName(null)); + Assert.IsTrue(Encoding.UTF8.GetBytes("password").SequenceEqual(options.Credentials.GetPassword(null))); } } } diff --git a/Tests/MQTTnet.Core.Tests/Client/MqttClient_Tests.cs b/Source/MQTTnet.Tests/Client/MqttClient_Tests.cs similarity index 74% rename from Tests/MQTTnet.Core.Tests/Client/MqttClient_Tests.cs rename to Source/MQTTnet.Tests/Client/MqttClient_Tests.cs index b6f3275..e6dbcdc 100644 --- a/Tests/MQTTnet.Core.Tests/Client/MqttClient_Tests.cs +++ b/Source/MQTTnet.Tests/Client/MqttClient_Tests.cs @@ -1,5 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Net.Sockets; using System.Text; @@ -7,107 +12,79 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; -using MQTTnet.Client.Connecting; -using MQTTnet.Client.Disconnecting; -using MQTTnet.Client.Options; -using MQTTnet.Client.Receiving; -using MQTTnet.Client.Subscribing; using MQTTnet.Exceptions; using MQTTnet.Formatter; +using MQTTnet.Implementations; using MQTTnet.Packets; using MQTTnet.Protocol; -using MQTTnet.Server; using MQTTnet.Tests.Mockups; namespace MQTTnet.Tests.Client { [TestClass] - public class Client_Tests + public sealed class Client_Tests : BaseTestClass { - public TestContext TestContext { get; set; } - - [TestMethod] - public async Task Ensure_Queue_Drain() + [DataTestMethod] + [DataRow(MqttQualityOfServiceLevel.ExactlyOnce)] + [DataRow(MqttQualityOfServiceLevel.AtMostOnce)] + [DataRow(MqttQualityOfServiceLevel.AtLeastOnce)] + public async Task Concurrent_Processing(MqttQualityOfServiceLevel qos) { + long concurrency = 0; + var success = false; + using (var testEnvironment = new TestEnvironment(TestContext)) { - var server = await testEnvironment.StartServer(); - var client = await testEnvironment.ConnectLowLevelClient(); - - var i = 0; - server.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(c => - { - i++; - }); + await testEnvironment.StartServer(); + var publisher = await testEnvironment.ConnectClient(); + var subscriber = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId(qos.ToString())); + await subscriber.SubscribeAsync("#", qos); - await client.SendAsync(new MqttConnectPacket + subscriber.ApplicationMessageReceivedAsync += e => { - ClientId = "Ensure_Queue_Drain_Test" - }, CancellationToken.None); + e.AutoAcknowledge = false; - await client.SendAsync(new MqttPublishPacket - { - Topic = "Test" - }, CancellationToken.None); - - await Task.Delay(500); - - // This will simulate a device which closes the connection directly - // after sending the data so do delay is added between send and dispose! - client.Dispose(); - - await Task.Delay(1000); - - Assert.AreEqual(1, i); - } - } - - [TestMethod] - public async Task Set_ClientWasConnected_On_ServerDisconnect() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - var server = await testEnvironment.StartServer(); - var client = await testEnvironment.ConnectClient(); - - Assert.IsTrue(client.IsConnected); - client.UseDisconnectedHandler(e => Assert.IsTrue(e.ClientWasConnected)); + async Task InvokeInternal() + { + if (Interlocked.Increment(ref concurrency) > 1) + { + success = true; + } - await server.StopAsync(); - await Task.Delay(4000); - } - } + await Task.Delay(100); + Interlocked.Decrement(ref concurrency); + } - [TestMethod] - public async Task Set_ClientWasConnected_On_ClientDisconnect() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - var server = await testEnvironment.StartServer(); - var client = await testEnvironment.ConnectClient(); + _ = InvokeInternal(); + return PlatformAbstractionLayer.CompletedTask; + }; - Assert.IsTrue(client.IsConnected); - client.UseDisconnectedHandler(e => Assert.IsTrue(e.ClientWasConnected)); + var publishes = Task.WhenAll(publisher.PublishStringAsync("a", null, qos), publisher.PublishStringAsync("b", null, qos)); - await client.DisconnectAsync(); await Task.Delay(200); + + await publishes; + Assert.IsTrue(success); } } [TestMethod] - [ExpectedException(typeof(MqttCommunicationTimedOutException))] - public async Task Connect_To_Invalid_Server_Wrong_IP() + [ExpectedException(typeof(MqttCommunicationException))] + public async Task Connect_To_Invalid_Server_Port_Not_Opened() { var client = new MqttFactory().CreateMqttClient(); - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("1.2.3.4").WithCommunicationTimeout(TimeSpan.FromSeconds(2)).Build()); + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", 12345).WithTimeout(TimeSpan.FromSeconds(5)).Build()); } [TestMethod] - [ExpectedException(typeof(MqttCommunicationException))] - public async Task Connect_To_Invalid_Server_Port_Not_Opened() + [ExpectedException(typeof(OperationCanceledException))] + public async Task Connect_To_Invalid_Server_Wrong_IP() { var client = new MqttFactory().CreateMqttClient(); - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", 12345).WithCommunicationTimeout(TimeSpan.FromSeconds(5)).Build()); + using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(2))) + { + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("1.2.3.4").Build(), timeout.Token); + } } [TestMethod] @@ -115,225 +92,177 @@ namespace MQTTnet.Tests.Client public async Task Connect_To_Invalid_Server_Wrong_Protocol() { var client = new MqttFactory().CreateMqttClient(); - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("http://127.0.0.1", 12345).WithCommunicationTimeout(TimeSpan.FromSeconds(2)).Build()); + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("http://127.0.0.1", 12345).WithTimeout(TimeSpan.FromSeconds(2)).Build()); } [TestMethod] - public async Task Send_Manual_Ping() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer(); - var client = await testEnvironment.ConnectClient(); - - await client.PingAsync(CancellationToken.None); - } - } - - [TestMethod] - public async Task Send_Reply_In_Message_Handler_For_Same_Client() + public async Task ConnectTimeout_Throws_Exception() { - using (var testEnvironment = new TestEnvironment(TestContext)) + var factory = new MqttFactory(); + using (var client = factory.CreateMqttClient()) { - await testEnvironment.StartServer(); - var client = await testEnvironment.ConnectClient(); - - await client.SubscribeAsync("#"); - - var replyReceived = false; - - client.UseApplicationMessageReceivedHandler(c => + var disconnectHandlerCalled = false; + try { - if (c.ApplicationMessage.Topic == "request") - { -#pragma warning disable 4014 - Task.Run(() => client.PublishAsync("reply", null, MqttQualityOfServiceLevel.AtLeastOnce)); -#pragma warning restore 4014 - } - else + client.DisconnectedAsync += args => { - replyReceived = true; - } - }); + disconnectHandlerCalled = true; + return PlatformAbstractionLayer.CompletedTask; + }; - await client.PublishAsync("request", null, MqttQualityOfServiceLevel.AtLeastOnce); + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("1.2.3.4").Build()); - SpinWait.SpinUntil(() => replyReceived, TimeSpan.FromSeconds(10)); + Assert.Fail("Must fail!"); + } + catch (Exception exception) + { + Assert.IsNotNull(exception); + Assert.IsInstanceOfType(exception, typeof(MqttCommunicationException)); + //Assert.IsInstanceOfType(exception.InnerException, typeof(SocketException)); + } - Assert.IsTrue(replyReceived); + await Task.Delay(100); // disconnected handler is called async + Assert.IsTrue(disconnectHandlerCalled); } } [TestMethod] - public async Task Send_Reply_In_Message_Handler() + public async Task Disconnect_Event_Contains_Exception() { - using (var testEnvironment = new TestEnvironment()) + var factory = new MqttFactory(); + using (var client = factory.CreateMqttClient()) { - await testEnvironment.StartServer(); - var client1 = await testEnvironment.ConnectClient(); - var client2 = await testEnvironment.ConnectClient(); - - await client1.SubscribeAsync("#"); - await client2.SubscribeAsync("#"); - - var replyReceived = false; - - client1.UseApplicationMessageReceivedHandler(c => + Exception ex = null; + client.DisconnectedAsync += e => { - if (c.ApplicationMessage.Topic == "reply") - { - replyReceived = true; - } - }); + ex = e.Exception; + return PlatformAbstractionLayer.CompletedTask; + }; - client2.UseApplicationMessageReceivedHandler(async c => + try { - if (c.ApplicationMessage.Topic == "request") - { - // Use AtMostOnce here because with QoS 1 or even QoS 2 the process waits for - // the ACK etc. The problem is that the SpinUntil below only waits until the - // flag is set. It does not wait until the client has sent the ACK - await client2.PublishAsync("reply", null, MqttQualityOfServiceLevel.AtMostOnce); - } - }); - - await client1.PublishAsync("request", null, MqttQualityOfServiceLevel.AtLeastOnce); - - await Task.Delay(500); - - SpinWait.SpinUntil(() => replyReceived, TimeSpan.FromSeconds(10)); + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("wrong-server").Build()); + } + catch + { + } await Task.Delay(500); - Assert.IsTrue(replyReceived); + Assert.IsNotNull(ex); + Assert.IsInstanceOfType(ex, typeof(MqttCommunicationException)); + Assert.IsInstanceOfType(ex.InnerException, typeof(SocketException)); } } [TestMethod] - public async Task Reconnect() + public async Task Ensure_Queue_Drain() { using (var testEnvironment = new TestEnvironment(TestContext)) { var server = await testEnvironment.StartServer(); - var client = await testEnvironment.ConnectClient(); + var client = await testEnvironment.ConnectLowLevelClient(); - await Task.Delay(500); - Assert.IsTrue(client.IsConnected); + var i = 0; + server.InterceptingPublishAsync += c => + { + i++; + return PlatformAbstractionLayer.CompletedTask; + }; - await server.StopAsync(); - await Task.Delay(500); - Assert.IsFalse(client.IsConnected); + await client.SendAsync( + new MqttConnectPacket + { + ClientId = "Ensure_Queue_Drain_Test" + }, + CancellationToken.None); + + await client.SendAsync( + new MqttPublishPacket + { + Topic = "Test" + }, + CancellationToken.None); - await server.StartAsync(new MqttServerOptionsBuilder().WithDefaultEndpointPort(testEnvironment.ServerPort).Build()); await Task.Delay(500); - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); - Assert.IsTrue(client.IsConnected); + // This will simulate a device which closes the connection directly + // after sending the data so do delay is added between send and dispose! + client.Dispose(); + + await Task.Delay(1000); + + Assert.AreEqual(1, i); } } [TestMethod] - public async Task Reconnect_While_Server_Offline() + public async Task Fire_Disconnected_Event_On_Server_Shutdown() { using (var testEnvironment = new TestEnvironment(TestContext)) { - testEnvironment.IgnoreClientLogErrors = true; - var server = await testEnvironment.StartServer(); var client = await testEnvironment.ConnectClient(); - await Task.Delay(500); - Assert.IsTrue(client.IsConnected); + var handlerFired = false; + client.DisconnectedAsync += e => + { + handlerFired = true; + return PlatformAbstractionLayer.CompletedTask; + }; await server.StopAsync(); - await Task.Delay(500); - Assert.IsFalse(client.IsConnected); - - for (var i = 0; i < 5; i++) - { - try - { - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); - Assert.Fail("Must fail!"); - } - catch - { - } - } - await server.StartAsync(new MqttServerOptionsBuilder().WithDefaultEndpointPort(testEnvironment.ServerPort).Build()); - await Task.Delay(500); + await Task.Delay(4000); - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); - Assert.IsTrue(client.IsConnected); + Assert.IsTrue(handlerFired); } } [TestMethod] - public async Task Reconnect_From_Disconnected_Event() + public async Task Frequent_Connects() { using (var testEnvironment = new TestEnvironment(TestContext)) { - testEnvironment.IgnoreClientLogErrors = true; - - var client = testEnvironment.CreateClient(); - - var tries = 0; - var maxTries = 3; + await testEnvironment.StartServer(); - client.UseDisconnectedHandler(async e => + var clients = new List(); + for (var i = 0; i < 100; i++) { - if (tries >= maxTries) - { - return; - } + clients.Add(await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("a"))); + } - Interlocked.Increment(ref tries); + await Task.Delay(500); - await Task.Delay(100); - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); - }); + var clientStatus = await testEnvironment.Server.GetClientsAsync(); + var sessionStatus = await testEnvironment.Server.GetSessionsAsync(); - try - { - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); - Assert.Fail("Must fail!"); - } - catch + for (var i = 0; i < 98; i++) { + Assert.IsFalse(clients[i].IsConnected, $"clients[{i}] is not connected"); } - SpinWait.SpinUntil(() => tries >= maxTries, 10000); - - Assert.AreEqual(maxTries, tries); - } - } - - [TestMethod] - public async Task PacketIdentifier_In_Publish_Result() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer(); - var client = await testEnvironment.ConnectClient(); + Assert.IsTrue(clients[99].IsConnected); - var result = await client.PublishAsync("a", "a", MqttQualityOfServiceLevel.AtMostOnce); - Assert.AreEqual(null, result.PacketIdentifier); + Assert.AreEqual(1, clientStatus.Count); + Assert.AreEqual(1, sessionStatus.Count); - result = await client.PublishAsync("b", "b", MqttQualityOfServiceLevel.AtMostOnce); - Assert.AreEqual(null, result.PacketIdentifier); + var receiveClient = clients[99]; + object receivedPayload = null; + receiveClient.ApplicationMessageReceivedAsync += e => + { + receivedPayload = e.ApplicationMessage.ConvertPayloadToString(); + return PlatformAbstractionLayer.CompletedTask; + }; - result = await client.PublishAsync("a", "a", MqttQualityOfServiceLevel.AtLeastOnce); - Assert.AreEqual((ushort)1, result.PacketIdentifier); + await receiveClient.SubscribeAsync("x"); - result = await client.PublishAsync("b", "b", MqttQualityOfServiceLevel.AtLeastOnce); - Assert.AreEqual((ushort)2, result.PacketIdentifier); + var sendClient = await testEnvironment.ConnectClient(); + await sendClient.PublishStringAsync("x", "1"); - result = await client.PublishAsync("a", "a", MqttQualityOfServiceLevel.ExactlyOnce); - Assert.AreEqual((ushort)3, result.PacketIdentifier); + await Task.Delay(250); - result = await client.PublishAsync("b", "b", MqttQualityOfServiceLevel.ExactlyOnce); - Assert.AreEqual((ushort)4, result.PacketIdentifier); + Assert.AreEqual("1", receivedPayload); } } @@ -359,79 +288,94 @@ namespace MQTTnet.Tests.Client } [TestMethod] - public async Task ConnectTimeout_Throws_Exception() + public async Task No_Payload() { - var factory = new MqttFactory(); - using (var client = factory.CreateMqttClient()) + using (var testEnvironment = new TestEnvironment(TestContext)) { - bool disconnectHandlerCalled = false; - try - { - client.DisconnectedHandler = new MqttClientDisconnectedHandlerDelegate(args => - { - disconnectHandlerCalled = true; - }); + await testEnvironment.StartServer(); - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("1.2.3.4").Build()); + var sender = await testEnvironment.ConnectClient(); + var receiver = await testEnvironment.ConnectClient(); - Assert.Fail("Must fail!"); - } - catch (Exception exception) + var message = new MqttApplicationMessageBuilder().WithTopic("A"); + + await receiver.SubscribeAsync( + new MqttClientSubscribeOptions + { + TopicFilters = new List { new MqttTopicFilter { Topic = "#" } } + }, + CancellationToken.None); + + MqttApplicationMessage receivedMessage = null; + receiver.ApplicationMessageReceivedAsync += e => { - Assert.IsNotNull(exception); - Assert.IsInstanceOfType(exception, typeof(MqttCommunicationException)); - //Assert.IsInstanceOfType(exception.InnerException, typeof(SocketException)); - } + receivedMessage = e.ApplicationMessage; + return PlatformAbstractionLayer.CompletedTask; + }; - await Task.Delay(100); // disconnected handler is called async - Assert.IsTrue(disconnectHandlerCalled); + await sender.PublishAsync(message.Build(), CancellationToken.None); + + await Task.Delay(1000); + + Assert.IsNotNull(receivedMessage); + Assert.AreEqual("A", receivedMessage.Topic); + Assert.AreEqual(null, receivedMessage.Payload); } } [TestMethod] - public async Task Fire_Disconnected_Event_On_Server_Shutdown() + public async Task NoConnectedHandler_Connect_DoesNotThrowException() { using (var testEnvironment = new TestEnvironment(TestContext)) { - var server = await testEnvironment.StartServer(); + await testEnvironment.StartServer(); + var client = await testEnvironment.ConnectClient(); - var handlerFired = false; - client.UseDisconnectedHandler(e => handlerFired = true); + Assert.IsTrue(client.IsConnected); + } + } - await server.StopAsync(); + [TestMethod] + public async Task NoDisconnectedHandler_Disconnect_DoesNotThrowException() + { + using (var testEnvironment = new TestEnvironment(TestContext)) + { + await testEnvironment.StartServer(); + var client = await testEnvironment.ConnectClient(); + Assert.IsTrue(client.IsConnected); - await Task.Delay(4000); + await client.DisconnectAsync(); - Assert.IsTrue(handlerFired); + Assert.IsFalse(client.IsConnected); } } [TestMethod] - public async Task Disconnect_Event_Contains_Exception() + public async Task PacketIdentifier_In_Publish_Result() { - var factory = new MqttFactory(); - using (var client = factory.CreateMqttClient()) + using (var testEnvironment = new TestEnvironment(TestContext)) { - Exception ex = null; - client.DisconnectedHandler = new MqttClientDisconnectedHandlerDelegate(e => - { - ex = e.Exception; - }); + await testEnvironment.StartServer(); + var client = await testEnvironment.ConnectClient(); - try - { - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("wrong-server").Build()); - } - catch - { - } + var result = await client.PublishStringAsync("a", "a"); + Assert.AreEqual(null, result.PacketIdentifier); - await Task.Delay(500); + result = await client.PublishStringAsync("b", "b"); + Assert.AreEqual(null, result.PacketIdentifier); - Assert.IsNotNull(ex); - Assert.IsInstanceOfType(ex, typeof(MqttCommunicationException)); - Assert.IsInstanceOfType(ex.InnerException, typeof(SocketException)); + result = await client.PublishStringAsync("a", "a", MqttQualityOfServiceLevel.AtLeastOnce); + Assert.AreEqual((ushort)1, result.PacketIdentifier); + + result = await client.PublishStringAsync("b", "b", MqttQualityOfServiceLevel.AtLeastOnce); + Assert.AreEqual((ushort)2, result.PacketIdentifier); + + result = await client.PublishStringAsync("a", "a", MqttQualityOfServiceLevel.ExactlyOnce); + Assert.AreEqual((ushort)3, result.PacketIdentifier); + + result = await client.PublishStringAsync("b", "b", MqttQualityOfServiceLevel.ExactlyOnce); + Assert.AreEqual((ushort)4, result.PacketIdentifier); } } @@ -463,12 +407,12 @@ namespace MQTTnet.Tests.Client } } - client1.UseApplicationMessageReceivedHandler(Handler1); + client1.ApplicationMessageReceivedAsync += Handler1; var client2 = await testEnvironment.ConnectClient(); for (var i = MessagesCount; i > 0; i--) { - await client2.PublishAsync("x", i.ToString()); + await client2.PublishStringAsync("x", i.ToString()); } await Task.Delay(5000); @@ -503,21 +447,21 @@ namespace MQTTnet.Tests.Client eventArgs.AutoAcknowledge = false; Task.Delay(value).ContinueWith(x => eventArgs.AcknowledgeAsync(CancellationToken.None)); - System.Diagnostics.Debug.WriteLine($"received {value}"); + Debug.WriteLine($"received {value}"); lock (receivedValues) { receivedValues.Add(value); } - return Task.CompletedTask; + return PlatformAbstractionLayer.CompletedTask; } - client1.UseApplicationMessageReceivedHandler(Handler1); + client1.ApplicationMessageReceivedAsync += Handler1; var client2 = await testEnvironment.ConnectClient(); for (var i = MessagesCount; i > 0; i--) { - await client2.PublishAsync("x", i.ToString(), MqttQualityOfServiceLevel.ExactlyOnce); + await client2.PublishStringAsync("x", i.ToString(), MqttQualityOfServiceLevel.ExactlyOnce); } await Task.Delay(5000); @@ -530,341 +474,433 @@ namespace MQTTnet.Tests.Client } [TestMethod] - public async Task Send_Reply_For_Any_Received_Message() + public async Task Publish_QoS_0_Over_Period_Exceeding_KeepAlive() + { + using (var testEnvironment = new TestEnvironment(TestContext)) + { + const int KeepAlivePeriodSecs = 3; + + await testEnvironment.StartServer(); + + var options = new MqttClientOptionsBuilder().WithKeepAlivePeriod(TimeSpan.FromSeconds(KeepAlivePeriodSecs)); + var client = await testEnvironment.ConnectClient(options); + var message = new MqttApplicationMessageBuilder().WithTopic("a").Build(); + + try + { + // Publish messages over a time period exceeding the keep alive period. + // This should not cause an exception because of, i.e. "Client disconnected". + + for (var count = 0; count < KeepAlivePeriodSecs * 3; ++count) + { + // Send Publish requests well before the keep alive period expires + await client.PublishAsync(message); + await Task.Delay(1000); + } + } + catch (Exception ex) + { + Assert.Fail(ex.Message); + } + } + } + + [TestMethod] + public async Task Publish_QoS_1_In_ApplicationMessageReceiveHandler() + { + using (var testEnvironment = new TestEnvironment(TestContext)) + { + await testEnvironment.StartServer(); + + const string client1Topic = "client1/topic"; + const string client2Topic = "client2/topic"; + const string expectedClient2Message = "hello client2"; + + var client1 = await testEnvironment.ConnectClient(); + client1.ApplicationMessageReceivedAsync += async e => + { + await client1.PublishStringAsync(client2Topic, expectedClient2Message, MqttQualityOfServiceLevel.AtLeastOnce); + }; + + await client1.SubscribeAsync(client1Topic, MqttQualityOfServiceLevel.AtLeastOnce); + + var client2 = await testEnvironment.ConnectClient(); + + var client2TopicResults = new List(); + + client2.ApplicationMessageReceivedAsync += e => + { + client2TopicResults.Add(Encoding.UTF8.GetString(e.ApplicationMessage.Payload)); + return PlatformAbstractionLayer.CompletedTask; + }; + + await client2.SubscribeAsync(client2Topic); + + var client3 = await testEnvironment.ConnectClient(); + var message = new MqttApplicationMessageBuilder().WithTopic(client1Topic).Build(); + await client3.PublishAsync(message); + await client3.PublishAsync(message); + + await Task.Delay(500); + + Assert.AreEqual(2, client2TopicResults.Count); + Assert.AreEqual(expectedClient2Message, client2TopicResults[0]); + Assert.AreEqual(expectedClient2Message, client2TopicResults[1]); + } + } + + [TestMethod] + public async Task Publish_With_Correct_Retain_Flag() + { + using (var testEnvironment = new TestEnvironment(TestContext)) + { + await testEnvironment.StartServer(); + + var receivedMessages = new List(); + + var client1 = await testEnvironment.ConnectClient(); + client1.ApplicationMessageReceivedAsync += e => + { + lock (receivedMessages) + { + receivedMessages.Add(e.ApplicationMessage); + } + + return PlatformAbstractionLayer.CompletedTask; + }; + + await client1.SubscribeAsync("a"); + + var client2 = await testEnvironment.ConnectClient(); + var message = new MqttApplicationMessageBuilder().WithTopic("a").WithRetainFlag().Build(); + await client2.PublishAsync(message); + + await Task.Delay(500); + + Assert.AreEqual(1, receivedMessages.Count); + Assert.IsFalse(receivedMessages.First().Retain); // Must be false even if set above! + } + } + + [TestMethod] + public async Task Reconnect() + { + using (var testEnvironment = new TestEnvironment(TestContext)) + { + var server = await testEnvironment.StartServer(); + var client = await testEnvironment.ConnectClient(); + + await Task.Delay(500); + Assert.IsTrue(client.IsConnected); + + await server.StopAsync(); + await Task.Delay(500); + Assert.IsFalse(client.IsConnected); + + await server.StartAsync(); + await Task.Delay(500); + + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); + Assert.IsTrue(client.IsConnected); + } + } + + [TestMethod] + public async Task Reconnect_From_Disconnected_Event() + { + using (var testEnvironment = new TestEnvironment(TestContext)) + { + testEnvironment.IgnoreClientLogErrors = true; + + var client = testEnvironment.CreateClient(); + + var tries = 0; + var maxTries = 3; + + client.DisconnectedAsync += async e => + { + if (tries >= maxTries) + { + return; + } + + Interlocked.Increment(ref tries); + + await Task.Delay(100); + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); + }; + + try + { + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); + Assert.Fail("Must fail!"); + } + catch + { + } + + SpinWait.SpinUntil(() => tries >= maxTries, 10000); + + Assert.AreEqual(maxTries, tries); + } + } + + [TestMethod] + public async Task Reconnect_While_Server_Offline() { using (var testEnvironment = new TestEnvironment(TestContext)) { - await testEnvironment.StartServer(); - - var client1 = await testEnvironment.ConnectClient(); - await client1.SubscribeAsync("request/+"); - - async Task Handler1(MqttApplicationMessageReceivedEventArgs eventArgs) - { - await client1.PublishAsync($"reply/{eventArgs.ApplicationMessage.Topic}"); - } + testEnvironment.IgnoreClientLogErrors = true; - client1.UseApplicationMessageReceivedHandler(Handler1); + var server = await testEnvironment.StartServer(); + var client = await testEnvironment.ConnectClient(); - var client2 = await testEnvironment.ConnectClient(); - await client2.SubscribeAsync("reply/#"); + await Task.Delay(500); + Assert.IsTrue(client.IsConnected); - var replies = new List(); + await server.StopAsync(); + await Task.Delay(500); + Assert.IsFalse(client.IsConnected); - void Handler2(MqttApplicationMessageReceivedEventArgs eventArgs) + for (var i = 0; i < 5; i++) { - lock (replies) + try + { + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); + Assert.Fail("Must fail!"); + } + catch { - replies.Add(eventArgs.ApplicationMessage.Topic); } } - client2.UseApplicationMessageReceivedHandler((Action)Handler2); - - await Task.Delay(500); - - await client2.PublishAsync("request/a"); - await client2.PublishAsync("request/b"); - await client2.PublishAsync("request/c"); - + await server.StartAsync(); await Task.Delay(500); - Assert.AreEqual("reply/request/a,reply/request/b,reply/request/c", string.Join(",", replies)); + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); + Assert.IsTrue(client.IsConnected); } } [TestMethod] - public async Task Publish_With_Correct_Retain_Flag() + public async Task Send_Manual_Ping() { using (var testEnvironment = new TestEnvironment(TestContext)) { await testEnvironment.StartServer(); + var client = await testEnvironment.ConnectClient(); - var receivedMessages = new List(); - - var client1 = await testEnvironment.ConnectClient(); - client1.UseApplicationMessageReceivedHandler(c => - { - lock (receivedMessages) - { - receivedMessages.Add(c.ApplicationMessage); - } - }); - - await client1.SubscribeAsync("a"); - - var client2 = await testEnvironment.ConnectClient(); - var message = new MqttApplicationMessageBuilder().WithTopic("a").WithRetainFlag().Build(); - await client2.PublishAsync(message); - - await Task.Delay(500); - - Assert.AreEqual(1, receivedMessages.Count); - Assert.IsFalse(receivedMessages.First().Retain); // Must be false even if set above! + await client.PingAsync(CancellationToken.None); } } [TestMethod] - public async Task Publish_QoS_1_In_ApplicationMessageReceiveHandler() + public async Task Send_Reply_For_Any_Received_Message() { using (var testEnvironment = new TestEnvironment(TestContext)) { await testEnvironment.StartServer(); - const string client1Topic = "client1/topic"; - const string client2Topic = "client2/topic"; - const string expectedClient2Message = "hello client2"; - var client1 = await testEnvironment.ConnectClient(); - client1.UseApplicationMessageReceivedHandler(async c => + await client1.SubscribeAsync("request/+"); + + async Task Handler1(MqttApplicationMessageReceivedEventArgs eventArgs) { - await client1.PublishAsync(client2Topic, expectedClient2Message, MqttQualityOfServiceLevel.AtLeastOnce); - }); + await client1.PublishStringAsync($"reply/{eventArgs.ApplicationMessage.Topic}"); + } - await client1.SubscribeAsync(client1Topic, MqttQualityOfServiceLevel.AtLeastOnce); + client1.ApplicationMessageReceivedAsync += Handler1; var client2 = await testEnvironment.ConnectClient(); + await client2.SubscribeAsync("reply/#"); - var client2TopicResults = new List(); + var replies = new List(); - client2.UseApplicationMessageReceivedHandler(c => + Task Handler2(MqttApplicationMessageReceivedEventArgs eventArgs) { - client2TopicResults.Add(Encoding.UTF8.GetString(c.ApplicationMessage.Payload)); - }); + lock (replies) + { + replies.Add(eventArgs.ApplicationMessage.Topic); + } - await client2.SubscribeAsync(client2Topic); + return PlatformAbstractionLayer.CompletedTask; + } - var client3 = await testEnvironment.ConnectClient(); - var message = new MqttApplicationMessageBuilder().WithTopic(client1Topic).Build(); - await client3.PublishAsync(message); - await client3.PublishAsync(message); + client2.ApplicationMessageReceivedAsync += Handler2; await Task.Delay(500); - Assert.AreEqual(2, client2TopicResults.Count); - Assert.AreEqual(expectedClient2Message, client2TopicResults[0]); - Assert.AreEqual(expectedClient2Message, client2TopicResults[1]); - } - } - - [TestMethod] - public async Task Publish_QoS_0_Over_Period_Exceeding_KeepAlive() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - const int KeepAlivePeriodSecs = 3; - - await testEnvironment.StartServer(); - - var options = new MqttClientOptionsBuilder().WithKeepAlivePeriod(TimeSpan.FromSeconds(KeepAlivePeriodSecs)); - var client = await testEnvironment.ConnectClient(options); - var message = new MqttApplicationMessageBuilder().WithTopic("a").Build(); + await client2.PublishStringAsync("request/a"); + await client2.PublishStringAsync("request/b"); + await client2.PublishStringAsync("request/c"); - try - { - // Publish messages over a time period exceeding the keep alive period. - // This should not cause an exception because of, i.e. "Client disconnected". + await Task.Delay(500); - for (var count = 0; count < KeepAlivePeriodSecs * 3; ++count) - { - // Send Publish requests well before the keep alive period expires - await client.PublishAsync(message); - await Task.Delay(1000); - } - } - catch (Exception ex) - { - Assert.Fail(ex.Message); - } + Assert.AreEqual("reply/request/a,reply/request/b,reply/request/c", string.Join(",", replies)); } } - [TestMethod] - public async Task Subscribe_In_Callback_Events() + public async Task Send_Reply_In_Message_Handler() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = new TestEnvironment()) { await testEnvironment.StartServer(); + var client1 = await testEnvironment.ConnectClient(); + var client2 = await testEnvironment.ConnectClient(); - var receivedMessages = new List(); + await client1.SubscribeAsync("#"); + await client2.SubscribeAsync("#"); - var client = testEnvironment.CreateClient(); + var replyReceived = false; - client.ConnectedHandler = new MqttClientConnectedHandlerDelegate(async e => + client1.ApplicationMessageReceivedAsync += e => { - await client.SubscribeAsync("RCU/P1/H0001/R0003"); - - var msg = new MqttApplicationMessageBuilder() - .WithPayload("DA|18RS00SC00XI0000RV00R100R200R300R400L100L200L300L400Y100Y200AC0102031800BELK0000BM0000|") - .WithTopic("RCU/P1/H0001/R0003"); + if (e.ApplicationMessage.Topic == "reply") + { + replyReceived = true; + } - await client.PublishAsync(msg.Build()); - }); + return PlatformAbstractionLayer.CompletedTask; + }; - client.UseApplicationMessageReceivedHandler(c => + client2.ApplicationMessageReceivedAsync += async e => { - lock (receivedMessages) + if (e.ApplicationMessage.Topic == "request") { - receivedMessages.Add(c.ApplicationMessage); + // Use AtMostOnce here because with QoS 1 or even QoS 2 the process waits for + // the ACK etc. The problem is that the SpinUntil below only waits until the + // flag is set. It does not wait until the client has sent the ACK + await client2.PublishStringAsync("reply"); } - }); + }; - await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort).Build()); + await client1.PublishStringAsync("request", null, MqttQualityOfServiceLevel.AtLeastOnce); await Task.Delay(500); - Assert.AreEqual(1, receivedMessages.Count); - Assert.AreEqual("DA|18RS00SC00XI0000RV00R100R200R300R400L100L200L300L400Y100Y200AC0102031800BELK0000BM0000|", receivedMessages.First().ConvertPayloadToString()); + SpinWait.SpinUntil(() => replyReceived, TimeSpan.FromSeconds(10)); + + await Task.Delay(500); + + Assert.IsTrue(replyReceived); } } [TestMethod] - public async Task Message_Send_Retry() + public async Task Send_Reply_In_Message_Handler_For_Same_Client() { using (var testEnvironment = new TestEnvironment(TestContext)) { - testEnvironment.IgnoreClientLogErrors = true; - testEnvironment.IgnoreServerLogErrors = true; - - await testEnvironment.StartServer( - new MqttServerOptionsBuilder() - .WithPersistentSessions() - .WithDefaultCommunicationTimeout(TimeSpan.FromMilliseconds(250))); + await testEnvironment.StartServer(); + var client = await testEnvironment.ConnectClient(); - var client1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithCleanSession(false)); - await client1.SubscribeAsync("x", MqttQualityOfServiceLevel.AtLeastOnce); + await client.SubscribeAsync("#"); - var retries = 0; + var replyReceived = false; - async Task Handler1(MqttApplicationMessageReceivedEventArgs eventArgs) + client.ApplicationMessageReceivedAsync += e => { - retries++; - - await Task.Delay(1000); - throw new Exception("Broken!"); - } - - client1.UseApplicationMessageReceivedHandler(Handler1); - - var client2 = await testEnvironment.ConnectClient(); - await client2.PublishAsync("x"); - - await Task.Delay(3000); + if (e.ApplicationMessage.Topic == "request") + { +#pragma warning disable 4014 + Task.Run(() => client.PublishStringAsync("reply", null, MqttQualityOfServiceLevel.AtLeastOnce)); +#pragma warning restore 4014 + } + else + { + replyReceived = true; + } - // The server should disconnect clients which are not responding. - Assert.IsFalse(client1.IsConnected); + return PlatformAbstractionLayer.CompletedTask; + }; - await client1.ReconnectAsync().ConfigureAwait(false); + await client.PublishStringAsync("request", null, MqttQualityOfServiceLevel.AtLeastOnce); - await Task.Delay(1000); + SpinWait.SpinUntil(() => replyReceived, TimeSpan.FromSeconds(10)); - Assert.AreEqual(2, retries); + Assert.IsTrue(replyReceived); } } [TestMethod] - public async Task NoConnectedHandler_Connect_DoesNotThrowException() + public async Task Set_ClientWasConnected_On_ClientDisconnect() { using (var testEnvironment = new TestEnvironment(TestContext)) { - await testEnvironment.StartServer(); - + var server = await testEnvironment.StartServer(); var client = await testEnvironment.ConnectClient(); Assert.IsTrue(client.IsConnected); + client.DisconnectedAsync += e => + { + Assert.IsTrue(e.ClientWasConnected); + return PlatformAbstractionLayer.CompletedTask; + }; + + await client.DisconnectAsync(); + await Task.Delay(200); } } [TestMethod] - public async Task NoDisconnectedHandler_Disconnect_DoesNotThrowException() + public async Task Set_ClientWasConnected_On_ServerDisconnect() { using (var testEnvironment = new TestEnvironment(TestContext)) { - await testEnvironment.StartServer(); + var server = await testEnvironment.StartServer(); var client = await testEnvironment.ConnectClient(); - Assert.IsTrue(client.IsConnected); - await client.DisconnectAsync(); + Assert.IsTrue(client.IsConnected); + client.DisconnectedAsync += e => + { + Assert.IsTrue(e.ClientWasConnected); + return PlatformAbstractionLayer.CompletedTask; + }; - Assert.IsFalse(client.IsConnected); + await server.StopAsync(); + await Task.Delay(4000); } } + [TestMethod] - public async Task Frequent_Connects() + public async Task Subscribe_In_Callback_Events() { using (var testEnvironment = new TestEnvironment(TestContext)) { await testEnvironment.StartServer(); - var clients = new List(); - for (var i = 0; i < 100; i++) - { - clients.Add(await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("a"))); - } - - await Task.Delay(500); - - var clientStatus = await testEnvironment.Server.GetClientStatusAsync(); - var sessionStatus = await testEnvironment.Server.GetSessionStatusAsync(); - - for (var i = 0; i < 98; i++) - { - Assert.IsFalse(clients[i].IsConnected, $"clients[{i}] is not connected"); - } - - Assert.IsTrue(clients[99].IsConnected); + var receivedMessages = new List(); - Assert.AreEqual(1, clientStatus.Count); - Assert.AreEqual(1, sessionStatus.Count); + var client = testEnvironment.CreateClient(); - var receiveClient = clients[99]; - object receivedPayload = null; - receiveClient.UseApplicationMessageReceivedHandler(e => + client.ConnectedAsync += async e => { - receivedPayload = e.ApplicationMessage.ConvertPayloadToString(); - }); - - await receiveClient.SubscribeAsync("x"); - - var sendClient = await testEnvironment.ConnectClient(); - await sendClient.PublishAsync("x", "1"); - - await Task.Delay(250); - - Assert.AreEqual("1", receivedPayload); - } - } - - [TestMethod] - public async Task No_Payload() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer(); + await client.SubscribeAsync("RCU/P1/H0001/R0003"); - var sender = await testEnvironment.ConnectClient(); - var receiver = await testEnvironment.ConnectClient(); + var msg = new MqttApplicationMessageBuilder().WithPayload("DA|18RS00SC00XI0000RV00R100R200R300R400L100L200L300L400Y100Y200AC0102031800BELK0000BM0000|") + .WithTopic("RCU/P1/H0001/R0003"); - var message = new MqttApplicationMessageBuilder() - .WithTopic("A"); + await client.PublishAsync(msg.Build()); + }; - await receiver.SubscribeAsync(new MqttClientSubscribeOptions + client.ApplicationMessageReceivedAsync += e => { - TopicFilters = new List { new MqttTopicFilter { Topic = "#" } } - }, CancellationToken.None); + lock (receivedMessages) + { + receivedMessages.Add(e.ApplicationMessage); + } - MqttApplicationMessage receivedMessage = null; - receiver.UseApplicationMessageReceivedHandler(e => receivedMessage = e.ApplicationMessage); + return PlatformAbstractionLayer.CompletedTask; + }; - await sender.PublishAsync(message.Build(), CancellationToken.None); + await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort).Build()); - await Task.Delay(1000); + await Task.Delay(500); - Assert.IsNotNull(receivedMessage); - Assert.AreEqual("A", receivedMessage.Topic); - Assert.AreEqual(null, receivedMessage.Payload); + Assert.AreEqual(1, receivedMessages.Count); + Assert.AreEqual("DA|18RS00SC00XI0000RV00R100R200R300R400L100L200L300L400Y100Y200AC0102031800BELK0000BM0000|", receivedMessages.First().ConvertPayloadToString()); } } @@ -878,23 +914,29 @@ namespace MQTTnet.Tests.Client var client2 = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); var disconnectedFired = false; - client1.DisconnectedHandler = new MqttClientDisconnectedHandlerDelegate(c => + client1.DisconnectedAsync += c => { disconnectedFired = true; - }); + return PlatformAbstractionLayer.CompletedTask; + }; var messageReceived = false; - client1.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(c => + client1.ApplicationMessageReceivedAsync += e => { messageReceived = true; - }); + return PlatformAbstractionLayer.CompletedTask; + }; await client1.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("topic1").WithExactlyOnceQoS().Build()); await Task.Delay(500); - var message = new MqttApplicationMessageBuilder().WithTopic("topic1").WithPayload("Hello World").WithExactlyOnceQoS().WithRetainFlag().Build(); - + var message = new MqttApplicationMessageBuilder().WithTopic("topic1") + .WithPayload("Hello World") + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.ExactlyOnce) + .WithRetainFlag() + .Build(); + await client2.PublishAsync(message); await Task.Delay(500); @@ -904,4 +946,4 @@ namespace MQTTnet.Tests.Client } } } -} +} \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/Logger/Logger_Tests.cs b/Source/MQTTnet.Tests/Diagnostics/Logger_Tests.cs similarity index 87% rename from Tests/MQTTnet.Core.Tests/Logger/Logger_Tests.cs rename to Source/MQTTnet.Tests/Diagnostics/Logger_Tests.cs index 0e29362..b525623 100644 --- a/Tests/MQTTnet.Core.Tests/Logger/Logger_Tests.cs +++ b/Source/MQTTnet.Tests/Diagnostics/Logger_Tests.cs @@ -1,8 +1,12 @@ -using System; +// 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 Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Diagnostics.Logger; +using MQTTnet.Diagnostics; -namespace MQTTnet.Tests.Logger +namespace MQTTnet.Tests.Diagnostics { [TestClass] public sealed class Logger_Tests : BaseTestClass diff --git a/Source/MQTTnet.Tests/Diagnostics/PacketInspection_Tests.cs b/Source/MQTTnet.Tests/Diagnostics/PacketInspection_Tests.cs new file mode 100644 index 0000000..225f2ce --- /dev/null +++ b/Source/MQTTnet.Tests/Diagnostics/PacketInspection_Tests.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Implementations; + +namespace MQTTnet.Tests.Diagnostics +{ + [TestClass] + public sealed class PacketInspection_Tests : BaseTestClass + { + [TestMethod] + public async Task Inspect_Client_Packets() + { + using (var testEnvironment = CreateTestEnvironment()) + { + await testEnvironment.StartServer(); + + using (var mqttClient = testEnvironment.CreateClient()) + { + var mqttClientOptions = testEnvironment.Factory.CreateClientOptionsBuilder() + .WithClientId("CLIENT_ID") // Must be fixed. + .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) + .Build(); + + var packets = new List(); + + mqttClient.InspectPackage += eventArgs => + { + packets.Add(eventArgs.Direction + ":" + Convert.ToBase64String(eventArgs.Buffer)); + return PlatformAbstractionLayer.CompletedTask; + }; + + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + + Assert.AreEqual(2, packets.Count); + Assert.AreEqual("Outbound:ECwABE1RVFQEAgAPACBJbnNwZWN0X0NsaWVudF9QYWNrZXRzX0NMSUVOVF9JRA==", packets[0]); // CONNECT + Assert.AreEqual("Inbound:IAIAAA==", packets[1]); // CONNACK + } + } + } + } +} \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/Logger/SourceLogger_Tests.cs b/Source/MQTTnet.Tests/Diagnostics/SourceLogger_Tests.cs similarity index 66% rename from Tests/MQTTnet.Core.Tests/Logger/SourceLogger_Tests.cs rename to Source/MQTTnet.Tests/Diagnostics/SourceLogger_Tests.cs index 1eb3d16..2785bc2 100644 --- a/Tests/MQTTnet.Core.Tests/Logger/SourceLogger_Tests.cs +++ b/Source/MQTTnet.Tests/Diagnostics/SourceLogger_Tests.cs @@ -1,7 +1,11 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Diagnostics.Logger; +// 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.Tests.Logger +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Diagnostics; + +namespace MQTTnet.Tests.Diagnostics { [TestClass] public sealed class SourceLogger_Tests : BaseTestClass diff --git a/Tests/MQTTnet.Core.Tests/Extension_Tests.cs b/Source/MQTTnet.Tests/Extension_Tests.cs similarity index 88% rename from Tests/MQTTnet.Core.Tests/Extension_Tests.cs rename to Source/MQTTnet.Tests/Extension_Tests.cs index 6895100..fa72dbc 100644 --- a/Tests/MQTTnet.Core.Tests/Extension_Tests.cs +++ b/Source/MQTTnet.Tests/Extension_Tests.cs @@ -1,4 +1,8 @@ -using System; +// 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.Linq; using System.Threading; using System.Threading.Tasks; @@ -27,16 +31,7 @@ namespace MQTTnet.Tests ct => Task.Delay(TimeSpan.FromMilliseconds(500), ct).ContinueWith(t => 5, ct), TimeSpan.FromMilliseconds(100), CancellationToken.None); } - - [TestMethod] - public async Task TimeoutAfterCompleteInTime() - { - var result = await MqttTaskTimeout.WaitAsync( - ct => Task.Delay(TimeSpan.FromMilliseconds(100), ct).ContinueWith(t => 5, ct), - TimeSpan.FromMilliseconds(500), CancellationToken.None); - Assert.AreEqual(5, result); - } - + [TestMethod] public async Task TimeoutAfterWithInnerException() { diff --git a/Source/MQTTnet.Tests/Extensions/MqttPacketWriterExtensions.cs b/Source/MQTTnet.Tests/Extensions/MqttPacketWriterExtensions.cs new file mode 100644 index 0000000..e254354 --- /dev/null +++ b/Source/MQTTnet.Tests/Extensions/MqttPacketWriterExtensions.cs @@ -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.Formatter; +using MQTTnet.Protocol; + +namespace MQTTnet.Tests.Extensions +{ + public static class MqttPacketWriterExtensions + { + public static byte[] AddMqttHeader(this MqttBufferWriter writer, MqttControlPacketType header, byte[] body) + { + writer.WriteByte(MqttBufferWriter.BuildFixedHeader(header)); + writer.WriteVariableByteInteger((uint)body.Length); + writer.WriteBinary(body, 0, body.Length); + return writer.GetBuffer(); + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/Factory/MqttFactory_Tests.cs b/Source/MQTTnet.Tests/Factory/MqttFactory_Tests.cs similarity index 84% rename from Tests/MQTTnet.Core.Tests/Factory/MqttFactory_Tests.cs rename to Source/MQTTnet.Tests/Factory/MqttFactory_Tests.cs index d7b5b27..b34d555 100644 --- a/Tests/MQTTnet.Core.Tests/Factory/MqttFactory_Tests.cs +++ b/Source/MQTTnet.Tests/Factory/MqttFactory_Tests.cs @@ -1,9 +1,15 @@ -using System; +// 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.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Diagnostics.Logger; +using MQTTnet.Diagnostics; using MQTTnet.Extensions.ManagedClient; +using MQTTnet.Implementations; +using MQTTnet.Server; namespace MQTTnet.Tests.Factory { @@ -40,15 +46,20 @@ namespace MQTTnet.Tests.Factory { var clientOptions = new ManagedMqttClientOptionsBuilder(); - clientOptions.WithClientOptions(o => o.WithTcpServer("this_is_an_invalid_host").WithCommunicationTimeout(TimeSpan.FromSeconds(1))); + clientOptions.WithClientOptions(o => o.WithTcpServer("this_is_an_invalid_host").WithTimeout(TimeSpan.FromSeconds(1))); // try connect to get some log entries await managedClient.StartAsync(clientOptions.Build()); // wait at least connect timeout or we have some log messages var tcs = new TaskCompletionSource(); - managedClient.ConnectingFailedHandler = new ConnectingFailedHandlerDelegate(e => tcs.TrySetResult(null)); - await Task.WhenAny(Task.Delay(managedClient.Options.ClientOptions.CommunicationTimeout), tcs.Task); + managedClient.ConnectingFailedAsync += e => + { + tcs.TrySetResult(null); + return PlatformAbstractionLayer.CompletedTask; + }; + + await Task.WhenAny(Task.Delay(managedClient.Options.ClientOptions.Timeout), tcs.Task); } finally { @@ -66,88 +77,88 @@ namespace MQTTnet.Tests.Factory { var factory = new MqttFactory(); var builder = factory.CreateApplicationMessageBuilder(); - + Assert.IsNotNull(builder); } - + [TestMethod] public void Create_ClientOptionsBuilder() { var factory = new MqttFactory(); var builder = factory.CreateClientOptionsBuilder(); - + Assert.IsNotNull(builder); } - + [TestMethod] public void Create_ServerOptionsBuilder() { var factory = new MqttFactory(); var builder = factory.CreateServerOptionsBuilder(); - + Assert.IsNotNull(builder); } - + [TestMethod] public void Create_SubscribeOptionsBuilder() { var factory = new MqttFactory(); var builder = factory.CreateSubscribeOptionsBuilder(); - + Assert.IsNotNull(builder); } - + [TestMethod] public void Create_UnsubscribeOptionsBuilder() { var factory = new MqttFactory(); var builder = factory.CreateUnsubscribeOptionsBuilder(); - + Assert.IsNotNull(builder); } - + [TestMethod] public void Create_TopicFilterBuilder() { var factory = new MqttFactory(); var builder = factory.CreateTopicFilterBuilder(); - + Assert.IsNotNull(builder); } - + [TestMethod] public void Create_MqttServer() { var factory = new MqttFactory(); - var server = factory.CreateMqttServer(); - + var server = factory.CreateMqttServer(new MqttServerOptionsBuilder().Build()); + Assert.IsNotNull(server); } - + [TestMethod] public void Create_MqttClient() { var factory = new MqttFactory(); var client = factory.CreateMqttClient(); - + Assert.IsNotNull(client); } - + [TestMethod] public void Create_LowLevelMqttClient() { var factory = new MqttFactory(); var client = factory.CreateLowLevelMqttClient(); - + Assert.IsNotNull(client); } - + [TestMethod] public void Create_ManagedMqttClient() { var factory = new MqttFactory(); var client = factory.CreateManagedMqttClient(); - + Assert.IsNotNull(client); } } diff --git a/Tests/MQTTnet.Core.Tests/MqttPacketSerializer_Tests.cs b/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Binary_Tests.cs similarity index 62% rename from Tests/MQTTnet.Core.Tests/MqttPacketSerializer_Tests.cs rename to Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Binary_Tests.cs index 26c2323..c268dff 100644 --- a/Tests/MQTTnet.Core.Tests/MqttPacketSerializer_Tests.cs +++ b/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Binary_Tests.cs @@ -1,4 +1,8 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -7,11 +11,8 @@ using System.Threading; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Adapter; using MQTTnet.Diagnostics; -using MQTTnet.Diagnostics.Logger; using MQTTnet.Exceptions; using MQTTnet.Formatter; -using MQTTnet.Formatter.V3; -using MQTTnet.Formatter.V5; using MQTTnet.Internal; using MQTTnet.Packets; using MQTTnet.Protocol; @@ -20,59 +21,33 @@ using MQTTnet.Tests.Extensions; namespace MQTTnet.Tests { [TestClass] - public class MqttPacketSerializer_Tests + public sealed class MqttPacketSerialization_V3_Binary_Tests { [TestMethod] - public void DetectVersionFromMqttConnectPacket() + public void DeserializeV310_MqttConnAckPacket() { - var packet = new MqttConnectPacket + var p = new MqttConnAckPacket { - ClientId = "XYZ", - Password = Encoding.UTF8.GetBytes("PASS"), - Username = "USER", - KeepAlivePeriod = 123, - CleanSession = true + ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized }; - Assert.AreEqual( - MqttProtocolVersion.V310, - DeserializeAndDetectVersion(new MqttPacketFormatterAdapter(new MqttPacketWriter()), Serialize(packet, MqttProtocolVersion.V310))); - - Assert.AreEqual( - MqttProtocolVersion.V311, - DeserializeAndDetectVersion(new MqttPacketFormatterAdapter(new MqttPacketWriter()), Serialize(packet, MqttProtocolVersion.V311))); - - Assert.AreEqual( - MqttProtocolVersion.V500, - DeserializeAndDetectVersion(new MqttPacketFormatterAdapter(new MqttPacketWriter()), Serialize(packet, MqttProtocolVersion.V500))); - - var adapter = new MqttPacketFormatterAdapter(new MqttPacketWriter()); - - var ex = Assert.ThrowsException(() => DeserializeAndDetectVersion(adapter, WriterFactory().AddMqttHeader(MqttControlPacketType.Connect, new byte[0]))); - Assert.AreEqual("CONNECT packet must have at least 7 bytes.", ex.Message); - ex = Assert.ThrowsException(() => DeserializeAndDetectVersion(adapter, WriterFactory().AddMqttHeader(MqttControlPacketType.Connect, new byte[7]))); - Assert.AreEqual("Protocol '' not supported.", ex.Message); - ex = Assert.ThrowsException(() => DeserializeAndDetectVersion(adapter, WriterFactory().AddMqttHeader(MqttControlPacketType.Connect, new byte[] { 255, 255, 0, 0, 0, 0, 0 }))); - Assert.AreEqual("Expected at least 65537 bytes but there are only 7 bytes", ex.Message); + DeserializeAndCompare(p, "IAIABQ==", MqttProtocolVersion.V310); } [TestMethod] - public void SerializeV310_MqttConnectPacket() + public void DeserializeV311_MqttConnAckPacket() { - var p = new MqttConnectPacket + var p = new MqttConnAckPacket { - ClientId = "XYZ", - Password = Encoding.UTF8.GetBytes("PASS"), - Username = "USER", - KeepAlivePeriod = 123, - CleanSession = true + IsSessionPresent = true, + ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized }; - SerializeAndCompare(p, "EB0ABk1RSXNkcAPCAHsAA1hZWgAEVVNFUgAEUEFTUw==", MqttProtocolVersion.V310); + DeserializeAndCompare(p, "IAIBBQ=="); } [TestMethod] - public void SerializeV311_MqttConnectPacket() + public void DeserializeV311_MqttConnectPacket() { var p = new MqttConnectPacket { @@ -83,11 +58,11 @@ namespace MQTTnet.Tests CleanSession = true }; - SerializeAndCompare(p, "EBsABE1RVFQEwgB7AANYWVoABFVTRVIABFBBU1M="); + DeserializeAndCompare(p, "EBsABE1RVFQEwgB7AANYWVoABFVTRVIABFBBU1M="); } [TestMethod] - public void SerializeV311_MqttConnectPacketWithWillMessage() + public void DeserializeV311_MqttConnectPacketWithWillMessage() { var p = new MqttConnectPacket { @@ -96,369 +71,377 @@ namespace MQTTnet.Tests Username = "USER", KeepAlivePeriod = 123, CleanSession = true, - WillMessage = new MqttApplicationMessage - { - Topic = "My/last/will", - Payload = Encoding.UTF8.GetBytes("Good byte."), - QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce, - Retain = true - } + WillFlag = true, + WillTopic = "My/last/will", + WillMessage = Encoding.UTF8.GetBytes("Good byte."), + WillQoS = MqttQualityOfServiceLevel.AtLeastOnce, + WillRetain = true }; - SerializeAndCompare(p, "EDUABE1RVFQE7gB7AANYWVoADE15L2xhc3Qvd2lsbAAKR29vZCBieXRlLgAEVVNFUgAEUEFTUw=="); + DeserializeAndCompare(p, "EDUABE1RVFQE7gB7AANYWVoADE15L2xhc3Qvd2lsbAAKR29vZCBieXRlLgAEVVNFUgAEUEFTUw=="); } [TestMethod] - public void DeserializeV311_MqttConnectPacket() + public void DeserializeV311_MqttPubAckPacket() { - var p = new MqttConnectPacket + var p = new MqttPubAckPacket { - ClientId = "XYZ", - Password = Encoding.UTF8.GetBytes("PASS"), - Username = "USER", - KeepAlivePeriod = 123, - CleanSession = true + PacketIdentifier = 123 }; - DeserializeAndCompare(p, "EBsABE1RVFQEwgB7AANYWVoABFVTRVIABFBBU1M="); + DeserializeAndCompare(p, "QAIAew=="); } [TestMethod] - public void DeserializeV311_MqttConnectPacketWithWillMessage() + public void DeserializeV311_MqttPubCompPacket() { - var p = new MqttConnectPacket + var p = new MqttPubCompPacket { - ClientId = "XYZ", - Password = Encoding.UTF8.GetBytes("PASS"), - Username = "USER", - KeepAlivePeriod = 123, - CleanSession = true, - WillMessage = new MqttApplicationMessage - { - Topic = "My/last/will", - Payload = Encoding.UTF8.GetBytes("Good byte."), - QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce, - Retain = true - } + PacketIdentifier = 123 }; - DeserializeAndCompare(p, "EDUABE1RVFQE7gB7AANYWVoADE15L2xhc3Qvd2lsbAAKR29vZCBieXRlLgAEVVNFUgAEUEFTUw=="); + DeserializeAndCompare(p, "cAIAew=="); } [TestMethod] - public void SerializeV311_MqttConnAckPacket() + public void DeserializeV311_MqttPublishPacket() { - var p = new MqttConnAckPacket + var p = new MqttPublishPacket { - IsSessionPresent = true, - ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized + PacketIdentifier = 123, + Dup = true, + Retain = true, + Payload = Encoding.ASCII.GetBytes("HELLO"), + QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce, + Topic = "A/B/C" }; - SerializeAndCompare(p, "IAIBBQ=="); + DeserializeAndCompare(p, "Ow4ABUEvQi9DAHtIRUxMTw=="); } + [TestMethod] - public void SerializeV310_MqttConnAckPacket() + public void DeserializeV311_MqttPublishPacket_DupFalse() { - var p = new MqttConnAckPacket + var p = new MqttPublishPacket { - ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized + Dup = false }; - SerializeAndCompare(p, "IAIABQ==", MqttProtocolVersion.V310); + var p2 = Roundtrip(p); + + Assert.AreEqual(p.Dup, p2.Dup); } [TestMethod] - public void DeserializeV311_MqttConnAckPacket() + public void DeserializeV311_MqttPublishPacket_Qos1() { - var p = new MqttConnAckPacket + var p = new MqttPublishPacket { - IsSessionPresent = true, - ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized + QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }; - DeserializeAndCompare(p, "IAIBBQ=="); + var p2 = Roundtrip(p); + + Assert.AreEqual(p.QualityOfServiceLevel, p2.QualityOfServiceLevel); + Assert.AreEqual(p.Dup, p2.Dup); } [TestMethod] - public void DeserializeV310_MqttConnAckPacket() + public void DeserializeV311_MqttPublishPacket_Qos2() { - var p = new MqttConnAckPacket + var p = new MqttPublishPacket { - ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized + QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce, + PacketIdentifier = 1 }; - DeserializeAndCompare(p, "IAIABQ==", MqttProtocolVersion.V310); + var p2 = Roundtrip(p); + + Assert.AreEqual(p.QualityOfServiceLevel, p2.QualityOfServiceLevel); + Assert.AreEqual(p.Dup, p2.Dup); } [TestMethod] - public void Serialize_LargePacket() + public void DeserializeV311_MqttPublishPacket_Qos3() { - var serializer = new MqttV311PacketFormatter(WriterFactory()); - - const int payloadLength = 80000; - - var payload = new byte[payloadLength]; - - var value = 0; - for (var i = 0; i < payloadLength; i++) - { - if (value > 255) - { - value = 0; - } - - payload[i] = (byte)value; - } - - var publishPacket = new MqttPublishPacket + var p = new MqttPublishPacket { - Topic = "abcdefghijklmnopqrstuvwxyz0123456789", - Payload = payload + QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce, + PacketIdentifier = 1 }; + var p2 = Roundtrip(p); - var publishPacketCopy = Roundtrip(publishPacket); - - //var buffer = serializer.Encode(publishPacket); - //var testChannel = new TestMqttChannel(new MemoryStream(buffer.Array, buffer.Offset, buffer.Count)); - - - //var header = new MqttPacketReader(testChannel).ReadFixedHeaderAsync( - // new byte[2], - // CancellationToken.None).GetAwaiter().GetResult().FixedHeader; - - //var eof = buffer.Offset + buffer.Count; - - //var receivedPacket = new ReceivedMqttPacket( - // header.Flags, - // new MqttPacketBodyReader(buffer.Array, eof - header.RemainingLength, buffer.Count + buffer.Offset), - // 0); - - //var packet = (MqttPublishPacket)serializer.Decode(receivedPacket); - - Assert.AreEqual(publishPacket.Topic, publishPacketCopy.Topic); - Assert.IsTrue(publishPacket.Payload.SequenceEqual(publishPacketCopy.Payload)); + Assert.AreEqual(p.QualityOfServiceLevel, p2.QualityOfServiceLevel); + Assert.AreEqual(p.Dup, p2.Dup); } [TestMethod] - public void SerializeV311_MqttDisconnectPacket() + public void DeserializeV311_MqttPubRecPacket() { - SerializeAndCompare(new MqttDisconnectPacket(), "4AA="); - } + var p = new MqttPubRecPacket + { + PacketIdentifier = 123 + }; - [TestMethod] - public void SerializeV311_MqttPingReqPacket() - { - SerializeAndCompare(new MqttPingReqPacket(), "wAA="); + DeserializeAndCompare(p, "UAIAew=="); } [TestMethod] - public void SerializeV311_MqttPingRespPacket() + public void DeserializeV311_MqttPubRelPacket() { - SerializeAndCompare(new MqttPingRespPacket(), "0AA="); + var p = new MqttPubRelPacket + { + PacketIdentifier = 123 + }; + + DeserializeAndCompare(p, "YgIAew=="); } [TestMethod] - public void SerializeV311_MqttPublishPacket() + public void DeserializeV311_MqttSubAckPacket() { - var p = new MqttPublishPacket + var p = new MqttSubAckPacket { PacketIdentifier = 123, - Dup = true, - Retain = true, - Payload = Encoding.ASCII.GetBytes("HELLO"), - QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce, - Topic = "A/B/C" + ReasonCodes = new List + { + MqttSubscribeReasonCode.GrantedQoS0, + MqttSubscribeReasonCode.GrantedQoS1, + MqttSubscribeReasonCode.GrantedQoS2, + MqttSubscribeReasonCode.UnspecifiedError + } }; - - SerializeAndCompare(p, "Ow4ABUEvQi9DAHtIRUxMTw=="); + + DeserializeAndCompare(p, "kAYAewABAoA="); } [TestMethod] - public void SerializeV500_MqttPublishPacket() + public void DeserializeV311_MqttSubscribePacket() { - var prop = new MqttPublishPacketProperties { UserProperties = new List() }; - - prop.ResponseTopic = "/Response"; - - prop.UserProperties.Add(new MqttUserProperty("Foo", "Bar")); - - var p = new MqttPublishPacket + var p = new MqttSubscribePacket { PacketIdentifier = 123, - Dup = true, - Retain = true, - Payload = Encoding.ASCII.GetBytes("HELLO"), - QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce, - Topic = "A/B/C", - Properties = prop + TopicFilters = new List + { + new MqttTopicFilter { Topic = "A/B/C", QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce }, + new MqttTopicFilter { Topic = "1/2/3", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }, + new MqttTopicFilter { Topic = "x/y/z", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce } + } }; - var deserialized = Roundtrip(p, MqttProtocolVersion.V500); - - Assert.AreEqual(prop.ResponseTopic, deserialized.Properties.ResponseTopic); - Assert.IsTrue(deserialized.Properties.UserProperties.Any(x => x.Name == "Foo")); + DeserializeAndCompare(p, "ghoAewAFQS9CL0MCAAUxLzIvMwEABXgveS96AA=="); } - [TestMethod] - public void SerializeV500_MqttPublishPacket_CorrelationData() + public void DeserializeV311_MqttUnsubAckPacket() { - var data = "123456789"; - var req = new MqttApplicationMessageBuilder() - .WithTopic("Foo") - .WithResponseTopic($"_") - .WithCorrelationData(Guid.NewGuid().ToByteArray()) - .WithPayload(data) - .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce) - .Build(); - - var p = new MqttV500DataConverter().CreatePublishPacket(req); - - var deserialized = Roundtrip(p, MqttProtocolVersion.V500); + var p = new MqttUnsubAckPacket + { + PacketIdentifier = 123 + }; - Assert.IsTrue(p.Payload.SequenceEqual(deserialized.Payload)); + DeserializeAndCompare(p, "sAIAew=="); } [TestMethod] - public void DeserializeV311_MqttPublishPacket() + public void DeserializeV311_MqttUnsubscribePacket() { - var p = new MqttPublishPacket + var p = new MqttUnsubscribePacket { - PacketIdentifier = 123, - Dup = true, - Retain = true, - Payload = Encoding.ASCII.GetBytes("HELLO"), - QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce, - Topic = "A/B/C" + PacketIdentifier = 123 }; - DeserializeAndCompare(p, "Ow4ABUEvQi9DAHtIRUxMTw=="); + p.TopicFilters.Add("A/B/C"); + p.TopicFilters.Add("1/2/3"); + p.TopicFilters.Add("x/y/z"); + + DeserializeAndCompare(p, "ohcAewAFQS9CL0MABTEvMi8zAAV4L3kveg=="); } [TestMethod] - public void DeserializeV311_MqttPublishPacket_Qos1() + public void DetectVersionFromMqttConnectPacket() { - var p = new MqttPublishPacket + var packet = new MqttConnectPacket { - QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce, + ClientId = "XYZ", + Password = Encoding.UTF8.GetBytes("PASS"), + Username = "USER", + KeepAlivePeriod = 123, + CleanSession = true }; - var p2 = Roundtrip(p); + Assert.AreEqual( + MqttProtocolVersion.V310, + DeserializeAndDetectVersion(new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)), Serialize(packet, MqttProtocolVersion.V310))); - Assert.AreEqual(p.QualityOfServiceLevel, p2.QualityOfServiceLevel); - Assert.AreEqual(p.Dup, p2.Dup); + Assert.AreEqual( + MqttProtocolVersion.V311, + DeserializeAndDetectVersion(new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)), Serialize(packet, MqttProtocolVersion.V311))); + + Assert.AreEqual( + MqttProtocolVersion.V500, + DeserializeAndDetectVersion(new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)), Serialize(packet, MqttProtocolVersion.V500))); + + var adapter = new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)); + + var ex = Assert.ThrowsException( + () => DeserializeAndDetectVersion(adapter, WriterFactory().AddMqttHeader(MqttControlPacketType.Connect, new byte[0]))); + Assert.AreEqual("CONNECT packet must have at least 7 bytes.", ex.Message); + ex = Assert.ThrowsException( + () => DeserializeAndDetectVersion(adapter, WriterFactory().AddMqttHeader(MqttControlPacketType.Connect, new byte[7]))); + Assert.AreEqual("Protocol '' not supported.", ex.Message); + ex = Assert.ThrowsException( + () => DeserializeAndDetectVersion(adapter, WriterFactory().AddMqttHeader(MqttControlPacketType.Connect, new byte[] { 255, 255, 0, 0, 0, 0, 0 }))); + Assert.AreEqual("Expected at least 65537 bytes but there are only 7 bytes", ex.Message); } [TestMethod] - public void DeserializeV311_MqttPublishPacket_Qos2() + public void Serialize_LargePacket() { - var p = new MqttPublishPacket + const int payloadLength = 80000; + + var payload = new byte[payloadLength]; + + var value = 0; + for (var i = 0; i < payloadLength; i++) { - QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce, - PacketIdentifier = 1 + if (value > 255) + { + value = 0; + } + + payload[i] = (byte)value; + } + + var publishPacket = new MqttPublishPacket + { + Topic = "abcdefghijklmnopqrstuvwxyz0123456789", + Payload = payload }; - var p2 = Roundtrip(p); - Assert.AreEqual(p.QualityOfServiceLevel, p2.QualityOfServiceLevel); - Assert.AreEqual(p.Dup, p2.Dup); + var serializationHelper = new MqttPacketSerializationHelper(); + + var buffer = serializationHelper.Encode(publishPacket); + var publishPacketCopy = serializationHelper.Decode(buffer) as MqttPublishPacket; + + Assert.IsNotNull(publishPacketCopy); + Assert.AreEqual(publishPacket.Topic, publishPacketCopy.Topic); + CollectionAssert.AreEqual(publishPacket.Payload, publishPacketCopy.Payload); + + // Now modify the payload and test again. + publishPacket.Payload = Encoding.UTF8.GetBytes("MQTT"); + + buffer = serializationHelper.Encode(publishPacket); + var publishPacketCopy2 = serializationHelper.Decode(buffer) as MqttPublishPacket; + + Assert.IsNotNull(publishPacketCopy2); + Assert.AreEqual(publishPacket.Topic, publishPacketCopy2.Topic); + CollectionAssert.AreEqual(publishPacket.Payload, publishPacketCopy2.Payload); } [TestMethod] - public void DeserializeV311_MqttPublishPacket_Qos3() + public void SerializeV310_MqttConnAckPacket() { - var p = new MqttPublishPacket + var p = new MqttConnAckPacket { - QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce, - PacketIdentifier = 1 + ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized }; - var p2 = Roundtrip(p); - - Assert.AreEqual(p.QualityOfServiceLevel, p2.QualityOfServiceLevel); - Assert.AreEqual(p.Dup, p2.Dup); + SerializeAndCompare(p, "IAIABQ==", MqttProtocolVersion.V310); } - [TestMethod] - public void DeserializeV311_MqttPublishPacket_DupFalse() + public void SerializeV310_MqttConnectPacket() { - var p = new MqttPublishPacket + var p = new MqttConnectPacket { - Dup = false, + ClientId = "XYZ", + Password = Encoding.UTF8.GetBytes("PASS"), + Username = "USER", + KeepAlivePeriod = 123, + CleanSession = true }; - var p2 = Roundtrip(p); - - Assert.AreEqual(p.Dup, p2.Dup); + SerializeAndCompare(p, "EB0ABk1RSXNkcAPCAHsAA1hZWgAEVVNFUgAEUEFTUw==", MqttProtocolVersion.V310); } [TestMethod] - public void SerializeV311_MqttPubAckPacket() + public void SerializeV311_MqttConnAckPacket() { - var p = new MqttPubAckPacket + var p = new MqttConnAckPacket { - PacketIdentifier = 123 + IsSessionPresent = true, + ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized }; - SerializeAndCompare(p, "QAIAew=="); + SerializeAndCompare(p, "IAIBBQ=="); } [TestMethod] - public void DeserializeV311_MqttPubAckPacket() + public void SerializeV311_MqttConnectPacket() { - var p = new MqttPubAckPacket + var p = new MqttConnectPacket { - PacketIdentifier = 123 + ClientId = "XYZ", + Password = Encoding.UTF8.GetBytes("PASS"), + Username = "USER", + KeepAlivePeriod = 123, + CleanSession = true }; - DeserializeAndCompare(p, "QAIAew=="); + SerializeAndCompare(p, "EBsABE1RVFQEwgB7AANYWVoABFVTRVIABFBBU1M="); } [TestMethod] - public void SerializeV311_MqttPubRecPacket() + public void SerializeV311_MqttConnectPacketWithWillMessage() { - var p = new MqttPubRecPacket + var p = new MqttConnectPacket { - PacketIdentifier = 123 + ClientId = "XYZ", + Password = Encoding.UTF8.GetBytes("PASS"), + Username = "USER", + KeepAlivePeriod = 123, + CleanSession = true, + WillFlag = true, + WillTopic = "My/last/will", + WillMessage = Encoding.UTF8.GetBytes("Good byte."), + WillQoS = MqttQualityOfServiceLevel.AtLeastOnce, + WillRetain = true }; - SerializeAndCompare(p, "UAIAew=="); + SerializeAndCompare(p, "EDUABE1RVFQE7gB7AANYWVoADE15L2xhc3Qvd2lsbAAKR29vZCBieXRlLgAEVVNFUgAEUEFTUw=="); } [TestMethod] - public void DeserializeV311_MqttPubRecPacket() + public void SerializeV311_MqttDisconnectPacket() { - var p = new MqttPubRecPacket - { - PacketIdentifier = 123 - }; - - DeserializeAndCompare(p, "UAIAew=="); + SerializeAndCompare(new MqttDisconnectPacket(), "4AA="); } [TestMethod] - public void SerializeV311_MqttPubRelPacket() + public void SerializeV311_MqttPingReqPacket() { - var p = new MqttPubRelPacket - { - PacketIdentifier = 123 - }; + SerializeAndCompare(new MqttPingReqPacket(), "wAA="); + } - SerializeAndCompare(p, "YgIAew=="); + [TestMethod] + public void SerializeV311_MqttPingRespPacket() + { + SerializeAndCompare(new MqttPingRespPacket(), "0AA="); } [TestMethod] - public void DeserializeV311_MqttPubRelPacket() + public void SerializeV311_MqttPubAckPacket() { - var p = new MqttPubRelPacket + var p = new MqttPubAckPacket { PacketIdentifier = 123 }; - DeserializeAndCompare(p, "YgIAew=="); + SerializeAndCompare(p, "QAIAew=="); } [TestMethod] @@ -473,44 +456,41 @@ namespace MQTTnet.Tests } [TestMethod] - public void DeserializeV311_MqttPubCompPacket() + public void SerializeV311_MqttPublishPacket() { - var p = new MqttPubCompPacket + var p = new MqttPublishPacket { - PacketIdentifier = 123 + PacketIdentifier = 123, + Dup = true, + Retain = true, + Payload = Encoding.ASCII.GetBytes("HELLO"), + QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce, + Topic = "A/B/C" }; - DeserializeAndCompare(p, "cAIAew=="); + SerializeAndCompare(p, "Ow4ABUEvQi9DAHtIRUxMTw=="); } [TestMethod] - public void SerializeV311_MqttSubscribePacket() + public void SerializeV311_MqttPubRecPacket() { - var p = new MqttSubscribePacket + var p = new MqttPubRecPacket { PacketIdentifier = 123 }; - p.TopicFilters.Add(new MqttTopicFilter { Topic = "A/B/C", QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce }); - p.TopicFilters.Add(new MqttTopicFilter { Topic = "1/2/3", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }); - p.TopicFilters.Add(new MqttTopicFilter { Topic = "x/y/z", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }); - - SerializeAndCompare(p, "ghoAewAFQS9CL0MCAAUxLzIvMwEABXgveS96AA=="); + SerializeAndCompare(p, "UAIAew=="); } [TestMethod] - public void DeserializeV311_MqttSubscribePacket() + public void SerializeV311_MqttPubRelPacket() { - var p = new MqttSubscribePacket + var p = new MqttPubRelPacket { PacketIdentifier = 123 }; - p.TopicFilters.Add(new MqttTopicFilter { Topic = "A/B/C", QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce }); - p.TopicFilters.Add(new MqttTopicFilter { Topic = "1/2/3", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }); - p.TopicFilters.Add(new MqttTopicFilter { Topic = "x/y/z", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }); - - DeserializeAndCompare(p, "ghoAewAFQS9CL0MCAAUxLzIvMwEABXgveS96AA=="); + SerializeAndCompare(p, "YgIAew=="); } [TestMethod] @@ -518,50 +498,47 @@ namespace MQTTnet.Tests { var p = new MqttSubAckPacket { - PacketIdentifier = 123 + PacketIdentifier = 123, + ReasonCodes = new List + { + MqttSubscribeReasonCode.GrantedQoS0, + MqttSubscribeReasonCode.GrantedQoS1, + MqttSubscribeReasonCode.GrantedQoS2, + MqttSubscribeReasonCode.UnspecifiedError + } }; - - p.ReturnCodes.Add(MqttSubscribeReturnCode.SuccessMaximumQoS0); - p.ReturnCodes.Add(MqttSubscribeReturnCode.SuccessMaximumQoS1); - p.ReturnCodes.Add(MqttSubscribeReturnCode.SuccessMaximumQoS2); - p.ReturnCodes.Add(MqttSubscribeReturnCode.Failure); - + SerializeAndCompare(p, "kAYAewABAoA="); } [TestMethod] - public void DeserializeV311_MqttSubAckPacket() + public void SerializeV311_MqttSubscribePacket() { - var p = new MqttSubAckPacket + var p = new MqttSubscribePacket { PacketIdentifier = 123 }; - p.ReturnCodes.Add(MqttSubscribeReturnCode.SuccessMaximumQoS0); - p.ReturnCodes.Add(MqttSubscribeReturnCode.SuccessMaximumQoS1); - p.ReturnCodes.Add(MqttSubscribeReturnCode.SuccessMaximumQoS2); - p.ReturnCodes.Add(MqttSubscribeReturnCode.Failure); + p.TopicFilters.Add(new MqttTopicFilter { Topic = "A/B/C", QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce }); + p.TopicFilters.Add(new MqttTopicFilter { Topic = "1/2/3", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }); + p.TopicFilters.Add(new MqttTopicFilter { Topic = "x/y/z", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }); - DeserializeAndCompare(p, "kAYAewABAoA="); + SerializeAndCompare(p, "ghoAewAFQS9CL0MCAAUxLzIvMwEABXgveS96AA=="); } [TestMethod] - public void SerializeV311_MqttUnsubscribePacket() + public void SerializeV311_MqttUnsubAckPacket() { - var p = new MqttUnsubscribePacket + var p = new MqttUnsubAckPacket { PacketIdentifier = 123 }; - p.TopicFilters.Add("A/B/C"); - p.TopicFilters.Add("1/2/3"); - p.TopicFilters.Add("x/y/z"); - - SerializeAndCompare(p, "ohcAewAFQS9CL0MABTEvMi8zAAV4L3kveg=="); + SerializeAndCompare(p, "sAIAew=="); } [TestMethod] - public void DeserializeV311_MqttUnsubscribePacket() + public void SerializeV311_MqttUnsubscribePacket() { var p = new MqttUnsubscribePacket { @@ -572,137 +549,79 @@ namespace MQTTnet.Tests p.TopicFilters.Add("1/2/3"); p.TopicFilters.Add("x/y/z"); - DeserializeAndCompare(p, "ohcAewAFQS9CL0MABTEvMi8zAAV4L3kveg=="); + SerializeAndCompare(p, "ohcAewAFQS9CL0MABTEvMi8zAAV4L3kveg=="); } - [TestMethod] - public void SerializeV311_MqttUnsubAckPacket() - { - var p = new MqttUnsubAckPacket - { - PacketIdentifier = 123 - }; - - SerializeAndCompare(p, "sAIAew=="); - } - [TestMethod] - public void DeserializeV311_MqttUnsubAckPacket() + void DeserializeAndCompare(MqttPacket packet, string expectedBase64Value, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311) { - var p = new MqttUnsubAckPacket - { - PacketIdentifier = 123 - }; + var writer = WriterFactory(); - DeserializeAndCompare(p, "sAIAew=="); - } + var serializer = MqttPacketFormatterAdapter.GetMqttPacketFormatter(protocolVersion, writer); + var buffer1 = serializer.Encode(packet); - void SerializeAndCompare(MqttBasePacket packet, string expectedBase64Value, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311) - { - Assert.AreEqual(expectedBase64Value, Convert.ToBase64String(Serialize(packet, protocolVersion))); + using (var headerStream = new MemoryStream(buffer1.Join().ToArray())) + { + using (var channel = new TestMqttChannel(headerStream)) + { + using (var adapter = new MqttChannelAdapter( + channel, + new MqttPacketFormatterAdapter(protocolVersion, new MqttBufferWriter(4096, 65535)), + null, + new MqttNetEventLogger())) + { + var receivedPacket = adapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult(); + + var buffer2 = serializer.Encode(receivedPacket); + + Assert.AreEqual(expectedBase64Value, Convert.ToBase64String(buffer2.Join().ToArray())); + } + } + } } - byte[] Serialize(MqttBasePacket packet, MqttProtocolVersion protocolVersion) + MqttProtocolVersion DeserializeAndDetectVersion(MqttPacketFormatterAdapter packetFormatterAdapter, byte[] buffer) { - return MqttPacketFormatterAdapter.GetMqttPacketFormatter(protocolVersion, WriterFactory()).Encode(packet).ToArray(); - } + var channel = new TestMqttChannel(buffer); + var adapter = new MqttChannelAdapter(channel, packetFormatterAdapter, null, new MqttNetEventLogger()); - protected virtual IMqttPacketWriter WriterFactory() - { - return new MqttPacketWriter(); + adapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult(); + return packetFormatterAdapter.ProtocolVersion; } - protected virtual IMqttPacketBodyReader ReaderFactory(byte[] data) + MqttBufferReader ReaderFactory(byte[] data) { - return new MqttPacketBodyReader(data, 0, data.Length); + var reader = new MqttBufferReader(); + reader.SetBuffer(data, 0, data.Length); + return reader; } - void DeserializeAndCompare(MqttBasePacket packet, string expectedBase64Value, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311) + TPacket Roundtrip(TPacket packet, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311, MqttBufferWriter bufferWriter = null) where TPacket : MqttPacket { - var writer = WriterFactory(); - + var writer = bufferWriter ?? WriterFactory(); var serializer = MqttPacketFormatterAdapter.GetMqttPacketFormatter(protocolVersion, writer); - var buffer1 = serializer.Encode(packet); + var buffer = serializer.Encode(packet); - using (var headerStream = new MemoryStream(buffer1.ToArray())) + using (var channel = new TestMqttChannel(buffer.Join().ToArray())) { - var channel = new TestMqttChannel(headerStream); - var adapter = new MqttChannelAdapter(channel, new MqttPacketFormatterAdapter(protocolVersion, writer), null, new MqttNetEventLogger()); - var receivedPacket = adapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult(); - - var buffer2 = serializer.Encode(receivedPacket); - - Assert.AreEqual(expectedBase64Value, Convert.ToBase64String(buffer2.ToArray())); - - //adapter.ReceivePacketAsync(CancellationToken.None); - //var fixedHeader = new byte[2]; - //var header = new MqttPacketReader(channel).ReadFixedHeaderAsync(fixedHeader, CancellationToken.None).GetAwaiter().GetResult().FixedHeader; - - //using (var bodyStream = new MemoryStream(Join(buffer1), (int)headerStream.Position, header.RemainingLength)) - //{ - // var reader = ReaderFactory(bodyStream.ToArray()); - // var deserializedPacket = serializer.Decode(new ReceivedMqttPacket(header.Flags, reader, 0)); - // var buffer2 = serializer.Encode(deserializedPacket); - - // Assert.AreEqual(expectedBase64Value, Convert.ToBase64String(Join(buffer2))); - //} + var adapter = new MqttChannelAdapter(channel, new MqttPacketFormatterAdapter(protocolVersion, new MqttBufferWriter(4096, 65535)), null, new MqttNetEventLogger()); + return (TPacket)adapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult(); } } - TPacket Roundtrip(TPacket packet, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311) - where TPacket : MqttBasePacket + byte[] Serialize(MqttPacket packet, MqttProtocolVersion protocolVersion) { - var writer = WriterFactory(); - var serializer = MqttPacketFormatterAdapter.GetMqttPacketFormatter(protocolVersion, writer); - var buffer = serializer.Encode(packet); - - var channel = new TestMqttChannel(buffer.ToArray()); - var adapter = new MqttChannelAdapter(channel, new MqttPacketFormatterAdapter(protocolVersion, writer), null, new MqttNetEventLogger()); - return (TPacket)adapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult(); - - //using (var headerStream = new MemoryStream(buffer1.ToArray())) - //{ - - - - - // //var fixedHeader = new byte[2]; - - // //var header = new MqttPacketReader(channel).ReadFixedHeaderAsync(fixedHeader, CancellationToken.None).GetAwaiter().GetResult().FixedHeader; - - // //using (var bodyStream = new MemoryStream(Join(buffer1), (int)headerStream.Position, (int)header.RemainingLength)) - // //{ - // // var reader = ReaderFactory(bodyStream.ToArray()); - // // return (T)serializer.Decode(new ReceivedMqttPacket(header.Flags, reader, 0)); - // //} - //} + return MqttPacketFormatterAdapter.GetMqttPacketFormatter(protocolVersion, WriterFactory()).Encode(packet).Join().ToArray(); } - MqttProtocolVersion DeserializeAndDetectVersion(MqttPacketFormatterAdapter packetFormatterAdapter, byte[] buffer) + void SerializeAndCompare(MqttPacket packet, string expectedBase64Value, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311) { - var channel = new TestMqttChannel(buffer); - var adapter = new MqttChannelAdapter(channel, packetFormatterAdapter, null, new MqttNetEventLogger()); - - adapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult(); - return packetFormatterAdapter.ProtocolVersion; - - //using (var headerStream = new MemoryStream(buffer)) - //{ - - - - - // //var fixedHeader = new byte[2]; - // //var header = new MqttPacketReader(channel).ReadFixedHeaderAsync(fixedHeader, CancellationToken.None).GetAwaiter().GetResult().FixedHeader; + Assert.AreEqual(expectedBase64Value, Convert.ToBase64String(Serialize(packet, protocolVersion))); + } - // //using (var bodyStream = new MemoryStream(buffer, (int)headerStream.Position, (int)header.RemainingLength)) - // //{ - // // var reader = ReaderFactory(bodyStream.ToArray()); - // // var packet = new ReceivedMqttPacket(header.Flags, reader, 0); - // // packetFormatterAdapter.DetectProtocolVersion(packet); - // // return adapter.ProtocolVersion; - // //} - //} + MqttBufferWriter WriterFactory() + { + return new MqttBufferWriter(4096, 65535); } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Tests.cs b/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Tests.cs new file mode 100644 index 0000000..7532c34 --- /dev/null +++ b/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V3_Tests.cs @@ -0,0 +1,495 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Exceptions; +using MQTTnet.Formatter; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Tests +{ + [TestClass] + public sealed class MqttPacketSerialization_V3_Tests + { + [TestMethod] + public void Serialize_Full_MqttAuthPacket_V311() + { + var authPacket = new MqttAuthPacket(); + Assert.ThrowsException(() => MqttPacketSerializationHelper.EncodeAndDecodePacket(authPacket, MqttProtocolVersion.V311)); + } + + [TestMethod] + public void Serialize_Full_MqttConnAckPacket_V311() + { + var connAckPacket = new MqttConnAckPacket + { + AuthenticationData = Encoding.UTF8.GetBytes("AuthenticationData"), + AuthenticationMethod = "AuthenticationMethod", + ReasonCode = MqttConnectReasonCode.ServerUnavailable, + ReasonString = "ReasonString", + ReceiveMaximum = 123, + ResponseInformation = "ResponseInformation", + RetainAvailable = true, + ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized, + ServerReference = "ServerReference", + AssignedClientIdentifier = "AssignedClientIdentifier", + IsSessionPresent = true, + MaximumPacketSize = 456, + MaximumQoS = MqttQualityOfServiceLevel.ExactlyOnce, + ServerKeepAlive = 789, + SessionExpiryInterval = 852, + SharedSubscriptionAvailable = true, + SubscriptionIdentifiersAvailable = true, + TopicAliasMaximum = 963, + WildcardSubscriptionAvailable = true, + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(connAckPacket, MqttProtocolVersion.V311); + + CollectionAssert.AreEqual(null, deserialized.AuthenticationData); // Not supported in v3.1.1 + Assert.AreEqual(null, deserialized.AuthenticationMethod); // Not supported in v3.1.1 + //Assert.AreEqual(connAckPacket.ReasonCode, deserialized.ReasonCode); + Assert.AreEqual(null, deserialized.ReasonString); // Not supported in v3.1.1 + Assert.AreEqual(0U, deserialized.ReceiveMaximum); // Not supported in v3.1.1 + Assert.AreEqual(null, deserialized.ResponseInformation); // Not supported in v3.1.1 + Assert.AreEqual(false, deserialized.RetainAvailable); // Not supported in v3.1.1 + Assert.AreEqual(MqttConnectReturnCode.ConnectionRefusedNotAuthorized, deserialized.ReturnCode); + Assert.AreEqual(null, deserialized.ServerReference); // Not supported in v3.1.1 + Assert.AreEqual(null, deserialized.AssignedClientIdentifier); // Not supported in v3.1.1 + Assert.AreEqual(connAckPacket.IsSessionPresent, deserialized.IsSessionPresent); + Assert.AreEqual(0U, deserialized.MaximumPacketSize); // Not supported in v3.1.1 + Assert.AreEqual(MqttQualityOfServiceLevel.AtMostOnce, deserialized.MaximumQoS); // Not supported in v3.1.1 + Assert.AreEqual(0U, deserialized.ServerKeepAlive); // Not supported in v3.1.1 + Assert.AreEqual(0U, deserialized.SessionExpiryInterval); // Not supported in v3.1.1 + Assert.AreEqual(false, deserialized.SharedSubscriptionAvailable); // Not supported in v3.1.1 + Assert.AreEqual(false, deserialized.SubscriptionIdentifiersAvailable); // Not supported in v3.1.1 + Assert.AreEqual(0U, deserialized.TopicAliasMaximum); // Not supported in v3.1.1 + Assert.AreEqual(false, deserialized.WildcardSubscriptionAvailable); + Assert.IsNull(deserialized.UserProperties); // Not supported in v3.1.1 + } + + [TestMethod] + public void Serialize_Full_MqttConnAckPacket_V310() + { + var connAckPacket = new MqttConnAckPacket + { + AuthenticationData = Encoding.UTF8.GetBytes("AuthenticationData"), + AuthenticationMethod = "AuthenticationMethod", + ReasonCode = MqttConnectReasonCode.ServerUnavailable, + ReasonString = "ReasonString", + ReceiveMaximum = 123, + ResponseInformation = "ResponseInformation", + RetainAvailable = true, + ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized, + ServerReference = "ServerReference", + AssignedClientIdentifier = "AssignedClientIdentifier", + IsSessionPresent = true, + MaximumPacketSize = 456, + MaximumQoS = MqttQualityOfServiceLevel.ExactlyOnce, + ServerKeepAlive = 789, + SessionExpiryInterval = 852, + SharedSubscriptionAvailable = true, + SubscriptionIdentifiersAvailable = true, + TopicAliasMaximum = 963, + WildcardSubscriptionAvailable = true, + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(connAckPacket, MqttProtocolVersion.V310); + + CollectionAssert.AreEqual(null, deserialized.AuthenticationData); // Not supported in v3.1.1 + Assert.AreEqual(null, deserialized.AuthenticationMethod); // Not supported in v3.1.1 + //Assert.AreEqual(connAckPacket.ReasonCode, deserialized.ReasonCode); + Assert.AreEqual(null, deserialized.ReasonString); // Not supported in v3.1.1 + Assert.AreEqual(0U, deserialized.ReceiveMaximum); // Not supported in v3.1.1 + Assert.AreEqual(null, deserialized.ResponseInformation); // Not supported in v3.1.1 + Assert.AreEqual(false, deserialized.RetainAvailable); // Not supported in v3.1.1 + Assert.AreEqual(MqttConnectReturnCode.ConnectionRefusedNotAuthorized, deserialized.ReturnCode); + Assert.AreEqual(null, deserialized.ServerReference); // Not supported in v3.1.1 + Assert.AreEqual(null, deserialized.AssignedClientIdentifier); // Not supported in v3.1.1 + Assert.AreEqual(false, deserialized.IsSessionPresent); // Not supported in v3.1.0 <- ! + Assert.AreEqual(0U, deserialized.MaximumPacketSize); // Not supported in v3.1.1 + Assert.AreEqual(MqttQualityOfServiceLevel.AtMostOnce, deserialized.MaximumQoS); // Not supported in v3.1.1 + Assert.AreEqual(0U, deserialized.ServerKeepAlive); // Not supported in v3.1.1 + Assert.AreEqual(0U, deserialized.SessionExpiryInterval); // Not supported in v3.1.1 + Assert.AreEqual(false, deserialized.SharedSubscriptionAvailable); // Not supported in v3.1.1 + Assert.AreEqual(false, deserialized.SubscriptionIdentifiersAvailable); // Not supported in v3.1.1 + Assert.AreEqual(0U, deserialized.TopicAliasMaximum); // Not supported in v3.1.1 + Assert.AreEqual(false, deserialized.WildcardSubscriptionAvailable); + Assert.IsNull(deserialized.UserProperties); // Not supported in v3.1.1 + } + + [TestMethod] + public void Serialize_Full_MqttConnectPacket_V311() + { + var connectPacket = new MqttConnectPacket + { + Username = "Username", + Password = Encoding.UTF8.GetBytes("Password"), + ClientId = "ClientId", + AuthenticationData = Encoding.UTF8.GetBytes("AuthenticationData"), + AuthenticationMethod = "AuthenticationMethod", + CleanSession = true, + ReceiveMaximum = 123, + WillFlag = true, + WillTopic = "WillTopic", + WillMessage = Encoding.UTF8.GetBytes("WillMessage"), + WillRetain = true, + KeepAlivePeriod = 456, + MaximumPacketSize = 789, + RequestProblemInformation = true, + RequestResponseInformation = true, + SessionExpiryInterval = 27, + TopicAliasMaximum = 67, + WillContentType = "WillContentType", + WillCorrelationData = Encoding.UTF8.GetBytes("WillCorrelationData"), + WillDelayInterval = 782, + WillQoS = MqttQualityOfServiceLevel.ExactlyOnce, + WillResponseTopic = "WillResponseTopic", + WillMessageExpiryInterval = 542, + WillPayloadFormatIndicator = MqttPayloadFormatIndicator.CharacterData, + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + }, + WillUserProperties = new List + { + new MqttUserProperty("WillFoo", "WillBar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(connectPacket, MqttProtocolVersion.V311); + + Assert.AreEqual(connectPacket.Username, deserialized.Username); + CollectionAssert.AreEqual(connectPacket.Password, deserialized.Password); + Assert.AreEqual(connectPacket.ClientId, deserialized.ClientId); + CollectionAssert.AreEqual(null, deserialized.AuthenticationData); // Not supported in v3.1.1 + Assert.AreEqual(null, deserialized.AuthenticationMethod); // Not supported in v3.1.1 + Assert.AreEqual(connectPacket.CleanSession, deserialized.CleanSession); + Assert.AreEqual(0L, deserialized.ReceiveMaximum); // Not supported in v3.1.1 + Assert.AreEqual(connectPacket.WillFlag, deserialized.WillFlag); + Assert.AreEqual(connectPacket.WillTopic, deserialized.WillTopic); + CollectionAssert.AreEqual(connectPacket.WillMessage, deserialized.WillMessage); + Assert.AreEqual(connectPacket.WillRetain, deserialized.WillRetain); + Assert.AreEqual(connectPacket.KeepAlivePeriod, deserialized.KeepAlivePeriod); + // MaximumPacketSize not available in MQTTv3. + // RequestProblemInformation not available in MQTTv3. + // RequestResponseInformation not available in MQTTv3. + // SessionExpiryInterval not available in MQTTv3. + // TopicAliasMaximum not available in MQTTv3. + // WillContentType not available in MQTTv3. + // WillCorrelationData not available in MQTTv3. + // WillDelayInterval not available in MQTTv3. + Assert.AreEqual(connectPacket.WillQoS, deserialized.WillQoS); + // WillResponseTopic not available in MQTTv3. + // WillMessageExpiryInterval not available in MQTTv3. + // WillPayloadFormatIndicator not available in MQTTv3. + Assert.IsNull(deserialized.UserProperties); // Not supported in v3.1.1 + Assert.IsNull(deserialized.WillUserProperties); // Not supported in v3.1.1 + } + + [TestMethod] + public void Serialize_Full_MqttDisconnectPacket_V311() + { + var disconnectPacket = new MqttDisconnectPacket + { + ReasonCode = MqttDisconnectReasonCode.NormalDisconnection, + ReasonString = "ReasonString", + ServerReference = "ServerReference", + SessionExpiryInterval = 234, + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(disconnectPacket, MqttProtocolVersion.V311); + + + Assert.AreEqual(disconnectPacket.ReasonCode, deserialized.ReasonCode); + Assert.AreEqual(null, deserialized.ReasonString); // Not supported in v3.1.1 + Assert.AreEqual(null, deserialized.ServerReference); // Not supported in v3.1.1 + Assert.AreEqual(0U, deserialized.SessionExpiryInterval); // Not supported in v3.1.1 + CollectionAssert.AreEqual(null, deserialized.UserProperties); + } + + [TestMethod] + public void Serialize_Full_MqttPingReqPacket_V311() + { + var pingReqPacket = new MqttPingReqPacket(); + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pingReqPacket, MqttProtocolVersion.V311); + + Assert.IsNotNull(deserialized); + } + + [TestMethod] + public void Serialize_Full_MqttPingRespPacket_V311() + { + var pingRespPacket = new MqttPingRespPacket(); + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pingRespPacket, MqttProtocolVersion.V311); + + Assert.IsNotNull(deserialized); + } + + [TestMethod] + public void Serialize_Full_MqttPubAckPacket_V311() + { + var pubAckPacket = new MqttPubAckPacket + { + PacketIdentifier = 123, + ReasonCode = MqttPubAckReasonCode.NoMatchingSubscribers, + ReasonString = "ReasonString", + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubAckPacket, MqttProtocolVersion.V311); + + Assert.AreEqual(pubAckPacket.PacketIdentifier, deserialized.PacketIdentifier); + Assert.AreEqual(MqttPubAckReasonCode.Success, deserialized.ReasonCode); // Not supported in v3.1.1 + Assert.AreEqual(null, deserialized.ReasonString); // Not supported in v3.1.1 + CollectionAssert.AreEqual(null, deserialized.UserProperties); + } + + [TestMethod] + public void Serialize_Full_MqttPubCompPacket_V311() + { + var pubCompPacket = new MqttPubCompPacket + { + PacketIdentifier = 123, + ReasonCode = MqttPubCompReasonCode.PacketIdentifierNotFound, + ReasonString = "ReasonString", + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubCompPacket, MqttProtocolVersion.V311); + + Assert.AreEqual(pubCompPacket.PacketIdentifier, deserialized.PacketIdentifier); + // ReasonCode not available in MQTTv3. + // ReasonString not available in MQTTv3. + // UserProperties not available in MQTTv3. + Assert.IsNull(deserialized.UserProperties); + } + + [TestMethod] + public void Serialize_Full_MqttPublishPacket_V311() + { + var publishPacket = new MqttPublishPacket + { + PacketIdentifier = 123, + Dup = true, + Retain = true, + Payload = Encoding.ASCII.GetBytes("Payload"), + QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce, + Topic = "Topic", + ResponseTopic = "/Response", + ContentType = "Content-Type", + CorrelationData = Encoding.UTF8.GetBytes("CorrelationData"), + TopicAlias = 27, + SubscriptionIdentifiers = new List + { + 123 + }, + MessageExpiryInterval = 38, + PayloadFormatIndicator = MqttPayloadFormatIndicator.CharacterData, + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(publishPacket, MqttProtocolVersion.V311); + + Assert.AreEqual(publishPacket.PacketIdentifier, deserialized.PacketIdentifier); + Assert.AreEqual(publishPacket.Dup, deserialized.Dup); + Assert.AreEqual(publishPacket.Retain, deserialized.Retain); + CollectionAssert.AreEqual(publishPacket.Payload, deserialized.Payload); + Assert.AreEqual(publishPacket.QualityOfServiceLevel, deserialized.QualityOfServiceLevel); + Assert.AreEqual(publishPacket.Topic, deserialized.Topic); + Assert.AreEqual(null, deserialized.ResponseTopic); // Not supported in v3.1.1. + Assert.AreEqual(null, deserialized.ContentType); // Not supported in v3.1.1. + CollectionAssert.AreEqual(null, deserialized.CorrelationData); // Not supported in v3.1.1. + Assert.AreEqual(0U, deserialized.TopicAlias); // Not supported in v3.1.1. + CollectionAssert.AreEqual(null, deserialized.SubscriptionIdentifiers); // Not supported in v3.1.1 + Assert.AreEqual(0U, deserialized.MessageExpiryInterval); // Not supported in v3.1.1 + Assert.AreEqual(MqttPayloadFormatIndicator.Unspecified, deserialized.PayloadFormatIndicator); // Not supported in v3.1.1 + Assert.IsNull(deserialized.UserProperties); // Not supported in v3.1.1 + } + + [TestMethod] + public void Serialize_Full_MqttPubRecPacket_V311() + { + var pubRecPacket = new MqttPubRecPacket + { + PacketIdentifier = 123, + ReasonCode = MqttPubRecReasonCode.UnspecifiedError, + ReasonString = "ReasonString", + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubRecPacket, MqttProtocolVersion.V311); + + Assert.AreEqual(pubRecPacket.PacketIdentifier, deserialized.PacketIdentifier); + // ReasonCode not available in MQTTv3. + // ReasonString not available in MQTTv3. + // UserProperties not available in MQTTv3. + Assert.IsNull(deserialized.UserProperties); + } + + [TestMethod] + public void Serialize_Full_MqttPubRelPacket_V311() + { + var pubRelPacket = new MqttPubRelPacket + { + PacketIdentifier = 123, + ReasonCode = MqttPubRelReasonCode.PacketIdentifierNotFound, + ReasonString = "ReasonString", + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubRelPacket, MqttProtocolVersion.V311); + + Assert.AreEqual(pubRelPacket.PacketIdentifier, deserialized.PacketIdentifier); + // ReasonCode not available in MQTTv3. + // ReasonString not available in MQTTv3. + // UserProperties not available in MQTTv3. + Assert.IsNull(deserialized.UserProperties); + } + + [TestMethod] + public void Serialize_Full_MqttSubAckPacket_V311() + { + var subAckPacket = new MqttSubAckPacket + { + PacketIdentifier = 123, + ReasonString = "ReasonString", + ReasonCodes = new List + { + MqttSubscribeReasonCode.GrantedQoS1 + }, + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(subAckPacket, MqttProtocolVersion.V311); + + Assert.AreEqual(subAckPacket.PacketIdentifier, deserialized.PacketIdentifier); + Assert.AreEqual(null, deserialized.ReasonString); // Not supported in v3.1.1 + Assert.AreEqual(subAckPacket.ReasonCodes.Count, deserialized.ReasonCodes.Count); + Assert.AreEqual(subAckPacket.ReasonCodes[0], deserialized.ReasonCodes[0]); + CollectionAssert.AreEqual(null, deserialized.UserProperties); // Not supported in v3.1.1 + } + + [TestMethod] + public void Serialize_Full_MqttSubscribePacket_V311() + { + var subscribePacket = new MqttSubscribePacket + { + PacketIdentifier = 123, + SubscriptionIdentifier = 456, + TopicFilters = new List + { + new MqttTopicFilter + { + Topic = "Topic", + NoLocal = true, + RetainHandling = MqttRetainHandling.SendAtSubscribeIfNewSubscriptionOnly, + RetainAsPublished = true, + QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce + } + }, + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(subscribePacket, MqttProtocolVersion.V311); + + Assert.AreEqual(subscribePacket.PacketIdentifier, deserialized.PacketIdentifier); + Assert.AreEqual(0U, deserialized.SubscriptionIdentifier); // Not supported in v3.1.1 + Assert.AreEqual(1, deserialized.TopicFilters.Count); + Assert.AreEqual(subscribePacket.TopicFilters[0].Topic, deserialized.TopicFilters[0].Topic); + Assert.AreEqual(false, deserialized.TopicFilters[0].NoLocal); // Not supported in v3.1.1 + Assert.AreEqual(MqttRetainHandling.SendAtSubscribe, deserialized.TopicFilters[0].RetainHandling); // Not supported in v3.1.1 + Assert.AreEqual(false, deserialized.TopicFilters[0].RetainAsPublished); // Not supported in v3.1.1 + Assert.AreEqual(subscribePacket.TopicFilters[0].QualityOfServiceLevel, deserialized.TopicFilters[0].QualityOfServiceLevel); + CollectionAssert.AreEqual(null, deserialized.UserProperties); // Not supported in v3.1.1 + } + + [TestMethod] + public void Serialize_Full_MqttUnsubAckPacket_V311() + { + var unsubAckPacket = new MqttUnsubAckPacket + { + PacketIdentifier = 123, + ReasonCodes = new List + { + MqttUnsubscribeReasonCode.ImplementationSpecificError + }, + ReasonString = "ReasonString", + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(unsubAckPacket, MqttProtocolVersion.V311); + + Assert.AreEqual(unsubAckPacket.PacketIdentifier, deserialized.PacketIdentifier); + Assert.AreEqual(null, deserialized.ReasonString); // Not supported in v3.1.1 + CollectionAssert.AreEqual(null, deserialized.ReasonCodes); // Not supported in v3.1.1 + CollectionAssert.AreEqual(null, deserialized.UserProperties); // Not supported in v3.1.1 + } + + [TestMethod] + public void Serialize_Full_MqttUnsubscribePacket_V311() + { + var unsubscribePacket = new MqttUnsubscribePacket + { + PacketIdentifier = 123, + TopicFilters = new List + { + "TopicFilter1" + }, + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(unsubscribePacket, MqttProtocolVersion.V311); + + Assert.AreEqual(unsubscribePacket.PacketIdentifier, deserialized.PacketIdentifier); + Assert.AreEqual(unsubscribePacket.TopicFilters.Count, deserialized.TopicFilters.Count); + Assert.AreEqual(unsubscribePacket.TopicFilters[0], deserialized.TopicFilters[0]); + CollectionAssert.AreEqual(null, deserialized.UserProperties); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V5_Tests.cs b/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V5_Tests.cs new file mode 100644 index 0000000..bbbf191 --- /dev/null +++ b/Source/MQTTnet.Tests/Formatter/MqttPacketSerialization_V5_Tests.cs @@ -0,0 +1,454 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Formatter; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Tests +{ + [TestClass] + public sealed class MqttPacketSerialization_V5_Tests + { + [TestMethod] + public void Serialize_Full_MqttAuthPacket_V500() + { + var authPacket = new MqttAuthPacket + { + AuthenticationData = Encoding.UTF8.GetBytes("AuthenticationData"), + AuthenticationMethod = "AuthenticationMethod", + ReasonCode = MqttAuthenticateReasonCode.ContinueAuthentication, + ReasonString = "ReasonString", + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(authPacket, MqttProtocolVersion.V500); + + CollectionAssert.AreEqual(authPacket.AuthenticationData, deserialized.AuthenticationData); + Assert.AreEqual(authPacket.AuthenticationMethod, deserialized.AuthenticationMethod); + Assert.AreEqual(authPacket.ReasonCode, deserialized.ReasonCode); + Assert.AreEqual(authPacket.ReasonString, deserialized.ReasonString); + CollectionAssert.AreEqual(authPacket.UserProperties, deserialized.UserProperties); + } + + [TestMethod] + public void Serialize_Full_MqttConnAckPacket_V500() + { + var connAckPacket = new MqttConnAckPacket + { + AuthenticationData = Encoding.UTF8.GetBytes("AuthenticationData"), + AuthenticationMethod = "AuthenticationMethod", + ReasonCode = MqttConnectReasonCode.ServerUnavailable, + ReasonString = "ReasonString", + ReceiveMaximum = 123, + ResponseInformation = "ResponseInformation", + RetainAvailable = true, + ReturnCode = MqttConnectReturnCode.ConnectionRefusedNotAuthorized, + ServerReference = "ServerReference", + AssignedClientIdentifier = "AssignedClientIdentifier", + IsSessionPresent = true, + MaximumPacketSize = 456, + MaximumQoS = MqttQualityOfServiceLevel.ExactlyOnce, + ServerKeepAlive = 789, + SessionExpiryInterval = 852, + SharedSubscriptionAvailable = true, + SubscriptionIdentifiersAvailable = true, + TopicAliasMaximum = 963, + WildcardSubscriptionAvailable = true, + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(connAckPacket, MqttProtocolVersion.V500); + + CollectionAssert.AreEqual(connAckPacket.AuthenticationData, deserialized.AuthenticationData); + Assert.AreEqual(connAckPacket.AuthenticationMethod, deserialized.AuthenticationMethod); + Assert.AreEqual(connAckPacket.ReasonCode, deserialized.ReasonCode); + Assert.AreEqual(connAckPacket.ReasonString, deserialized.ReasonString); + Assert.AreEqual(connAckPacket.ReceiveMaximum, deserialized.ReceiveMaximum); + Assert.AreEqual(connAckPacket.ResponseInformation, deserialized.ResponseInformation); + Assert.AreEqual(connAckPacket.RetainAvailable, deserialized.RetainAvailable); + // Return Code only used in MQTTv3 + Assert.AreEqual(connAckPacket.ServerReference, deserialized.ServerReference); + Assert.AreEqual(connAckPacket.AssignedClientIdentifier, deserialized.AssignedClientIdentifier); + Assert.AreEqual(connAckPacket.IsSessionPresent, deserialized.IsSessionPresent); + Assert.AreEqual(connAckPacket.MaximumPacketSize, deserialized.MaximumPacketSize); + Assert.AreEqual(connAckPacket.MaximumQoS, deserialized.MaximumQoS); + Assert.AreEqual(connAckPacket.ServerKeepAlive, deserialized.ServerKeepAlive); + Assert.AreEqual(connAckPacket.SessionExpiryInterval, deserialized.SessionExpiryInterval); + Assert.AreEqual(connAckPacket.SharedSubscriptionAvailable, deserialized.SharedSubscriptionAvailable); + Assert.AreEqual(connAckPacket.SubscriptionIdentifiersAvailable, deserialized.SubscriptionIdentifiersAvailable); + Assert.AreEqual(connAckPacket.TopicAliasMaximum, deserialized.TopicAliasMaximum); + Assert.AreEqual(connAckPacket.WildcardSubscriptionAvailable, deserialized.WildcardSubscriptionAvailable); + CollectionAssert.AreEqual(connAckPacket.UserProperties, deserialized.UserProperties); + } + + [TestMethod] + public void Serialize_Full_MqttConnectPacket_V500() + { + var connectPacket = new MqttConnectPacket + { + Username = "Username", + Password = Encoding.UTF8.GetBytes("Password"), + ClientId = "ClientId", + AuthenticationData = Encoding.UTF8.GetBytes("AuthenticationData"), + AuthenticationMethod = "AuthenticationMethod", + CleanSession = true, + ReceiveMaximum = 123, + WillFlag = true, + WillTopic = "WillTopic", + WillMessage = Encoding.UTF8.GetBytes("WillMessage"), + WillRetain = true, + KeepAlivePeriod = 456, + MaximumPacketSize = 789, + RequestProblemInformation = true, + RequestResponseInformation = true, + SessionExpiryInterval = 27, + TopicAliasMaximum = 67, + WillContentType = "WillContentType", + WillCorrelationData = Encoding.UTF8.GetBytes("WillCorrelationData"), + WillDelayInterval = 782, + WillQoS = MqttQualityOfServiceLevel.ExactlyOnce, + WillResponseTopic = "WillResponseTopic", + WillMessageExpiryInterval = 542, + WillPayloadFormatIndicator = MqttPayloadFormatIndicator.CharacterData, + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + }, + WillUserProperties = new List + { + new MqttUserProperty("WillFoo", "WillBar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(connectPacket, MqttProtocolVersion.V500); + + Assert.AreEqual(connectPacket.Username, deserialized.Username); + CollectionAssert.AreEqual(connectPacket.Password, deserialized.Password); + Assert.AreEqual(connectPacket.ClientId, deserialized.ClientId); + CollectionAssert.AreEqual(connectPacket.AuthenticationData, deserialized.AuthenticationData); + Assert.AreEqual(connectPacket.AuthenticationMethod, deserialized.AuthenticationMethod); + Assert.AreEqual(connectPacket.CleanSession, deserialized.CleanSession); + Assert.AreEqual(connectPacket.ReceiveMaximum, deserialized.ReceiveMaximum); + Assert.AreEqual(connectPacket.WillFlag, deserialized.WillFlag); + Assert.AreEqual(connectPacket.WillTopic, deserialized.WillTopic); + CollectionAssert.AreEqual(connectPacket.WillMessage, deserialized.WillMessage); + Assert.AreEqual(connectPacket.WillRetain, deserialized.WillRetain); + Assert.AreEqual(connectPacket.KeepAlivePeriod, deserialized.KeepAlivePeriod); + Assert.AreEqual(connectPacket.MaximumPacketSize, deserialized.MaximumPacketSize); + Assert.AreEqual(connectPacket.RequestProblemInformation, deserialized.RequestProblemInformation); + Assert.AreEqual(connectPacket.RequestResponseInformation, deserialized.RequestResponseInformation); + Assert.AreEqual(connectPacket.SessionExpiryInterval, deserialized.SessionExpiryInterval); + Assert.AreEqual(connectPacket.TopicAliasMaximum, deserialized.TopicAliasMaximum); + Assert.AreEqual(connectPacket.WillContentType, deserialized.WillContentType); + CollectionAssert.AreEqual(connectPacket.WillCorrelationData, deserialized.WillCorrelationData); + Assert.AreEqual(connectPacket.WillDelayInterval, deserialized.WillDelayInterval); + Assert.AreEqual(connectPacket.WillQoS, deserialized.WillQoS); + Assert.AreEqual(connectPacket.WillResponseTopic, deserialized.WillResponseTopic); + Assert.AreEqual(connectPacket.WillMessageExpiryInterval, deserialized.WillMessageExpiryInterval); + Assert.AreEqual(connectPacket.WillPayloadFormatIndicator, deserialized.WillPayloadFormatIndicator); + CollectionAssert.AreEqual(connectPacket.UserProperties, deserialized.UserProperties); + CollectionAssert.AreEqual(connectPacket.WillUserProperties, deserialized.WillUserProperties); + } + + [TestMethod] + public void Serialize_Full_MqttDisconnectPacket_V500() + { + var disconnectPacket = new MqttDisconnectPacket + { + ReasonCode = MqttDisconnectReasonCode.NormalDisconnection, + ReasonString = "ReasonString", + ServerReference = "ServerReference", + SessionExpiryInterval = 234, + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(disconnectPacket, MqttProtocolVersion.V500); + + Assert.AreEqual(disconnectPacket.ReasonCode, deserialized.ReasonCode); + Assert.AreEqual(disconnectPacket.ReasonString, deserialized.ReasonString); + Assert.AreEqual(disconnectPacket.ServerReference, deserialized.ServerReference); + Assert.AreEqual(disconnectPacket.SessionExpiryInterval, deserialized.SessionExpiryInterval); + CollectionAssert.AreEqual(disconnectPacket.UserProperties, deserialized.UserProperties); + } + + [TestMethod] + public void Serialize_Full_MqttPingReqPacket_V500() + { + var pingReqPacket = new MqttPingReqPacket(); + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pingReqPacket, MqttProtocolVersion.V500); + + Assert.IsNotNull(deserialized); + } + + [TestMethod] + public void Serialize_Full_MqttPingRespPacket_V500() + { + var pingRespPacket = new MqttPingRespPacket(); + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pingRespPacket, MqttProtocolVersion.V500); + + Assert.IsNotNull(deserialized); + } + + [TestMethod] + public void Serialize_Full_MqttPubAckPacket_V500() + { + var pubAckPacket = new MqttPubAckPacket + { + PacketIdentifier = 123, + ReasonCode = MqttPubAckReasonCode.NoMatchingSubscribers, + ReasonString = "ReasonString", + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubAckPacket, MqttProtocolVersion.V500); + + Assert.AreEqual(pubAckPacket.PacketIdentifier, deserialized.PacketIdentifier); + Assert.AreEqual(pubAckPacket.ReasonCode, deserialized.ReasonCode); + Assert.AreEqual(pubAckPacket.ReasonString, deserialized.ReasonString); + CollectionAssert.AreEqual(pubAckPacket.UserProperties, deserialized.UserProperties); + } + + [TestMethod] + public void Serialize_Full_MqttPubCompPacket_V500() + { + var pubCompPacket = new MqttPubCompPacket + { + PacketIdentifier = 123, + ReasonCode = MqttPubCompReasonCode.PacketIdentifierNotFound, + ReasonString = "ReasonString", + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubCompPacket, MqttProtocolVersion.V500); + + Assert.AreEqual(pubCompPacket.PacketIdentifier, deserialized.PacketIdentifier); + Assert.AreEqual(pubCompPacket.ReasonCode, deserialized.ReasonCode); + Assert.AreEqual(pubCompPacket.ReasonString, deserialized.ReasonString); + CollectionAssert.AreEqual(pubCompPacket.UserProperties, deserialized.UserProperties); + } + + [TestMethod] + public void Serialize_Full_MqttPublishPacket_V500() + { + var publishPacket = new MqttPublishPacket + { + PacketIdentifier = 123, + Dup = true, + Retain = true, + Payload = Encoding.ASCII.GetBytes("Payload"), + QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce, + Topic = "Topic", + ResponseTopic = "/Response", + ContentType = "Content-Type", + CorrelationData = Encoding.UTF8.GetBytes("CorrelationData"), + TopicAlias = 27, + SubscriptionIdentifiers = new List + { + 123 + }, + MessageExpiryInterval = 38, + PayloadFormatIndicator = MqttPayloadFormatIndicator.CharacterData, + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(publishPacket, MqttProtocolVersion.V500); + + Assert.AreEqual(publishPacket.PacketIdentifier, deserialized.PacketIdentifier); + Assert.AreEqual(publishPacket.Dup, deserialized.Dup); + Assert.AreEqual(publishPacket.Retain, deserialized.Retain); + CollectionAssert.AreEqual(publishPacket.Payload, deserialized.Payload); + Assert.AreEqual(publishPacket.QualityOfServiceLevel, deserialized.QualityOfServiceLevel); + Assert.AreEqual(publishPacket.Topic, deserialized.Topic); + Assert.AreEqual(publishPacket.ResponseTopic, deserialized.ResponseTopic); + Assert.AreEqual(publishPacket.ContentType, deserialized.ContentType); + CollectionAssert.AreEqual(publishPacket.CorrelationData, deserialized.CorrelationData); + Assert.AreEqual(publishPacket.TopicAlias, deserialized.TopicAlias); + CollectionAssert.AreEqual(publishPacket.SubscriptionIdentifiers, deserialized.SubscriptionIdentifiers); + Assert.AreEqual(publishPacket.MessageExpiryInterval, deserialized.MessageExpiryInterval); + Assert.AreEqual(publishPacket.PayloadFormatIndicator, deserialized.PayloadFormatIndicator); + CollectionAssert.AreEqual(publishPacket.UserProperties, deserialized.UserProperties); + } + + [TestMethod] + public void Serialize_Full_MqttPubRecPacket_V500() + { + var pubRecPacket = new MqttPubRecPacket + { + PacketIdentifier = 123, + ReasonCode = MqttPubRecReasonCode.UnspecifiedError, + ReasonString = "ReasonString", + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubRecPacket, MqttProtocolVersion.V500); + + Assert.AreEqual(pubRecPacket.PacketIdentifier, deserialized.PacketIdentifier); + Assert.AreEqual(pubRecPacket.ReasonCode, deserialized.ReasonCode); + Assert.AreEqual(pubRecPacket.ReasonString, deserialized.ReasonString); + CollectionAssert.AreEqual(pubRecPacket.UserProperties, deserialized.UserProperties); + } + + [TestMethod] + public void Serialize_Full_MqttPubRelPacket_V500() + { + var pubRelPacket = new MqttPubRelPacket + { + PacketIdentifier = 123, + ReasonCode = MqttPubRelReasonCode.PacketIdentifierNotFound, + ReasonString = "ReasonString", + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(pubRelPacket, MqttProtocolVersion.V500); + + Assert.AreEqual(pubRelPacket.PacketIdentifier, deserialized.PacketIdentifier); + Assert.AreEqual(pubRelPacket.ReasonCode, deserialized.ReasonCode); + Assert.AreEqual(pubRelPacket.ReasonString, deserialized.ReasonString); + CollectionAssert.AreEqual(pubRelPacket.UserProperties, deserialized.UserProperties); + } + + [TestMethod] + public void Serialize_Full_MqttSubAckPacket_V500() + { + var subAckPacket = new MqttSubAckPacket + { + PacketIdentifier = 123, + ReasonString = "ReasonString", + ReasonCodes = new List + { + MqttSubscribeReasonCode.GrantedQoS1 + }, + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(subAckPacket, MqttProtocolVersion.V500); + + Assert.AreEqual(subAckPacket.PacketIdentifier, deserialized.PacketIdentifier); + Assert.AreEqual(subAckPacket.ReasonString, deserialized.ReasonString); + Assert.AreEqual(subAckPacket.ReasonCodes.Count, deserialized.ReasonCodes.Count); + Assert.AreEqual(subAckPacket.ReasonCodes[0], deserialized.ReasonCodes[0]); + CollectionAssert.AreEqual(subAckPacket.UserProperties, deserialized.UserProperties); + } + + [TestMethod] + public void Serialize_Full_MqttSubscribePacket_V500() + { + var subscribePacket = new MqttSubscribePacket + { + PacketIdentifier = 123, + SubscriptionIdentifier = 456, + TopicFilters = new List + { + new MqttTopicFilter + { + Topic = "Topic", + NoLocal = true, + RetainHandling = MqttRetainHandling.SendAtSubscribeIfNewSubscriptionOnly, + RetainAsPublished = true, + QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce + } + }, + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(subscribePacket, MqttProtocolVersion.V500); + + Assert.AreEqual(subscribePacket.PacketIdentifier, deserialized.PacketIdentifier); + Assert.AreEqual(subscribePacket.SubscriptionIdentifier, deserialized.SubscriptionIdentifier); + Assert.AreEqual(1, deserialized.TopicFilters.Count); + Assert.AreEqual(subscribePacket.TopicFilters[0].Topic, deserialized.TopicFilters[0].Topic); + Assert.AreEqual(subscribePacket.TopicFilters[0].NoLocal, deserialized.TopicFilters[0].NoLocal); + Assert.AreEqual(subscribePacket.TopicFilters[0].RetainHandling, deserialized.TopicFilters[0].RetainHandling); + Assert.AreEqual(subscribePacket.TopicFilters[0].RetainAsPublished, deserialized.TopicFilters[0].RetainAsPublished); + Assert.AreEqual(subscribePacket.TopicFilters[0].QualityOfServiceLevel, deserialized.TopicFilters[0].QualityOfServiceLevel); + CollectionAssert.AreEqual(subscribePacket.UserProperties, deserialized.UserProperties); + } + + [TestMethod] + public void Serialize_Full_MqttUnsubAckPacket_V500() + { + var unsubAckPacket = new MqttUnsubAckPacket + { + PacketIdentifier = 123, + ReasonCodes = new List + { + MqttUnsubscribeReasonCode.ImplementationSpecificError + }, + ReasonString = "ReasonString", + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(unsubAckPacket, MqttProtocolVersion.V500); + + Assert.AreEqual(unsubAckPacket.PacketIdentifier, deserialized.PacketIdentifier); + Assert.AreEqual(unsubAckPacket.ReasonString, deserialized.ReasonString); + Assert.AreEqual(unsubAckPacket.ReasonCodes.Count, deserialized.ReasonCodes.Count); + Assert.AreEqual(unsubAckPacket.ReasonCodes[0], deserialized.ReasonCodes[0]); + CollectionAssert.AreEqual(unsubAckPacket.UserProperties, deserialized.UserProperties); + } + + [TestMethod] + public void Serialize_Full_MqttUnsubscribePacket_V500() + { + var unsubscribePacket = new MqttUnsubscribePacket + { + PacketIdentifier = 123, + TopicFilters = new List + { + "TopicFilter1" + }, + UserProperties = new List + { + new MqttUserProperty("Foo", "Bar") + } + }; + + var deserialized = MqttPacketSerializationHelper.EncodeAndDecodePacket(unsubscribePacket, MqttProtocolVersion.V500); + + Assert.AreEqual(unsubscribePacket.PacketIdentifier, deserialized.PacketIdentifier); + Assert.AreEqual(unsubscribePacket.TopicFilters.Count, deserialized.TopicFilters.Count); + Assert.AreEqual(unsubscribePacket.TopicFilters[0], deserialized.TopicFilters[0]); + CollectionAssert.AreEqual(unsubscribePacket.UserProperties, deserialized.UserProperties); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Formatter/MqttPacketWriter_Tests.cs b/Source/MQTTnet.Tests/Formatter/MqttPacketWriter_Tests.cs new file mode 100644 index 0000000..e2748f0 --- /dev/null +++ b/Source/MQTTnet.Tests/Formatter/MqttPacketWriter_Tests.cs @@ -0,0 +1,92 @@ +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Formatter; + +namespace MQTTnet.Tests.Formatter +{ + [TestClass] + public sealed class MqttPacketWriter_Tests + { + [TestMethod] + public void Reset_After_Usage() + { + var writer = new MqttBufferWriter(4096, 65535); + writer.WriteString("AString"); + writer.WriteByte(0x1); + writer.WriteByte(0x0); + writer.WriteByte(0x1); + writer.WriteVariableByteInteger(1234U); + writer.WriteVariableByteInteger(9876U); + + writer.Reset(0); + + Assert.AreEqual(0, writer.Length); + } + + [TestMethod] + public void Use_All_Data_Types() + { + var writer = new MqttBufferWriter(4096, 65535); + writer.WriteString("AString"); + writer.WriteByte(0x1); + writer.WriteByte(0x0); + writer.WriteByte(0x1); + writer.WriteVariableByteInteger(1234U); + writer.WriteVariableByteInteger(9876U); + + var buffer = writer.GetBuffer(); + + var reader = new MqttBufferReader(); + reader.SetBuffer(buffer, 0, writer.Length); + + Assert.AreEqual("AString", reader.ReadString()); + Assert.IsTrue(reader.ReadByte() == 1); + Assert.IsTrue(reader.ReadByte() == 0); + Assert.IsTrue(reader.ReadByte() == 1); + Assert.AreEqual(1234U, reader.ReadVariableByteInteger()); + Assert.AreEqual(9876U, reader.ReadVariableByteInteger()); + } + + [TestMethod] + public void Write_And_Read_Multiple_Times() + { + var writer = new MqttBufferWriter(4096, 65535); + writer.WriteString("A relative short string."); + writer.WriteBinaryData(new byte[1234]); + writer.WriteByte(0x01); + writer.WriteByte(0x02); + writer.WriteVariableByteInteger(5647382); + writer.WriteString("A relative short string."); + writer.WriteVariableByteInteger(8574489); + writer.WriteBinaryData(new byte[48]); + writer.WriteByte(2); + writer.WriteByte(0x02); + writer.WriteString("fjgffiogfhgfhoihgoireghreghreguhreguireoghreouighreouighreughreguiorehreuiohruiorehreuioghreug"); + writer.WriteBinaryData(new byte[3]); + + var readPayload = new ArraySegment(writer.GetBuffer(), 0, writer.Length).ToArray(); + + var reader = new MqttBufferReader(); + reader.SetBuffer(readPayload, 0, readPayload.Length); + + for (var i = 0; i < 100000; i++) + { + reader.Seek(0); + + reader.ReadString(); + reader.ReadBinaryData(); + reader.ReadByte(); + reader.ReadByte(); + reader.ReadVariableByteInteger(); + reader.ReadString(); + reader.ReadVariableByteInteger(); + reader.ReadBinaryData(); + reader.ReadByte(); + reader.ReadByte(); + reader.ReadString(); + reader.ReadBinaryData(); + } + } + } +} \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/AsyncLock_Tests.cs b/Source/MQTTnet.Tests/Internal/AsyncLock_Tests.cs similarity index 92% rename from Tests/MQTTnet.Core.Tests/AsyncLock_Tests.cs rename to Source/MQTTnet.Tests/Internal/AsyncLock_Tests.cs index dfdce18..69cf79a 100644 --- a/Tests/MQTTnet.Core.Tests/AsyncLock_Tests.cs +++ b/Source/MQTTnet.Tests/Internal/AsyncLock_Tests.cs @@ -1,10 +1,14 @@ -using System; +// 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.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Internal; -namespace MQTTnet.Tests +namespace MQTTnet.Tests.Internal { [TestClass] public class AsyncLock_Tests diff --git a/Tests/MQTTnet.Core.Tests/AsyncQueue_Tests.cs b/Source/MQTTnet.Tests/Internal/AsyncQueue_Tests.cs similarity index 93% rename from Tests/MQTTnet.Core.Tests/AsyncQueue_Tests.cs rename to Source/MQTTnet.Tests/Internal/AsyncQueue_Tests.cs index fb1ad93..91887eb 100644 --- a/Tests/MQTTnet.Core.Tests/AsyncQueue_Tests.cs +++ b/Source/MQTTnet.Tests/Internal/AsyncQueue_Tests.cs @@ -1,10 +1,14 @@ -using System; +// 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.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Internal; -namespace MQTTnet.Tests +namespace MQTTnet.Tests.Internal { [TestClass] public class AsyncQueue_Tests diff --git a/Tests/MQTTnet.Core.Tests/BlockingQueue_Tests.cs b/Source/MQTTnet.Tests/Internal/BlockingQueue_Tests.cs similarity index 92% rename from Tests/MQTTnet.Core.Tests/BlockingQueue_Tests.cs rename to Source/MQTTnet.Tests/Internal/BlockingQueue_Tests.cs index 01e786f..4983706 100644 --- a/Tests/MQTTnet.Core.Tests/BlockingQueue_Tests.cs +++ b/Source/MQTTnet.Tests/Internal/BlockingQueue_Tests.cs @@ -1,10 +1,14 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Internal; +// 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.Threading; using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Internal; -namespace MQTTnet.Tests +namespace MQTTnet.Tests.Internal { [TestClass] public class BlockingQueue_Tests diff --git a/Tests/MQTTnet.Core.Tests/CrossPlatformSocket_Tests.cs b/Source/MQTTnet.Tests/Internal/CrossPlatformSocket_Tests.cs similarity index 90% rename from Tests/MQTTnet.Core.Tests/CrossPlatformSocket_Tests.cs rename to Source/MQTTnet.Tests/Internal/CrossPlatformSocket_Tests.cs index c59e40c..1c2ecdf 100644 --- a/Tests/MQTTnet.Core.Tests/CrossPlatformSocket_Tests.cs +++ b/Source/MQTTnet.Tests/Internal/CrossPlatformSocket_Tests.cs @@ -1,11 +1,15 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Implementations; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + using System; using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Implementations; -namespace MQTTnet.Tests +namespace MQTTnet.Tests.Internal { [TestClass] public class CrossPlatformSocket_Tests diff --git a/Source/MQTTnet.Tests/Internal/MqttPacketBus_Tests.cs b/Source/MQTTnet.Tests/Internal/MqttPacketBus_Tests.cs new file mode 100644 index 0000000..b701a26 --- /dev/null +++ b/Source/MQTTnet.Tests/Internal/MqttPacketBus_Tests.cs @@ -0,0 +1,120 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Internal; +using MQTTnet.Packets; + +namespace MQTTnet.Tests.Internal +{ + [TestClass] + public sealed class MqttPacketBus_Tests + { + [TestMethod] + [ExpectedException(typeof(OperationCanceledException))] + public async Task Wait_With_Empty_Bus() + { + var bus = new MqttPacketBus(); + + using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(1))) + { + await bus.DequeueItemAsync(timeout.Token); + } + } + + [TestMethod] + public void Alternate_Priorities() + { + var bus = new MqttPacketBus(); + + bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); + bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); + bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); + + bus.EnqueueItem(new MqttPacketBusItem(new MqttSubAckPacket()), MqttPacketBusPartition.Control); + bus.EnqueueItem(new MqttPacketBusItem(new MqttSubAckPacket()), MqttPacketBusPartition.Control); + bus.EnqueueItem(new MqttPacketBusItem(new MqttSubAckPacket()), MqttPacketBusPartition.Control); + + bus.EnqueueItem(new MqttPacketBusItem(new MqttPingRespPacket()), MqttPacketBusPartition.Health); + bus.EnqueueItem(new MqttPacketBusItem(new MqttPingRespPacket()), MqttPacketBusPartition.Health); + bus.EnqueueItem(new MqttPacketBusItem(new MqttPingRespPacket()), MqttPacketBusPartition.Health); + + Assert.AreEqual(9, bus.ItemsCount); + + Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttPublishPacket)); + Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttSubAckPacket)); + Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttPingRespPacket)); + Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttPublishPacket)); + Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttSubAckPacket)); + Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttPingRespPacket)); + Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttPublishPacket)); + Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttSubAckPacket)); + Assert.IsInstanceOfType(bus.DequeueItemAsync(CancellationToken.None).Result.Packet, typeof(MqttPingRespPacket)); + + Assert.AreEqual(0, bus.ItemsCount); + } + + [TestMethod] + public void Export_Packets_Without_Dequeue() + { + var bus = new MqttPacketBus(); + + bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); + bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); + bus.EnqueueItem(new MqttPacketBusItem(new MqttPublishPacket()), MqttPacketBusPartition.Data); + + Assert.AreEqual(3, bus.ItemsCount); + + var exportedPackets = bus.ExportPackets(MqttPacketBusPartition.Control); + Assert.AreEqual(0, exportedPackets.Count); + + exportedPackets = bus.ExportPackets(MqttPacketBusPartition.Health); + Assert.AreEqual(0, exportedPackets.Count); + + exportedPackets = bus.ExportPackets(MqttPacketBusPartition.Data); + Assert.AreEqual(3, exportedPackets.Count); + + Assert.AreEqual(3, bus.ItemsCount); + } + + [TestMethod] + public void Await_Single_Packet() + { + var bus = new MqttPacketBus(); + + var delivered = false; + + var item1 = new MqttPacketBusItem(new MqttPublishPacket()); + var item2 = new MqttPacketBusItem(new MqttPublishPacket()); + + var item3 = new MqttPacketBusItem(new MqttPublishPacket()); + item3.Delivered += (_, __) => + { + delivered = true; + }; + + bus.EnqueueItem(item1, MqttPacketBusPartition.Data); + bus.EnqueueItem(item2, MqttPacketBusPartition.Data); + bus.EnqueueItem(item3, MqttPacketBusPartition.Data); + + Assert.IsFalse(delivered); + + bus.DequeueItemAsync(CancellationToken.None).Result.MarkAsDelivered(); + + Assert.IsFalse(delivered); + + bus.DequeueItemAsync(CancellationToken.None).Result.MarkAsDelivered(); + + Assert.IsFalse(delivered); + + bus.DequeueItemAsync(CancellationToken.None).Result.MarkAsDelivered(); + + // The third packet has the event attached. + Assert.IsTrue(delivered); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/MQTTnet.Tests.csproj b/Source/MQTTnet.Tests/MQTTnet.Tests.csproj new file mode 100644 index 0000000..15d646d --- /dev/null +++ b/Source/MQTTnet.Tests/MQTTnet.Tests.csproj @@ -0,0 +1,27 @@ + + + + net452;net461;netcoreapp3.1;net6.0 + false + 7.3 + false + false + true + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/MQTTv5/Client_Tests.cs b/Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs similarity index 76% rename from Tests/MQTTnet.Core.Tests/MQTTv5/Client_Tests.cs rename to Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs index 11a20ac..8abf63e 100644 --- a/Tests/MQTTnet.Core.Tests/MQTTv5/Client_Tests.cs +++ b/Source/MQTTnet.Tests/MQTTv5/Client_Tests.cs @@ -1,29 +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.Collections.Generic; -using System.Threading; +using System.Linq; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; -using MQTTnet.Client.Options; -using MQTTnet.Client.Publishing; -using MQTTnet.Client.Receiving; -using MQTTnet.Client.Subscribing; -using MQTTnet.Client.Unsubscribing; using MQTTnet.Formatter; +using MQTTnet.Implementations; +using MQTTnet.Packets; using MQTTnet.Protocol; -using MQTTnet.Server; using MQTTnet.Tests.Mockups; namespace MQTTnet.Tests.MQTTv5 { [TestClass] - public sealed class Client_Tests + public sealed class Client_Tests : BaseTestClass { - public TestContext TestContext { get; set; } - [TestMethod] public async Task Connect_With_New_Mqtt_Features() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { await testEnvironment.StartServer(); @@ -35,14 +33,18 @@ namespace MQTTnet.Tests.MQTTv5 .WithProtocolVersion(MqttProtocolVersion.V500) .WithTopicAliasMaximum(20) .WithReceiveMaximum(20) - .WithWillMessage(new MqttApplicationMessageBuilder().WithTopic("abc").Build()) + .WithWillTopic("abc") .WithWillDelayInterval(20) .Build()); MqttApplicationMessage receivedMessage = null; await client.SubscribeAsync("a"); - client.UseApplicationMessageReceivedHandler(context => { receivedMessage = context.ApplicationMessage; }); + client.ApplicationMessageReceivedAsync += e => + { + receivedMessage = e.ApplicationMessage; + return PlatformAbstractionLayer.CompletedTask; + }; await client.PublishAsync(new MqttApplicationMessageBuilder() .WithTopic("a") @@ -50,7 +52,7 @@ namespace MQTTnet.Tests.MQTTv5 .WithUserProperty("a", "1") .WithUserProperty("b", "2") .WithPayloadFormatIndicator(MqttPayloadFormatIndicator.CharacterData) - .WithAtLeastOnceQoS() + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) .Build()); await Task.Delay(500); @@ -64,7 +66,7 @@ namespace MQTTnet.Tests.MQTTv5 [TestMethod] public async Task Connect() { - using (var testEnvironment = new TestEnvironment()) + using (var testEnvironment = CreateTestEnvironment()) { await testEnvironment.StartServer(); await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500).Build()); @@ -74,7 +76,7 @@ namespace MQTTnet.Tests.MQTTv5 [TestMethod] public async Task Connect_And_Disconnect() { - using (var testEnvironment = new TestEnvironment()) + using (var testEnvironment = CreateTestEnvironment()) { await testEnvironment.StartServer(); @@ -86,7 +88,7 @@ namespace MQTTnet.Tests.MQTTv5 [TestMethod] public async Task Subscribe() { - using (var testEnvironment = new TestEnvironment()) + using (var testEnvironment = CreateTestEnvironment()) { await testEnvironment.StartServer(); @@ -105,14 +107,14 @@ namespace MQTTnet.Tests.MQTTv5 await client.DisconnectAsync(); Assert.AreEqual(1, result.Items.Count); - Assert.AreEqual(MqttClientSubscribeResultCode.GrantedQoS1, result.Items[0].ResultCode); + Assert.AreEqual(MqttClientSubscribeResultCode.GrantedQoS1, result.Items.First().ResultCode); } } [TestMethod] public async Task Unsubscribe() { - using (var testEnvironment = new TestEnvironment()) + using (var testEnvironment = CreateTestEnvironment()) { await testEnvironment.StartServer(); @@ -123,19 +125,19 @@ namespace MQTTnet.Tests.MQTTv5 await client.DisconnectAsync(); Assert.AreEqual(1, result.Items.Count); - Assert.AreEqual(MqttClientUnsubscribeResultCode.Success, result.Items[0].ReasonCode); + Assert.AreEqual(MqttClientUnsubscribeResultCode.Success, result.Items.First().ResultCode); } } [TestMethod] public async Task Publish_QoS_0() { - using (var testEnvironment = new TestEnvironment()) + using (var testEnvironment = CreateTestEnvironment()) { await testEnvironment.StartServer(); var client = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); - var result = await client.PublishAsync("a", "b"); + var result = await client.PublishStringAsync("a", "b"); await client.DisconnectAsync(); Assert.AreEqual(MqttClientPublishReasonCode.Success, result.ReasonCode); @@ -145,12 +147,12 @@ namespace MQTTnet.Tests.MQTTv5 [TestMethod] public async Task Publish_QoS_1() { - using (var testEnvironment = new TestEnvironment()) + using (var testEnvironment = CreateTestEnvironment()) { await testEnvironment.StartServer(); var client = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); - var result = await client.PublishAsync("a", "b", MqttQualityOfServiceLevel.AtLeastOnce); + var result = await client.PublishStringAsync("a", "b", MqttQualityOfServiceLevel.AtLeastOnce); await client.DisconnectAsync(); Assert.AreEqual(MqttClientPublishReasonCode.Success, result.ReasonCode); @@ -160,12 +162,12 @@ namespace MQTTnet.Tests.MQTTv5 [TestMethod] public async Task Publish_QoS_2() { - using (var testEnvironment = new TestEnvironment()) + using (var testEnvironment = CreateTestEnvironment()) { await testEnvironment.StartServer(); var client = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500)); - var result = await client.PublishAsync("a", "b", MqttQualityOfServiceLevel.ExactlyOnce); + var result = await client.PublishStringAsync("a", "b", MqttQualityOfServiceLevel.ExactlyOnce); await client.DisconnectAsync(); Assert.AreEqual(MqttClientPublishReasonCode.Success, result.ReasonCode); @@ -175,7 +177,7 @@ namespace MQTTnet.Tests.MQTTv5 [TestMethod] public async Task Publish_With_Properties() { - using (var testEnvironment = new TestEnvironment()) + using (var testEnvironment = CreateTestEnvironment()) { await testEnvironment.StartServer(); @@ -184,7 +186,7 @@ namespace MQTTnet.Tests.MQTTv5 var applicationMessage = new MqttApplicationMessageBuilder() .WithTopic("Hello") .WithPayload("World") - .WithAtMostOnceQoS() + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce) .WithUserProperty("x", "1") .WithUserProperty("y", "2") .WithResponseTopic("response") @@ -204,42 +206,35 @@ namespace MQTTnet.Tests.MQTTv5 [TestMethod] public async Task Subscribe_And_Publish() { - using (var testEnvironment = new TestEnvironment()) + using (var testEnvironment = CreateTestEnvironment()) { await testEnvironment.StartServer(); - - var receivedMessages = new List(); - + var client1 = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500).WithClientId("client1")); - client1.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(e => - { - lock (receivedMessages) - { - receivedMessages.Add(e); - } - }); + + var testMessageHandler = new TestApplicationMessageReceivedHandler(client1); await client1.SubscribeAsync("a"); var client2 = await testEnvironment.ConnectClient(o => o.WithProtocolVersion(MqttProtocolVersion.V500).WithClientId("client2")); - await client2.PublishAsync("a", "b"); + await client2.PublishStringAsync("a", "b"); await Task.Delay(500); await client2.DisconnectAsync(); await client1.DisconnectAsync(); - Assert.AreEqual(1, receivedMessages.Count); - Assert.AreEqual("client1", receivedMessages[0].ClientId); - Assert.AreEqual("a", receivedMessages[0].ApplicationMessage.Topic); - Assert.AreEqual("b", receivedMessages[0].ApplicationMessage.ConvertPayloadToString()); + Assert.AreEqual(1, testMessageHandler.ReceivedEventArgs.Count); + Assert.AreEqual("Subscribe_And_Publish_client1", testMessageHandler.ReceivedEventArgs[0].ClientId); + Assert.AreEqual("a", testMessageHandler.ReceivedEventArgs[0].ApplicationMessage.Topic); + Assert.AreEqual("b", testMessageHandler.ReceivedEventArgs[0].ApplicationMessage.ConvertPayloadToString()); } } [TestMethod] public async Task Publish_And_Receive_New_Properties() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { await testEnvironment.StartServer(); @@ -247,17 +242,18 @@ namespace MQTTnet.Tests.MQTTv5 await receiver.SubscribeAsync("#"); MqttApplicationMessage receivedMessage = null; - receiver.UseApplicationMessageReceivedHandler(c => + receiver.ApplicationMessageReceivedAsync += e => { - receivedMessage = c.ApplicationMessage; - }); + receivedMessage = e.ApplicationMessage; + return PlatformAbstractionLayer.CompletedTask; + }; var sender = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); var applicationMessage = new MqttApplicationMessageBuilder() .WithTopic("Hello") .WithPayload("World") - .WithAtMostOnceQoS() + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce) .WithUserProperty("x", "1") .WithUserProperty("y", "2") .WithResponseTopic("response") diff --git a/Tests/MQTTnet.Core.Tests/MQTTv5/Server_Tests.cs b/Source/MQTTnet.Tests/MQTTv5/Server_Tests.cs similarity index 83% rename from Tests/MQTTnet.Core.Tests/MQTTv5/Server_Tests.cs rename to Source/MQTTnet.Tests/MQTTv5/Server_Tests.cs index ce015b1..7326c94 100644 --- a/Tests/MQTTnet.Core.Tests/MQTTv5/Server_Tests.cs +++ b/Source/MQTTnet.Tests/MQTTv5/Server_Tests.cs @@ -1,33 +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 Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; -using MQTTnet.Client.Options; using MQTTnet.Formatter; using MQTTnet.Tests.Mockups; using System.Threading; using System.Threading.Tasks; +using MQTTnet.Implementations; +using MQTTnet.Protocol; namespace MQTTnet.Tests.MQTTv5 { [TestClass] - public class Server_Tests + public sealed class Server_Tests : BaseTestClass { - public TestContext TestContext { get; set; } - [TestMethod] public async Task Will_Message_Send() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { - var receivedMessagesCount = 0; - await testEnvironment.StartServer(); - - var willMessage = new MqttApplicationMessageBuilder().WithTopic("My/last/will").WithAtMostOnceQoS().Build(); - - var clientOptions = new MqttClientOptionsBuilder().WithWillMessage(willMessage).WithProtocolVersion(MqttProtocolVersion.V500); + + var clientOptions = new MqttClientOptionsBuilder().WithWillTopic("My/last/will").WithWillQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce).WithProtocolVersion(MqttProtocolVersion.V500); var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); - c1.UseApplicationMessageReceivedHandler(c => Interlocked.Increment(ref receivedMessagesCount)); + + var receivedMessagesCount = 0; + c1.ApplicationMessageReceivedAsync += e => + { + Interlocked.Increment(ref receivedMessagesCount); + return PlatformAbstractionLayer.CompletedTask; + }; + await c1.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("#").Build()); var c2 = await testEnvironment.ConnectClient(clientOptions); @@ -42,7 +48,7 @@ namespace MQTTnet.Tests.MQTTv5 [TestMethod] public async Task Validate_IsSessionPresent() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { // Create server with persistent sessions enabled @@ -79,7 +85,7 @@ namespace MQTTnet.Tests.MQTTv5 [TestMethod] public async Task Connect_with_Undefined_SessionExpiryInterval() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { // Create server with persistent sessions enabled @@ -90,7 +96,7 @@ namespace MQTTnet.Tests.MQTTv5 // Create client without clean session and NO session expiry interval, // that means, the session should not persist - var options1 = CreateClientOptions(testEnvironment, ClientId, false, null); + var options1 = CreateClientOptions(testEnvironment, ClientId, false, 0); var client1 = await testEnvironment.ConnectClient(options1); // Disconnect; no session should remain on server because the session expiry interval was undefined @@ -119,7 +125,7 @@ namespace MQTTnet.Tests.MQTTv5 [TestMethod] public async Task Reconnect_with_different_SessionExpiryInterval() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { // Create server with persistent sessions enabled @@ -136,15 +142,13 @@ namespace MQTTnet.Tests.MQTTv5 await client1.DisconnectAsync(); - // Simulate some time delay between connections - await Task.Delay(1000); // Reconnect the same client ID to the existing session but leave session expiry interval undefined this time. // Session should be present because the client1 connection had SessionExpiryInterval > 0 var client2 = testEnvironment.CreateClient(); - var options2 = CreateClientOptions(testEnvironment, ClientId, false, null); + var options2 = CreateClientOptions(testEnvironment, ClientId, false, 0); var result2 = await client2.ConnectAsync(options2).ConfigureAwait(false); await client2.DisconnectAsync(); @@ -159,7 +163,7 @@ namespace MQTTnet.Tests.MQTTv5 // No session should be present because the previous session expiry interval was undefined for the client2 connection var client3 = testEnvironment.CreateClient(); - var options3 = CreateClientOptions(testEnvironment, ClientId, false, null); + var options3 = CreateClientOptions(testEnvironment, ClientId, false, 0); var result3 = await client2.ConnectAsync(options3).ConfigureAwait(false); await client3.DisconnectAsync(); @@ -171,7 +175,7 @@ namespace MQTTnet.Tests.MQTTv5 } } - IMqttClientOptions CreateClientOptions(TestEnvironment testEnvironment, string clientId, bool cleanSession, uint? sessionExpiryInterval) + MqttClientOptions CreateClientOptions(TestEnvironment testEnvironment, string clientId, bool cleanSession, uint sessionExpiryInterval) { return testEnvironment.Factory.CreateClientOptionsBuilder() .WithProtocolVersion(MqttProtocolVersion.V500) diff --git a/Source/MQTTnet.Tests/Mockups/MqttPacketAsserts.cs b/Source/MQTTnet.Tests/Mockups/MqttPacketAsserts.cs new file mode 100644 index 0000000..5d94407 --- /dev/null +++ b/Source/MQTTnet.Tests/Mockups/MqttPacketAsserts.cs @@ -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 Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Packets; + +namespace MQTTnet.Tests.Mockups +{ + public sealed class MqttPacketAsserts + { + public void AssertIsConnectPacket(MqttPacket packet) + { + Assert.AreEqual(packet.GetType(), typeof(MqttConnectPacket)); + } + + public void AssertIsConnAckPacket(MqttPacket packet) + { + Assert.AreEqual(packet.GetType(), typeof(MqttConnAckPacket)); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Mockups/TestApplicationMessageReceivedHandler.cs b/Source/MQTTnet.Tests/Mockups/TestApplicationMessageReceivedHandler.cs new file mode 100644 index 0000000..0a7af12 --- /dev/null +++ b/Source/MQTTnet.Tests/Mockups/TestApplicationMessageReceivedHandler.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Client; +using MQTTnet.Implementations; + +namespace MQTTnet.Tests.Mockups +{ + public sealed class TestApplicationMessageReceivedHandler + { + readonly List _receivedEventArgs = new List(); + + public TestApplicationMessageReceivedHandler(MqttClient mqttClient) + { + mqttClient.ApplicationMessageReceivedAsync += MqttClientOnApplicationMessageReceivedAsync; + } + + public List ReceivedEventArgs + { + get + { + lock (_receivedEventArgs) + { + return _receivedEventArgs.ToList(); + } + } + } + + public void AssertReceivedCountEquals(int expectedCount) + { + lock (_receivedEventArgs) + { + Assert.AreEqual(expectedCount, _receivedEventArgs.Count); + } + } + + Task MqttClientOnApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs) + { + lock (_receivedEventArgs) + { + _receivedEventArgs.Add(eventArgs); + } + + return PlatformAbstractionLayer.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Mockups/TestEnvironment.cs b/Source/MQTTnet.Tests/Mockups/TestEnvironment.cs new file mode 100644 index 0000000..5da8eb8 --- /dev/null +++ b/Source/MQTTnet.Tests/Mockups/TestEnvironment.cs @@ -0,0 +1,381 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Client; +using MQTTnet.Diagnostics; +using MQTTnet.Extensions.Rpc; +using MQTTnet.Formatter; +using MQTTnet.Implementations; +using MQTTnet.LowLevelClient; +using MQTTnet.Protocol; +using MQTTnet.Server; +using MqttClient = MQTTnet.Client.MqttClient; + +namespace MQTTnet.Tests.Mockups +{ + public sealed class TestEnvironment : IDisposable + { + readonly List _clientErrors = new List(); + readonly List _clients = new List(); + + readonly List _exceptions = new List(); + readonly List _lowLevelClients = new List(); + readonly MqttProtocolVersion _protocolVersion; + readonly List _serverErrors = new List(); + + public TestEnvironment() : this(null) + { + } + + public TestEnvironment(TestContext testContext, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311) + { + _protocolVersion = protocolVersion; + TestContext = testContext; + + ServerLogger.LogMessagePublished += (s, e) => + { + if (Debugger.IsAttached) + { + Debug.WriteLine(e.LogMessage.ToString()); + } + + if (e.LogMessage.Level == MqttNetLogLevel.Error) + { + lock (_serverErrors) + { + _serverErrors.Add(e.LogMessage.ToString()); + } + } + }; + + ClientLogger.LogMessagePublished += (s, e) => + { + if (Debugger.IsAttached) + { + Debug.WriteLine(e.LogMessage.ToString()); + } + + if (e.LogMessage.Level == MqttNetLogLevel.Error) + { + lock (_clientErrors) + { + _clientErrors.Add(e.LogMessage.ToString()); + } + } + }; + } + + public MqttNetEventLogger ClientLogger { get; } = new MqttNetEventLogger("client"); + + public static bool EnableLogger { get; set; } = true; + + public MqttFactory Factory { get; } = new MqttFactory(); + + public bool IgnoreClientLogErrors { get; set; } + + public bool IgnoreServerLogErrors { get; set; } + + public MqttServer Server { get; private set; } + + public MqttNetEventLogger ServerLogger { get; } = new MqttNetEventLogger("server"); + + public int ServerPort { get; set; } + + public TestContext TestContext { get; } + + public Task ConnectClient() + { + return ConnectClient(Factory.CreateClientOptionsBuilder().WithProtocolVersion(_protocolVersion)); + } + + public async Task ConnectClient(Action optionsBuilder, TimeSpan timeout = default) + { + if (optionsBuilder == null) + { + throw new ArgumentNullException(nameof(optionsBuilder)); + } + + var options = Factory.CreateClientOptionsBuilder().WithProtocolVersion(_protocolVersion).WithTcpServer("127.0.0.1", ServerPort); + + optionsBuilder.Invoke(options); + + var client = CreateClient(); + + if (timeout == TimeSpan.Zero) + { + await client.ConnectAsync(options.Build()).ConfigureAwait(false); + } + else + { + using (var timeoutToken = new CancellationTokenSource(timeout)) + { + await client.ConnectAsync(options.Build(), timeoutToken.Token).ConfigureAwait(false); + } + } + + return client; + } + + public async Task ConnectClient(MqttClientOptionsBuilder options, TimeSpan timeout = default) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + options = options.WithTcpServer("127.0.0.1", ServerPort); + + var client = CreateClient(); + + if (timeout == TimeSpan.Zero) + { + await client.ConnectAsync(options.Build()).ConfigureAwait(false); + } + else + { + using (var timeoutToken = new CancellationTokenSource(timeout)) + { + await client.ConnectAsync(options.Build(), timeoutToken.Token).ConfigureAwait(false); + } + } + + return client; + } + + public async Task ConnectClient(MqttClientOptions options, TimeSpan timeout = default) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + var client = CreateClient(); + + if (timeout == TimeSpan.Zero) + { + await client.ConnectAsync(options).ConfigureAwait(false); + } + else + { + using (var timeoutToken = new CancellationTokenSource(timeout)) + { + await client.ConnectAsync(options, timeoutToken.Token).ConfigureAwait(false); + } + } + + return client; + } + + public async Task ConnectLowLevelClient(Action optionsBuilder = null) + { + var options = new MqttClientOptionsBuilder(); + options = options.WithTcpServer("127.0.0.1", ServerPort); + optionsBuilder?.Invoke(options); + + var client = CreateLowLevelClient(); + await client.ConnectAsync(options.Build(), CancellationToken.None).ConfigureAwait(false); + + return client; + } + + public async Task ConnectRpcClient(MqttRpcClientOptions options) + { + return new MqttRpcClient(await ConnectClient(), options); + } + + public TestApplicationMessageReceivedHandler CreateApplicationMessageHandler(MqttClient mqttClient) + { + return new TestApplicationMessageReceivedHandler(mqttClient); + } + + public MqttClient CreateClient() + { + lock (_clients) + { + var logger = EnableLogger ? (IMqttNetLogger)ClientLogger : new MqttNetNullLogger(); + + var client = Factory.CreateMqttClient(logger); + _clients.Add(client); + + client.ConnectingAsync += e => + { + if (TestContext != null) + { + var clientOptions = e.ClientOptions; + var existingClientId = clientOptions.ClientId; + if (existingClientId != null && !existingClientId.StartsWith(TestContext.TestName)) + { + clientOptions.ClientId = TestContext.TestName + "_" + existingClientId; + } + } + + return PlatformAbstractionLayer.CompletedTask; + }; + + return client; + } + } + + public LowLevelMqttClient CreateLowLevelClient() + { + lock (_clients) + { + var client = Factory.CreateLowLevelMqttClient(ClientLogger); + _lowLevelClients.Add(client); + + return client; + } + } + + public MqttServer CreateServer(MqttServerOptions options) + { + if (Server != null) + { + throw new InvalidOperationException("Server already started."); + } + + var logger = EnableLogger ? (IMqttNetLogger)ServerLogger : new MqttNetNullLogger(); + + Server = Factory.CreateMqttServer(options, logger); + + Server.ValidatingConnectionAsync += e => + { + if (TestContext != null) + { + // Null is used when the client id is assigned from the server! + if (!string.IsNullOrEmpty(e.ClientId) && !e.ClientId.StartsWith(TestContext.TestName)) + { + TrackException(new InvalidOperationException($"Invalid client ID used ({e.ClientId}). It must start with UnitTest name.")); + e.ReasonCode = MqttConnectReasonCode.ClientIdentifierNotValid; + } + } + + return PlatformAbstractionLayer.CompletedTask; + }; + + return Server; + } + + public void Dispose() + { + foreach (var mqttClient in _clients) + { + try + { + //mqttClient.DisconnectAsync().GetAwaiter().GetResult(); + } + catch + { + // This can happen when the test already disconnected the client. + } + finally + { + mqttClient?.Dispose(); + } + } + + foreach (var lowLevelMqttClient in _lowLevelClients) + { + lowLevelMqttClient.Dispose(); + } + + try + { + Server?.StopAsync().GetAwaiter().GetResult(); + } + catch + { + // This can happen when the test already stopped the server. + } + finally + { + Server?.Dispose(); + } + + ThrowIfLogErrors(); + + if (_exceptions.Any()) + { + throw new Exception($"{_exceptions.Count} exceptions tracked.\r\n" + string.Join(Environment.NewLine, _exceptions)); + } + } + + public Task StartServer() + { + return StartServer(Factory.CreateServerOptionsBuilder()); + } + + public async Task StartServer(MqttServerOptionsBuilder optionsBuilder) + { + optionsBuilder.WithDefaultEndpoint(); + optionsBuilder.WithDefaultEndpointPort(ServerPort); + optionsBuilder.WithMaxPendingMessagesPerClient(int.MaxValue); + + var options = optionsBuilder.Build(); + var server = CreateServer(options); + await server.StartAsync(); + + // The OS has chosen the port to we have to properly expose it to the tests. + ServerPort = options.DefaultEndpointOptions.Port; + return server; + } + + public async Task StartServer(Action configure) + { + var optionsBuilder = Factory.CreateServerOptionsBuilder(); + + optionsBuilder.WithDefaultEndpoint(); + optionsBuilder.WithDefaultEndpointPort(ServerPort); + optionsBuilder.WithMaxPendingMessagesPerClient(int.MaxValue); + + configure?.Invoke(optionsBuilder); + + var options = optionsBuilder.Build(); + var server = CreateServer(options); + await server.StartAsync(); + + // The OS has chosen the port to we have to properly expose it to the tests. + ServerPort = options.DefaultEndpointOptions.Port; + return server; + } + + public void ThrowIfLogErrors() + { + lock (_serverErrors) + { + if (!IgnoreServerLogErrors && _serverErrors.Count > 0) + { + var message = $"Server had {_serverErrors.Count} errors (${string.Join(Environment.NewLine, _serverErrors)})."; + Console.WriteLine(message); + throw new Exception(message); + } + } + + lock (_clientErrors) + { + if (!IgnoreClientLogErrors && _clientErrors.Count > 0) + { + var message = $"Client(s) had {_clientErrors.Count} errors (${string.Join(Environment.NewLine, _clientErrors)})"; + Console.WriteLine(message); + throw new Exception(message); + } + } + } + + public void TrackException(Exception exception) + { + lock (_exceptions) + { + _exceptions.Add(exception); + } + } + } +} \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/Mockups/TestLogger.cs b/Source/MQTTnet.Tests/Mockups/TestLogger.cs similarity index 72% rename from Tests/MQTTnet.Core.Tests/Mockups/TestLogger.cs rename to Source/MQTTnet.Tests/Mockups/TestLogger.cs index be8f8cf..0514664 100644 --- a/Tests/MQTTnet.Core.Tests/Mockups/TestLogger.cs +++ b/Source/MQTTnet.Tests/Mockups/TestLogger.cs @@ -1,5 +1,9 @@ -using System; -using MQTTnet.Diagnostics.Logger; +// 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.Diagnostics; namespace MQTTnet.Tests.Mockups { diff --git a/Tests/MQTTnet.Core.Tests/MqttApplicationMessageBuilder_Tests.cs b/Source/MQTTnet.Tests/MqttApplicationMessageBuilder_Tests.cs similarity index 92% rename from Tests/MQTTnet.Core.Tests/MqttApplicationMessageBuilder_Tests.cs rename to Source/MQTTnet.Tests/MqttApplicationMessageBuilder_Tests.cs index f4e5d9f..50aa447 100644 --- a/Tests/MQTTnet.Core.Tests/MqttApplicationMessageBuilder_Tests.cs +++ b/Source/MQTTnet.Tests/MqttApplicationMessageBuilder_Tests.cs @@ -1,4 +1,8 @@ -using System; +// 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.IO; using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/Tests/MQTTnet.Core.Tests/MqttPacketIdentifierProvider_Tests.cs b/Source/MQTTnet.Tests/MqttPacketIdentifierProvider_Tests.cs similarity index 76% rename from Tests/MQTTnet.Core.Tests/MqttPacketIdentifierProvider_Tests.cs rename to Source/MQTTnet.Tests/MqttPacketIdentifierProvider_Tests.cs index d3fb35c..b728036 100644 --- a/Tests/MQTTnet.Core.Tests/MqttPacketIdentifierProvider_Tests.cs +++ b/Source/MQTTnet.Tests/MqttPacketIdentifierProvider_Tests.cs @@ -1,4 +1,8 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +// 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 Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; namespace MQTTnet.Tests diff --git a/Source/MQTTnet.Tests/MqttPacketSerializationHelper.cs b/Source/MQTTnet.Tests/MqttPacketSerializationHelper.cs new file mode 100644 index 0000000..4d4a671 --- /dev/null +++ b/Source/MQTTnet.Tests/MqttPacketSerializationHelper.cs @@ -0,0 +1,65 @@ +using System; +using System.Threading; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; +using MQTTnet.Formatter; +using MQTTnet.Internal; +using MQTTnet.Packets; + +namespace MQTTnet.Tests +{ + public sealed class MqttPacketSerializationHelper : IDisposable + { + readonly IMqttPacketFormatter _packetFormatter; + readonly MqttProtocolVersion _protocolVersion; + + public MqttPacketSerializationHelper(MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311, MqttBufferWriter bufferWriter = null) + { + _protocolVersion = protocolVersion; + + if (bufferWriter == null) + { + bufferWriter = new MqttBufferWriter(4096, 65535); + } + + _packetFormatter = MqttPacketFormatterAdapter.GetMqttPacketFormatter(_protocolVersion, bufferWriter); + } + + public MqttPacket Decode(MqttPacketBuffer buffer) + { + using (var channel = new TestMqttChannel(buffer.ToArray())) + { + var formatterAdapter = new MqttPacketFormatterAdapter(_protocolVersion, new MqttBufferWriter(4096, 65535)); + + var adapter = new MqttChannelAdapter(channel, formatterAdapter, null, MqttNetNullLogger.Instance); + return adapter.ReceivePacketAsync(CancellationToken.None).GetAwaiter().GetResult(); + } + } + + public void Dispose() + { + } + + public MqttPacketBuffer Encode(MqttPacket packet) + { + return _packetFormatter.Encode(packet); + } + + public static TPacket EncodeAndDecodePacket(TPacket packet, MqttProtocolVersion protocolVersion) where TPacket : MqttPacket + { + using (var helper = new MqttPacketSerializationHelper(protocolVersion)) + { + var buffer = helper.Encode(packet); + return (TPacket)helper.Decode(buffer); + } + } + + public static byte[] EncodePacket(MqttPacket packet) + { + using (var helper = new MqttPacketSerializationHelper()) + { + return helper.Encode(packet).ToArray(); + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/MqttPacketWriter_Tests.cs b/Source/MQTTnet.Tests/MqttPacketWriter_Tests.cs new file mode 100644 index 0000000..02eeb40 --- /dev/null +++ b/Source/MQTTnet.Tests/MqttPacketWriter_Tests.cs @@ -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 Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Formatter; + +namespace MQTTnet.Tests +{ + [TestClass] + public class MqttPacketWriter_Tests + { + protected virtual MqttBufferWriter WriterFactory() + { + return new MqttBufferWriter(4096, 65535); + } + + [TestMethod] + public void WritePacket() + { + var writer = WriterFactory(); + Assert.AreEqual(0, writer.Length); + + writer.WriteString("1234567890"); + Assert.AreEqual(10 + 2, writer.Length); + + writer.WriteBinaryData(new byte[300]); + Assert.AreEqual(300 + 2 + 12, writer.Length); + + writer.WriteBinaryData(new byte[5000]); + Assert.AreEqual(5000 + 2 + 300 + 2 + 12, writer.Length); + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/MqttTcpChannel_Tests.cs b/Source/MQTTnet.Tests/MqttTcpChannel_Tests.cs similarity index 90% rename from Tests/MQTTnet.Core.Tests/MqttTcpChannel_Tests.cs rename to Source/MQTTnet.Tests/MqttTcpChannel_Tests.cs index 4965da2..efbca1f 100644 --- a/Tests/MQTTnet.Core.Tests/MqttTcpChannel_Tests.cs +++ b/Source/MQTTnet.Tests/MqttTcpChannel_Tests.cs @@ -1,4 +1,8 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +// 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 Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Implementations; using System; using System.Net; diff --git a/Tests/MQTTnet.Core.Tests/MqttTopicValidatorSubscribe_Tests.cs b/Source/MQTTnet.Tests/MqttTopicValidatorSubscribe_Tests.cs similarity index 87% rename from Tests/MQTTnet.Core.Tests/MqttTopicValidatorSubscribe_Tests.cs rename to Source/MQTTnet.Tests/MqttTopicValidatorSubscribe_Tests.cs index 14797a0..13394a7 100644 --- a/Tests/MQTTnet.Core.Tests/MqttTopicValidatorSubscribe_Tests.cs +++ b/Source/MQTTnet.Tests/MqttTopicValidatorSubscribe_Tests.cs @@ -1,3 +1,7 @@ +// 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 Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Exceptions; using MQTTnet.Protocol; diff --git a/Tests/MQTTnet.Core.Tests/MqttTopicValidator_Tests.cs b/Source/MQTTnet.Tests/MqttTopicValidator_Tests.cs similarity index 78% rename from Tests/MQTTnet.Core.Tests/MqttTopicValidator_Tests.cs rename to Source/MQTTnet.Tests/MqttTopicValidator_Tests.cs index f192294..10f6052 100644 --- a/Tests/MQTTnet.Core.Tests/MqttTopicValidator_Tests.cs +++ b/Source/MQTTnet.Tests/MqttTopicValidator_Tests.cs @@ -1,4 +1,8 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +// 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 Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Exceptions; using MQTTnet.Protocol; diff --git a/Source/MQTTnet.Tests/Protocol_Tests.cs b/Source/MQTTnet.Tests/Protocol_Tests.cs new file mode 100644 index 0000000..41b703c --- /dev/null +++ b/Source/MQTTnet.Tests/Protocol_Tests.cs @@ -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 Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Formatter; + +namespace MQTTnet.Tests +{ + [TestClass] + public class Protocol_Tests + { + [TestMethod] + public void Encode_Four_Byte_Integer() + { + var writer = new MqttBufferWriter(4, 4); + + for (uint value = 0; value < 268435455; value++) + { + writer.WriteVariableByteInteger(value); + + var buffer = writer.GetBuffer(); + + var reader = new MqttBufferReader(); + reader.SetBuffer(buffer, 0, writer.Length); + var checkValue = reader.ReadVariableByteInteger(); + + Assert.AreEqual(value, checkValue); + + writer.Reset(0); + } + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/RPC_Tests.cs b/Source/MQTTnet.Tests/RPC_Tests.cs similarity index 82% rename from Tests/MQTTnet.Core.Tests/RPC_Tests.cs rename to Source/MQTTnet.Tests/RPC_Tests.cs index 332c374..e37996e 100644 --- a/Tests/MQTTnet.Core.Tests/RPC_Tests.cs +++ b/Source/MQTTnet.Tests/RPC_Tests.cs @@ -1,25 +1,24 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; using System.Text; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Tests.Mockups; using MQTTnet.Client; -using MQTTnet.Client.Receiving; using MQTTnet.Exceptions; using MQTTnet.Extensions.Rpc; using MQTTnet.Protocol; -using MQTTnet.Client.Options; using MQTTnet.Formatter; -using MQTTnet.Extensions.Rpc.Options; -using MQTTnet.Extensions.Rpc.Options.TopicGeneration; +using MQTTnet.Implementations; namespace MQTTnet.Tests { [TestClass] - public class RPC_Tests + public sealed class RPC_Tests : BaseTestClass { - public TestContext TestContext { get; set; } - [TestMethod] public Task Execute_Success_With_QoS_0() { @@ -79,7 +78,7 @@ namespace MQTTnet.Tests [ExpectedException(typeof(MqttCommunicationTimedOutException))] public async Task Execute_Timeout() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { await testEnvironment.StartServer(); @@ -94,7 +93,7 @@ namespace MQTTnet.Tests [ExpectedException(typeof(MqttCommunicationTimedOutException))] public async Task Execute_With_Custom_Topic_Names() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { await testEnvironment.StartServer(); @@ -108,16 +107,13 @@ namespace MQTTnet.Tests async Task Execute_Success(MqttQualityOfServiceLevel qosLevel, MqttProtocolVersion protocolVersion) { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { await testEnvironment.StartServer(); var responseSender = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(protocolVersion)); await responseSender.SubscribeAsync("MQTTnet.RPC/+/ping", qosLevel); - responseSender.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(async e => - { - await responseSender.PublishAsync(e.ApplicationMessage.Topic + "/response", "pong"); - }); + responseSender.ApplicationMessageReceivedAsync += e => responseSender.PublishStringAsync(e.ApplicationMessage.Topic + "/response", "pong"); var requestSender = await testEnvironment.ConnectClient(); @@ -132,16 +128,16 @@ namespace MQTTnet.Tests async Task Execute_Success_MQTT_V5(MqttQualityOfServiceLevel qosLevel) { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { await testEnvironment.StartServer(); var responseSender = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); await responseSender.SubscribeAsync("MQTTnet.RPC/+/ping", qosLevel); - responseSender.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(async e => + responseSender.ApplicationMessageReceivedAsync += async e => { - await responseSender.PublishAsync(e.ApplicationMessage.ResponseTopic, "pong"); - }); + await responseSender.PublishStringAsync(e.ApplicationMessage.ResponseTopic, "pong"); + }; var requestSender = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); @@ -157,17 +153,17 @@ namespace MQTTnet.Tests [TestMethod] public async Task Execute_Success_MQTT_V5_Mixed_Clients() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { await testEnvironment.StartServer(); var responseSender = await testEnvironment.ConnectClient(); await responseSender.SubscribeAsync("MQTTnet.RPC/+/ping", MqttQualityOfServiceLevel.AtMostOnce); - responseSender.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(async e => + responseSender.ApplicationMessageReceivedAsync += async e => { Assert.IsNull(e.ApplicationMessage.ResponseTopic); - await responseSender.PublishAsync(e.ApplicationMessage.Topic + "/response", "pong"); - }); + await responseSender.PublishStringAsync(e.ApplicationMessage.Topic + "/response", "pong"); + }; var requestSender = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); @@ -190,10 +186,11 @@ namespace MQTTnet.Tests var responseSender = await testEnvironment.ConnectClient(); await responseSender.SubscribeAsync("MQTTnet.RPC/+/ping", MqttQualityOfServiceLevel.AtMostOnce); - responseSender.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(e => + responseSender.ApplicationMessageReceivedAsync += async e => { Assert.IsNull(e.ApplicationMessage.ResponseTopic); - }); + await PlatformAbstractionLayer.CompletedTask; + }; var requestSender = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)); diff --git a/Tests/MQTTnet.Core.Tests/RoundtripTime_Tests.cs b/Source/MQTTnet.Tests/RoundtripTime_Tests.cs similarity index 62% rename from Tests/MQTTnet.Core.Tests/RoundtripTime_Tests.cs rename to Source/MQTTnet.Tests/RoundtripTime_Tests.cs index a790c54..6ebbab0 100644 --- a/Tests/MQTTnet.Core.Tests/RoundtripTime_Tests.cs +++ b/Source/MQTTnet.Tests/RoundtripTime_Tests.cs @@ -1,9 +1,14 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; +using MQTTnet.Implementations; using MQTTnet.Tests.Mockups; namespace MQTTnet.Tests @@ -19,18 +24,20 @@ namespace MQTTnet.Tests using (var testEnvironment = new TestEnvironment(TestContext)) { await testEnvironment.StartServer(); + var receiverClient = await testEnvironment.ConnectClient(); var senderClient = await testEnvironment.ConnectClient(); - await receiverClient.SubscribeAsync("#"); - TaskCompletionSource response = null; - receiverClient.UseApplicationMessageReceivedHandler(e => + receiverClient.ApplicationMessageReceivedAsync += e => { - response?.SetResult(e.ApplicationMessage.ConvertPayloadToString()); - }); + response?.TrySetResult(e.ApplicationMessage.ConvertPayloadToString()); + return PlatformAbstractionLayer.CompletedTask; + }; + await receiverClient.SubscribeAsync("#"); + var times = new List(); var stopwatch = Stopwatch.StartNew(); @@ -39,8 +46,11 @@ namespace MQTTnet.Tests for (var i = 0; i < 100; i++) { response = new TaskCompletionSource(); - await senderClient.PublishAsync("test", DateTime.UtcNow.Ticks.ToString()); - response.Task.GetAwaiter().GetResult(); + await senderClient.PublishStringAsync("test", DateTime.UtcNow.Ticks.ToString()); + if (!response.Task.Wait(TimeSpan.FromSeconds(5))) + { + throw new TimeoutException(); + } stopwatch.Stop(); times.Add(stopwatch.Elapsed); diff --git a/Tests/MQTTnet.Core.Tests/Server/Assigned_Client_ID_Tests.cs b/Source/MQTTnet.Tests/Server/Assigned_Client_ID_Tests.cs similarity index 65% rename from Tests/MQTTnet.Core.Tests/Server/Assigned_Client_ID_Tests.cs rename to Source/MQTTnet.Tests/Server/Assigned_Client_ID_Tests.cs index 6636598..8e983be 100644 --- a/Tests/MQTTnet.Core.Tests/Server/Assigned_Client_ID_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Assigned_Client_ID_Tests.cs @@ -1,11 +1,14 @@ -using System.Threading; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; -using MQTTnet.Client.Options; using MQTTnet.Formatter; +using MQTTnet.Implementations; using MQTTnet.Protocol; -using MQTTnet.Server; namespace MQTTnet.Tests.Server { @@ -21,7 +24,7 @@ namespace MQTTnet.Tests.Server [TestMethod] public Task Connect_With_Client_Id() { - return Connect_With_Client_Id("Connect_With_Client_Idtest_456", null, "test_456", null); + return Connect_With_Client_Id("Connect_With_Client_Id_test_456", null, "test_456", null); } async Task Connect_With_Client_Id(string expectedClientId, string expectedReturnedClientId, string usedClientId, string assignedClientId) @@ -34,34 +37,39 @@ namespace MQTTnet.Tests.Server // Arrange server var disconnectedMre = new ManualResetEventSlim(); - var serverOptions = new MqttServerOptionsBuilder() - .WithConnectionValidator(context => + + var server = await testEnvironment.StartServer(); + server.ValidatingConnectionAsync += e => + { + if (string.IsNullOrEmpty(e.ClientId)) { - if (string.IsNullOrEmpty(context.ClientId)) - { - context.AssignedClientIdentifier = assignedClientId; - context.ReasonCode = MqttConnectReasonCode.Success; - } - }); + e.AssignedClientIdentifier = assignedClientId; + e.ReasonCode = MqttConnectReasonCode.Success; + } - await testEnvironment.StartServer(serverOptions); - testEnvironment.Server.UseClientConnectedHandler(args => + return PlatformAbstractionLayer.CompletedTask; + }; + + testEnvironment.Server.ClientConnectedAsync += args => { serverConnectedClientId = args.ClientId; - }); + return PlatformAbstractionLayer.CompletedTask; + }; - testEnvironment.Server.UseClientDisconnectedHandler(args => + testEnvironment.Server.ClientDisconnectedAsync += args => { serverDisconnectedClientId = args.ClientId; disconnectedMre.Set(); - }); + return PlatformAbstractionLayer.CompletedTask; + }; // Arrange client var client = testEnvironment.CreateClient(); - client.UseConnectedHandler(args => + client.ConnectedAsync += args => { clientAssignedClientId = args.ConnectResult.AssignedClientIdentifier; - }); + return PlatformAbstractionLayer.CompletedTask; + }; // Act await client.ConnectAsync(new MqttClientOptionsBuilder() diff --git a/Tests/MQTTnet.Core.Tests/Server/Connection_Tests.cs b/Source/MQTTnet.Tests/Server/Connection_Tests.cs similarity index 92% rename from Tests/MQTTnet.Core.Tests/Server/Connection_Tests.cs rename to Source/MQTTnet.Tests/Server/Connection_Tests.cs index cbe9e99..f4d5ea9 100644 --- a/Tests/MQTTnet.Core.Tests/Server/Connection_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Connection_Tests.cs @@ -1,4 +1,8 @@ -using System; +// 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.Sockets; using System.Text; using System.Threading; diff --git a/Tests/MQTTnet.Core.Tests/Server/Events_Tests.cs b/Source/MQTTnet.Tests/Server/Events_Tests.cs similarity index 75% rename from Tests/MQTTnet.Core.Tests/Server/Events_Tests.cs rename to Source/MQTTnet.Tests/Server/Events_Tests.cs index f74d713..a8d2336 100644 --- a/Tests/MQTTnet.Core.Tests/Server/Events_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Events_Tests.cs @@ -1,9 +1,13 @@ -using System; +// 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.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; -using MQTTnet.Client.Receiving; using MQTTnet.Formatter; +using MQTTnet.Implementations; using MQTTnet.Protocol; using MQTTnet.Server; @@ -19,11 +23,12 @@ namespace MQTTnet.Tests.Server { var server = await testEnvironment.StartServer(); - MqttServerClientConnectedEventArgs eventArgs = null; - server.ClientConnectedHandler = new MqttServerClientConnectedHandlerDelegate(e => + ClientConnectedEventArgs eventArgs = null; + server.ClientConnectedAsync += e => { eventArgs = e; - }); + return PlatformAbstractionLayer.CompletedTask; + }; await testEnvironment.ConnectClient(o => o.WithCredentials("TheUser")); @@ -45,11 +50,12 @@ namespace MQTTnet.Tests.Server { var server = await testEnvironment.StartServer(); - MqttServerClientDisconnectedEventArgs eventArgs = null; - server.ClientDisconnectedHandler = new MqttServerClientDisconnectedHandlerDelegate(e => + ClientDisconnectedEventArgs eventArgs = null; + server.ClientDisconnectedAsync += e => { eventArgs = e; - }); + return PlatformAbstractionLayer.CompletedTask; + }; var client = await testEnvironment.ConnectClient(o => o.WithCredentials("TheUser")); await client.DisconnectAsync(); @@ -71,11 +77,12 @@ namespace MQTTnet.Tests.Server { var server = await testEnvironment.StartServer(); - MqttServerClientSubscribedTopicEventArgs eventArgs = null; - server.ClientSubscribedTopicHandler = new MqttServerClientSubscribedTopicHandlerDelegate(e => + ClientSubscribedTopicEventArgs eventArgs = null; + server.ClientSubscribedTopicAsync += e => { eventArgs = e; - }); + return PlatformAbstractionLayer.CompletedTask; + }; var client = await testEnvironment.ConnectClient(); await client.SubscribeAsync("The/Topic", MqttQualityOfServiceLevel.AtLeastOnce); @@ -97,11 +104,12 @@ namespace MQTTnet.Tests.Server { var server = await testEnvironment.StartServer(); - MqttServerClientUnsubscribedTopicEventArgs eventArgs = null; - server.ClientUnsubscribedTopicHandler = new MqttServerClientUnsubscribedTopicHandlerDelegate(e => + ClientUnsubscribedTopicEventArgs eventArgs = null; + server.ClientUnsubscribedTopicAsync += e => { eventArgs = e; - }); + return PlatformAbstractionLayer.CompletedTask; + }; var client = await testEnvironment.ConnectClient(); await client.UnsubscribeAsync("The/Topic"); @@ -122,14 +130,15 @@ namespace MQTTnet.Tests.Server { var server = await testEnvironment.StartServer(); - MqttApplicationMessageReceivedEventArgs eventArgs = null; - server.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(e => + InterceptingPublishEventArgs eventArgs = null; + server.InterceptingPublishAsync += e => { eventArgs = e; - }); + return PlatformAbstractionLayer.CompletedTask; + }; var client = await testEnvironment.ConnectClient(); - await client.PublishAsync("The_Topic", "The_Payload"); + await client.PublishStringAsync("The_Topic", "The_Payload"); await LongTestDelay(); @@ -146,15 +155,16 @@ namespace MQTTnet.Tests.Server { using (var testEnvironment = CreateTestEnvironment()) { - var server = testEnvironment.CreateServer(); - + var server = testEnvironment.CreateServer(new MqttServerOptions()); + EventArgs eventArgs = null; - server.StartedHandler = new MqttServerStartedHandlerDelegate(e => + server.StartedAsync += e => { eventArgs = e; - }); - - await server.StartAsync(new MqttServerOptionsBuilder().Build()); + return PlatformAbstractionLayer.CompletedTask; + }; + + await server.StartAsync(); await LongTestDelay(); @@ -170,10 +180,11 @@ namespace MQTTnet.Tests.Server var server = await testEnvironment.StartServer(); EventArgs eventArgs = null; - server.StoppedHandler = new MqttServerStoppedHandlerDelegate(e => + server.StoppedAsync += e => { eventArgs = e; - }); + return PlatformAbstractionLayer.CompletedTask; + }; await server.StopAsync(); diff --git a/Tests/MQTTnet.Core.Tests/Server/General.cs b/Source/MQTTnet.Tests/Server/General.cs similarity index 50% rename from Tests/MQTTnet.Core.Tests/Server/General.cs rename to Source/MQTTnet.Tests/Server/General.cs index 7d0ba93..f7bf4a7 100644 --- a/Tests/MQTTnet.Core.Tests/Server/General.cs +++ b/Source/MQTTnet.Tests/Server/General.cs @@ -1,3 +1,7 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + using System; using System.Collections.Generic; using System.Linq; @@ -7,459 +11,493 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Adapter; using MQTTnet.Client; -using MQTTnet.Client.Connecting; -using MQTTnet.Client.Disconnecting; -using MQTTnet.Client.Options; -using MQTTnet.Client.Receiving; -using MQTTnet.Client.Subscribing; using MQTTnet.Formatter; +using MQTTnet.Implementations; +using MQTTnet.Packets; using MQTTnet.Protocol; using MQTTnet.Server; -using MQTTnet.Tests.Mockups; namespace MQTTnet.Tests.Server { [TestClass] - public sealed class General_Tests + public sealed class General_Tests : BaseTestClass { - public TestContext TestContext { get; set; } + Dictionary _connected; [TestMethod] - [DataRow("", null)] - [DataRow("", "")] - [DataRow(null, null)] - public async Task Use_Admissible_Credentials(string username, string password) + public async Task Client_Disconnect_Without_Errors() { - using (var testEnvironment = new TestEnvironment()) + using (var testEnvironment = CreateTestEnvironment()) { - await testEnvironment.StartServer(); + bool clientWasConnected; - var client = testEnvironment.CreateClient(); + var server = await testEnvironment.StartServer(new MqttServerOptionsBuilder()); + try + { + var client = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder()); - var clientOptions = new MqttClientOptionsBuilder() - .WithTcpServer("localhost", testEnvironment.ServerPort) - .WithCredentials(username, password) - .Build(); + clientWasConnected = true; - var connectResult = await client.ConnectAsync(clientOptions); + await client.DisconnectAsync(); - Assert.IsFalse(connectResult.IsSessionPresent); - Assert.IsTrue(client.IsConnected); + await Task.Delay(500); + } + finally + { + await server.StopAsync(); + } + + Assert.IsTrue(clientWasConnected); + + testEnvironment.ThrowIfLogErrors(); } } [TestMethod] - public async Task Use_Empty_Client_ID() + public async Task Collect_Messages_In_Disconnected_Session() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { - await testEnvironment.StartServer(); + var server = await testEnvironment.StartServer(new MqttServerOptionsBuilder().WithPersistentSessions()); - var client = testEnvironment.CreateClient(); + // Create the session including the subscription. + var client1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("a").WithCleanSession(false)); + await client1.SubscribeAsync("x"); + await client1.DisconnectAsync(); + await Task.Delay(500); + + var clientStatus = await server.GetClientsAsync(); + Assert.AreEqual(0, clientStatus.Count); - var clientOptions = new MqttClientOptionsBuilder() - .WithTcpServer("localhost", testEnvironment.ServerPort) - .WithClientId(string.Empty) - .Build(); + var client2 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("b").WithCleanSession(false)); + await client2.PublishStringAsync("x", "1"); + await client2.PublishStringAsync("x", "2"); + await client2.PublishStringAsync("x", "3"); + await client2.DisconnectAsync(); - var connectResult = await client.ConnectAsync(clientOptions); + await Task.Delay(500); - Assert.IsFalse(connectResult.IsSessionPresent); - Assert.IsTrue(client.IsConnected); + clientStatus = await server.GetClientsAsync(); + var sessionStatus = await server.GetSessionsAsync(); + + Assert.AreEqual(0, clientStatus.Count); + Assert.AreEqual(2, sessionStatus.Count); + + Assert.AreEqual(3, sessionStatus.First(s => s.Id == client1.Options.ClientId).PendingApplicationMessagesCount); } } [TestMethod] - public async Task Publish_At_Most_Once_0x00() + public async Task Deny_Connection() { - await TestPublishAsync( - "A/B/C", - MqttQualityOfServiceLevel.AtMostOnce, - "A/B/C", - MqttQualityOfServiceLevel.AtMostOnce, - 1, - TestContext); - } + using (var testEnvironment = CreateTestEnvironment()) + { + testEnvironment.IgnoreClientLogErrors = true; - [TestMethod] - public async Task Publish_At_Least_Once_0x01() - { - await TestPublishAsync( - "A/B/C", - MqttQualityOfServiceLevel.AtLeastOnce, - "A/B/C", - MqttQualityOfServiceLevel.AtLeastOnce, - 1, - TestContext); - } + var server = await testEnvironment.StartServer(); - [TestMethod] - public async Task Publish_Exactly_Once_0x02() - { - await TestPublishAsync( - "A/B/C", - MqttQualityOfServiceLevel.ExactlyOnce, - "A/B/C", - MqttQualityOfServiceLevel.ExactlyOnce, - 1, - TestContext); + server.ValidatingConnectionAsync += e => + { + e.ReasonCode = MqttConnectReasonCode.NotAuthorized; + return PlatformAbstractionLayer.CompletedTask; + }; + + var connectingFailedException = await Assert.ThrowsExceptionAsync(() => testEnvironment.ConnectClient()); + Assert.AreEqual(MqttClientConnectResultCode.NotAuthorized, connectingFailedException.ResultCode); + } } [TestMethod] - public async Task Use_Clean_Session() + public async Task Do_Not_Send_Retained_Messages_For_Denied_Subscription() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { - await testEnvironment.StartServer(); + var server = await testEnvironment.StartServer(); - var client = testEnvironment.CreateClient(); - var connectResult = await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort).WithCleanSession().Build()); + server.InterceptingSubscriptionAsync += e => + { + // This should lead to no subscriptions for "n" at all. So also no sending of retained messages. + if (e.TopicFilter.Topic == "n") + { + e.Response.ReasonCode = MqttSubscribeReasonCode.UnspecifiedError; + } - Assert.IsFalse(connectResult.IsSessionPresent); + return PlatformAbstractionLayer.CompletedTask; + }; + + // Prepare some retained messages. + var client1 = await testEnvironment.ConnectClient(); + await client1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("y").WithPayload("x").WithRetainFlag().Build()); + await client1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("n").WithPayload("x").WithRetainFlag().Build()); + await client1.DisconnectAsync(); + + await Task.Delay(500); + + // Subscribe to all retained message types. + // It is important to do this in a range of filters to ensure that a subscription is not "hidden". + var client2 = await testEnvironment.ConnectClient(); + + var buffer = new StringBuilder(); + + client2.ApplicationMessageReceivedAsync += e => + { + lock (buffer) + { + buffer.Append(e.ApplicationMessage.Topic); + } + + return PlatformAbstractionLayer.CompletedTask; + }; + + await client2.SubscribeAsync("y"); + await client2.SubscribeAsync("n"); + + await Task.Delay(500); + + // No 'n' in buffer! + Assert.AreEqual("y", buffer.ToString()); } } [TestMethod] - public async Task Will_Message_Do_Not_Send_On_Clean_Disconnect() + public async Task Handle_Clean_Disconnect() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { - var receivedMessagesCount = 0; + var server = await testEnvironment.StartServer(new MqttServerOptionsBuilder()); - await testEnvironment.StartServer(); + var clientConnectedCalled = 0; + var clientDisconnectedCalled = 0; - var willMessage = new MqttApplicationMessageBuilder().WithTopic("My/last/will").Build(); + server.ClientConnectedAsync += e => + { + Interlocked.Increment(ref clientConnectedCalled); + return PlatformAbstractionLayer.CompletedTask; + }; - var clientOptions = new MqttClientOptionsBuilder().WithWillMessage(willMessage); + server.ClientDisconnectedAsync += e => + { + Interlocked.Increment(ref clientDisconnectedCalled); + return PlatformAbstractionLayer.CompletedTask; + }; - var c1 = await testEnvironment.ConnectClient(); - c1.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(c => Interlocked.Increment(ref receivedMessagesCount)); - await c1.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("#").Build()); + var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder()); - var c2 = await testEnvironment.ConnectClient(clientOptions); - await c2.DisconnectAsync().ConfigureAwait(false); + await Task.Delay(1000); + + Assert.AreEqual(1, clientConnectedCalled); + Assert.AreEqual(0, clientDisconnectedCalled); await Task.Delay(1000); - Assert.AreEqual(0, receivedMessagesCount); + await c1.DisconnectAsync(); + + await Task.Delay(1000); + + Assert.AreEqual(1, clientConnectedCalled); + Assert.AreEqual(1, clientDisconnectedCalled); } } [TestMethod] - public async Task Will_Message_Do_Not_Send_On_Takeover() + public async Task Handle_Lots_Of_Parallel_Retained_Messages() { - using (var testEnvironment = new TestEnvironment(TestContext)) + const int clientCount = 50; + + using (var testEnvironment = CreateTestEnvironment()) { - var receivedMessagesCount = 0; + var server = await testEnvironment.StartServer(); - await testEnvironment.StartServer(); + var tasks = new List(); + for (var i = 0; i < clientCount; i++) + { + var i2 = i; + var testEnvironment2 = testEnvironment; - // C1 will receive the last will! - var c1 = await testEnvironment.ConnectClient(); - c1.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(c => Interlocked.Increment(ref receivedMessagesCount)); - await c1.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("#").Build()); + tasks.Add( + Task.Run( + async () => + { + try + { + using (var client = await testEnvironment2.ConnectClient()) + { + // Clear retained message. + await client.PublishAsync( + new MqttApplicationMessageBuilder().WithTopic("r" + i2) + .WithPayload(PlatformAbstractionLayer.EmptyByteArray) + .WithRetainFlag() + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) + .Build()); + + // Set retained message. + await client.PublishAsync( + new MqttApplicationMessageBuilder().WithTopic("r" + i2) + .WithPayload("value") + .WithRetainFlag() + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) + .Build()); + + await client.DisconnectAsync(); + } + } + catch (Exception exception) + { + testEnvironment2.TrackException(exception); + } + })); - // C2 has the last will defined. - var willMessage = new MqttApplicationMessageBuilder().WithTopic("My/last/will").Build(); + await Task.Delay(10); + } - var clientOptions = new MqttClientOptionsBuilder() - .WithWillMessage(willMessage) - .WithClientId("WillOwner"); + await Task.WhenAll(tasks); - var c2 = await testEnvironment.ConnectClient(clientOptions); + await Task.Delay(1000); - // C3 will do the connection takeover. - var c3 = await testEnvironment.ConnectClient(clientOptions); + var retainedMessages = await server.GetRetainedMessagesAsync(); - await Task.Delay(1000); + Assert.AreEqual(clientCount, retainedMessages.Count); - Assert.AreEqual(0, receivedMessagesCount); + for (var i = 0; i < clientCount; i++) + { + Assert.IsTrue(retainedMessages.Any(m => m.Topic == "r" + i)); + } } } [TestMethod] - public async Task Will_Message_Send() + public async Task Intercept_Application_Message() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { - var receivedMessagesCount = 0; - - await testEnvironment.StartServer(); + var server = await testEnvironment.StartServer(); - var willMessage = new MqttApplicationMessageBuilder().WithTopic("My/last/will").WithAtMostOnceQoS().Build(); + server.InterceptingPublishAsync += e => + { + e.ApplicationMessage = new MqttApplicationMessage { Topic = "new_topic" }; - var clientOptions = new MqttClientOptionsBuilder().WithWillMessage(willMessage); + return PlatformAbstractionLayer.CompletedTask; + }; + string receivedTopic = null; var c1 = await testEnvironment.ConnectClient(); - c1.UseApplicationMessageReceivedHandler(c => Interlocked.Increment(ref receivedMessagesCount)); - await c1.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("#").Build()); + await c1.SubscribeAsync("#"); - var c2 = await testEnvironment.ConnectClient(clientOptions); - c2.Dispose(); // Dispose will not send a DISCONNECT pattern first so the will message must be sent. + c1.ApplicationMessageReceivedAsync += a => + { + receivedTopic = a.ApplicationMessage.Topic; + return PlatformAbstractionLayer.CompletedTask; + }; - await Task.Delay(1000); + await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("original_topic").Build()); - Assert.AreEqual(1, receivedMessagesCount); + await Task.Delay(500); + Assert.AreEqual("new_topic", receivedTopic); } } [TestMethod] - public async Task Intercept_Subscription() + public async Task Intercept_Message() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { - await testEnvironment.StartServer(new MqttServerOptionsBuilder().WithSubscriptionInterceptor( - c => - { - // Set the topic to "a" regards what the client wants to subscribe. - c.TopicFilter.Topic = "a"; - })); + var server = await testEnvironment.StartServer(); + server.InterceptingPublishAsync += e => + { + e.ApplicationMessage.Payload = Encoding.ASCII.GetBytes("extended"); + return PlatformAbstractionLayer.CompletedTask; + }; - var topicAReceived = false; - var topicBReceived = false; + var c1 = await testEnvironment.ConnectClient(); + var c2 = await testEnvironment.ConnectClient(); + await c2.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("test").Build()); - var client = await testEnvironment.ConnectClient(); - client.UseApplicationMessageReceivedHandler(c => + var isIntercepted = false; + c2.ApplicationMessageReceivedAsync += e => { - if (c.ApplicationMessage.Topic == "a") - { - topicAReceived = true; - } - else if (c.ApplicationMessage.Topic == "b") - { - topicBReceived = true; - } - }); + isIntercepted = string.Compare("extended", Encoding.UTF8.GetString(e.ApplicationMessage.Payload), StringComparison.Ordinal) == 0; + return PlatformAbstractionLayer.CompletedTask; + }; - await client.SubscribeAsync("b"); - - await client.PublishAsync("a"); + await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("test").Build()); + await c1.DisconnectAsync(); await Task.Delay(500); - Assert.IsTrue(topicAReceived); - Assert.IsFalse(topicBReceived); + Assert.IsTrue(isIntercepted); } } [TestMethod] - public async Task Subscribe_Unsubscribe() + public async Task Intercept_Undelivered() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { - var receivedMessagesCount = 0; + var undeliverd = string.Empty; var server = await testEnvironment.StartServer(); + server.ApplicationMessageNotConsumedAsync += e => + { + undeliverd = e.ApplicationMessage.Topic; + return PlatformAbstractionLayer.CompletedTask; + }; - var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("c1")); - c1.UseApplicationMessageReceivedHandler(c => Interlocked.Increment(ref receivedMessagesCount)); + var client = await testEnvironment.ConnectClient(); - var c2 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("c2")); + await client.SubscribeAsync("b"); - var message = new MqttApplicationMessageBuilder().WithTopic("a").WithAtLeastOnceQoS().Build(); - await c2.PublishAsync(message); + await client.PublishStringAsync("a", null, MqttQualityOfServiceLevel.ExactlyOnce); await Task.Delay(500); - Assert.AreEqual(0, receivedMessagesCount); - var subscribeEventCalled = false; - server.ClientSubscribedTopicHandler = new MqttServerClientSubscribedTopicHandlerDelegate(e => - { - subscribeEventCalled = e.TopicFilter.Topic == "a" && e.ClientId == c1.Options.ClientId; - }); + Assert.AreEqual("a", undeliverd); + } + } + + [TestMethod] + public async Task No_Messages_If_No_Subscription() + { + using (var testEnvironment = CreateTestEnvironment()) + { + await testEnvironment.StartServer(); - await c1.SubscribeAsync(new MqttTopicFilter { Topic = "a", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }); - await Task.Delay(250); - Assert.IsTrue(subscribeEventCalled, "Subscribe event not called."); + var client = await testEnvironment.ConnectClient(); + var receivedMessages = new List(); - await c2.PublishAsync(message); - await Task.Delay(250); - Assert.AreEqual(1, receivedMessagesCount); + client.ConnectedAsync += async e => + { + await client.PublishStringAsync("Connected"); + }; - var unsubscribeEventCalled = false; - server.ClientUnsubscribedTopicHandler = new MqttServerClientUnsubscribedTopicHandlerDelegate(e => + client.ApplicationMessageReceivedAsync += e => { - unsubscribeEventCalled = e.TopicFilter == "a" && e.ClientId == c1.Options.ClientId; - }); + lock (receivedMessages) + { + receivedMessages.Add(e.ApplicationMessage); + } - await c1.UnsubscribeAsync("a"); - await Task.Delay(250); - Assert.IsTrue(unsubscribeEventCalled, "Unsubscribe event not called."); + return PlatformAbstractionLayer.CompletedTask; + }; - await c2.PublishAsync(message); await Task.Delay(500); - Assert.AreEqual(1, receivedMessagesCount); + + await client.PublishStringAsync("Hello"); await Task.Delay(500); - Assert.AreEqual(1, receivedMessagesCount); + Assert.AreEqual(0, receivedMessages.Count); } } [TestMethod] - public async Task Subscribe_Multiple_In_Single_Request() + public async Task Persist_Retained_Message() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { - var receivedMessagesCount = 0; + List savedRetainedMessages = null; - await testEnvironment.StartServer(); + var s = await testEnvironment.StartServer(); + s.RetainedMessageChangedAsync += e => + { + savedRetainedMessages = e.StoredRetainedMessages; + return PlatformAbstractionLayer.CompletedTask; + }; var c1 = await testEnvironment.ConnectClient(); - c1.UseApplicationMessageReceivedHandler(c => Interlocked.Increment(ref receivedMessagesCount)); - await c1.SubscribeAsync(new MqttClientSubscribeOptionsBuilder() - .WithTopicFilter("a") - .WithTopicFilter("b") - .WithTopicFilter("c") - .Build()); - - var c2 = await testEnvironment.ConnectClient(); - await c2.PublishAsync("a"); - await Task.Delay(100); - Assert.AreEqual(receivedMessagesCount, 1); + await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); - await c2.PublishAsync("b"); - await Task.Delay(100); - Assert.AreEqual(receivedMessagesCount, 2); + await Task.Delay(500); - await c2.PublishAsync("c"); - await Task.Delay(100); - Assert.AreEqual(receivedMessagesCount, 3); + Assert.AreEqual(1, savedRetainedMessages?.Count); } } [TestMethod] - public async Task Subscribe_Lots_In_Single_Request() + public async Task Publish_After_Client_Connects() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { - var receivedMessagesCount = 0; + var server = await testEnvironment.StartServer(); + server.ClientConnectedAsync += async e => + { + await server.InjectApplicationMessage( + new InjectedMqttApplicationMessage( + new MqttApplicationMessage + { + Topic = "/test/1", + Payload = Encoding.UTF8.GetBytes("true"), + QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce + }) + { + SenderClientId = "server" + }); + }; - await testEnvironment.StartServer(); + string receivedTopic = null; var c1 = await testEnvironment.ConnectClient(); - c1.UseApplicationMessageReceivedHandler(c => Interlocked.Increment(ref receivedMessagesCount)); - - var optionsBuilder = new MqttClientSubscribeOptionsBuilder(); - for (var i = 0; i < 500; i++) + c1.ApplicationMessageReceivedAsync += e => { - optionsBuilder.WithTopicFilter(i.ToString(), MqttQualityOfServiceLevel.AtMostOnce); - } - - await c1.SubscribeAsync(optionsBuilder.Build()).ConfigureAwait(false); + receivedTopic = e.ApplicationMessage.Topic; + return PlatformAbstractionLayer.CompletedTask; + }; - var c2 = await testEnvironment.ConnectClient(); - - var messageBuilder = new MqttApplicationMessageBuilder(); - for (var i = 0; i < 500; i++) - { - messageBuilder.WithTopic(i.ToString()); + await c1.SubscribeAsync("#"); - await c2.PublishAsync(messageBuilder.Build()).ConfigureAwait(false); - } + await testEnvironment.ConnectClient(); + await testEnvironment.ConnectClient(); + await testEnvironment.ConnectClient(); + await testEnvironment.ConnectClient(); - SpinWait.SpinUntil(() => receivedMessagesCount == 500, TimeSpan.FromSeconds(20)); + await Task.Delay(500); - Assert.AreEqual(500, receivedMessagesCount); + Assert.AreEqual("/test/1", receivedTopic); } } [TestMethod] - public async Task Subscribe_Lots_In_Multiple_Requests() + public async Task Publish_At_Least_Once_0x01() { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - var receivedMessagesCount = 0; - - await testEnvironment.StartServer(); - - var c1 = await testEnvironment.ConnectClient(); - c1.UseApplicationMessageReceivedHandler(c => Interlocked.Increment(ref receivedMessagesCount)); - - for (var i = 0; i < 500; i++) - { - var so = new MqttClientSubscribeOptionsBuilder() - .WithTopicFilter(i.ToString()).Build(); - - await c1.SubscribeAsync(so).ConfigureAwait(false); - - await Task.Delay(10); - } - - var c2 = await testEnvironment.ConnectClient(); - - var messageBuilder = new MqttApplicationMessageBuilder(); - for (var i = 0; i < 500; i++) - { - messageBuilder.WithTopic(i.ToString()); - - await c2.PublishAsync(messageBuilder.Build()).ConfigureAwait(false); - - await Task.Delay(10); - } - - SpinWait.SpinUntil(() => receivedMessagesCount == 500, 5000); - - Assert.AreEqual(500, receivedMessagesCount); - } + await TestPublishAsync("A/B/C", MqttQualityOfServiceLevel.AtLeastOnce, "A/B/C", MqttQualityOfServiceLevel.AtLeastOnce, 1); } [TestMethod] - public async Task Subscribe_Multiple_In_Multiple_Request() + public async Task Publish_At_Most_Once_0x00() { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - var receivedMessagesCount = 0; - - await testEnvironment.StartServer(); - - var c1 = await testEnvironment.ConnectClient(); - c1.UseApplicationMessageReceivedHandler(c => Interlocked.Increment(ref receivedMessagesCount)); - await c1.SubscribeAsync(new MqttClientSubscribeOptionsBuilder() - .WithTopicFilter("a") - .Build()); - - await c1.SubscribeAsync(new MqttClientSubscribeOptionsBuilder() - .WithTopicFilter("b") - .Build()); - - await c1.SubscribeAsync(new MqttClientSubscribeOptionsBuilder() - .WithTopicFilter("c") - .Build()); - - var c2 = await testEnvironment.ConnectClient(); - - await c2.PublishAsync("a"); - await Task.Delay(100); - Assert.AreEqual(receivedMessagesCount, 1); - - await c2.PublishAsync("b"); - await Task.Delay(100); - Assert.AreEqual(receivedMessagesCount, 2); + await TestPublishAsync("A/B/C", MqttQualityOfServiceLevel.AtMostOnce, "A/B/C", MqttQualityOfServiceLevel.AtMostOnce, 1); + } - await c2.PublishAsync("c"); - await Task.Delay(100); - Assert.AreEqual(receivedMessagesCount, 3); - } + [TestMethod] + public async Task Publish_Exactly_Once_0x02() + { + await TestPublishAsync("A/B/C", MqttQualityOfServiceLevel.ExactlyOnce, "A/B/C", MqttQualityOfServiceLevel.ExactlyOnce, 1); } [TestMethod] public async Task Publish_From_Server() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { var server = await testEnvironment.StartServer(); var receivedMessagesCount = 0; var client = await testEnvironment.ConnectClient(); - client.UseApplicationMessageReceivedHandler(c => Interlocked.Increment(ref receivedMessagesCount)); + client.ApplicationMessageReceivedAsync += e => + { + Interlocked.Increment(ref receivedMessagesCount); + return PlatformAbstractionLayer.CompletedTask; + }; - var message = new MqttApplicationMessageBuilder().WithTopic("a").WithAtLeastOnceQoS().Build(); + var message = new MqttApplicationMessageBuilder().WithTopic("a").WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce).Build(); await client.SubscribeAsync(new MqttTopicFilter { Topic = "a", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }); - await server.PublishAsync(message); + await server.InjectApplicationMessage( + new InjectedMqttApplicationMessage(message) + { + SenderClientId = "server" + }); await Task.Delay(1000); @@ -472,7 +510,7 @@ namespace MQTTnet.Tests.Server { var receivedMessagesCount = 0; - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { await testEnvironment.StartServer(); @@ -480,14 +518,22 @@ namespace MQTTnet.Tests.Server var c2 = await testEnvironment.ConnectClient(); var c3 = await testEnvironment.ConnectClient(); - c2.UseApplicationMessageReceivedHandler(c => { Interlocked.Increment(ref receivedMessagesCount); }); + c2.ApplicationMessageReceivedAsync += e => + { + Interlocked.Increment(ref receivedMessagesCount); + return PlatformAbstractionLayer.CompletedTask; + }; - c3.UseApplicationMessageReceivedHandler(c => { Interlocked.Increment(ref receivedMessagesCount); }); + c3.ApplicationMessageReceivedAsync += e => + { + Interlocked.Increment(ref receivedMessagesCount); + return PlatformAbstractionLayer.CompletedTask; + }; await c2.SubscribeAsync(new MqttTopicFilter { Topic = "a", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }).ConfigureAwait(false); await c3.SubscribeAsync(new MqttTopicFilter { Topic = "a", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }).ConfigureAwait(false); - var message = new MqttApplicationMessageBuilder().WithTopic("a").WithAtLeastOnceQoS().Build(); + var message = new MqttApplicationMessageBuilder().WithTopic("a").WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce).Build(); for (var i = 0; i < 500; i++) { @@ -501,460 +547,183 @@ namespace MQTTnet.Tests.Server } [TestMethod] - public async Task Session_Takeover() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer(); - - var options = new MqttClientOptionsBuilder() - .WithCleanSession(false) - .WithProtocolVersion(MqttProtocolVersion.V500) // Disconnect reason is only available in MQTT 5+ - .WithClientId("a"); - - var client1 = await testEnvironment.ConnectClient(options); - await Task.Delay(500); - - MqttClientDisconnectReason disconnectReason = MqttClientDisconnectReason.NormalDisconnection; - client1.DisconnectedHandler = new MqttClientDisconnectedHandlerDelegate(c => { disconnectReason = c.Reason; }); - - var client2 = await testEnvironment.ConnectClient(options); - await Task.Delay(500); - - Assert.IsFalse(client1.IsConnected); - Assert.IsTrue(client2.IsConnected); - - Assert.AreEqual(MqttClientDisconnectReason.SessionTakenOver, disconnectReason); - } - } - - [TestMethod] - public async Task No_Messages_If_No_Subscription() + public async Task Remove_Session() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { - await testEnvironment.StartServer(); - - var client = await testEnvironment.ConnectClient(); - var receivedMessages = new List(); - - client.ConnectedHandler = new MqttClientConnectedHandlerDelegate(async e => { await client.PublishAsync("Connected"); }); - - client.UseApplicationMessageReceivedHandler(c => - { - lock (receivedMessages) - { - receivedMessages.Add(c.ApplicationMessage); - } - }); + var server = await testEnvironment.StartServer(new MqttServerOptionsBuilder()); + var clientOptions = new MqttClientOptionsBuilder(); + var c1 = await testEnvironment.ConnectClient(clientOptions); await Task.Delay(500); + Assert.AreEqual(1, (await server.GetClientsAsync()).Count); - await client.PublishAsync("Hello"); - + await c1.DisconnectAsync(); await Task.Delay(500); - Assert.AreEqual(0, receivedMessages.Count); + Assert.AreEqual(0, (await server.GetClientsAsync()).Count); } } [TestMethod] - public async Task Set_Subscription_At_Server() + public async Task Same_Client_Id_Connect_Disconnect_Event_Order() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { var server = await testEnvironment.StartServer(); - server.ClientConnectedHandler = new MqttServerClientConnectedHandlerDelegate(async e => - { - // Every client will automatically subscribe to this topic. - await server.SubscribeAsync(e.ClientId, "topic1"); - }); - - var client = await testEnvironment.ConnectClient(); - var receivedMessages = new List(); + var events = new List(); - client.UseApplicationMessageReceivedHandler(c => + server.ClientConnectedAsync += e => { - lock (receivedMessages) + lock (events) { - receivedMessages.Add(c.ApplicationMessage); + events.Add("c"); } - }); - - await Task.Delay(500); - - await client.PublishAsync("Hello"); - await Task.Delay(100); - Assert.AreEqual(0, receivedMessages.Count); - - await client.PublishAsync("topic1"); - await Task.Delay(100); - Assert.AreEqual(1, receivedMessages.Count); - } - } - - [TestMethod] - public async Task Shutdown_Disconnects_Clients_Gracefully() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - var server = await testEnvironment.StartServer(new MqttServerOptionsBuilder()); - - var disconnectCalled = 0; - - var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder()); - c1.DisconnectedHandler = new MqttClientDisconnectedHandlerDelegate(e => disconnectCalled++); - await Task.Delay(100); - - await server.StopAsync(); - - await Task.Delay(100); - - Assert.AreEqual(1, disconnectCalled); - } - } - - [TestMethod] - public async Task Handle_Clean_Disconnect() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - var server = await testEnvironment.StartServer(new MqttServerOptionsBuilder()); - - var clientConnectedCalled = 0; - var clientDisconnectedCalled = 0; - - server.ClientConnectedHandler = new MqttServerClientConnectedHandlerDelegate(_ => Interlocked.Increment(ref clientConnectedCalled)); - server.ClientDisconnectedHandler = new MqttServerClientDisconnectedHandlerDelegate(_ => Interlocked.Increment(ref clientDisconnectedCalled)); - - var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder()); - - await Task.Delay(1000); - - Assert.AreEqual(1, clientConnectedCalled); - Assert.AreEqual(0, clientDisconnectedCalled); + return PlatformAbstractionLayer.CompletedTask; + }; - await Task.Delay(1000); - - await c1.DisconnectAsync(); - - await Task.Delay(1000); - - Assert.AreEqual(1, clientConnectedCalled); - Assert.AreEqual(1, clientDisconnectedCalled); - } - } - - [TestMethod] - public async Task Client_Disconnect_Without_Errors() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - bool clientWasConnected; - - var server = await testEnvironment.StartServer(new MqttServerOptionsBuilder()); - try + server.ClientDisconnectedAsync += e => { - var client = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder()); - - clientWasConnected = true; - - await client.DisconnectAsync(); - - await Task.Delay(500); - } - finally - { - await server.StopAsync(); - } - - Assert.IsTrue(clientWasConnected); - - testEnvironment.ThrowIfLogErrors(); - } - } - - [TestMethod] - public async Task Handle_Lots_Of_Parallel_Retained_Messages() - { - const int ClientCount = 50; - - using (var testEnvironment = new TestEnvironment(TestContext)) - { - var server = await testEnvironment.StartServer(); - - var tasks = new List(); - for (var i = 0; i < ClientCount; i++) - { - var i2 = i; - var testEnvironment2 = testEnvironment; - - tasks.Add(Task.Run(async () => + lock (events) { - try - { - using (var client = await testEnvironment2.ConnectClient()) - { - // Clear retained message. - await client.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("r" + i2) - .WithPayload(new byte[0]).WithRetainFlag().WithQualityOfServiceLevel(1).Build()); - - // Set retained message. - await client.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("r" + i2) - .WithPayload("value").WithRetainFlag().WithQualityOfServiceLevel(1).Build()); - - await client.DisconnectAsync(); - } - } - catch (Exception exception) - { - testEnvironment2.TrackException(exception); - } - })); - - await Task.Delay(10); - } - - await Task.WhenAll(tasks); - - await Task.Delay(1000); - - var retainedMessages = await server.GetRetainedApplicationMessagesAsync(); - - Assert.AreEqual(ClientCount, retainedMessages.Count); - - for (var i = 0; i < ClientCount; i++) - { - Assert.IsTrue(retainedMessages.Any(m => m.Topic == "r" + i)); - } - } - } - - [TestMethod] - public async Task Intercept_Application_Message() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer( - new MqttServerOptionsBuilder().WithApplicationMessageInterceptor( - c => { c.ApplicationMessage = new MqttApplicationMessage { Topic = "new_topic" }; })); - - string receivedTopic = null; - var c1 = await testEnvironment.ConnectClient(); - await c1.SubscribeAsync("#"); - c1.UseApplicationMessageReceivedHandler(a => { receivedTopic = a.ApplicationMessage.Topic; }); - - await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("original_topic").Build()); - - await Task.Delay(500); - Assert.AreEqual("new_topic", receivedTopic); - } - } - - [TestMethod] - public async Task Persist_Retained_Message() - { - var serverStorage = new TestServerStorage(); - - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer(new MqttServerOptionsBuilder().WithStorage(serverStorage)); - - var c1 = await testEnvironment.ConnectClient(); - - await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("retained").WithPayload(new byte[3]).WithRetainFlag().Build()); - - await Task.Delay(500); - - Assert.AreEqual(1, serverStorage.Messages.Count); - } - } - - [TestMethod] - public async Task Publish_After_Client_Connects() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - var server = await testEnvironment.StartServer(); - server.UseClientConnectedHandler(async e => { await server.PublishAsync("/test/1", "true", MqttQualityOfServiceLevel.ExactlyOnce, false); }); + events.Add("d"); + } - string receivedTopic = null; + return PlatformAbstractionLayer.CompletedTask; + }; - var c1 = await testEnvironment.ConnectClient(); - c1.UseApplicationMessageReceivedHandler(e => { receivedTopic = e.ApplicationMessage.Topic; }); - await c1.SubscribeAsync("#"); + var clientOptionsBuilder = new MqttClientOptionsBuilder().WithClientId(Guid.NewGuid().ToString()); - await testEnvironment.ConnectClient(); - await testEnvironment.ConnectClient(); - await testEnvironment.ConnectClient(); - await testEnvironment.ConnectClient(); + // c + var c1 = await testEnvironment.ConnectClient(clientOptionsBuilder); await Task.Delay(500); - Assert.AreEqual("/test/1", receivedTopic); - } - } - - [TestMethod] - public async Task Intercept_Message() - { - void Interceptor(MqttApplicationMessageInterceptorContext context) - { - context.ApplicationMessage.Payload = Encoding.ASCII.GetBytes("extended"); - } - - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer(new MqttServerOptionsBuilder().WithApplicationMessageInterceptor(Interceptor)); - - var c1 = await testEnvironment.ConnectClient(); - var c2 = await testEnvironment.ConnectClient(); - await c2.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("test").Build()); - - var isIntercepted = false; - c2.UseApplicationMessageReceivedHandler(c => { isIntercepted = string.Compare("extended", Encoding.UTF8.GetString(c.ApplicationMessage.Payload), StringComparison.Ordinal) == 0; }); + var flow = string.Join(string.Empty, events); + Assert.AreEqual("c", flow); - await c1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("test").Build()); - await c1.DisconnectAsync(); + // dc + // Connect client with same client ID. Should disconnect existing client. + var c2 = await testEnvironment.ConnectClient(clientOptionsBuilder); await Task.Delay(500); - Assert.IsTrue(isIntercepted); - } - } - - [TestMethod] - public async Task Send_Long_Body() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - const int PayloadSizeInMB = 30; - const int CharCount = PayloadSizeInMB * 1024 * 1024; + flow = string.Join(string.Empty, events); - var longBody = new byte[CharCount]; - byte @char = 32; + Assert.AreEqual("cdc", flow); - for (long i = 0; i < PayloadSizeInMB * 1024L * 1024L; i++) + c2.ApplicationMessageReceivedAsync += e => { - longBody[i] = @char; - - @char++; - - if (@char > 126) + lock (events) { - @char = 32; + events.Add("r"); } - } - - byte[] receivedBody = null; - await testEnvironment.StartServer(); + return PlatformAbstractionLayer.CompletedTask; + }; - var client1 = await testEnvironment.ConnectClient(); - client1.UseApplicationMessageReceivedHandler(c => { receivedBody = c.ApplicationMessage.Payload; }); + await c2.SubscribeAsync("topic"); - await client1.SubscribeAsync("string"); + // r + await c2.PublishStringAsync("topic"); - var client2 = await testEnvironment.ConnectClient(); - await client2.PublishAsync("string", longBody); + await Task.Delay(500); - await Task.Delay(TimeSpan.FromSeconds(5)); + flow = string.Join(string.Empty, events); + Assert.AreEqual("cdcr", flow); - Assert.IsTrue(longBody.SequenceEqual(receivedBody ?? new byte[0])); - } - } + // nothing - [TestMethod] - public async Task Deny_Connection() - { - var serverOptions = new MqttServerOptionsBuilder().WithConnectionValidator(context => { context.ReasonCode = MqttConnectReasonCode.NotAuthorized; }); + Assert.AreEqual(false, c1.IsConnected); + await c1.DisconnectAsync(); + Assert.AreEqual(false, c1.IsConnected); - using (var testEnvironment = new TestEnvironment(TestContext)) - { - testEnvironment.IgnoreClientLogErrors = true; + await Task.Delay(500); - await testEnvironment.StartServer(serverOptions); + // d + Assert.AreEqual(true, c2.IsConnected); + await c2.DisconnectAsync(); - var connectingFailedException = await Assert.ThrowsExceptionAsync(() => testEnvironment.ConnectClient()); - Assert.AreEqual(MqttClientConnectResultCode.NotAuthorized, connectingFailedException.ResultCode); - } - } + await Task.Delay(500); - Dictionary _connected; + await server.StopAsync(); - private void ConnectionValidationHandler(MqttConnectionValidatorContext eventArgs) - { - if (_connected.ContainsKey(eventArgs.ClientId)) - { - eventArgs.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword; - return; + flow = string.Join(string.Empty, events); + Assert.AreEqual("cdcrd", flow); } - - _connected[eventArgs.ClientId] = true; - eventArgs.ReasonCode = MqttConnectReasonCode.Success; - return; } [TestMethod] public async Task Same_Client_Id_Refuse_Connection() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { testEnvironment.IgnoreClientLogErrors = true; _connected = new Dictionary(); - var options = new MqttServerOptionsBuilder(); - options.WithConnectionValidator(e => ConnectionValidationHandler(e)); - var server = await testEnvironment.StartServer(options); + + var server = await testEnvironment.StartServer(); + + server.ValidatingConnectionAsync += e => + { + ConnectionValidationHandler(e); + return PlatformAbstractionLayer.CompletedTask; + }; var events = new List(); - server.ClientConnectedHandler = new MqttServerClientConnectedHandlerDelegate(_ => + server.ClientConnectedAsync += e => { lock (events) { events.Add("c"); } - }); - server.ClientDisconnectedHandler = new MqttServerClientDisconnectedHandlerDelegate(_ => + return PlatformAbstractionLayer.CompletedTask; + }; + + server.ClientDisconnectedAsync += e => { lock (events) { events.Add("d"); } - }); - var clientOptions = new MqttClientOptionsBuilder() - .WithClientId("same_id"); + return PlatformAbstractionLayer.CompletedTask; + }; + + var clientOptions = new MqttClientOptionsBuilder().WithClientId("same_id"); // c var c1 = await testEnvironment.ConnectClient(clientOptions); - c1.UseDisconnectedHandler(_ => + c1.DisconnectedAsync += _ => { lock (events) { events.Add("x"); } - }); + return PlatformAbstractionLayer.CompletedTask; + }; - c1.UseApplicationMessageReceivedHandler(_ => + c1.ApplicationMessageReceivedAsync += e => { lock (events) { events.Add("r"); } - }); + + return PlatformAbstractionLayer.CompletedTask; + }; c1.SubscribeAsync("topic").Wait(); await Task.Delay(500); - c1.PublishAsync("topic").Wait(); + c1.PublishStringAsync("topic").Wait(); await Task.Delay(500); @@ -971,128 +740,164 @@ namespace MQTTnet.Tests.Server //same id connection is expected to fail } - await Task.Delay(500); + await Task.Delay(500); + + flow = string.Join(string.Empty, events); + Assert.AreEqual("cr", flow); + + c1.PublishStringAsync("topic").Wait(); + + await Task.Delay(500); + + flow = string.Join(string.Empty, events); + Assert.AreEqual("crr", flow); + } + } + + [TestMethod] + public async Task Send_Long_Body() + { + using (var testEnvironment = CreateTestEnvironment()) + { + const int PayloadSizeInMB = 30; + const int CharCount = PayloadSizeInMB * 1024 * 1024; + + var longBody = new byte[CharCount]; + byte @char = 32; + + for (long i = 0; i < PayloadSizeInMB * 1024L * 1024L; i++) + { + longBody[i] = @char; + + @char++; + + if (@char > 126) + { + @char = 32; + } + } + + byte[] receivedBody = null; - flow = string.Join(string.Empty, events); - Assert.AreEqual("cr", flow); + await testEnvironment.StartServer(); - c1.PublishAsync("topic").Wait(); + var client1 = await testEnvironment.ConnectClient(); + client1.ApplicationMessageReceivedAsync += e => + { + receivedBody = e.ApplicationMessage.Payload; + return PlatformAbstractionLayer.CompletedTask; + }; - await Task.Delay(500); + await client1.SubscribeAsync("string"); - flow = string.Join(string.Empty, events); - Assert.AreEqual("crr", flow); + var client2 = await testEnvironment.ConnectClient(); + await client2.PublishBinaryAsync("string", longBody); + + await Task.Delay(TimeSpan.FromSeconds(5)); + + Assert.IsTrue(longBody.SequenceEqual(receivedBody ?? new byte[0])); } } [TestMethod] - public async Task Same_Client_Id_Connect_Disconnect_Event_Order() + public async Task Session_Takeover() { - using (var testEnvironment = new TestEnvironment()) + using (var testEnvironment = CreateTestEnvironment()) { - var server = await testEnvironment.StartServer(new MqttServerOptionsBuilder()); + await testEnvironment.StartServer(); - var events = new List(); + var options = new MqttClientOptionsBuilder().WithCleanSession(false) + .WithProtocolVersion(MqttProtocolVersion.V500) // Disconnect reason is only available in MQTT 5+ + .WithClientId("a"); - server.ClientConnectedHandler = new MqttServerClientConnectedHandlerDelegate(_ => - { - lock (events) - { - events.Add("c"); - } - }); + var client1 = await testEnvironment.ConnectClient(options); + await Task.Delay(500); - server.ClientDisconnectedHandler = new MqttServerClientDisconnectedHandlerDelegate(_ => + var disconnectReason = MqttClientDisconnectReason.NormalDisconnection; + client1.DisconnectedAsync += c => { - lock (events) - { - events.Add("d"); - } - }); - - var clientOptionsBuilder = new MqttClientOptionsBuilder() - .WithClientId(Guid.NewGuid().ToString()); - - // c - var c1 = await testEnvironment.ConnectClient(clientOptionsBuilder); + disconnectReason = c.Reason; + return PlatformAbstractionLayer.CompletedTask; + }; + var client2 = await testEnvironment.ConnectClient(options); await Task.Delay(500); - var flow = string.Join(string.Empty, events); - Assert.AreEqual("c", flow); + Assert.IsFalse(client1.IsConnected); + Assert.IsTrue(client2.IsConnected); - // dc - // Connect client with same client ID. Should disconnect existing client. - var c2 = await testEnvironment.ConnectClient(clientOptionsBuilder); + Assert.AreEqual(MqttClientDisconnectReason.SessionTakenOver, disconnectReason); + } + } - await Task.Delay(500); + [TestMethod] + public async Task Set_Subscription_At_Server() + { + using (var testEnvironment = CreateTestEnvironment()) + { + var server = await testEnvironment.StartServer(); - flow = string.Join(string.Empty, events); + server.ClientConnectedAsync += async e => + { + // Every client will automatically subscribe to this topic. + await server.SubscribeAsync(e.ClientId, "topic1"); + }; - Assert.AreEqual("cdc", flow); + var client = await testEnvironment.ConnectClient(); + var receivedMessages = new List(); - c2.UseApplicationMessageReceivedHandler(_ => + client.ApplicationMessageReceivedAsync += e => { - lock (events) + lock (receivedMessages) { - events.Add("r"); + receivedMessages.Add(e.ApplicationMessage); } - }); - - await c2.SubscribeAsync("topic"); - - // r - await c2.PublishAsync("topic"); - - await Task.Delay(500); - - flow = string.Join(string.Empty, events); - Assert.AreEqual("cdcr", flow); - - // nothing - - Assert.AreEqual(false, c1.IsConnected); - await c1.DisconnectAsync(); - Assert.AreEqual(false, c1.IsConnected); - - await Task.Delay(500); - // d - Assert.AreEqual(true, c2.IsConnected); - await c2.DisconnectAsync(); + return PlatformAbstractionLayer.CompletedTask; + }; await Task.Delay(500); - await server.StopAsync(); + await client.PublishStringAsync("Hello"); + await Task.Delay(100); + Assert.AreEqual(0, receivedMessages.Count); - flow = string.Join(string.Empty, events); - Assert.AreEqual("cdcrd", flow); + await client.PublishStringAsync("topic1"); + await Task.Delay(100); + Assert.AreEqual(1, receivedMessages.Count); } } [TestMethod] - public async Task Remove_Session() + public async Task Shutdown_Disconnects_Clients_Gracefully() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { var server = await testEnvironment.StartServer(new MqttServerOptionsBuilder()); - var clientOptions = new MqttClientOptionsBuilder(); - var c1 = await testEnvironment.ConnectClient(clientOptions); - await Task.Delay(500); - Assert.AreEqual(1, (await server.GetClientStatusAsync()).Count); + var disconnectCalled = 0; - await c1.DisconnectAsync(); - await Task.Delay(500); + var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder()); + c1.DisconnectedAsync += e => + { + disconnectCalled++; + return PlatformAbstractionLayer.CompletedTask; + }; + + await Task.Delay(100); + + await server.StopAsync(); + + await Task.Delay(100); - Assert.AreEqual(0, (await server.GetClientStatusAsync()).Count); + Assert.AreEqual(1, disconnectCalled); } } [TestMethod] public async Task Stop_And_Restart() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { testEnvironment.IgnoreClientLogErrors = true; @@ -1106,65 +911,48 @@ namespace MQTTnet.Tests.Server await testEnvironment.ConnectClient(); Assert.Fail("Connecting should fail."); } - catch (Exception) + catch { + // Ignore errors. } - await server.StartAsync(new MqttServerOptionsBuilder().WithDefaultEndpointPort(testEnvironment.ServerPort).Build()); + await server.StartAsync(); + + // The client should be able to connect again. await testEnvironment.ConnectClient(); } } [TestMethod] - public async Task Do_Not_Send_Retained_Messages_For_Denied_Subscription() + [DataRow("", null)] + [DataRow("", "")] + [DataRow(null, null)] + public async Task Use_Admissible_Credentials(string username, string password) { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { - await testEnvironment.StartServer(new MqttServerOptionsBuilder().WithSubscriptionInterceptor(c => - { - // This should lead to no subscriptions for "n" at all. So also no sending of retained messages. - if (c.TopicFilter.Topic == "n") - { - c.AcceptSubscription = false; - } - })); - - // Prepare some retained messages. - var client1 = await testEnvironment.ConnectClient(); - await client1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("y").WithPayload("x").WithRetainFlag().Build()); - await client1.PublishAsync(new MqttApplicationMessageBuilder().WithTopic("n").WithPayload("x").WithRetainFlag().Build()); - await client1.DisconnectAsync(); - - await Task.Delay(500); - - // Subscribe to all retained message types. - // It is important to do this in a range of filters to ensure that a subscription is not "hidden". - var client2 = await testEnvironment.ConnectClient(); - - var buffer = new StringBuilder(); + await testEnvironment.StartServer(); - client2.UseApplicationMessageReceivedHandler(c => - { - lock (buffer) - { - buffer.Append(c.ApplicationMessage.Topic); - } - }); + var client = testEnvironment.CreateClient(); - await client2.SubscribeAsync(new MqttTopicFilter { Topic = "y" }, new MqttTopicFilter { Topic = "n" }); + var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort).WithCredentials(username, password).Build(); - await Task.Delay(500); + var connectResult = await client.ConnectAsync(clientOptions); - Assert.AreEqual("y", buffer.ToString()); + Assert.IsFalse(connectResult.IsSessionPresent); + Assert.IsTrue(client.IsConnected); } } [TestMethod] - public async Task Collect_Messages_In_Disconnected_Session() + public async Task Use_Clean_Session() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { - var server = await testEnvironment.StartServer(new MqttServerOptionsBuilder().WithPersistentSessions()); + await testEnvironment.StartServer(); + + var client = testEnvironment.CreateClient(); + var connectResult = await client.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort).WithCleanSession().Build()); // Create the session including the subscription. var client1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("a").WithCleanSession(false)); @@ -1172,138 +960,162 @@ namespace MQTTnet.Tests.Server await client1.DisconnectAsync(); await Task.Delay(500); - var clientStatus = await server.GetClientStatusAsync(); - Assert.AreEqual(0, clientStatus.Count); + Assert.IsFalse(connectResult.IsSessionPresent); + } + } + [TestMethod] + public async Task Use_Empty_Client_ID() + { + using (var testEnvironment = CreateTestEnvironment()) + { + await testEnvironment.StartServer(); var client2 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("b").WithCleanSession(false)); - await client2.PublishAsync("x", "1"); - await client2.PublishAsync("x", "2"); - await client2.PublishAsync("x", "3"); + await client2.PublishStringAsync("x", "1"); + await client2.PublishStringAsync("x", "2"); + await client2.PublishStringAsync("x", "3"); await client2.DisconnectAsync(); - await Task.Delay(500); + var client = testEnvironment.CreateClient(); - clientStatus = await server.GetClientStatusAsync(); - var sessionStatus = await server.GetSessionStatusAsync(); + var clientOptions = new MqttClientOptionsBuilder().WithTcpServer("localhost", testEnvironment.ServerPort).WithClientId(string.Empty).Build(); - Assert.AreEqual(0, clientStatus.Count); - Assert.AreEqual(2, sessionStatus.Count); + var connectResult = await client.ConnectAsync(clientOptions); - Assert.AreEqual(3, sessionStatus.First(s => s.ClientId == client1.Options.ClientId).PendingApplicationMessagesCount); + Assert.IsFalse(connectResult.IsSessionPresent); + Assert.IsTrue(client.IsConnected); } } - static async Task TestPublishAsync( - string topic, - MqttQualityOfServiceLevel qualityOfServiceLevel, - string topicFilter, - MqttQualityOfServiceLevel filterQualityOfServiceLevel, - int expectedReceivedMessagesCount, - TestContext testContext) + [TestMethod] + public async Task Will_Message_Do_Not_Send_On_Clean_Disconnect() { - using (var testEnvironment = new TestEnvironment(testContext)) + using (var testEnvironment = CreateTestEnvironment()) { var receivedMessagesCount = 0; await testEnvironment.StartServer(); - var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("receiver")); - c1.UseApplicationMessageReceivedHandler(c => Interlocked.Increment(ref receivedMessagesCount)); - await c1.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic(topicFilter).WithQualityOfServiceLevel(filterQualityOfServiceLevel).Build()); + var clientOptions = new MqttClientOptionsBuilder().WithWillTopic("My/last/will"); - var c2 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("sender")); - await c2.PublishAsync(new MqttApplicationMessageBuilder().WithTopic(topic).WithPayload(new byte[0]).WithQualityOfServiceLevel(qualityOfServiceLevel).Build()); + var c1 = await testEnvironment.ConnectClient(); + + c1.ApplicationMessageReceivedAsync += e => + { + Interlocked.Increment(ref receivedMessagesCount); + return PlatformAbstractionLayer.CompletedTask; + }; + + await c1.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("#").Build()); + + var c2 = await testEnvironment.ConnectClient(clientOptions); await c2.DisconnectAsync().ConfigureAwait(false); - await Task.Delay(500); - await c1.UnsubscribeAsync(topicFilter); - await Task.Delay(500); + await Task.Delay(1000); - Assert.AreEqual(expectedReceivedMessagesCount, receivedMessagesCount); + Assert.AreEqual(0, receivedMessagesCount); } } [TestMethod] - public async Task Intercept_Undelivered() + public async Task Will_Message_Do_Not_Send_On_Takeover() { - using (var testEnvironment = new TestEnvironment()) + using (var testEnvironment = CreateTestEnvironment()) { - var undeliverd = string.Empty; + var receivedMessagesCount = 0; + + await testEnvironment.StartServer(); - var options = new MqttServerOptionsBuilder().WithUndeliveredMessageInterceptor( - context => { undeliverd = context.ApplicationMessage.Topic; }); + // C1 will receive the last will! + var c1 = await testEnvironment.ConnectClient(); + c1.ApplicationMessageReceivedAsync += e => + { + Interlocked.Increment(ref receivedMessagesCount); + return PlatformAbstractionLayer.CompletedTask; + }; - await testEnvironment.StartServer(options); + await c1.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("#").Build()); - var client = await testEnvironment.ConnectClient(); + // C2 has the last will defined. + var clientOptions = new MqttClientOptionsBuilder().WithWillTopic("My/last/will").WithClientId("WillOwner"); - await client.SubscribeAsync("b"); + var c2 = await testEnvironment.ConnectClient(clientOptions); - await client.PublishAsync("a", null, MqttQualityOfServiceLevel.ExactlyOnce); + // C3 will do the connection takeover. + var c3 = await testEnvironment.ConnectClient(clientOptions); - await Task.Delay(500); + await Task.Delay(1000); - Assert.AreEqual("a", undeliverd); + Assert.AreEqual(0, receivedMessagesCount); } } [TestMethod] - public async Task Intercept_ClientMessageQueue() + public async Task Will_Message_Send() { - using (var testEnvironment = new TestEnvironment(TestContext)) + using (var testEnvironment = CreateTestEnvironment()) { - await testEnvironment.StartServer(new MqttServerOptionsBuilder() - .WithClientMessageQueueInterceptor(c => c.ApplicationMessage.Topic = "a")); + await testEnvironment.StartServer(); - var topicAReceived = false; - var topicBReceived = false; + var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder()); - var client = await testEnvironment.ConnectClient(); - client.UseApplicationMessageReceivedHandler(c => + var receivedMessagesCount = 0; + c1.ApplicationMessageReceivedAsync += e => { - if (c.ApplicationMessage.Topic == "a") - { - topicAReceived = true; - } - else if (c.ApplicationMessage.Topic == "b") - { - topicBReceived = true; - } - }); + Interlocked.Increment(ref receivedMessagesCount); + return PlatformAbstractionLayer.CompletedTask; + }; - await client.SubscribeAsync("a"); - await client.SubscribeAsync("b"); + await c1.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("#").Build()); - await client.PublishAsync("b"); + var clientOptions = new MqttClientOptionsBuilder().WithWillTopic("My/last/will").WithWillQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce); + var c2 = await testEnvironment.ConnectClient(clientOptions); + c2.Dispose(); // Dispose will not send a DISCONNECT pattern first so the will message must be sent. - await Task.Delay(500); + await Task.Delay(1000); - Assert.IsTrue(topicAReceived); - Assert.IsFalse(topicBReceived); + Assert.AreEqual(1, receivedMessagesCount); } } - [TestMethod] - public async Task Intercept_ClientMessageQueue_Different_QoS_Of_Subscription_And_Message() + void ConnectionValidationHandler(ValidatingConnectionEventArgs eventArgs) { - const string topic = "a"; + if (_connected.ContainsKey(eventArgs.ClientId)) + { + eventArgs.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword; + return; + } + + _connected[eventArgs.ClientId] = true; + eventArgs.ReasonCode = MqttConnectReasonCode.Success; + } - using (var testEnvironment = new TestEnvironment(TestContext)) + async Task TestPublishAsync( + string topic, + MqttQualityOfServiceLevel qualityOfServiceLevel, + string topicFilter, + MqttQualityOfServiceLevel filterQualityOfServiceLevel, + int expectedReceivedMessagesCount) + { + using (var testEnvironment = CreateTestEnvironment()) { - await testEnvironment.StartServer(new MqttServerOptionsBuilder() - .WithClientMessageQueueInterceptor(c => { })); // Interceptor does nothing but has to be present. + await testEnvironment.StartServer(); - bool receivedMessage = false; - var client = await testEnvironment.ConnectClient(); - client.UseApplicationMessageReceivedHandler(c => { receivedMessage = true; }); + var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("receiver")); + var c1MessageHandler = testEnvironment.CreateApplicationMessageHandler(c1); + await c1.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic(topicFilter).WithQualityOfServiceLevel(filterQualityOfServiceLevel).Build()); - await client.SubscribeAsync(topic, MqttQualityOfServiceLevel.AtLeastOnce); + var c2 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("sender")); + await c2.PublishAsync(new MqttApplicationMessageBuilder().WithTopic(topic).WithPayload(new byte[0]).WithQualityOfServiceLevel(qualityOfServiceLevel).Build()); + await Task.Delay(500); - await client.PublishAsync(new MqttApplicationMessage { Topic = topic, QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }); + await c2.DisconnectAsync().ConfigureAwait(false); + await Task.Delay(500); + await c1.UnsubscribeAsync(topicFilter); await Task.Delay(500); - Assert.IsTrue(receivedMessage); + Assert.AreEqual(expectedReceivedMessagesCount, c1MessageHandler.ReceivedEventArgs.Count); } } } diff --git a/Source/MQTTnet.Tests/Server/Injection_Tests.cs b/Source/MQTTnet.Tests/Server/Injection_Tests.cs new file mode 100644 index 0000000..7ce3687 --- /dev/null +++ b/Source/MQTTnet.Tests/Server/Injection_Tests.cs @@ -0,0 +1,64 @@ +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Client; +using MQTTnet.Server; + +namespace MQTTnet.Tests.Server +{ + [TestClass] + public sealed class Injection_Tests : BaseTestClass + { + [TestMethod] + public async Task Inject_Application_Message_At_Session_Level() + { + using (var testEnvironment = CreateTestEnvironment()) + { + var server = await testEnvironment.StartServer(); + var receiver1 = await testEnvironment.ConnectClient(); + var receiver2 = await testEnvironment.ConnectClient(); + var messageReceivedHandler1 = testEnvironment.CreateApplicationMessageHandler(receiver1); + var messageReceivedHandler2 = testEnvironment.CreateApplicationMessageHandler(receiver2); + + var status = await server.GetSessionsAsync(); + var clientStatus = status[0]; + + await receiver1.SubscribeAsync("#"); + await receiver2.SubscribeAsync("#"); + + await clientStatus.EnqueueApplicationMessageAsync(new MqttApplicationMessageBuilder().WithTopic("InjectedOne").Build()); + + await LongTestDelay(); + + Assert.AreEqual(1, messageReceivedHandler1.ReceivedEventArgs.Count); + Assert.AreEqual("InjectedOne", messageReceivedHandler1.ReceivedEventArgs[0].ApplicationMessage.Topic); + + // The second receiver should NOT receive the message. + Assert.AreEqual(0, messageReceivedHandler2.ReceivedEventArgs.Count); + } + } + + [TestMethod] + public async Task Inject_ApplicationMessage_At_Server_Level() + { + using (var testEnvironment = CreateTestEnvironment()) + { + var server = await testEnvironment.StartServer(); + + var receiver = await testEnvironment.ConnectClient(); + + var messageReceivedHandler = testEnvironment.CreateApplicationMessageHandler(receiver); + + await receiver.SubscribeAsync("#"); + + var injectedApplicationMessage = new MqttApplicationMessageBuilder().WithTopic("InjectedOne").Build(); + + await server.InjectApplicationMessage(new InjectedMqttApplicationMessage(injectedApplicationMessage)); + + await LongTestDelay(); + + Assert.AreEqual(1, messageReceivedHandler.ReceivedEventArgs.Count); + Assert.AreEqual("InjectedOne", messageReceivedHandler.ReceivedEventArgs[0].ApplicationMessage.Topic); + } + } + } +} \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/Server/Keep_Alive_Tests.cs b/Source/MQTTnet.Tests/Server/Keep_Alive_Tests.cs similarity index 88% rename from Tests/MQTTnet.Core.Tests/Server/Keep_Alive_Tests.cs rename to Source/MQTTnet.Tests/Server/Keep_Alive_Tests.cs index dd98509..8bcd8b0 100644 --- a/Tests/MQTTnet.Core.Tests/Server/Keep_Alive_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Keep_Alive_Tests.cs @@ -1,4 +1,8 @@ -using System; +// 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.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -19,8 +23,8 @@ namespace MQTTnet.Tests.Server await testEnvironment.StartServer(); var client = await testEnvironment.ConnectLowLevelClient(o => o - .WithCommunicationTimeout(TimeSpan.FromSeconds(1)) - .WithCommunicationTimeout(TimeSpan.Zero) + .WithTimeout(TimeSpan.FromSeconds(1)) + .WithTimeout(TimeSpan.Zero) .WithProtocolVersion(MqttProtocolVersion.V500)).ConfigureAwait(false); await client.SendAsync(new MqttConnectPacket diff --git a/Source/MQTTnet.Tests/Server/Load_Tests.cs b/Source/MQTTnet.Tests/Server/Load_Tests.cs new file mode 100644 index 0000000..b4ddfe4 --- /dev/null +++ b/Source/MQTTnet.Tests/Server/Load_Tests.cs @@ -0,0 +1,172 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Client; +using MQTTnet.Implementations; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Tests.Server +{ + [TestClass] + public sealed class Load_Tests : BaseTestClass + { + [TestMethod] + public async Task Handle_100_000_Messages_In_Receiving_Client() + { + using (var testEnvironment = CreateTestEnvironment()) + { + await testEnvironment.StartServer(); + + var receivedMessages = 0; + + using (var receiverClient = await testEnvironment.ConnectClient()) + { + receiverClient.ApplicationMessageReceivedAsync += e => + { + Interlocked.Increment(ref receivedMessages); + return PlatformAbstractionLayer.CompletedTask; + }; + + await receiverClient.SubscribeAsync("t/+"); + + + for (var i = 0; i < 100; i++) + { + _ = Task.Run( + async () => + { + using (var client = await testEnvironment.ConnectClient()) + { + var applicationMessageBuilder = new MqttApplicationMessageBuilder(); + + for (var j = 0; j < 1000; j++) + { + var message = applicationMessageBuilder.WithTopic("t/" + j) + .Build(); + + await client.PublishAsync(message) + .ConfigureAwait(false); + } + + await client.DisconnectAsync(); + } + }); + } + + SpinWait.SpinUntil(() => receivedMessages == 100000, TimeSpan.FromSeconds(60)); + + Assert.AreEqual(100000, receivedMessages); + } + } + } + + [TestMethod] + public async Task Handle_100_000_Messages_In_Low_Level_Client() + { + using (var testEnvironment = CreateTestEnvironment()) + { + var server = await testEnvironment.StartServer(); + + var receivedMessages = 0; + + server.InterceptingPublishAsync += e => + { + Interlocked.Increment(ref receivedMessages); + return PlatformAbstractionLayer.CompletedTask; + }; + + for (var i = 0; i < 100; i++) + { + _ = Task.Run( + async () => + { + try + { + using (var client = await testEnvironment.ConnectLowLevelClient()) + { + await client.SendAsync( + new MqttConnectPacket + { + ClientId = "Handle_100_000_Messages_In_Low_Level_Client_" + Guid.NewGuid() + }, CancellationToken.None); + + var packet = await client.ReceiveAsync(CancellationToken.None); + + var connAckPacket = packet as MqttConnAckPacket; + + Assert.IsTrue(connAckPacket != null); + Assert.AreEqual(MqttConnectReasonCode.Success, connAckPacket.ReasonCode); + + var publishPacket = new MqttPublishPacket(); + + for (var j = 0; j < 1000; j++) + { + publishPacket.Topic = j.ToString(); + + await client.SendAsync(publishPacket, CancellationToken.None) + .ConfigureAwait(false); + } + + await client.DisconnectAsync(CancellationToken.None); + } + } + catch (Exception exception) + { + Console.WriteLine(exception); + } + }); + } + + SpinWait.SpinUntil(() => receivedMessages == 100000, TimeSpan.FromSeconds(10)); + + Assert.AreEqual(100000, receivedMessages); + } + } + + [TestMethod] + public async Task Handle_100_000_Messages_In_Server() + { + using (var testEnvironment = CreateTestEnvironment()) + { + var server = await testEnvironment.StartServer(); + + var receivedMessages = 0; + + server.InterceptingPublishAsync += e => + { + Interlocked.Increment(ref receivedMessages); + return PlatformAbstractionLayer.CompletedTask; + }; + + for (var i = 0; i < 100; i++) + { + _ = Task.Run( + async () => + { + using (var client = await testEnvironment.ConnectClient()) + { + var applicationMessageBuilder = new MqttApplicationMessageBuilder(); + + for (var j = 0; j < 1000; j++) + { + var message = applicationMessageBuilder.WithTopic(j.ToString()) + .Build(); + + await client.PublishAsync(message) + .ConfigureAwait(false); + } + + await client.DisconnectAsync(); + } + }); + } + + SpinWait.SpinUntil(() => receivedMessages == 100000, TimeSpan.FromSeconds(10)); + + Assert.AreEqual(100000, receivedMessages); + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/MqttSubscriptionsManager_Tests.cs b/Source/MQTTnet.Tests/Server/MqttSubscriptionsManager_Tests.cs new file mode 100644 index 0000000..ea3e743 --- /dev/null +++ b/Source/MQTTnet.Tests/Server/MqttSubscriptionsManager_Tests.cs @@ -0,0 +1,139 @@ +// 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.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Packets; +using MQTTnet.Protocol; +using MQTTnet.Server; +using MQTTnet.Tests.Mockups; + +namespace MQTTnet.Tests.Server +{ + [TestClass] + public sealed class MqttSubscriptionsManager_Tests : BaseTestClass + { + MqttClientSubscriptionsManager _subscriptionsManager; + + [TestMethod] + public async Task MqttSubscriptionsManager_SubscribeAndUnsubscribeSingle() + { + var sp = new MqttSubscribePacket + { + TopicFilters = new List + { + new MqttTopicFilterBuilder().WithTopic("A/B/C").Build() + } + }; + + await _subscriptionsManager.Subscribe(sp, CancellationToken.None); + + Assert.IsTrue(CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.AtMostOnce, "").IsSubscribed); + + var up = new MqttUnsubscribePacket(); + up.TopicFilters.Add("A/B/C"); + await _subscriptionsManager.Unsubscribe(up, CancellationToken.None); + + Assert.IsFalse(CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.AtMostOnce, "").IsSubscribed); + } + + [TestMethod] + public async Task MqttSubscriptionsManager_SubscribeDifferentQoSSuccess() + { + var sp = new MqttSubscribePacket + { + TopicFilters = new List + { + new MqttTopicFilter { Topic = "A/B/C", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce } + } + }; + + await _subscriptionsManager.Subscribe(sp, CancellationToken.None); + + var result = CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.ExactlyOnce, ""); + Assert.IsTrue(result.IsSubscribed); + Assert.AreEqual(result.QualityOfServiceLevel, MqttQualityOfServiceLevel.AtMostOnce); + } + + [TestMethod] + public async Task MqttSubscriptionsManager_SubscribeSingleNoSuccess() + { + var sp = new MqttSubscribePacket + { + TopicFilters = new List + { + new MqttTopicFilterBuilder().WithTopic("A/B/C").Build() + } + }; + + await _subscriptionsManager.Subscribe(sp, CancellationToken.None); + + Assert.IsFalse(CheckSubscriptions("A/B/X", MqttQualityOfServiceLevel.AtMostOnce, "").IsSubscribed); + } + + [TestMethod] + public async Task MqttSubscriptionsManager_SubscribeSingleSuccess() + { + var sp = new MqttSubscribePacket + { + TopicFilters = new List + { + new MqttTopicFilterBuilder().WithTopic("A/B/C").Build() + } + }; + + await _subscriptionsManager.Subscribe(sp, CancellationToken.None); + + var result = CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.AtMostOnce, ""); + + Assert.IsTrue(result.IsSubscribed); + Assert.AreEqual(result.QualityOfServiceLevel, MqttQualityOfServiceLevel.AtMostOnce); + } + + [TestMethod] + public async Task MqttSubscriptionsManager_SubscribeTwoTimesSuccess() + { + var sp = new MqttSubscribePacket + { + TopicFilters = new List + { + new MqttTopicFilter { Topic = "#", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }, + new MqttTopicFilter { Topic = "A/B/C", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce } + } + }; + + await _subscriptionsManager.Subscribe(sp, CancellationToken.None); + + var result = CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.ExactlyOnce, ""); + + Assert.IsTrue(result.IsSubscribed); + Assert.AreEqual(result.QualityOfServiceLevel, MqttQualityOfServiceLevel.AtLeastOnce); + } + + [TestInitialize] + public void TestInitialize() + { + var logger = new TestLogger(); + var options = new MqttServerOptions(); + var retainedMessagesManager = new MqttRetainedMessagesManager(new MqttServerEventContainer(), logger); + var eventContainer = new MqttServerEventContainer(); + var clientSessionManager = new MqttClientSessionsManager(options, retainedMessagesManager, eventContainer, logger); + + var session = new MqttSession("", false, new ConcurrentDictionary(), options, eventContainer, retainedMessagesManager, clientSessionManager); + + _subscriptionsManager = new MqttClientSubscriptionsManager(session, new MqttServerEventContainer(), retainedMessagesManager, clientSessionManager); + } + + CheckSubscriptionsResult CheckSubscriptions(string topic, MqttQualityOfServiceLevel applicationMessageQoSLevel, string senderClientId) + { + ulong topicHashMask; // not needed + bool hasWildcard; // not needed + MqttSubscription.CalculateTopicHash(topic, out var topicHash, out topicHashMask, out hasWildcard); + return _subscriptionsManager.CheckSubscriptions(topic, topicHash, applicationMessageQoSLevel, senderClientId); + } + } +} \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/Server/No_Local_Tests.cs b/Source/MQTTnet.Tests/Server/No_Local_Tests.cs similarity index 82% rename from Tests/MQTTnet.Core.Tests/Server/No_Local_Tests.cs rename to Source/MQTTnet.Tests/Server/No_Local_Tests.cs index 04f4e28..dadb25d 100644 --- a/Tests/MQTTnet.Core.Tests/Server/No_Local_Tests.cs +++ b/Source/MQTTnet.Tests/Server/No_Local_Tests.cs @@ -1,4 +1,8 @@ -using System.Threading.Tasks; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; using MQTTnet.Formatter; @@ -37,7 +41,7 @@ namespace MQTTnet.Tests.Server applicationMessageHandler.AssertReceivedCountEquals(0); // The client will publish a message where it is itself subscribing to. - await client1.PublishAsync("Topic", "Payload", true); + await client1.PublishStringAsync("Topic", "Payload", retain: true); await LongTestDelay(); applicationMessageHandler.AssertReceivedCountEquals(expectedCountAfterPublish); diff --git a/Tests/MQTTnet.Core.Tests/Server/Retain_As_Published_Tests.cs b/Source/MQTTnet.Tests/Server/Retain_As_Published_Tests.cs similarity index 79% rename from Tests/MQTTnet.Core.Tests/Server/Retain_As_Published_Tests.cs rename to Source/MQTTnet.Tests/Server/Retain_As_Published_Tests.cs index f65557f..653d029 100644 --- a/Tests/MQTTnet.Core.Tests/Server/Retain_As_Published_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Retain_As_Published_Tests.cs @@ -1,4 +1,8 @@ -using System.Threading.Tasks; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Formatter; using MQTTnet.Client; @@ -33,11 +37,11 @@ namespace MQTTnet.Tests.Server await LongTestDelay(); // The client will publish a message where it is itself subscribing to. - await client1.PublishAsync("Topic", "Payload", true); + await client1.PublishStringAsync("Topic", "Payload", retain: true); await LongTestDelay(); applicationMessageHandler.AssertReceivedCountEquals(1); - Assert.AreEqual(retainAsPublished, applicationMessageHandler.ReceivedApplicationMessages[0].Retain); + Assert.AreEqual(retainAsPublished, applicationMessageHandler.ReceivedEventArgs[0].ApplicationMessage.Retain); } } } diff --git a/Tests/MQTTnet.Core.Tests/Server/Retain_Handling_Tests.cs b/Source/MQTTnet.Tests/Server/Retain_Handling_Tests.cs similarity index 84% rename from Tests/MQTTnet.Core.Tests/Server/Retain_Handling_Tests.cs rename to Source/MQTTnet.Tests/Server/Retain_Handling_Tests.cs index 98827ec..cbb1485 100644 --- a/Tests/MQTTnet.Core.Tests/Server/Retain_Handling_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Retain_Handling_Tests.cs @@ -1,4 +1,8 @@ -using System.Threading.Tasks; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; using MQTTnet.Formatter; @@ -38,7 +42,7 @@ namespace MQTTnet.Tests.Server await testEnvironment.StartServer(); var client1 = await testEnvironment.ConnectClient(); - await client1.PublishAsync("Topic", "Payload", true); + await client1.PublishStringAsync("Topic", "Payload", retain: true); await LongTestDelay(); @@ -51,7 +55,7 @@ namespace MQTTnet.Tests.Server applicationMessageHandler.AssertReceivedCountEquals(expectedCountAfterSubscribe); - await client1.PublishAsync("Topic", "Payload", true); + await client1.PublishStringAsync("Topic", "Payload", retain: true); await LongTestDelay(); applicationMessageHandler.AssertReceivedCountEquals(expectedCountAfterSecondPublish); diff --git a/Tests/MQTTnet.Core.Tests/Server/Retained_Messages_Tests.cs b/Source/MQTTnet.Tests/Server/Retained_Messages_Tests.cs similarity index 94% rename from Tests/MQTTnet.Core.Tests/Server/Retained_Messages_Tests.cs rename to Source/MQTTnet.Tests/Server/Retained_Messages_Tests.cs index d9ea9d2..ae6c5f3 100644 --- a/Tests/MQTTnet.Core.Tests/Server/Retained_Messages_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Retained_Messages_Tests.cs @@ -1,8 +1,13 @@ -using System.Linq; +// 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.Linq; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; using MQTTnet.Formatter; +using MQTTnet.Packets; using MQTTnet.Protocol; namespace MQTTnet.Tests.Server @@ -18,6 +23,7 @@ namespace MQTTnet.Tests.Server await testEnvironment.StartServer(); var client = testEnvironment.CreateClient(); + var connectResult = await client.ConnectAsync(testEnvironment.Factory.CreateClientOptionsBuilder() .WithProtocolVersion(MqttProtocolVersion.V311) .WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); @@ -114,7 +120,7 @@ namespace MQTTnet.Tests.Server await Task.Delay(500); messageHandler.AssertReceivedCountEquals(1); - Assert.IsTrue(messageHandler.ReceivedApplicationMessages.First().Retain); + Assert.IsTrue(messageHandler.ReceivedEventArgs.First().ApplicationMessage.Retain); } } diff --git a/Tests/MQTTnet.Core.Tests/Server/Security_Tests.cs b/Source/MQTTnet.Tests/Server/Security_Tests.cs similarity index 77% rename from Tests/MQTTnet.Core.Tests/Server/Security_Tests.cs rename to Source/MQTTnet.Tests/Server/Security_Tests.cs index bc1e3bd..57d57a0 100644 --- a/Tests/MQTTnet.Core.Tests/Server/Security_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Security_Tests.cs @@ -1,10 +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. + using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Adapter; using MQTTnet.Client; -using MQTTnet.Client.Connecting; -using MQTTnet.Client.Options; using MQTTnet.Exceptions; +using MQTTnet.Implementations; using MQTTnet.Protocol; namespace MQTTnet.Tests.Server @@ -62,19 +65,22 @@ namespace MQTTnet.Tests.Server { testEnvironment.IgnoreClientLogErrors = true; - await testEnvironment.StartServer(testEnvironment.Factory.CreateServerOptionsBuilder() - .WithConnectionValidator(c => + var server = await testEnvironment.StartServer(); + + server.ValidatingConnectionAsync += e => + { + if (e.Username != "UserName1") { - if (c.Username != "UserName1") - { - c.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword; - } - - if (c.Password != "Password1") - { - c.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword; - } - })); + e.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword; + } + + if (e.Password != "Password1") + { + e.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword; + } + + return PlatformAbstractionLayer.CompletedTask; + }; var client = testEnvironment.CreateClient(); diff --git a/Tests/MQTTnet.Core.Tests/Server/Server_Reference_Tests.cs b/Source/MQTTnet.Tests/Server/Server_Reference_Tests.cs similarity index 68% rename from Tests/MQTTnet.Core.Tests/Server/Server_Reference_Tests.cs rename to Source/MQTTnet.Tests/Server/Server_Reference_Tests.cs index 73b2f11..19885d9 100644 --- a/Tests/MQTTnet.Core.Tests/Server/Server_Reference_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Server_Reference_Tests.cs @@ -1,10 +1,13 @@ -using System.Threading.Tasks; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Adapter; using MQTTnet.Client; -using MQTTnet.Client.Connecting; -using MQTTnet.Client.Options; using MQTTnet.Formatter; +using MQTTnet.Implementations; using MQTTnet.Protocol; namespace MQTTnet.Tests.Server @@ -19,14 +22,14 @@ namespace MQTTnet.Tests.Server { testEnvironment.IgnoreClientLogErrors = true; - await testEnvironment.StartServer(o => + var server = await testEnvironment.StartServer(); + + server.ValidatingConnectionAsync += e => { - o.WithConnectionValidator(v => - { - v.ReasonCode = MqttConnectReasonCode.ServerMoved; - v.ServerReference = "new_server"; - }); - }); + e.ReasonCode = MqttConnectReasonCode.ServerMoved; + e.ServerReference = "new_server"; + return PlatformAbstractionLayer.CompletedTask; + }; try { diff --git a/Source/MQTTnet.Tests/Server/Session_Tests.cs b/Source/MQTTnet.Tests/Server/Session_Tests.cs new file mode 100644 index 0000000..c11a878 --- /dev/null +++ b/Source/MQTTnet.Tests/Server/Session_Tests.cs @@ -0,0 +1,317 @@ +// 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.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Client; +using MQTTnet.Formatter; +using MQTTnet.Implementations; +using MQTTnet.Protocol; +using MQTTnet.Server; +using MQTTnet.Tests.Mockups; +using MqttClient = MQTTnet.Client.MqttClient; + +namespace MQTTnet.Tests.Server +{ + [TestClass] + public sealed class Session_Tests : BaseTestClass + { + [TestMethod] + public async Task Clean_Session_Persistence() + { + using (var testEnvironment = new TestEnvironment(TestContext)) + { + // Create server with persistent sessions enabled + + await testEnvironment.StartServer(o => o.WithPersistentSessions()); + + const string ClientId = "Client1"; + + // Create client with clean session and long session expiry interval + + var client1 = await testEnvironment.ConnectClient( + o => o.WithProtocolVersion(MqttProtocolVersion.V311) + .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) + .WithSessionExpiryInterval(9999) // not relevant for v311 but testing impact + .WithCleanSession() // start and end with clean session + .WithClientId(ClientId) + .Build()); + + // Disconnect; empty session should be removed from server + + await client1.DisconnectAsync(); + + // Simulate some time delay between connections + + await Task.Delay(1000); + + // Reconnect the same client ID without clean session + + var client2 = testEnvironment.CreateClient(); + var options = testEnvironment.Factory.CreateClientOptionsBuilder() + .WithProtocolVersion(MqttProtocolVersion.V311) + .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) + .WithSessionExpiryInterval(9999) // not relevant for v311 but testing impact + .WithCleanSession(false) // see if there is a session + .WithClientId(ClientId) + .Build(); + + + var result = await client2.ConnectAsync(options).ConfigureAwait(false); + + await client2.DisconnectAsync(); + + // Session should NOT be present for MQTT v311 and initial CleanSession == true + + Assert.IsTrue(!result.IsSessionPresent, "Session present"); + } + } + + [TestMethod] + public async Task Fire_Deleted_Event() + { + using (var testEnvironment = CreateTestEnvironment()) + { + // Arrange client and server. + var server = await testEnvironment.StartServer(o => o.WithPersistentSessions(false)); + + var deletedEventFired = false; + server.SessionDeletedAsync += e => + { + deletedEventFired = true; + return PlatformAbstractionLayer.CompletedTask; + }; + + var client = await testEnvironment.ConnectClient(); + + // Act: Disconnect the client -> Event must be fired. + await client.DisconnectAsync(); + + await LongTestDelay(); + + // Assert that the event was fired properly. + Assert.IsTrue(deletedEventFired); + } + } + + [TestMethod] + public async Task Get_Session_Items_In_Status() + { + using (var testEnvironment = CreateTestEnvironment()) + { + var server = await testEnvironment.StartServer(); + + server.ValidatingConnectionAsync += e => + { + // Don't validate anything. Just set some session items. + e.SessionItems["can_subscribe_x"] = true; + e.SessionItems["default_payload"] = "Hello World"; + + return PlatformAbstractionLayer.CompletedTask; + }; + + await testEnvironment.ConnectClient(); + + var sessionStatus = await testEnvironment.Server.GetSessionsAsync(); + var session = sessionStatus.First(); + + Assert.AreEqual(true, session.Items["can_subscribe_x"]); + } + } + + [TestMethod] + public async Task Handle_Parallel_Connection_Attempts() + { + using (var testEnvironment = CreateTestEnvironment()) + { + testEnvironment.IgnoreClientLogErrors = true; + + await testEnvironment.StartServer(); + + var options = new MqttClientOptionsBuilder().WithClientId("1").WithKeepAlivePeriod(TimeSpan.FromSeconds(5)); + + var hasReceive = false; + + void OnReceive() + { + if (!hasReceive) + { + hasReceive = true; + } + } + + // Try to connect 50 clients at the same time. + var clients = await Task.WhenAll(Enumerable.Range(0, 50).Select(i => ConnectAndSubscribe(testEnvironment, options, OnReceive))); + var connectedClients = clients.Where(c => c?.IsConnected ?? false).ToList(); + + Assert.AreEqual(1, connectedClients.Count); + + await Task.Delay(5000); + + var option2 = new MqttClientOptionsBuilder().WithClientId("2").WithKeepAlivePeriod(TimeSpan.FromSeconds(10)); + var sendClient = await testEnvironment.ConnectClient(option2); + await sendClient.PublishStringAsync("aaa", "1"); + + await Task.Delay(3000); + + Assert.AreEqual(true, hasReceive); + } + } + + [TestMethod] + public async Task Manage_Session_MaxParallel() + { + using (var testEnvironment = CreateTestEnvironment()) + { + testEnvironment.IgnoreClientLogErrors = true; + var serverOptions = new MqttServerOptionsBuilder(); + await testEnvironment.StartServer(serverOptions); + + var options = new MqttClientOptionsBuilder().WithClientId("1"); + + var clients = await Task.WhenAll(Enumerable.Range(0, 10).Select(i => TryConnect(testEnvironment, options))); + + var connectedClients = clients.Where(c => c?.IsConnected ?? false).ToList(); + + Assert.AreEqual(1, connectedClients.Count); + } + } + + [DataTestMethod] + [DataRow(MqttQualityOfServiceLevel.ExactlyOnce)] + [DataRow(MqttQualityOfServiceLevel.AtLeastOnce)] + public async Task Retry_If_Not_PubAck(MqttQualityOfServiceLevel qos) + { + long count = 0; + using (var testEnvironment = CreateTestEnvironment()) + { + await testEnvironment.StartServer(o => o.WithPersistentSessions()); + + var publisher = await testEnvironment.ConnectClient(); + + var subscriber = await testEnvironment.ConnectClient(o => o.WithClientId(qos.ToString()).WithCleanSession(false)); + + subscriber.ApplicationMessageReceivedAsync += c => + { + c.AutoAcknowledge = false; + ++count; + Console.WriteLine("process"); + return PlatformAbstractionLayer.CompletedTask; + }; + + await subscriber.SubscribeAsync("#", qos); + + var pub = publisher.PublishStringAsync("a", null, qos); + + await Task.Delay(100); + await subscriber.DisconnectAsync(); + await subscriber.ConnectAsync(subscriber.Options); + await Task.Delay(100); + + var res = await pub; + + Assert.AreEqual(MqttClientPublishReasonCode.Success, res.ReasonCode); + Assert.AreEqual(2, count); + } + } + + [TestMethod] + public async Task Set_Session_Item() + { + using (var testEnvironment = CreateTestEnvironment()) + { + var server = await testEnvironment.StartServer(); + + server.ValidatingConnectionAsync += e => + { + // Don't validate anything. Just set some session items. + e.SessionItems["can_subscribe_x"] = true; + e.SessionItems["default_payload"] = "Hello World"; + + return PlatformAbstractionLayer.CompletedTask; + }; + + server.InterceptingSubscriptionAsync += e => + { + if (e.TopicFilter.Topic == "x") + { + if (e.SessionItems["can_subscribe_x"] as bool? == false) + { + e.Response.ReasonCode = MqttSubscribeReasonCode.ImplementationSpecificError; + } + } + + return PlatformAbstractionLayer.CompletedTask; + }; + + server.InterceptingPublishAsync += e => + { + e.ApplicationMessage.Payload = Encoding.UTF8.GetBytes(e.SessionItems["default_payload"] as string ?? string.Empty); + return PlatformAbstractionLayer.CompletedTask; + }; + + string receivedPayload = null; + + var client = await testEnvironment.ConnectClient(); + client.ApplicationMessageReceivedAsync += e => + { + receivedPayload = e.ApplicationMessage.ConvertPayloadToString(); + return PlatformAbstractionLayer.CompletedTask; + }; + + var subscribeResult = await client.SubscribeAsync("x"); + + Assert.AreEqual(MqttClientSubscribeResultCode.GrantedQoS0, subscribeResult.Items.First().ResultCode); + + var client2 = await testEnvironment.ConnectClient(); + await client2.PublishStringAsync("x"); + + await Task.Delay(1000); + + Assert.AreEqual("Hello World", receivedPayload); + } + } + + async Task ConnectAndSubscribe(TestEnvironment testEnvironment, MqttClientOptionsBuilder options, Action onReceive) + { + try + { + var sendClient = await testEnvironment.ConnectClient(options); + + sendClient.ApplicationMessageReceivedAsync += e => + { + onReceive(); + return PlatformAbstractionLayer.CompletedTask; + }; + + using (var subscribeTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5))) + { + await sendClient.SubscribeAsync("aaa", MqttQualityOfServiceLevel.AtMostOnce, subscribeTimeout.Token).ConfigureAwait(false); + } + + return sendClient; + } + catch (Exception) + { + return null; + } + } + + async Task TryConnect(TestEnvironment testEnvironment, MqttClientOptionsBuilder options) + { + try + { + return await testEnvironment.ConnectClient(options); + } + catch + { + return null; + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/Server/Shared_Subscriptions_Tests.cs b/Source/MQTTnet.Tests/Server/Shared_Subscriptions_Tests.cs new file mode 100644 index 0000000..9d114d1 --- /dev/null +++ b/Source/MQTTnet.Tests/Server/Shared_Subscriptions_Tests.cs @@ -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 System.Linq; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Client; +using MQTTnet.Formatter; + +namespace MQTTnet.Tests.Server +{ + [TestClass] + public sealed class Shared_Subscriptions_Tests : BaseTestClass + { + [TestMethod] + public async Task Server_Reports_Shared_Subscriptions_Not_Supported() + { + using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) + { + await testEnvironment.StartServer(); + + var client = testEnvironment.CreateClient(); + var connectResult = await client.ConnectAsync(testEnvironment.Factory.CreateClientOptionsBuilder() + .WithProtocolVersion(MqttProtocolVersion.V500) + .WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); + + Assert.IsFalse(connectResult.SharedSubscriptionAvailable); + } + } + + [TestMethod] + public async Task Subscription_Of_Shared_Subscription_Is_Denied() + { + using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) + { + await testEnvironment.StartServer(); + + var client = testEnvironment.CreateClient(); + await client.ConnectAsync(testEnvironment.Factory.CreateClientOptionsBuilder() + .WithProtocolVersion(MqttProtocolVersion.V500) + .WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); + + var subscribeResult = await client.SubscribeAsync("$share/A"); + + Assert.AreEqual(MqttClientSubscribeResultCode.SharedSubscriptionsNotSupported, subscribeResult.Items.First().ResultCode); + } + } + } +} \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/Server/Status_Tests.cs b/Source/MQTTnet.Tests/Server/Status_Tests.cs similarity index 73% rename from Tests/MQTTnet.Core.Tests/Server/Status_Tests.cs rename to Source/MQTTnet.Tests/Server/Status_Tests.cs index 2995859..7504692 100644 --- a/Tests/MQTTnet.Core.Tests/Server/Status_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Status_Tests.cs @@ -1,8 +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. + using System.Linq; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; -using MQTTnet.Client.Options; using MQTTnet.Protocol; using MQTTnet.Server; using MQTTnet.Tests.Mockups; @@ -24,22 +27,22 @@ namespace MQTTnet.Tests.Server await Task.Delay(500); - var clientStatus = await server.GetClientStatusAsync(); - var sessionStatus = await server.GetSessionStatusAsync(); + var clientStatus = await server.GetClientsAsync(); + var sessionStatus = await server.GetSessionsAsync(); Assert.AreEqual(2, clientStatus.Count); Assert.AreEqual(2, sessionStatus.Count); - Assert.IsTrue(clientStatus.Any(s => s.ClientId == c1.Options.ClientId)); - Assert.IsTrue(clientStatus.Any(s => s.ClientId == c2.Options.ClientId)); + Assert.IsTrue(clientStatus.Any(s => s.Id == c1.Options.ClientId)); + Assert.IsTrue(clientStatus.Any(s => s.Id == c2.Options.ClientId)); await c1.DisconnectAsync(); await c2.DisconnectAsync(); await Task.Delay(500); - clientStatus = await server.GetClientStatusAsync(); - sessionStatus = await server.GetSessionStatusAsync(); + clientStatus = await server.GetClientsAsync(); + sessionStatus = await server.GetSessionsAsync(); Assert.AreEqual(0, clientStatus.Count); Assert.AreEqual(0, sessionStatus.Count); @@ -57,10 +60,10 @@ namespace MQTTnet.Tests.Server await Task.Delay(1000); - var clientStatus = await server.GetClientStatusAsync(); + var clientStatus = await server.GetClientsAsync(); Assert.AreEqual(1, clientStatus.Count); - Assert.IsTrue(clientStatus.Any(s => s.ClientId == c1.Options.ClientId)); + Assert.IsTrue(clientStatus.Any(s => s.Id == c1.Options.ClientId)); await clientStatus.First().DisconnectAsync(); @@ -68,7 +71,7 @@ namespace MQTTnet.Tests.Server Assert.IsFalse(c1.IsConnected); - clientStatus = await server.GetClientStatusAsync(); + clientStatus = await server.GetClientsAsync(); Assert.AreEqual(0, clientStatus.Count); } @@ -88,8 +91,8 @@ namespace MQTTnet.Tests.Server await Task.Delay(500); - var clientStatus = await server.GetClientStatusAsync(); - var sessionStatus = await server.GetSessionStatusAsync(); + var clientStatus = await server.GetClientsAsync(); + var sessionStatus = await server.GetSessionsAsync(); Assert.AreEqual(1, clientStatus.Count); Assert.AreEqual(2, sessionStatus.Count); @@ -98,8 +101,8 @@ namespace MQTTnet.Tests.Server await Task.Delay(500); - clientStatus = await server.GetClientStatusAsync(); - sessionStatus = await server.GetSessionStatusAsync(); + clientStatus = await server.GetClientsAsync(); + sessionStatus = await server.GetSessionsAsync(); Assert.AreEqual(0, clientStatus.Count); Assert.AreEqual(2, sessionStatus.Count); @@ -117,10 +120,10 @@ namespace MQTTnet.Tests.Server for (var i = 1; i < 25; i++) { - await c1.PublishAsync("a"); + await c1.PublishStringAsync("a"); await Task.Delay(50); - var clientStatus = await server.GetClientStatusAsync(); + var clientStatus = await server.GetClientsAsync(); Assert.AreEqual(i, clientStatus.First().SentApplicationMessagesCount); Assert.AreEqual(0, clientStatus.First().ReceivedApplicationMessagesCount); } @@ -140,18 +143,19 @@ namespace MQTTnet.Tests.Server { // At most once will send one packet to the client and the server will reply // with an additional ACK packet. - await c1.PublishAsync("a", string.Empty, MqttQualityOfServiceLevel.AtLeastOnce); - await Task.Delay(250); + await c1.PublishStringAsync("a", string.Empty, MqttQualityOfServiceLevel.AtLeastOnce); + + await Task.Delay(500); - var clientStatus = await server.GetClientStatusAsync(); + var clientStatus = await server.GetClientsAsync(); Assert.AreEqual(i, clientStatus.First().SentApplicationMessagesCount, "SAMC invalid!"); // + 1 because CONNECT is also counted. Assert.AreEqual(i + 1, clientStatus.First().SentPacketsCount, "SPC invalid!"); - // +1 because ConnACK package is already counted. - Assert.AreEqual(i + 1, clientStatus.First().ReceivedPacketsCount, "RPC invalid!"); + // +2 because ConnACK + PubAck package is already counted. + Assert.AreEqual(i + 2, clientStatus.First().ReceivedPacketsCount, "RPC invalid!"); } } } diff --git a/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs b/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs new file mode 100644 index 0000000..f79fcdf --- /dev/null +++ b/Source/MQTTnet.Tests/Server/Subscribe_Tests.cs @@ -0,0 +1,317 @@ +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Client; +using MQTTnet.Formatter; +using MQTTnet.Implementations; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Tests.Server +{ + [TestClass] + public sealed class Subscribe_Tests : BaseTestClass + { + [TestMethod] + public async Task Intercept_Subscription() + { + using (var testEnvironment = CreateTestEnvironment()) + { + var server = await testEnvironment.StartServer(); + + server.InterceptingSubscriptionAsync += e => + { + // Set the topic to "a" regards what the client wants to subscribe. + e.TopicFilter.Topic = "a"; + return PlatformAbstractionLayer.CompletedTask; + }; + + var topicAReceived = false; + var topicBReceived = false; + + var client = await testEnvironment.ConnectClient(); + client.ApplicationMessageReceivedAsync += e => + { + if (e.ApplicationMessage.Topic == "a") + { + topicAReceived = true; + } + else if (e.ApplicationMessage.Topic == "b") + { + topicBReceived = true; + } + + return PlatformAbstractionLayer.CompletedTask; + }; + + await client.SubscribeAsync("b"); + + await client.PublishStringAsync("a"); + + await Task.Delay(500); + + Assert.IsTrue(topicAReceived); + Assert.IsFalse(topicBReceived); + } + } + + [TestMethod] + public async Task Subscribe_Unsubscribe() + { + using (var testEnvironment = CreateTestEnvironment()) + { + var receivedMessagesCount = 0; + + var server = await testEnvironment.StartServer(); + + var c1 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("c1")); + c1.ApplicationMessageReceivedAsync += e => + { + Interlocked.Increment(ref receivedMessagesCount); + return PlatformAbstractionLayer.CompletedTask; + }; + + var c2 = await testEnvironment.ConnectClient(new MqttClientOptionsBuilder().WithClientId("c2")); + + var message = new MqttApplicationMessageBuilder().WithTopic("a").WithQualityOfServiceLevel(MqttQualityOfServiceLevel.ExactlyOnce).Build(); + await c2.PublishAsync(message); + + await Task.Delay(500); + Assert.AreEqual(0, receivedMessagesCount); + + var subscribeEventCalled = false; + server.ClientSubscribedTopicAsync += e => + { + subscribeEventCalled = e.TopicFilter.Topic == "a" && e.ClientId == c1.Options.ClientId; + return PlatformAbstractionLayer.CompletedTask; + }; + + await c1.SubscribeAsync(new MqttTopicFilter { Topic = "a", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }); + await Task.Delay(250); + Assert.IsTrue(subscribeEventCalled, "Subscribe event not called."); + + await c2.PublishAsync(message); + await Task.Delay(250); + Assert.AreEqual(1, receivedMessagesCount); + + var unsubscribeEventCalled = false; + server.ClientUnsubscribedTopicAsync += e => + { + unsubscribeEventCalled = e.TopicFilter == "a" && e.ClientId == c1.Options.ClientId; + return PlatformAbstractionLayer.CompletedTask; + }; + + await c1.UnsubscribeAsync("a"); + await Task.Delay(250); + Assert.IsTrue(unsubscribeEventCalled, "Unsubscribe event not called."); + + await c2.PublishAsync(message); + await Task.Delay(500); + Assert.AreEqual(1, receivedMessagesCount); + + await Task.Delay(500); + + Assert.AreEqual(1, receivedMessagesCount); + } + } + + [TestMethod] + public async Task Subscribe_Multiple_In_Single_Request() + { + using (var testEnvironment = CreateTestEnvironment()) + { + var receivedMessagesCount = 0; + + await testEnvironment.StartServer(); + + var c1 = await testEnvironment.ConnectClient(); + c1.ApplicationMessageReceivedAsync += e => + { + Interlocked.Increment(ref receivedMessagesCount); + return PlatformAbstractionLayer.CompletedTask; + }; + + await c1.SubscribeAsync(new MqttClientSubscribeOptionsBuilder() + .WithTopicFilter("a") + .WithTopicFilter("b") + .WithTopicFilter("c") + .Build()); + + var c2 = await testEnvironment.ConnectClient(); + + await c2.PublishStringAsync("a"); + await Task.Delay(100); + Assert.AreEqual(receivedMessagesCount, 1); + + await c2.PublishStringAsync("b"); + await Task.Delay(100); + Assert.AreEqual(receivedMessagesCount, 2); + + await c2.PublishStringAsync("c"); + await Task.Delay(100); + Assert.AreEqual(receivedMessagesCount, 3); + } + } + + [TestMethod] + public async Task Subscribe_Lots_In_Single_Request() + { + using (var testEnvironment = CreateTestEnvironment()) + { + var receivedMessagesCount = 0; + + await testEnvironment.StartServer(); + + var c1 = await testEnvironment.ConnectClient(); + c1.ApplicationMessageReceivedAsync += e => + { + Interlocked.Increment(ref receivedMessagesCount); + return PlatformAbstractionLayer.CompletedTask; + }; + + var optionsBuilder = new MqttClientSubscribeOptionsBuilder(); + for (var i = 0; i < 500; i++) + { + optionsBuilder.WithTopicFilter(i.ToString()); + } + + await c1.SubscribeAsync(optionsBuilder.Build()).ConfigureAwait(false); + + var c2 = await testEnvironment.ConnectClient(); + + var messageBuilder = new MqttApplicationMessageBuilder(); + for (var i = 0; i < 500; i++) + { + messageBuilder.WithTopic(i.ToString()); + + await c2.PublishAsync(messageBuilder.Build()).ConfigureAwait(false); + } + + SpinWait.SpinUntil(() => receivedMessagesCount == 500, TimeSpan.FromSeconds(20)); + + Assert.AreEqual(500, receivedMessagesCount); + } + } + + [TestMethod] + public async Task Subscribe_Lots_In_Multiple_Requests() + { + using (var testEnvironment = CreateTestEnvironment()) + { + var receivedMessagesCount = 0; + + await testEnvironment.StartServer(); + + var c1 = await testEnvironment.ConnectClient(); + c1.ApplicationMessageReceivedAsync += e => + { + Interlocked.Increment(ref receivedMessagesCount); + return PlatformAbstractionLayer.CompletedTask; + }; + + for (var i = 0; i < 500; i++) + { + var so = new MqttClientSubscribeOptionsBuilder() + .WithTopicFilter(i.ToString()).Build(); + + await c1.SubscribeAsync(so).ConfigureAwait(false); + + await Task.Delay(10); + } + + var c2 = await testEnvironment.ConnectClient(); + + var messageBuilder = new MqttApplicationMessageBuilder(); + for (var i = 0; i < 500; i++) + { + messageBuilder.WithTopic(i.ToString()); + + await c2.PublishAsync(messageBuilder.Build()).ConfigureAwait(false); + + await Task.Delay(10); + } + + SpinWait.SpinUntil(() => receivedMessagesCount == 500, 5000); + + Assert.AreEqual(500, receivedMessagesCount); + } + } + + [TestMethod] + public async Task Subscribe_Multiple_In_Multiple_Request() + { + using (var testEnvironment = CreateTestEnvironment()) + { + var receivedMessagesCount = 0; + + await testEnvironment.StartServer(); + + var c1 = await testEnvironment.ConnectClient(); + c1.ApplicationMessageReceivedAsync += e => + { + Interlocked.Increment(ref receivedMessagesCount); + return PlatformAbstractionLayer.CompletedTask; + }; + + await c1.SubscribeAsync(new MqttClientSubscribeOptionsBuilder() + .WithTopicFilter("a") + .Build()); + + await c1.SubscribeAsync(new MqttClientSubscribeOptionsBuilder() + .WithTopicFilter("b") + .Build()); + + await c1.SubscribeAsync(new MqttClientSubscribeOptionsBuilder() + .WithTopicFilter("c") + .Build()); + + var c2 = await testEnvironment.ConnectClient(); + + await c2.PublishStringAsync("a"); + await Task.Delay(100); + Assert.AreEqual(receivedMessagesCount, 1); + + await c2.PublishStringAsync("b"); + await Task.Delay(100); + Assert.AreEqual(receivedMessagesCount, 2); + + await c2.PublishStringAsync("c"); + await Task.Delay(100); + Assert.AreEqual(receivedMessagesCount, 3); + } + } + + [TestMethod] + public async Task Deny_Invalid_Topic() + { + using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) + { + var server = await testEnvironment.StartServer(); + + server.InterceptingSubscriptionAsync += e => + { + if (e.TopicFilter.Topic == "not_allowed_topic") + { + e.Response.ReasonCode = MqttSubscribeReasonCode.TopicFilterInvalid; + } + + return PlatformAbstractionLayer.CompletedTask; + }; + + var client = await testEnvironment.ConnectClient(); + + var subscribeResult =await client.SubscribeAsync("allowed_topic"); + Assert.AreEqual(MqttClientSubscribeResultCode.GrantedQoS0, subscribeResult.Items.First().ResultCode); + + subscribeResult =await client.SubscribeAsync("not_allowed_topic"); + Assert.AreEqual(MqttClientSubscribeResultCode.TopicFilterInvalid, subscribeResult.Items.First().ResultCode); + } + } + } +} \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/Server/Subscription_Identifier_Tests.cs b/Source/MQTTnet.Tests/Server/Subscription_Identifier_Tests.cs similarity index 82% rename from Tests/MQTTnet.Core.Tests/Server/Subscription_Identifier_Tests.cs rename to Source/MQTTnet.Tests/Server/Subscription_Identifier_Tests.cs index c69202a..bf87db6 100644 --- a/Tests/MQTTnet.Core.Tests/Server/Subscription_Identifier_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Subscription_Identifier_Tests.cs @@ -1,4 +1,8 @@ -using System.Threading.Tasks; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; using MQTTnet.Formatter; @@ -42,12 +46,12 @@ namespace MQTTnet.Tests.Server applicationMessageHandler.AssertReceivedCountEquals(0); // The client will publish a message where it is itself subscribing to. - await client1.PublishAsync("Topic", "Payload", true); + await client1.PublishStringAsync("Topic", "Payload", retain: true); await LongTestDelay(); applicationMessageHandler.AssertReceivedCountEquals(1); - applicationMessageHandler.ReceivedApplicationMessages[0].SubscriptionIdentifiers.Contains(456); + applicationMessageHandler.ReceivedEventArgs[0].ApplicationMessage.SubscriptionIdentifiers.Contains(456); } } @@ -76,13 +80,13 @@ namespace MQTTnet.Tests.Server applicationMessageHandler.AssertReceivedCountEquals(0); // The client will publish a message where it is itself subscribing to. - await client1.PublishAsync("Topic/A", "Payload", true); + await client1.PublishStringAsync("Topic/A", "Payload", retain: true); await LongTestDelay(); applicationMessageHandler.AssertReceivedCountEquals(1); - applicationMessageHandler.ReceivedApplicationMessages[0].SubscriptionIdentifiers.Contains(456); - applicationMessageHandler.ReceivedApplicationMessages[0].SubscriptionIdentifiers.Contains(789); + applicationMessageHandler.ReceivedEventArgs[0].ApplicationMessage.SubscriptionIdentifiers.Contains(456); + applicationMessageHandler.ReceivedEventArgs[0].ApplicationMessage.SubscriptionIdentifiers.Contains(789); } } } diff --git a/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs b/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs new file mode 100644 index 0000000..b514a39 --- /dev/null +++ b/Source/MQTTnet.Tests/Server/Subscription_TopicHash_Tests.cs @@ -0,0 +1,612 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Client; +using MQTTnet.Protocol; +using MQTTnet.Server; +using MQTTnet.Tests.Mockups; + +namespace MQTTnet.Tests.Server +{ + [TestClass] + public class Subscription_TopicHash_Tests + { + enum TopicHashSelector { NoWildcard, SingleWildcard, MultiWildcard }; + + MQTTnet.Server.MqttSession _clientSession; + + [TestMethod] + public void Match_Hash_Test_SingleWildCard() + { + var l0 = "pub0"; + var l1 = "topic1"; + var l2 = "+"; + var l3 = "prop1"; + var topic = string.Format("{0}/{1}/{2}/{3}", l0, l1, l2, l3); + + + UInt64 topicHash; + UInt64 topicHashMask; + bool topicHasWildcard; + MQTTnet.Server.MqttSubscription.CalculateTopicHash(topic, out topicHash, out topicHashMask, out topicHasWildcard); + + Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); + + var hashBytes = GetBytes(topicHash); + Assert.AreNotEqual(hashBytes[0], 0, "checksum 0 mismatch"); + Assert.AreNotEqual(hashBytes[1], 0, "checksum 1 mismatch"); + Assert.AreEqual(hashBytes[2], 0, "checksum 2 mismatch"); + Assert.AreNotEqual(hashBytes[3], 0, "checksum 3 mismatch"); + Assert.AreEqual(hashBytes[4], 0, "checksum 4 mismatch"); + Assert.AreEqual(hashBytes[5], 0, "checksum 5 mismatch"); + Assert.AreEqual(hashBytes[6], 0, "checksum 6 mismatch"); + Assert.AreEqual(hashBytes[7], 0, "checksum 7 mismatch"); + + // The mask should have zeroes where the wildcard and ff at the end + var hashMaskBytes = GetBytes(topicHashMask); + Assert.AreEqual(hashMaskBytes[0], 0xff, "mask 0 mismatch"); + Assert.AreEqual(hashMaskBytes[1], 0xff, "mask 1 mismatch"); + Assert.AreEqual(hashMaskBytes[2], 0, "mask 2 mismatch"); + Assert.AreEqual(hashMaskBytes[3], 0xff, "mask 3 mismatch"); + Assert.AreEqual(hashMaskBytes[4], 0xff, "mask 4 mismatch"); + Assert.AreEqual(hashMaskBytes[5], 0xff, "mask 5 mismatch"); + Assert.AreEqual(hashMaskBytes[6], 0xff, "mask 6 mismatch"); + Assert.AreEqual(hashMaskBytes[7], 0xff, "mask 7 mismatch"); + } + + [TestMethod] + public void Match_Hash_Test_MultiWildCard() + { + var l0 = "pub0"; + var l1 = "topic1"; + var l2 = "sub1"; + var l3 = "#"; + var topic = string.Format("{0}/{1}/{2}/{3}", l0, l1, l2, l3); + + UInt64 topicHash; + UInt64 topicHashMask; + bool topicHasWildcard; + + MQTTnet.Server.MqttSubscription.CalculateTopicHash(topic, out topicHash, out topicHashMask, out topicHasWildcard); + + Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); + + var hashBytes = GetBytes(topicHash); + Assert.AreNotEqual(hashBytes[0], 0, "checksum 0 mismatch"); + Assert.AreNotEqual(hashBytes[1], 0, "checksum 1 mismatch"); + Assert.AreNotEqual(hashBytes[2], 0, "checksum 2 mismatch"); + Assert.AreEqual(hashBytes[3], 0, "checksum 3 mismatch"); + Assert.AreEqual(hashBytes[4], 0, "checksum 4 mismatch"); + Assert.AreEqual(hashBytes[5], 0, "checksum 5 mismatch"); + Assert.AreEqual(hashBytes[6], 0, "checksum 6 mismatch"); + Assert.AreEqual(hashBytes[7], 0, "checksum 7 mismatch"); + + // The mask should have zeroes where the wildcard is and zero onward + var hashMaskBytes = GetBytes(topicHashMask); + Assert.AreEqual(hashMaskBytes[0], 0xff, "mask 0 mismatch"); + Assert.AreEqual(hashMaskBytes[1], 0xff, "mask 1 mismatch"); + Assert.AreEqual(hashMaskBytes[2], 0xff, "mask 2 mismatch"); + Assert.AreEqual(hashMaskBytes[3], 0, "mask 3 mismatch"); + Assert.AreEqual(hashMaskBytes[4], 0, "mask 4 mismatch"); + Assert.AreEqual(hashMaskBytes[5], 0, "mask 5 mismatch"); + Assert.AreEqual(hashMaskBytes[6], 0, "mask 6 mismatch"); + Assert.AreEqual(hashMaskBytes[7], 0, "mask 7 mismatch"); + } + + [TestMethod] + public void Match_Hash_Test_NoWildCard() + { + var l0 = "pub0"; + var l1 = "topic1"; + var l2 = "sub1"; + var l3 = "prop1"; + var topic = string.Format("{0}/{1}/{2}/{3}", l0, l1, l2, l3); + + UInt64 topicHash; + UInt64 topicHashMask; + bool topicHasWildcard; + + MQTTnet.Server.MqttSubscription.CalculateTopicHash(topic, out topicHash, out topicHashMask, out topicHasWildcard); + + Assert.IsFalse(topicHasWildcard, "Wildcard detected when not wildcard present"); + + + var hashBytes = GetBytes(topicHash); + Assert.AreNotEqual(hashBytes[0], 0, "checksum 0 mismatch"); + Assert.AreNotEqual(hashBytes[1], 0, "checksum 1 mismatch"); + Assert.AreNotEqual(hashBytes[2], 0, "checksum 2 mismatch"); + Assert.AreNotEqual(hashBytes[3], 0, "checksum 3 mismatch"); + Assert.AreEqual(hashBytes[4], 0, "checksum 4 mismatch"); + Assert.AreEqual(hashBytes[5], 0, "checksum 5 mismatch"); + Assert.AreEqual(hashBytes[6], 0, "checksum 6 mismatch"); + Assert.AreEqual(hashBytes[7], 0, "checksum 7 mismatch"); + + // The mask should have ff + var hashMaskBytes = GetBytes(topicHashMask); + Assert.AreEqual(hashMaskBytes[0], 0xff, "mask 0 mismatch"); + Assert.AreEqual(hashMaskBytes[1], 0xff, "mask 1 mismatch"); + Assert.AreEqual(hashMaskBytes[2], 0xff, "mask 2 mismatch"); + Assert.AreEqual(hashMaskBytes[3], 0xff, "mask 3 mismatch"); + Assert.AreEqual(hashMaskBytes[4], 0xff, "mask 4 mismatch"); + Assert.AreEqual(hashMaskBytes[5], 0xff, "mask 5 mismatch"); + Assert.AreEqual(hashMaskBytes[6], 0xff, "mask 6 mismatch"); + Assert.AreEqual(hashMaskBytes[7], 0xff, "mask 7 mismatch"); + } + + /// + /// Test long topic name with last level being # + /// + [TestMethod] + public void Match_Hash_Test_LongTopic_MultiWildcard() + { + var sb = new StringBuilder(); + const int NumLevels = 8; + var levelNames = new string[NumLevels]; + for (var i = 0; i < NumLevels; ++i) + { + if (i > 0) + sb.Append("/"); + string levelName; + if (i == NumLevels - 1) + { + // last one is # + levelName = "#"; + } + else + { + levelName = "level" + i; + } + levelNames[i] = levelName; + sb.Append(levelName); + } + var topic = sb.ToString(); + + // UInt64 is limited to 8 levels + + UInt64 topicHash; + UInt64 topicHashMask; + bool topicHasWildcard; + + MQTTnet.Server.MqttSubscription.CalculateTopicHash(topic, out topicHash, out topicHashMask, out topicHasWildcard); + + Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); + + var hashBytes = GetBytes(topicHash); + // all bytes should contain checksum + int count = 0; + foreach (var h in hashBytes) + { + if (count < 7) + { + Assert.AreNotEqual(h, 0, "checksum mismatch"); + } + else + { + Assert.AreEqual(h, 0, "checksum mismatch"); + } + ++count; + } + // The mask should have ff except for last level + var hashMaskBytes = GetBytes(topicHashMask); + count = 0; + foreach (var h in hashMaskBytes) + { + if (count < 7) + { + Assert.AreEqual(h, 0xff, "mask mismatch"); + } + else + { + Assert.AreEqual(h, 0, "last mask mismatch"); + } + ++count; + } + } + + /// + /// Test long topic name with last level being + + /// + [TestMethod] + public void Match_Hash_Test_LongTopic_SingleWildCard() + { + var sb = new StringBuilder(); + const int NumLevels = 8; + var levelNames = new string[NumLevels]; + for (var i = 0; i < NumLevels; ++i) + { + if (i > 0) + sb.Append("/"); + string levelName; + if (i == NumLevels - 1) + { + // last one is + + levelName = "+"; + } + else + { + levelName = "level" + i; + } + levelNames[i] = levelName; + sb.Append(levelName); + } + var topic = sb.ToString(); + + UInt64 topicHash; + UInt64 topicHashMask; + bool topicHasWildcard; + + MQTTnet.Server.MqttSubscription.CalculateTopicHash(topic, out topicHash, out topicHashMask, out topicHasWildcard); + + Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); + + + var hashBytes = GetBytes(topicHash); + // all bytes should contain checksum + int count = 0; + foreach (var h in hashBytes) + { + if (count < 7) + { + Assert.AreNotEqual(h, 0, "checksum mismatch"); + } + else + { + // wildcard position + Assert.AreEqual(h, 0, "checksum mismatch"); + } + ++count; + } + // The mask should have ff + var hashMaskBytes = GetBytes(topicHashMask); + count = 0; + foreach (var h in hashMaskBytes) + { + if (count < 7) + { + Assert.AreEqual(h, 0xff, "mask mismatch"); + } + else + { + Assert.AreEqual(h, 0, "last mask mismatch"); + } + ++count; + } + } + + /// + /// Test long topic name exceeding 8 levels + /// + [TestMethod] + public void Match_Hash_Test_LongTopic_NoWildCard() + { + var sb = new StringBuilder(); + const int NumLevels = 9; + var levelNames = new string[NumLevels]; + for(var i=0; i < NumLevels; ++i) + { + if (i > 0) + sb.Append("/"); + var levelName = "level" + i; + levelNames[i] = levelName; + sb.Append(levelName); + } + var topic = sb.ToString(); + + UInt64 topicHash; + UInt64 topicHashMask; + bool topicHasWildcard; + + MQTTnet.Server.MqttSubscription.CalculateTopicHash(topic, out topicHash, out topicHashMask, out topicHasWildcard); + + Assert.IsFalse(topicHasWildcard, "Wildcard detected when not present"); + + + var hashBytes = GetBytes(topicHash); + // all bytes should contain checksum + int count = 0; + foreach(var h in hashBytes) + { + Assert.AreNotEqual(h, 0, "checksum mismatch"); + ++count; + } + // The mask should have ff + var hashMaskBytes = GetBytes(topicHashMask); + count = 0; + foreach (var h in hashMaskBytes) + { + Assert.AreEqual(h, 0xff, "mask mismatch"); + ++count; + } + } + + + /// + /// Test long topic name with last level being + + /// + [TestMethod] + public void Match_Hash_Test_LongTopic_DetectSingleLevelWildcard() + { + var topic = "asdfasdf/asdfasdf/asdfasdf/asdfasdf/asdfas/dfaf/assfdgsdfgdf/+"; + + UInt64 topicHash; + UInt64 topicHashMask; + bool topicHasWildcard; + + MQTTnet.Server.MqttSubscription.CalculateTopicHash(topic, out topicHash, out topicHashMask, out topicHasWildcard); + + Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); + } + + /// + /// Test long topic name with last level being # + /// + [TestMethod] + public void Match_Hash_Test_LongTopic_DetectMultiLevelWildcard() + { + var topic = "asdfasdf/asdfasdf/asdfasdf/asdfasdf/asdfas/dfaf/assfdgsdfgdf/#"; + + UInt64 topicHash; + UInt64 topicHashMask; + bool topicHasWildcard; + + MQTTnet.Server.MqttSubscription.CalculateTopicHash(topic, out topicHash, out topicHashMask, out topicHasWildcard); + + Assert.IsTrue(topicHasWildcard, "Wildcard not detected"); + } + + [TestMethod] + public async Task Match_Hash_Test_Search_NoWildcard() + { + var topics = await PrepareTopicHashSubscriptions(TopicHashSelector.NoWildcard); + + // all match lookup + var matchCount = CheckTopicSubscriptions(_clientSession, topics, "No Wildcard All Match"); + + Assert.AreEqual(topics.Count, matchCount, "Not all topics matched"); + + // no match lookup + { + { + var topicsToFind = new List(); + foreach (var t in topics) + { + topicsToFind.Add(t + "x"); + } + + matchCount = CheckTopicSubscriptions(_clientSession, topicsToFind, "No Wildcard No Match (Append X)"); + + Assert.AreEqual(0, matchCount, "Topic match count not zero"); + } + + { + var topicsToFind = new List(); + foreach (var t in topics) + { + // replace last letter with x + topicsToFind.Add(t.Substring(0, t.Length - 1) + "x"); + } + + matchCount = CheckTopicSubscriptions(_clientSession, topicsToFind, "No Wildcard No Match (Replace X)"); + + Assert.AreEqual(0, matchCount, "Topic match count not zero"); + } + } + } + + [TestMethod] + public async Task Match_Hash_Test_Search_SingleWildcard() + { + var topics = await PrepareTopicHashSubscriptions(TopicHashSelector.SingleWildcard); + var matchCount = CheckTopicSubscriptions(_clientSession, topics, "Single Wildcard"); + // Should match all topics + Assert.AreEqual(topics.Count, matchCount, "Topics not matched"); + } + + [TestMethod] + public async Task Match_Hash_Test_Search_MultiWildcard() + { + var topics = await PrepareTopicHashSubscriptions(TopicHashSelector.MultiWildcard); + var matchCount = CheckTopicSubscriptions(_clientSession, topics, "Multi Wildcard"); + // Should match all topics + Assert.AreEqual(topics.Count, matchCount, "Topics not matched"); + } + + /// + /// Even fairly regularly named topics as generated by the topic generator should result in shallow hash buckets + /// + /// + [TestMethod] + public void Check_Hash_Bucket_Depth() + { + Dictionary> topicsByPublisher; + Dictionary> singleWildcardTopicsByPublisher; + Dictionary> multiWildcardTopicsByPublisher; + + const int NumPublishers = 5000; + const int NumTopicsPerPublisher = 10; + + TopicGenerator.Generate(NumPublishers, NumTopicsPerPublisher, out topicsByPublisher, out singleWildcardTopicsByPublisher, out multiWildcardTopicsByPublisher); + + // There will be many 'similar' topics ending with, i.e. "sensor100", "sensor101", ... + // Hash bucket depths should remain low. + var bucketDepths = new Dictionary(); + int maxBucketDepth = 0; + UInt64 maxBucketDepthHash = 0; + + var topicsByHash = new Dictionary>(); + + foreach (var t in topicsByPublisher) + { + var topics = t.Value; + foreach (var topic in topics) + { + UInt64 topicHash; + UInt64 hashMask; + bool hasWildcard; + MQTTnet.Server.MqttSubscription.CalculateTopicHash(topic, out topicHash, out hashMask, out hasWildcard); + + bucketDepths.TryGetValue(topicHash, out var currentValue); + ++currentValue; + bucketDepths[topicHash] = currentValue; + + if (currentValue > maxBucketDepth) + { + maxBucketDepth = currentValue; + maxBucketDepthHash = topicHash; + } + + if (!topicsByHash.TryGetValue(topicHash, out var topicList)) + { + topicList = new List(); + topicsByHash.Add(topicHash, topicList); + } + topicList.Add(topic); + } + } + + var maxDepthTopics = topicsByHash[maxBucketDepthHash]; + + Console.Write("Max bucket depth is " + maxBucketDepth); + + // for the test case the bucket depth should be less than 100 + Assert.IsTrue(maxBucketDepth < 100, "Unexpected high topic hash bucket depth"); + } + + + [TestMethod] + public void Check_Selected_Topic_Hashes() + { + CheckTopicHash("client1/building1/level1/sensor1", 0x655D4AF100000000, 0xFFFFFFFFFFFFFFFF); + CheckTopicHash("client1/building1/+/sensor1", 0x655D00F100000000, 0xFFFF00FFFFFFFFFF); + CheckTopicHash("client1/+/level1/+", 0x65004A0000000000, 0xFF00FF00FFFFFFFF); + CheckTopicHash("client1/building1/level1/#", 0x655D4A0000000000, 0xFFFFFF0000000000); + CheckTopicHash("client1/+/level1/#", 0x65004A0000000000, 0xFF00FF0000000000); + } + + void CheckTopicHash(string topic, ulong expectedHash, ulong expectedHashMask) + { + ulong topicHash; + ulong hashMask; + bool hasWildcard; + + MQTTnet.Server.MqttSubscription.CalculateTopicHash(topic, out topicHash, out hashMask, out hasWildcard); + + Console.WriteLine(); + Console.WriteLine("Topic: " + topic); + Console.WriteLine(string.Format("Hash: {0:X8}", topicHash)); + Console.WriteLine(string.Format("Hash Mask: {0:X8}", hashMask)); + + Assert.AreEqual(expectedHash, topicHash, "Topic hash not as expected. Has the hash function changed?"); + Assert.AreEqual(expectedHashMask, hashMask, "Topic hash mask not as expected"); + } + + + async Task> PrepareTopicHashSubscriptions(TopicHashSelector selector) + { + Dictionary> topicsByPublisher; + Dictionary> singleWildcardTopicsByPublisher; + Dictionary> multiWildcardTopicsByPublisher; + + const int NumPublishers = 1; + const int NumTopicsPerPublisher = 10000; + + TopicGenerator.Generate(NumPublishers, NumTopicsPerPublisher, out topicsByPublisher, out singleWildcardTopicsByPublisher, out multiWildcardTopicsByPublisher); + + var topics = topicsByPublisher.FirstOrDefault().Value; + var singleWildcardTopics = singleWildcardTopicsByPublisher.FirstOrDefault().Value; + var multiWildcardTopics = multiWildcardTopicsByPublisher.FirstOrDefault().Value; + + const string ClientId = "Client1"; + var logger = new Mockups.TestLogger(); + var serverOptions = new MQTTnet.Server.MqttServerOptions(); + var eventContainer = new MQTTnet.Server.MqttServerEventContainer(); + var retainedMessagesManager = new MqttRetainedMessagesManager(eventContainer, logger); + var sessionManager = new MqttClientSessionsManager(serverOptions, retainedMessagesManager, eventContainer, logger); + _clientSession = new MQTTnet.Server.MqttSession( + ClientId, + false, + new Dictionary(), + serverOptions, + eventContainer, + retainedMessagesManager, + sessionManager + ); + + List topicsToSubscribe; + + switch (selector) + { + case TopicHashSelector.SingleWildcard: + topicsToSubscribe = singleWildcardTopics; + break; + case TopicHashSelector.MultiWildcard: + topicsToSubscribe = multiWildcardTopics; + break; + default: + topicsToSubscribe = topics; + break; + } + foreach (var t in topicsToSubscribe) + { + var subPacket = new Packets.MqttSubscribePacket(); + var filter = new Packets.MqttTopicFilter(); + filter.Topic = t; + subPacket.TopicFilters.Add(filter); + await _clientSession.SubscriptionsManager.Subscribe(subPacket, default(CancellationToken)); + } + + return topics; + } + + int CheckTopicSubscriptions(MQTTnet.Server.MqttSession clientSession, List topicsToFind, string subject) + { + var matchCount = 0; + + { + int resultCount = 0; + + var stopWatch = new System.Diagnostics.Stopwatch(); + stopWatch.Start(); + var countUp = 0; + var countDown = topicsToFind.Count - 1; + for (; countUp < topicsToFind.Count; ++countUp, --countDown) + { + var topicToFind = topicsToFind[countDown]; + + UInt64 topicHash; + UInt64 hashMask; + bool hasWildcard; + MQTTnet.Server.MqttSubscription.CalculateTopicHash((string)topicToFind, out topicHash, out hashMask, out hasWildcard); + + var result = clientSession.SubscriptionsManager.CheckSubscriptions((string)topicToFind, topicHash, MqttQualityOfServiceLevel.AtMostOnce, "OtherClient"); + if (result.IsSubscribed) + { + ++matchCount; + } + ++resultCount; + } + + stopWatch.Stop(); + + Console.Write("Match count: " + matchCount + "; "); + + Console.WriteLine(subject + " lookup milliseconds: " + stopWatch.ElapsedMilliseconds); + } + + return matchCount; + } + + byte[] GetBytes(UInt64 value) + { + var bytes = BitConverter.GetBytes(value); + // Ensure that highest byte comes first for comparison left to right + if (BitConverter.IsLittleEndian) + return bytes.Reverse().ToArray(); + return bytes; + } + } +} diff --git a/Tests/MQTTnet.Core.Tests/Server/Topic_Alias_Tests.cs b/Source/MQTTnet.Tests/Server/Topic_Alias_Tests.cs similarity index 84% rename from Tests/MQTTnet.Core.Tests/Server/Topic_Alias_Tests.cs rename to Source/MQTTnet.Tests/Server/Topic_Alias_Tests.cs index ba5e6f6..b15c3b1 100644 --- a/Tests/MQTTnet.Core.Tests/Server/Topic_Alias_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Topic_Alias_Tests.cs @@ -1,10 +1,14 @@ -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; -using MQTTnet.Client.Options; using MQTTnet.Formatter; +using MQTTnet.Implementations; namespace MQTTnet.Tests.Server { @@ -39,13 +43,15 @@ namespace MQTTnet.Tests.Server var receivedTopics = new List(); var c1 = await testEnvironment.ConnectClient(options => options.WithProtocolVersion(MqttProtocolVersion.V500)); - c1.UseApplicationMessageReceivedHandler(e => + c1.ApplicationMessageReceivedAsync += e => { lock (receivedTopics) { receivedTopics.Add(e.ApplicationMessage.Topic); } - }); + + return PlatformAbstractionLayer.CompletedTask; + }; await c1.SubscribeAsync("#"); diff --git a/Tests/MQTTnet.Core.Tests/Server/User_Properties_Tests.cs b/Source/MQTTnet.Tests/Server/User_Properties_Tests.cs similarity index 78% rename from Tests/MQTTnet.Core.Tests/Server/User_Properties_Tests.cs rename to Source/MQTTnet.Tests/Server/User_Properties_Tests.cs index fc30591..27f3e72 100644 --- a/Tests/MQTTnet.Core.Tests/Server/User_Properties_Tests.cs +++ b/Source/MQTTnet.Tests/Server/User_Properties_Tests.cs @@ -1,11 +1,15 @@ -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; -using MQTTnet.Client.Options; -using MQTTnet.Client.Subscribing; using MQTTnet.Formatter; +using MQTTnet.Implementations; +using MQTTnet.Packets; using MQTTnet.Tests.Mockups; namespace MQTTnet.Tests.Server @@ -41,7 +45,11 @@ namespace MQTTnet.Tests.Server }, CancellationToken.None); MqttApplicationMessage receivedMessage = null; - receiver.UseApplicationMessageReceivedHandler(e => receivedMessage = e.ApplicationMessage); + receiver.ApplicationMessageReceivedAsync += e => + { + receivedMessage = e.ApplicationMessage; + return PlatformAbstractionLayer.CompletedTask; + }; await sender.PublishAsync(message.Build(), CancellationToken.None); diff --git a/Tests/MQTTnet.Core.Tests/Server/Wildcard_Subscription_Available_Tests.cs b/Source/MQTTnet.Tests/Server/Wildcard_Subscription_Available_Tests.cs similarity index 87% rename from Tests/MQTTnet.Core.Tests/Server/Wildcard_Subscription_Available_Tests.cs rename to Source/MQTTnet.Tests/Server/Wildcard_Subscription_Available_Tests.cs index 1b2abe1..e62f1e8 100644 --- a/Tests/MQTTnet.Core.Tests/Server/Wildcard_Subscription_Available_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Wildcard_Subscription_Available_Tests.cs @@ -1,4 +1,8 @@ -using System.Threading.Tasks; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MQTTnet.Client; using MQTTnet.Formatter; diff --git a/Source/MQTTnet.Tests/TopicFilterComparer_Tests.cs b/Source/MQTTnet.Tests/TopicFilterComparer_Tests.cs new file mode 100644 index 0000000..60be436 --- /dev/null +++ b/Source/MQTTnet.Tests/TopicFilterComparer_Tests.cs @@ -0,0 +1,173 @@ +// 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 Microsoft.VisualStudio.TestTools.UnitTesting; +using MQTTnet.Server; + +namespace MQTTnet.Tests +{ + [TestClass] + public sealed class MqttTopicFilterComparer_Tests + { + [TestMethod] + public void AllLevelsWildcardMatch() + { + CompareAndAssert("A/B/C/D", "#", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void BeginningOneLevelWildcardMatch() + { + CompareAndAssert("A/B/C", "+/B/C", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void Compare_UTF8_String_Match() + { + CompareAndAssert("öäüß", "öäüß", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void Compare_UTF8_String_No_Match() + { + CompareAndAssert("ae", "ä", MqttTopicFilterCompareResult.NoMatch); + } + + [TestMethod] + public void DirectMatch() + { + CompareAndAssert("A/B/C", "A/B/C", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void DirectNoMatch() + { + CompareAndAssert("A/B/X", "A/B/C", MqttTopicFilterCompareResult.NoMatch); + } + + [TestMethod] + public void EndMultipleLevelsWildcardMatch() + { + CompareAndAssert("A/B/C", "A/#", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void EndMultipleLevelsWildcardMatchEmptyLevel() + { + CompareAndAssert("A/", "A/#", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void EndMultipleLevelsWildcardNoMatch() + { + CompareAndAssert("A/B/C/D", "A/C/#", MqttTopicFilterCompareResult.NoMatch); + } + + [TestMethod] + public void EndOneLevelWildcardMatch() + { + CompareAndAssert("A/B/C", "A/B/+", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void Hash_Match_With_Separator_Only() + { + //A Topic Name or Topic Filter consisting only of the ‘/’ character is valid + CompareAndAssert("/", "#", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void MiddleOneLevelWildcardMatch() + { + CompareAndAssert("A/B/C", "A/+/C", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void MiddleOneLevelWildcardNoMatch() + { + CompareAndAssert("A/B/C/D", "A/+/C", MqttTopicFilterCompareResult.NoMatch); + } + + [TestMethod] + public void MultiLevel_Sport() + { + // Tests from official MQTT spec (4.7.1.2 Multi-level wildcard) + CompareAndAssert("sport/tennis/player1", "sport/tennis/player1/#", MqttTopicFilterCompareResult.IsMatch); + CompareAndAssert("sport/tennis/player1/ranking", "sport/tennis/player1/#", MqttTopicFilterCompareResult.IsMatch); + CompareAndAssert("sport/tennis/player1/score/wimbledon", "sport/tennis/player1/#", MqttTopicFilterCompareResult.IsMatch); + + CompareAndAssert("sport/tennis/player1", "sport/tennis/+", MqttTopicFilterCompareResult.IsMatch); + CompareAndAssert("sport/tennis/player2", "sport/tennis/+", MqttTopicFilterCompareResult.IsMatch); + CompareAndAssert("sport/tennis/player1/ranking", "sport/tennis/+", MqttTopicFilterCompareResult.NoMatch); + + CompareAndAssert("sport", "sport/#", MqttTopicFilterCompareResult.IsMatch); + CompareAndAssert("sport", "sport/+", MqttTopicFilterCompareResult.NoMatch); + CompareAndAssert("sport/", "sport/+", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void Plus_Match_With_Separator_Only() + { + //A Topic Name or Topic Filter consisting only of the ‘/’ character is valid + CompareAndAssert("A", "+", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void Reserved_Multi_Level_Wildcard_Only() + { + CompareAndAssert("$SPECIAL/TOPIC", "#", MqttTopicFilterCompareResult.NoMatch); + } + + [TestMethod] + public void Reserved_Single_Level_Wildcard() + { + CompareAndAssert("$SYS/monitor/Clients", "$SYS/#", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void Reserved_Single_Level_Wildcard_Prefix() + { + CompareAndAssert("$SYS/monitor/Clients", "+/monitor/Clients", MqttTopicFilterCompareResult.NoMatch); + } + + [TestMethod] + public void Reserved_Single_Level_Wildcard_Suffix() + { + CompareAndAssert("$SYS/monitor/Clients", "$SYS/monitor/+", MqttTopicFilterCompareResult.IsMatch); + } + + [TestMethod] + public void SingleLevel_Finance() + { + // Tests from official MQTT spec (4.7.1.3 Single level wildcard) + CompareAndAssert("/finance", "+/+", MqttTopicFilterCompareResult.IsMatch); + CompareAndAssert("/finance", "/+", MqttTopicFilterCompareResult.IsMatch); + CompareAndAssert("/finance", "+", MqttTopicFilterCompareResult.NoMatch); + } + + static void CompareAndAssert(string topic, string filter, MqttTopicFilterCompareResult expectedResult) + { + Assert.AreEqual(expectedResult, MqttTopicFilterComparer.Compare(topic, filter)); + + ulong topicHash; + ulong topicHashMask; + bool topicHasWildcard; + + MqttSubscription.CalculateTopicHash(topic, out topicHash, out topicHashMask, out topicHasWildcard); + + + ulong filterTopicHash; + ulong filterTopicHashMask; + bool filterTopicHasWildcard; + + MqttSubscription.CalculateTopicHash(filter, out filterTopicHash, out filterTopicHashMask, out filterTopicHasWildcard); + + if (expectedResult == MqttTopicFilterCompareResult.IsMatch) + { + // If it matches then the hash evaluation should also indicate a match + Assert.IsTrue((topicHash & filterTopicHashMask) == filterTopicHash, "Incorrect topic hash (is equal)"); + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.Tests/TopicGenerator.cs b/Source/MQTTnet.Tests/TopicGenerator.cs new file mode 100644 index 0000000..64ae114 --- /dev/null +++ b/Source/MQTTnet.Tests/TopicGenerator.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MQTTnet.Tests.Server +{ + public class TopicGenerator + { + public static void Generate( + int numPublishers, int numTopicsPerPublisher, + out Dictionary> topicsByPublisher, + out Dictionary> singleWildcardTopicsByPublisher, + out Dictionary> multiWildcardTopicsByPublisher + ) + { + topicsByPublisher = new Dictionary>(); + singleWildcardTopicsByPublisher = new Dictionary>(); + multiWildcardTopicsByPublisher = new Dictionary>(); + + // Find some reasonable distribution across three topic levels + var topicsPerLevel = (int)Math.Pow(numTopicsPerPublisher, (1.0 / 3.0)); + if (topicsPerLevel <= 0) + { + topicsPerLevel = 1; + } + + int numLevel1Topics = topicsPerLevel; + int numLevel2Topics = topicsPerLevel; + + var maxNumLevel3Topics = 1 + (int)((double)numTopicsPerPublisher / numLevel1Topics / numLevel2Topics); + if (maxNumLevel3Topics <= 0) + { + maxNumLevel3Topics = 1; + } + + for (var p = 0; p < numPublishers; ++p) + { + int publisherTopicCount = 0; + var publisherName = "pub" + p; + for (var l1 = 0; l1 < numLevel1Topics; ++l1) + { + for (var l2 = 0; l2 < numLevel2Topics; ++l2) + { + for (var l3 = 0; l3 < maxNumLevel3Topics; ++l3) + { + if (publisherTopicCount >= numTopicsPerPublisher) + break; + + var topic = string.Format("{0}/building{1}/level{2}/sensor{3}", publisherName, l1 + 1, l2 + 1, l3 + 1); + AddPublisherTopic(publisherName, topic, topicsByPublisher); + + if (l2 == 0) + { + var singleWildcardTopic = string.Format("{0}/building{1}/+/sensor{2}", publisherName, l1 + 1, l3 + 1); + AddPublisherTopic(publisherName, singleWildcardTopic, singleWildcardTopicsByPublisher); + } + if ((l1 == 0) && (l3 == 0)) + { + var multiWildcardTopic = string.Format("{0}/+/level{1}/+", publisherName, l2 + 1); + AddPublisherTopic(publisherName, multiWildcardTopic, multiWildcardTopicsByPublisher); + } + + ++publisherTopicCount; + } + } + } + } + } + + static void AddPublisherTopic(string publisherName, string topic, Dictionary> topicsByPublisher) + { + List topicList; + if (!topicsByPublisher.TryGetValue(publisherName, out topicList)) + { + topicList = new List(); + topicsByPublisher.Add(publisherName, topicList); + } + topicList.Add(topic); + } + } +} diff --git a/Source/MQTTnet/Adapter/IMqttChannelAdapter.cs b/Source/MQTTnet/Adapter/IMqttChannelAdapter.cs index c6026a5..88930ee 100644 --- a/Source/MQTTnet/Adapter/IMqttChannelAdapter.cs +++ b/Source/MQTTnet/Adapter/IMqttChannelAdapter.cs @@ -1,4 +1,8 @@ -using System; +// 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.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; @@ -23,13 +27,13 @@ namespace MQTTnet.Adapter bool IsReadingPacket { get; } - Task ConnectAsync(TimeSpan timeout, CancellationToken cancellationToken); + Task ConnectAsync(CancellationToken cancellationToken); - Task DisconnectAsync(TimeSpan timeout, CancellationToken cancellationToken); + Task DisconnectAsync(CancellationToken cancellationToken); - Task SendPacketAsync(MqttBasePacket packet, CancellationToken cancellationToken); + Task SendPacketAsync(MqttPacket packet, CancellationToken cancellationToken); - Task ReceivePacketAsync(CancellationToken cancellationToken); + Task ReceivePacketAsync(CancellationToken cancellationToken); void ResetStatistics(); } diff --git a/Source/MQTTnet/Adapter/IMqttClientAdapterFactory.cs b/Source/MQTTnet/Adapter/IMqttClientAdapterFactory.cs index 9d0e599..85f7a74 100644 --- a/Source/MQTTnet/Adapter/IMqttClientAdapterFactory.cs +++ b/Source/MQTTnet/Adapter/IMqttClientAdapterFactory.cs @@ -1,9 +1,14 @@ -using MQTTnet.Client.Options; +// 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.Client; +using MQTTnet.Diagnostics; namespace MQTTnet.Adapter { public interface IMqttClientAdapterFactory { - IMqttChannelAdapter CreateClientAdapter(IMqttClientOptions options); + IMqttChannelAdapter CreateClientAdapter(MqttClientOptions options, MqttPacketInspector packetInspector, IMqttNetLogger logger); } } diff --git a/Source/MQTTnet/Adapter/IMqttServerAdapter.cs b/Source/MQTTnet/Adapter/IMqttServerAdapter.cs index 52cda51..3284305 100644 --- a/Source/MQTTnet/Adapter/IMqttServerAdapter.cs +++ b/Source/MQTTnet/Adapter/IMqttServerAdapter.cs @@ -1,5 +1,10 @@ -using System; +// 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.Threading.Tasks; +using MQTTnet.Diagnostics; using MQTTnet.Server; namespace MQTTnet.Adapter @@ -8,7 +13,7 @@ namespace MQTTnet.Adapter { Func ClientHandler { get; set; } - Task StartAsync(IMqttServerOptions options); + Task StartAsync(MqttServerOptions options, IMqttNetLogger logger); Task StopAsync(); } } diff --git a/Source/MQTTnet/Adapter/MqttChannelAdapter.cs b/Source/MQTTnet/Adapter/MqttChannelAdapter.cs index 8ca0996..121c4cd 100644 --- a/Source/MQTTnet/Adapter/MqttChannelAdapter.cs +++ b/Source/MQTTnet/Adapter/MqttChannelAdapter.cs @@ -1,9 +1,7 @@ -using MQTTnet.Channel; -using MQTTnet.Diagnostics; -using MQTTnet.Exceptions; -using MQTTnet.Formatter; -using MQTTnet.Internal; -using MQTTnet.Packets; +// 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.IO; using System.Net.Sockets; @@ -11,8 +9,13 @@ using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; -using MQTTnet.Diagnostics.Logger; -using MQTTnet.Diagnostics.PacketInspection; +using MQTTnet.Channel; +using MQTTnet.Diagnostics; +using MQTTnet.Exceptions; +using MQTTnet.Formatter; +using MQTTnet.Implementations; +using MQTTnet.Internal; +using MQTTnet.Packets; namespace MQTTnet.Adapter { @@ -20,152 +23,111 @@ namespace MQTTnet.Adapter { const uint ErrorOperationAborted = 0x800703E3; const int ReadBufferSize = 4096; - - readonly byte[] _singleByteBuffer = new byte[1]; + + readonly IMqttChannel _channel; readonly byte[] _fixedHeaderBuffer = new byte[2]; - - readonly MqttPacketInspectorHandler _packetInspectorHandler; readonly MqttNetSourceLogger _logger; - readonly IMqttChannel _channel; + + readonly MqttPacketInspector _packetInspector; + + readonly byte[] _singleByteBuffer = new byte[1]; readonly AsyncLock _syncRoot = new AsyncLock(); long _bytesReceived; long _bytesSent; - public MqttChannelAdapter(IMqttChannel channel, MqttPacketFormatterAdapter packetFormatterAdapter, IMqttPacketInspector packetInspector, IMqttNetLogger logger) + public MqttChannelAdapter(IMqttChannel channel, MqttPacketFormatterAdapter packetFormatterAdapter, MqttPacketInspector packetInspector, IMqttNetLogger logger) { _channel = channel ?? throw new ArgumentNullException(nameof(channel)); + _packetInspector = packetInspector; + PacketFormatterAdapter = packetFormatterAdapter ?? throw new ArgumentNullException(nameof(packetFormatterAdapter)); - _packetInspectorHandler = new MqttPacketInspectorHandler(packetInspector, logger); + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } - if (logger == null) throw new ArgumentNullException(nameof(logger)); _logger = logger.WithSource(nameof(MqttChannelAdapter)); } - public string Endpoint => _channel.Endpoint; + public long BytesReceived => Interlocked.Read(ref _bytesReceived); - public bool IsSecureConnection => _channel.IsSecureConnection; + public long BytesSent => Interlocked.Read(ref _bytesSent); public X509Certificate2 ClientCertificate => _channel.ClientCertificate; - public MqttPacketFormatterAdapter PacketFormatterAdapter { get; } + public string Endpoint => _channel.Endpoint; - public long BytesSent => Interlocked.Read(ref _bytesSent); + public bool IsReadingPacket { get; private set; } - public long BytesReceived => Interlocked.Read(ref _bytesReceived); + public bool IsSecureConnection => _channel.IsSecureConnection; - public bool IsReadingPacket { get; private set; } + public MqttPacketFormatterAdapter PacketFormatterAdapter { get; } - public async Task ConnectAsync(TimeSpan timeout, CancellationToken cancellationToken) + public async Task ConnectAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); try { - if (timeout == TimeSpan.Zero) - { - await _channel.ConnectAsync(cancellationToken).ConfigureAwait(false); - } - else - { - await MqttTaskTimeout.WaitAsync(t => _channel.ConnectAsync(t), timeout, cancellationToken).ConfigureAwait(false); - } + await _channel.ConnectAsync(cancellationToken).ConfigureAwait(false); } catch (Exception exception) { - if (IsWrappedException(exception)) + if (!WrapAndThrowException(exception)) { throw; } - - WrapAndThrowException(exception); } } - public async Task DisconnectAsync(TimeSpan timeout, CancellationToken cancellationToken) + public async Task DisconnectAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); try { - if (timeout == TimeSpan.Zero) - { - await _channel.DisconnectAsync(cancellationToken).ConfigureAwait(false); - } - else - { - await MqttTaskTimeout.WaitAsync( - t => _channel.DisconnectAsync(t), timeout, cancellationToken).ConfigureAwait(false); - } + await _channel.DisconnectAsync(cancellationToken).ConfigureAwait(false); } catch (Exception exception) { - if (IsWrappedException(exception)) + if (!WrapAndThrowException(exception)) { throw; } - - WrapAndThrowException(exception); } } - public async Task SendPacketAsync(MqttBasePacket packet, CancellationToken cancellationToken) + public async Task ReceivePacketAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - using (await _syncRoot.WaitAsync(cancellationToken).ConfigureAwait(false)) + try { - // Check for cancellation here again because "WaitAsync" might take some time. - cancellationToken.ThrowIfCancellationRequested(); + _packetInspector?.BeginReceivePacket(); - try + ReceivedMqttPacket receivedPacket; + var receivedPacketTask = ReceiveAsync(cancellationToken); + if (receivedPacketTask.IsCompleted) { - var packetData = PacketFormatterAdapter.Encode(packet); - _packetInspectorHandler.BeginSendPacket(packetData); - - await _channel.WriteAsync(packetData.Array, packetData.Offset, packetData.Count, cancellationToken).ConfigureAwait(false); - - Interlocked.Add(ref _bytesReceived, packetData.Count); - - _logger.Verbose("TX ({0} bytes) >>> {1}", packetData.Count, packet); - } - catch (Exception exception) - { - if (IsWrappedException(exception)) - { - throw; - } - - WrapAndThrowException(exception); + receivedPacket = receivedPacketTask.Result; } - finally + else { - PacketFormatterAdapter.FreeBuffer(); + receivedPacket = await receivedPacketTask.ConfigureAwait(false); } - } - } - - public async Task ReceivePacketAsync(CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - - try - { - _packetInspectorHandler.BeginReceivePacket(); - var receivedPacket = await ReceiveAsync(cancellationToken).ConfigureAwait(false); - if (receivedPacket == null || cancellationToken.IsCancellationRequested) + if (receivedPacket.TotalLength == 0 || cancellationToken.IsCancellationRequested) { return null; } - _packetInspectorHandler.EndReceivePacket(); + _packetInspector?.EndReceivePacket(); Interlocked.Add(ref _bytesSent, receivedPacket.TotalLength); @@ -192,12 +154,10 @@ namespace MQTTnet.Adapter } catch (Exception exception) { - if (IsWrappedException(exception)) + if (!WrapAndThrowException(exception)) { throw; } - - WrapAndThrowException(exception); } return null; @@ -209,6 +169,48 @@ namespace MQTTnet.Adapter Interlocked.Exchange(ref _bytesSent, 0L); } + public async Task SendPacketAsync(MqttPacket packet, CancellationToken cancellationToken) + { + ThrowIfDisposed(); + + // This lock makes sure that multiple threads can send packets at the same time. + // This is required when a disconnect is sent from another thread while the + // worker thread is still sending publish packets etc. + using (await _syncRoot.WaitAsync(cancellationToken).ConfigureAwait(false)) + { + // Check for cancellation here again because "WaitAsync" might take some time. + cancellationToken.ThrowIfCancellationRequested(); + + try + { + var packetBuffer = PacketFormatterAdapter.Encode(packet); + _packetInspector?.BeginSendPacket(packetBuffer); + + _logger.Verbose("TX ({0} bytes) >>> {1}", packetBuffer.Length, packet); + + await _channel.WriteAsync(packetBuffer.Packet.Array, packetBuffer.Packet.Offset, packetBuffer.Packet.Count, cancellationToken).ConfigureAwait(false); + + if (packetBuffer.Payload.Count > 0) + { + await _channel.WriteAsync(packetBuffer.Payload.Array, packetBuffer.Payload.Offset, packetBuffer.Payload.Count, cancellationToken).ConfigureAwait(false); + } + + Interlocked.Add(ref _bytesReceived, packetBuffer.Length); + } + catch (Exception exception) + { + if (!WrapAndThrowException(exception)) + { + throw; + } + } + finally + { + PacketFormatterAdapter.Cleanup(); + } + } + } + protected override void Dispose(bool disposing) { if (disposing) @@ -220,73 +222,47 @@ namespace MQTTnet.Adapter base.Dispose(disposing); } - async Task ReceiveAsync(CancellationToken cancellationToken) + async Task ReadBodyLengthAsync(byte initialEncodedByte, CancellationToken cancellationToken) { - if (cancellationToken.IsCancellationRequested) - { - return null; - } - - var readFixedHeaderResult = await ReadFixedHeaderAsync(cancellationToken).ConfigureAwait(false); - - if (cancellationToken.IsCancellationRequested) - { - return null; - } - - if (readFixedHeaderResult.ConnectionClosed) - { - return null; - } + var offset = 0; + var multiplier = 128; + var value = initialEncodedByte & 127; + int encodedByte = initialEncodedByte; - try + while ((encodedByte & 128) != 0) { - IsReadingPacket = true; - - var fixedHeader = readFixedHeaderResult.FixedHeader; - if (fixedHeader.RemainingLength == 0) + offset++; + if (offset > 3) { - return new ReceivedMqttPacket(fixedHeader.Flags, new MqttPacketBodyReader(new byte[0], 0, 0), 2); + throw new MqttProtocolViolationException("Remaining length is invalid."); } - var bodyLength = fixedHeader.RemainingLength; - var body = new byte[bodyLength]; - - var bodyOffset = 0; - var chunkSize = Math.Min(ReadBufferSize, bodyLength); - - do + if (cancellationToken.IsCancellationRequested) { - var bytesLeft = body.Length - bodyOffset; - if (chunkSize > bytesLeft) - { - chunkSize = bytesLeft; - } + return -1; + } - var readBytes = await _channel.ReadAsync(body, bodyOffset, chunkSize, cancellationToken).ConfigureAwait(false); + var readCount = await _channel.ReadAsync(_singleByteBuffer, 0, 1, cancellationToken).ConfigureAwait(false); - if (cancellationToken.IsCancellationRequested) - { - return null; - } + if (cancellationToken.IsCancellationRequested) + { + return -1; + } - if (readBytes == 0) - { - return null; - } + if (readCount == 0) + { + return -1; + } - bodyOffset += readBytes; - } while (bodyOffset < bodyLength); + _packetInspector?.FillReceiveBuffer(_singleByteBuffer); - _packetInspectorHandler.FillReceiveBuffer(body); + encodedByte = _singleByteBuffer[0]; - var bodyReader = new MqttPacketBodyReader(body, 0, bodyLength); - return new ReceivedMqttPacket(fixedHeader.Flags, bodyReader, fixedHeader.TotalLength); - } - finally - { - IsReadingPacket = false; + value += (encodedByte & 127) * multiplier; + multiplier *= 128; } + + return value; } async Task ReadFixedHeaderAsync(CancellationToken cancellationToken) @@ -302,21 +278,18 @@ namespace MQTTnet.Adapter if (cancellationToken.IsCancellationRequested) { - return null; + return ReadFixedHeaderResult.Cancelled; } if (bytesRead == 0) { - return new ReadFixedHeaderResult - { - ConnectionClosed = true - }; + return ReadFixedHeaderResult.ConnectionClosed; } totalBytesRead += bytesRead; } - _packetInspectorHandler.FillReceiveBuffer(buffer); + _packetInspector?.FillReceiveBuffer(buffer); var hasRemainingLength = buffer[1] != 0; if (!hasRemainingLength) @@ -329,74 +302,98 @@ namespace MQTTnet.Adapter var bodyLength = await ReadBodyLengthAsync(buffer[1], cancellationToken).ConfigureAwait(false); - if (!bodyLength.HasValue) + if (bodyLength == -1) { return new ReadFixedHeaderResult { - ConnectionClosed = true + IsConnectionClosed = true }; } - totalBytesRead += bodyLength.Value; + totalBytesRead += bodyLength; return new ReadFixedHeaderResult { - FixedHeader = new MqttFixedHeader(buffer[0], bodyLength.Value, totalBytesRead) + FixedHeader = new MqttFixedHeader(buffer[0], bodyLength, totalBytesRead) }; } - async Task ReadBodyLengthAsync(byte initialEncodedByte, CancellationToken cancellationToken) + async Task ReceiveAsync(CancellationToken cancellationToken) { - var offset = 0; - var multiplier = 128; - var value = initialEncodedByte & 127; - int encodedByte = initialEncodedByte; + if (cancellationToken.IsCancellationRequested) + { + return ReceivedMqttPacket.Empty; + } - while ((encodedByte & 128) != 0) + var readFixedHeaderResult = await ReadFixedHeaderAsync(cancellationToken).ConfigureAwait(false); + + if (cancellationToken.IsCancellationRequested) { - offset++; - if (offset > 3) - { - throw new MqttProtocolViolationException("Remaining length is invalid."); - } + return ReceivedMqttPacket.Empty; + } - if (cancellationToken.IsCancellationRequested) - { - return null; - } + if (readFixedHeaderResult.IsConnectionClosed) + { + return ReceivedMqttPacket.Empty; + } - var readCount = await _channel.ReadAsync(_singleByteBuffer, 0, 1, cancellationToken).ConfigureAwait(false); + try + { + IsReadingPacket = true; - if (cancellationToken.IsCancellationRequested) + var fixedHeader = readFixedHeaderResult.FixedHeader; + if (fixedHeader.RemainingLength == 0) { - return null; + return new ReceivedMqttPacket(fixedHeader.Flags, PlatformAbstractionLayer.EmptyByteArraySegment, 2); } - if (readCount == 0) + var bodyLength = fixedHeader.RemainingLength; + var body = new byte[bodyLength]; + + var bodyOffset = 0; + var chunkSize = Math.Min(ReadBufferSize, bodyLength); + + do { - return null; - } + var bytesLeft = body.Length - bodyOffset; + if (chunkSize > bytesLeft) + { + chunkSize = bytesLeft; + } - _packetInspectorHandler.FillReceiveBuffer(_singleByteBuffer); + var readBytes = await _channel.ReadAsync(body, bodyOffset, chunkSize, cancellationToken).ConfigureAwait(false); - encodedByte = _singleByteBuffer[0]; + if (cancellationToken.IsCancellationRequested) + { + return ReceivedMqttPacket.Empty; + } - value += (encodedByte & 127) * multiplier; - multiplier *= 128; - } + if (readBytes == 0) + { + return ReceivedMqttPacket.Empty; + } - return value; - } + bodyOffset += readBytes; + } while (bodyOffset < bodyLength); - static bool IsWrappedException(Exception exception) - { - return exception is OperationCanceledException || - exception is MqttCommunicationTimedOutException || - exception is MqttCommunicationException || - exception is MqttProtocolViolationException; + _packetInspector?.FillReceiveBuffer(body); + + var bodySegment = new ArraySegment(body, 0, bodyLength); + return new ReceivedMqttPacket(fixedHeader.Flags, bodySegment, fixedHeader.TotalLength); + } + finally + { + IsReadingPacket = false; + } } - static void WrapAndThrowException(Exception exception) + static bool WrapAndThrowException(Exception exception) { + if (exception is OperationCanceledException || exception is MqttCommunicationTimedOutException || exception is MqttCommunicationException || + exception is MqttProtocolViolationException) + { + return false; + } + if (exception is IOException && exception.InnerException is SocketException innerException) { exception = innerException; @@ -426,4 +423,4 @@ namespace MQTTnet.Adapter throw new MqttCommunicationException(exception); } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Adapter/MqttConnectingFailedException.cs b/Source/MQTTnet/Adapter/MqttConnectingFailedException.cs index a465bb6..ea3cb47 100644 --- a/Source/MQTTnet/Adapter/MqttConnectingFailedException.cs +++ b/Source/MQTTnet/Adapter/MqttConnectingFailedException.cs @@ -1,5 +1,9 @@ +// 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.Client.Connecting; +using MQTTnet.Client; using MQTTnet.Exceptions; namespace MQTTnet.Adapter diff --git a/Source/MQTTnet/Adapter/MqttPacketInspectorHandler.cs b/Source/MQTTnet/Adapter/MqttPacketInspector.cs similarity index 59% rename from Source/MQTTnet/Adapter/MqttPacketInspectorHandler.cs rename to Source/MQTTnet/Adapter/MqttPacketInspector.cs index 04eb260..c74114a 100644 --- a/Source/MQTTnet/Adapter/MqttPacketInspectorHandler.cs +++ b/Source/MQTTnet/Adapter/MqttPacketInspector.cs @@ -1,39 +1,48 @@ +// 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.IO; -using System.Linq; using MQTTnet.Diagnostics; -using MQTTnet.Diagnostics.Logger; -using MQTTnet.Diagnostics.PacketInspection; +using MQTTnet.Formatter; +using MQTTnet.Internal; namespace MQTTnet.Adapter { - public sealed class MqttPacketInspectorHandler + public sealed class MqttPacketInspector { - readonly MemoryStream _receivedPacketBuffer; - readonly IMqttPacketInspector _packetInspector; readonly MqttNetSourceLogger _logger; - - public MqttPacketInspectorHandler(IMqttPacketInspector packetInspector, IMqttNetLogger logger) + readonly AsyncEvent _asyncEvent; + + MemoryStream _receivedPacketBuffer; + + public MqttPacketInspector(AsyncEvent asyncEvent, IMqttNetLogger logger) { - _packetInspector = packetInspector; - - if (packetInspector != null) - { - _receivedPacketBuffer = new MemoryStream(); - } - + _asyncEvent = asyncEvent ?? throw new ArgumentNullException(nameof(asyncEvent)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); - _logger = logger.WithSource(nameof(MqttPacketInspectorHandler)); + _logger = logger.WithSource(nameof(MqttPacketInspector)); } public void BeginReceivePacket() { + if (!_asyncEvent.HasHandlers) + { + return; + } + + if (_receivedPacketBuffer == null) + { + _receivedPacketBuffer = new MemoryStream(); + } + _receivedPacketBuffer?.SetLength(0); } public void EndReceivePacket() { - if (_packetInspector == null) + if (!_asyncEvent.HasHandlers) { return; } @@ -44,9 +53,9 @@ namespace MQTTnet.Adapter InspectPacket(buffer, MqttPacketFlowDirection.Inbound); } - public void BeginSendPacket(ArraySegment buffer) + public void BeginSendPacket(MqttPacketBuffer buffer) { - if (_packetInspector == null) + if (!_asyncEvent.HasHandlers) { return; } @@ -61,6 +70,11 @@ namespace MQTTnet.Adapter public void FillReceiveBuffer(byte[] buffer) { + if (!_asyncEvent.HasHandlers) + { + return; + } + _receivedPacketBuffer?.Write(buffer, 0, buffer.Length); } @@ -68,13 +82,13 @@ namespace MQTTnet.Adapter { try { - var context = new ProcessMqttPacketContext + var eventArgs = new InspectMqttPacketEventArgs { Buffer = buffer, Direction = direction }; - _packetInspector.ProcessMqttPacket(context); + _asyncEvent.InvokeAsync(eventArgs).GetAwaiter().GetResult(); } catch (Exception exception) { diff --git a/Source/MQTTnet/Adapter/ReceivedMqttPacket.cs b/Source/MQTTnet/Adapter/ReceivedMqttPacket.cs index 4a3c931..74815cb 100644 --- a/Source/MQTTnet/Adapter/ReceivedMqttPacket.cs +++ b/Source/MQTTnet/Adapter/ReceivedMqttPacket.cs @@ -1,20 +1,25 @@ -using System; -using MQTTnet.Formatter; +// 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.Adapter { - public sealed class ReceivedMqttPacket + public readonly struct ReceivedMqttPacket { - public ReceivedMqttPacket(byte fixedHeader, IMqttPacketBodyReader bodyReader, int totalLength) + public static readonly ReceivedMqttPacket Empty = new ReceivedMqttPacket(); + + public ReceivedMqttPacket(byte fixedHeader, ArraySegment body, int totalLength) { FixedHeader = fixedHeader; - BodyReader = bodyReader ?? throw new ArgumentNullException(nameof(bodyReader)); + Body = body; TotalLength = totalLength; } public byte FixedHeader { get; } - public IMqttPacketBodyReader BodyReader { get; } + public ArraySegment Body { get; } public int TotalLength { get; } } diff --git a/Source/MQTTnet/Certificates/BlobCertificateProvider.cs b/Source/MQTTnet/Certificates/BlobCertificateProvider.cs index f8daebb..be2024e 100644 --- a/Source/MQTTnet/Certificates/BlobCertificateProvider.cs +++ b/Source/MQTTnet/Certificates/BlobCertificateProvider.cs @@ -1,4 +1,8 @@ -using System; +// 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.Security.Cryptography.X509Certificates; namespace MQTTnet.Certificates diff --git a/Source/MQTTnet/Certificates/ICertificateProvider.cs b/Source/MQTTnet/Certificates/ICertificateProvider.cs index 42113d4..58b4a90 100644 --- a/Source/MQTTnet/Certificates/ICertificateProvider.cs +++ b/Source/MQTTnet/Certificates/ICertificateProvider.cs @@ -1,4 +1,8 @@ -using System.Security.Cryptography.X509Certificates; +// 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.Cryptography.X509Certificates; namespace MQTTnet.Certificates { diff --git a/Source/MQTTnet/Certificates/X509CertificateProvider.cs b/Source/MQTTnet/Certificates/X509CertificateProvider.cs index 4bbfa4d..3dcca23 100644 --- a/Source/MQTTnet/Certificates/X509CertificateProvider.cs +++ b/Source/MQTTnet/Certificates/X509CertificateProvider.cs @@ -1,4 +1,8 @@ -#if !WINDOWS_UWP +// 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. + +#if !WINDOWS_UWP using System; using System.Security.Cryptography.X509Certificates; diff --git a/Source/MQTTnet/Channel/IMqttChannel.cs b/Source/MQTTnet/Channel/IMqttChannel.cs index 188e55f..5360ce2 100644 --- a/Source/MQTTnet/Channel/IMqttChannel.cs +++ b/Source/MQTTnet/Channel/IMqttChannel.cs @@ -1,4 +1,8 @@ -using System; +// 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.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; diff --git a/Source/MQTTnet/Client/Connecting/IMqttClientConnectedHandler.cs b/Source/MQTTnet/Client/Connecting/IMqttClientConnectedHandler.cs deleted file mode 100644 index 8b7be27..0000000 --- a/Source/MQTTnet/Client/Connecting/IMqttClientConnectedHandler.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; - -namespace MQTTnet.Client.Connecting -{ - public interface IMqttClientConnectedHandler - { - Task HandleConnectedAsync(MqttClientConnectedEventArgs eventArgs); - } -} diff --git a/Source/MQTTnet/Client/Connecting/MqttClientConnectResult.cs b/Source/MQTTnet/Client/Connecting/MqttClientConnectResult.cs index 1643793..831d14c 100644 --- a/Source/MQTTnet/Client/Connecting/MqttClientConnectResult.cs +++ b/Source/MQTTnet/Client/Connecting/MqttClientConnectResult.cs @@ -1,8 +1,12 @@ -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; using MQTTnet.Packets; using MQTTnet.Protocol; -namespace MQTTnet.Client.Connecting +namespace MQTTnet.Client { public sealed class MqttClientConnectResult { @@ -83,11 +87,12 @@ namespace MQTTnet.Client.Connecting public string ServerReference { get; internal set; } /// + /// MQTTv5 only. /// Gets the keep alive interval which was chosen by the server instead of the /// keep alive interval from the client CONNECT packet. - /// MQTTv5 only. + /// A value of 0 indicates that the feature is not used. /// - public ushort? ServerKeepAlive { get; internal set; } + public ushort ServerKeepAlive { get; internal set; } public uint? SessionExpiryInterval { get; internal set; } diff --git a/Source/MQTTnet/Client/Connecting/MqttClientConnectResultCode.cs b/Source/MQTTnet/Client/Connecting/MqttClientConnectResultCode.cs index a0dbae7..a25aa07 100644 --- a/Source/MQTTnet/Client/Connecting/MqttClientConnectResultCode.cs +++ b/Source/MQTTnet/Client/Connecting/MqttClientConnectResultCode.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Client.Connecting +// 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.Client { public enum MqttClientConnectResultCode { diff --git a/Source/MQTTnet/Client/Connecting/MqttClientConnectResultFactory.cs b/Source/MQTTnet/Client/Connecting/MqttClientConnectResultFactory.cs new file mode 100644 index 0000000..f32a915 --- /dev/null +++ b/Source/MQTTnet/Client/Connecting/MqttClientConnectResultFactory.cs @@ -0,0 +1,107 @@ +// 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.Exceptions; +using MQTTnet.Formatter; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Client +{ + public sealed class MqttClientConnectResultFactory + { + public MqttClientConnectResult Create(MqttConnAckPacket connAckPacket, MqttProtocolVersion protocolVersion) + { + if (connAckPacket == null) throw new ArgumentNullException(nameof(connAckPacket)); + + if (protocolVersion == MqttProtocolVersion.V500) + { + return CreateForMqtt500(connAckPacket); + } + + return CreateForMqtt311(connAckPacket); + } + + static MqttClientConnectResult CreateForMqtt500(MqttConnAckPacket connAckPacket) + { + if (connAckPacket == null) throw new ArgumentNullException(nameof(connAckPacket)); + + return new MqttClientConnectResult + { + IsSessionPresent = connAckPacket.IsSessionPresent, + ResultCode = (MqttClientConnectResultCode) (int) connAckPacket.ReasonCode, + WildcardSubscriptionAvailable = connAckPacket.WildcardSubscriptionAvailable, + RetainAvailable = connAckPacket.RetainAvailable, + AssignedClientIdentifier = connAckPacket.AssignedClientIdentifier, + AuthenticationMethod = connAckPacket.AuthenticationMethod, + AuthenticationData = connAckPacket.AuthenticationData, + MaximumPacketSize = connAckPacket.MaximumPacketSize, + ReasonString = connAckPacket.ReasonString, + ReceiveMaximum = connAckPacket.ReceiveMaximum, + MaximumQoS = connAckPacket.MaximumQoS, + ResponseInformation = connAckPacket.ResponseInformation, + TopicAliasMaximum = connAckPacket.TopicAliasMaximum, + ServerReference = connAckPacket.ServerReference, + ServerKeepAlive = connAckPacket.ServerKeepAlive, + SessionExpiryInterval = connAckPacket.SessionExpiryInterval, + SubscriptionIdentifiersAvailable = connAckPacket.SubscriptionIdentifiersAvailable, + SharedSubscriptionAvailable = connAckPacket.SharedSubscriptionAvailable, + UserProperties = connAckPacket.UserProperties + }; + } + + static MqttClientConnectResult CreateForMqtt311(MqttConnAckPacket connAckPacket) + { + if (connAckPacket == null) throw new ArgumentNullException(nameof(connAckPacket)); + + return new MqttClientConnectResult + { + RetainAvailable = true, // Always true because v3.1.1 does not have a way to "disable" that feature. + WildcardSubscriptionAvailable = true, // Always true because v3.1.1 does not have a way to "disable" that feature. + IsSessionPresent = connAckPacket.IsSessionPresent, + ResultCode = ConvertReturnCodeToResultCode(connAckPacket.ReturnCode) + }; + } + + static MqttClientConnectResultCode ConvertReturnCodeToResultCode(MqttConnectReturnCode connectReturnCode) + { + switch (connectReturnCode) + { + case MqttConnectReturnCode.ConnectionAccepted: + { + return MqttClientConnectResultCode.Success; + } + + case MqttConnectReturnCode.ConnectionRefusedUnacceptableProtocolVersion: + { + return MqttClientConnectResultCode.UnsupportedProtocolVersion; + } + + case MqttConnectReturnCode.ConnectionRefusedNotAuthorized: + { + return MqttClientConnectResultCode.NotAuthorized; + } + + case MqttConnectReturnCode.ConnectionRefusedBadUsernameOrPassword: + { + return MqttClientConnectResultCode.BadUserNameOrPassword; + } + + case MqttConnectReturnCode.ConnectionRefusedIdentifierRejected: + { + return MqttClientConnectResultCode.ClientIdentifierNotValid; + } + + case MqttConnectReturnCode.ConnectionRefusedServerUnavailable: + { + return MqttClientConnectResultCode.ServerUnavailable; + } + + default: + throw new MqttProtocolViolationException("Received unexpected return code."); + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Connecting/MqttClientConnectedEventArgs.cs b/Source/MQTTnet/Client/Connecting/MqttClientConnectedEventArgs.cs index dc65e27..efb28e8 100644 --- a/Source/MQTTnet/Client/Connecting/MqttClientConnectedEventArgs.cs +++ b/Source/MQTTnet/Client/Connecting/MqttClientConnectedEventArgs.cs @@ -1,8 +1,12 @@ -using System; +// 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.Client.Connecting +using System; + +namespace MQTTnet.Client { - public class MqttClientConnectedEventArgs : EventArgs + public sealed class MqttClientConnectedEventArgs : EventArgs { public MqttClientConnectedEventArgs(MqttClientConnectResult connectResult) { @@ -10,9 +14,9 @@ namespace MQTTnet.Client.Connecting } /// - /// Gets the authentication result. - /// Hint: MQTT 5 feature only. + /// Gets the authentication result. + /// Hint: MQTT 5 feature only. /// public MqttClientConnectResult ConnectResult { get; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Connecting/MqttClientConnectedHandlerDelegate.cs b/Source/MQTTnet/Client/Connecting/MqttClientConnectedHandlerDelegate.cs deleted file mode 100644 index 96812af..0000000 --- a/Source/MQTTnet/Client/Connecting/MqttClientConnectedHandlerDelegate.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace MQTTnet.Client.Connecting -{ - public class MqttClientConnectedHandlerDelegate : IMqttClientConnectedHandler - { - private readonly Func _handler; - - public MqttClientConnectedHandlerDelegate(Action handler) - { - if (handler == null) throw new ArgumentNullException(nameof(handler)); - - _handler = context => - { - handler(context); - return Task.FromResult(0); - }; - } - - public MqttClientConnectedHandlerDelegate(Func handler) - { - _handler = handler ?? throw new ArgumentNullException(nameof(handler)); - } - - public Task HandleConnectedAsync(MqttClientConnectedEventArgs eventArgs) - { - return _handler(eventArgs); - } - } -} diff --git a/Source/MQTTnet/Client/Connecting/MqttClientConnectingEventArgs.cs b/Source/MQTTnet/Client/Connecting/MqttClientConnectingEventArgs.cs new file mode 100644 index 0000000..a4b87bc --- /dev/null +++ b/Source/MQTTnet/Client/Connecting/MqttClientConnectingEventArgs.cs @@ -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 System; + +namespace MQTTnet.Client +{ + public sealed class MqttClientConnectingEventArgs : EventArgs + { + public MqttClientConnectingEventArgs(MqttClientOptions clientOptions) + { + ClientOptions = clientOptions; + } + + public MqttClientOptions ClientOptions { get; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Disconnecting/IMqttClientDisconnectedHandler.cs b/Source/MQTTnet/Client/Disconnecting/IMqttClientDisconnectedHandler.cs deleted file mode 100644 index 14347bf..0000000 --- a/Source/MQTTnet/Client/Disconnecting/IMqttClientDisconnectedHandler.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; - -namespace MQTTnet.Client.Disconnecting -{ - public interface IMqttClientDisconnectedHandler - { - Task HandleDisconnectedAsync(MqttClientDisconnectedEventArgs eventArgs); - } -} diff --git a/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectOptions.cs b/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectOptions.cs index 7f8c644..3f4e88a 100644 --- a/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectOptions.cs +++ b/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectOptions.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Client.Disconnecting +// 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.Client { public sealed class MqttClientDisconnectOptions { @@ -6,7 +10,7 @@ /// Gets or sets the reason code. /// Hint: MQTT 5 feature only. /// - public MqttClientDisconnectReason ReasonCode { get; set; } = MqttClientDisconnectReason.NormalDisconnection; + public MqttClientDisconnectReason Reason { get; set; } = MqttClientDisconnectReason.NormalDisconnection; /// /// Gets or sets the reason string. diff --git a/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectOptionsBuilder.cs b/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectOptionsBuilder.cs new file mode 100644 index 0000000..34f16b7 --- /dev/null +++ b/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectOptionsBuilder.cs @@ -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. + +namespace MQTTnet.Client +{ + public sealed class MqttClientDisconnectOptionsBuilder + { + MqttClientDisconnectReason _reason = MqttClientDisconnectReason.NormalDisconnection; + string _reasonString; + + public MqttClientDisconnectOptionsBuilder WithReasonString(string value) + { + _reasonString = value; + return this; + } + + public MqttClientDisconnectOptionsBuilder WithReason(MqttClientDisconnectReason value) + { + _reason = value; + return this; + } + + public MqttClientDisconnectOptions Build() + { + return new MqttClientDisconnectOptions + { + Reason = _reason, + ReasonString = _reasonString + }; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectReason.cs b/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectReason.cs index cce2377..a451132 100644 --- a/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectReason.cs +++ b/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectReason.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Client.Disconnecting +// 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.Client { public enum MqttClientDisconnectReason { diff --git a/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectedEventArgs.cs b/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectedEventArgs.cs index f4a4731..009e020 100644 --- a/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectedEventArgs.cs +++ b/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectedEventArgs.cs @@ -1,37 +1,29 @@ -using System; -using MQTTnet.Client.Connecting; +// 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.Client.Disconnecting +using System; + +namespace MQTTnet.Client { public sealed class MqttClientDisconnectedEventArgs : EventArgs { - public MqttClientDisconnectedEventArgs(bool clientWasConnected, Exception exception, MqttClientConnectResult connectResult, MqttClientDisconnectReason reason) - { - ClientWasConnected = clientWasConnected; - Exception = exception; - ConnectResult = connectResult; - Reason = reason; - - ReasonCode = reason; - } + public bool ClientWasConnected { get; internal set; } - public bool ClientWasConnected { get; } - - public Exception Exception { get; } + public Exception Exception { get; internal set; } /// /// Gets the authentication result. /// Hint: MQTT 5 feature only. /// - public MqttClientConnectResult ConnectResult { get; } + public MqttClientConnectResult ConnectResult { get; internal set; } /// /// Gets or sets the reason. /// Hint: MQTT 5 feature only. /// - public MqttClientDisconnectReason Reason { get; set; } - - [Obsolete("Please use 'Reason' instead. This property will be removed in the future!")] - public MqttClientDisconnectReason ReasonCode { get; set; } + public MqttClientDisconnectReason Reason { get; internal set; } + + public string ReasonString { get; internal set; } } } diff --git a/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectedHandlerDelegate.cs b/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectedHandlerDelegate.cs deleted file mode 100644 index c02443e..0000000 --- a/Source/MQTTnet/Client/Disconnecting/MqttClientDisconnectedHandlerDelegate.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace MQTTnet.Client.Disconnecting -{ - public class MqttClientDisconnectedHandlerDelegate : IMqttClientDisconnectedHandler - { - private readonly Func _handler; - - public MqttClientDisconnectedHandlerDelegate(Action handler) - { - if (handler == null) throw new ArgumentNullException(nameof(handler)); - - _handler = context => - { - handler(context); - return Task.FromResult(0); - }; - } - - public MqttClientDisconnectedHandlerDelegate(Func handler) - { - _handler = handler ?? throw new ArgumentNullException(nameof(handler)); - } - - public Task HandleDisconnectedAsync(MqttClientDisconnectedEventArgs eventArgs) - { - return _handler(eventArgs); - } - } -} diff --git a/Source/MQTTnet/Client/ExtendedAuthenticationExchange/IMqttExtendedAuthenticationExchangeHandler.cs b/Source/MQTTnet/Client/ExtendedAuthenticationExchange/IMqttExtendedAuthenticationExchangeHandler.cs index 8cd3542..8b66334 100644 --- a/Source/MQTTnet/Client/ExtendedAuthenticationExchange/IMqttExtendedAuthenticationExchangeHandler.cs +++ b/Source/MQTTnet/Client/ExtendedAuthenticationExchange/IMqttExtendedAuthenticationExchangeHandler.cs @@ -1,6 +1,10 @@ -using System.Threading.Tasks; +// 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.Client.ExtendedAuthenticationExchange +using System.Threading.Tasks; + +namespace MQTTnet.Client { public interface IMqttExtendedAuthenticationExchangeHandler { diff --git a/Source/MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeContext.cs b/Source/MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeContext.cs index 082e9a8..c406b5e 100644 --- a/Source/MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeContext.cs +++ b/Source/MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeContext.cs @@ -1,21 +1,25 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; using System.Collections.Generic; using MQTTnet.Packets; using MQTTnet.Protocol; -namespace MQTTnet.Client.ExtendedAuthenticationExchange +namespace MQTTnet.Client { public class MqttExtendedAuthenticationExchangeContext { - public MqttExtendedAuthenticationExchangeContext(MqttAuthPacket authPacket, IMqttClient client) + public MqttExtendedAuthenticationExchangeContext(MqttAuthPacket authPacket, MqttClient client) { if (authPacket == null) throw new ArgumentNullException(nameof(authPacket)); ReasonCode = authPacket.ReasonCode; - ReasonString = authPacket.Properties?.ReasonString; - AuthenticationMethod = authPacket.Properties?.AuthenticationMethod; - AuthenticationData = authPacket.Properties?.AuthenticationData; - UserProperties = authPacket.Properties?.UserProperties; + ReasonString = authPacket.ReasonString; + AuthenticationMethod = authPacket.AuthenticationMethod; + AuthenticationData = authPacket.AuthenticationData; + UserProperties = authPacket.UserProperties; Client = client ?? throw new ArgumentNullException(nameof(client)); } @@ -53,6 +57,6 @@ namespace MQTTnet.Client.ExtendedAuthenticationExchange /// public List UserProperties { get; } - public IMqttClient Client { get; } + public MqttClient Client { get; } } } diff --git a/Source/MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeData.cs b/Source/MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeData.cs index 29f3821..74f1464 100644 --- a/Source/MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeData.cs +++ b/Source/MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeData.cs @@ -1,8 +1,12 @@ -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; using MQTTnet.Packets; using MQTTnet.Protocol; -namespace MQTTnet.Client.ExtendedAuthenticationExchange +namespace MQTTnet.Client { public class MqttExtendedAuthenticationExchangeData { diff --git a/Source/MQTTnet/Client/IMqttClient.cs b/Source/MQTTnet/Client/IMqttClient.cs deleted file mode 100644 index 03bee88..0000000 --- a/Source/MQTTnet/Client/IMqttClient.cs +++ /dev/null @@ -1,50 +0,0 @@ -using MQTTnet.Client.Connecting; -using MQTTnet.Client.Disconnecting; -using MQTTnet.Client.ExtendedAuthenticationExchange; -using MQTTnet.Client.Options; -using MQTTnet.Client.Subscribing; -using MQTTnet.Client.Unsubscribing; -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace MQTTnet.Client -{ - public interface IMqttClient : IApplicationMessageReceiver, IApplicationMessagePublisher, IDisposable - { - bool IsConnected { get; } - - IMqttClientOptions Options { get; } - - /// - /// Gets or sets the connected handler that is fired after the client has connected to the server successfully. - /// Hint: Initialize handlers before you connect the client to avoid issues. - /// - IMqttClientConnectedHandler ConnectedHandler { get; set; } - - /// - /// Gets or sets the disconnected handler that is fired after the client has disconnected from the server. - /// Hint: Initialize handlers before you connect the client to avoid issues. - /// - IMqttClientDisconnectedHandler DisconnectedHandler { get; set; } - - Task ConnectAsync(IMqttClientOptions options, CancellationToken cancellationToken); - - Task DisconnectAsync(MqttClientDisconnectOptions options, CancellationToken cancellationToken); - - Task PingAsync(CancellationToken cancellationToken); - - /// - /// Sends extended authentication data. - /// Hint: MQTT 5 feature only. - /// - /// The extended data. - /// A cancellation token to stop the task. - /// A representing any asynchronous operation. - Task SendExtendedAuthenticationExchangeDataAsync(MqttExtendedAuthenticationExchangeData data, CancellationToken cancellationToken); - - Task SubscribeAsync(MqttClientSubscribeOptions options, CancellationToken cancellationToken); - - Task UnsubscribeAsync(MqttClientUnsubscribeOptions options, CancellationToken cancellationToken); - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Client/IMqttClientFactory.cs b/Source/MQTTnet/Client/IMqttClientFactory.cs deleted file mode 100644 index 4831456..0000000 --- a/Source/MQTTnet/Client/IMqttClientFactory.cs +++ /dev/null @@ -1,28 +0,0 @@ -using MQTTnet.Adapter; -using MQTTnet.Diagnostics; -using MQTTnet.Diagnostics.Logger; -using MQTTnet.LowLevelClient; - -namespace MQTTnet.Client -{ - public interface IMqttClientFactory - { - IMqttFactory UseClientAdapterFactory(IMqttClientAdapterFactory clientAdapterFactory); - - ILowLevelMqttClient CreateLowLevelMqttClient(); - - ILowLevelMqttClient CreateLowLevelMqttClient(IMqttNetLogger logger); - - ILowLevelMqttClient CreateLowLevelMqttClient(IMqttClientAdapterFactory clientAdapterFactory); - - ILowLevelMqttClient CreateLowLevelMqttClient(IMqttNetLogger logger, IMqttClientAdapterFactory clientAdapterFactory); - - IMqttClient CreateMqttClient(); - - IMqttClient CreateMqttClient(IMqttNetLogger logger); - - IMqttClient CreateMqttClient(IMqttClientAdapterFactory adapterFactory); - - IMqttClient CreateMqttClient(IMqttNetLogger logger, IMqttClientAdapterFactory adapterFactory); - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Client/MqttClient.cs b/Source/MQTTnet/Client/MqttClient.cs index 9c091ca..d231d87 100644 --- a/Source/MQTTnet/Client/MqttClient.cs +++ b/Source/MQTTnet/Client/MqttClient.cs @@ -1,13 +1,8 @@ +// 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.Client.Connecting; -using MQTTnet.Client.Disconnecting; -using MQTTnet.Client.ExtendedAuthenticationExchange; -using MQTTnet.Client.Options; -using MQTTnet.Client.Publishing; -using MQTTnet.Client.Receiving; -using MQTTnet.Client.Subscribing; -using MQTTnet.Client.Unsubscribing; -using MQTTnet.Diagnostics; using MQTTnet.Exceptions; using MQTTnet.Internal; using MQTTnet.PacketDispatcher; @@ -16,18 +11,33 @@ using MQTTnet.Protocol; using System; using System.Threading; using System.Threading.Tasks; -using MQTTnet.Diagnostics.Logger; +using MQTTnet.Diagnostics; +using MQTTnet.Formatter; using MQTTnet.Implementations; namespace MQTTnet.Client { - public class MqttClient : Disposable, IMqttClient + public sealed class MqttClient : Disposable { + readonly MqttPacketFactories _packetFactories = new MqttPacketFactories(); + + readonly MqttClientPublishResultFactory _clientPublishResultFactory = new MqttClientPublishResultFactory(); + readonly MqttClientSubscribeResultFactory _clientSubscribeResultFactory = new MqttClientSubscribeResultFactory(); + readonly MqttClientUnsubscribeResultFactory _clientUnsubscribeResultFactory = new MqttClientUnsubscribeResultFactory(); + readonly MqttApplicationMessageFactory _applicationMessageFactory = new MqttApplicationMessageFactory(); + readonly MqttPacketIdentifierProvider _packetIdentifierProvider = new MqttPacketIdentifierProvider(); readonly MqttPacketDispatcher _packetDispatcher = new MqttPacketDispatcher(); readonly object _disconnectLock = new object(); + readonly AsyncEvent _connectedEvent = new AsyncEvent(); + readonly AsyncEvent _connectingEvent = new AsyncEvent(); + readonly AsyncEvent _disconnectedEvent = new AsyncEvent(); + readonly AsyncEvent _applicationMessageReceivedEvent = new AsyncEvent(); + readonly AsyncEvent _inspectPacketEvent = new AsyncEvent(); + readonly IMqttClientAdapterFactory _adapterFactory; + readonly IMqttNetLogger _rootLogger; readonly MqttNetSourceLogger _logger; CancellationTokenSource _backgroundCancellationTokenSource; @@ -44,26 +54,50 @@ namespace MQTTnet.Client MqttClientDisconnectReason _disconnectReason; DateTime _lastPacketSentTimestamp; - + string _disconnectReasonString; + public MqttClient(IMqttClientAdapterFactory channelFactory, IMqttNetLogger logger) { - if (logger == null) throw new ArgumentNullException(nameof(logger)); - _adapterFactory = channelFactory ?? throw new ArgumentNullException(nameof(channelFactory)); + _rootLogger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger.WithSource(nameof(MqttClient)); } - - public IMqttClientConnectedHandler ConnectedHandler { get; set; } - - public IMqttClientDisconnectedHandler DisconnectedHandler { get; set; } - - public IMqttApplicationMessageReceivedHandler ApplicationMessageReceivedHandler { get; set; } - + + public event Func ConnectedAsync + { + add => _connectedEvent.AddHandler(value); + remove => _connectedEvent.RemoveHandler(value); + } + + public event Func ConnectingAsync + { + add => _connectingEvent.AddHandler(value); + remove => _connectingEvent.RemoveHandler(value); + } + + public event Func DisconnectedAsync + { + add => _disconnectedEvent.AddHandler(value); + remove => _disconnectedEvent.RemoveHandler(value); + } + + public event Func ApplicationMessageReceivedAsync + { + add => _applicationMessageReceivedEvent.AddHandler(value); + remove => _applicationMessageReceivedEvent.RemoveHandler(value); + } + + public event Func InspectPackage + { + add => _inspectPacketEvent.AddHandler(value); + remove => _inspectPacketEvent.RemoveHandler(value); + } + public bool IsConnected => (MqttClientConnectionStatus)_connectionStatus == MqttClientConnectionStatus.Connected; - public IMqttClientOptions Options { get; private set; } + public MqttClientOptions Options { get; private set; } - public async Task ConnectAsync(IMqttClientOptions options, CancellationToken cancellationToken) + public async Task ConnectAsync(MqttClientOptions options, CancellationToken cancellationToken = default) { if (options == null) throw new ArgumentNullException(nameof(options)); if (options.ChannelOptions == null) throw new ArgumentException("ChannelOptions are not set."); @@ -83,32 +117,45 @@ namespace MQTTnet.Client { Options = options; + if (_connectingEvent.HasHandlers) + { + await _connectingEvent.InvokeAsync(new MqttClientConnectingEventArgs(options)); + } + _packetIdentifierProvider.Reset(); _packetDispatcher.CancelAll(); _backgroundCancellationTokenSource = new CancellationTokenSource(); var backgroundCancellationToken = _backgroundCancellationTokenSource.Token; - var adapter = _adapterFactory.CreateClientAdapter(options); + var adapter = _adapterFactory.CreateClientAdapter(options, new MqttPacketInspector(_inspectPacketEvent, _rootLogger), _rootLogger); _adapter = adapter; - using (var combined = CancellationTokenSource.CreateLinkedTokenSource(backgroundCancellationToken, cancellationToken)) + using (var effectiveCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(backgroundCancellationToken, cancellationToken)) { - _logger.Verbose("Trying to connect with server '{0}' (Timeout={1}).", options.ChannelOptions, options.CommunicationTimeout); - await adapter.ConnectAsync(options.CommunicationTimeout, combined.Token).ConfigureAwait(false); + _logger.Verbose("Trying to connect with server '{0}'.", options.ChannelOptions); + await adapter.ConnectAsync(effectiveCancellationToken.Token).ConfigureAwait(false); _logger.Verbose("Connection with server established."); + _publishPacketReceiverQueue?.Dispose(); _publishPacketReceiverQueue = new AsyncQueue(); _publishPacketReceiverTask = Task.Run(() => ProcessReceivedPublishPackets(backgroundCancellationToken), backgroundCancellationToken); _packetReceiverTask = Task.Run(() => TryReceivePacketsAsync(backgroundCancellationToken), backgroundCancellationToken); - connectResult = await AuthenticateAsync(adapter, options.WillMessage, combined.Token).ConfigureAwait(false); + connectResult = await AuthenticateAsync(options, effectiveCancellationToken.Token).ConfigureAwait(false); } _lastPacketSentTimestamp = DateTime.UtcNow; - if (Options.KeepAlivePeriod != TimeSpan.Zero) + var keepAliveInterval = Options.KeepAlivePeriod; + if (connectResult.ServerKeepAlive > 0) + { + _logger.Info($"Using keep alive value ({connectResult.ServerKeepAlive}) sent from the server."); + keepAliveInterval = TimeSpan.FromSeconds(connectResult.ServerKeepAlive); + } + + if (keepAliveInterval != TimeSpan.Zero) { _keepAlivePacketsSenderTask = Task.Run(() => TrySendKeepAliveMessagesAsync(backgroundCancellationToken), backgroundCancellationToken); } @@ -117,10 +164,10 @@ namespace MQTTnet.Client _logger.Info("Connected."); - var connectedHandler = ConnectedHandler; - if (connectedHandler != null) + if (_connectedEvent.HasHandlers) { - await connectedHandler.HandleConnectedAsync(new MqttClientConnectedEventArgs(connectResult)).ConfigureAwait(false); + var eventArgs = new MqttClientConnectedEventArgs(connectResult); + await _connectedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); } return connectResult; @@ -137,7 +184,7 @@ namespace MQTTnet.Client } } - public async Task DisconnectAsync(MqttClientDisconnectOptions options, CancellationToken cancellationToken) + public async Task DisconnectAsync(MqttClientDisconnectOptions options, CancellationToken cancellationToken = default) { if (options is null) throw new ArgumentNullException(nameof(options)); @@ -156,7 +203,7 @@ namespace MQTTnet.Client if (clientWasConnected) { - var disconnectPacket = _adapter.PacketFormatterAdapter.DataConverter.CreateDisconnectPacket(options); + var disconnectPacket = _packetFactories.Disconnect.Create(options); await SendAsync(disconnectPacket, cancellationToken).ConfigureAwait(false); } } @@ -166,32 +213,31 @@ namespace MQTTnet.Client } } - public Task PingAsync(CancellationToken cancellationToken) + public Task PingAsync(CancellationToken cancellationToken = default) { return SendAndReceiveAsync(MqttPingReqPacket.Instance, cancellationToken); } - public Task SendExtendedAuthenticationExchangeDataAsync(MqttExtendedAuthenticationExchangeData data, CancellationToken cancellationToken) + public Task SendExtendedAuthenticationExchangeDataAsync(MqttExtendedAuthenticationExchangeData data, CancellationToken cancellationToken = default) { if (data == null) throw new ArgumentNullException(nameof(data)); ThrowIfDisposed(); ThrowIfNotConnected(); - return SendAsync(new MqttAuthPacket + var authPacket = new MqttAuthPacket { - Properties = new MqttAuthPacketProperties - { - // This must always be equal to the value from the CONNECT packet. So we use it here to ensure that. - AuthenticationMethod = Options.AuthenticationMethod, - AuthenticationData = data.AuthenticationData, - ReasonString = data.ReasonString, - UserProperties = data.UserProperties - } - }, cancellationToken); + // This must always be equal to the value from the CONNECT packet. So we use it here to ensure that. + AuthenticationMethod = Options.AuthenticationMethod, + AuthenticationData = data.AuthenticationData, + ReasonString = data.ReasonString, + UserProperties = data.UserProperties + }; + + return SendAsync(authPacket, cancellationToken); } - public async Task SubscribeAsync(MqttClientSubscribeOptions options, CancellationToken cancellationToken) + public async Task SubscribeAsync(MqttClientSubscribeOptions options, CancellationToken cancellationToken = default) { if (options == null) throw new ArgumentNullException(nameof(options)); @@ -203,28 +249,30 @@ namespace MQTTnet.Client ThrowIfDisposed(); ThrowIfNotConnected(); - var subscribePacket = _adapter.PacketFormatterAdapter.DataConverter.CreateSubscribePacket(options); + var subscribePacket = _packetFactories.Subscribe.Create(options); subscribePacket.PacketIdentifier = _packetIdentifierProvider.GetNextPacketIdentifier(); var subAckPacket = await SendAndReceiveAsync(subscribePacket, cancellationToken).ConfigureAwait(false); - return _adapter.PacketFormatterAdapter.DataConverter.CreateClientSubscribeResult(subscribePacket, subAckPacket); + + return _clientSubscribeResultFactory.Create(subscribePacket, subAckPacket); } - public async Task UnsubscribeAsync(MqttClientUnsubscribeOptions options, CancellationToken cancellationToken) + public async Task UnsubscribeAsync(MqttClientUnsubscribeOptions options, CancellationToken cancellationToken = default) { if (options == null) throw new ArgumentNullException(nameof(options)); ThrowIfDisposed(); ThrowIfNotConnected(); - - var unsubscribePacket = _adapter.PacketFormatterAdapter.DataConverter.CreateUnsubscribePacket(options); + + var unsubscribePacket = _packetFactories.Unsubscribe.Create(options); unsubscribePacket.PacketIdentifier = _packetIdentifierProvider.GetNextPacketIdentifier(); var unsubAckPacket = await SendAndReceiveAsync(unsubscribePacket, cancellationToken).ConfigureAwait(false); - return _adapter.PacketFormatterAdapter.DataConverter.CreateClientUnsubscribeResult(unsubscribePacket, unsubAckPacket); + + return _clientUnsubscribeResultFactory.Create(unsubscribePacket, unsubAckPacket); } - public Task PublishAsync(MqttApplicationMessage applicationMessage, CancellationToken cancellationToken) + public Task PublishAsync(MqttApplicationMessage applicationMessage, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -233,7 +281,7 @@ namespace MQTTnet.Client ThrowIfDisposed(); ThrowIfNotConnected(); - var publishPacket = _adapter.PacketFormatterAdapter.DataConverter.CreatePublishPacket(applicationMessage); + var publishPacket = _packetFactories.Publish.Create(applicationMessage); switch (applicationMessage.QualityOfServiceLevel) { @@ -284,18 +332,18 @@ namespace MQTTnet.Client base.Dispose(disposing); } - async Task AuthenticateAsync(IMqttChannelAdapter channelAdapter, MqttApplicationMessage willApplicationMessage, CancellationToken cancellationToken) + async Task AuthenticateAsync(MqttClientOptions options, CancellationToken cancellationToken) { MqttClientConnectResult result; try { - var connectPacket = channelAdapter.PacketFormatterAdapter.DataConverter.CreateConnectPacket( - willApplicationMessage, - Options); + var connectPacket = _packetFactories.Connect.Create(options); var connAckPacket = await SendAndReceiveAsync(connectPacket, cancellationToken).ConfigureAwait(false); - result = channelAdapter.PacketFormatterAdapter.DataConverter.CreateClientConnectResult(connAckPacket); + + var clientConnectResultFactory = new MqttClientConnectResultFactory(); + result = clientConnectResultFactory.Create(connAckPacket, options.ProtocolVersion); } catch (Exception exception) { @@ -322,7 +370,10 @@ namespace MQTTnet.Client void ThrowIfConnected(string message) { - if (IsConnected) throw new MqttProtocolViolationException(message); + if (IsConnected) + { + throw new MqttProtocolViolationException(message); + } } Task DisconnectInternalAsync(Task sender, Exception exception, MqttClientConnectResult connectResult) @@ -345,8 +396,12 @@ namespace MQTTnet.Client { if (_adapter != null) { - _logger.Verbose("Disconnecting [Timeout={0}]", Options.CommunicationTimeout); - await _adapter.DisconnectAsync(Options.CommunicationTimeout, CancellationToken.None).ConfigureAwait(false); + _logger.Verbose("Disconnecting [Timeout={0}]", Options.Timeout); + + using (var timeout = new CancellationTokenSource(Options.Timeout)) + { + await _adapter.DisconnectAsync(timeout.Token).ConfigureAwait(false); + } } _logger.Verbose("Disconnected from adapter."); @@ -376,13 +431,18 @@ namespace MQTTnet.Client _logger.Info("Disconnected."); - var disconnectedHandler = DisconnectedHandler; - if (disconnectedHandler != null) + var eventArgs = new MqttClientDisconnectedEventArgs { - // This handler must be executed in a new thread because otherwise a dead lock may happen - // when trying to reconnect in that handler etc. - Task.Run(() => disconnectedHandler.HandleDisconnectedAsync(new MqttClientDisconnectedEventArgs(clientWasConnected, exception, connectResult, _disconnectReason))).RunInBackground(_logger); - } + ClientWasConnected = clientWasConnected, + Exception = exception, + ConnectResult = connectResult, + Reason = _disconnectReason, + ReasonString = _disconnectReasonString + }; + + // This handler must be executed in a new thread because otherwise a dead lock may happen + // when trying to reconnect in that handler etc. + Task.Run(() => _disconnectedEvent.InvokeAsync(eventArgs)).RunInBackground(_logger); } } @@ -401,7 +461,7 @@ namespace MQTTnet.Client } } - Task SendAsync(MqttBasePacket packet, CancellationToken cancellationToken) + Task SendAsync(MqttPacket packet, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -410,17 +470,17 @@ namespace MQTTnet.Client return _adapter.SendPacketAsync(packet, cancellationToken); } - async Task SendAndReceiveAsync(MqttBasePacket requestPacket, CancellationToken cancellationToken) where TResponsePacket : MqttBasePacket + async Task SendAndReceiveAsync(MqttPacket requestPacket, CancellationToken cancellationToken) where TResponsePacket : MqttPacket { cancellationToken.ThrowIfCancellationRequested(); ushort packetIdentifier = 0; - if (requestPacket is IMqttPacketWithIdentifier packetWithIdentifier) + if (requestPacket is MqttPacketWithIdentifier packetWithIdentifier) { packetIdentifier = packetWithIdentifier.PacketIdentifier; } - using (var packetAwaiter = _packetDispatcher.AddAwaitable(packetIdentifier)) + using (var packetAwaitable = _packetDispatcher.AddAwaitable(packetIdentifier)) { try { @@ -429,12 +489,12 @@ namespace MQTTnet.Client catch (Exception exception) { _logger.Warning(exception, "Error when sending request packet ({0}).", requestPacket.GetType().Name); - packetAwaiter.Fail(exception); + packetAwaitable.Fail(exception); } try { - return await packetAwaiter.WaitOneAsync(Options.CommunicationTimeout).ConfigureAwait(false); + return await packetAwaitable.WaitOneAsync(cancellationToken).ConfigureAwait(false); } catch (Exception exception) { @@ -509,7 +569,17 @@ namespace MQTTnet.Client while (!cancellationToken.IsCancellationRequested) { - var packet = await _adapter.ReceivePacketAsync(cancellationToken).ConfigureAwait(false); + MqttPacket packet; + var packetTask = _adapter.ReceivePacketAsync(cancellationToken); + + if (packetTask.IsCompleted) + { + packet = packetTask.Result; + } + else + { + packet = await packetTask.ConfigureAwait(false); + } if (cancellationToken.IsCancellationRequested) { @@ -555,7 +625,7 @@ namespace MQTTnet.Client } } - async Task TryProcessReceivedPacketAsync(MqttBasePacket packet, CancellationToken cancellationToken) + async Task TryProcessReceivedPacketAsync(MqttPacket packet, CancellationToken cancellationToken) { try { @@ -672,7 +742,7 @@ namespace MQTTnet.Client { if (!eventArgs.ProcessingFailed) { - var pubAckPacket = _adapter.PacketFormatterAdapter.DataConverter.CreatePubAckPacket(eventArgs.PublishPacket, eventArgs.ReasonCode); + var pubAckPacket = _packetFactories.PubAck.Create(eventArgs); return SendAsync(pubAckPacket, cancellationToken); } } @@ -680,7 +750,7 @@ namespace MQTTnet.Client { if (!eventArgs.ProcessingFailed) { - var pubRecPacket = _adapter.PacketFormatterAdapter.DataConverter.CreatePubRecPacket(eventArgs.PublishPacket, eventArgs.ReasonCode); + var pubRecPacket = _packetFactories.PubRec.Create(eventArgs); return SendAsync(pubRecPacket, cancellationToken); } } @@ -698,7 +768,7 @@ namespace MQTTnet.Client { // The packet is unknown. Probably due to a restart of the client. // So wen send this to the server to trigger a full resend of the message. - var pubRelPacket = _adapter.PacketFormatterAdapter.DataConverter.CreatePubRelPacket(pubRecPacket, MqttApplicationMessageReceivedReasonCode.PacketIdentifierNotFound); + var pubRelPacket = _packetFactories.PubRel.Create(pubRecPacket, MqttApplicationMessageReceivedReasonCode.PacketIdentifierNotFound); return SendAsync(pubRelPacket, cancellationToken); } @@ -707,14 +777,15 @@ namespace MQTTnet.Client Task ProcessReceivedPubRelPacket(MqttPubRelPacket pubRelPacket, CancellationToken cancellationToken) { - var pubCompPacket = _adapter.PacketFormatterAdapter.DataConverter.CreatePubCompPacket(pubRelPacket, MqttApplicationMessageReceivedReasonCode.Success); + var pubCompPacket = _packetFactories.PubComp.Create(pubRelPacket, MqttApplicationMessageReceivedReasonCode.Success); return SendAsync(pubCompPacket, cancellationToken); } Task ProcessReceivedDisconnectPacket(MqttDisconnectPacket disconnectPacket) { - _disconnectReason = (MqttClientDisconnectReason) (disconnectPacket.ReasonCode ?? MqttDisconnectReasonCode.NormalDisconnection); - + _disconnectReason = (MqttClientDisconnectReason) disconnectPacket.ReasonCode; + _disconnectReasonString = disconnectPacket.ReasonString; + // Also dispatch disconnect to waiting threads to generate a proper exception. _packetDispatcher.FailAll(new MqttUnexpectedDisconnectReceivedException(disconnectPacket)); @@ -735,8 +806,10 @@ namespace MQTTnet.Client async Task PublishAtMostOnce(MqttPublishPacket publishPacket, CancellationToken cancellationToken) { // No packet identifier is used for QoS 0 [3.3.2.2 Packet Identifier] - await SendAsync(publishPacket, cancellationToken).ConfigureAwait(false); - return _adapter.PacketFormatterAdapter.DataConverter.CreateClientPublishResult(null); + await SendAsync(publishPacket, cancellationToken) + .ConfigureAwait(false); + + return _clientPublishResultFactory.Create(null); } async Task PublishAtLeastOnceAsync(MqttPublishPacket publishPacket, CancellationToken cancellationToken) @@ -744,8 +817,8 @@ namespace MQTTnet.Client publishPacket.PacketIdentifier = _packetIdentifierProvider.GetNextPacketIdentifier(); var pubAckPacket = await SendAndReceiveAsync(publishPacket, cancellationToken).ConfigureAwait(false); - - return _adapter.PacketFormatterAdapter.DataConverter.CreateClientPublishResult(pubAckPacket); + + return _clientPublishResultFactory.Create(pubAckPacket); } async Task PublishExactlyOnceAsync(MqttPublishPacket publishPacket, CancellationToken cancellationToken) @@ -754,23 +827,19 @@ namespace MQTTnet.Client var pubRecPacket = await SendAndReceiveAsync(publishPacket, cancellationToken).ConfigureAwait(false); - var pubRelPacket = _adapter.PacketFormatterAdapter.DataConverter.CreatePubRelPacket(pubRecPacket, MqttApplicationMessageReceivedReasonCode.Success); + var pubRelPacket = _packetFactories.PubRel.Create(pubRecPacket, MqttApplicationMessageReceivedReasonCode.Success); var pubCompPacket = await SendAndReceiveAsync(pubRelPacket, cancellationToken).ConfigureAwait(false); - return _adapter.PacketFormatterAdapter.DataConverter.CreateClientPublishResult(pubRecPacket, pubCompPacket); + return _clientPublishResultFactory.Create(pubRecPacket, pubCompPacket); } async Task HandleReceivedApplicationMessageAsync(MqttPublishPacket publishPacket) { - var applicationMessage = _adapter.PacketFormatterAdapter.DataConverter.CreateApplicationMessage(publishPacket); + var applicationMessage = _applicationMessageFactory.Create(publishPacket); var eventArgs = new MqttApplicationMessageReceivedEventArgs(Options.ClientId, applicationMessage, publishPacket, AcknowledgeReceivedPublishPacket); - - var handler = ApplicationMessageReceivedHandler; - if (handler != null) - { - await handler.HandleApplicationMessageReceivedAsync(eventArgs).ConfigureAwait(false); - } + + await _applicationMessageReceivedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); return eventArgs; } diff --git a/Source/MQTTnet/Client/MqttClientConnectionStatus.cs b/Source/MQTTnet/Client/MqttClientConnectionStatus.cs index cde8df0..65833ae 100644 --- a/Source/MQTTnet/Client/MqttClientConnectionStatus.cs +++ b/Source/MQTTnet/Client/MqttClientConnectionStatus.cs @@ -1,3 +1,7 @@ +// 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.Client { public enum MqttClientConnectionStatus diff --git a/Source/MQTTnet/Client/MqttClientExtensions.cs b/Source/MQTTnet/Client/MqttClientExtensions.cs index 117f398..b48dac5 100644 --- a/Source/MQTTnet/Client/MqttClientExtensions.cs +++ b/Source/MQTTnet/Client/MqttClientExtensions.cs @@ -1,314 +1,140 @@ -using MQTTnet.Client.Connecting; -using MQTTnet.Client.Disconnecting; -using MQTTnet.Client.ExtendedAuthenticationExchange; -using MQTTnet.Client.Options; -using MQTTnet.Client.Publishing; -using MQTTnet.Client.Receiving; -using MQTTnet.Client.Subscribing; -using MQTTnet.Client.Unsubscribing; -using MQTTnet.Protocol; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + using System; using System.Collections.Generic; +using System.Text; using System.Threading; using System.Threading.Tasks; +using MQTTnet.Packets; +using MQTTnet.Protocol; namespace MQTTnet.Client { public static class MqttClientExtensions { - public static IMqttClient UseConnectedHandler(this IMqttClient client, Func handler) + public static Task DisconnectAsync(this MqttClient client, MqttClientDisconnectReason reason = MqttClientDisconnectReason.NormalDisconnection, string reasonString = null) { - if (client == null) throw new ArgumentNullException(nameof(client)); - - if (handler == null) + if (client == null) { - return client.UseConnectedHandler((IMqttClientConnectedHandler)null); + throw new ArgumentNullException(nameof(client)); } - return client.UseConnectedHandler(new MqttClientConnectedHandlerDelegate(handler)); + return client.DisconnectAsync( + new MqttClientDisconnectOptions + { + Reason = reason, + ReasonString = reasonString + }, + CancellationToken.None); } - public static IMqttClient UseConnectedHandler(this IMqttClient client, Action handler) + public static Task PublishBinaryAsync( + this MqttClient mqttClient, + string topic, + IEnumerable payload = null, + MqttQualityOfServiceLevel qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce, + bool retain = false, + CancellationToken cancellationToken = default) { - if (client == null) throw new ArgumentNullException(nameof(client)); - - if (handler == null) + if (mqttClient == null) { - return client.UseConnectedHandler((IMqttClientConnectedHandler)null); + throw new ArgumentNullException(nameof(mqttClient)); } - return client.UseConnectedHandler(new MqttClientConnectedHandlerDelegate(handler)); - } - - public static IMqttClient UseConnectedHandler(this IMqttClient client, IMqttClientConnectedHandler handler) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - - client.ConnectedHandler = handler; - return client; - } - - public static IMqttClient UseDisconnectedHandler(this IMqttClient client, Func handler) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - - if (handler == null) + if (topic == null) { - return client.UseDisconnectedHandler((IMqttClientDisconnectedHandler)null); + throw new ArgumentNullException(nameof(topic)); } - return client.UseDisconnectedHandler(new MqttClientDisconnectedHandlerDelegate(handler)); - } - - public static IMqttClient UseDisconnectedHandler(this IMqttClient client, Action handler) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - - if (handler == null) - { - return client.UseDisconnectedHandler((IMqttClientDisconnectedHandler)null); - } + var applicationMessage = new MqttApplicationMessageBuilder().WithTopic(topic) + .WithPayload(payload) + .WithRetainFlag(retain) + .WithQualityOfServiceLevel(qualityOfServiceLevel) + .Build(); - return client.UseDisconnectedHandler(new MqttClientDisconnectedHandlerDelegate(handler)); + return mqttClient.PublishAsync(applicationMessage, cancellationToken); } - public static IMqttClient UseDisconnectedHandler(this IMqttClient client, IMqttClientDisconnectedHandler handler) + public static Task PublishStringAsync( + this MqttClient mqttClient, + string topic, + string payload = null, + MqttQualityOfServiceLevel qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce, + bool retain = false, + CancellationToken cancellationToken = default) { - if (client == null) throw new ArgumentNullException(nameof(client)); - - client.DisconnectedHandler = handler; - return client; + var payloadBuffer = Encoding.UTF8.GetBytes(payload ?? string.Empty); + return mqttClient.PublishBinaryAsync(topic, payloadBuffer, qualityOfServiceLevel, retain, cancellationToken); } - public static IMqttClient UseApplicationMessageReceivedHandler(this IMqttClient client, Func handler) + public static Task SendExtendedAuthenticationExchangeDataAsync(this MqttClient client, MqttExtendedAuthenticationExchangeData data) { - if (client == null) throw new ArgumentNullException(nameof(client)); - - if (handler == null) + if (client == null) { - return client.UseApplicationMessageReceivedHandler((IMqttApplicationMessageReceivedHandler)null); + throw new ArgumentNullException(nameof(client)); } - return client.UseApplicationMessageReceivedHandler(new MqttApplicationMessageReceivedHandlerDelegate(handler)); + return client.SendExtendedAuthenticationExchangeDataAsync(data, CancellationToken.None); } - public static IMqttClient UseApplicationMessageReceivedHandler(this IMqttClient client, Action handler) + public static Task SubscribeAsync(this MqttClient mqttClient, MqttTopicFilter topicFilter, CancellationToken cancellationToken = default) { - if (client == null) throw new ArgumentNullException(nameof(client)); - - if (handler == null) + if (mqttClient == null) { - return client.UseApplicationMessageReceivedHandler((IMqttApplicationMessageReceivedHandler)null); + throw new ArgumentNullException(nameof(mqttClient)); } - return client.UseApplicationMessageReceivedHandler(new MqttApplicationMessageReceivedHandlerDelegate(handler)); - } + if (topicFilter == null) + { + throw new ArgumentNullException(nameof(topicFilter)); + } - public static IMqttClient UseApplicationMessageReceivedHandler(this IMqttClient client, IMqttApplicationMessageReceivedHandler handler) - { - if (client == null) throw new ArgumentNullException(nameof(client)); + var subscribeOptions = new MqttClientSubscribeOptionsBuilder().WithTopicFilter(topicFilter) + .Build(); - client.ApplicationMessageReceivedHandler = handler; - return client; + return mqttClient.SubscribeAsync(subscribeOptions, cancellationToken); } - public static Task ReconnectAsync(this IMqttClient client) + public static Task SubscribeAsync( + this MqttClient mqttClient, + string topic, + MqttQualityOfServiceLevel qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce, + CancellationToken cancellationToken = default) { - if (client == null) throw new ArgumentNullException(nameof(client)); - - if (client.Options == null) + if (mqttClient == null) { - throw new InvalidOperationException("_ReconnectAsync_ can be used only if _ConnectAsync_ was called before."); + throw new ArgumentNullException(nameof(mqttClient)); } - return client.ConnectAsync(client.Options); - } - - public static Task ReconnectAsync(this IMqttClient client, CancellationToken cancellationToken) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - - if (client.Options == null) + if (topic == null) { - throw new InvalidOperationException("_ReconnectAsync_ can be used only if _ConnectAsync_ was called before."); + throw new ArgumentNullException(nameof(topic)); } - return client.ConnectAsync(client.Options, cancellationToken); - } - - public static Task DisconnectAsync(this IMqttClient client) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - - return client.DisconnectAsync(CancellationToken.None); - } - - public static Task DisconnectAsync(this IMqttClient client, MqttClientDisconnectOptions options) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - - return client.DisconnectAsync(options, CancellationToken.None); - } - - public static Task DisconnectAsync(this IMqttClient client, CancellationToken cancellationToken) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - - return client.DisconnectAsync(new MqttClientDisconnectOptions(), cancellationToken); - } - - public static Task SubscribeAsync(this IMqttClient client, params MqttTopicFilter[] topicFilters) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); - - var options = new MqttClientSubscribeOptions(); - options.TopicFilters.AddRange(topicFilters); - - return client.SubscribeAsync(options); - } - - public static Task SubscribeAsync(this IMqttClient client, string topic, MqttQualityOfServiceLevel qualityOfServiceLevel) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); - - return client.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic(topic).WithQualityOfServiceLevel(qualityOfServiceLevel).Build()); - } - - public static Task SubscribeAsync(this IMqttClient client, string topic) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); - - return client.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic(topic).Build()); - } - - public static Task UnsubscribeAsync(this IMqttClient client, params string[] topicFilters) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); - - var options = new MqttClientUnsubscribeOptions(); - options.TopicFilters.AddRange(topicFilters); - - return client.UnsubscribeAsync(options); - } - - public static Task ConnectAsync(this IMqttClient client, IMqttClientOptions options) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - - return client.ConnectAsync(options, CancellationToken.None); - } - - public static Task SendExtendedAuthenticationExchangeDataAsync(this IMqttClient client, MqttExtendedAuthenticationExchangeData data) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - - return client.SendExtendedAuthenticationExchangeDataAsync(data, CancellationToken.None); - } - - public static Task SubscribeAsync(this IMqttClient client, MqttClientSubscribeOptions options) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - - return client.SubscribeAsync(options, CancellationToken.None); - } - - public static Task UnsubscribeAsync(this IMqttClient client, MqttClientUnsubscribeOptions options) - { - if (client == null) throw new ArgumentNullException(nameof(client)); + var subscribeOptions = new MqttClientSubscribeOptionsBuilder().WithTopicFilter(topic, qualityOfServiceLevel) + .Build(); - return client.UnsubscribeAsync(options, CancellationToken.None); + return mqttClient.SubscribeAsync(subscribeOptions, cancellationToken); } - public static Task PublishAsync(this IMqttClient client, MqttApplicationMessage applicationMessage) + public static Task UnsubscribeAsync(this MqttClient mqttClient, string topic, CancellationToken cancellationToken = default) { - if (client == null) throw new ArgumentNullException(nameof(client)); - if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); - - return client.PublishAsync(applicationMessage, CancellationToken.None); - } - - public static async Task PublishAsync(this IMqttClient client, IEnumerable applicationMessages) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - if (applicationMessages == null) throw new ArgumentNullException(nameof(applicationMessages)); - - foreach (var applicationMessage in applicationMessages) + if (mqttClient == null) { - await client.PublishAsync(applicationMessage).ConfigureAwait(false); + throw new ArgumentNullException(nameof(mqttClient)); } - } - - public static Task PublishAsync(this IMqttClient client, string topic) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); - return client.PublishAsync(new MqttApplicationMessageBuilder() - .WithTopic(topic) - .Build()); - } - - public static Task PublishAsync(this IMqttClient client, string topic, IEnumerable payload) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); - - return client.PublishAsync(new MqttApplicationMessageBuilder() - .WithTopic(topic) - .WithPayload(payload) - .Build()); - } - - public static Task PublishAsync(this IMqttClient client, string topic, string payload) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); - - return client.PublishAsync(new MqttApplicationMessageBuilder() - .WithTopic(topic) - .WithPayload(payload) - .Build()); - } - - public static Task PublishAsync(this IMqttClient client, string topic, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); - - return client.PublishAsync(new MqttApplicationMessageBuilder() - .WithTopic(topic) - .WithPayload(payload) - .WithQualityOfServiceLevel(qualityOfServiceLevel) - .Build()); - } - - public static Task PublishAsync(this IMqttClient client, string topic, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel, bool retain) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); - - return client.PublishAsync(new MqttApplicationMessageBuilder() - .WithTopic(topic) - .WithPayload(payload) - .WithQualityOfServiceLevel(qualityOfServiceLevel) - .WithRetainFlag(retain) - .Build()); - } + if (topic == null) + { + throw new ArgumentNullException(nameof(topic)); + } - public static Task PublishAsync(this IMqttClient client, string topic, string payload, bool retain) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); + var unsubscribeOptions = new MqttClientUnsubscribeOptionsBuilder().WithTopicFilter(topic) + .Build(); - return client.PublishAsync(new MqttApplicationMessageBuilder() - .WithTopic(topic) - .WithPayload(payload) - .WithRetainFlag(retain) - .Build()); + return mqttClient.UnsubscribeAsync(unsubscribeOptions, cancellationToken); } } } \ No newline at end of file diff --git a/Source/MQTTnet/Client/MqttPacketIdentifierProvider.cs b/Source/MQTTnet/Client/MqttPacketIdentifierProvider.cs index a517fff..21cde64 100644 --- a/Source/MQTTnet/Client/MqttPacketIdentifierProvider.cs +++ b/Source/MQTTnet/Client/MqttPacketIdentifierProvider.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Client +// 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.Client { public sealed class MqttPacketIdentifierProvider { diff --git a/Source/MQTTnet/Client/Options/IMqttClientChannelOptions.cs b/Source/MQTTnet/Client/Options/IMqttClientChannelOptions.cs index 729d12f..11ecf16 100644 --- a/Source/MQTTnet/Client/Options/IMqttClientChannelOptions.cs +++ b/Source/MQTTnet/Client/Options/IMqttClientChannelOptions.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Client.Options +// 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.Client { public interface IMqttClientChannelOptions { diff --git a/Source/MQTTnet/Client/Options/IMqttClientCredentials.cs b/Source/MQTTnet/Client/Options/IMqttClientCredentials.cs deleted file mode 100644 index 1caa9f0..0000000 --- a/Source/MQTTnet/Client/Options/IMqttClientCredentials.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MQTTnet.Client.Options -{ - public interface IMqttClientCredentials - { - string Username { get; } - byte[] Password { get; } - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Options/IMqttClientCredentialsProvider.cs b/Source/MQTTnet/Client/Options/IMqttClientCredentialsProvider.cs new file mode 100644 index 0000000..382f91e --- /dev/null +++ b/Source/MQTTnet/Client/Options/IMqttClientCredentialsProvider.cs @@ -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.Client +{ + public interface IMqttClientCredentialsProvider + { + string GetUserName(MqttClientOptions clientOptions); + + byte[] GetPassword(MqttClientOptions clientOptions); + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Options/IMqttClientOptions.cs b/Source/MQTTnet/Client/Options/IMqttClientOptions.cs deleted file mode 100644 index 1ef462a..0000000 --- a/Source/MQTTnet/Client/Options/IMqttClientOptions.cs +++ /dev/null @@ -1,114 +0,0 @@ -using MQTTnet.Client.ExtendedAuthenticationExchange; -using MQTTnet.Formatter; -using MQTTnet.Packets; -using System; -using System.Collections.Generic; -using MQTTnet.Diagnostics.PacketInspection; - -namespace MQTTnet.Client.Options -{ - public interface IMqttClientOptions - { - /// - /// Gets the client identifier. - /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. - /// - string ClientId { get; } - - /// - /// Gets 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. - /// - bool CleanSession { get; } - - IMqttClientCredentials Credentials { get; } - - IMqttExtendedAuthenticationExchangeHandler ExtendedAuthenticationExchangeHandler { get; } - - MqttProtocolVersion ProtocolVersion { get; } - - IMqttClientChannelOptions ChannelOptions { get; } - - TimeSpan CommunicationTimeout { get; } - - /// - /// Gets 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. - /// - TimeSpan KeepAlivePeriod { get; } - - /// - /// Gets the last will message. - /// In MQTT, you use the last will message feature to notify other clients about an ungracefully disconnected client. - /// - MqttApplicationMessage WillMessage { get; } - - /// - /// Gets the will delay interval. - /// This is the time between the client disconnect and the time the will message will be sent. - /// - uint? WillDelayInterval { get; } - - /// - /// Gets the authentication method. - /// Hint: MQTT 5 feature only. - /// - string AuthenticationMethod { get; } - - /// - /// Gets the authentication data. - /// Hint: MQTT 5 feature only. - /// - byte[] AuthenticationData { get; } - - uint? MaximumPacketSize { get; } - - /// - /// Gets the receive maximum. - /// This gives the maximum length of the receive messages. - /// - ushort? ReceiveMaximum { get; } - - /// - /// Gets the request problem information. - /// Hint: MQTT 5 feature only. - /// - bool? RequestProblemInformation { get; } - - /// - /// Gets the request response information. - /// Hint: MQTT 5 feature only. - /// - bool? RequestResponseInformation { get; } - - /// - /// Gets the session expiry interval. - /// The time after a session expires when it's not actively used. - /// - uint? SessionExpiryInterval { get; } - - /// - /// Gets the topic alias maximum. - /// This gives the maximum length of the topic alias. - /// - ushort? TopicAliasMaximum { get; } - - /// - /// Gets or sets the user properties. - /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT packet. - /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add metadata to MQTT messages and pass information between publisher, broker, and subscriber. - /// The feature is very similar to the HTTP header concept. - /// Hint: MQTT 5 feature only. - /// - List UserProperties { get; set; } - - IMqttPacketInspector PacketInspector { get; set; } - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Options/MqttClientCertificateValidationCallbackContext.cs b/Source/MQTTnet/Client/Options/MqttClientCertificateValidationCallbackContext.cs deleted file mode 100644 index 3827f05..0000000 --- a/Source/MQTTnet/Client/Options/MqttClientCertificateValidationCallbackContext.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; - -namespace MQTTnet.Client.Options -{ - public class MqttClientCertificateValidationCallbackContext - { - public X509Certificate Certificate { get; set; } - - public X509Chain Chain { get; set; } - - public SslPolicyErrors SslPolicyErrors { get; set; } - - public IMqttClientChannelOptions ClientOptions { get; set; } - } -} diff --git a/Source/MQTTnet/Client/Options/MqttClientCertificateValidationEventArgs.cs b/Source/MQTTnet/Client/Options/MqttClientCertificateValidationEventArgs.cs new file mode 100644 index 0000000..f1842cb --- /dev/null +++ b/Source/MQTTnet/Client/Options/MqttClientCertificateValidationEventArgs.cs @@ -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; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +namespace MQTTnet.Client +{ + public sealed class MqttClientCertificateValidationEventArgs : EventArgs + { + public X509Certificate Certificate { get; set; } + + public X509Chain Chain { get; set; } + + public SslPolicyErrors SslPolicyErrors { get; set; } + + public IMqttClientChannelOptions ClientOptions { get; set; } + } +} diff --git a/Source/MQTTnet/Client/Options/MqttClientCredentials.cs b/Source/MQTTnet/Client/Options/MqttClientCredentials.cs index e068639..b2d50a9 100644 --- a/Source/MQTTnet/Client/Options/MqttClientCredentials.cs +++ b/Source/MQTTnet/Client/Options/MqttClientCredentials.cs @@ -1,9 +1,28 @@ -namespace MQTTnet.Client.Options +// 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.Client { - public class MqttClientCredentials : IMqttClientCredentials + public sealed class MqttClientCredentials : IMqttClientCredentialsProvider { - public string Username { get; set; } + readonly string _userName; + readonly byte[] _password; + + public MqttClientCredentials(string userName, byte[] password = null) + { + _userName = userName; + _password = password; + } + + public string GetUserName(MqttClientOptions clientOptions) + { + return _userName; + } - public byte[] Password { get; set; } + public byte[] GetPassword(MqttClientOptions clientOptions) + { + return _password; + } } } diff --git a/Source/MQTTnet/Client/Options/MqttClientDefaultCertificateValidationHandler.cs b/Source/MQTTnet/Client/Options/MqttClientDefaultCertificateValidationHandler.cs new file mode 100644 index 0000000..ffc4c08 --- /dev/null +++ b/Source/MQTTnet/Client/Options/MqttClientDefaultCertificateValidationHandler.cs @@ -0,0 +1,40 @@ +// 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.Linq; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +namespace MQTTnet.Client +{ + public sealed class MqttClientDefaultCertificateValidationHandler + { + public static bool Handle(MqttClientCertificateValidationEventArgs eventArgs) + { + if (eventArgs.SslPolicyErrors == SslPolicyErrors.None) + { + return true; + } + + if (eventArgs.Chain.ChainStatus.Any(c => + c.Status == X509ChainStatusFlags.RevocationStatusUnknown || c.Status == X509ChainStatusFlags.Revoked || c.Status == X509ChainStatusFlags.OfflineRevocation)) + { + if (eventArgs.ClientOptions?.TlsOptions?.IgnoreCertificateRevocationErrors != true) + { + return false; + } + } + + if (eventArgs.Chain.ChainStatus.Any(c => c.Status == X509ChainStatusFlags.PartialChain)) + { + if (eventArgs.ClientOptions?.TlsOptions?.IgnoreCertificateChainErrors != true) + { + return false; + } + } + + return eventArgs.ClientOptions?.TlsOptions?.AllowUntrustedCertificates == true; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Options/MqttClientOptions.cs b/Source/MQTTnet/Client/Options/MqttClientOptions.cs index d67410a..ce5aed5 100644 --- a/Source/MQTTnet/Client/Options/MqttClientOptions.cs +++ b/Source/MQTTnet/Client/Options/MqttClientOptions.cs @@ -1,114 +1,182 @@ -using MQTTnet.Client.ExtendedAuthenticationExchange; -using MQTTnet.Formatter; -using MQTTnet.Packets; +// 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.Diagnostics.PacketInspection; +using MQTTnet.Formatter; +using MQTTnet.Packets; +using MQTTnet.Protocol; -namespace MQTTnet.Client.Options +namespace MQTTnet.Client { - public class MqttClientOptions : IMqttClientOptions + public sealed class MqttClientOptions { /// - /// Gets the client identifier. - /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. + /// Gets or sets the authentication data. + /// Hint: MQTT 5 feature only. /// - public string ClientId { get; set; } = Guid.NewGuid().ToString("N"); + public byte[] AuthenticationData { get; set; } + + /// + /// Gets or sets the authentication method. + /// Hint: MQTT 5 feature only. + /// + public string AuthenticationMethod { get; set; } + + public IMqttClientChannelOptions ChannelOptions { get; set; } /// - /// 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. + /// 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. /// public bool CleanSession { get; set; } = true; - public IMqttClientCredentials Credentials { get; set; } + /// + /// Gets the client identifier. + /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. + /// + public string ClientId { get; set; } = Guid.NewGuid().ToString("N"); + + public IMqttClientCredentialsProvider Credentials { get; set; } public IMqttExtendedAuthenticationExchangeHandler ExtendedAuthenticationExchangeHandler { get; set; } + /// + /// Gets or sets the keep alive period. + /// The connection is normally left open by the client so that is can send and receive data at any time. + /// 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. + /// + public TimeSpan KeepAlivePeriod { get; set; } = TimeSpan.FromSeconds(15); + + public uint MaximumPacketSize { get; set; } + public MqttProtocolVersion ProtocolVersion { get; set; } = MqttProtocolVersion.V311; - public IMqttClientChannelOptions ChannelOptions { get; set; } + /// + /// Gets or sets the receive maximum. + /// This gives the maximum length of the receive messages. + /// + public ushort ReceiveMaximum { get; set; } - public TimeSpan CommunicationTimeout { get; set; } = TimeSpan.FromSeconds(10); + /// + /// Gets or sets the request problem information. + /// Hint: MQTT 5 feature only. + /// + public bool RequestProblemInformation { get; set; } = true; /// - /// 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. + /// Gets or sets the request response information. + /// Hint: MQTT 5 feature only. /// - public TimeSpan KeepAlivePeriod { get; set; } = TimeSpan.FromSeconds(15); + public bool RequestResponseInformation { get; set; } /// - /// Gets or sets the last will message. - /// In MQTT, you use the last will message feature to notify other clients about an ungracefully disconnected client. + /// Gets or sets the session expiry interval. + /// The time after a session expires when it's not actively used. /// - public MqttApplicationMessage WillMessage { get; set; } + public uint SessionExpiryInterval { get; set; } /// - /// Gets or sets the will delay interval. - /// This is the time between the client disconnect and the time the will message will be sent. + /// Gets or sets the timeout which will be applied at socket level and internal operations. + /// The default value is the same as for sockets in .NET in general. /// - public uint? WillDelayInterval { get; set; } + public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(100); /// - /// Gets or sets the authentication method. - /// Hint: MQTT 5 feature only. + /// Gets or sets the topic alias maximum. + /// This gives the maximum length of the topic alias. /// - public string AuthenticationMethod { get; set; } + public ushort TopicAliasMaximum { get; set; } /// - /// Gets or sets the authentication data. - /// Hint: MQTT 5 feature only. + /// Gets or sets the user properties. + /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT + /// packet. + /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add + /// metadata to MQTT messages and pass information between publisher, broker, and subscriber. + /// The feature is very similar to the HTTP header concept. + /// Hint: MQTT 5 feature only. /// - public byte[] AuthenticationData { get; set; } + public List UserProperties { get; set; } - public uint? MaximumPacketSize { get; set; } + /// + /// Gets or sets the content type of the will message. + /// + public string WillContentType { get; set; } /// - /// Gets or sets the receive maximum. - /// This gives the maximum length of the receive messages. + /// Gets or sets the correlation data of the will message. /// - public ushort? ReceiveMaximum { get; set; } + public byte[] WillCorrelationData { get; set; } /// - /// Gets or sets the request problem information. - /// Hint: MQTT 5 feature only. + /// Gets or sets the will delay interval. + /// This is the time between the client disconnect and the time the will message will be sent. /// - public bool? RequestProblemInformation { get; set; } + public uint WillDelayInterval { get; set; } /// - /// Gets or sets the request response information. - /// Hint: MQTT 5 feature only. + /// Gets or sets the message expiry interval of the will message. /// - public bool? RequestResponseInformation { get; set; } + public uint WillMessageExpiryInterval { get; set; } /// - /// Gets or sets the session expiry interval. - /// The time after a session expires when it's not actively used. + /// Gets or sets the payload of the will message. /// - public uint? SessionExpiryInterval { get; set; } + public byte[] WillPayload { get; set; } /// - /// Gets or sets the topic alias maximum. - /// This gives the maximum length of the topic alias. + /// Gets or sets the payload format indicator of the will message. /// - public ushort? TopicAliasMaximum { get; set; } + public MqttPayloadFormatIndicator WillPayloadFormatIndicator { get; set; } /// - /// Gets or sets the user properties. - /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT packet. - /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add metadata to MQTT messages and pass information between publisher, broker, and subscriber. - /// The feature is very similar to the HTTP header concept. - /// Hint: MQTT 5 feature only. + /// Gets or sets the QoS level of the will message. /// - public List UserProperties { get; set; } + public MqttQualityOfServiceLevel WillQualityOfServiceLevel { get; set; } + + /// + /// Gets or sets the response topic of the will message. + /// + public string WillResponseTopic { get; set; } - public IMqttPacketInspector PacketInspector { get; set; } + /// + /// Gets or sets the retain flag of the will message. + /// + public bool WillRetain { get; set; } + + /// + /// Gets or sets the topic of the will message. + /// + public string WillTopic { get; set; } + + /// + /// Gets or sets the user properties of the will message. + /// + public List WillUserProperties { get; set; } + + /// + /// 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. + /// + public int WriterBufferSize { get; set; } = 4096; + + /// + /// 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. + /// + public int WriterBufferSizeMax { get; set; } = 65535; } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Options/MqttClientOptionsBuilder.cs b/Source/MQTTnet/Client/Options/MqttClientOptionsBuilder.cs index a8560b3..4896d2b 100644 --- a/Source/MQTTnet/Client/Options/MqttClientOptionsBuilder.cs +++ b/Source/MQTTnet/Client/Options/MqttClientOptionsBuilder.cs @@ -1,37 +1,86 @@ -using MQTTnet.Client.ExtendedAuthenticationExchange; -using MQTTnet.Formatter; -using MQTTnet.Packets; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + using System; using System.Collections.Generic; using System.Linq; using System.Text; -using MQTTnet.Diagnostics.PacketInspection; +using MQTTnet.Formatter; +using MQTTnet.Packets; +using MQTTnet.Protocol; -namespace MQTTnet.Client.Options +namespace MQTTnet.Client { - public class MqttClientOptionsBuilder + public sealed class MqttClientOptionsBuilder { readonly MqttClientOptions _options = new MqttClientOptions(); + MqttClientWebSocketProxyOptions _proxyOptions; MqttClientTcpOptions _tcpOptions; - MqttClientWebSocketOptions _webSocketOptions; MqttClientOptionsBuilderTlsParameters _tlsParameters; - MqttClientWebSocketProxyOptions _proxyOptions; + MqttClientWebSocketOptions _webSocketOptions; - public MqttClientOptionsBuilder WithProtocolVersion(MqttProtocolVersion value) + public MqttClientOptions Build() { - if (value == MqttProtocolVersion.Unknown) + if (_tcpOptions == null && _webSocketOptions == null) { - throw new ArgumentException("Protocol version is invalid."); + throw new InvalidOperationException("A channel must be set."); } - _options.ProtocolVersion = value; - return this; + if (_tlsParameters != null) + { + if (_tlsParameters?.UseTls == true) + { + var tlsOptions = new MqttClientTlsOptions + { + UseTls = true, + SslProtocol = _tlsParameters.SslProtocol, + AllowUntrustedCertificates = _tlsParameters.AllowUntrustedCertificates, + CertificateValidationHandler = _tlsParameters.CertificateValidationHandler, + IgnoreCertificateChainErrors = _tlsParameters.IgnoreCertificateChainErrors, + IgnoreCertificateRevocationErrors = _tlsParameters.IgnoreCertificateRevocationErrors, +#if WINDOWS_UWP + Certificates = _tlsParameters.Certificates?.Select(c => c.ToArray()).ToList(), +#else + Certificates = _tlsParameters.Certificates?.ToList(), +#endif + +#if NETCOREAPP3_1 || NET5_0_OR_GREATER + ApplicationProtocols = _tlsParameters.ApplicationProtocols, +#endif + }; + + if (_tcpOptions != null) + { + _tcpOptions.TlsOptions = tlsOptions; + } + else if (_webSocketOptions != null) + { + _webSocketOptions.TlsOptions = tlsOptions; + } + } + } + + if (_proxyOptions != null) + { + if (_webSocketOptions == null) + { + throw new InvalidOperationException("Proxies are only supported for WebSocket connections."); + } + + _webSocketOptions.ProxyOptions = _proxyOptions; + } + + _options.ChannelOptions = (IMqttClientChannelOptions)_tcpOptions ?? _webSocketOptions; + + return _options; } - public MqttClientOptionsBuilder WithCommunicationTimeout(TimeSpan value) + public MqttClientOptionsBuilder WithAuthentication(string method, byte[] data) { - _options.CommunicationTimeout = value; + _options.AuthenticationMethod = method; + _options.AuthenticationData = data; return this; } @@ -41,141 +90,177 @@ namespace MQTTnet.Client.Options return this; } - [Obsolete("This method is no longer supported. The client will send ping requests just before the keep alive interval is going to elapse. As per MQTT RFC the serve has to wait 1.5 times the interval so we don't need this anymore.")] - public MqttClientOptionsBuilder WithKeepAliveSendInterval(TimeSpan value) + public MqttClientOptionsBuilder WithClientId(string value) { + _options.ClientId = value; return this; } - public MqttClientOptionsBuilder WithNoKeepAlive() - { - return WithKeepAlivePeriod(TimeSpan.Zero); - } - - public MqttClientOptionsBuilder WithKeepAlivePeriod(TimeSpan value) + /// + /// Sets the timeout which will be applied at socket level and internal operations. + /// The default value is the same as for sockets in .NET in general. + /// + public MqttClientOptionsBuilder WithTimeout(TimeSpan value) { - _options.KeepAlivePeriod = value; + _options.Timeout = value; return this; } - public MqttClientOptionsBuilder WithClientId(string value) + public MqttClientOptionsBuilder WithConnectionUri(Uri uri) { - _options.ClientId = value; - return this; - } + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + var port = uri.IsDefaultPort ? null : (int?)uri.Port; + switch (uri.Scheme.ToLower()) + { + case "tcp": + case "mqtt": + WithTcpServer(uri.Host, port); + break; + + case "mqtts": + WithTcpServer(uri.Host, port).WithTls(); + break; + + case "ws": + case "wss": + WithWebSocketServer(uri.ToString()); + break; + + default: + throw new ArgumentException("Unexpected scheme in uri."); + } + + if (!string.IsNullOrEmpty(uri.UserInfo)) + { + var userInfo = uri.UserInfo.Split(':'); + var username = userInfo[0]; + var password = userInfo.Length > 1 ? userInfo[1] : ""; + WithCredentials(username, password); + } - public MqttClientOptionsBuilder WithWillMessage(MqttApplicationMessage value) - { - _options.WillMessage = value; return this; } - public MqttClientOptionsBuilder WithAuthentication(string method, byte[] data) + public MqttClientOptionsBuilder WithConnectionUri(string uri) { - _options.AuthenticationMethod = method; - _options.AuthenticationData = data; - return this; + return WithConnectionUri(new Uri(uri, UriKind.Absolute)); } - public MqttClientOptionsBuilder WithWillDelayInterval(uint? willDelayInterval) + public MqttClientOptionsBuilder WithCredentials(string username, string password) { - _options.WillDelayInterval = willDelayInterval; - return this; + byte[] passwordBuffer = null; + + if (password != null) + { + passwordBuffer = Encoding.UTF8.GetBytes(password); + } + + return WithCredentials(username, passwordBuffer); } - public MqttClientOptionsBuilder WithTopicAliasMaximum(ushort? topicAliasMaximum) + public MqttClientOptionsBuilder WithCredentials(string username, byte[] password = null) { - _options.TopicAliasMaximum = topicAliasMaximum; - return this; + return WithCredentials(new MqttClientCredentials(username, password)); } - public MqttClientOptionsBuilder WithMaximumPacketSize(uint? maximumPacketSize) + public MqttClientOptionsBuilder WithCredentials(IMqttClientCredentialsProvider credentials) { - _options.MaximumPacketSize = maximumPacketSize; + _options.Credentials = credentials; return this; } - public MqttClientOptionsBuilder WithReceiveMaximum(ushort? receiveMaximum) + public MqttClientOptionsBuilder WithExtendedAuthenticationExchangeHandler(IMqttExtendedAuthenticationExchangeHandler handler) { - _options.ReceiveMaximum = receiveMaximum; + _options.ExtendedAuthenticationExchangeHandler = handler; return this; } - public MqttClientOptionsBuilder WithRequestProblemInformation(bool? requestProblemInformation = true) + public MqttClientOptionsBuilder WithKeepAlivePeriod(TimeSpan value) { - _options.RequestProblemInformation = requestProblemInformation; + _options.KeepAlivePeriod = value; return this; } - public MqttClientOptionsBuilder WithRequestResponseInformation(bool? requestResponseInformation = true) + public MqttClientOptionsBuilder WithMaximumPacketSize(uint maximumPacketSize) { - _options.RequestResponseInformation = requestResponseInformation; + _options.MaximumPacketSize = maximumPacketSize; return this; } - public MqttClientOptionsBuilder WithSessionExpiryInterval(uint? sessionExpiryInterval) + public MqttClientOptionsBuilder WithNoKeepAlive() { - _options.SessionExpiryInterval = sessionExpiryInterval; - return this; + return WithKeepAlivePeriod(TimeSpan.Zero); } - public MqttClientOptionsBuilder WithUserProperty(string name, string value) + public MqttClientOptionsBuilder WithProtocolVersion(MqttProtocolVersion value) { - if (name is null) throw new ArgumentNullException(nameof(name)); - if (value is null) throw new ArgumentNullException(nameof(value)); - - if (_options.UserProperties == null) + if (value == MqttProtocolVersion.Unknown) { - _options.UserProperties = new List(); + throw new ArgumentException("Protocol version is invalid."); } - _options.UserProperties.Add(new MqttUserProperty(name, value)); + _options.ProtocolVersion = value; return this; } - public MqttClientOptionsBuilder WithCredentials(string username, string password) + public MqttClientOptionsBuilder WithProxy( + string address, + string username = null, + string password = null, + string domain = null, + bool bypassOnLocal = false, + string[] bypassList = null) { - byte[] passwordBuffer = null; - - if (password != null) + _proxyOptions = new MqttClientWebSocketProxyOptions { - passwordBuffer = Encoding.UTF8.GetBytes(password); - } + Address = address, + Username = username, + Password = password, + Domain = domain, + BypassOnLocal = bypassOnLocal, + BypassList = bypassList + }; - return WithCredentials(username, passwordBuffer); + return this; } - public MqttClientOptionsBuilder WithCredentials(string username, byte[] password) + public MqttClientOptionsBuilder WithProxy(Action optionsBuilder) { - _options.Credentials = new MqttClientCredentials + if (optionsBuilder == null) { - Username = username, - Password = password - }; + throw new ArgumentNullException(nameof(optionsBuilder)); + } + _proxyOptions = new MqttClientWebSocketProxyOptions(); + optionsBuilder(_proxyOptions); return this; } - - public MqttClientOptionsBuilder WithCredentials(string username) - { - _options.Credentials = new MqttClientCredentials - { - Username = username - }; + public MqttClientOptionsBuilder WithReceiveMaximum(ushort receiveMaximum) + { + _options.ReceiveMaximum = receiveMaximum; return this; } - public MqttClientOptionsBuilder WithCredentials(IMqttClientCredentials credentials) + public MqttClientOptionsBuilder WithRequestProblemInformation(bool requestProblemInformation = true) { - _options.Credentials = credentials; + _options.RequestProblemInformation = requestProblemInformation; + return this; + } + public MqttClientOptionsBuilder WithRequestResponseInformation(bool requestResponseInformation = true) + { + _options.RequestResponseInformation = requestResponseInformation; return this; } - public MqttClientOptionsBuilder WithExtendedAuthenticationExchangeHandler(IMqttExtendedAuthenticationExchangeHandler handler) + public MqttClientOptionsBuilder WithSessionExpiryInterval(uint sessionExpiryInterval) { - _options.ExtendedAuthenticationExchangeHandler = handler; + _options.SessionExpiryInterval = sessionExpiryInterval; return this; } @@ -193,7 +278,10 @@ namespace MQTTnet.Client.Options // TODO: Consider creating _MqttClientTcpOptionsBuilder_ as overload. public MqttClientOptionsBuilder WithTcpServer(Action optionsBuilder) { - if (optionsBuilder == null) throw new ArgumentNullException(nameof(optionsBuilder)); + if (optionsBuilder == null) + { + throw new ArgumentNullException(nameof(optionsBuilder)); + } _tcpOptions = new MqttClientTcpOptions(); optionsBuilder.Invoke(_tcpOptions); @@ -201,27 +289,57 @@ namespace MQTTnet.Client.Options return this; } - public MqttClientOptionsBuilder WithProxy(string address, string username = null, string password = null, string domain = null, bool bypassOnLocal = false, string[] bypassList = null) + public MqttClientOptionsBuilder WithTls(MqttClientOptionsBuilderTlsParameters parameters) { - _proxyOptions = new MqttClientWebSocketProxyOptions + _tlsParameters = parameters; + return this; + } + + public MqttClientOptionsBuilder WithTls() + { + return WithTls(new MqttClientOptionsBuilderTlsParameters { UseTls = true }); + } + + public MqttClientOptionsBuilder WithTls(Action optionsBuilder) + { + if (optionsBuilder == null) { - Address = address, - Username = username, - Password = password, - Domain = domain, - BypassOnLocal = bypassOnLocal, - BypassList = bypassList + throw new ArgumentNullException(nameof(optionsBuilder)); + } + + _tlsParameters = new MqttClientOptionsBuilderTlsParameters + { + UseTls = true }; + optionsBuilder(_tlsParameters); return this; } - public MqttClientOptionsBuilder WithProxy(Action optionsBuilder) + public MqttClientOptionsBuilder WithTopicAliasMaximum(ushort topicAliasMaximum) { - if (optionsBuilder == null) throw new ArgumentNullException(nameof(optionsBuilder)); + _options.TopicAliasMaximum = topicAliasMaximum; + return this; + } - _proxyOptions = new MqttClientWebSocketProxyOptions(); - optionsBuilder(_proxyOptions); + public MqttClientOptionsBuilder WithUserProperty(string name, string value) + { + if (name is null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (_options.UserProperties == null) + { + _options.UserProperties = new List(); + } + + _options.UserProperties.Add(new MqttUserProperty(name, value)); return this; } @@ -239,7 +357,10 @@ namespace MQTTnet.Client.Options public MqttClientOptionsBuilder WithWebSocketServer(Action optionsBuilder) { - if (optionsBuilder == null) throw new ArgumentNullException(nameof(optionsBuilder)); + if (optionsBuilder == null) + { + throw new ArgumentNullException(nameof(optionsBuilder)); + } _webSocketOptions = new MqttClientWebSocketOptions(); optionsBuilder.Invoke(_webSocketOptions); @@ -247,92 +368,28 @@ namespace MQTTnet.Client.Options return this; } - public MqttClientOptionsBuilder WithTls(MqttClientOptionsBuilderTlsParameters parameters) + public MqttClientOptionsBuilder WithWillDelayInterval(uint willDelayInterval) { - _tlsParameters = parameters; + _options.WillDelayInterval = willDelayInterval; return this; } - public MqttClientOptionsBuilder WithTls() - { - return WithTls(new MqttClientOptionsBuilderTlsParameters { UseTls = true }); - } - - public MqttClientOptionsBuilder WithTls(Action optionsBuilder) + public MqttClientOptionsBuilder WithWillPayload(byte[] willPayload) { - if (optionsBuilder == null) throw new ArgumentNullException(nameof(optionsBuilder)); - - _tlsParameters = new MqttClientOptionsBuilderTlsParameters - { - UseTls = true - }; - - optionsBuilder(_tlsParameters); + _options.WillPayload = willPayload; return this; } - public MqttClientOptionsBuilder WithPacketInspector(IMqttPacketInspector packetInspector) + public MqttClientOptionsBuilder WithWillQualityOfServiceLevel(MqttQualityOfServiceLevel willQualityOfServiceLevel) { - _options.PacketInspector = packetInspector; + _options.WillQualityOfServiceLevel = willQualityOfServiceLevel; return this; } - public IMqttClientOptions Build() + public MqttClientOptionsBuilder WithWillTopic(string willTopic) { - if (_tcpOptions == null && _webSocketOptions == null) - { - throw new InvalidOperationException("A channel must be set."); - } - - if (_tlsParameters != null) - { - if (_tlsParameters?.UseTls == true) - { - var tlsOptions = new MqttClientTlsOptions - { - UseTls = true, - SslProtocol = _tlsParameters.SslProtocol, - AllowUntrustedCertificates = _tlsParameters.AllowUntrustedCertificates, -#if WINDOWS_UWP - Certificates = _tlsParameters.Certificates?.Select(c => c.ToArray()).ToList(), -#else - Certificates = _tlsParameters.Certificates?.ToList(), -#endif -#pragma warning disable CS0618 // Type or member is obsolete - CertificateValidationCallback = _tlsParameters.CertificateValidationCallback, -#pragma warning restore CS0618 // Type or member is obsolete -#if NETCOREAPP3_1 || NET5_0 - ApplicationProtocols = _tlsParameters.ApplicationProtocols, -#endif - CertificateValidationHandler = _tlsParameters.CertificateValidationHandler, - IgnoreCertificateChainErrors = _tlsParameters.IgnoreCertificateChainErrors, - IgnoreCertificateRevocationErrors = _tlsParameters.IgnoreCertificateRevocationErrors - }; - - if (_tcpOptions != null) - { - _tcpOptions.TlsOptions = tlsOptions; - } - else if (_webSocketOptions != null) - { - _webSocketOptions.TlsOptions = tlsOptions; - } - } - } - - if (_proxyOptions != null) - { - if (_webSocketOptions == null) - { - throw new InvalidOperationException("Proxies are only supported for WebSocket connections."); - } - - _webSocketOptions.ProxyOptions = _proxyOptions; - } - - _options.ChannelOptions = (IMqttClientChannelOptions)_tcpOptions ?? _webSocketOptions; - - return _options; + _options.WillTopic = willTopic; + return this; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Options/MqttClientOptionsBuilderTlsParameters.cs b/Source/MQTTnet/Client/Options/MqttClientOptionsBuilderTlsParameters.cs index a523cd0..7ca1d7a 100644 --- a/Source/MQTTnet/Client/Options/MqttClientOptionsBuilderTlsParameters.cs +++ b/Source/MQTTnet/Client/Options/MqttClientOptionsBuilderTlsParameters.cs @@ -1,23 +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 System; using System.Collections.Generic; using System.Net.Security; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; -namespace MQTTnet.Client.Options +namespace MQTTnet.Client { public class MqttClientOptionsBuilderTlsParameters { public bool UseTls { get; set; } - - [Obsolete("This property will be removed soon. Use CertificateValidationHandler instead.")] - public Func CertificateValidationCallback - { - get; - set; - } - - public Func CertificateValidationHandler { get; set; } + + public Func CertificateValidationHandler { get; set; } #if NET48 || NETCOREAPP3_1 || NET5 || NET6 public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls12 | SslProtocols.Tls13; @@ -31,8 +28,8 @@ namespace MQTTnet.Client.Options public IEnumerable Certificates { get; set; } #endif -#if NETCOREAPP3_1 || NET5_0 - public List ApplicationProtocols { get;set; } +#if NETCOREAPP3_1 || NET5_0_OR_GREATER + public List ApplicationProtocols { get;set; } #endif public bool AllowUntrustedCertificates { get; set; } diff --git a/Source/MQTTnet/Client/Options/MqttClientOptionsBuilderWebSocketParameters.cs b/Source/MQTTnet/Client/Options/MqttClientOptionsBuilderWebSocketParameters.cs index f516e83..ba20a10 100644 --- a/Source/MQTTnet/Client/Options/MqttClientOptionsBuilderWebSocketParameters.cs +++ b/Source/MQTTnet/Client/Options/MqttClientOptionsBuilderWebSocketParameters.cs @@ -1,7 +1,11 @@ -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; using System.Net; -namespace MQTTnet.Client.Options +namespace MQTTnet.Client { public class MqttClientOptionsBuilderWebSocketParameters { diff --git a/Source/MQTTnet/Client/Options/MqttClientTcpOptions.cs b/Source/MQTTnet/Client/Options/MqttClientTcpOptions.cs index 797e904..479bb1c 100644 --- a/Source/MQTTnet/Client/Options/MqttClientTcpOptions.cs +++ b/Source/MQTTnet/Client/Options/MqttClientTcpOptions.cs @@ -1,20 +1,26 @@ -using System.Net.Sockets; +// 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.Client.Options +using System.Net.Sockets; + +namespace MQTTnet.Client { - public class MqttClientTcpOptions : IMqttClientChannelOptions + public sealed class MqttClientTcpOptions : IMqttClientChannelOptions { - public string Server { get; set; } - - public int? Port { get; set; } + public AddressFamily AddressFamily { get; set; } = AddressFamily.Unspecified; public int BufferSize { get; set; } = 8192; public bool? DualMode { get; set; } + public LingerOption LingerState { get; set; } = new LingerOption(true, 0); + public bool NoDelay { get; set; } = true; - public AddressFamily AddressFamily { get; set; } = AddressFamily.Unspecified; + public int? Port { get; set; } + + public string Server { get; set; } public MqttClientTlsOptions TlsOptions { get; set; } = new MqttClientTlsOptions(); @@ -23,4 +29,4 @@ namespace MQTTnet.Client.Options return Server + ":" + this.GetPort(); } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Options/MqttClientTcpOptionsExtensions.cs b/Source/MQTTnet/Client/Options/MqttClientTcpOptionsExtensions.cs index 1b49a58..f214150 100644 --- a/Source/MQTTnet/Client/Options/MqttClientTcpOptionsExtensions.cs +++ b/Source/MQTTnet/Client/Options/MqttClientTcpOptionsExtensions.cs @@ -1,6 +1,10 @@ -using System; +// 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.Client.Options +using System; + +namespace MQTTnet.Client { public static class MqttClientTcpOptionsExtensions { diff --git a/Source/MQTTnet/Client/Options/MqttClientTlsOptions.cs b/Source/MQTTnet/Client/Options/MqttClientTlsOptions.cs index 9ad1897..840886c 100644 --- a/Source/MQTTnet/Client/Options/MqttClientTlsOptions.cs +++ b/Source/MQTTnet/Client/Options/MqttClientTlsOptions.cs @@ -1,13 +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 System; using System.Collections.Generic; -using System.Net.Security; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; -namespace MQTTnet.Client.Options +namespace MQTTnet.Client { - public class MqttClientTlsOptions + public sealed class MqttClientTlsOptions { + public Func CertificateValidationHandler { get; set; } + public bool UseTls { get; set; } public bool IgnoreCertificateRevocationErrors { get; set; } @@ -22,8 +27,8 @@ namespace MQTTnet.Client.Options public List Certificates { get; set; } #endif -#if NETCOREAPP3_1 || NET5_0 - public List ApplicationProtocols { get; set; } +#if NETCOREAPP3_1 || NET5_0_OR_GREATER + public List ApplicationProtocols { get; set; } #endif #if NET48 || NETCOREAPP3_1 || NET5 || NET6 @@ -31,10 +36,5 @@ namespace MQTTnet.Client.Options #else public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls12 | (SslProtocols)0x00003000 /*Tls13*/; #endif - - [Obsolete("This property will be removed soon. Use CertificateValidationHandler instead.")] - public Func CertificateValidationCallback { get; set; } - - public Func CertificateValidationHandler { get; set; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Options/MqttClientWebSocketOptions.cs b/Source/MQTTnet/Client/Options/MqttClientWebSocketOptions.cs index d62f1f1..03160a8 100644 --- a/Source/MQTTnet/Client/Options/MqttClientWebSocketOptions.cs +++ b/Source/MQTTnet/Client/Options/MqttClientWebSocketOptions.cs @@ -1,7 +1,11 @@ -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; using System.Net; -namespace MQTTnet.Client.Options +namespace MQTTnet.Client { public class MqttClientWebSocketOptions : IMqttClientChannelOptions { diff --git a/Source/MQTTnet/Client/Options/MqttClientWebSocketProxyOptions.cs b/Source/MQTTnet/Client/Options/MqttClientWebSocketProxyOptions.cs index 7ff40f8..17a6e2d 100644 --- a/Source/MQTTnet/Client/Options/MqttClientWebSocketProxyOptions.cs +++ b/Source/MQTTnet/Client/Options/MqttClientWebSocketProxyOptions.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Client.Options +// 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.Client { public class MqttClientWebSocketProxyOptions { diff --git a/Source/MQTTnet/Client/Publishing/MqttClientPublishReasonCode.cs b/Source/MQTTnet/Client/Publishing/MqttClientPublishReasonCode.cs index 7b485c6..ee9f6c7 100644 --- a/Source/MQTTnet/Client/Publishing/MqttClientPublishReasonCode.cs +++ b/Source/MQTTnet/Client/Publishing/MqttClientPublishReasonCode.cs @@ -1,8 +1,13 @@ -namespace MQTTnet.Client.Publishing +// 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.Client { public enum MqttClientPublishReasonCode { Success = 0, + NoMatchingSubscribers = 16, UnspecifiedError = 128, ImplementationSpecificError = 131, diff --git a/Source/MQTTnet/Client/Publishing/MqttClientPublishResult.cs b/Source/MQTTnet/Client/Publishing/MqttClientPublishResult.cs index 429f130..47d338e 100644 --- a/Source/MQTTnet/Client/Publishing/MqttClientPublishResult.cs +++ b/Source/MQTTnet/Client/Publishing/MqttClientPublishResult.cs @@ -1,11 +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 System.Collections.Generic; using MQTTnet.Packets; -namespace MQTTnet.Client.Publishing +namespace MQTTnet.Client { - public class MqttClientPublishResult + public sealed class MqttClientPublishResult { + /// + /// Gets the packet identifier which was used for this publish. + /// public ushort? PacketIdentifier { get; set; } /// @@ -27,6 +33,6 @@ namespace MQTTnet.Client.Publishing /// The feature is very similar to the HTTP header concept. /// Hint: MQTT 5 feature only. /// - public List UserProperties { get; set; } + public IReadOnlyCollection UserProperties { get; internal set; } } } diff --git a/Source/MQTTnet/Client/Publishing/MqttClientPublishResultFactory.cs b/Source/MQTTnet/Client/Publishing/MqttClientPublishResultFactory.cs new file mode 100644 index 0000000..ea5bc23 --- /dev/null +++ b/Source/MQTTnet/Client/Publishing/MqttClientPublishResultFactory.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Client +{ + public sealed class MqttClientPublishResultFactory + { + static readonly MqttClientPublishResult EmptySuccessResult = new MqttClientPublishResult(); + static readonly IReadOnlyCollection EmptyUserProperties = new List(); + + public MqttClientPublishResult Create(MqttPubAckPacket pubAckPacket) + { + // QoS 0 has no response. So we treat it as a success always. + if (pubAckPacket == null) + { + return EmptySuccessResult; + } + + var result = new MqttClientPublishResult + { + // Both enums have the same values. So it can be easily converted. + ReasonCode = (MqttClientPublishReasonCode)(int)pubAckPacket.ReasonCode, + PacketIdentifier = pubAckPacket.PacketIdentifier, + ReasonString = pubAckPacket.ReasonString, + UserProperties = pubAckPacket.UserProperties ?? EmptyUserProperties + }; + + return result; + } + + public MqttClientPublishResult Create(MqttPubRecPacket pubRecPacket, MqttPubCompPacket pubCompPacket) + { + if (pubRecPacket == null || pubCompPacket == null) + { + return new MqttClientPublishResult + { + ReasonCode = MqttClientPublishReasonCode.UnspecifiedError + }; + } + + MqttClientPublishResult result; + + // The PUBCOMP is the last packet in QoS 2. So we use the results from that instead of PUBREC. + if (pubCompPacket.ReasonCode == MqttPubCompReasonCode.PacketIdentifierNotFound) + { + result = new MqttClientPublishResult + { + PacketIdentifier = pubCompPacket.PacketIdentifier, + ReasonCode = MqttClientPublishReasonCode.UnspecifiedError, + ReasonString = pubCompPacket.ReasonString, + UserProperties = pubCompPacket.UserProperties ?? EmptyUserProperties + }; + + return result; + } + + result = new MqttClientPublishResult + { + PacketIdentifier = pubCompPacket.PacketIdentifier, + ReasonCode = MqttClientPublishReasonCode.Success, + ReasonString = pubCompPacket.ReasonString, + UserProperties = pubCompPacket.UserProperties ?? EmptyUserProperties + }; + + if (pubRecPacket.ReasonCode != MqttPubRecReasonCode.Success) + { + // Both enums share the same values. + result.ReasonCode = (MqttClientPublishReasonCode)pubRecPacket.ReasonCode; + } + + return result; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Receiving/IMqttApplicationMessageReceivedHandler.cs b/Source/MQTTnet/Client/Receiving/IMqttApplicationMessageReceivedHandler.cs deleted file mode 100644 index 3ea7317..0000000 --- a/Source/MQTTnet/Client/Receiving/IMqttApplicationMessageReceivedHandler.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; - -namespace MQTTnet.Client.Receiving -{ - public interface IMqttApplicationMessageReceivedHandler - { - Task HandleApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs); - } -} diff --git a/Source/MQTTnet/MqttApplicationMessageReceivedEventArgs.cs b/Source/MQTTnet/Client/Receiving/MqttApplicationMessageReceivedEventArgs.cs similarity index 73% rename from Source/MQTTnet/MqttApplicationMessageReceivedEventArgs.cs rename to Source/MQTTnet/Client/Receiving/MqttApplicationMessageReceivedEventArgs.cs index ea54dd5..c288ab2 100644 --- a/Source/MQTTnet/MqttApplicationMessageReceivedEventArgs.cs +++ b/Source/MQTTnet/Client/Receiving/MqttApplicationMessageReceivedEventArgs.cs @@ -1,9 +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; using System.Threading; using System.Threading.Tasks; using MQTTnet.Packets; -namespace MQTTnet +namespace MQTTnet.Client { public sealed class MqttApplicationMessageReceivedEventArgs : EventArgs { @@ -23,7 +28,7 @@ namespace MQTTnet _acknowledgeHandler = acknowledgeHandler; } - internal MqttPublishPacket PublishPacket { get; } + internal MqttPublishPacket PublishPacket { get; set; } /// /// Gets the client identifier. @@ -35,7 +40,20 @@ namespace MQTTnet public bool ProcessingFailed { get; set; } + /// + /// Gets or sets the reason code which will be sent to the server. + /// public MqttApplicationMessageReceivedReasonCode ReasonCode { get; set; } = MqttApplicationMessageReceivedReasonCode.Success; + + /// + /// Gets or sets the user properties which will be sent to the server in the ACK packet etc. + /// + public List ResponseUserProperties { get; } = new List(); + + /// + /// Gets or sets the reason string which will be sent to the server in the ACK packet. + /// + public string ResponseReasonString { get; set; } /// /// Gets or sets whether this message was handled. diff --git a/Source/MQTTnet/Client/Receiving/MqttApplicationMessageReceivedHandlerDelegate.cs b/Source/MQTTnet/Client/Receiving/MqttApplicationMessageReceivedHandlerDelegate.cs deleted file mode 100644 index dd25bb5..0000000 --- a/Source/MQTTnet/Client/Receiving/MqttApplicationMessageReceivedHandlerDelegate.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Threading.Tasks; -using MQTTnet.Implementations; - -namespace MQTTnet.Client.Receiving -{ - public sealed class MqttApplicationMessageReceivedHandlerDelegate : IMqttApplicationMessageReceivedHandler - { - readonly Func _handler; - - public MqttApplicationMessageReceivedHandlerDelegate(Action handler) - { - if (handler == null) throw new ArgumentNullException(nameof(handler)); - - _handler = context => - { - handler(context); - return PlatformAbstractionLayer.CompletedTask; - }; - } - - public MqttApplicationMessageReceivedHandlerDelegate(Func handler) - { - _handler = handler ?? throw new ArgumentNullException(nameof(handler)); - } - - public Task HandleApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs context) - { - return _handler(context); - } - } -} diff --git a/Source/MQTTnet/MqttApplicationMessageReceivedReasonCode.cs b/Source/MQTTnet/Client/Receiving/MqttApplicationMessageReceivedReasonCode.cs similarity index 63% rename from Source/MQTTnet/MqttApplicationMessageReceivedReasonCode.cs rename to Source/MQTTnet/Client/Receiving/MqttApplicationMessageReceivedReasonCode.cs index 3cd6e69..1a00bbd 100644 --- a/Source/MQTTnet/MqttApplicationMessageReceivedReasonCode.cs +++ b/Source/MQTTnet/Client/Receiving/MqttApplicationMessageReceivedReasonCode.cs @@ -1,4 +1,8 @@ -namespace MQTTnet +// 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.Client { public enum MqttApplicationMessageReceivedReasonCode { diff --git a/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeOptions.cs b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeOptions.cs index 2e22998..5c1c1ee 100644 --- a/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeOptions.cs +++ b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeOptions.cs @@ -1,9 +1,13 @@ -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; using MQTTnet.Packets; -namespace MQTTnet.Client.Subscribing +namespace MQTTnet.Client { - public class MqttClientSubscribeOptions + public sealed class MqttClientSubscribeOptions { /// /// Gets or sets a list of topic filters the client wants to subscribe to. diff --git a/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeOptionsBuilder.cs b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeOptionsBuilder.cs index 96a6643..c6b5890 100644 --- a/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeOptionsBuilder.cs +++ b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeOptionsBuilder.cs @@ -1,10 +1,14 @@ -using MQTTnet.Packets; -using MQTTnet.Protocol; +// 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.Exceptions; +using MQTTnet.Packets; +using MQTTnet.Protocol; -namespace MQTTnet.Client.Subscribing +namespace MQTTnet.Client { public sealed class MqttClientSubscribeOptionsBuilder { diff --git a/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResult.cs b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResult.cs index 66d68d4..67b5260 100644 --- a/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResult.cs +++ b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResult.cs @@ -1,9 +1,31 @@ -using System.Collections.Generic; +// 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.Client.Subscribing +using System.Collections.Generic; +using MQTTnet.Packets; + +namespace MQTTnet.Client { - public class MqttClientSubscribeResult + public sealed class MqttClientSubscribeResult { - public List Items { get; } = new List(); + public IReadOnlyCollection Items { get; internal set; } + + /// + /// Gets the user properties which were part of the SUBACK packet. + /// MQTTv5 only. + /// + public IReadOnlyCollection UserProperties { get; internal set; } + + /// + /// Gets the reason string. + /// MQTTv5 only. + /// + public string ReasonString { get; internal set; } + + /// + /// Gets the packet identifier which was used. + /// + public ushort PacketIdentifier { get; internal set; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResultCode.cs b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResultCode.cs index 119ec4f..2213f41 100644 --- a/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResultCode.cs +++ b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResultCode.cs @@ -1,11 +1,16 @@ -namespace MQTTnet.Client.Subscribing +// 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.Client { public enum MqttClientSubscribeResultCode { - GrantedQoS0 = 0, - GrantedQoS1 = 1, - GrantedQoS2 = 2, - UnspecifiedError = 128, + GrantedQoS0 = 0x00, + GrantedQoS1 = 0x01, + GrantedQoS2 = 0x02, + UnspecifiedError = 0x80, + ImplementationSpecificError = 131, NotAuthorized = 135, TopicFilterInvalid = 143, diff --git a/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResultFactory.cs b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResultFactory.cs new file mode 100644 index 0000000..a061324 --- /dev/null +++ b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResultFactory.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using MQTTnet.Exceptions; +using MQTTnet.Packets; + +namespace MQTTnet.Client +{ + public sealed class MqttClientSubscribeResultFactory + { + static readonly IReadOnlyCollection EmptyUserProperties = new List(); + + public MqttClientSubscribeResult Create(MqttSubscribePacket subscribePacket, MqttSubAckPacket subAckPacket) + { + if (subscribePacket == null) throw new ArgumentNullException(nameof(subscribePacket)); + if (subAckPacket == null) throw new ArgumentNullException(nameof(subAckPacket)); + + // MQTTv5.0.0 handling. + if (subAckPacket.ReasonCodes.Any() && subAckPacket.ReasonCodes.Count != subscribePacket.TopicFilters.Count) + { + throw new MqttProtocolViolationException( + "The reason codes are not matching the topic filters [MQTT-3.9.3-1]."); + } + + var items = new List(); + for (var i = 0; i < subscribePacket.TopicFilters.Count; i++) + { + items.Add(CreateSubscribeResultItem(i, subscribePacket, subAckPacket)); + } + + var result = new MqttClientSubscribeResult + { + PacketIdentifier = subAckPacket.PacketIdentifier, + ReasonString = subAckPacket.ReasonString, + UserProperties = subAckPacket.UserProperties ?? EmptyUserProperties, + Items = items + }; + + return result; + } + + static MqttClientSubscribeResultItem CreateSubscribeResultItem(int index, MqttSubscribePacket subscribePacket, MqttSubAckPacket subAckPacket) + { + var resultCode = (MqttClientSubscribeResultCode) subAckPacket.ReasonCodes[index]; + + return new MqttClientSubscribeResultItem + { + TopicFilter = subscribePacket.TopicFilters[index], + ResultCode = resultCode, + }; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResultItem.cs b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResultItem.cs index 3e2d27f..f1fcee8 100644 --- a/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResultItem.cs +++ b/Source/MQTTnet/Client/Subscribing/MqttClientSubscribeResultItem.cs @@ -1,25 +1,23 @@ -using System; +// 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.Client.Subscribing +using MQTTnet.Packets; + +namespace MQTTnet.Client { - public class MqttClientSubscribeResultItem + public sealed class MqttClientSubscribeResultItem { - public MqttClientSubscribeResultItem(MqttTopicFilter topicFilter, MqttClientSubscribeResultCode resultCode) - { - TopicFilter = topicFilter ?? throw new ArgumentNullException(nameof(topicFilter)); - ResultCode = resultCode; - } - /// /// Gets or sets the topic filter. /// The topic filter can contain topics and wildcards. /// - public MqttTopicFilter TopicFilter { get; } + public MqttTopicFilter TopicFilter { get; internal set; } /// /// Gets or sets the result code. /// Hint: MQTT 5 feature only. /// - public MqttClientSubscribeResultCode ResultCode { get; } + public MqttClientSubscribeResultCode ResultCode { get; internal set; } } } diff --git a/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeOptions.cs b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeOptions.cs index 09a4ce2..da33108 100644 --- a/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeOptions.cs +++ b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeOptions.cs @@ -1,9 +1,13 @@ -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; using MQTTnet.Packets; -namespace MQTTnet.Client.Unsubscribing +namespace MQTTnet.Client { - public class MqttClientUnsubscribeOptions + public sealed class MqttClientUnsubscribeOptions { /// /// Gets or sets a list of topic filters the client wants to unsubscribe from. diff --git a/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeOptionsBuilder.cs b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeOptionsBuilder.cs index a7f022e..b9aabe4 100644 --- a/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeOptionsBuilder.cs +++ b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeOptionsBuilder.cs @@ -1,8 +1,12 @@ -using MQTTnet.Packets; +// 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; -namespace MQTTnet.Client.Unsubscribing +namespace MQTTnet.Client { public class MqttClientUnsubscribeOptionsBuilder { diff --git a/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResult.cs b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResult.cs index f3e4c4c..6bd6940 100644 --- a/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResult.cs +++ b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResult.cs @@ -1,9 +1,31 @@ -using System.Collections.Generic; +// 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.Client.Unsubscribing +using System.Collections.Generic; +using MQTTnet.Packets; + +namespace MQTTnet.Client { - public class MqttClientUnsubscribeResult + public sealed class MqttClientUnsubscribeResult { - public List Items { get; } =new List(); + public IReadOnlyCollection Items { get; internal set; } + + /// + /// Gets the user properties which were part of the UNSUBACK packet. + /// MQTTv5 only. + /// + public IReadOnlyCollection UserProperties { get; internal set; } + + /// + /// Gets the reason string. + /// MQTTv5 only. + /// + public string ReasonString { get; internal set; } + + /// + /// Gets the packet identifier which was used. + /// + public ushort PacketIdentifier { get; internal set; } } } diff --git a/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultCode.cs b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultCode.cs index 269abc1..521c88d 100644 --- a/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultCode.cs +++ b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultCode.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Client.Unsubscribing +// 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.Client { public enum MqttClientUnsubscribeResultCode { diff --git a/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultFactory.cs b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultFactory.cs new file mode 100644 index 0000000..bee3499 --- /dev/null +++ b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultFactory.cs @@ -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; +using System.Collections.Generic; +using System.Linq; +using MQTTnet.Exceptions; +using MQTTnet.Packets; + +namespace MQTTnet.Client +{ + public sealed class MqttClientUnsubscribeResultFactory + { + static readonly IReadOnlyCollection EmptyUserProperties = new List(); + + public MqttClientUnsubscribeResult Create(MqttUnsubscribePacket unsubscribePacket, MqttUnsubAckPacket unsubAckPacket) + { + if (unsubscribePacket == null) throw new ArgumentNullException(nameof(unsubscribePacket)); + if (unsubAckPacket == null) throw new ArgumentNullException(nameof(unsubAckPacket)); + + // MQTTv3.1.1 has no reason code at all! + if (unsubAckPacket.ReasonCodes != null && unsubAckPacket.ReasonCodes.Count != unsubscribePacket.TopicFilters.Count) + { + throw new MqttProtocolViolationException( + "The return codes are not matching the topic filters [MQTT-3.9.3-1]."); + } + + var items = new List(); + for (var i = 0; i < unsubscribePacket.TopicFilters.Count; i++) + { + items.Add(CreateUnsubscribeResultItem(i, unsubscribePacket, unsubAckPacket)); + } + + var result = new MqttClientUnsubscribeResult + { + PacketIdentifier = unsubAckPacket.PacketIdentifier, + ReasonString = unsubAckPacket.ReasonString, + UserProperties = unsubAckPacket.UserProperties ?? EmptyUserProperties, + Items = items + }; + + return result; + } + + static MqttClientUnsubscribeResultItem CreateUnsubscribeResultItem(int index, MqttUnsubscribePacket unsubscribePacket, MqttUnsubAckPacket unsubAckPacket) + { + var resultCode = MqttClientUnsubscribeResultCode.Success; + + if (unsubAckPacket.ReasonCodes != null) + { + // MQTTv3.1.1 has no reason code and no return code!. + resultCode = (MqttClientUnsubscribeResultCode) unsubAckPacket.ReasonCodes[index]; + } + + return new MqttClientUnsubscribeResultItem + { + TopicFilter = unsubscribePacket.TopicFilters[index], + ResultCode = resultCode + }; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultItem.cs b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultItem.cs index 065fb7f..9f39035 100644 --- a/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultItem.cs +++ b/Source/MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultItem.cs @@ -1,25 +1,21 @@ -using System; +// 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.Client.Unsubscribing +namespace MQTTnet.Client { - public class MqttClientUnsubscribeResultItem + public sealed class MqttClientUnsubscribeResultItem { - public MqttClientUnsubscribeResultItem(string topicFilter, MqttClientUnsubscribeResultCode reasonCode) - { - TopicFilter = topicFilter ?? throw new ArgumentNullException(nameof(topicFilter)); - ReasonCode = reasonCode; - } - /// /// Gets or sets the topic filter. /// The topic filter can contain topics and wildcards. /// - public string TopicFilter { get; } - + public string TopicFilter { get; internal set; } + /// /// Gets or sets the result code. /// Hint: MQTT 5 feature only. /// - public MqttClientUnsubscribeResultCode ReasonCode { get; } + public MqttClientUnsubscribeResultCode ResultCode { get; internal set; } } } \ No newline at end of file diff --git a/Source/MQTTnet/Diagnostics/Logger/IMqttNetLogger.cs b/Source/MQTTnet/Diagnostics/Logger/IMqttNetLogger.cs index 6d3018b..530b976 100644 --- a/Source/MQTTnet/Diagnostics/Logger/IMqttNetLogger.cs +++ b/Source/MQTTnet/Diagnostics/Logger/IMqttNetLogger.cs @@ -1,6 +1,10 @@ -using System; +// 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.Diagnostics.Logger +using System; + +namespace MQTTnet.Diagnostics { public interface IMqttNetLogger { diff --git a/Source/MQTTnet/Diagnostics/Logger/MqttNetEventLogger.cs b/Source/MQTTnet/Diagnostics/Logger/MqttNetEventLogger.cs index 9eb7918..f639d02 100644 --- a/Source/MQTTnet/Diagnostics/Logger/MqttNetEventLogger.cs +++ b/Source/MQTTnet/Diagnostics/Logger/MqttNetEventLogger.cs @@ -1,9 +1,13 @@ -using System; +// 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.Diagnostics.Logger +using System; + +namespace MQTTnet.Diagnostics { /// - /// This logger fires an event when a new message was published. + /// This logger fires an event when a new message was published. /// public sealed class MqttNetEventLogger : IMqttNetLogger { @@ -14,9 +18,9 @@ namespace MQTTnet.Diagnostics.Logger public event EventHandler LogMessagePublished; - public string LogId { get; } + public bool IsEnabled => LogMessagePublished != null; - public bool IsEnabled { get; set; } = true; + public string LogId { get; } public void Publish(MqttNetLogLevel level, string source, string message, object[] parameters, Exception exception) { @@ -28,7 +32,7 @@ namespace MQTTnet.Diagnostics.Logger // might be null after preparing the message. return; } - + if (parameters?.Length > 0 && message?.Length > 0) { try @@ -53,7 +57,7 @@ namespace MQTTnet.Diagnostics.Logger Message = message, Exception = exception }; - + eventHandler.Invoke(this, new MqttNetLogMessagePublishedEventArgs(logMessage)); } } diff --git a/Source/MQTTnet/Diagnostics/Logger/MqttNetLogLevel.cs b/Source/MQTTnet/Diagnostics/Logger/MqttNetLogLevel.cs index f4b07b7..629df73 100644 --- a/Source/MQTTnet/Diagnostics/Logger/MqttNetLogLevel.cs +++ b/Source/MQTTnet/Diagnostics/Logger/MqttNetLogLevel.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Diagnostics.Logger +// 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.Diagnostics { public enum MqttNetLogLevel { diff --git a/Source/MQTTnet/Diagnostics/Logger/MqttNetLogMessage.cs b/Source/MQTTnet/Diagnostics/Logger/MqttNetLogMessage.cs index 8d3b06c..7a806bb 100644 --- a/Source/MQTTnet/Diagnostics/Logger/MqttNetLogMessage.cs +++ b/Source/MQTTnet/Diagnostics/Logger/MqttNetLogMessage.cs @@ -1,6 +1,10 @@ -using System; +// 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.Diagnostics.Logger +using System; + +namespace MQTTnet.Diagnostics { public sealed class MqttNetLogMessage { diff --git a/Source/MQTTnet/Diagnostics/Logger/MqttNetLogMessagePublishedEventArgs.cs b/Source/MQTTnet/Diagnostics/Logger/MqttNetLogMessagePublishedEventArgs.cs index 581921d..f5f8e05 100644 --- a/Source/MQTTnet/Diagnostics/Logger/MqttNetLogMessagePublishedEventArgs.cs +++ b/Source/MQTTnet/Diagnostics/Logger/MqttNetLogMessagePublishedEventArgs.cs @@ -1,6 +1,10 @@ -using System; +// 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.Diagnostics.Logger +using System; + +namespace MQTTnet.Diagnostics { public sealed class MqttNetLogMessagePublishedEventArgs : EventArgs { diff --git a/Source/MQTTnet/Diagnostics/Logger/MqttNetNullLogger.cs b/Source/MQTTnet/Diagnostics/Logger/MqttNetNullLogger.cs index 551afeb..c83fc9a 100644 --- a/Source/MQTTnet/Diagnostics/Logger/MqttNetNullLogger.cs +++ b/Source/MQTTnet/Diagnostics/Logger/MqttNetNullLogger.cs @@ -1,12 +1,18 @@ -using System; +// 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.Diagnostics.Logger +using System; + +namespace MQTTnet.Diagnostics { /// /// This logger does nothing with the messages. /// public sealed class MqttNetNullLogger : IMqttNetLogger { + public static MqttNetNullLogger Instance { get; } = new MqttNetNullLogger(); + public bool IsEnabled { get; } public void Publish(MqttNetLogLevel logLevel, string source, string message, object[] parameters, Exception exception) diff --git a/Source/MQTTnet/Diagnostics/Logger/MqttNetSourceLogger.cs b/Source/MQTTnet/Diagnostics/Logger/MqttNetSourceLogger.cs index 9808ade..438a2f6 100644 --- a/Source/MQTTnet/Diagnostics/Logger/MqttNetSourceLogger.cs +++ b/Source/MQTTnet/Diagnostics/Logger/MqttNetSourceLogger.cs @@ -1,6 +1,10 @@ -using System; +// 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.Diagnostics.Logger +using System; + +namespace MQTTnet.Diagnostics { public sealed class MqttNetSourceLogger { diff --git a/Source/MQTTnet/Diagnostics/Logger/MqttNetSourceLoggerExtensions.cs b/Source/MQTTnet/Diagnostics/Logger/MqttNetSourceLoggerExtensions.cs index a3b7529..18d70a5 100644 --- a/Source/MQTTnet/Diagnostics/Logger/MqttNetSourceLoggerExtensions.cs +++ b/Source/MQTTnet/Diagnostics/Logger/MqttNetSourceLoggerExtensions.cs @@ -1,6 +1,10 @@ -using System; +// 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.Diagnostics.Logger +using System; + +namespace MQTTnet.Diagnostics { public static class MqttNetSourceLoggerExtensions { @@ -126,6 +130,16 @@ namespace MQTTnet.Diagnostics.Logger logger.Publish(MqttNetLogLevel.Warning, message, new object[] {parameter1}, null); } + public static void Warning(this MqttNetSourceLogger logger, string message, TParameter1 parameter1, TParameter2 parameter2) + { + if (!logger.IsEnabled) + { + return; + } + + logger.Publish(MqttNetLogLevel.Warning, message, new object[] {parameter1, parameter2}, null); + } + public static void Warning(this MqttNetSourceLogger logger, string message) { if (!logger.IsEnabled) diff --git a/Source/MQTTnet/Diagnostics/PacketInspection/IMqttPacketInspector.cs b/Source/MQTTnet/Diagnostics/PacketInspection/IMqttPacketInspector.cs deleted file mode 100644 index 9d7db80..0000000 --- a/Source/MQTTnet/Diagnostics/PacketInspection/IMqttPacketInspector.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MQTTnet.Diagnostics.PacketInspection -{ - public interface IMqttPacketInspector - { - void ProcessMqttPacket(ProcessMqttPacketContext context); - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Diagnostics/PacketInspection/InspectMqttPacketEventArgs.cs b/Source/MQTTnet/Diagnostics/PacketInspection/InspectMqttPacketEventArgs.cs new file mode 100644 index 0000000..6d9e3a6 --- /dev/null +++ b/Source/MQTTnet/Diagnostics/PacketInspection/InspectMqttPacketEventArgs.cs @@ -0,0 +1,15 @@ +// 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.Diagnostics +{ + public sealed class InspectMqttPacketEventArgs : EventArgs + { + public MqttPacketFlowDirection Direction { get; internal set; } + + public byte[] Buffer { get; set; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Diagnostics/PacketInspection/MqttPacketFlowDirection.cs b/Source/MQTTnet/Diagnostics/PacketInspection/MqttPacketFlowDirection.cs index 7f39117..1d9f5af 100644 --- a/Source/MQTTnet/Diagnostics/PacketInspection/MqttPacketFlowDirection.cs +++ b/Source/MQTTnet/Diagnostics/PacketInspection/MqttPacketFlowDirection.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Diagnostics.PacketInspection +// 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.Diagnostics { public enum MqttPacketFlowDirection { diff --git a/Source/MQTTnet/Diagnostics/PacketInspection/ProcessMqttPacketContext.cs b/Source/MQTTnet/Diagnostics/PacketInspection/ProcessMqttPacketContext.cs deleted file mode 100644 index 644b2a7..0000000 --- a/Source/MQTTnet/Diagnostics/PacketInspection/ProcessMqttPacketContext.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MQTTnet.Diagnostics.PacketInspection -{ - public sealed class ProcessMqttPacketContext - { - public MqttPacketFlowDirection Direction { get; set; } - - public byte[] Buffer { get; set; } - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Diagnostics/Runtime/TargetFrameworkProvider.cs b/Source/MQTTnet/Diagnostics/Runtime/TargetFrameworkProvider.cs index abe8631..046c3dc 100644 --- a/Source/MQTTnet/Diagnostics/Runtime/TargetFrameworkProvider.cs +++ b/Source/MQTTnet/Diagnostics/Runtime/TargetFrameworkProvider.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Diagnostics.Runtime +// 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.Diagnostics { public static class TargetFrameworkProvider { @@ -24,6 +28,8 @@ return "netcoreapp3.1"; #elif NET5_0 return "net5.0"; +#elif NET6_0 + return "net6.0"; #endif } } diff --git a/Source/MQTTnet/Exceptions/MqttCommunicationException.cs b/Source/MQTTnet/Exceptions/MqttCommunicationException.cs index 302107a..6c6ba05 100644 --- a/Source/MQTTnet/Exceptions/MqttCommunicationException.cs +++ b/Source/MQTTnet/Exceptions/MqttCommunicationException.cs @@ -1,4 +1,8 @@ -using System; +// 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.Exceptions { diff --git a/Source/MQTTnet/Exceptions/MqttCommunicationTimedOutException.cs b/Source/MQTTnet/Exceptions/MqttCommunicationTimedOutException.cs index 38ee182..5d41c1b 100644 --- a/Source/MQTTnet/Exceptions/MqttCommunicationTimedOutException.cs +++ b/Source/MQTTnet/Exceptions/MqttCommunicationTimedOutException.cs @@ -1,10 +1,14 @@ -using System; +// 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.Exceptions { - public class MqttCommunicationTimedOutException : MqttCommunicationException + public sealed class MqttCommunicationTimedOutException : MqttCommunicationException { - public MqttCommunicationTimedOutException() + public MqttCommunicationTimedOutException() : base("The operation has timed out.") { } diff --git a/Source/MQTTnet/Exceptions/MqttConfigurationException.cs b/Source/MQTTnet/Exceptions/MqttConfigurationException.cs index 4d10faf..9e2bc58 100644 --- a/Source/MQTTnet/Exceptions/MqttConfigurationException.cs +++ b/Source/MQTTnet/Exceptions/MqttConfigurationException.cs @@ -1,4 +1,8 @@ -using System; +// 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.Exceptions { diff --git a/Source/MQTTnet/Exceptions/MqttProtocolViolationException.cs b/Source/MQTTnet/Exceptions/MqttProtocolViolationException.cs index 262f4af..e8229a8 100644 --- a/Source/MQTTnet/Exceptions/MqttProtocolViolationException.cs +++ b/Source/MQTTnet/Exceptions/MqttProtocolViolationException.cs @@ -1,4 +1,8 @@ -using System; +// 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.Exceptions { diff --git a/Source/MQTTnet/Exceptions/MqttUnexpectedDisconnectReceivedException.cs b/Source/MQTTnet/Exceptions/MqttUnexpectedDisconnectReceivedException.cs index 8edb2b2..eca736f 100644 --- a/Source/MQTTnet/Exceptions/MqttUnexpectedDisconnectReceivedException.cs +++ b/Source/MQTTnet/Exceptions/MqttUnexpectedDisconnectReceivedException.cs @@ -1,4 +1,8 @@ -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; using MQTTnet.Packets; using MQTTnet.Protocol; @@ -10,10 +14,10 @@ namespace MQTTnet.Exceptions : base($"Unexpected DISCONNECT (Reason code={disconnectPacket.ReasonCode}) received.") { ReasonCode = disconnectPacket.ReasonCode; - SessionExpiryInterval = disconnectPacket.Properties?.SessionExpiryInterval; - ReasonString = disconnectPacket.Properties?.ReasonString; - ServerReference = disconnectPacket.Properties?.ServerReference; - UserProperties = disconnectPacket.Properties?.UserProperties; + SessionExpiryInterval = disconnectPacket.SessionExpiryInterval; + ReasonString = disconnectPacket.ReasonString; + ServerReference = disconnectPacket.ServerReference; + UserProperties = disconnectPacket.UserProperties; } public MqttDisconnectReasonCode? ReasonCode { get; } diff --git a/Source/MQTTnet/Extensions/MqttClientOptionsBuilderExtension.cs b/Source/MQTTnet/Extensions/MqttClientOptionsBuilderExtension.cs deleted file mode 100644 index fc9da23..0000000 --- a/Source/MQTTnet/Extensions/MqttClientOptionsBuilderExtension.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Linq; -using MQTTnet.Client.Options; - -namespace MQTTnet.Extensions -{ - public static class MqttClientOptionsBuilderExtension - { - public static MqttClientOptionsBuilder WithConnectionUri(this MqttClientOptionsBuilder builder, Uri uri) - { - var port = uri.IsDefaultPort ? null : (int?) uri.Port; - switch (uri.Scheme.ToLower()) - { - case "tcp": - case "mqtt": - builder.WithTcpServer(uri.Host, port); - break; - - case "mqtts": - builder.WithTcpServer(uri.Host, port).WithTls(); - break; - - case "ws": - case "wss": - builder.WithWebSocketServer(uri.ToString()); - break; - - default: - throw new ArgumentException("Unexpected scheme in uri."); - } - - if (!string.IsNullOrEmpty(uri.UserInfo)) - { - var userInfo = uri.UserInfo.Split(':'); - var username = userInfo[0]; - var password = userInfo.Length > 1 ? userInfo[1] : ""; - builder.WithCredentials(username, password); - } - - return builder; - } - - public static MqttClientOptionsBuilder WithConnectionUri(this MqttClientOptionsBuilder builder, string uri) - { - return WithConnectionUri(builder, new Uri(uri, UriKind.Absolute)); - } - } -} diff --git a/Source/MQTTnet/Extensions/UserPropertyExtension.cs b/Source/MQTTnet/Extensions/UserPropertyExtension.cs deleted file mode 100644 index f861c32..0000000 --- a/Source/MQTTnet/Extensions/UserPropertyExtension.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Linq; - -namespace MQTTnet.Extensions -{ - public static class UserPropertyExtension - { - public static string GetUserProperty(this MqttApplicationMessage message, string propertyName, StringComparison comparisonType = StringComparison.Ordinal) - { - if (message == null) throw new ArgumentNullException(nameof(message)); - if (propertyName == null) throw new ArgumentNullException(nameof(propertyName)); - - return message.UserProperties?.SingleOrDefault(up => up.Name.Equals(propertyName, comparisonType))?.Value; - } - } -} diff --git a/Source/MQTTnet/Formatter/IMqttDataConverter.cs b/Source/MQTTnet/Formatter/IMqttDataConverter.cs deleted file mode 100644 index 7f23f62..0000000 --- a/Source/MQTTnet/Formatter/IMqttDataConverter.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Collections.Generic; -using MQTTnet.Client.Connecting; -using MQTTnet.Client.Disconnecting; -using MQTTnet.Client.Options; -using MQTTnet.Client.Publishing; -using MQTTnet.Client.Subscribing; -using MQTTnet.Client.Unsubscribing; -using MQTTnet.Packets; -using MQTTnet.Protocol; -using MQTTnet.Server; -using MQTTnet.Server.Internal; - -namespace MQTTnet.Formatter -{ - public interface IMqttDataConverter - { - MqttApplicationMessage CreateApplicationMessage(MqttPublishPacket publishPacket); - - MqttClientConnectResult CreateClientConnectResult(MqttConnAckPacket connAckPacket); - - MqttClientPublishResult CreateClientPublishResult(MqttPubAckPacket pubAckPacket); - - MqttClientPublishResult CreateClientPublishResult(MqttPubRecPacket pubRecPacket, MqttPubCompPacket pubCompPacket); - - MqttClientSubscribeResult CreateClientSubscribeResult(MqttSubscribePacket subscribePacket, MqttSubAckPacket subAckPacket); - - MqttClientUnsubscribeResult CreateClientUnsubscribeResult(MqttUnsubscribePacket unsubscribePacket, MqttUnsubAckPacket unsubAckPacket); - - MqttConnectPacket CreateConnectPacket(MqttApplicationMessage willApplicationMessage, IMqttClientOptions options); - - MqttConnAckPacket CreateConnAckPacket(MqttConnectionValidatorContext connectionValidatorContext); - - MqttPublishPacket CreatePublishPacket(MqttApplicationMessage applicationMessage); - - MqttPubAckPacket CreatePubAckPacket(MqttPublishPacket publishPacket, MqttApplicationMessageReceivedReasonCode reasonCode); - - MqttPubRecPacket CreatePubRecPacket(MqttPublishPacket publishPacket, MqttApplicationMessageReceivedReasonCode reasonCode); - - MqttPubCompPacket CreatePubCompPacket(MqttPubRelPacket pubRelPacket, MqttApplicationMessageReceivedReasonCode reasonCode); - - MqttPubRelPacket CreatePubRelPacket(MqttPubRecPacket pubRecPacket, MqttApplicationMessageReceivedReasonCode reasonCode); - - MqttSubscribePacket CreateSubscribePacket(MqttClientSubscribeOptions options); - - MqttSubAckPacket CreateSubAckPacket(MqttSubscribePacket subscribePacket, SubscribeResult subscribeResult); - - MqttUnsubscribePacket CreateUnsubscribePacket(MqttClientUnsubscribeOptions options); - - MqttUnsubAckPacket CreateUnsubAckPacket(MqttUnsubscribePacket unsubscribePacket, List reasonCodes); - - MqttDisconnectPacket CreateDisconnectPacket(MqttClientDisconnectOptions options); - } -} diff --git a/Source/MQTTnet/Formatter/IMqttPacketBodyReader.cs b/Source/MQTTnet/Formatter/IMqttPacketBodyReader.cs deleted file mode 100644 index fdf8764..0000000 --- a/Source/MQTTnet/Formatter/IMqttPacketBodyReader.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace MQTTnet.Formatter -{ - public interface IMqttPacketBodyReader - { - int Length { get; } - - int Offset { get; } - - bool EndOfStream { get; } - - byte ReadByte(); - - byte[] ReadRemainingData(); - - ushort ReadTwoByteInteger(); - - string ReadStringWithLengthPrefix(); - - byte[] ReadWithLengthPrefix(); - - uint ReadFourByteInteger(); - - uint ReadVariableLengthInteger(); - - bool ReadBoolean(); - - void Seek(int position); - } -} diff --git a/Source/MQTTnet/Formatter/IMqttPacketFormatter.cs b/Source/MQTTnet/Formatter/IMqttPacketFormatter.cs index 6dd417c..24210b8 100644 --- a/Source/MQTTnet/Formatter/IMqttPacketFormatter.cs +++ b/Source/MQTTnet/Formatter/IMqttPacketFormatter.cs @@ -1,4 +1,7 @@ -using System; +// 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.Packets; @@ -6,12 +9,8 @@ namespace MQTTnet.Formatter { public interface IMqttPacketFormatter { - IMqttDataConverter DataConverter { get; } - - ArraySegment Encode(MqttBasePacket mqttPacket); - - MqttBasePacket Decode(ReceivedMqttPacket receivedMqttPacket); - - void FreeBuffer(); + MqttPacket Decode(ReceivedMqttPacket receivedMqttPacket); + + MqttPacketBuffer Encode(MqttPacket mqttPacket); } } \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/IMqttPacketWriter.cs b/Source/MQTTnet/Formatter/IMqttPacketWriter.cs deleted file mode 100644 index bae6dec..0000000 --- a/Source/MQTTnet/Formatter/IMqttPacketWriter.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace MQTTnet.Formatter -{ - public interface IMqttPacketWriter - { - int Length { get; } - - void WriteWithLengthPrefix(string value); - - void Write(byte value); - - void WriteWithLengthPrefix(byte[] value); - - void Write(ushort value); - - void Write(IMqttPacketWriter value); - - void WriteVariableLengthInteger(uint value); - - void Write(byte[] value, int offset, int length); - - void Reset(int length); - - void Seek(int offset); - - void FreeBuffer(); - - byte[] GetBuffer(); - } -} diff --git a/Source/MQTTnet/Formatter/MqttApplicationMessageFactory.cs b/Source/MQTTnet/Formatter/MqttApplicationMessageFactory.cs new file mode 100644 index 0000000..0e80f1d --- /dev/null +++ b/Source/MQTTnet/Formatter/MqttApplicationMessageFactory.cs @@ -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 MQTTnet.Packets; + +namespace MQTTnet.Formatter +{ + public sealed class MqttApplicationMessageFactory + { + public MqttApplicationMessage Create(MqttPublishPacket publishPacket) + { + if (publishPacket == null) + { + throw new ArgumentNullException(nameof(publishPacket)); + } + + return new MqttApplicationMessage + { + Topic = publishPacket.Topic, + Payload = publishPacket.Payload, + QualityOfServiceLevel = publishPacket.QualityOfServiceLevel, + Retain = publishPacket.Retain, + Dup = publishPacket.Dup, + ResponseTopic = publishPacket.ResponseTopic, + ContentType = publishPacket.ContentType, + CorrelationData = publishPacket.CorrelationData, + MessageExpiryInterval = publishPacket.MessageExpiryInterval, + SubscriptionIdentifiers = publishPacket.SubscriptionIdentifiers, + TopicAlias = publishPacket.TopicAlias, + PayloadFormatIndicator = publishPacket.PayloadFormatIndicator, + UserProperties = publishPacket.UserProperties + }; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttBufferReader.cs b/Source/MQTTnet/Formatter/MqttBufferReader.cs new file mode 100644 index 0000000..75f367d --- /dev/null +++ b/Source/MQTTnet/Formatter/MqttBufferReader.cs @@ -0,0 +1,150 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; +using MQTTnet.Exceptions; +using MQTTnet.Implementations; +using MQTTnet.Protocol; + +namespace MQTTnet.Formatter +{ + public sealed class MqttBufferReader + { + byte[] _buffer = PlatformAbstractionLayer.EmptyByteArray; + int _initialPosition; + int _length; + int _position; + + public bool EndOfStream => _position == _length; + + public int Position => _position; + + public int BytesLeft => _length - _position; + + public byte[] ReadBinaryData() + { + var length = ReadTwoByteInteger(); + + if (length == 0) + { + return PlatformAbstractionLayer.EmptyByteArray; + } + + ValidateReceiveBuffer(length); + + var result = new byte[length]; + Array.Copy(_buffer, _position, result, 0, length); + _position += length; + + return result; + } + + public byte ReadByte() + { + ValidateReceiveBuffer(1); + + return _buffer[_position++]; + } + + public uint ReadFourByteInteger() + { + ValidateReceiveBuffer(4); + + var byte0 = _buffer[_position++]; + var byte1 = _buffer[_position++]; + var byte2 = _buffer[_position++]; + var byte3 = _buffer[_position++]; + + return (uint)((byte0 << 24) | (byte1 << 16) | (byte2 << 8) | byte3); + } + + public byte[] ReadRemainingData() + { + var bufferLength = _length - _position; + + if (bufferLength == 0) + { + return PlatformAbstractionLayer.EmptyByteArray; + } + + var buffer = new byte[bufferLength]; + Array.Copy(_buffer, _position, buffer, 0, bufferLength); + + return buffer; + } + + public string ReadString() + { + var length = ReadTwoByteInteger(); + + if (length == 0) + { + return string.Empty; + } + + ValidateReceiveBuffer(length); + + var result = Encoding.UTF8.GetString(_buffer, _position, length); + _position += length; + return result; + } + + public ushort ReadTwoByteInteger() + { + ValidateReceiveBuffer(2); + + var msb = _buffer[_position++]; + var lsb = _buffer[_position++]; + + return (ushort)((msb << 8) | lsb); + } + + public uint ReadVariableByteInteger() + { + var multiplier = 1; + var value = 0U; + byte encodedByte; + + do + { + encodedByte = ReadByte(); + value += (uint)((encodedByte & 127) * multiplier); + + if (multiplier > 2097152) + { + throw new MqttProtocolViolationException("Variable length integer is invalid."); + } + + multiplier *= 128; + } while ((encodedByte & 128) != 0); + + return value; + } + + public void Seek(int position) + { + _position = _initialPosition + position; + } + + public void SetBuffer(byte[] buffer, int position, int length) + { + _buffer = buffer; + _initialPosition = position; + _position = position; + _length = length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void ValidateReceiveBuffer(int length) + { + if (_length < _position + length) + { + throw new MqttProtocolViolationException($"Expected at least {Position + length} bytes but there are only {_length} bytes"); + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttBufferWriter.cs b/Source/MQTTnet/Formatter/MqttBufferWriter.cs new file mode 100644 index 0000000..701908b --- /dev/null +++ b/Source/MQTTnet/Formatter/MqttBufferWriter.cs @@ -0,0 +1,284 @@ +// 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.Runtime.CompilerServices; +using System.Text; +using MQTTnet.Exceptions; +using MQTTnet.Protocol; + +namespace MQTTnet.Formatter +{ + /// + /// This is a custom implementation of a memory stream which provides only MQTTnet relevant features. + /// The goal is to avoid lots of argument checks like in the original stream. The growth rule is the + /// same as for the original MemoryStream in .net. Also this implementation allows accessing the internal + /// buffer for all platforms and .net framework versions (which is not available at the regular MemoryStream). + /// + public sealed class MqttBufferWriter + { + public const uint VariableByteIntegerMaxValue = 268435455; + + readonly int _maxBufferSize; + + byte[] _buffer; + int _position; + + public MqttBufferWriter(int bufferSize, int maxBufferSize) + { + _buffer = new byte[bufferSize]; + _maxBufferSize = maxBufferSize; + } + + public int Length { get; private set; } + + public static byte BuildFixedHeader(MqttControlPacketType packetType, byte flags = 0) + { + var fixedHeader = (int)packetType << 4; + fixedHeader |= flags; + return (byte)fixedHeader; + } + + public void Cleanup() + { + // This method frees the used memory by shrinking the buffer. This is required because the buffer + // is used across several messages. In general this is not a big issue because subsequent Ping packages + // have the same size but a very big publish package with 100 MB of payload will increase the buffer + // a lot and the size will never reduced. So this method tries to find a size which can be held in + // memory for a long time without causing troubles. + + if (_buffer.Length <= _maxBufferSize) + { + return; + } + + // Create a new and empty buffer. Do not use Array.Resize because it will copy all data from + // the old array to the new one which is not required in this case. + _buffer = new byte[_maxBufferSize]; + } + + public byte[] GetBuffer() + { + return _buffer; + } + + public static int GetLengthOfVariableInteger(uint value) + { + var result = 0; + var x = value; + do + { + x /= 128; + result++; + } while (x > 0); + + return result; + } + + public void Reset(int length) + { + _position = 0; + Length = length; + } + + public void Seek(int position) + { + EnsureCapacity(position); + _position = position; + } + + public void Write(MqttBufferWriter propertyWriter) + { + if (propertyWriter is MqttBufferWriter writer) + { + WriteBinary(writer._buffer, 0, writer.Length); + return; + } + + if (propertyWriter == null) + { + throw new ArgumentNullException(nameof(propertyWriter)); + } + + throw new InvalidOperationException($"{nameof(propertyWriter)} must be of type {nameof(MqttBufferWriter)}"); + } + + public void WriteBinary(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (count == 0) + { + return; + } + + EnsureAdditionalCapacity(count); + + Array.Copy(buffer, offset, _buffer, _position, count); + IncreasePosition(count); + } + + public void WriteBinaryData(byte[] value) + { + if (value == null || value.Length == 0) + { + EnsureAdditionalCapacity(2); + + _buffer[_position] = 0; + _buffer[_position + 1] = 0; + + IncreasePosition(2); + } + else + { + var valueLength = value.Length; + + EnsureAdditionalCapacity(valueLength + 2); + + _buffer[_position] = (byte)(valueLength >> 8); + _buffer[_position + 1] = (byte)valueLength; + + Array.Copy(value, 0, _buffer, _position + 2, valueLength); + IncreasePosition(valueLength + 2); + } + } + + public void WriteByte(byte @byte) + { + EnsureAdditionalCapacity(1); + + _buffer[_position] = @byte; + IncreasePosition(1); + } + + public void WriteString(string value) + { + if (string.IsNullOrEmpty(value)) + { + EnsureAdditionalCapacity(2); + + _buffer[_position] = 0; + _buffer[_position + 1] = 0; + + IncreasePosition(2); + } + else + { + // Do not use Encoding.UTF8.GetByteCount(value); + // UTF8 chars can have a max length of 4 and the used buffer increase *2 every time. + // So the buffer should always have much more capacity left so that a correct value + // here is only waste of CPU cycles. + var byteCount = value.Length * 4; + + EnsureAdditionalCapacity(byteCount + 2); + + var writtenBytes = Encoding.UTF8.GetBytes(value, 0, value.Length, _buffer, _position + 2); + + _buffer[_position] = (byte)(writtenBytes >> 8); + _buffer[_position + 1] = (byte)writtenBytes; + + IncreasePosition(writtenBytes + 2); + } + } + + public void WriteTwoByteInteger(ushort value) + { + EnsureAdditionalCapacity(2); + + _buffer[_position] = (byte)(value >> 8); + IncreasePosition(1); + _buffer[_position] = (byte)value; + IncreasePosition(1); + } + + public void WriteVariableByteInteger(uint value) + { + if (value == 0) + { + _buffer[_position] = 0; + IncreasePosition(1); + + return; + } + + if (value <= 127) + { + _buffer[_position] = (byte)value; + IncreasePosition(1); + + return; + } + + if (value > VariableByteIntegerMaxValue) + { + throw new MqttProtocolViolationException($"The specified value ({value}) is too large for a variable byte integer."); + } + + var size = 0; + var x = value; + do + { + var encodedByte = x % 128; + x /= 128; + if (x > 0) + { + encodedByte |= 128; + } + + _buffer[_position + size] = (byte)encodedByte; + size++; + } while (x > 0); + + IncreasePosition(size); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void EnsureAdditionalCapacity(int additionalCapacity) + { + var bufferLength = _buffer.Length; + + var freeSpace = bufferLength - _position; + if (freeSpace >= additionalCapacity) + { + return; + } + + EnsureCapacity(bufferLength + additionalCapacity - freeSpace); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void EnsureCapacity(int capacity) + { + var newBufferLength = _buffer.Length; + + if (newBufferLength >= capacity) + { + return; + } + + while (newBufferLength < capacity) + { + newBufferLength *= 2; + } + + // Array.Resize will create a new array and copy the existing + // data to the new one. + Array.Resize(ref _buffer, newBufferLength); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void IncreasePosition(int length) + { + _position += length; + + if (_position > Length) + { + Length = _position; + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttConnAckPacketFactory.cs b/Source/MQTTnet/Formatter/MqttConnAckPacketFactory.cs new file mode 100644 index 0000000..cf8e2a3 --- /dev/null +++ b/Source/MQTTnet/Formatter/MqttConnAckPacketFactory.cs @@ -0,0 +1,41 @@ +// 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; +using MQTTnet.Server; + +namespace MQTTnet.Formatter +{ + public sealed class MqttConnAckPacketFactory + { + public 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, + WildcardSubscriptionAvailable = true, + + AuthenticationMethod = validatingConnectionEventArgs.AuthenticationMethod, + AuthenticationData = validatingConnectionEventArgs.ResponseAuthenticationData, + AssignedClientIdentifier = validatingConnectionEventArgs.AssignedClientIdentifier, + ReasonString = validatingConnectionEventArgs.ReasonString, + ServerReference = validatingConnectionEventArgs.ServerReference, + UserProperties = validatingConnectionEventArgs.ResponseUserProperties + }; + + return connAckPacket; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttConnectPacketFactory.cs b/Source/MQTTnet/Formatter/MqttConnectPacketFactory.cs new file mode 100644 index 0000000..d818630 --- /dev/null +++ b/Source/MQTTnet/Formatter/MqttConnectPacketFactory.cs @@ -0,0 +1,58 @@ +// 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.Client; +using MQTTnet.Packets; + +namespace MQTTnet.Formatter +{ + public sealed class MqttConnectPacketFactory + { + public MqttConnectPacket Create(MqttClientOptions clientOptions) + { + if (clientOptions == null) + { + throw new ArgumentNullException(nameof(clientOptions)); + } + + var connectPacket = new MqttConnectPacket + { + ClientId = clientOptions.ClientId, + Username = clientOptions.Credentials?.GetUserName(clientOptions), + Password = clientOptions.Credentials?.GetPassword(clientOptions), + CleanSession = clientOptions.CleanSession, + KeepAlivePeriod = (ushort)clientOptions.KeepAlivePeriod.TotalSeconds, + AuthenticationMethod = clientOptions.AuthenticationMethod, + AuthenticationData = clientOptions.AuthenticationData, + WillDelayInterval = clientOptions.WillDelayInterval, + MaximumPacketSize = clientOptions.MaximumPacketSize, + ReceiveMaximum = clientOptions.ReceiveMaximum, + RequestProblemInformation = clientOptions.RequestProblemInformation, + RequestResponseInformation = clientOptions.RequestResponseInformation, + SessionExpiryInterval = clientOptions.SessionExpiryInterval, + TopicAliasMaximum = clientOptions.TopicAliasMaximum, + UserProperties = clientOptions.UserProperties + }; + + if (!string.IsNullOrEmpty(clientOptions.WillTopic)) + { + connectPacket.WillFlag = true; + connectPacket.WillTopic = clientOptions.WillTopic; + connectPacket.WillQoS = clientOptions.WillQualityOfServiceLevel; + connectPacket.WillMessage = clientOptions.WillPayload; + connectPacket.WillRetain = clientOptions.WillRetain; + connectPacket.WillDelayInterval = clientOptions.WillDelayInterval; + connectPacket.WillContentType = clientOptions.WillContentType; + connectPacket.WillCorrelationData = clientOptions.WillCorrelationData; + connectPacket.WillResponseTopic = clientOptions.WillResponseTopic; + connectPacket.WillMessageExpiryInterval = clientOptions.WillMessageExpiryInterval; + connectPacket.WillPayloadFormatIndicator = clientOptions.WillPayloadFormatIndicator; + connectPacket.WillUserProperties = clientOptions.WillUserProperties; + } + + return connectPacket; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttConnectReasonCodeConverter.cs b/Source/MQTTnet/Formatter/MqttConnectReasonCodeConverter.cs new file mode 100644 index 0000000..6278a60 --- /dev/null +++ b/Source/MQTTnet/Formatter/MqttConnectReasonCodeConverter.cs @@ -0,0 +1,96 @@ +// 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.Protocol; + +namespace MQTTnet.Formatter +{ + public static class MqttConnectReasonCodeConverter + { + public static MqttConnectReasonCode ToConnectReasonCode(MqttConnectReturnCode returnCode) + { + switch (returnCode) + { + case MqttConnectReturnCode.ConnectionAccepted: + { + return MqttConnectReasonCode.Success; + } + + case MqttConnectReturnCode.ConnectionRefusedUnacceptableProtocolVersion: + { + return MqttConnectReasonCode.UnsupportedProtocolVersion; + } + + case MqttConnectReturnCode.ConnectionRefusedBadUsernameOrPassword: + { + return MqttConnectReasonCode.BadUserNameOrPassword; + } + + case MqttConnectReturnCode.ConnectionRefusedIdentifierRejected: + { + return MqttConnectReasonCode.ClientIdentifierNotValid; + } + + case MqttConnectReturnCode.ConnectionRefusedServerUnavailable: + { + return MqttConnectReasonCode.ServerUnavailable; + } + + case MqttConnectReturnCode.ConnectionRefusedNotAuthorized: + { + return MqttConnectReasonCode.NotAuthorized; + } + + default: + { + throw new MqttProtocolViolationException("Unable to convert connect reason code (MQTTv5) to return code (MQTTv3)."); + } + } + } + + public static MqttConnectReturnCode ToConnectReturnCode(MqttConnectReasonCode reasonCode) + { + switch (reasonCode) + { + case MqttConnectReasonCode.Success: + { + return MqttConnectReturnCode.ConnectionAccepted; + } + + case MqttConnectReasonCode.NotAuthorized: + { + return MqttConnectReturnCode.ConnectionRefusedNotAuthorized; + } + + case MqttConnectReasonCode.BadUserNameOrPassword: + { + return MqttConnectReturnCode.ConnectionRefusedBadUsernameOrPassword; + } + + case MqttConnectReasonCode.ClientIdentifierNotValid: + { + return MqttConnectReturnCode.ConnectionRefusedIdentifierRejected; + } + + case MqttConnectReasonCode.UnsupportedProtocolVersion: + { + return MqttConnectReturnCode.ConnectionRefusedUnacceptableProtocolVersion; + } + + case MqttConnectReasonCode.ServerUnavailable: + case MqttConnectReasonCode.ServerBusy: + case MqttConnectReasonCode.ServerMoved: + { + return MqttConnectReturnCode.ConnectionRefusedServerUnavailable; + } + + default: + { + throw new MqttProtocolViolationException("Unable to convert connect reason code (MQTTv5) to return code (MQTTv3)."); + } + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttDisconnectPacketFactory.cs b/Source/MQTTnet/Formatter/MqttDisconnectPacketFactory.cs new file mode 100644 index 0000000..889bb97 --- /dev/null +++ b/Source/MQTTnet/Formatter/MqttDisconnectPacketFactory.cs @@ -0,0 +1,47 @@ +// 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.Client; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Formatter +{ + public sealed class MqttDisconnectPacketFactory + { + readonly MqttDisconnectPacket _normalDisconnection = new MqttDisconnectPacket + { + ReasonCode = MqttDisconnectReasonCode.NormalDisconnection + }; + + public MqttDisconnectPacket Create(MqttDisconnectReasonCode reasonCode) + { + if (reasonCode == MqttDisconnectReasonCode.NormalDisconnection) + { + return _normalDisconnection; + } + + return new MqttDisconnectPacket + { + ReasonCode = reasonCode + }; + } + + public MqttDisconnectPacket Create(MqttClientDisconnectOptions clientDisconnectOptions) + { + var packet = new MqttDisconnectPacket(); + + if (clientDisconnectOptions == null) + { + packet.ReasonCode = MqttDisconnectReasonCode.NormalDisconnection; + } + else + { + packet.ReasonCode = (MqttDisconnectReasonCode)clientDisconnectOptions.Reason; + } + + return packet; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttFixedHeader.cs b/Source/MQTTnet/Formatter/MqttFixedHeader.cs index 4c69333..cb446d6 100644 --- a/Source/MQTTnet/Formatter/MqttFixedHeader.cs +++ b/Source/MQTTnet/Formatter/MqttFixedHeader.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Formatter +// 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.Formatter { public struct MqttFixedHeader { @@ -15,4 +19,4 @@ public int TotalLength { get; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttPacketBodyReader.cs b/Source/MQTTnet/Formatter/MqttPacketBodyReader.cs deleted file mode 100644 index 82478e1..0000000 --- a/Source/MQTTnet/Formatter/MqttPacketBodyReader.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using MQTTnet.Exceptions; - -namespace MQTTnet.Formatter -{ - public sealed class MqttPacketBodyReader : IMqttPacketBodyReader - { - readonly byte[] _buffer; - readonly int _initialOffset; - readonly int _length; - - int _offset; - - public MqttPacketBodyReader(byte[] buffer, int offset, int length) - { - _buffer = buffer; - _initialOffset = offset; - _offset = offset; - _length = length; - } - - public int Offset => _offset; - - public int Length => _length - _offset; - - public bool EndOfStream => _offset == _length; - - public void Seek(int position) - { - _offset = _initialOffset + position; - } - - public byte ReadByte() - { - ValidateReceiveBuffer(1); - - return _buffer[_offset++]; - } - - public bool ReadBoolean() - { - ValidateReceiveBuffer(1); - - var buffer = _buffer[_offset++]; - - if (buffer == 0) - { - return false; - } - - if (buffer == 1) - { - return true; - } - - throw new MqttProtocolViolationException("Boolean values can be 0 or 1 only."); - } - - public byte[] ReadRemainingData() - { - var bufferLength = _length - _offset; - var buffer = new byte[bufferLength]; - Array.Copy(_buffer, _offset, buffer, 0, bufferLength); - - return buffer; - } - - public ushort ReadTwoByteInteger() - { - ValidateReceiveBuffer(2); - - var msb = _buffer[_offset++]; - var lsb = _buffer[_offset++]; - - return (ushort)(msb << 8 | lsb); - } - - public uint ReadFourByteInteger() - { - ValidateReceiveBuffer(4); - - var byte0 = _buffer[_offset++]; - var byte1 = _buffer[_offset++]; - var byte2 = _buffer[_offset++]; - var byte3 = _buffer[_offset++]; - - return (uint)(byte0 << 24 | byte1 << 16 | byte2 << 8 | byte3); - } - - public uint ReadVariableLengthInteger() - { - var multiplier = 1; - var value = 0U; - byte encodedByte; - - do - { - encodedByte = ReadByte(); - value += (uint)((encodedByte & 127) * multiplier); - - if (multiplier > 2097152) - { - throw new MqttProtocolViolationException("Variable length integer is invalid."); - } - - multiplier *= 128; - } while ((encodedByte & 128) != 0); - - return value; - } - - public byte[] ReadWithLengthPrefix() - { - return ReadSegmentWithLengthPrefix().ToArray(); - } - - private ArraySegment ReadSegmentWithLengthPrefix() - { - var length = ReadTwoByteInteger(); - - ValidateReceiveBuffer(length); - - var result = new ArraySegment(_buffer, _offset, length); - _offset += length; - - return result; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ValidateReceiveBuffer(int length) - { - if (_length < _offset + length) - { - throw new MqttProtocolViolationException($"Expected at least {_offset + length} bytes but there are only {_length} bytes"); - } - } - - public string ReadStringWithLengthPrefix() - { - var buffer = ReadSegmentWithLengthPrefix(); - return Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count); - } - } -} diff --git a/Source/MQTTnet/Formatter/MqttPacketBuffer.cs b/Source/MQTTnet/Formatter/MqttPacketBuffer.cs new file mode 100644 index 0000000..4fdd77e --- /dev/null +++ b/Source/MQTTnet/Formatter/MqttPacketBuffer.cs @@ -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 System; +using System.Linq; +using MQTTnet.Implementations; + +namespace MQTTnet.Formatter +{ + public readonly struct MqttPacketBuffer + { + static readonly ArraySegment EmptyPayload = PlatformAbstractionLayer.EmptyByteArraySegment; + + public MqttPacketBuffer(ArraySegment packet, ArraySegment payload) + { + Packet = packet; + Payload = payload; + + Length = Packet.Count + Payload.Count; + } + + public MqttPacketBuffer(ArraySegment packet) + { + Packet = packet; + Payload = EmptyPayload; + + Length = Packet.Count; + } + + public int Length { get; } + + public ArraySegment Packet { get; } + + public ArraySegment Payload { get; } + + public byte[] ToArray() + { + if (Packet.Count == 0) + { + return Packet.ToArray(); + } + + var buffer = new byte[Length]; + Array.Copy(Packet.Array, Packet.Offset, buffer, 0, Packet.Count); + Array.Copy(Payload.Array, Payload.Offset, buffer, Packet.Count, Payload.Count); + + return buffer; + } + + public ArraySegment Join() + { + if (Packet.Count == 0) + { + return Packet; + } + + var buffer = new byte[Length]; + Array.Copy(Packet.Array, Packet.Offset, buffer, 0, Packet.Count); + Array.Copy(Payload.Array, Payload.Offset, buffer, Packet.Count, Payload.Count); + + return new ArraySegment(buffer); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttPacketFactories.cs b/Source/MQTTnet/Formatter/MqttPacketFactories.cs new file mode 100644 index 0000000..a13c2aa --- /dev/null +++ b/Source/MQTTnet/Formatter/MqttPacketFactories.cs @@ -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. + +namespace MQTTnet.Formatter +{ + public sealed class MqttPacketFactories + { + public MqttConnAckPacketFactory ConnAck { get; } = new MqttConnAckPacketFactory(); + public MqttConnectPacketFactory Connect { get; } = new MqttConnectPacketFactory(); + + public MqttDisconnectPacketFactory Disconnect { get; } = new MqttDisconnectPacketFactory(); + + public MqttPubAckPacketFactory PubAck { get; } = new MqttPubAckPacketFactory(); + + public MqttPubCompPacketFactory PubComp { get; } = new MqttPubCompPacketFactory(); + + public MqttPublishPacketFactory Publish { get; } = new MqttPublishPacketFactory(); + + public MqttPubRecPacketFactory PubRec { get; } = new MqttPubRecPacketFactory(); + + public MqttPubRelPacketFactory PubRel { get; } = new MqttPubRelPacketFactory(); + + public MqttSubAckPacketFactory SubAck { get; } = new MqttSubAckPacketFactory(); + + public MqttSubscribePacketFactory Subscribe { get; } = new MqttSubscribePacketFactory(); + + public MqttUnsubAckPacketFactory UnsubAck { get; } = new MqttUnsubAckPacketFactory(); + + public MqttUnsubscribePacketFactory Unsubscribe { get; } = new MqttUnsubscribePacketFactory(); + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttPacketFormatterAdapter.cs b/Source/MQTTnet/Formatter/MqttPacketFormatterAdapter.cs index 1e2df94..2dcf00b 100644 --- a/Source/MQTTnet/Formatter/MqttPacketFormatterAdapter.cs +++ b/Source/MQTTnet/Formatter/MqttPacketFormatterAdapter.cs @@ -1,4 +1,8 @@ -using System; +// 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.Runtime.CompilerServices; using MQTTnet.Adapter; using MQTTnet.Exceptions; @@ -10,74 +14,48 @@ namespace MQTTnet.Formatter { public sealed class MqttPacketFormatterAdapter { + readonly MqttBufferReader _bufferReader = new MqttBufferReader(); + readonly MqttBufferWriter _bufferWriter; + IMqttPacketFormatter _formatter; - public MqttPacketFormatterAdapter(MqttProtocolVersion protocolVersion) - : this(protocolVersion, new MqttPacketWriter()) + public MqttPacketFormatterAdapter(MqttBufferWriter mqttBufferWriter) { + _bufferWriter = mqttBufferWriter ?? throw new ArgumentNullException(nameof(mqttBufferWriter)); } - public MqttPacketFormatterAdapter(MqttProtocolVersion protocolVersion, IMqttPacketWriter writer) - : this(writer) + public MqttPacketFormatterAdapter(MqttProtocolVersion protocolVersion, MqttBufferWriter bufferWriter) : this(bufferWriter) { UseProtocolVersion(protocolVersion); } - public MqttPacketFormatterAdapter(IMqttPacketWriter writer) - { - Writer = writer; - } - public MqttProtocolVersion ProtocolVersion { get; private set; } = MqttProtocolVersion.Unknown; - public IMqttDataConverter DataConverter + public MqttPacket Decode(ReceivedMqttPacket receivedMqttPacket) { - get - { - ThrowIfFormatterNotSet(); - - return _formatter.DataConverter; - } - } - - public IMqttPacketWriter Writer { get; } - - public ArraySegment Encode(MqttBasePacket packet) - { - if (packet == null) throw new ArgumentNullException(nameof(packet)); - ThrowIfFormatterNotSet(); - return _formatter.Encode(packet); + return _formatter.Decode(receivedMqttPacket); } - public MqttBasePacket Decode(ReceivedMqttPacket receivedMqttPacket) + public void DetectProtocolVersion(ReceivedMqttPacket receivedMqttPacket) { - if (receivedMqttPacket == null) throw new ArgumentNullException(nameof(receivedMqttPacket)); - - ThrowIfFormatterNotSet(); - - return _formatter.Decode(receivedMqttPacket); + var protocolVersion = ParseProtocolVersion(receivedMqttPacket); + UseProtocolVersion(protocolVersion); } - public void FreeBuffer() + public MqttPacketBuffer Encode(MqttPacket packet) { - _formatter?.FreeBuffer(); + ThrowIfFormatterNotSet(); + return _formatter.Encode(packet); } - public void DetectProtocolVersion(ReceivedMqttPacket receivedMqttPacket) + public void Cleanup() { - var protocolVersion = ParseProtocolVersion(receivedMqttPacket); - - // Reset the position of the stream because the protocol version is part of - // the regular CONNECT packet. So it will not properly deserialized if this - // data is missing. - receivedMqttPacket.BodyReader.Seek(0); - - UseProtocolVersion(protocolVersion); + _bufferWriter.Cleanup(); } - public static IMqttPacketFormatter GetMqttPacketFormatter(MqttProtocolVersion protocolVersion, IMqttPacketWriter writer) + public static IMqttPacketFormatter GetMqttPacketFormatter(MqttProtocolVersion protocolVersion, MqttBufferWriter bufferWriter) { if (protocolVersion == MqttProtocolVersion.Unknown) { @@ -87,40 +65,24 @@ namespace MQTTnet.Formatter switch (protocolVersion) { case MqttProtocolVersion.V500: - { - return new MqttV500PacketFormatter(writer); - } - case MqttProtocolVersion.V311: - { - return new MqttV311PacketFormatter(writer); - } + { + return new MqttV5PacketFormatter(bufferWriter); + } case MqttProtocolVersion.V310: - { - return new MqttV310PacketFormatter(writer); - } + case MqttProtocolVersion.V311: + { + return new MqttV3PacketFormatter(bufferWriter, protocolVersion); + } default: - { - throw new NotSupportedException(); - } + { + throw new NotSupportedException(); + } } } - void UseProtocolVersion(MqttProtocolVersion protocolVersion) + MqttProtocolVersion ParseProtocolVersion(ReceivedMqttPacket receivedMqttPacket) { - if (protocolVersion == MqttProtocolVersion.Unknown) - { - throw new InvalidOperationException("MQTT protocol version is invalid."); - } - - ProtocolVersion = protocolVersion; - _formatter = GetMqttPacketFormatter(protocolVersion, Writer); - } - - static MqttProtocolVersion ParseProtocolVersion(ReceivedMqttPacket receivedMqttPacket) - { - if (receivedMqttPacket == null) throw new ArgumentNullException(nameof(receivedMqttPacket)); - - if (receivedMqttPacket.BodyReader.Length < 7) + if (receivedMqttPacket.Body.Count < 7) { // 2 byte protocol name length // at least 4 byte protocol name @@ -128,8 +90,10 @@ namespace MQTTnet.Formatter throw new MqttProtocolViolationException("CONNECT packet must have at least 7 bytes."); } - var protocolName = receivedMqttPacket.BodyReader.ReadStringWithLengthPrefix(); - var protocolLevel = receivedMqttPacket.BodyReader.ReadByte(); + _bufferReader.SetBuffer(receivedMqttPacket.Body.Array, receivedMqttPacket.Body.Offset, receivedMqttPacket.Body.Count); + + var protocolName = _bufferReader.ReadString(); + var protocolLevel = _bufferReader.ReadByte(); if (protocolName == "MQTT") { @@ -167,5 +131,16 @@ namespace MQTTnet.Formatter throw new InvalidOperationException("Protocol version not set or detected."); } } + + void UseProtocolVersion(MqttProtocolVersion protocolVersion) + { + if (protocolVersion == MqttProtocolVersion.Unknown) + { + throw new InvalidOperationException("MQTT protocol version is invalid."); + } + + ProtocolVersion = protocolVersion; + _formatter = GetMqttPacketFormatter(protocolVersion, _bufferWriter); + } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttPacketWriter.cs b/Source/MQTTnet/Formatter/MqttPacketWriter.cs deleted file mode 100644 index 48d9578..0000000 --- a/Source/MQTTnet/Formatter/MqttPacketWriter.cs +++ /dev/null @@ -1,258 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using System.Text; -using MQTTnet.Protocol; - -namespace MQTTnet.Formatter -{ - /// - /// This is a custom implementation of a memory stream which provides only MQTTnet relevant features. - /// The goal is to avoid lots of argument checks like in the original stream. The growth rule is the - /// same as for the original MemoryStream in .net. Also this implementation allows accessing the internal - /// buffer for all platforms and .net framework versions (which is not available at the regular MemoryStream). - /// - public sealed class MqttPacketWriter : IMqttPacketWriter - { - public static int InitialBufferSize = 4096; - - public static int MaxBufferSize = 4096 * 4; - - byte[] _buffer = new byte[InitialBufferSize]; - - int _offset; - - public int Length { get; private set; } - - public static byte BuildFixedHeader(MqttControlPacketType packetType, byte flags = 0) - { - var fixedHeader = (int)packetType << 4; - fixedHeader |= flags; - return (byte)fixedHeader; - } - - public static int GetLengthOfVariableInteger(uint value) - { - var result = 0; - var x = value; - do - { - x /= 128; - result++; - } while (x > 0); - - return result; - } - - public void WriteVariableLengthInteger(uint value) - { - if (value == 0) - { - _buffer[_offset] = 0; - IncreasePosition(1); - - return; - } - - if (value <= 127) - { - _buffer[_offset] = (byte)value; - IncreasePosition(1); - - return; - } - - var size = 0; - var x = value; - do - { - var encodedByte = x % 128; - x /= 128; - if (x > 0) - { - encodedByte |= 128; - } - - _buffer[_offset + size] = (byte)encodedByte; - size++; - } while (x > 0); - - IncreasePosition(size); - } - - public void WriteWithLengthPrefix(string value) - { - if (string.IsNullOrEmpty(value)) - { - EnsureAdditionalCapacity(2); - - _buffer[_offset] = 0; - _buffer[_offset + 1] = 0; - - IncreasePosition(2); - } - else - { - var bufferSize = Encoding.UTF8.GetByteCount(value); - - EnsureAdditionalCapacity(bufferSize + 2); - - _buffer[_offset] = (byte)(bufferSize >> 8); - _buffer[_offset + 1] = (byte)bufferSize; - - Encoding.UTF8.GetBytes(value, 0, value.Length, _buffer, _offset + 2); - - IncreasePosition(bufferSize + 2); - } - } - - public void WriteWithLengthPrefix(byte[] value) - { - if (value == null || value.Length == 0) - { - EnsureAdditionalCapacity(2); - - _buffer[_offset] = 0; - _buffer[_offset + 1] = 0; - - IncreasePosition(2); - } - else - { - EnsureAdditionalCapacity(value.Length + 2); - - _buffer[_offset] = (byte)(value.Length >> 8); - _buffer[_offset + 1] = (byte)value.Length; - - Array.Copy(value, 0, _buffer, _offset + 2, value.Length); - IncreasePosition(value.Length + 2); - } - } - - public void Write(byte @byte) - { - EnsureAdditionalCapacity(1); - - _buffer[_offset] = @byte; - IncreasePosition(1); - } - - public void Write(ushort value) - { - EnsureAdditionalCapacity(2); - - _buffer[_offset] = (byte)(value >> 8); - IncreasePosition(1); - _buffer[_offset] = (byte)value; - IncreasePosition(1); - } - - public void Write(byte[] buffer, int offset, int count) - { - if (buffer == null) throw new ArgumentNullException(nameof(buffer)); - - if (count == 0) - { - return; - } - - EnsureAdditionalCapacity(count); - - Array.Copy(buffer, offset, _buffer, _offset, count); - IncreasePosition(count); - } - - public void Write(IMqttPacketWriter propertyWriter) - { - if (propertyWriter is MqttPacketWriter writer) - { - if (writer.Length == 0) - { - return; - } - - Write(writer._buffer, 0, writer.Length); - return; - } - - if (propertyWriter == null) - { - throw new ArgumentNullException(nameof(propertyWriter)); - } - - throw new InvalidOperationException($"{nameof(propertyWriter)} must be of type {nameof(MqttPacketWriter)}"); - } - - public void Reset(int length) - { - Length = length; - } - - public void Seek(int position) - { - EnsureCapacity(position); - _offset = position; - } - - public byte[] GetBuffer() - { - return _buffer; - } - - public void FreeBuffer() - { - // This method frees the used memory by shrinking the buffer. This is required because the buffer - // is used across several messages. In general this is not a big issue because subsequent Ping packages - // have the same size but a very big publish package with 100 MB of payload will increase the buffer - // a lot and the size will never reduced. So this method tries to find a size which can be held in - // memory for a long time without causing troubles. - - if (_buffer.Length < MaxBufferSize) - { - return; - } - - Array.Resize(ref _buffer, MaxBufferSize); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - void EnsureAdditionalCapacity(int additionalCapacity) - { - var freeSpace = _buffer.Length - _offset; - if (freeSpace >= additionalCapacity) - { - return; - } - - EnsureCapacity(_buffer.Length + additionalCapacity - freeSpace); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - void EnsureCapacity(int capacity) - { - var newBufferLength = _buffer.Length; - - if (newBufferLength >= capacity) - { - return; - } - - while (newBufferLength < capacity) - { - newBufferLength *= 2; - } - - Array.Resize(ref _buffer, newBufferLength); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - void IncreasePosition(int length) - { - _offset += length; - - if (_offset > Length) - { - Length = _offset; - } - } - } -} diff --git a/Source/MQTTnet/Formatter/MqttProtocolVersion.cs b/Source/MQTTnet/Formatter/MqttProtocolVersion.cs index 2121e7e..436951d 100644 --- a/Source/MQTTnet/Formatter/MqttProtocolVersion.cs +++ b/Source/MQTTnet/Formatter/MqttProtocolVersion.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Formatter +// 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.Formatter { public enum MqttProtocolVersion { @@ -8,4 +12,4 @@ V311 = 4, V500 = 5 } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttPubAckPacketFactory.cs b/Source/MQTTnet/Formatter/MqttPubAckPacketFactory.cs new file mode 100644 index 0000000..4d6c687 --- /dev/null +++ b/Source/MQTTnet/Formatter/MqttPubAckPacketFactory.cs @@ -0,0 +1,68 @@ +// 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.Client; +using MQTTnet.Packets; +using MQTTnet.Protocol; +using MQTTnet.Server; + +namespace MQTTnet.Formatter +{ + public sealed class MqttPubAckPacketFactory + { + public MqttPubAckPacket Create(MqttPublishPacket publishPacket, InterceptingPublishEventArgs interceptingPublishEventArgs) + { + if (publishPacket == null) + { + throw new ArgumentNullException(nameof(publishPacket)); + } + + var pubAckPacket = new MqttPubAckPacket + { + PacketIdentifier = publishPacket.PacketIdentifier, + ReasonCode = MqttPubAckReasonCode.Success + }; + + if (interceptingPublishEventArgs != null) + { + pubAckPacket.ReasonCode = (MqttPubAckReasonCode)(int)interceptingPublishEventArgs.Response.ReasonCode; + pubAckPacket.ReasonString = interceptingPublishEventArgs.Response.ReasonString; + pubAckPacket.UserProperties = interceptingPublishEventArgs.Response.UserProperties; + } + + return pubAckPacket; + } + + public MqttPubAckPacket Create(MqttApplicationMessageReceivedEventArgs applicationMessageReceivedEventArgs) + { + if (applicationMessageReceivedEventArgs == null) + { + throw new ArgumentNullException(nameof(applicationMessageReceivedEventArgs)); + } + + var pubAckPacket = Create(applicationMessageReceivedEventArgs.PublishPacket, applicationMessageReceivedEventArgs.ReasonCode); + pubAckPacket.UserProperties = applicationMessageReceivedEventArgs.ResponseUserProperties; + pubAckPacket.ReasonString = applicationMessageReceivedEventArgs.ResponseReasonString; + + return pubAckPacket; + } + + static MqttPubAckPacket Create(MqttPublishPacket publishPacket, MqttApplicationMessageReceivedReasonCode applicationMessageReceivedReasonCode) + { + if (publishPacket == null) + { + throw new ArgumentNullException(nameof(publishPacket)); + } + + var pubAckPacket = new MqttPubAckPacket + { + PacketIdentifier = publishPacket.PacketIdentifier, + ReasonCode = (MqttPubAckReasonCode)(int)applicationMessageReceivedReasonCode + }; + + return pubAckPacket; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttPubCompPacketFactory.cs b/Source/MQTTnet/Formatter/MqttPubCompPacketFactory.cs new file mode 100644 index 0000000..fd4b349 --- /dev/null +++ b/Source/MQTTnet/Formatter/MqttPubCompPacketFactory.cs @@ -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 MQTTnet.Client; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Formatter +{ + public sealed class MqttPubCompPacketFactory + { + public MqttPubCompPacket Create(MqttPubRelPacket pubRelPacket, MqttApplicationMessageReceivedReasonCode reasonCode) + { + if (pubRelPacket == null) + { + throw new ArgumentNullException(nameof(pubRelPacket)); + } + + return new MqttPubCompPacket + { + PacketIdentifier = pubRelPacket.PacketIdentifier, + ReasonCode = (MqttPubCompReasonCode)(int)reasonCode + }; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttPubRecPacketFactory.cs b/Source/MQTTnet/Formatter/MqttPubRecPacketFactory.cs new file mode 100644 index 0000000..5c0726d --- /dev/null +++ b/Source/MQTTnet/Formatter/MqttPubRecPacketFactory.cs @@ -0,0 +1,62 @@ +// 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.Client; +using MQTTnet.Packets; +using MQTTnet.Protocol; +using MQTTnet.Server; + +namespace MQTTnet.Formatter +{ + public sealed class MqttPubRecPacketFactory + { + public MqttPubRecPacket Create(MqttApplicationMessageReceivedEventArgs applicationMessageReceivedEventArgs) + { + if (applicationMessageReceivedEventArgs == null) + { + throw new ArgumentNullException(nameof(applicationMessageReceivedEventArgs)); + } + + var pubRecPacket = Create(applicationMessageReceivedEventArgs.PublishPacket, applicationMessageReceivedEventArgs.ReasonCode); + pubRecPacket.UserProperties = applicationMessageReceivedEventArgs.ResponseUserProperties; + + return pubRecPacket; + } + + public MqttPacket Create(MqttPublishPacket publishPacket, InterceptingPublishEventArgs interceptingPublishEventArgs) + { + if (publishPacket == null) + { + throw new ArgumentNullException(nameof(publishPacket)); + } + + var pubRecPacket = new MqttPubRecPacket + { + PacketIdentifier = publishPacket.PacketIdentifier, + ReasonCode = MqttPubRecReasonCode.Success + }; + + if (interceptingPublishEventArgs != null) + { + pubRecPacket.ReasonCode = (MqttPubRecReasonCode)(int)interceptingPublishEventArgs.Response.ReasonCode; + pubRecPacket.ReasonString = interceptingPublishEventArgs.Response.ReasonString; + pubRecPacket.UserProperties = interceptingPublishEventArgs.Response.UserProperties; + } + + return pubRecPacket; + } + + static MqttPubRecPacket Create(MqttPublishPacket publishPacket, MqttApplicationMessageReceivedReasonCode applicationMessageReceivedReasonCode) + { + var pubRecPacket = new MqttPubRecPacket + { + PacketIdentifier = publishPacket.PacketIdentifier, + ReasonCode = (MqttPubRecReasonCode)(int)applicationMessageReceivedReasonCode + }; + + return pubRecPacket; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttPubRelPacketFactory.cs b/Source/MQTTnet/Formatter/MqttPubRelPacketFactory.cs new file mode 100644 index 0000000..a16790b --- /dev/null +++ b/Source/MQTTnet/Formatter/MqttPubRelPacketFactory.cs @@ -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 MQTTnet.Client; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Formatter +{ + public sealed class MqttPubRelPacketFactory + { + public MqttPubRelPacket Create(MqttPubRecPacket pubRecPacket, MqttApplicationMessageReceivedReasonCode reasonCode) + { + if (pubRecPacket == null) + { + throw new ArgumentNullException(nameof(pubRecPacket)); + } + + return new MqttPubRelPacket + { + PacketIdentifier = pubRecPacket.PacketIdentifier, + ReasonCode = (MqttPubRelReasonCode)(int)reasonCode + }; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttPublishPacketFactory.cs b/Source/MQTTnet/Formatter/MqttPublishPacketFactory.cs new file mode 100644 index 0000000..90325e9 --- /dev/null +++ b/Source/MQTTnet/Formatter/MqttPublishPacketFactory.cs @@ -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 MQTTnet.Exceptions; +using MQTTnet.Packets; + +namespace MQTTnet.Formatter +{ + public sealed class MqttPublishPacketFactory + { + public MqttPublishPacket Clone(MqttPublishPacket publishPacket) + { + return new MqttPublishPacket + { + Topic = publishPacket.Topic, + Payload = publishPacket.Payload, + Retain = publishPacket.Retain, + QualityOfServiceLevel = publishPacket.QualityOfServiceLevel, + Dup = publishPacket.Dup, + PacketIdentifier = publishPacket.PacketIdentifier + }; + } + + public 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 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)."); + } + + var packet = new MqttPublishPacket + { + Topic = connectPacket.WillTopic, + Payload = connectPacket.WillMessage, + 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; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttSubAckPacketFactory.cs b/Source/MQTTnet/Formatter/MqttSubAckPacketFactory.cs new file mode 100644 index 0000000..4f5436d --- /dev/null +++ b/Source/MQTTnet/Formatter/MqttSubAckPacketFactory.cs @@ -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 MQTTnet.Packets; +using MQTTnet.Server; + +namespace MQTTnet.Formatter +{ + public sealed class MqttSubAckPacketFactory + { + public 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; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttSubscribePacketFactory.cs b/Source/MQTTnet/Formatter/MqttSubscribePacketFactory.cs new file mode 100644 index 0000000..e24fe9f --- /dev/null +++ b/Source/MQTTnet/Formatter/MqttSubscribePacketFactory.cs @@ -0,0 +1,30 @@ +// 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.Client; +using MQTTnet.Packets; + +namespace MQTTnet.Formatter +{ + public sealed class MqttSubscribePacketFactory + { + public MqttSubscribePacket Create(MqttClientSubscribeOptions clientSubscribeOptions) + { + if (clientSubscribeOptions == null) + { + throw new ArgumentNullException(nameof(clientSubscribeOptions)); + } + + var packet = new MqttSubscribePacket + { + TopicFilters = clientSubscribeOptions.TopicFilters, + SubscriptionIdentifier = clientSubscribeOptions.SubscriptionIdentifier, + UserProperties = clientSubscribeOptions.UserProperties + }; + + return packet; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttUnsubAckPacketFactory.cs b/Source/MQTTnet/Formatter/MqttUnsubAckPacketFactory.cs new file mode 100644 index 0000000..18da46a --- /dev/null +++ b/Source/MQTTnet/Formatter/MqttUnsubAckPacketFactory.cs @@ -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 MQTTnet.Packets; +using MQTTnet.Server; + +namespace MQTTnet.Formatter +{ + public sealed class MqttUnsubAckPacketFactory + { + public MqttUnsubAckPacket Create(MqttUnsubscribePacket unsubscribePacket, MqttUnsubscribeResult mqttUnsubscribeResult) + { + if (unsubscribePacket == null) + { + throw new ArgumentNullException(nameof(unsubscribePacket)); + } + + if (mqttUnsubscribeResult == null) + { + throw new ArgumentNullException(nameof(mqttUnsubscribeResult)); + } + + var unsubAckPacket = new MqttUnsubAckPacket + { + PacketIdentifier = unsubscribePacket.PacketIdentifier + }; + + // MQTTv5.0.0 only. + unsubAckPacket.ReasonCodes = mqttUnsubscribeResult.ReasonCodes; + + return unsubAckPacket; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/MqttUnsubscribePacketFactory.cs b/Source/MQTTnet/Formatter/MqttUnsubscribePacketFactory.cs new file mode 100644 index 0000000..85cda94 --- /dev/null +++ b/Source/MQTTnet/Formatter/MqttUnsubscribePacketFactory.cs @@ -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 System; +using MQTTnet.Client; +using MQTTnet.Packets; + +namespace MQTTnet.Formatter +{ + public sealed class MqttUnsubscribePacketFactory + { + public MqttUnsubscribePacket Create(MqttClientUnsubscribeOptions clientUnsubscribeOptions) + { + if (clientUnsubscribeOptions == null) + { + throw new ArgumentNullException(nameof(clientUnsubscribeOptions)); + } + + var packet = new MqttUnsubscribePacket + { + UserProperties = clientUnsubscribeOptions.UserProperties + }; + + if (clientUnsubscribeOptions.TopicFilters != null) + { + packet.TopicFilters.AddRange(clientUnsubscribeOptions.TopicFilters); + } + + return packet; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/ReadFixedHeaderResult.cs b/Source/MQTTnet/Formatter/ReadFixedHeaderResult.cs index 30ad73e..5866393 100644 --- a/Source/MQTTnet/Formatter/ReadFixedHeaderResult.cs +++ b/Source/MQTTnet/Formatter/ReadFixedHeaderResult.cs @@ -1,8 +1,24 @@ -namespace MQTTnet.Formatter +// 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.Formatter { - public class ReadFixedHeaderResult + public struct ReadFixedHeaderResult { - public bool ConnectionClosed { get; set; } + public static ReadFixedHeaderResult Cancelled { get; } = new ReadFixedHeaderResult + { + IsCancelled = true + }; + + public static ReadFixedHeaderResult ConnectionClosed { get; } = new ReadFixedHeaderResult + { + IsConnectionClosed = true + }; + + public bool IsCancelled { get; set; } + + public bool IsConnectionClosed { get; set; } public MqttFixedHeader FixedHeader { get; set; } } diff --git a/Source/MQTTnet/Formatter/V3/MqttV310DataConverter.cs b/Source/MQTTnet/Formatter/V3/MqttV310DataConverter.cs deleted file mode 100644 index 775dc00..0000000 --- a/Source/MQTTnet/Formatter/V3/MqttV310DataConverter.cs +++ /dev/null @@ -1,278 +0,0 @@ -using MQTTnet.Client.Connecting; -using MQTTnet.Client.Disconnecting; -using MQTTnet.Client.Options; -using MQTTnet.Client.Publishing; -using MQTTnet.Client.Subscribing; -using MQTTnet.Client.Unsubscribing; -using MQTTnet.Exceptions; -using MQTTnet.Packets; -using MQTTnet.Protocol; -using MQTTnet.Server; -using System; -using System.Collections.Generic; -using System.Linq; -using MQTTnet.Server.Internal; - -namespace MQTTnet.Formatter.V3 -{ - public class MqttV310DataConverter : IMqttDataConverter - { - public MqttPublishPacket CreatePublishPacket(MqttApplicationMessage applicationMessage) - { - if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); - - return new MqttPublishPacket - { - Topic = applicationMessage.Topic, - Payload = applicationMessage.Payload, - QualityOfServiceLevel = applicationMessage.QualityOfServiceLevel, - Retain = applicationMessage.Retain, - Dup = false - }; - } - - public MqttPubAckPacket CreatePubAckPacket(MqttPublishPacket publishPacket, MqttApplicationMessageReceivedReasonCode reasonCode) - { - if (publishPacket == null) throw new ArgumentNullException(nameof(publishPacket)); - - return new MqttPubAckPacket - { - PacketIdentifier = publishPacket.PacketIdentifier - }; - } - - public MqttPubRecPacket CreatePubRecPacket(MqttPublishPacket publishPacket, MqttApplicationMessageReceivedReasonCode reasonCode) - { - if (publishPacket == null) throw new ArgumentNullException(nameof(publishPacket)); - - return new MqttPubRecPacket - { - PacketIdentifier = publishPacket.PacketIdentifier - }; - } - - public MqttPubCompPacket CreatePubCompPacket(MqttPubRelPacket pubRelPacket, MqttApplicationMessageReceivedReasonCode reasonCode) - { - if (pubRelPacket == null) throw new ArgumentNullException(nameof(pubRelPacket)); - - return new MqttPubCompPacket - { - PacketIdentifier = pubRelPacket.PacketIdentifier - }; - } - - public MqttPubRelPacket CreatePubRelPacket(MqttPubRecPacket pubRecPacket, MqttApplicationMessageReceivedReasonCode reasonCode) - { - if (pubRecPacket == null) throw new ArgumentNullException(nameof(pubRecPacket)); - - return new MqttPubRelPacket - { - PacketIdentifier = pubRecPacket.PacketIdentifier - }; - } - - public MqttApplicationMessage CreateApplicationMessage(MqttPublishPacket publishPacket) - { - if (publishPacket == null) throw new ArgumentNullException(nameof(publishPacket)); - - return new MqttApplicationMessage - { - Topic = publishPacket.Topic, - Payload = publishPacket.Payload, - QualityOfServiceLevel = publishPacket.QualityOfServiceLevel, - Retain = publishPacket.Retain, - Dup = publishPacket.Dup - }; - } - - public MqttClientConnectResult CreateClientConnectResult(MqttConnAckPacket connAckPacket) - { - if (connAckPacket == null) throw new ArgumentNullException(nameof(connAckPacket)); - - MqttClientConnectResultCode resultCode; - switch (connAckPacket.ReturnCode) - { - case MqttConnectReturnCode.ConnectionAccepted: - { - resultCode = MqttClientConnectResultCode.Success; - break; - } - - case MqttConnectReturnCode.ConnectionRefusedUnacceptableProtocolVersion: - { - resultCode = MqttClientConnectResultCode.UnsupportedProtocolVersion; - break; - } - - case MqttConnectReturnCode.ConnectionRefusedNotAuthorized: - { - resultCode = MqttClientConnectResultCode.NotAuthorized; - break; - } - - case MqttConnectReturnCode.ConnectionRefusedBadUsernameOrPassword: - { - resultCode = MqttClientConnectResultCode.BadUserNameOrPassword; - break; - } - - case MqttConnectReturnCode.ConnectionRefusedIdentifierRejected: - { - resultCode = MqttClientConnectResultCode.ClientIdentifierNotValid; - break; - } - - case MqttConnectReturnCode.ConnectionRefusedServerUnavailable: - { - resultCode = MqttClientConnectResultCode.ServerUnavailable; - break; - } - - default: - throw new MqttProtocolViolationException("Received unexpected return code."); - } - - return new MqttClientConnectResult - { - RetainAvailable = true, // Always true because v3.1.1 does not have a way to "disable" that feature. - WildcardSubscriptionAvailable = true, // Always true because v3.1.1 does not have a way to "disable" that feature. - IsSessionPresent = connAckPacket.IsSessionPresent, - ResultCode = resultCode - }; - } - - public MqttConnectPacket CreateConnectPacket(MqttApplicationMessage willApplicationMessage, IMqttClientOptions options) - { - if (options == null) throw new ArgumentNullException(nameof(options)); - - return new MqttConnectPacket - { - ClientId = options.ClientId, - Username = options.Credentials?.Username, - Password = options.Credentials?.Password, - CleanSession = options.CleanSession, - KeepAlivePeriod = (ushort)options.KeepAlivePeriod.TotalSeconds, - WillMessage = willApplicationMessage - }; - } - - public MqttConnAckPacket CreateConnAckPacket(MqttConnectionValidatorContext connectionValidatorContext) - { - if (connectionValidatorContext == null) throw new ArgumentNullException(nameof(connectionValidatorContext)); - - return new MqttConnAckPacket - { - ReturnCode = new MqttConnectReasonCodeConverter().ToConnectReturnCode(connectionValidatorContext.ReasonCode) - }; - } - - public MqttClientSubscribeResult CreateClientSubscribeResult(MqttSubscribePacket subscribePacket, MqttSubAckPacket subAckPacket) - { - if (subscribePacket == null) throw new ArgumentNullException(nameof(subscribePacket)); - if (subAckPacket == null) throw new ArgumentNullException(nameof(subAckPacket)); - - if (subAckPacket.ReturnCodes.Count != subscribePacket.TopicFilters.Count) - { - throw new MqttProtocolViolationException("The return codes are not matching the topic filters [MQTT-3.9.3-1]."); - } - - var result = new MqttClientSubscribeResult(); - - result.Items.AddRange(subscribePacket.TopicFilters.Select((t, i) => - new MqttClientSubscribeResultItem(t, (MqttClientSubscribeResultCode)subAckPacket.ReturnCodes[i]))); - - return result; - } - - public MqttClientUnsubscribeResult CreateClientUnsubscribeResult(MqttUnsubscribePacket unsubscribePacket, MqttUnsubAckPacket unsubAckPacket) - { - if (unsubscribePacket == null) throw new ArgumentNullException(nameof(unsubscribePacket)); - if (unsubAckPacket == null) throw new ArgumentNullException(nameof(unsubAckPacket)); - - var result = new MqttClientUnsubscribeResult(); - - result.Items.AddRange(unsubscribePacket.TopicFilters.Select((t, i) => - new MqttClientUnsubscribeResultItem(t, MqttClientUnsubscribeResultCode.Success))); - - return result; - } - - public MqttSubscribePacket CreateSubscribePacket(MqttClientSubscribeOptions options) - { - if (options == null) throw new ArgumentNullException(nameof(options)); - - var subscribePacket = new MqttSubscribePacket(); - subscribePacket.TopicFilters.AddRange(options.TopicFilters); - - return subscribePacket; - } - - public MqttSubAckPacket CreateSubAckPacket(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 - }; - - subackPacket.ReturnCodes.AddRange(subscribeResult.ReturnCodes); - - return subackPacket; - } - - public MqttUnsubscribePacket CreateUnsubscribePacket(MqttClientUnsubscribeOptions options) - { - if (options == null) throw new ArgumentNullException(nameof(options)); - - var unsubscribePacket = new MqttUnsubscribePacket(); - unsubscribePacket.TopicFilters.AddRange(options.TopicFilters); - - return unsubscribePacket; - } - - public MqttUnsubAckPacket CreateUnsubAckPacket(MqttUnsubscribePacket unsubscribePacket, List reasonCodes) - { - if (unsubscribePacket == null) throw new ArgumentNullException(nameof(unsubscribePacket)); - if (reasonCodes == null) throw new ArgumentNullException(nameof(reasonCodes)); - - return new MqttUnsubAckPacket - { - PacketIdentifier = unsubscribePacket.PacketIdentifier, - ReasonCodes = reasonCodes - }; - } - - public MqttDisconnectPacket CreateDisconnectPacket(MqttClientDisconnectOptions options) - { - return new MqttDisconnectPacket(); - } - - public MqttClientPublishResult CreateClientPublishResult(MqttPubAckPacket pubAckPacket) - { - return new MqttClientPublishResult - { - PacketIdentifier = pubAckPacket?.PacketIdentifier, - ReasonCode = MqttClientPublishReasonCode.Success - }; - } - - public MqttClientPublishResult CreateClientPublishResult(MqttPubRecPacket pubRecPacket, MqttPubCompPacket pubCompPacket) - { - if (pubRecPacket == null || pubCompPacket == null) - { - return new MqttClientPublishResult - { - ReasonCode = MqttClientPublishReasonCode.UnspecifiedError - }; - } - - return new MqttClientPublishResult - { - PacketIdentifier = pubCompPacket.PacketIdentifier, - ReasonCode = MqttClientPublishReasonCode.Success - }; - } - } -} diff --git a/Source/MQTTnet/Formatter/V3/MqttV310PacketFormatter.cs b/Source/MQTTnet/Formatter/V3/MqttV310PacketFormatter.cs deleted file mode 100644 index 16fb380..0000000 --- a/Source/MQTTnet/Formatter/V3/MqttV310PacketFormatter.cs +++ /dev/null @@ -1,614 +0,0 @@ -using System; -using System.Linq; -using System.Net; -using System.Runtime.CompilerServices; -using MQTTnet.Adapter; -using MQTTnet.Exceptions; -using MQTTnet.Packets; -using MQTTnet.Protocol; - -namespace MQTTnet.Formatter.V3 -{ - public class MqttV310PacketFormatter : IMqttPacketFormatter - { - const int FixedHeaderSize = 1; - - static readonly MqttPingReqPacket PingReqPacket = new MqttPingReqPacket(); - static readonly MqttPingRespPacket PingRespPacket = new MqttPingRespPacket(); - static readonly MqttDisconnectPacket DisconnectPacket = new MqttDisconnectPacket(); - - readonly IMqttPacketWriter _packetWriter; - - public MqttV310PacketFormatter(IMqttPacketWriter packetWriter) - { - _packetWriter = packetWriter; - } - - public IMqttDataConverter DataConverter { get; } = new MqttV310DataConverter(); - - public ArraySegment Encode(MqttBasePacket packet) - { - if (packet == null) throw new ArgumentNullException(nameof(packet)); - - // Leave enough head space for max header size (fixed + 4 variable remaining length = 5 bytes) - _packetWriter.Reset(5); - _packetWriter.Seek(5); - - var fixedHeader = EncodePacket(packet, _packetWriter); - var remainingLength = (uint)(_packetWriter.Length - 5); - - var remainingLengthSize = MqttPacketWriter.GetLengthOfVariableInteger(remainingLength); - - var headerSize = FixedHeaderSize + remainingLengthSize; - var headerOffset = 5 - headerSize; - - // Position cursor on correct offset on beginning of array (has leading 0x0) - _packetWriter.Seek(headerOffset); - _packetWriter.Write(fixedHeader); - _packetWriter.WriteVariableLengthInteger(remainingLength); - - var buffer = _packetWriter.GetBuffer(); - - return new ArraySegment(buffer, headerOffset, _packetWriter.Length - headerOffset); - } - - public MqttBasePacket Decode(ReceivedMqttPacket receivedMqttPacket) - { - if (receivedMqttPacket == null) throw new ArgumentNullException(nameof(receivedMqttPacket)); - - var controlPacketType = receivedMqttPacket.FixedHeader >> 4; - if (controlPacketType < 1 || controlPacketType > 14) - { - throw new MqttProtocolViolationException($"The packet type is invalid ({controlPacketType})."); - } - - switch ((MqttControlPacketType)controlPacketType) - { - case MqttControlPacketType.Connect: return DecodeConnectPacket(receivedMqttPacket.BodyReader); - case MqttControlPacketType.ConnAck: return DecodeConnAckPacket(receivedMqttPacket.BodyReader); - case MqttControlPacketType.Disconnect: return DisconnectPacket; - case MqttControlPacketType.Publish: return DecodePublishPacket(receivedMqttPacket); - case MqttControlPacketType.PubAck: return DecodePubAckPacket(receivedMqttPacket.BodyReader); - case MqttControlPacketType.PubRec: return DecodePubRecPacket(receivedMqttPacket.BodyReader); - case MqttControlPacketType.PubRel: return DecodePubRelPacket(receivedMqttPacket.BodyReader); - case MqttControlPacketType.PubComp: return DecodePubCompPacket(receivedMqttPacket.BodyReader); - case MqttControlPacketType.PingReq: return PingReqPacket; - case MqttControlPacketType.PingResp: return PingRespPacket; - case MqttControlPacketType.Subscribe: return DecodeSubscribePacket(receivedMqttPacket.BodyReader); - case MqttControlPacketType.SubAck: return DecodeSubAckPacket(receivedMqttPacket.BodyReader); - case MqttControlPacketType.Unsubscibe: return DecodeUnsubscribePacket(receivedMqttPacket.BodyReader); - case MqttControlPacketType.UnsubAck: return DecodeUnsubAckPacket(receivedMqttPacket.BodyReader); - - default: throw new MqttProtocolViolationException($"Packet type ({controlPacketType}) not supported."); - } - } - - public void FreeBuffer() - { - _packetWriter.FreeBuffer(); - } - - byte EncodePacket(MqttBasePacket packet, IMqttPacketWriter packetWriter) - { - switch (packet) - { - case MqttConnectPacket connectPacket: return EncodeConnectPacket(connectPacket, packetWriter); - case MqttConnAckPacket connAckPacket: return EncodeConnAckPacket(connAckPacket, packetWriter); - case MqttDisconnectPacket _: return EncodeEmptyPacket(MqttControlPacketType.Disconnect); - case MqttPingReqPacket _: return EncodeEmptyPacket(MqttControlPacketType.PingReq); - case MqttPingRespPacket _: return EncodeEmptyPacket(MqttControlPacketType.PingResp); - case MqttPublishPacket publishPacket: return EncodePublishPacket(publishPacket, packetWriter); - case MqttPubAckPacket pubAckPacket: return EncodePubAckPacket(pubAckPacket, packetWriter); - case MqttPubRecPacket pubRecPacket: return EncodePubRecPacket(pubRecPacket, packetWriter); - case MqttPubRelPacket pubRelPacket: return EncodePubRelPacket(pubRelPacket, packetWriter); - case MqttPubCompPacket pubCompPacket: return EncodePubCompPacket(pubCompPacket, packetWriter); - case MqttSubscribePacket subscribePacket: return EncodeSubscribePacket(subscribePacket, packetWriter); - case MqttSubAckPacket subAckPacket: return EncodeSubAckPacket(subAckPacket, packetWriter); - case MqttUnsubscribePacket unsubscribePacket: return EncodeUnsubscribePacket(unsubscribePacket, packetWriter); - case MqttUnsubAckPacket unsubAckPacket: return EncodeUnsubAckPacket(unsubAckPacket, packetWriter); - - default: throw new MqttProtocolViolationException("Packet type invalid."); - } - } - - static MqttBasePacket DecodeUnsubAckPacket(IMqttPacketBodyReader body) - { - ThrowIfBodyIsEmpty(body); - - return new MqttUnsubAckPacket - { - PacketIdentifier = body.ReadTwoByteInteger() - }; - } - - static MqttBasePacket DecodePubCompPacket(IMqttPacketBodyReader body) - { - ThrowIfBodyIsEmpty(body); - - return new MqttPubCompPacket - { - PacketIdentifier = body.ReadTwoByteInteger() - }; - } - - static MqttBasePacket DecodePubRelPacket(IMqttPacketBodyReader body) - { - ThrowIfBodyIsEmpty(body); - - return new MqttPubRelPacket - { - PacketIdentifier = body.ReadTwoByteInteger() - }; - } - - static MqttBasePacket DecodePubRecPacket(IMqttPacketBodyReader body) - { - ThrowIfBodyIsEmpty(body); - - return new MqttPubRecPacket - { - PacketIdentifier = body.ReadTwoByteInteger() - }; - } - - static MqttBasePacket DecodePubAckPacket(IMqttPacketBodyReader body) - { - ThrowIfBodyIsEmpty(body); - - return new MqttPubAckPacket - { - PacketIdentifier = body.ReadTwoByteInteger() - }; - } - - static MqttBasePacket DecodeUnsubscribePacket(IMqttPacketBodyReader body) - { - ThrowIfBodyIsEmpty(body); - - var packet = new MqttUnsubscribePacket - { - PacketIdentifier = body.ReadTwoByteInteger(), - }; - - while (!body.EndOfStream) - { - packet.TopicFilters.Add(body.ReadStringWithLengthPrefix()); - } - - return packet; - } - - static MqttBasePacket DecodeSubscribePacket(IMqttPacketBodyReader body) - { - ThrowIfBodyIsEmpty(body); - - var packet = new MqttSubscribePacket - { - PacketIdentifier = body.ReadTwoByteInteger() - }; - - while (!body.EndOfStream) - { - var topicFilter = new MqttTopicFilter - { - Topic = body.ReadStringWithLengthPrefix(), - QualityOfServiceLevel = (MqttQualityOfServiceLevel)body.ReadByte() - }; - - packet.TopicFilters.Add(topicFilter); - } - - return packet; - } - - static MqttBasePacket DecodePublishPacket(ReceivedMqttPacket receivedMqttPacket) - { - ThrowIfBodyIsEmpty(receivedMqttPacket.BodyReader); - - var retain = (receivedMqttPacket.FixedHeader & 0x1) > 0; - var qualityOfServiceLevel = (MqttQualityOfServiceLevel)(receivedMqttPacket.FixedHeader >> 1 & 0x3); - var dup = (receivedMqttPacket.FixedHeader & 0x8) > 0; - - var topic = receivedMqttPacket.BodyReader.ReadStringWithLengthPrefix(); - - ushort packetIdentifier = 0; - if (qualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) - { - packetIdentifier = receivedMqttPacket.BodyReader.ReadTwoByteInteger(); - } - - var packet = new MqttPublishPacket - { - PacketIdentifier = packetIdentifier, - Retain = retain, - Topic = topic, - QualityOfServiceLevel = qualityOfServiceLevel, - Dup = dup - }; - - if (!receivedMqttPacket.BodyReader.EndOfStream) - { - packet.Payload = receivedMqttPacket.BodyReader.ReadRemainingData(); - } - - return packet; - } - - MqttBasePacket DecodeConnectPacket(IMqttPacketBodyReader body) - { - ThrowIfBodyIsEmpty(body); - - var protocolName = body.ReadStringWithLengthPrefix(); - var protocolVersion = body.ReadByte(); - - if (protocolName != "MQTT" && protocolName != "MQIsdp") - { - throw new MqttProtocolViolationException("MQTT protocol name do not match MQTT v3."); - } - - if (protocolVersion != 3 && protocolVersion != 4) - { - throw new MqttProtocolViolationException("MQTT protocol version do not match MQTT v3."); - } - - var packet = new MqttConnectPacket(); - - var connectFlags = body.ReadByte(); - if ((connectFlags & 0x1) > 0) - { - throw new MqttProtocolViolationException("The first bit of the Connect Flags must be set to 0."); - } - - packet.CleanSession = (connectFlags & 0x2) > 0; - - var willFlag = (connectFlags & 0x4) > 0; - var willQoS = (connectFlags & 0x18) >> 3; - var willRetain = (connectFlags & 0x20) > 0; - var passwordFlag = (connectFlags & 0x40) > 0; - var usernameFlag = (connectFlags & 0x80) > 0; - - packet.KeepAlivePeriod = body.ReadTwoByteInteger(); - packet.ClientId = body.ReadStringWithLengthPrefix(); - - if (willFlag) - { - packet.WillMessage = new MqttApplicationMessage - { - Topic = body.ReadStringWithLengthPrefix(), - Payload = body.ReadWithLengthPrefix(), - QualityOfServiceLevel = (MqttQualityOfServiceLevel)willQoS, - Retain = willRetain - }; - } - - if (usernameFlag) - { - packet.Username = body.ReadStringWithLengthPrefix(); - } - - if (passwordFlag) - { - packet.Password = body.ReadWithLengthPrefix(); - } - - ValidateConnectPacket(packet); - return packet; - } - - static MqttBasePacket DecodeSubAckPacket(IMqttPacketBodyReader body) - { - ThrowIfBodyIsEmpty(body); - - var packet = new MqttSubAckPacket - { - PacketIdentifier = body.ReadTwoByteInteger() - }; - - while (!body.EndOfStream) - { - packet.ReturnCodes.Add((MqttSubscribeReturnCode)body.ReadByte()); - } - - return packet; - } - - protected virtual MqttBasePacket DecodeConnAckPacket(IMqttPacketBodyReader body) - { - ThrowIfBodyIsEmpty(body); - - var packet = new MqttConnAckPacket(); - - body.ReadByte(); // Reserved. - packet.ReturnCode = (MqttConnectReturnCode)body.ReadByte(); - - return packet; - } - - protected void ValidateConnectPacket(MqttConnectPacket packet) - { - if (packet == null) throw new ArgumentNullException(nameof(packet)); - - if (string.IsNullOrEmpty(packet.ClientId) && !packet.CleanSession) - { - throw new MqttProtocolViolationException("CleanSession must be set if ClientId is empty [MQTT-3.1.3-7]."); - } - } - - // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local - static void ValidatePublishPacket(MqttPublishPacket packet) - { - if (packet.QualityOfServiceLevel == 0 && packet.Dup) - { - throw new MqttProtocolViolationException("Dup flag must be false for QoS 0 packets [MQTT-3.3.1-2]."); - } - } - - protected virtual byte EncodeConnectPacket(MqttConnectPacket packet, IMqttPacketWriter packetWriter) - { - ValidateConnectPacket(packet); - - packetWriter.WriteWithLengthPrefix("MQIsdp"); - packetWriter.Write(3); // Protocol Level 3 - - byte connectFlags = 0x0; - if (packet.CleanSession) - { - connectFlags |= 0x2; - } - - if (packet.WillMessage != null) - { - connectFlags |= 0x4; - connectFlags |= (byte)((byte)packet.WillMessage.QualityOfServiceLevel << 3); - - if (packet.WillMessage.Retain) - { - connectFlags |= 0x20; - } - } - - if (packet.Password != null && packet.Username == null) - { - throw new MqttProtocolViolationException("If the User Name Flag is set to 0, the Password Flag MUST be set to 0 [MQTT-3.1.2-22]."); - } - - if (packet.Password != null) - { - connectFlags |= 0x40; - } - - if (packet.Username != null) - { - connectFlags |= 0x80; - } - - packetWriter.Write(connectFlags); - packetWriter.Write(packet.KeepAlivePeriod); - packetWriter.WriteWithLengthPrefix(packet.ClientId); - - if (packet.WillMessage != null) - { - packetWriter.WriteWithLengthPrefix(packet.WillMessage.Topic); - packetWriter.WriteWithLengthPrefix(packet.WillMessage.Payload); - } - - if (packet.Username != null) - { - packetWriter.WriteWithLengthPrefix(packet.Username); - } - - if (packet.Password != null) - { - packetWriter.WriteWithLengthPrefix(packet.Password); - } - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Connect); - } - - protected virtual byte EncodeConnAckPacket(MqttConnAckPacket packet, IMqttPacketWriter packetWriter) - { - packetWriter.Write(0); // Reserved. - packetWriter.Write((byte)packet.ReturnCode); - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.ConnAck); - } - - static byte EncodePubRelPacket(MqttPubRelPacket packet, IMqttPacketWriter packetWriter) - { - if (packet.PacketIdentifier == 0) - { - throw new MqttProtocolViolationException("PubRel packet has no packet identifier."); - } - - packetWriter.Write(packet.PacketIdentifier); - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PubRel, 0x02); - } - - static byte EncodePublishPacket(MqttPublishPacket packet, IMqttPacketWriter packetWriter) - { - ValidatePublishPacket(packet); - - packetWriter.WriteWithLengthPrefix(packet.Topic); - - if (packet.QualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) - { - if (packet.PacketIdentifier == 0) - { - throw new MqttProtocolViolationException("Publish packet has no packet identifier."); - } - - packetWriter.Write(packet.PacketIdentifier); - } - else - { - if (packet.PacketIdentifier > 0) - { - throw new MqttProtocolViolationException("Packet identifier must be empty if QoS == 0 [MQTT-2.3.1-5]."); - } - } - - if (packet.Payload?.Length > 0) - { - packetWriter.Write(packet.Payload, 0, packet.Payload.Length); - } - - byte fixedHeader = 0; - - if (packet.Retain) - { - fixedHeader |= 0x01; - } - - fixedHeader |= (byte)((byte)packet.QualityOfServiceLevel << 1); - - if (packet.Dup) - { - fixedHeader |= 0x08; - } - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Publish, fixedHeader); - } - - static byte EncodePubAckPacket(MqttPubAckPacket packet, IMqttPacketWriter packetWriter) - { - if (packet.PacketIdentifier == 0) - { - throw new MqttProtocolViolationException("PubAck packet has no packet identifier."); - } - - packetWriter.Write(packet.PacketIdentifier); - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PubAck); - } - - static byte EncodePubRecPacket(MqttPubRecPacket packet, IMqttPacketWriter packetWriter) - { - if (packet.PacketIdentifier == 0) - { - throw new MqttProtocolViolationException("PubRec packet has no packet identifier."); - } - - packetWriter.Write(packet.PacketIdentifier); - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PubRec); - } - - static byte EncodePubCompPacket(MqttPubCompPacket packet, IMqttPacketWriter packetWriter) - { - if (packet.PacketIdentifier == 0) - { - throw new MqttProtocolViolationException("PubComp packet has no packet identifier."); - } - - packetWriter.Write(packet.PacketIdentifier); - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PubComp); - } - - static byte EncodeSubscribePacket(MqttSubscribePacket packet, IMqttPacketWriter packetWriter) - { - if (!packet.TopicFilters.Any()) throw new MqttProtocolViolationException("At least one topic filter must be set [MQTT-3.8.3-3]."); - - if (packet.PacketIdentifier == 0) - { - throw new MqttProtocolViolationException("Subscribe packet has no packet identifier."); - } - - packetWriter.Write(packet.PacketIdentifier); - - if (packet.TopicFilters?.Count > 0) - { - foreach (var topicFilter in packet.TopicFilters) - { - if (topicFilter.NoLocal) - { - throw new MqttProtocolViolationException("NoLocal is not supported in 3.1.1."); - } - - if (topicFilter.RetainAsPublished) - { - throw new MqttProtocolViolationException("RetainAsPublished is not supported in 3.1.1."); - } - - if (topicFilter.RetainHandling != MqttRetainHandling.SendAtSubscribe) - { - throw new MqttProtocolViolationException("RetainHandling is not supported in 3.1.1."); - } - - packetWriter.WriteWithLengthPrefix(topicFilter.Topic); - packetWriter.Write((byte)topicFilter.QualityOfServiceLevel); - } - } - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Subscribe, 0x02); - } - - static byte EncodeSubAckPacket(MqttSubAckPacket packet, IMqttPacketWriter packetWriter) - { - if (packet.PacketIdentifier == 0) - { - throw new MqttProtocolViolationException("SubAck packet has no packet identifier."); - } - - packetWriter.Write(packet.PacketIdentifier); - - if (packet.ReturnCodes?.Any() == true) - { - foreach (var packetSubscribeReturnCode in packet.ReturnCodes) - { - packetWriter.Write((byte)packetSubscribeReturnCode); - } - } - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.SubAck); - } - - static byte EncodeUnsubscribePacket(MqttUnsubscribePacket packet, IMqttPacketWriter packetWriter) - { - if (!packet.TopicFilters.Any()) throw new MqttProtocolViolationException("At least one topic filter must be set [MQTT-3.10.3-2]."); - - if (packet.PacketIdentifier == 0) - { - throw new MqttProtocolViolationException("Unsubscribe packet has no packet identifier."); - } - - packetWriter.Write(packet.PacketIdentifier); - - if (packet.TopicFilters?.Any() == true) - { - foreach (var topicFilter in packet.TopicFilters) - { - packetWriter.WriteWithLengthPrefix(topicFilter); - } - } - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Unsubscibe, 0x02); - } - - static byte EncodeUnsubAckPacket(MqttUnsubAckPacket packet, IMqttPacketWriter packetWriter) - { - if (packet.PacketIdentifier == 0) - { - throw new MqttProtocolViolationException("UnsubAck packet has no packet identifier."); - } - - packetWriter.Write(packet.PacketIdentifier); - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.UnsubAck); - } - - static byte EncodeEmptyPacket(MqttControlPacketType type) - { - return MqttPacketWriter.BuildFixedHeader(type); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static void ThrowIfBodyIsEmpty(IMqttPacketBodyReader body) - { - if (body == null || body.Length == 0) - { - throw new MqttProtocolViolationException("Data from the body is required but not present."); - } - } - } -} diff --git a/Source/MQTTnet/Formatter/V3/MqttV311PacketFormatter.cs b/Source/MQTTnet/Formatter/V3/MqttV311PacketFormatter.cs deleted file mode 100644 index 09b0d0e..0000000 --- a/Source/MQTTnet/Formatter/V3/MqttV311PacketFormatter.cs +++ /dev/null @@ -1,104 +0,0 @@ -using MQTTnet.Exceptions; -using MQTTnet.Packets; -using MQTTnet.Protocol; - -namespace MQTTnet.Formatter.V3 -{ - public sealed class MqttV311PacketFormatter : MqttV310PacketFormatter - { - public MqttV311PacketFormatter(IMqttPacketWriter packetWriter) - : base(packetWriter) - { - } - - protected override byte EncodeConnectPacket(MqttConnectPacket packet, IMqttPacketWriter packetWriter) - { - ValidateConnectPacket(packet); - - packetWriter.WriteWithLengthPrefix("MQTT"); - packetWriter.Write(4); // 3.1.2.2 Protocol Level 4 - - byte connectFlags = 0x0; - if (packet.CleanSession) - { - connectFlags |= 0x2; - } - - if (packet.WillMessage != null) - { - connectFlags |= 0x4; - connectFlags |= (byte)((byte)packet.WillMessage.QualityOfServiceLevel << 3); - - if (packet.WillMessage.Retain) - { - connectFlags |= 0x20; - } - } - - if (packet.Password != null && packet.Username == null) - { - throw new MqttProtocolViolationException("If the User Name Flag is set to 0, the Password Flag MUST be set to 0 [MQTT-3.1.2-22]."); - } - - if (packet.Password != null) - { - connectFlags |= 0x40; - } - - if (packet.Username != null) - { - connectFlags |= 0x80; - } - - packetWriter.Write(connectFlags); - packetWriter.Write(packet.KeepAlivePeriod); - packetWriter.WriteWithLengthPrefix(packet.ClientId); - - if (packet.WillMessage != null) - { - packetWriter.WriteWithLengthPrefix(packet.WillMessage.Topic); - packetWriter.WriteWithLengthPrefix(packet.WillMessage.Payload); - } - - if (packet.Username != null) - { - packetWriter.WriteWithLengthPrefix(packet.Username); - } - - if (packet.Password != null) - { - packetWriter.WriteWithLengthPrefix(packet.Password); - } - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Connect); - } - - protected override byte EncodeConnAckPacket(MqttConnAckPacket packet, IMqttPacketWriter packetWriter) - { - byte connectAcknowledgeFlags = 0x0; - if (packet.IsSessionPresent) - { - connectAcknowledgeFlags |= 0x1; - } - - packetWriter.Write(connectAcknowledgeFlags); - packetWriter.Write((byte)packet.ReturnCode); - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.ConnAck); - } - - protected override MqttBasePacket DecodeConnAckPacket(IMqttPacketBodyReader body) - { - ThrowIfBodyIsEmpty(body); - - var packet = new MqttConnAckPacket(); - - var acknowledgeFlags = body.ReadByte(); - - packet.IsSessionPresent = (acknowledgeFlags & 0x1) > 0; - packet.ReturnCode = (MqttConnectReturnCode)body.ReadByte(); - - return packet; - } - } -} diff --git a/Source/MQTTnet/Formatter/V3/MqttV3PacketFormatter.cs b/Source/MQTTnet/Formatter/V3/MqttV3PacketFormatter.cs new file mode 100644 index 0000000..d8276bb --- /dev/null +++ b/Source/MQTTnet/Formatter/V3/MqttV3PacketFormatter.cs @@ -0,0 +1,807 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using MQTTnet.Adapter; +using MQTTnet.Exceptions; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Formatter.V3 +{ + public sealed class MqttV3PacketFormatter : IMqttPacketFormatter + { + const int FixedHeaderSize = 1; + + static readonly MqttDisconnectPacket DisconnectPacket = new MqttDisconnectPacket(); + + readonly MqttBufferReader _bufferReader = new MqttBufferReader(); + readonly MqttBufferWriter _bufferWriter; + readonly MqttProtocolVersion _mqttProtocolVersion; + + public MqttV3PacketFormatter(MqttBufferWriter bufferWriter, MqttProtocolVersion mqttProtocolVersion) + { + _bufferWriter = bufferWriter; + _mqttProtocolVersion = mqttProtocolVersion; + } + + public MqttPacket Decode(ReceivedMqttPacket receivedMqttPacket) + { + if (receivedMqttPacket.TotalLength == 0) + { + return null; + } + + var controlPacketType = receivedMqttPacket.FixedHeader >> 4; + if (controlPacketType < 1 || controlPacketType > 14) + { + throw new MqttProtocolViolationException($"The packet type is invalid ({controlPacketType})."); + } + + switch ((MqttControlPacketType)controlPacketType) + { + case MqttControlPacketType.Publish: + return DecodePublishPacket(receivedMqttPacket); + case MqttControlPacketType.PubAck: + return DecodePubAckPacket(receivedMqttPacket.Body); + case MqttControlPacketType.PubRec: + return DecodePubRecPacket(receivedMqttPacket.Body); + case MqttControlPacketType.PubRel: + return DecodePubRelPacket(receivedMqttPacket.Body); + case MqttControlPacketType.PubComp: + return DecodePubCompPacket(receivedMqttPacket.Body); + + case MqttControlPacketType.PingReq: + return MqttPingReqPacket.Instance; + case MqttControlPacketType.PingResp: + return MqttPingRespPacket.Instance; + + case MqttControlPacketType.Connect: + return DecodeConnectPacket(receivedMqttPacket.Body); + case MqttControlPacketType.ConnAck: + if (_mqttProtocolVersion == MqttProtocolVersion.V311) + { + return DecodeConnAckPacketV311(receivedMqttPacket.Body); + } + else + { + return DecodeConnAckPacket(receivedMqttPacket.Body); + } + case MqttControlPacketType.Disconnect: + return DisconnectPacket; + + case MqttControlPacketType.Subscribe: + return DecodeSubscribePacket(receivedMqttPacket.Body); + case MqttControlPacketType.SubAck: + return DecodeSubAckPacket(receivedMqttPacket.Body); + case MqttControlPacketType.Unsubscibe: + return DecodeUnsubscribePacket(receivedMqttPacket.Body); + case MqttControlPacketType.UnsubAck: + return DecodeUnsubAckPacket(receivedMqttPacket.Body); + + default: + throw new MqttProtocolViolationException($"Packet type ({controlPacketType}) not supported."); + } + } + + public MqttPacketBuffer Encode(MqttPacket packet) + { + if (packet == null) + { + throw new ArgumentNullException(nameof(packet)); + } + + // Leave enough head space for max header size (fixed + 4 variable remaining length = 5 bytes) + _bufferWriter.Reset(5); + _bufferWriter.Seek(5); + + var fixedHeader = EncodePacket(packet, _bufferWriter); + var remainingLength = (uint)(_bufferWriter.Length - 5); + + var publishPacket = packet as MqttPublishPacket; + if (publishPacket?.Payload != null) + { + remainingLength += (uint)publishPacket.Payload.Length; + } + + var remainingLengthSize = MqttBufferWriter.GetLengthOfVariableInteger(remainingLength); + + var headerSize = FixedHeaderSize + remainingLengthSize; + var headerOffset = 5 - headerSize; + + // Position cursor on correct offset on beginning of array (has leading 0x0) + _bufferWriter.Seek(headerOffset); + _bufferWriter.WriteByte(fixedHeader); + _bufferWriter.WriteVariableByteInteger(remainingLength); + + var buffer = _bufferWriter.GetBuffer(); + + var firstSegment = new ArraySegment(buffer, headerOffset, _bufferWriter.Length - headerOffset); + + if (publishPacket?.Payload != null) + { + var payloadSegment = new ArraySegment(publishPacket.Payload, 0, publishPacket.Payload.Length); + return new MqttPacketBuffer(firstSegment, payloadSegment); + } + + return new MqttPacketBuffer(firstSegment); + } + + MqttPacket DecodeConnAckPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttConnAckPacket(); + + _bufferReader.ReadByte(); // Reserved. + packet.ReturnCode = (MqttConnectReturnCode)_bufferReader.ReadByte(); + + return packet; + } + + MqttPacket DecodeConnAckPacketV311(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttConnAckPacket(); + + var acknowledgeFlags = _bufferReader.ReadByte(); + + packet.IsSessionPresent = (acknowledgeFlags & 0x1) > 0; + packet.ReturnCode = (MqttConnectReturnCode)_bufferReader.ReadByte(); + + return packet; + } + + MqttPacket DecodeConnectPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var protocolName = _bufferReader.ReadString(); + var protocolVersion = _bufferReader.ReadByte(); + + if (protocolName != "MQTT" && protocolName != "MQIsdp") + { + throw new MqttProtocolViolationException("MQTT protocol name do not match MQTT v3."); + } + + if (protocolVersion != 3 && protocolVersion != 4) + { + throw new MqttProtocolViolationException("MQTT protocol version do not match MQTT v3."); + } + + var packet = new MqttConnectPacket(); + + var connectFlags = _bufferReader.ReadByte(); + if ((connectFlags & 0x1) > 0) + { + throw new MqttProtocolViolationException("The first bit of the Connect Flags must be set to 0."); + } + + packet.CleanSession = (connectFlags & 0x2) > 0; + + var willFlag = (connectFlags & 0x4) > 0; + var willQoS = (connectFlags & 0x18) >> 3; + var willRetain = (connectFlags & 0x20) > 0; + var passwordFlag = (connectFlags & 0x40) > 0; + var usernameFlag = (connectFlags & 0x80) > 0; + + packet.KeepAlivePeriod = _bufferReader.ReadTwoByteInteger(); + packet.ClientId = _bufferReader.ReadString(); + + if (willFlag) + { + packet.WillFlag = true; + packet.WillQoS = (MqttQualityOfServiceLevel)willQoS; + packet.WillRetain = willRetain; + + packet.WillTopic = _bufferReader.ReadString(); + packet.WillMessage = _bufferReader.ReadBinaryData(); + } + + if (usernameFlag) + { + packet.Username = _bufferReader.ReadString(); + } + + if (passwordFlag) + { + packet.Password = _bufferReader.ReadBinaryData(); + } + + ValidateConnectPacket(packet); + return packet; + } + + MqttPacket DecodePubAckPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + return new MqttPubAckPacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + } + + MqttPacket DecodePubCompPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + return new MqttPubCompPacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + } + + MqttPacket DecodePublishPacket(ReceivedMqttPacket receivedMqttPacket) + { + ThrowIfBodyIsEmpty(receivedMqttPacket.Body); + + _bufferReader.SetBuffer(receivedMqttPacket.Body.Array, receivedMqttPacket.Body.Offset, receivedMqttPacket.Body.Count); + + var retain = (receivedMqttPacket.FixedHeader & 0x1) > 0; + var qualityOfServiceLevel = (MqttQualityOfServiceLevel)((receivedMqttPacket.FixedHeader >> 1) & 0x3); + var dup = (receivedMqttPacket.FixedHeader & 0x8) > 0; + + var topic = _bufferReader.ReadString(); + + ushort packetIdentifier = 0; + if (qualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) + { + packetIdentifier = _bufferReader.ReadTwoByteInteger(); + } + + var packet = new MqttPublishPacket + { + PacketIdentifier = packetIdentifier, + Retain = retain, + Topic = topic, + QualityOfServiceLevel = qualityOfServiceLevel, + Dup = dup + }; + + if (!_bufferReader.EndOfStream) + { + packet.Payload = _bufferReader.ReadRemainingData(); + } + + return packet; + } + + MqttPacket DecodePubRecPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + return new MqttPubRecPacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + } + + MqttPacket DecodePubRelPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + return new MqttPubRelPacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + } + + MqttPacket DecodeSubAckPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttSubAckPacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger(), + ReasonCodes = new List(_bufferReader.BytesLeft) + }; + + while (!_bufferReader.EndOfStream) + { + packet.ReasonCodes.Add((MqttSubscribeReasonCode)_bufferReader.ReadByte()); + } + + return packet; + } + + MqttPacket DecodeSubscribePacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttSubscribePacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + + while (!_bufferReader.EndOfStream) + { + var topicFilter = new MqttTopicFilter + { + Topic = _bufferReader.ReadString(), + QualityOfServiceLevel = (MqttQualityOfServiceLevel)_bufferReader.ReadByte() + }; + + packet.TopicFilters.Add(topicFilter); + } + + return packet; + } + + MqttPacket DecodeUnsubAckPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + return new MqttUnsubAckPacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + } + + MqttPacket DecodeUnsubscribePacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttUnsubscribePacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + + while (!_bufferReader.EndOfStream) + { + packet.TopicFilters.Add(_bufferReader.ReadString()); + } + + return packet; + } + + byte EncodeConnAckPacket(MqttConnAckPacket packet, MqttBufferWriter bufferWriter) + { + bufferWriter.WriteByte(0); // Reserved. + bufferWriter.WriteByte((byte)packet.ReturnCode); + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.ConnAck); + } + + byte EncodeConnAckPacketV311(MqttConnAckPacket packet, MqttBufferWriter bufferWriter) + { + byte connectAcknowledgeFlags = 0x0; + if (packet.IsSessionPresent) + { + connectAcknowledgeFlags |= 0x1; + } + + bufferWriter.WriteByte(connectAcknowledgeFlags); + bufferWriter.WriteByte((byte)packet.ReturnCode); + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.ConnAck); + } + + byte EncodeConnectPacket(MqttConnectPacket packet, MqttBufferWriter bufferWriter) + { + ValidateConnectPacket(packet); + + bufferWriter.WriteString("MQIsdp"); + bufferWriter.WriteByte(3); // Protocol Level 3 + + byte connectFlags = 0x0; + if (packet.CleanSession) + { + connectFlags |= 0x2; + } + + if (packet.WillFlag) + { + connectFlags |= 0x4; + connectFlags |= (byte)((byte)packet.WillQoS << 3); + + if (packet.WillRetain) + { + connectFlags |= 0x20; + } + } + + if (packet.Password != null && packet.Username == null) + { + throw new MqttProtocolViolationException("If the User Name Flag is set to 0, the Password Flag MUST be set to 0 [MQTT-3.1.2-22]."); + } + + if (packet.Password != null) + { + connectFlags |= 0x40; + } + + if (packet.Username != null) + { + connectFlags |= 0x80; + } + + bufferWriter.WriteByte(connectFlags); + bufferWriter.WriteTwoByteInteger(packet.KeepAlivePeriod); + bufferWriter.WriteString(packet.ClientId); + + if (packet.WillFlag) + { + bufferWriter.WriteString(packet.WillTopic); + bufferWriter.WriteBinaryData(packet.WillMessage); + } + + if (packet.Username != null) + { + bufferWriter.WriteString(packet.Username); + } + + if (packet.Password != null) + { + bufferWriter.WriteBinaryData(packet.Password); + } + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.Connect); + } + + byte EncodeConnectPacketV311(MqttConnectPacket packet, MqttBufferWriter bufferWriter) + { + ValidateConnectPacket(packet); + + bufferWriter.WriteString("MQTT"); + bufferWriter.WriteByte(4); // 3.1.2.2 Protocol Level 4 + + byte connectFlags = 0x0; + if (packet.CleanSession) + { + connectFlags |= 0x2; + } + + if (packet.WillFlag) + { + connectFlags |= 0x4; + connectFlags |= (byte)((byte)packet.WillQoS << 3); + + if (packet.WillRetain) + { + connectFlags |= 0x20; + } + } + + if (packet.Password != null && packet.Username == null) + { + throw new MqttProtocolViolationException("If the User Name Flag is set to 0, the Password Flag MUST be set to 0 [MQTT-3.1.2-22]."); + } + + if (packet.Password != null) + { + connectFlags |= 0x40; + } + + if (packet.Username != null) + { + connectFlags |= 0x80; + } + + bufferWriter.WriteByte(connectFlags); + bufferWriter.WriteTwoByteInteger(packet.KeepAlivePeriod); + bufferWriter.WriteString(packet.ClientId); + + if (packet.WillFlag) + { + bufferWriter.WriteString(packet.WillTopic); + bufferWriter.WriteBinaryData(packet.WillMessage); + } + + if (packet.Username != null) + { + bufferWriter.WriteString(packet.Username); + } + + if (packet.Password != null) + { + bufferWriter.WriteBinaryData(packet.Password); + } + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.Connect); + } + + static byte EncodeEmptyPacket(MqttControlPacketType type) + { + return MqttBufferWriter.BuildFixedHeader(type); + } + + byte EncodePacket(MqttPacket packet, MqttBufferWriter bufferWriter) + { + switch (packet) + { + case MqttConnectPacket connectPacket: + if (_mqttProtocolVersion == MqttProtocolVersion.V311) + { + return EncodeConnectPacketV311(connectPacket, bufferWriter); + } + else + { + return EncodeConnectPacket(connectPacket, bufferWriter); + } + case MqttConnAckPacket connAckPacket: + if (_mqttProtocolVersion == MqttProtocolVersion.V311) + { + return EncodeConnAckPacketV311(connAckPacket, bufferWriter); + } + else + { + return EncodeConnAckPacket(connAckPacket, bufferWriter); + } + case MqttDisconnectPacket _: + return EncodeEmptyPacket(MqttControlPacketType.Disconnect); + case MqttPingReqPacket _: + return EncodeEmptyPacket(MqttControlPacketType.PingReq); + case MqttPingRespPacket _: + return EncodeEmptyPacket(MqttControlPacketType.PingResp); + case MqttPublishPacket publishPacket: + return EncodePublishPacket(publishPacket, bufferWriter); + case MqttPubAckPacket pubAckPacket: + return EncodePubAckPacket(pubAckPacket, bufferWriter); + case MqttPubRecPacket pubRecPacket: + return EncodePubRecPacket(pubRecPacket, bufferWriter); + case MqttPubRelPacket pubRelPacket: + return EncodePubRelPacket(pubRelPacket, bufferWriter); + case MqttPubCompPacket pubCompPacket: + return EncodePubCompPacket(pubCompPacket, bufferWriter); + case MqttSubscribePacket subscribePacket: + return EncodeSubscribePacket(subscribePacket, bufferWriter); + case MqttSubAckPacket subAckPacket: + return EncodeSubAckPacket(subAckPacket, bufferWriter); + case MqttUnsubscribePacket unsubscribePacket: + return EncodeUnsubscribePacket(unsubscribePacket, bufferWriter); + case MqttUnsubAckPacket unsubAckPacket: + return EncodeUnsubAckPacket(unsubAckPacket, bufferWriter); + + default: + throw new MqttProtocolViolationException("Packet type invalid."); + } + } + + static byte EncodePubAckPacket(MqttPubAckPacket packet, MqttBufferWriter bufferWriter) + { + if (packet.PacketIdentifier == 0) + { + throw new MqttProtocolViolationException("PubAck packet has no packet identifier."); + } + + bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.PubAck); + } + + static byte EncodePubCompPacket(MqttPubCompPacket packet, MqttBufferWriter bufferWriter) + { + if (packet.PacketIdentifier == 0) + { + throw new MqttProtocolViolationException("PubComp packet has no packet identifier."); + } + + bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.PubComp); + } + + static byte EncodePublishPacket(MqttPublishPacket packet, MqttBufferWriter bufferWriter) + { + ValidatePublishPacket(packet); + + bufferWriter.WriteString(packet.Topic); + + if (packet.QualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) + { + if (packet.PacketIdentifier == 0) + { + throw new MqttProtocolViolationException("Publish packet has no packet identifier."); + } + + bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + } + else + { + if (packet.PacketIdentifier > 0) + { + throw new MqttProtocolViolationException("Packet identifier must be empty if QoS == 0 [MQTT-2.3.1-5]."); + } + } + + // The payload is the past part of the packet. But it is not added here in order to keep + // memory allocation low. + + byte fixedHeader = 0; + + if (packet.Retain) + { + fixedHeader |= 0x01; + } + + fixedHeader |= (byte)((byte)packet.QualityOfServiceLevel << 1); + + if (packet.Dup) + { + fixedHeader |= 0x08; + } + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.Publish, fixedHeader); + } + + static byte EncodePubRecPacket(MqttPubRecPacket packet, MqttBufferWriter bufferWriter) + { + if (packet.PacketIdentifier == 0) + { + throw new MqttProtocolViolationException("PubRec packet has no packet identifier."); + } + + bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.PubRec); + } + + static byte EncodePubRelPacket(MqttPubRelPacket packet, MqttBufferWriter bufferWriter) + { + if (packet.PacketIdentifier == 0) + { + throw new MqttProtocolViolationException("PubRel packet has no packet identifier."); + } + + bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.PubRel, 0x02); + } + + static byte EncodeSubAckPacket(MqttSubAckPacket packet, MqttBufferWriter bufferWriter) + { + if (packet.PacketIdentifier == 0) + { + throw new MqttProtocolViolationException("SubAck packet has no packet identifier."); + } + + bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + + if (packet.ReasonCodes.Any()) + { + foreach (var packetSubscribeReturnCode in packet.ReasonCodes) + { + if (packetSubscribeReturnCode == MqttSubscribeReasonCode.GrantedQoS0) + { + bufferWriter.WriteByte((byte)MqttSubscribeReturnCode.SuccessMaximumQoS0); + } + else if (packetSubscribeReturnCode == MqttSubscribeReasonCode.GrantedQoS1) + { + bufferWriter.WriteByte((byte)MqttSubscribeReturnCode.SuccessMaximumQoS1); + } + else if (packetSubscribeReturnCode == MqttSubscribeReasonCode.GrantedQoS2) + { + bufferWriter.WriteByte((byte)MqttSubscribeReturnCode.SuccessMaximumQoS2); + } + else + { + bufferWriter.WriteByte((byte)MqttSubscribeReturnCode.Failure); + } + } + } + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.SubAck); + } + + static byte EncodeSubscribePacket(MqttSubscribePacket packet, MqttBufferWriter bufferWriter) + { + if (!packet.TopicFilters.Any()) + { + throw new MqttProtocolViolationException("At least one topic filter must be set [MQTT-3.8.3-3]."); + } + + if (packet.PacketIdentifier == 0) + { + throw new MqttProtocolViolationException("Subscribe packet has no packet identifier."); + } + + bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + + if (packet.TopicFilters?.Count > 0) + { + foreach (var topicFilter in packet.TopicFilters) + { + bufferWriter.WriteString(topicFilter.Topic); + bufferWriter.WriteByte((byte)topicFilter.QualityOfServiceLevel); + } + } + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.Subscribe, 0x02); + } + + static byte EncodeUnsubAckPacket(MqttUnsubAckPacket packet, MqttBufferWriter bufferWriter) + { + if (packet.PacketIdentifier == 0) + { + throw new MqttProtocolViolationException("UnsubAck packet has no packet identifier."); + } + + bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.UnsubAck); + } + + static byte EncodeUnsubscribePacket(MqttUnsubscribePacket packet, MqttBufferWriter bufferWriter) + { + if (!packet.TopicFilters.Any()) + { + throw new MqttProtocolViolationException("At least one topic filter must be set [MQTT-3.10.3-2]."); + } + + if (packet.PacketIdentifier == 0) + { + throw new MqttProtocolViolationException("Unsubscribe packet has no packet identifier."); + } + + bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + + if (packet.TopicFilters?.Any() == true) + { + foreach (var topicFilter in packet.TopicFilters) + { + bufferWriter.WriteString(topicFilter); + } + } + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.Unsubscibe, 0x02); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void ThrowIfBodyIsEmpty(ArraySegment body) + { + if (body.Count == 0) + { + throw new MqttProtocolViolationException("Data from the body is required but not present."); + } + } + + void ValidateConnectPacket(MqttConnectPacket packet) + { + if (packet == null) + { + throw new ArgumentNullException(nameof(packet)); + } + + if (string.IsNullOrEmpty(packet.ClientId) && !packet.CleanSession) + { + throw new MqttProtocolViolationException("CleanSession must be set if ClientId is empty [MQTT-3.1.3-7]."); + } + } + + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local + static void ValidatePublishPacket(MqttPublishPacket packet) + { + if (packet.QualityOfServiceLevel == 0 && packet.Dup) + { + throw new MqttProtocolViolationException("Dup flag must be false for QoS 0 packets [MQTT-3.3.1-2]."); + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/V5/MqttV500DataConverter.cs b/Source/MQTTnet/Formatter/V5/MqttV500DataConverter.cs deleted file mode 100644 index 5f034c0..0000000 --- a/Source/MQTTnet/Formatter/V5/MqttV500DataConverter.cs +++ /dev/null @@ -1,369 +0,0 @@ -using MQTTnet.Client.Connecting; -using MQTTnet.Client.Disconnecting; -using MQTTnet.Client.Options; -using MQTTnet.Client.Publishing; -using MQTTnet.Client.Subscribing; -using MQTTnet.Client.Unsubscribing; -using MQTTnet.Exceptions; -using MQTTnet.Packets; -using MQTTnet.Protocol; -using MQTTnet.Server; -using System; -using System.Collections.Generic; -using System.Linq; -using MQTTnet.Server.Internal; - -namespace MQTTnet.Formatter.V5 -{ - public sealed class MqttV500DataConverter : IMqttDataConverter - { - public MqttPublishPacket CreatePublishPacket(MqttApplicationMessage applicationMessage) - { - if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); - - var packet = new MqttPublishPacket - { - Topic = applicationMessage.Topic, - Payload = applicationMessage.Payload, - QualityOfServiceLevel = applicationMessage.QualityOfServiceLevel, - Retain = applicationMessage.Retain, - Dup = applicationMessage.Dup, - Properties = new MqttPublishPacketProperties - { - ContentType = applicationMessage.ContentType, - CorrelationData = applicationMessage.CorrelationData, - MessageExpiryInterval = applicationMessage.MessageExpiryInterval, - PayloadFormatIndicator = applicationMessage.PayloadFormatIndicator, - ResponseTopic = applicationMessage.ResponseTopic, - SubscriptionIdentifiers = applicationMessage.SubscriptionIdentifiers, - TopicAlias = applicationMessage.TopicAlias - } - }; - - if (applicationMessage.UserProperties != null) - { - packet.Properties.UserProperties = new List(); - packet.Properties.UserProperties.AddRange(applicationMessage.UserProperties); - } - - return packet; - } - - public MqttPubAckPacket CreatePubAckPacket(MqttPublishPacket publishPacket, MqttApplicationMessageReceivedReasonCode reasonCode) - { - if (publishPacket == null) throw new ArgumentNullException(nameof(publishPacket)); - - return new MqttPubAckPacket - { - PacketIdentifier = publishPacket.PacketIdentifier, - ReasonCode = (MqttPubAckReasonCode)(int)reasonCode - }; - } - - public MqttPubRecPacket CreatePubRecPacket(MqttPublishPacket publishPacket, MqttApplicationMessageReceivedReasonCode reasonCode) - { - if (publishPacket == null) throw new ArgumentNullException(nameof(publishPacket)); - - return new MqttPubRecPacket - { - PacketIdentifier = publishPacket.PacketIdentifier, - ReasonCode = (MqttPubRecReasonCode)(int)reasonCode - }; - } - - public MqttPubCompPacket CreatePubCompPacket(MqttPubRelPacket pubRelPacket, MqttApplicationMessageReceivedReasonCode reasonCode) - { - if (pubRelPacket == null) throw new ArgumentNullException(nameof(pubRelPacket)); - - return new MqttPubCompPacket - { - PacketIdentifier = pubRelPacket.PacketIdentifier, - ReasonCode = (MqttPubCompReasonCode)(int)reasonCode - }; - } - - public MqttPubRelPacket CreatePubRelPacket(MqttPubRecPacket pubRecPacket, MqttApplicationMessageReceivedReasonCode reasonCode) - { - if (pubRecPacket == null) throw new ArgumentNullException(nameof(pubRecPacket)); - - return new MqttPubRelPacket - { - PacketIdentifier = pubRecPacket.PacketIdentifier, - ReasonCode = (MqttPubRelReasonCode)(int)reasonCode - }; - } - - public MqttApplicationMessage CreateApplicationMessage(MqttPublishPacket publishPacket) - { - if (publishPacket == null) throw new ArgumentNullException(nameof(publishPacket)); - - return new MqttApplicationMessage - { - Topic = publishPacket.Topic, - Payload = publishPacket.Payload, - QualityOfServiceLevel = publishPacket.QualityOfServiceLevel, - Retain = publishPacket.Retain, - Dup = publishPacket.Dup, - ResponseTopic = publishPacket.Properties?.ResponseTopic, - ContentType = publishPacket.Properties?.ContentType, - CorrelationData = publishPacket.Properties?.CorrelationData, - MessageExpiryInterval = publishPacket.Properties?.MessageExpiryInterval, - SubscriptionIdentifiers = publishPacket.Properties?.SubscriptionIdentifiers, - TopicAlias = publishPacket.Properties?.TopicAlias, - PayloadFormatIndicator = publishPacket.Properties?.PayloadFormatIndicator, - UserProperties = publishPacket.Properties?.UserProperties ?? new List() - }; - } - - public MqttClientConnectResult CreateClientConnectResult(MqttConnAckPacket connAckPacket) - { - if (connAckPacket == null) throw new ArgumentNullException(nameof(connAckPacket)); - - return new MqttClientConnectResult - { - IsSessionPresent = connAckPacket.IsSessionPresent, - ResultCode = (MqttClientConnectResultCode)(int)connAckPacket.ReasonCode, - WildcardSubscriptionAvailable = connAckPacket.Properties?.WildcardSubscriptionAvailable ?? true, - RetainAvailable = connAckPacket.Properties?.RetainAvailable ?? true, - AssignedClientIdentifier = connAckPacket.Properties?.AssignedClientIdentifier, - AuthenticationMethod = connAckPacket.Properties?.AuthenticationMethod, - AuthenticationData = connAckPacket.Properties?.AuthenticationData, - MaximumPacketSize = connAckPacket.Properties?.MaximumPacketSize, - ReasonString = connAckPacket.Properties?.ReasonString, - ReceiveMaximum = connAckPacket.Properties?.ReceiveMaximum, - MaximumQoS = connAckPacket.Properties?.MaximumQoS ?? MqttQualityOfServiceLevel.ExactlyOnce, - ResponseInformation = connAckPacket.Properties?.ResponseInformation, - TopicAliasMaximum = connAckPacket.Properties?.TopicAliasMaximum ?? 0, - ServerReference = connAckPacket.Properties?.ServerReference, - ServerKeepAlive = connAckPacket.Properties?.ServerKeepAlive, - SessionExpiryInterval = connAckPacket.Properties?.SessionExpiryInterval, - SubscriptionIdentifiersAvailable = connAckPacket.Properties?.SubscriptionIdentifiersAvailable ?? true, - SharedSubscriptionAvailable = connAckPacket.Properties?.SharedSubscriptionAvailable ?? true, - UserProperties = connAckPacket.Properties?.UserProperties ?? new List() - }; - } - - public MqttConnectPacket CreateConnectPacket(MqttApplicationMessage willApplicationMessage, IMqttClientOptions options) - { - if (options == null) throw new ArgumentNullException(nameof(options)); - - return new MqttConnectPacket - { - ClientId = options.ClientId, - Username = options.Credentials?.Username, - Password = options.Credentials?.Password, - CleanSession = options.CleanSession, - KeepAlivePeriod = (ushort)options.KeepAlivePeriod.TotalSeconds, - WillMessage = willApplicationMessage, - Properties = new MqttConnectPacketProperties - { - AuthenticationMethod = options.AuthenticationMethod, - AuthenticationData = options.AuthenticationData, - WillDelayInterval = options.WillDelayInterval, - MaximumPacketSize = options.MaximumPacketSize, - ReceiveMaximum = options.ReceiveMaximum, - RequestProblemInformation = options.RequestProblemInformation, - RequestResponseInformation = options.RequestResponseInformation, - SessionExpiryInterval = options.SessionExpiryInterval, - TopicAliasMaximum = options.TopicAliasMaximum, - UserProperties = options.UserProperties - } - }; - } - - public MqttConnAckPacket CreateConnAckPacket(MqttConnectionValidatorContext connectionValidatorContext) - { - if (connectionValidatorContext == null) throw new ArgumentNullException(nameof(connectionValidatorContext)); - - return new MqttConnAckPacket - { - ReasonCode = connectionValidatorContext.ReasonCode, - Properties = new MqttConnAckPacketProperties - { - RetainAvailable = true, - SubscriptionIdentifiersAvailable = true, - SharedSubscriptionAvailable = false, - TopicAliasMaximum = ushort.MaxValue, - WildcardSubscriptionAvailable = true, - - UserProperties = connectionValidatorContext.ResponseUserProperties, - AuthenticationMethod = connectionValidatorContext.AuthenticationMethod, - AuthenticationData = connectionValidatorContext.ResponseAuthenticationData, - AssignedClientIdentifier = connectionValidatorContext.AssignedClientIdentifier, - ReasonString = connectionValidatorContext.ReasonString, - ServerReference = connectionValidatorContext.ServerReference - } - }; - } - - public MqttClientSubscribeResult CreateClientSubscribeResult(MqttSubscribePacket subscribePacket, MqttSubAckPacket subAckPacket) - { - if (subscribePacket == null) throw new ArgumentNullException(nameof(subscribePacket)); - if (subAckPacket == null) throw new ArgumentNullException(nameof(subAckPacket)); - - if (subAckPacket.ReasonCodes.Count != subscribePacket.TopicFilters.Count) - { - throw new MqttProtocolViolationException("The reason codes are not matching the topic filters [MQTT-3.9.3-1]."); - } - - var result = new MqttClientSubscribeResult(); - - result.Items.AddRange(subscribePacket.TopicFilters.Select((t, i) => - new MqttClientSubscribeResultItem(t, (MqttClientSubscribeResultCode)subAckPacket.ReasonCodes[i]))); - - return result; - } - - public MqttClientUnsubscribeResult CreateClientUnsubscribeResult(MqttUnsubscribePacket unsubscribePacket, MqttUnsubAckPacket unsubAckPacket) - { - if (unsubscribePacket == null) throw new ArgumentNullException(nameof(unsubscribePacket)); - if (unsubAckPacket == null) throw new ArgumentNullException(nameof(unsubAckPacket)); - - if (unsubAckPacket.ReasonCodes.Count != unsubscribePacket.TopicFilters.Count) - { - throw new MqttProtocolViolationException("The return codes are not matching the topic filters [MQTT-3.9.3-1]."); - } - - var result = new MqttClientUnsubscribeResult(); - - result.Items.AddRange(unsubscribePacket.TopicFilters.Select((t, i) => - new MqttClientUnsubscribeResultItem(t, (MqttClientUnsubscribeResultCode)unsubAckPacket.ReasonCodes[i]))); - - return result; - } - - public MqttSubscribePacket CreateSubscribePacket(MqttClientSubscribeOptions options) - { - if (options == null) throw new ArgumentNullException(nameof(options)); - - var packet = new MqttSubscribePacket - { - Properties = new MqttSubscribePacketProperties() - }; - - packet.TopicFilters.AddRange(options.TopicFilters); - packet.Properties.SubscriptionIdentifier = options.SubscriptionIdentifier; - packet.Properties.UserProperties = options.UserProperties; - - return packet; - } - - public MqttSubAckPacket CreateSubAckPacket(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 - }; - - subackPacket.ReasonCodes.AddRange(subscribeResult.ReasonCodes); - - return subackPacket; - } - - public MqttUnsubscribePacket CreateUnsubscribePacket(MqttClientUnsubscribeOptions options) - { - if (options == null) throw new ArgumentNullException(nameof(options)); - - var packet = new MqttUnsubscribePacket - { - Properties = new MqttUnsubscribePacketProperties() - }; - - packet.TopicFilters.AddRange(options.TopicFilters); - packet.Properties.UserProperties = options.UserProperties; - - return packet; - } - - public MqttUnsubAckPacket CreateUnsubAckPacket(MqttUnsubscribePacket unsubscribePacket, List reasonCodes) - { - if (unsubscribePacket == null) throw new ArgumentNullException(nameof(unsubscribePacket)); - if (reasonCodes == null) throw new ArgumentNullException(nameof(reasonCodes)); - - return new MqttUnsubAckPacket - { - PacketIdentifier = unsubscribePacket.PacketIdentifier, - ReasonCodes = reasonCodes - }; - } - - public MqttDisconnectPacket CreateDisconnectPacket(MqttClientDisconnectOptions options) - { - var packet = new MqttDisconnectPacket(); - - if (options == null) - { - packet.ReasonCode = MqttDisconnectReasonCode.NormalDisconnection; - } - else - { - packet.ReasonCode = (MqttDisconnectReasonCode)options.ReasonCode; - } - - return packet; - } - - public MqttClientPublishResult CreateClientPublishResult(MqttPubAckPacket pubAckPacket) - { - var result = new MqttClientPublishResult - { - ReasonCode = MqttClientPublishReasonCode.Success, - ReasonString = pubAckPacket?.Properties?.ReasonString, - UserProperties = pubAckPacket?.Properties?.UserProperties - }; - - if (pubAckPacket != null) - { - // QoS 0 has no response. So we treat it as a success always. - // Both enums have the same values. So it can be easily converted. - result.ReasonCode = (MqttClientPublishReasonCode)(int)(pubAckPacket.ReasonCode ?? 0); - - result.PacketIdentifier = pubAckPacket.PacketIdentifier; - } - - return result; - } - - public MqttClientPublishResult CreateClientPublishResult(MqttPubRecPacket pubRecPacket, MqttPubCompPacket pubCompPacket) - { - if (pubRecPacket == null || pubCompPacket == null) - { - return new MqttClientPublishResult - { - ReasonCode = MqttClientPublishReasonCode.UnspecifiedError - }; - } - - // The PUBCOMP is the last packet in QoS 2. So we use the results from that instead of PUBREC. - if (pubCompPacket.ReasonCode == MqttPubCompReasonCode.PacketIdentifierNotFound) - { - return new MqttClientPublishResult - { - PacketIdentifier = pubCompPacket.PacketIdentifier, - ReasonCode = MqttClientPublishReasonCode.UnspecifiedError, - ReasonString = pubCompPacket.Properties?.ReasonString, - UserProperties = pubCompPacket.Properties?.UserProperties - }; - } - - var result = new MqttClientPublishResult - { - PacketIdentifier = pubCompPacket.PacketIdentifier, - ReasonCode = MqttClientPublishReasonCode.Success, - ReasonString = pubCompPacket.Properties?.ReasonString, - UserProperties = pubCompPacket.Properties?.UserProperties - }; - - if (pubRecPacket.ReasonCode.HasValue) - { - // Both enums share the same values. - result.ReasonCode = (MqttClientPublishReasonCode)(pubRecPacket.ReasonCode ?? 0); - } - - return result; - } - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/V5/MqttV500PacketDecoder.cs b/Source/MQTTnet/Formatter/V5/MqttV500PacketDecoder.cs deleted file mode 100644 index 6fc6365..0000000 --- a/Source/MQTTnet/Formatter/V5/MqttV500PacketDecoder.cs +++ /dev/null @@ -1,901 +0,0 @@ -using System; -using System.Collections.Generic; -using MQTTnet.Adapter; -using MQTTnet.Exceptions; -using MQTTnet.Packets; -using MQTTnet.Protocol; - -namespace MQTTnet.Formatter.V5 -{ - public class MqttV500PacketDecoder - { - static readonly MqttPingReqPacket PingReqPacket = new MqttPingReqPacket(); - - static readonly MqttPingRespPacket PingRespPacket = new MqttPingRespPacket(); - - public MqttBasePacket Decode(ReceivedMqttPacket receivedMqttPacket) - { - if (receivedMqttPacket == null) throw new ArgumentNullException(nameof(receivedMqttPacket)); - - var controlPacketType = receivedMqttPacket.FixedHeader >> 4; - if (controlPacketType < 1 || controlPacketType > 15) - { - throw new MqttProtocolViolationException($"The packet type is invalid ({controlPacketType})."); - } - - switch ((MqttControlPacketType)controlPacketType) - { - case MqttControlPacketType.Connect: return DecodeConnectPacket(receivedMqttPacket.BodyReader); - case MqttControlPacketType.ConnAck: return DecodeConnAckPacket(receivedMqttPacket.BodyReader); - case MqttControlPacketType.Disconnect: return DecodeDisconnectPacket(receivedMqttPacket.BodyReader); - case MqttControlPacketType.Publish: return DecodePublishPacket(receivedMqttPacket.FixedHeader, receivedMqttPacket.BodyReader); - case MqttControlPacketType.PubAck: return DecodePubAckPacket(receivedMqttPacket.BodyReader); - case MqttControlPacketType.PubRec: return DecodePubRecPacket(receivedMqttPacket.BodyReader); - case MqttControlPacketType.PubRel: return DecodePubRelPacket(receivedMqttPacket.BodyReader); - case MqttControlPacketType.PubComp: return DecodePubCompPacket(receivedMqttPacket.BodyReader); - case MqttControlPacketType.PingReq: return DecodePingReqPacket(); - case MqttControlPacketType.PingResp: return DecodePingRespPacket(); - case MqttControlPacketType.Subscribe: return DecodeSubscribePacket(receivedMqttPacket.BodyReader); - case MqttControlPacketType.SubAck: return DecodeSubAckPacket(receivedMqttPacket.BodyReader); - case MqttControlPacketType.Unsubscibe: return DecodeUnsubscribePacket(receivedMqttPacket.BodyReader); - case MqttControlPacketType.UnsubAck: return DecodeUnsubAckPacket(receivedMqttPacket.BodyReader); - case MqttControlPacketType.Auth: return DecodeAuthPacket(receivedMqttPacket.BodyReader); - - default: throw new MqttProtocolViolationException($"Packet type ({controlPacketType}) not supported."); - } - } - - static MqttBasePacket DecodeConnectPacket(IMqttPacketBodyReader body) - { - ThrowIfBodyIsEmpty(body); - - var packet = new MqttConnectPacket(); - - var protocolName = body.ReadStringWithLengthPrefix(); - var protocolVersion = body.ReadByte(); - - if (protocolName != "MQTT" && protocolVersion != 5) - { - throw new MqttProtocolViolationException("MQTT protocol name and version do not match MQTT v5."); - } - - var connectFlags = body.ReadByte(); - - var cleanSessionFlag = (connectFlags & 0x02) > 0; - var willMessageFlag = (connectFlags & 0x04) > 0; - var willMessageQoS = (byte)(connectFlags >> 3 & 3); - var willMessageRetainFlag = (connectFlags & 0x20) > 0; - var passwordFlag = (connectFlags & 0x40) > 0; - var usernameFlag = (connectFlags & 0x80) > 0; - - packet.CleanSession = cleanSessionFlag; - - if (willMessageFlag) - { - packet.WillMessage = new MqttApplicationMessage - { - QualityOfServiceLevel = (MqttQualityOfServiceLevel)willMessageQoS, - Retain = willMessageRetainFlag - }; - } - - packet.KeepAlivePeriod = body.ReadTwoByteInteger(); - - var propertiesReader = new MqttV500PropertiesReader(body); - while (propertiesReader.MoveNext()) - { - if (packet.Properties == null) - { - packet.Properties = new MqttConnectPacketProperties(); - } - - if (propertiesReader.CurrentPropertyId == MqttPropertyId.SessionExpiryInterval) - { - packet.Properties.SessionExpiryInterval = propertiesReader.ReadSessionExpiryInterval(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationMethod) - { - packet.Properties.AuthenticationMethod = propertiesReader.ReadAuthenticationMethod(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationData) - { - packet.Properties.AuthenticationData = propertiesReader.ReadAuthenticationData(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReceiveMaximum) - { - packet.Properties.ReceiveMaximum = propertiesReader.ReadReceiveMaximum(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.TopicAliasMaximum) - { - packet.Properties.TopicAliasMaximum = propertiesReader.ReadTopicAliasMaximum(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.MaximumPacketSize) - { - packet.Properties.MaximumPacketSize = propertiesReader.ReadMaximumPacketSize(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.RequestResponseInformation) - { - packet.Properties.RequestResponseInformation = propertiesReader.RequestResponseInformation(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.RequestProblemInformation) - { - packet.Properties.RequestProblemInformation = propertiesReader.RequestProblemInformation(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) - { - if (packet.Properties.UserProperties == null) - { - packet.Properties.UserProperties = new List(); - } - - propertiesReader.AddUserPropertyTo(packet.Properties.UserProperties); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttConnectPacket)); - } - } - - packet.ClientId = body.ReadStringWithLengthPrefix(); - - if (packet.WillMessage != null) - { - var willPropertiesReader = new MqttV500PropertiesReader(body); - - while (willPropertiesReader.MoveNext()) - { - if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.PayloadFormatIndicator) - { - packet.WillMessage.PayloadFormatIndicator = propertiesReader.ReadPayloadFormatIndicator(); - } - else if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.MessageExpiryInterval) - { - packet.WillMessage.MessageExpiryInterval = propertiesReader.ReadMessageExpiryInterval(); - } - else if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.TopicAlias) - { - packet.WillMessage.TopicAlias = propertiesReader.ReadTopicAlias(); - } - else if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.ResponseTopic) - { - packet.WillMessage.ResponseTopic = propertiesReader.ReadResponseTopic(); - } - else if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.CorrelationData) - { - packet.WillMessage.CorrelationData = propertiesReader.ReadCorrelationData(); - } - else if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.SubscriptionIdentifier) - { - if (packet.WillMessage.SubscriptionIdentifiers == null) - { - packet.WillMessage.SubscriptionIdentifiers = new List(); - } - - packet.WillMessage.SubscriptionIdentifiers.Add(propertiesReader.ReadSubscriptionIdentifier()); - } - else if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.ContentType) - { - packet.WillMessage.ContentType = propertiesReader.ReadContentType(); - } - else if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.WillDelayInterval) - { - // This is a special case! - packet.Properties.WillDelayInterval = propertiesReader.ReadWillDelayInterval(); - } - else if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) - { - if (packet.WillMessage.UserProperties == null) - { - packet.WillMessage.UserProperties = new List(); - } - - propertiesReader.AddUserPropertyTo(packet.Properties.UserProperties); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPublishPacket)); - } - } - - packet.WillMessage.Topic = body.ReadStringWithLengthPrefix(); - packet.WillMessage.Payload = body.ReadWithLengthPrefix(); - } - - if (usernameFlag) - { - packet.Username = body.ReadStringWithLengthPrefix(); - } - - if (passwordFlag) - { - packet.Password = body.ReadWithLengthPrefix(); - } - - return packet; - } - - static MqttBasePacket DecodeConnAckPacket(IMqttPacketBodyReader body) - { - ThrowIfBodyIsEmpty(body); - - var acknowledgeFlags = body.ReadByte(); - - var packet = new MqttConnAckPacket - { - IsSessionPresent = (acknowledgeFlags & 0x1) > 0, - ReasonCode = (MqttConnectReasonCode)body.ReadByte() - }; - - // Set all default values according to specification. When they are missing the often - // indicate that a feature is available. - packet.Properties.RetainAvailable = true; - packet.Properties.SharedSubscriptionAvailable = true; - packet.Properties.SubscriptionIdentifiersAvailable = true; - packet.Properties.WildcardSubscriptionAvailable = true; - - var propertiesReader = new MqttV500PropertiesReader(body); - while (propertiesReader.MoveNext()) - { - if (propertiesReader.CurrentPropertyId == MqttPropertyId.SessionExpiryInterval) - { - packet.Properties.SessionExpiryInterval = propertiesReader.ReadSessionExpiryInterval(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationMethod) - { - packet.Properties.AuthenticationMethod = propertiesReader.ReadAuthenticationMethod(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationData) - { - packet.Properties.AuthenticationData = propertiesReader.ReadAuthenticationData(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.RetainAvailable) - { - packet.Properties.RetainAvailable = propertiesReader.ReadRetainAvailable(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReceiveMaximum) - { - packet.Properties.ReceiveMaximum = propertiesReader.ReadReceiveMaximum(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.MaximumQoS) - { - packet.Properties.MaximumQoS = propertiesReader.ReadMaximumQoS(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AssignedClientIdentifier) - { - packet.Properties.AssignedClientIdentifier = propertiesReader.ReadAssignedClientIdentifier(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.TopicAliasMaximum) - { - packet.Properties.TopicAliasMaximum = propertiesReader.ReadTopicAliasMaximum(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) - { - packet.Properties.ReasonString = propertiesReader.ReadReasonString(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.MaximumPacketSize) - { - packet.Properties.MaximumPacketSize = propertiesReader.ReadMaximumPacketSize(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.WildcardSubscriptionAvailable) - { - packet.Properties.WildcardSubscriptionAvailable = propertiesReader.ReadWildcardSubscriptionAvailable(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.SubscriptionIdentifiersAvailable) - { - packet.Properties.SubscriptionIdentifiersAvailable = propertiesReader.ReadSubscriptionIdentifiersAvailable(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.SharedSubscriptionAvailable) - { - packet.Properties.SharedSubscriptionAvailable = propertiesReader.ReadSharedSubscriptionAvailable(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ServerKeepAlive) - { - packet.Properties.ServerKeepAlive = propertiesReader.ReadServerKeepAlive(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ResponseInformation) - { - packet.Properties.ResponseInformation = propertiesReader.ReadResponseInformation(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ServerReference) - { - packet.Properties.ServerReference = propertiesReader.ReadServerReference(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) - { - if (packet.Properties.UserProperties == null) - { - packet.Properties.UserProperties = new List(); - } - - propertiesReader.AddUserPropertyTo(packet.Properties.UserProperties); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttConnAckPacket)); - } - } - - return packet; - } - - static MqttBasePacket DecodeDisconnectPacket(IMqttPacketBodyReader body) - { - ThrowIfBodyIsEmpty(body); - - var packet = new MqttDisconnectPacket - { - ReasonCode = (MqttDisconnectReasonCode)body.ReadByte() - }; - - var propertiesReader = new MqttV500PropertiesReader(body); - while (propertiesReader.MoveNext()) - { - if (packet.Properties == null) - { - packet.Properties = new MqttDisconnectPacketProperties(); - } - - if (propertiesReader.CurrentPropertyId == MqttPropertyId.SessionExpiryInterval) - { - packet.Properties.SessionExpiryInterval = propertiesReader.ReadSessionExpiryInterval(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) - { - packet.Properties.ReasonString = propertiesReader.ReadReasonString(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ServerReference) - { - packet.Properties.ServerReference = propertiesReader.ReadServerReference(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) - { - if (packet.Properties.UserProperties == null) - { - packet.Properties.UserProperties = new List(); - } - - propertiesReader.AddUserPropertyTo(packet.Properties.UserProperties); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttDisconnectPacket)); - } - } - - return packet; - } - - static MqttBasePacket DecodeSubscribePacket(IMqttPacketBodyReader body) - { - ThrowIfBodyIsEmpty(body); - - var packet = new MqttSubscribePacket - { - PacketIdentifier = body.ReadTwoByteInteger() - }; - - var propertiesReader = new MqttV500PropertiesReader(body); - while (propertiesReader.MoveNext()) - { - if (packet.Properties == null) - { - packet.Properties = new MqttSubscribePacketProperties(); - } - - if (propertiesReader.CurrentPropertyId == MqttPropertyId.SubscriptionIdentifier) - { - packet.Properties.SubscriptionIdentifier = propertiesReader.ReadSubscriptionIdentifier(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) - { - if (packet.Properties.UserProperties == null) - { - packet.Properties.UserProperties = new List(); - } - - propertiesReader.AddUserPropertyTo(packet.Properties.UserProperties); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttSubscribePacket)); - } - } - - while (!body.EndOfStream) - { - var topic = body.ReadStringWithLengthPrefix(); - var options = body.ReadByte(); - - var qos = (MqttQualityOfServiceLevel)(options & 3); - var noLocal = (options & (1 << 2)) > 0; - var retainAsPublished = (options & (1 << 3)) > 0; - var retainHandling = (MqttRetainHandling)((options >> 4) & 3); - - packet.TopicFilters.Add(new MqttTopicFilter - { - Topic = topic, - QualityOfServiceLevel = qos, - NoLocal = noLocal, - RetainAsPublished = retainAsPublished, - RetainHandling = retainHandling - }); - } - - return packet; - } - - static MqttBasePacket DecodeSubAckPacket(IMqttPacketBodyReader body) - { - ThrowIfBodyIsEmpty(body); - - var packet = new MqttSubAckPacket - { - PacketIdentifier = body.ReadTwoByteInteger() - }; - - var propertiesReader = new MqttV500PropertiesReader(body); - while (propertiesReader.MoveNext()) - { - if (packet.Properties == null) - { - packet.Properties = new MqttSubAckPacketProperties(); - } - - if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) - { - packet.Properties.ReasonString = propertiesReader.ReadReasonString(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) - { - if (packet.Properties.UserProperties == null) - { - packet.Properties.UserProperties = new List(); - } - - propertiesReader.AddUserPropertyTo(packet.Properties.UserProperties); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttSubAckPacket)); - } - } - - while (!body.EndOfStream) - { - var reasonCode = (MqttSubscribeReasonCode)body.ReadByte(); - packet.ReasonCodes.Add(reasonCode); - } - - return packet; - } - - static MqttBasePacket DecodeUnsubscribePacket(IMqttPacketBodyReader body) - { - ThrowIfBodyIsEmpty(body); - - var packet = new MqttUnsubscribePacket - { - PacketIdentifier = body.ReadTwoByteInteger() - }; - - var propertiesReader = new MqttV500PropertiesReader(body); - while (propertiesReader.MoveNext()) - { - if (packet.Properties == null) - { - packet.Properties = new MqttUnsubscribePacketProperties(); - } - - if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) - { - if (packet.Properties.UserProperties == null) - { - packet.Properties.UserProperties = new List(); - } - - propertiesReader.AddUserPropertyTo(packet.Properties.UserProperties); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttUnsubscribePacket)); - } - } - - while (!body.EndOfStream) - { - packet.TopicFilters.Add(body.ReadStringWithLengthPrefix()); - } - - return packet; - } - - static MqttBasePacket DecodeUnsubAckPacket(IMqttPacketBodyReader body) - { - ThrowIfBodyIsEmpty(body); - - var packet = new MqttUnsubAckPacket - { - PacketIdentifier = body.ReadTwoByteInteger() - }; - - var propertiesReader = new MqttV500PropertiesReader(body); - while (propertiesReader.MoveNext()) - { - if (packet.Properties == null) - { - packet.Properties = new MqttUnsubAckPacketProperties(); - } - - if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) - { - packet.Properties.ReasonString = propertiesReader.ReadReasonString(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) - { - if (packet.Properties.UserProperties == null) - { - packet.Properties.UserProperties = new List(); - } - - propertiesReader.AddUserPropertyTo(packet.Properties.UserProperties); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttUnsubAckPacket)); - } - } - - while (!body.EndOfStream) - { - var reasonCode = (MqttUnsubscribeReasonCode)body.ReadByte(); - packet.ReasonCodes.Add(reasonCode); - } - - return packet; - } - - static MqttBasePacket DecodePingReqPacket() - { - return PingReqPacket; - } - - static MqttBasePacket DecodePingRespPacket() - { - return PingRespPacket; - } - - static MqttBasePacket DecodePublishPacket(byte header, IMqttPacketBodyReader body) - { - ThrowIfBodyIsEmpty(body); - - var retain = (header & 1) > 0; - var qos = (MqttQualityOfServiceLevel)(header >> 1 & 3); - var dup = (header >> 3 & 1) > 0; - - var packet = new MqttPublishPacket - { - Topic = body.ReadStringWithLengthPrefix(), - Retain = retain, - QualityOfServiceLevel = qos, - Dup = dup - }; - - if (qos > 0) - { - packet.PacketIdentifier = body.ReadTwoByteInteger(); - } - - var propertiesReader = new MqttV500PropertiesReader(body); - while (propertiesReader.MoveNext()) - { - if (packet.Properties == null) - { - packet.Properties = new MqttPublishPacketProperties(); - } - - if (propertiesReader.CurrentPropertyId == MqttPropertyId.PayloadFormatIndicator) - { - packet.Properties.PayloadFormatIndicator = propertiesReader.ReadPayloadFormatIndicator(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.MessageExpiryInterval) - { - packet.Properties.MessageExpiryInterval = propertiesReader.ReadMessageExpiryInterval(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.TopicAlias) - { - packet.Properties.TopicAlias = propertiesReader.ReadTopicAlias(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ResponseTopic) - { - packet.Properties.ResponseTopic = propertiesReader.ReadResponseTopic(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.CorrelationData) - { - packet.Properties.CorrelationData = propertiesReader.ReadCorrelationData(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.SubscriptionIdentifier) - { - if (packet.Properties.SubscriptionIdentifiers == null) - { - packet.Properties.SubscriptionIdentifiers = new List(); - } - - packet.Properties.SubscriptionIdentifiers.Add(propertiesReader.ReadSubscriptionIdentifier()); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ContentType) - { - packet.Properties.ContentType = propertiesReader.ReadContentType(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) - { - if (packet.Properties.UserProperties == null) - { - packet.Properties.UserProperties = new List(); - } - - propertiesReader.AddUserPropertyTo(packet.Properties.UserProperties); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPublishPacket)); - } - } - - if (!body.EndOfStream) - { - packet.Payload = body.ReadRemainingData(); - } - - return packet; - } - - static MqttBasePacket DecodePubAckPacket(IMqttPacketBodyReader body) - { - ThrowIfBodyIsEmpty(body); - - var packet = new MqttPubAckPacket - { - PacketIdentifier = body.ReadTwoByteInteger() - }; - - if (body.EndOfStream) - { - packet.ReasonCode = MqttPubAckReasonCode.Success; - return packet; - } - - packet.ReasonCode = (MqttPubAckReasonCode)body.ReadByte(); - - var propertiesReader = new MqttV500PropertiesReader(body); - while (propertiesReader.MoveNext()) - { - if (packet.Properties == null) - { - packet.Properties = new MqttPubAckPacketProperties(); - } - - if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) - { - packet.Properties.ReasonString = propertiesReader.ReadReasonString(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) - { - if (packet.Properties.UserProperties == null) - { - packet.Properties.UserProperties = new List(); - } - - propertiesReader.AddUserPropertyTo(packet.Properties.UserProperties); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPubAckPacket)); - } - } - - return packet; - } - - static MqttBasePacket DecodePubRecPacket(IMqttPacketBodyReader body) - { - ThrowIfBodyIsEmpty(body); - - var packet = new MqttPubRecPacket - { - PacketIdentifier = body.ReadTwoByteInteger() - }; - - if (body.EndOfStream) - { - packet.ReasonCode = MqttPubRecReasonCode.Success; - return packet; - } - - packet.ReasonCode = (MqttPubRecReasonCode)body.ReadByte(); - - var propertiesReader = new MqttV500PropertiesReader(body); - while (propertiesReader.MoveNext()) - { - if (packet.Properties == null) - { - packet.Properties = new MqttPubRecPacketProperties(); - } - - if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) - { - packet.Properties.ReasonString = propertiesReader.ReadReasonString(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) - { - if (packet.Properties.UserProperties == null) - { - packet.Properties.UserProperties = new List(); - } - - propertiesReader.AddUserPropertyTo(packet.Properties.UserProperties); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPubRecPacket)); - } - } - - return packet; - } - - static MqttBasePacket DecodePubRelPacket(IMqttPacketBodyReader body) - { - ThrowIfBodyIsEmpty(body); - - var packet = new MqttPubRelPacket - { - PacketIdentifier = body.ReadTwoByteInteger() - }; - - if (body.EndOfStream) - { - packet.ReasonCode = MqttPubRelReasonCode.Success; - return packet; - } - - packet.ReasonCode = (MqttPubRelReasonCode)body.ReadByte(); - - var propertiesReader = new MqttV500PropertiesReader(body); - while (propertiesReader.MoveNext()) - { - if (packet.Properties == null) - { - packet.Properties = new MqttPubRelPacketProperties(); - } - - if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) - { - packet.Properties.ReasonString = propertiesReader.ReadReasonString(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) - { - if (packet.Properties.UserProperties == null) - { - packet.Properties.UserProperties = new List(); - } - - propertiesReader.AddUserPropertyTo(packet.Properties.UserProperties); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPubRelPacket)); - } - } - - return packet; - } - - static MqttBasePacket DecodePubCompPacket(IMqttPacketBodyReader body) - { - ThrowIfBodyIsEmpty(body); - - var packet = new MqttPubCompPacket - { - PacketIdentifier = body.ReadTwoByteInteger() - }; - - if (body.EndOfStream) - { - packet.ReasonCode = MqttPubCompReasonCode.Success; - return packet; - } - - packet.ReasonCode = (MqttPubCompReasonCode)body.ReadByte(); - - var propertiesReader = new MqttV500PropertiesReader(body); - while (propertiesReader.MoveNext()) - { - if (packet.Properties == null) - { - packet.Properties = new MqttPubCompPacketProperties(); - } - - if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) - { - packet.Properties.ReasonString = propertiesReader.ReadReasonString(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) - { - if (packet.Properties.UserProperties == null) - { - packet.Properties.UserProperties = new List(); - } - - propertiesReader.AddUserPropertyTo(packet.Properties.UserProperties); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPubCompPacket)); - } - } - - return packet; - } - - static MqttBasePacket DecodeAuthPacket(IMqttPacketBodyReader body) - { - ThrowIfBodyIsEmpty(body); - - var packet = new MqttAuthPacket(); - - if (body.EndOfStream) - { - packet.ReasonCode = MqttAuthenticateReasonCode.Success; - return packet; - } - - packet.ReasonCode = (MqttAuthenticateReasonCode)body.ReadByte(); - - var propertiesReader = new MqttV500PropertiesReader(body); - while (propertiesReader.MoveNext()) - { - if (packet.Properties == null) - { - packet.Properties = new MqttAuthPacketProperties(); - } - - if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationMethod) - { - packet.Properties.AuthenticationMethod = propertiesReader.ReadAuthenticationMethod(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationData) - { - packet.Properties.AuthenticationData = propertiesReader.ReadAuthenticationData(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) - { - packet.Properties.ReasonString = propertiesReader.ReadReasonString(); - } - else if (propertiesReader.CurrentPropertyId == MqttPropertyId.UserProperty) - { - if (packet.Properties.UserProperties == null) - { - packet.Properties.UserProperties = new List(); - } - - propertiesReader.AddUserPropertyTo(packet.Properties.UserProperties); - } - else - { - propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttAuthPacket)); - } - } - - return packet; - } - - // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local - static void ThrowIfBodyIsEmpty(IMqttPacketBodyReader body) - { - if (body == null || body.Length == 0) - { - throw new MqttProtocolViolationException("Data from the body is required but not present."); - } - } - } -} diff --git a/Source/MQTTnet/Formatter/V5/MqttV500PacketEncoder.cs b/Source/MQTTnet/Formatter/V5/MqttV500PacketEncoder.cs deleted file mode 100644 index abfc527..0000000 --- a/Source/MQTTnet/Formatter/V5/MqttV500PacketEncoder.cs +++ /dev/null @@ -1,588 +0,0 @@ -using System; -using System.Linq; -using MQTTnet.Exceptions; -using MQTTnet.Packets; -using MQTTnet.Protocol; - -namespace MQTTnet.Formatter.V5 -{ - public sealed class MqttV500PacketEncoder - { - readonly IMqttPacketWriter _packetWriter; - - public MqttV500PacketEncoder(IMqttPacketWriter packetWriter) - { - _packetWriter = packetWriter ?? throw new ArgumentNullException(nameof(packetWriter)); - } - - public ArraySegment Encode(MqttBasePacket packet) - { - if (packet == null) throw new ArgumentNullException(nameof(packet)); - - // Leave enough head space for max header size (fixed + 4 variable remaining length = 5 bytes) - _packetWriter.Reset(5); - _packetWriter.Seek(5); - - var fixedHeader = EncodePacket(packet, _packetWriter); - var remainingLength = (uint)(_packetWriter.Length - 5); - - var remainingLengthSize = MqttPacketWriter.GetLengthOfVariableInteger(remainingLength); - - var headerSize = 1 + remainingLengthSize; - var headerOffset = 5 - headerSize; - - // Position cursor on correct offset on beginning of array (has leading 0x0) - _packetWriter.Seek(headerOffset); - _packetWriter.Write(fixedHeader); - _packetWriter.WriteVariableLengthInteger(remainingLength); - - var buffer = _packetWriter.GetBuffer(); - return new ArraySegment(buffer, headerOffset, _packetWriter.Length - headerOffset); - } - - public void FreeBuffer() - { - _packetWriter.FreeBuffer(); - } - - static byte EncodePacket(MqttBasePacket packet, IMqttPacketWriter packetWriter) - { - switch (packet) - { - case MqttConnectPacket connectPacket: return EncodeConnectPacket(connectPacket, packetWriter); - case MqttConnAckPacket connAckPacket: return EncodeConnAckPacket(connAckPacket, packetWriter); - case MqttDisconnectPacket disconnectPacket: return EncodeDisconnectPacket(disconnectPacket, packetWriter); - case MqttPingReqPacket _: return EncodePingReqPacket(); - case MqttPingRespPacket _: return EncodePingRespPacket(); - case MqttPublishPacket publishPacket: return EncodePublishPacket(publishPacket, packetWriter); - case MqttPubAckPacket pubAckPacket: return EncodePubAckPacket(pubAckPacket, packetWriter); - case MqttPubRecPacket pubRecPacket: return EncodePubRecPacket(pubRecPacket, packetWriter); - case MqttPubRelPacket pubRelPacket: return EncodePubRelPacket(pubRelPacket, packetWriter); - case MqttPubCompPacket pubCompPacket: return EncodePubCompPacket(pubCompPacket, packetWriter); - case MqttSubscribePacket subscribePacket: return EncodeSubscribePacket(subscribePacket, packetWriter); - case MqttSubAckPacket subAckPacket: return EncodeSubAckPacket(subAckPacket, packetWriter); - case MqttUnsubscribePacket unsubscribePacket: return EncodeUnsubscribePacket(unsubscribePacket, packetWriter); - case MqttUnsubAckPacket unsubAckPacket: return EncodeUnsubAckPacket(unsubAckPacket, packetWriter); - case MqttAuthPacket authPacket: return EncodeAuthPacket(authPacket, packetWriter); - - default: throw new MqttProtocolViolationException("Packet type invalid."); - } - } - - static byte EncodeConnectPacket(MqttConnectPacket packet, IMqttPacketWriter packetWriter) - { - if (packet == null) throw new ArgumentNullException(nameof(packet)); - if (packetWriter == null) throw new ArgumentNullException(nameof(packetWriter)); - - if (string.IsNullOrEmpty(packet.ClientId) && !packet.CleanSession) - { - throw new MqttProtocolViolationException("CleanSession must be set if ClientId is empty [MQTT-3.1.3-7]."); - } - - packetWriter.WriteWithLengthPrefix("MQTT"); - packetWriter.Write(5); // [3.1.2.2 Protocol Version] - - byte connectFlags = 0x0; - if (packet.CleanSession) - { - connectFlags |= 0x2; - } - - if (packet.WillMessage != null) - { - connectFlags |= 0x4; - connectFlags |= (byte)((byte)packet.WillMessage.QualityOfServiceLevel << 3); - - if (packet.WillMessage.Retain) - { - connectFlags |= 0x20; - } - } - - if (packet.Password != null && packet.Username == null) - { - throw new MqttProtocolViolationException("If the User Name Flag is set to 0, the Password Flag MUST be set to 0 [MQTT-3.1.2-22]."); - } - - if (packet.Password != null) - { - connectFlags |= 0x40; - } - - if (packet.Username != null) - { - connectFlags |= 0x80; - } - - packetWriter.Write(connectFlags); - packetWriter.Write(packet.KeepAlivePeriod); - - var propertiesWriter = new MqttV500PropertiesWriter(); - if (packet.Properties != null) - { - propertiesWriter.WriteSessionExpiryInterval(packet.Properties.SessionExpiryInterval); - propertiesWriter.WriteAuthenticationMethod(packet.Properties.AuthenticationMethod); - propertiesWriter.WriteAuthenticationData(packet.Properties.AuthenticationData); - propertiesWriter.WriteRequestProblemInformation(packet.Properties.RequestProblemInformation); - propertiesWriter.WriteRequestResponseInformation(packet.Properties.RequestResponseInformation); - propertiesWriter.WriteReceiveMaximum(packet.Properties.ReceiveMaximum); - propertiesWriter.WriteTopicAliasMaximum(packet.Properties.TopicAliasMaximum); - propertiesWriter.WriteMaximumPacketSize(packet.Properties.MaximumPacketSize); - propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); - } - - propertiesWriter.WriteTo(packetWriter); - - packetWriter.WriteWithLengthPrefix(packet.ClientId); - - if (packet.WillMessage != null) - { - var willPropertiesWriter = new MqttV500PropertiesWriter(); - willPropertiesWriter.WritePayloadFormatIndicator(packet.WillMessage.PayloadFormatIndicator); - willPropertiesWriter.WriteMessageExpiryInterval(packet.WillMessage.MessageExpiryInterval); - willPropertiesWriter.WriteTopicAlias(packet.WillMessage.TopicAlias); - willPropertiesWriter.WriteResponseTopic(packet.WillMessage.ResponseTopic); - willPropertiesWriter.WriteCorrelationData(packet.WillMessage.CorrelationData); - willPropertiesWriter.WriteSubscriptionIdentifiers(packet.WillMessage.SubscriptionIdentifiers); - willPropertiesWriter.WriteContentType(packet.WillMessage.ContentType); - willPropertiesWriter.WriteUserProperties(packet.WillMessage.UserProperties); - - // This is a special case! - willPropertiesWriter.WriteWillDelayInterval(packet.Properties?.WillDelayInterval); - - willPropertiesWriter.WriteTo(packetWriter); - - packetWriter.WriteWithLengthPrefix(packet.WillMessage.Topic); - packetWriter.WriteWithLengthPrefix(packet.WillMessage.Payload); - } - - if (packet.Username != null) - { - packetWriter.WriteWithLengthPrefix(packet.Username); - } - - if (packet.Password != null) - { - packetWriter.WriteWithLengthPrefix(packet.Password); - } - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Connect); - } - - static byte EncodeConnAckPacket(MqttConnAckPacket packet, IMqttPacketWriter packetWriter) - { - if (packet == null) throw new ArgumentNullException(nameof(packet)); - if (packetWriter == null) throw new ArgumentNullException(nameof(packetWriter)); - - byte connectAcknowledgeFlags = 0x0; - if (packet.IsSessionPresent) - { - connectAcknowledgeFlags |= 0x1; - } - - packetWriter.Write(connectAcknowledgeFlags); - packetWriter.Write((byte)packet.ReasonCode); - - var propertiesWriter = new MqttV500PropertiesWriter(); - if (packet.Properties != null) - { - propertiesWriter.WriteSessionExpiryInterval(packet.Properties.SessionExpiryInterval); - propertiesWriter.WriteAuthenticationMethod(packet.Properties.AuthenticationMethod); - propertiesWriter.WriteAuthenticationData(packet.Properties.AuthenticationData); - propertiesWriter.WriteRetainAvailable(packet.Properties.RetainAvailable); - propertiesWriter.WriteReceiveMaximum(packet.Properties.ReceiveMaximum); - propertiesWriter.WriteMaximumQoS(packet.Properties.MaximumQoS); - propertiesWriter.WriteAssignedClientIdentifier(packet.Properties.AssignedClientIdentifier); - propertiesWriter.WriteTopicAliasMaximum(packet.Properties.TopicAliasMaximum); - propertiesWriter.WriteReasonString(packet.Properties.ReasonString); - propertiesWriter.WriteMaximumPacketSize(packet.Properties.MaximumPacketSize); - propertiesWriter.WriteWildcardSubscriptionAvailable(packet.Properties.WildcardSubscriptionAvailable); - propertiesWriter.WriteSubscriptionIdentifiersAvailable(packet.Properties.SubscriptionIdentifiersAvailable); - propertiesWriter.WriteSharedSubscriptionAvailable(packet.Properties.SharedSubscriptionAvailable); - propertiesWriter.WriteServerKeepAlive(packet.Properties.ServerKeepAlive); - propertiesWriter.WriteResponseInformation(packet.Properties.ResponseInformation); - propertiesWriter.WriteServerReference(packet.Properties.ServerReference); - propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); - } - - propertiesWriter.WriteTo(packetWriter); - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.ConnAck); - } - - static byte EncodePublishPacket(MqttPublishPacket packet, IMqttPacketWriter packetWriter) - { - if (packet == null) throw new ArgumentNullException(nameof(packet)); - if (packetWriter == null) throw new ArgumentNullException(nameof(packetWriter)); - - if (packet.QualityOfServiceLevel == 0 && packet.Dup) - { - throw new MqttProtocolViolationException("Dup flag must be false for QoS 0 packets [MQTT-3.3.1-2]."); - } - - packetWriter.WriteWithLengthPrefix(packet.Topic); - - if (packet.QualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) - { - if (packet.PacketIdentifier == 0) - { - throw new MqttProtocolViolationException("Publish packet has no packet identifier."); - } - - packetWriter.Write(packet.PacketIdentifier); - } - else - { - if (packet.PacketIdentifier > 0) - { - throw new MqttProtocolViolationException("Packet identifier must be 0 if QoS == 0 [MQTT-2.3.1-5]."); - } - } - - var propertiesWriter = new MqttV500PropertiesWriter(); - if (packet.Properties != null) - { - propertiesWriter.WriteContentType(packet.Properties.ContentType); - propertiesWriter.WriteCorrelationData(packet.Properties.CorrelationData); - propertiesWriter.WriteMessageExpiryInterval(packet.Properties.MessageExpiryInterval); - propertiesWriter.WritePayloadFormatIndicator(packet.Properties.PayloadFormatIndicator); - propertiesWriter.WriteResponseTopic(packet.Properties.ResponseTopic); - propertiesWriter.WriteSubscriptionIdentifiers(packet.Properties.SubscriptionIdentifiers); - propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); - - if (packet.Properties.TopicAlias > 0) - { - propertiesWriter.WriteTopicAlias(packet.Properties.TopicAlias); - } - } - - propertiesWriter.WriteTo(packetWriter); - - if (packet.Payload?.Length > 0) - { - packetWriter.Write(packet.Payload, 0, packet.Payload.Length); - } - - byte fixedHeader = 0; - - if (packet.Retain) - { - fixedHeader |= 0x01; - } - - fixedHeader |= (byte)((byte)packet.QualityOfServiceLevel << 1); - - if (packet.Dup) - { - fixedHeader |= 0x08; - } - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Publish, fixedHeader); - } - - static byte EncodePubAckPacket(MqttPubAckPacket packet, IMqttPacketWriter packetWriter) - { - if (packet == null) throw new ArgumentNullException(nameof(packet)); - if (packetWriter == null) throw new ArgumentNullException(nameof(packetWriter)); - - if (packet.PacketIdentifier == 0) - { - throw new MqttProtocolViolationException("PubAck packet has no packet identifier."); - } - - if (!packet.ReasonCode.HasValue) - { - throw new MqttProtocolViolationException("PubAck packet must contain a reason code."); - } - - packetWriter.Write(packet.PacketIdentifier); - - var propertiesWriter = new MqttV500PropertiesWriter(); - if (packet.Properties != null) - { - propertiesWriter.WriteReasonString(packet.Properties.ReasonString); - propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); - } - - if (packetWriter.Length > 0 || packet.ReasonCode.Value != MqttPubAckReasonCode.Success) - { - packetWriter.Write((byte)packet.ReasonCode.Value); - propertiesWriter.WriteTo(packetWriter); - } - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PubAck); - } - - static byte EncodePubRecPacket(MqttPubRecPacket packet, IMqttPacketWriter packetWriter) - { - ThrowIfPacketIdentifierIsInvalid(packet.PacketIdentifier, packet); - - if (!packet.ReasonCode.HasValue) - { - ThrowReasonCodeNotSetException(); - } - - var propertiesWriter = new MqttV500PropertiesWriter(); - if (packet.Properties != null) - { - propertiesWriter.WriteReasonString(packet.Properties.ReasonString); - propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); - } - - packetWriter.Write(packet.PacketIdentifier); - - if (packetWriter.Length > 0 || packet.ReasonCode.Value != MqttPubRecReasonCode.Success) - { - packetWriter.Write((byte)packet.ReasonCode.Value); - propertiesWriter.WriteTo(packetWriter); - } - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PubRec); - } - - static byte EncodePubRelPacket(MqttPubRelPacket packet, IMqttPacketWriter packetWriter) - { - ThrowIfPacketIdentifierIsInvalid(packet.PacketIdentifier, packet); - - if (!packet.ReasonCode.HasValue) - { - ThrowReasonCodeNotSetException(); - } - - var propertiesWriter = new MqttV500PropertiesWriter(); - if (packet.Properties != null) - { - propertiesWriter.WriteReasonString(packet.Properties.ReasonString); - propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); - } - - packetWriter.Write(packet.PacketIdentifier); - - if (propertiesWriter.Length > 0 || packet.ReasonCode.Value != MqttPubRelReasonCode.Success) - { - packetWriter.Write((byte)packet.ReasonCode.Value); - propertiesWriter.WriteTo(packetWriter); - } - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PubRel, 0x02); - } - - static byte EncodePubCompPacket(MqttPubCompPacket packet, IMqttPacketWriter packetWriter) - { - ThrowIfPacketIdentifierIsInvalid(packet.PacketIdentifier, packet); - - if (!packet.ReasonCode.HasValue) - { - ThrowReasonCodeNotSetException(); - } - - packetWriter.Write(packet.PacketIdentifier); - - var propertiesWriter = new MqttV500PropertiesWriter(); - if (packet.Properties != null) - { - propertiesWriter.WriteReasonString(packet.Properties.ReasonString); - propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); - } - - if (propertiesWriter.Length > 0 || packet.ReasonCode.Value != MqttPubCompReasonCode.Success) - { - packetWriter.Write((byte)packet.ReasonCode.Value); - propertiesWriter.WriteTo(packetWriter); - } - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PubComp); - } - - static byte EncodeSubscribePacket(MqttSubscribePacket packet, IMqttPacketWriter packetWriter) - { - if (packet.TopicFilters?.Any() != true) throw new MqttProtocolViolationException("At least one topic filter must be set [MQTT-3.8.3-3]."); - - ThrowIfPacketIdentifierIsInvalid(packet.PacketIdentifier, packet); - - packetWriter.Write(packet.PacketIdentifier); - - var propertiesWriter = new MqttV500PropertiesWriter(); - if (packet.Properties != null) - { - if (packet.Properties.SubscriptionIdentifier > 0) - { - propertiesWriter.WriteSubscriptionIdentifier(packet.Properties.SubscriptionIdentifier); - } - - propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); - } - - propertiesWriter.WriteTo(packetWriter); - - if (packet.TopicFilters?.Count > 0) - { - foreach (var topicFilter in packet.TopicFilters) - { - packetWriter.WriteWithLengthPrefix(topicFilter.Topic); - - var options = (byte)topicFilter.QualityOfServiceLevel; - - if (topicFilter.NoLocal) - { - options |= 1 << 2; - } - - if (topicFilter.RetainAsPublished) - { - options |= 1 << 3; - } - - if (topicFilter.RetainHandling != MqttRetainHandling.SendAtSubscribe) - { - options |= (byte)((byte)topicFilter.RetainHandling << 4); - } - - packetWriter.Write(options); - } - } - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Subscribe, 0x02); - } - - static byte EncodeSubAckPacket(MqttSubAckPacket packet, IMqttPacketWriter packetWriter) - { - if (packet.ReasonCodes?.Any() != true) throw new MqttProtocolViolationException("At least one reason code must be set[MQTT - 3.8.3 - 3]."); - - ThrowIfPacketIdentifierIsInvalid(packet.PacketIdentifier, packet); - - packetWriter.Write(packet.PacketIdentifier); - - var propertiesWriter = new MqttV500PropertiesWriter(); - if (packet.Properties != null) - { - propertiesWriter.WriteReasonString(packet.Properties.ReasonString); - propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); - } - - propertiesWriter.WriteTo(packetWriter); - - foreach (var reasonCode in packet.ReasonCodes) - { - packetWriter.Write((byte)reasonCode); - } - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.SubAck); - } - - static byte EncodeUnsubscribePacket(MqttUnsubscribePacket packet, IMqttPacketWriter packetWriter) - { - if (packet.TopicFilters?.Any() != true) throw new MqttProtocolViolationException("At least one topic filter must be set [MQTT-3.10.3-2]."); - - ThrowIfPacketIdentifierIsInvalid(packet.PacketIdentifier, packet); - - packetWriter.Write(packet.PacketIdentifier); - - var propertiesWriter = new MqttV500PropertiesWriter(); - if (packet.Properties != null) - { - propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); - } - - propertiesWriter.WriteTo(packetWriter); - - foreach (var topicFilter in packet.TopicFilters) - { - packetWriter.WriteWithLengthPrefix(topicFilter); - } - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Unsubscibe, 0x02); - } - - static byte EncodeUnsubAckPacket(MqttUnsubAckPacket packet, IMqttPacketWriter packetWriter) - { - if (packet.ReasonCodes?.Any() != true) throw new MqttProtocolViolationException("At least one reason code must be set[MQTT - 3.8.3 - 3]."); - - ThrowIfPacketIdentifierIsInvalid(packet.PacketIdentifier, packet); - - packetWriter.Write(packet.PacketIdentifier); - - var propertiesWriter = new MqttV500PropertiesWriter(); - if (packet.Properties != null) - { - propertiesWriter.WriteReasonString(packet.Properties.ReasonString); - propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); - } - - propertiesWriter.WriteTo(packetWriter); - - foreach (var reasonCode in packet.ReasonCodes) - { - packetWriter.Write((byte)reasonCode); - } - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.UnsubAck); - } - - static byte EncodeDisconnectPacket(MqttDisconnectPacket packet, IMqttPacketWriter packetWriter) - { - if (!packet.ReasonCode.HasValue) - { - ThrowReasonCodeNotSetException(); - } - - packetWriter.Write((byte)packet.ReasonCode.Value); - - var propertiesWriter = new MqttV500PropertiesWriter(); - if (packet.Properties != null) - { - propertiesWriter.WriteServerReference(packet.Properties.ServerReference); - propertiesWriter.WriteReasonString(packet.Properties.ReasonString); - propertiesWriter.WriteSessionExpiryInterval(packet.Properties.SessionExpiryInterval); - propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); - } - - propertiesWriter.WriteTo(packetWriter); - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Disconnect); - } - - static byte EncodePingReqPacket() - { - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PingReq); - } - - static byte EncodePingRespPacket() - { - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.PingResp); - } - - static byte EncodeAuthPacket(MqttAuthPacket packet, IMqttPacketWriter packetWriter) - { - packetWriter.Write((byte)packet.ReasonCode); - - var propertiesWriter = new MqttV500PropertiesWriter(); - if (packet.Properties != null) - { - propertiesWriter.WriteAuthenticationMethod(packet.Properties.AuthenticationMethod); - propertiesWriter.WriteAuthenticationData(packet.Properties.AuthenticationData); - propertiesWriter.WriteReasonString(packet.Properties.ReasonString); - propertiesWriter.WriteUserProperties(packet.Properties.UserProperties); - } - - propertiesWriter.WriteTo(packetWriter); - - return MqttPacketWriter.BuildFixedHeader(MqttControlPacketType.Auth); - } - - static void ThrowReasonCodeNotSetException() - { - throw new MqttProtocolViolationException("The ReasonCode must be set for MQTT version 5."); - } - - static void ThrowIfPacketIdentifierIsInvalid(ushort packetIdentifier, MqttBasePacket packet) - { - // SUBSCRIBE, UNSUBSCRIBE, and PUBLISH(in cases where QoS > 0) Control Packets MUST contain a non-zero 16 - bit Packet Identifier[MQTT - 2.3.1 - 1]. - - if (packetIdentifier == 0) - { - throw new MqttProtocolViolationException($"Packet identifier is not set for {packet.GetType().Name}."); - } - } - } -} diff --git a/Source/MQTTnet/Formatter/V5/MqttV500PacketFormatter.cs b/Source/MQTTnet/Formatter/V5/MqttV500PacketFormatter.cs deleted file mode 100644 index a34aa53..0000000 --- a/Source/MQTTnet/Formatter/V5/MqttV500PacketFormatter.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using MQTTnet.Adapter; -using MQTTnet.Packets; - -namespace MQTTnet.Formatter.V5 -{ - public sealed class MqttV500PacketFormatter : IMqttPacketFormatter - { - readonly MqttV500PacketDecoder _decoder = new MqttV500PacketDecoder(); - readonly MqttV500PacketEncoder _encoder; - - public MqttV500PacketFormatter(IMqttPacketWriter writer) - { - _encoder = new MqttV500PacketEncoder(writer); - } - - public IMqttDataConverter DataConverter { get; } = new MqttV500DataConverter(); - - public ArraySegment Encode(MqttBasePacket mqttPacket) - { - if (mqttPacket == null) throw new ArgumentNullException(nameof(mqttPacket)); - - return _encoder.Encode(mqttPacket); - } - - public MqttBasePacket Decode(ReceivedMqttPacket receivedMqttPacket) - { - if (receivedMqttPacket == null) throw new ArgumentNullException(nameof(receivedMqttPacket)); - - return _decoder.Decode(receivedMqttPacket); - } - - public void FreeBuffer() - { - _encoder.FreeBuffer(); - } - } -} diff --git a/Source/MQTTnet/Formatter/V5/MqttV500PropertiesWriter.cs b/Source/MQTTnet/Formatter/V5/MqttV500PropertiesWriter.cs deleted file mode 100644 index d9c1b91..0000000 --- a/Source/MQTTnet/Formatter/V5/MqttV500PropertiesWriter.cs +++ /dev/null @@ -1,284 +0,0 @@ -using System; -using System.Collections.Generic; -using MQTTnet.Packets; -using MQTTnet.Protocol; - -namespace MQTTnet.Formatter.V5 -{ - public sealed class MqttV500PropertiesWriter - { - readonly MqttPacketWriter _packetWriter = new MqttPacketWriter(); - - public int Length => _packetWriter.Length; - - public void WriteUserProperties(List userProperties) - { - if (userProperties == null || userProperties.Count == 0) - { - return; - } - - foreach (var property in userProperties) - { - _packetWriter.Write((byte)MqttPropertyId.UserProperty); - _packetWriter.WriteWithLengthPrefix(property.Name); - _packetWriter.WriteWithLengthPrefix(property.Value); - } - } - - public void WriteCorrelationData(byte[] value) - { - Write(MqttPropertyId.CorrelationData, value); - } - - public void WriteAuthenticationData(byte[] value) - { - Write(MqttPropertyId.AuthenticationData, value); - } - - public void WriteReasonString(string value) - { - Write(MqttPropertyId.ReasonString, value); - } - - public void WriteResponseTopic(string value) - { - Write(MqttPropertyId.ResponseTopic, value); - } - - public void WriteContentType(string value) - { - Write(MqttPropertyId.ContentType, value); - } - - public void WriteServerReference(string value) - { - Write(MqttPropertyId.ServerReference, value); - } - - public void WriteAuthenticationMethod(string value) - { - Write(MqttPropertyId.AuthenticationMethod, value); - } - - public void WriteTo(IMqttPacketWriter packetWriter) - { - if (packetWriter == null) throw new ArgumentNullException(nameof(packetWriter)); - - packetWriter.WriteVariableLengthInteger((uint)_packetWriter.Length); - packetWriter.Write(_packetWriter); - } - - public void WriteSessionExpiryInterval(uint? value) - { - WriteAsFourByteInteger(MqttPropertyId.SessionExpiryInterval, value); - } - - public void WriteSubscriptionIdentifier(uint value) - { - WriteAsVariableLengthInteger(MqttPropertyId.SubscriptionIdentifier, value); - } - - public void WriteSubscriptionIdentifiers(ICollection value) - { - if (value == null) - { - return; - } - - foreach (var subscriptionIdentifier in value) - { - WriteAsVariableLengthInteger(MqttPropertyId.SubscriptionIdentifier, subscriptionIdentifier); - } - } - - public void WriteTopicAlias(ushort? value) - { - Write(MqttPropertyId.TopicAlias, value); - } - - public void WriteMessageExpiryInterval(uint? value) - { - WriteAsFourByteInteger(MqttPropertyId.MessageExpiryInterval, value); - } - - public void WritePayloadFormatIndicator(MqttPayloadFormatIndicator? value) - { - if (!value.HasValue) - { - return; - } - - Write(MqttPropertyId.PayloadFormatIndicator, (byte)value.Value); - } - - public void WriteWillDelayInterval(uint? value) - { - WriteAsFourByteInteger(MqttPropertyId.WillDelayInterval, value); - } - - public void WriteRequestProblemInformation(bool? value) - { - Write(MqttPropertyId.RequestProblemInformation, value); - } - - public void WriteRequestResponseInformation(bool? value) - { - Write(MqttPropertyId.RequestResponseInformation, value); - } - - public void WriteReceiveMaximum(ushort? value) - { - Write(MqttPropertyId.ReceiveMaximum, value); - } - - public void WriteMaximumQoS(MqttQualityOfServiceLevel? value) - { - if (!value.HasValue || value.Value > MqttQualityOfServiceLevel.AtLeastOnce) - { - return; - } - - _packetWriter.Write((byte)MqttPropertyId.MaximumQoS); - _packetWriter.Write((byte)value.Value); - } - - public void WriteMaximumPacketSize(uint? value) - { - WriteAsFourByteInteger(MqttPropertyId.MaximumPacketSize, value); - } - - public void WriteRetainAvailable(bool value) - { - if (value) - { - // Absence of the flag means it is supported! - return; - } - - Write(MqttPropertyId.RetainAvailable, false); - } - - public void WriteAssignedClientIdentifier(string value) - { - Write(MqttPropertyId.AssignedClientIdentifier, value); - } - - public void WriteTopicAliasMaximum(ushort? value) - { - Write(MqttPropertyId.TopicAliasMaximum, value); - } - - public void WriteWildcardSubscriptionAvailable(bool? value) - { - Write(MqttPropertyId.WildcardSubscriptionAvailable, value); - } - - public void WriteSubscriptionIdentifiersAvailable(bool value) - { - if (value) - { - // Absence of the flag means it is supported! - return; - } - - Write(MqttPropertyId.SubscriptionIdentifiersAvailable, false); - } - - public void WriteSharedSubscriptionAvailable(bool value) - { - if (value) - { - // Absence of the flag means it is supported! - return; - } - - Write(MqttPropertyId.SharedSubscriptionAvailable, false); - } - - public void WriteServerKeepAlive(ushort? value) - { - Write(MqttPropertyId.ServerKeepAlive, value); - } - - public void WriteResponseInformation(string value) - { - Write(MqttPropertyId.ResponseInformation, value); - } - - void Write(MqttPropertyId id, bool? value) - { - if (!value.HasValue) - { - return; - } - - _packetWriter.Write((byte)id); - _packetWriter.Write(value.Value ? (byte)0x1 : (byte)0x0); - } - - void Write(MqttPropertyId id, byte? value) - { - if (!value.HasValue) - { - return; - } - - _packetWriter.Write((byte)id); - _packetWriter.Write(value.Value); - } - - void Write(MqttPropertyId id, ushort? value) - { - if (!value.HasValue) - { - return; - } - - _packetWriter.Write((byte)id); - _packetWriter.Write(value.Value); - } - - void WriteAsVariableLengthInteger(MqttPropertyId id, uint value) - { - _packetWriter.Write((byte)id); - _packetWriter.WriteVariableLengthInteger(value); - } - - void WriteAsFourByteInteger(MqttPropertyId id, uint? value) - { - if (!value.HasValue) - { - return; - } - - _packetWriter.Write((byte)id); - _packetWriter.Write((byte)(value.Value >> 24)); - _packetWriter.Write((byte)(value.Value >> 16)); - _packetWriter.Write((byte)(value.Value >> 8)); - _packetWriter.Write((byte)value.Value); - } - - void Write(MqttPropertyId id, string value) - { - if (value == null) - { - return; - } - - _packetWriter.Write((byte)id); - _packetWriter.WriteWithLengthPrefix(value); - } - - void Write(MqttPropertyId id, byte[] value) - { - if (value == null) - { - return; - } - - _packetWriter.Write((byte)id); - _packetWriter.WriteWithLengthPrefix(value); - } - } -} diff --git a/Source/MQTTnet/Formatter/V5/MqttV5PacketDecoder.cs b/Source/MQTTnet/Formatter/V5/MqttV5PacketDecoder.cs new file mode 100644 index 0000000..5f87585 --- /dev/null +++ b/Source/MQTTnet/Formatter/V5/MqttV5PacketDecoder.cs @@ -0,0 +1,770 @@ +// 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.Adapter; +using MQTTnet.Exceptions; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Formatter.V5 +{ + public sealed class MqttV5PacketDecoder + { + readonly MqttBufferReader _bufferReader = new MqttBufferReader(); + + public MqttPacket Decode(ReceivedMqttPacket receivedMqttPacket) + { + if (receivedMqttPacket.TotalLength == 0) + { + return null; + } + + var controlPacketType = receivedMqttPacket.FixedHeader >> 4; + if (controlPacketType < 1) + { + throw new MqttProtocolViolationException($"The packet type is invalid ({controlPacketType})."); + } + + switch ((MqttControlPacketType)controlPacketType) + { + case MqttControlPacketType.Connect: + return DecodeConnectPacket(receivedMqttPacket.Body); + case MqttControlPacketType.ConnAck: + return DecodeConnAckPacket(receivedMqttPacket.Body); + case MqttControlPacketType.Disconnect: + return DecodeDisconnectPacket(receivedMqttPacket.Body); + case MqttControlPacketType.Publish: + return DecodePublishPacket(receivedMqttPacket.FixedHeader, receivedMqttPacket.Body); + case MqttControlPacketType.PubAck: + return DecodePubAckPacket(receivedMqttPacket.Body); + case MqttControlPacketType.PubRec: + return DecodePubRecPacket(receivedMqttPacket.Body); + case MqttControlPacketType.PubRel: + return DecodePubRelPacket(receivedMqttPacket.Body); + case MqttControlPacketType.PubComp: + return DecodePubCompPacket(receivedMqttPacket.Body); + case MqttControlPacketType.PingReq: + return MqttPingReqPacket.Instance; + case MqttControlPacketType.PingResp: + return MqttPingRespPacket.Instance; + case MqttControlPacketType.Subscribe: + return DecodeSubscribePacket(receivedMqttPacket.Body); + case MqttControlPacketType.SubAck: + return DecodeSubAckPacket(receivedMqttPacket.Body); + case MqttControlPacketType.Unsubscibe: + return DecodeUnsubscribePacket(receivedMqttPacket.Body); + case MqttControlPacketType.UnsubAck: + return DecodeUnsubAckPacket(receivedMqttPacket.Body); + case MqttControlPacketType.Auth: + return DecodeAuthPacket(receivedMqttPacket.Body); + + default: + throw new MqttProtocolViolationException($"Packet type ({controlPacketType}) not supported."); + } + } + + MqttPacket DecodeAuthPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttAuthPacket(); + + if (_bufferReader.EndOfStream) + { + packet.ReasonCode = MqttAuthenticateReasonCode.Success; + return packet; + } + + packet.ReasonCode = (MqttAuthenticateReasonCode)_bufferReader.ReadByte(); + + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationMethod) + { + packet.AuthenticationMethod = propertiesReader.ReadAuthenticationMethod(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationData) + { + packet.AuthenticationData = propertiesReader.ReadAuthenticationData(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) + { + packet.ReasonString = propertiesReader.ReadReasonString(); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttAuthPacket)); + } + } + + packet.UserProperties = propertiesReader.CollectedUserProperties; + + return packet; + } + + MqttPacket DecodeConnAckPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var acknowledgeFlags = _bufferReader.ReadByte(); + + var packet = new MqttConnAckPacket + { + IsSessionPresent = (acknowledgeFlags & 0x1) > 0, + ReasonCode = (MqttConnectReasonCode)_bufferReader.ReadByte(), + // indicate that a feature is available. + // Set all default values according to specification. When they are missing the often + RetainAvailable = true, + SharedSubscriptionAvailable = true, + SubscriptionIdentifiersAvailable = true, + WildcardSubscriptionAvailable = true, + // Absence indicates max QoS level. + MaximumQoS = MqttQualityOfServiceLevel.ExactlyOnce + }; + + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.SessionExpiryInterval) + { + packet.SessionExpiryInterval = propertiesReader.ReadSessionExpiryInterval(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationMethod) + { + packet.AuthenticationMethod = propertiesReader.ReadAuthenticationMethod(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationData) + { + packet.AuthenticationData = propertiesReader.ReadAuthenticationData(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.RetainAvailable) + { + packet.RetainAvailable = propertiesReader.ReadRetainAvailable(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReceiveMaximum) + { + packet.ReceiveMaximum = propertiesReader.ReadReceiveMaximum(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.MaximumQoS) + { + packet.MaximumQoS = propertiesReader.ReadMaximumQoS(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AssignedClientIdentifier) + { + packet.AssignedClientIdentifier = propertiesReader.ReadAssignedClientIdentifier(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.TopicAliasMaximum) + { + packet.TopicAliasMaximum = propertiesReader.ReadTopicAliasMaximum(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) + { + packet.ReasonString = propertiesReader.ReadReasonString(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.MaximumPacketSize) + { + packet.MaximumPacketSize = propertiesReader.ReadMaximumPacketSize(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.WildcardSubscriptionAvailable) + { + packet.WildcardSubscriptionAvailable = propertiesReader.ReadWildcardSubscriptionAvailable(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.SubscriptionIdentifiersAvailable) + { + packet.SubscriptionIdentifiersAvailable = propertiesReader.ReadSubscriptionIdentifiersAvailable(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.SharedSubscriptionAvailable) + { + packet.SharedSubscriptionAvailable = propertiesReader.ReadSharedSubscriptionAvailable(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ServerKeepAlive) + { + packet.ServerKeepAlive = propertiesReader.ReadServerKeepAlive(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ResponseInformation) + { + packet.ResponseInformation = propertiesReader.ReadResponseInformation(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ServerReference) + { + packet.ServerReference = propertiesReader.ReadServerReference(); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttConnAckPacket)); + } + } + + packet.UserProperties = propertiesReader.CollectedUserProperties; + + return packet; + } + + MqttPacket DecodeConnectPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttConnectPacket + { + // If the Request Problem Information is absent, the value of 1 is used. + RequestProblemInformation = true + }; + + var protocolName = _bufferReader.ReadString(); + var protocolVersion = _bufferReader.ReadByte(); + + if (protocolName != "MQTT" && protocolVersion != 5) + { + throw new MqttProtocolViolationException("MQTT protocol name and version do not match MQTT v5."); + } + + var connectFlags = _bufferReader.ReadByte(); + + var cleanSessionFlag = (connectFlags & 0x02) > 0; + var willMessageFlag = (connectFlags & 0x04) > 0; + var willMessageQoS = (byte)((connectFlags >> 3) & 3); + var willMessageRetainFlag = (connectFlags & 0x20) > 0; + var passwordFlag = (connectFlags & 0x40) > 0; + var usernameFlag = (connectFlags & 0x80) > 0; + + packet.CleanSession = cleanSessionFlag; + + if (willMessageFlag) + { + packet.WillFlag = true; + packet.WillQoS = (MqttQualityOfServiceLevel)willMessageQoS; + packet.WillRetain = willMessageRetainFlag; + } + + packet.KeepAlivePeriod = _bufferReader.ReadTwoByteInteger(); + + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.SessionExpiryInterval) + { + packet.SessionExpiryInterval = propertiesReader.ReadSessionExpiryInterval(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationMethod) + { + packet.AuthenticationMethod = propertiesReader.ReadAuthenticationMethod(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.AuthenticationData) + { + packet.AuthenticationData = propertiesReader.ReadAuthenticationData(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReceiveMaximum) + { + packet.ReceiveMaximum = propertiesReader.ReadReceiveMaximum(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.TopicAliasMaximum) + { + packet.TopicAliasMaximum = propertiesReader.ReadTopicAliasMaximum(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.MaximumPacketSize) + { + packet.MaximumPacketSize = propertiesReader.ReadMaximumPacketSize(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.RequestResponseInformation) + { + packet.RequestResponseInformation = propertiesReader.RequestResponseInformation(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.RequestProblemInformation) + { + packet.RequestProblemInformation = propertiesReader.RequestProblemInformation(); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttConnectPacket)); + } + } + + packet.UserProperties = propertiesReader.CollectedUserProperties; + + packet.ClientId = _bufferReader.ReadString(); + + if (packet.WillFlag) + { + var willPropertiesReader = new MqttV5PropertiesReader(_bufferReader); + + while (willPropertiesReader.MoveNext()) + { + if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.PayloadFormatIndicator) + { + packet.WillPayloadFormatIndicator = willPropertiesReader.ReadPayloadFormatIndicator(); + } + else if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.MessageExpiryInterval) + { + packet.WillMessageExpiryInterval = willPropertiesReader.ReadMessageExpiryInterval(); + } + else if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.ResponseTopic) + { + packet.WillResponseTopic = willPropertiesReader.ReadResponseTopic(); + } + else if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.CorrelationData) + { + packet.WillCorrelationData = willPropertiesReader.ReadCorrelationData(); + } + else if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.ContentType) + { + packet.WillContentType = willPropertiesReader.ReadContentType(); + } + else if (willPropertiesReader.CurrentPropertyId == MqttPropertyId.WillDelayInterval) + { + packet.WillDelayInterval = willPropertiesReader.ReadWillDelayInterval(); + } + else + { + willPropertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPublishPacket)); + } + } + + packet.WillTopic = _bufferReader.ReadString(); + packet.WillMessage = _bufferReader.ReadBinaryData(); + packet.WillUserProperties = willPropertiesReader.CollectedUserProperties; + } + + if (usernameFlag) + { + packet.Username = _bufferReader.ReadString(); + } + + if (passwordFlag) + { + packet.Password = _bufferReader.ReadBinaryData(); + } + + return packet; + } + + MqttPacket DecodeDisconnectPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttDisconnectPacket + { + ReasonCode = (MqttDisconnectReasonCode)_bufferReader.ReadByte() + }; + + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.SessionExpiryInterval) + { + packet.SessionExpiryInterval = propertiesReader.ReadSessionExpiryInterval(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) + { + packet.ReasonString = propertiesReader.ReadReasonString(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ServerReference) + { + packet.ServerReference = propertiesReader.ReadServerReference(); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttDisconnectPacket)); + } + } + + packet.UserProperties = propertiesReader.CollectedUserProperties; + + return packet; + } + + MqttPacket DecodePubAckPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttPubAckPacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + + if (_bufferReader.EndOfStream) + { + packet.ReasonCode = MqttPubAckReasonCode.Success; + return packet; + } + + packet.ReasonCode = (MqttPubAckReasonCode)_bufferReader.ReadByte(); + + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) + { + packet.ReasonString = propertiesReader.ReadReasonString(); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPubAckPacket)); + } + } + + packet.UserProperties = propertiesReader.CollectedUserProperties; + + return packet; + } + + MqttPacket DecodePubCompPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttPubCompPacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + + if (_bufferReader.EndOfStream) + { + packet.ReasonCode = MqttPubCompReasonCode.Success; + return packet; + } + + packet.ReasonCode = (MqttPubCompReasonCode)_bufferReader.ReadByte(); + + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) + { + packet.ReasonString = propertiesReader.ReadReasonString(); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPubCompPacket)); + } + } + + packet.UserProperties = propertiesReader.CollectedUserProperties; + + return packet; + } + + + MqttPacket DecodePublishPacket(byte header, ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var retain = (header & 1) > 0; + var qos = (MqttQualityOfServiceLevel)((header >> 1) & 3); + var dup = ((header >> 3) & 1) > 0; + + var packet = new MqttPublishPacket + { + Topic = _bufferReader.ReadString(), + Retain = retain, + QualityOfServiceLevel = qos, + Dup = dup + }; + + if (qos > 0) + { + packet.PacketIdentifier = _bufferReader.ReadTwoByteInteger(); + } + + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.PayloadFormatIndicator) + { + packet.PayloadFormatIndicator = propertiesReader.ReadPayloadFormatIndicator(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.MessageExpiryInterval) + { + packet.MessageExpiryInterval = propertiesReader.ReadMessageExpiryInterval(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.TopicAlias) + { + packet.TopicAlias = propertiesReader.ReadTopicAlias(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ResponseTopic) + { + packet.ResponseTopic = propertiesReader.ReadResponseTopic(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.CorrelationData) + { + packet.CorrelationData = propertiesReader.ReadCorrelationData(); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.SubscriptionIdentifier) + { + if (packet.SubscriptionIdentifiers == null) + { + packet.SubscriptionIdentifiers = new List(); + } + + packet.SubscriptionIdentifiers.Add(propertiesReader.ReadSubscriptionIdentifier()); + } + else if (propertiesReader.CurrentPropertyId == MqttPropertyId.ContentType) + { + packet.ContentType = propertiesReader.ReadContentType(); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPublishPacket)); + } + } + + packet.UserProperties = propertiesReader.CollectedUserProperties; + + if (!_bufferReader.EndOfStream) + { + packet.Payload = _bufferReader.ReadRemainingData(); + } + + return packet; + } + + MqttPacket DecodePubRecPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttPubRecPacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + + if (_bufferReader.EndOfStream) + { + packet.ReasonCode = MqttPubRecReasonCode.Success; + return packet; + } + + packet.ReasonCode = (MqttPubRecReasonCode)_bufferReader.ReadByte(); + + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) + { + packet.ReasonString = propertiesReader.ReadReasonString(); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPubRecPacket)); + } + } + + packet.UserProperties = propertiesReader.CollectedUserProperties; + + return packet; + } + + MqttPacket DecodePubRelPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttPubRelPacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + + if (_bufferReader.EndOfStream) + { + packet.ReasonCode = MqttPubRelReasonCode.Success; + return packet; + } + + packet.ReasonCode = (MqttPubRelReasonCode)_bufferReader.ReadByte(); + + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) + { + packet.ReasonString = propertiesReader.ReadReasonString(); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttPubRelPacket)); + } + } + + packet.UserProperties = propertiesReader.CollectedUserProperties; + + return packet; + } + + MqttPacket DecodeSubAckPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttSubAckPacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) + { + packet.ReasonString = propertiesReader.ReadReasonString(); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttSubAckPacket)); + } + } + + packet.UserProperties = propertiesReader.CollectedUserProperties; + + packet.ReasonCodes = new List(_bufferReader.BytesLeft); + while (!_bufferReader.EndOfStream) + { + var reasonCode = (MqttSubscribeReasonCode)_bufferReader.ReadByte(); + packet.ReasonCodes.Add(reasonCode); + } + + return packet; + } + + MqttPacket DecodeSubscribePacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttSubscribePacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.SubscriptionIdentifier) + { + packet.SubscriptionIdentifier = propertiesReader.ReadSubscriptionIdentifier(); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttSubscribePacket)); + } + } + + packet.UserProperties = propertiesReader.CollectedUserProperties; + + while (!_bufferReader.EndOfStream) + { + var topic = _bufferReader.ReadString(); + var options = _bufferReader.ReadByte(); + + var qos = (MqttQualityOfServiceLevel)(options & 3); + var noLocal = (options & (1 << 2)) > 0; + var retainAsPublished = (options & (1 << 3)) > 0; + var retainHandling = (MqttRetainHandling)((options >> 4) & 3); + + packet.TopicFilters.Add( + new MqttTopicFilter + { + Topic = topic, + QualityOfServiceLevel = qos, + NoLocal = noLocal, + RetainAsPublished = retainAsPublished, + RetainHandling = retainHandling + }); + } + + return packet; + } + + MqttPacket DecodeUnsubAckPacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttUnsubAckPacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) + { + if (propertiesReader.CurrentPropertyId == MqttPropertyId.ReasonString) + { + packet.ReasonString = propertiesReader.ReadReasonString(); + } + else + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttUnsubAckPacket)); + } + } + + packet.UserProperties = propertiesReader.CollectedUserProperties; + + packet.ReasonCodes = new List(_bufferReader.BytesLeft); + + while (!_bufferReader.EndOfStream) + { + var reasonCode = (MqttUnsubscribeReasonCode)_bufferReader.ReadByte(); + packet.ReasonCodes.Add(reasonCode); + } + + return packet; + } + + MqttPacket DecodeUnsubscribePacket(ArraySegment body) + { + ThrowIfBodyIsEmpty(body); + + _bufferReader.SetBuffer(body.Array, body.Offset, body.Count); + + var packet = new MqttUnsubscribePacket + { + PacketIdentifier = _bufferReader.ReadTwoByteInteger() + }; + + var propertiesReader = new MqttV5PropertiesReader(_bufferReader); + while (propertiesReader.MoveNext()) + { + propertiesReader.ThrowInvalidPropertyIdException(typeof(MqttUnsubscribePacket)); + } + + packet.UserProperties = propertiesReader.CollectedUserProperties; + + while (!_bufferReader.EndOfStream) + { + packet.TopicFilters.Add(_bufferReader.ReadString()); + } + + return packet; + } + + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local + static void ThrowIfBodyIsEmpty(ArraySegment body) + { + if (body.Count == 0) + { + throw new MqttProtocolViolationException("Data from the body is required but not present."); + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/V5/MqttV5PacketEncoder.cs b/Source/MQTTnet/Formatter/V5/MqttV5PacketEncoder.cs new file mode 100644 index 0000000..2747848 --- /dev/null +++ b/Source/MQTTnet/Formatter/V5/MqttV5PacketEncoder.cs @@ -0,0 +1,538 @@ +// 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.Linq; +using MQTTnet.Exceptions; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Formatter.V5 +{ + public sealed class MqttV5PacketEncoder + { + readonly MqttBufferWriter _bufferWriter; + readonly MqttV5PropertiesWriter _propertiesWriter = new MqttV5PropertiesWriter(new MqttBufferWriter(1024, 4096)); + + public MqttV5PacketEncoder(MqttBufferWriter bufferWriter) + { + _bufferWriter = bufferWriter ?? throw new ArgumentNullException(nameof(bufferWriter)); + } + + public MqttPacketBuffer Encode(MqttPacket packet) + { + if (packet == null) + { + throw new ArgumentNullException(nameof(packet)); + } + + // Leave enough head space for max header size (fixed + 4 variable remaining length = 5 bytes) + _bufferWriter.Reset(5); + _bufferWriter.Seek(5); + + var fixedHeader = EncodePacket(packet); + var remainingLength = (uint)_bufferWriter.Length - 5; + + var publishPacket = packet as MqttPublishPacket; + if (publishPacket?.Payload != null) + { + remainingLength += (uint)publishPacket.Payload.Length; + } + + var remainingLengthSize = MqttBufferWriter.GetLengthOfVariableInteger(remainingLength); + + var headerSize = 1 + remainingLengthSize; + var headerOffset = 5 - headerSize; + + // Position cursor on correct offset on beginning of array (has leading 0x0) + _bufferWriter.Seek(headerOffset); + _bufferWriter.WriteByte(fixedHeader); + _bufferWriter.WriteVariableByteInteger(remainingLength); + + var buffer = _bufferWriter.GetBuffer(); + + var firstSegment = new ArraySegment(buffer, headerOffset, _bufferWriter.Length - headerOffset); + + if (publishPacket?.Payload != null) + { + var payloadSegment = new ArraySegment(publishPacket.Payload, 0, publishPacket.Payload.Length); + return new MqttPacketBuffer(firstSegment, payloadSegment); + } + + return new MqttPacketBuffer(firstSegment); + } + + byte EncodeAuthPacket(MqttAuthPacket packet) + { + _bufferWriter.WriteByte((byte)packet.ReasonCode); + + _propertiesWriter.WriteAuthenticationMethod(packet.AuthenticationMethod); + _propertiesWriter.WriteAuthenticationData(packet.AuthenticationData); + _propertiesWriter.WriteReasonString(packet.ReasonString); + _propertiesWriter.WriteUserProperties(packet.UserProperties); + + _propertiesWriter.WriteTo(_bufferWriter); + _propertiesWriter.Reset(); + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.Auth); + } + + byte EncodeConnAckPacket(MqttConnAckPacket packet) + { + byte connectAcknowledgeFlags = 0x0; + if (packet.IsSessionPresent) + { + connectAcknowledgeFlags |= 0x1; + } + + _bufferWriter.WriteByte(connectAcknowledgeFlags); + _bufferWriter.WriteByte((byte)packet.ReasonCode); + + _propertiesWriter.WriteSessionExpiryInterval(packet.SessionExpiryInterval); + _propertiesWriter.WriteAuthenticationMethod(packet.AuthenticationMethod); + _propertiesWriter.WriteAuthenticationData(packet.AuthenticationData); + _propertiesWriter.WriteRetainAvailable(packet.RetainAvailable); + _propertiesWriter.WriteReceiveMaximum(packet.ReceiveMaximum); + _propertiesWriter.WriteMaximumQoS(packet.MaximumQoS); + _propertiesWriter.WriteAssignedClientIdentifier(packet.AssignedClientIdentifier); + _propertiesWriter.WriteTopicAliasMaximum(packet.TopicAliasMaximum); + _propertiesWriter.WriteReasonString(packet.ReasonString); + _propertiesWriter.WriteMaximumPacketSize(packet.MaximumPacketSize); + _propertiesWriter.WriteWildcardSubscriptionAvailable(packet.WildcardSubscriptionAvailable); + _propertiesWriter.WriteSubscriptionIdentifiersAvailable(packet.SubscriptionIdentifiersAvailable); + _propertiesWriter.WriteSharedSubscriptionAvailable(packet.SharedSubscriptionAvailable); + _propertiesWriter.WriteServerKeepAlive(packet.ServerKeepAlive); + _propertiesWriter.WriteResponseInformation(packet.ResponseInformation); + _propertiesWriter.WriteServerReference(packet.ServerReference); + _propertiesWriter.WriteUserProperties(packet.UserProperties); + + _propertiesWriter.WriteTo(_bufferWriter); + _propertiesWriter.Reset(); + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.ConnAck); + } + + byte EncodeConnectPacket(MqttConnectPacket packet) + { + if (string.IsNullOrEmpty(packet.ClientId) && !packet.CleanSession) + { + throw new MqttProtocolViolationException("CleanSession must be set if ClientId is empty [MQTT-3.1.3-7]."); + } + + _bufferWriter.WriteString("MQTT"); + _bufferWriter.WriteByte(5); // [3.1.2.2 Protocol Version] + + byte connectFlags = 0x0; + if (packet.CleanSession) + { + connectFlags |= 0x2; + } + + if (packet.WillFlag) + { + connectFlags |= 0x4; + connectFlags |= (byte)((byte)packet.WillQoS << 3); + + if (packet.WillRetain) + { + connectFlags |= 0x20; + } + } + + if (packet.Password != null && packet.Username == null) + { + throw new MqttProtocolViolationException("If the User Name Flag is set to 0, the Password Flag MUST be set to 0 [MQTT-3.1.2-22]."); + } + + if (packet.Password != null) + { + connectFlags |= 0x40; + } + + if (packet.Username != null) + { + connectFlags |= 0x80; + } + + _bufferWriter.WriteByte(connectFlags); + _bufferWriter.WriteTwoByteInteger(packet.KeepAlivePeriod); + + _propertiesWriter.WriteSessionExpiryInterval(packet.SessionExpiryInterval); + _propertiesWriter.WriteAuthenticationMethod(packet.AuthenticationMethod); + _propertiesWriter.WriteAuthenticationData(packet.AuthenticationData); + _propertiesWriter.WriteRequestProblemInformation(packet.RequestProblemInformation); + _propertiesWriter.WriteRequestResponseInformation(packet.RequestResponseInformation); + _propertiesWriter.WriteReceiveMaximum(packet.ReceiveMaximum); + _propertiesWriter.WriteTopicAliasMaximum(packet.TopicAliasMaximum); + _propertiesWriter.WriteMaximumPacketSize(packet.MaximumPacketSize); + _propertiesWriter.WriteUserProperties(packet.UserProperties); + + _propertiesWriter.WriteTo(_bufferWriter); + _propertiesWriter.Reset(); + + _bufferWriter.WriteString(packet.ClientId); + + if (packet.WillFlag) + { + _propertiesWriter.WritePayloadFormatIndicator(packet.WillPayloadFormatIndicator); + _propertiesWriter.WriteMessageExpiryInterval(packet.WillMessageExpiryInterval); + _propertiesWriter.WriteResponseTopic(packet.WillResponseTopic); + _propertiesWriter.WriteCorrelationData(packet.WillCorrelationData); + _propertiesWriter.WriteContentType(packet.WillContentType); + _propertiesWriter.WriteUserProperties(packet.WillUserProperties); + _propertiesWriter.WriteWillDelayInterval(packet.WillDelayInterval); + + _propertiesWriter.WriteTo(_bufferWriter); + _propertiesWriter.Reset(); + + _bufferWriter.WriteString(packet.WillTopic); + _bufferWriter.WriteBinaryData(packet.WillMessage); + } + + if (packet.Username != null) + { + _bufferWriter.WriteString(packet.Username); + } + + if (packet.Password != null) + { + _bufferWriter.WriteBinaryData(packet.Password); + } + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.Connect); + } + + byte EncodeDisconnectPacket(MqttDisconnectPacket packet) + { + _bufferWriter.WriteByte((byte)packet.ReasonCode); + + _propertiesWriter.WriteServerReference(packet.ServerReference); + _propertiesWriter.WriteReasonString(packet.ReasonString); + _propertiesWriter.WriteSessionExpiryInterval(packet.SessionExpiryInterval); + _propertiesWriter.WriteUserProperties(packet.UserProperties); + + _propertiesWriter.WriteTo(_bufferWriter); + _propertiesWriter.Reset(); + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.Disconnect); + } + + byte EncodePacket(MqttPacket packet) + { + switch (packet) + { + case MqttConnectPacket connectPacket: + return EncodeConnectPacket(connectPacket); + case MqttConnAckPacket connAckPacket: + return EncodeConnAckPacket(connAckPacket); + case MqttDisconnectPacket disconnectPacket: + return EncodeDisconnectPacket(disconnectPacket); + case MqttPingReqPacket _: + return EncodePingReqPacket(); + case MqttPingRespPacket _: + return EncodePingRespPacket(); + case MqttPublishPacket publishPacket: + return EncodePublishPacket(publishPacket); + case MqttPubAckPacket pubAckPacket: + return EncodePubAckPacket(pubAckPacket); + case MqttPubRecPacket pubRecPacket: + return EncodePubRecPacket(pubRecPacket); + case MqttPubRelPacket pubRelPacket: + return EncodePubRelPacket(pubRelPacket); + case MqttPubCompPacket pubCompPacket: + return EncodePubCompPacket(pubCompPacket); + case MqttSubscribePacket subscribePacket: + return EncodeSubscribePacket(subscribePacket); + case MqttSubAckPacket subAckPacket: + return EncodeSubAckPacket(subAckPacket); + case MqttUnsubscribePacket unsubscribePacket: + return EncodeUnsubscribePacket(unsubscribePacket); + case MqttUnsubAckPacket unsubAckPacket: + return EncodeUnsubAckPacket(unsubAckPacket); + case MqttAuthPacket authPacket: + return EncodeAuthPacket(authPacket); + + default: + throw new MqttProtocolViolationException("Packet type invalid."); + } + } + + static byte EncodePingReqPacket() + { + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.PingReq); + } + + static byte EncodePingRespPacket() + { + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.PingResp); + } + + byte EncodePubAckPacket(MqttPubAckPacket packet) + { + if (packet.PacketIdentifier == 0) + { + throw new MqttProtocolViolationException("PubAck packet has no packet identifier."); + } + + _bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + + _propertiesWriter.WriteReasonString(packet.ReasonString); + _propertiesWriter.WriteUserProperties(packet.UserProperties); + + if (_bufferWriter.Length > 0 || packet.ReasonCode != MqttPubAckReasonCode.Success) + { + _bufferWriter.WriteByte((byte)packet.ReasonCode); + _propertiesWriter.WriteTo(_bufferWriter); + _propertiesWriter.Reset(); + } + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.PubAck); + } + + byte EncodePubCompPacket(MqttPubCompPacket packet) + { + ThrowIfPacketIdentifierIsInvalid(packet.PacketIdentifier, packet); + + _bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + + _propertiesWriter.WriteReasonString(packet.ReasonString); + _propertiesWriter.WriteUserProperties(packet.UserProperties); + + if (_propertiesWriter.Length > 0 || packet.ReasonCode != MqttPubCompReasonCode.Success) + { + _bufferWriter.WriteByte((byte)packet.ReasonCode); + _propertiesWriter.WriteTo(_bufferWriter); + _propertiesWriter.Reset(); + } + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.PubComp); + } + + byte EncodePublishPacket(MqttPublishPacket packet) + { + if (packet.QualityOfServiceLevel == 0 && packet.Dup) + { + throw new MqttProtocolViolationException("Dup flag must be false for QoS 0 packets [MQTT-3.3.1-2]."); + } + + _bufferWriter.WriteString(packet.Topic); + + if (packet.QualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) + { + if (packet.PacketIdentifier == 0) + { + throw new MqttProtocolViolationException("Publish packet has no packet identifier."); + } + + _bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + } + else + { + if (packet.PacketIdentifier > 0) + { + throw new MqttProtocolViolationException("Packet identifier must be 0 if QoS == 0 [MQTT-2.3.1-5]."); + } + } + + _propertiesWriter.WriteContentType(packet.ContentType); + _propertiesWriter.WriteCorrelationData(packet.CorrelationData); + _propertiesWriter.WriteMessageExpiryInterval(packet.MessageExpiryInterval); + _propertiesWriter.WritePayloadFormatIndicator(packet.PayloadFormatIndicator); + _propertiesWriter.WriteResponseTopic(packet.ResponseTopic); + _propertiesWriter.WriteSubscriptionIdentifiers(packet.SubscriptionIdentifiers); + _propertiesWriter.WriteUserProperties(packet.UserProperties); + _propertiesWriter.WriteTopicAlias(packet.TopicAlias); + + _propertiesWriter.WriteTo(_bufferWriter); + _propertiesWriter.Reset(); + + // The payload is the past part of the packet. But it is not added here in order to keep + // memory allocation low. + + byte fixedHeader = 0; + + if (packet.Retain) + { + fixedHeader |= 0x01; + } + + fixedHeader |= (byte)((byte)packet.QualityOfServiceLevel << 1); + + if (packet.Dup) + { + fixedHeader |= 0x08; + } + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.Publish, fixedHeader); + } + + byte EncodePubRecPacket(MqttPubRecPacket packet) + { + ThrowIfPacketIdentifierIsInvalid(packet.PacketIdentifier, packet); + + _propertiesWriter.WriteReasonString(packet.ReasonString); + _propertiesWriter.WriteUserProperties(packet.UserProperties); + + _bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + + if (_bufferWriter.Length > 0 || packet.ReasonCode != MqttPubRecReasonCode.Success) + { + _bufferWriter.WriteByte((byte)packet.ReasonCode); + _propertiesWriter.WriteTo(_bufferWriter); + _propertiesWriter.Reset(); + } + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.PubRec); + } + + byte EncodePubRelPacket(MqttPubRelPacket packet) + { + ThrowIfPacketIdentifierIsInvalid(packet.PacketIdentifier, packet); + + _propertiesWriter.WriteReasonString(packet.ReasonString); + _propertiesWriter.WriteUserProperties(packet.UserProperties); + + _bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + + if (_propertiesWriter.Length > 0 || packet.ReasonCode != MqttPubRelReasonCode.Success) + { + _bufferWriter.WriteByte((byte)packet.ReasonCode); + _propertiesWriter.WriteTo(_bufferWriter); + _propertiesWriter.Reset(); + } + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.PubRel, 0x02); + } + + byte EncodeSubAckPacket(MqttSubAckPacket packet) + { + if (packet.ReasonCodes?.Any() != true) + { + throw new MqttProtocolViolationException("At least one reason code must be set[MQTT - 3.8.3 - 3]."); + } + + ThrowIfPacketIdentifierIsInvalid(packet.PacketIdentifier, packet); + + _bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + + _propertiesWriter.WriteReasonString(packet.ReasonString); + _propertiesWriter.WriteUserProperties(packet.UserProperties); + + _propertiesWriter.WriteTo(_bufferWriter); + _propertiesWriter.Reset(); + + foreach (var reasonCode in packet.ReasonCodes) + { + _bufferWriter.WriteByte((byte)reasonCode); + } + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.SubAck); + } + + byte EncodeSubscribePacket(MqttSubscribePacket packet) + { + if (packet.TopicFilters?.Any() != true) + { + throw new MqttProtocolViolationException("At least one topic filter must be set [MQTT-3.8.3-3]."); + } + + ThrowIfPacketIdentifierIsInvalid(packet.PacketIdentifier, packet); + + _bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + + if (packet.SubscriptionIdentifier > 0) + { + _propertiesWriter.WriteSubscriptionIdentifier(packet.SubscriptionIdentifier); + } + + _propertiesWriter.WriteUserProperties(packet.UserProperties); + + _propertiesWriter.WriteTo(_bufferWriter); + _propertiesWriter.Reset(); + + if (packet.TopicFilters?.Count > 0) + { + foreach (var topicFilter in packet.TopicFilters) + { + _bufferWriter.WriteString(topicFilter.Topic); + + var options = (byte)topicFilter.QualityOfServiceLevel; + + if (topicFilter.NoLocal) + { + options |= 1 << 2; + } + + if (topicFilter.RetainAsPublished) + { + options |= 1 << 3; + } + + if (topicFilter.RetainHandling != MqttRetainHandling.SendAtSubscribe) + { + options |= (byte)((byte)topicFilter.RetainHandling << 4); + } + + _bufferWriter.WriteByte(options); + } + } + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.Subscribe, 0x02); + } + + byte EncodeUnsubAckPacket(MqttUnsubAckPacket packet) + { + ThrowIfPacketIdentifierIsInvalid(packet.PacketIdentifier, packet); + + _bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + + _propertiesWriter.WriteReasonString(packet.ReasonString); + _propertiesWriter.WriteUserProperties(packet.UserProperties); + + _propertiesWriter.WriteTo(_bufferWriter); + _propertiesWriter.Reset(); + + foreach (var reasonCode in packet.ReasonCodes) + { + _bufferWriter.WriteByte((byte)reasonCode); + } + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.UnsubAck); + } + + byte EncodeUnsubscribePacket(MqttUnsubscribePacket packet) + { + if (packet.TopicFilters?.Any() != true) + { + throw new MqttProtocolViolationException("At least one topic filter must be set [MQTT-3.10.3-2]."); + } + + ThrowIfPacketIdentifierIsInvalid(packet.PacketIdentifier, packet); + + _bufferWriter.WriteTwoByteInteger(packet.PacketIdentifier); + + _propertiesWriter.WriteUserProperties(packet.UserProperties); + + _propertiesWriter.WriteTo(_bufferWriter); + _propertiesWriter.Reset(); + + foreach (var topicFilter in packet.TopicFilters) + { + _bufferWriter.WriteString(topicFilter); + } + + return MqttBufferWriter.BuildFixedHeader(MqttControlPacketType.Unsubscibe, 0x02); + } + + static void ThrowIfPacketIdentifierIsInvalid(ushort packetIdentifier, MqttPacket packet) + { + // SUBSCRIBE, UNSUBSCRIBE, and PUBLISH(in cases where QoS > 0) Control Packets MUST contain a non-zero 16 - bit Packet Identifier[MQTT - 2.3.1 - 1]. + + if (packetIdentifier == 0) + { + throw new MqttProtocolViolationException($"Packet identifier is not set for {packet.GetType().Name}."); + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/V5/MqttV5PacketFormatter.cs b/Source/MQTTnet/Formatter/V5/MqttV5PacketFormatter.cs new file mode 100644 index 0000000..371cb86 --- /dev/null +++ b/Source/MQTTnet/Formatter/V5/MqttV5PacketFormatter.cs @@ -0,0 +1,30 @@ +// 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.Packets; + +namespace MQTTnet.Formatter.V5 +{ + public sealed class MqttV5PacketFormatter : IMqttPacketFormatter + { + readonly MqttV5PacketDecoder _decoder = new MqttV5PacketDecoder(); + readonly MqttV5PacketEncoder _encoder; + + public MqttV5PacketFormatter(MqttBufferWriter bufferWriter) + { + _encoder = new MqttV5PacketEncoder(bufferWriter); + } + + public MqttPacket Decode(ReceivedMqttPacket receivedMqttPacket) + { + return _decoder.Decode(receivedMqttPacket); + } + + public MqttPacketBuffer Encode(MqttPacket mqttPacket) + { + return _encoder.Encode(mqttPacket); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/V5/MqttV500PropertiesReader.cs b/Source/MQTTnet/Formatter/V5/MqttV5PropertiesReader.cs similarity index 54% rename from Source/MQTTnet/Formatter/V5/MqttV500PropertiesReader.cs rename to Source/MQTTnet/Formatter/V5/MqttV5PropertiesReader.cs index bb8beac..e5a3efe 100644 --- a/Source/MQTTnet/Formatter/V5/MqttV500PropertiesReader.cs +++ b/Source/MQTTnet/Formatter/V5/MqttV5PropertiesReader.cs @@ -1,4 +1,8 @@ -using System; +// 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.Exceptions; using MQTTnet.Packets; @@ -6,171 +10,191 @@ using MQTTnet.Protocol; namespace MQTTnet.Formatter.V5 { - public class MqttV500PropertiesReader + public struct MqttV5PropertiesReader { - private readonly IMqttPacketBodyReader _body; - private readonly int _length; - private readonly int _targetOffset; + readonly MqttBufferReader _body; + readonly int _length; + readonly int _targetOffset; - public MqttV500PropertiesReader(IMqttPacketBodyReader body) + public MqttV5PropertiesReader(MqttBufferReader body) { _body = body ?? throw new ArgumentNullException(nameof(body)); if (!body.EndOfStream) { - _length = (int)body.ReadVariableLengthInteger(); + _length = (int)body.ReadVariableByteInteger(); + } + else + { + _length = 0; } - _targetOffset = body.Offset + _length; + _targetOffset = body.Position + _length; + + CollectedUserProperties = null; + CurrentPropertyId = MqttPropertyId.None; } + public List CollectedUserProperties { get; private set; } + public MqttPropertyId CurrentPropertyId { get; private set; } public bool MoveNext() { - if (_length == 0) + while (true) { - return false; + if (_length == 0) + { + return false; + } + + if (_body.Position >= _targetOffset) + { + return false; + } + + CurrentPropertyId = (MqttPropertyId)_body.ReadByte(); + + // User properties are special because they can appear multiple times in the + // buffer and at any position. So we collect them here to expose them as a + // final result list. + if (CurrentPropertyId == MqttPropertyId.UserProperty) + { + var name = _body.ReadString(); + var value = _body.ReadString(); + + if (CollectedUserProperties == null) + { + CollectedUserProperties = new List(); + } + + CollectedUserProperties.Add(new MqttUserProperty(name, value)); + continue; + } + + return true; } - - if (_body.Offset >= _targetOffset) - { - return false; - } - - CurrentPropertyId = (MqttPropertyId)_body.ReadByte(); - return true; } - public void AddUserPropertyTo(List userProperties) + public string ReadAssignedClientIdentifier() { - if (userProperties == null) throw new ArgumentNullException(nameof(userProperties)); - - var name = _body.ReadStringWithLengthPrefix(); - var value = _body.ReadStringWithLengthPrefix(); - - userProperties.Add(new MqttUserProperty(name, value)); + return _body.ReadString(); } - public string ReadReasonString() + public byte[] ReadAuthenticationData() { - return _body.ReadStringWithLengthPrefix(); + return _body.ReadBinaryData(); } public string ReadAuthenticationMethod() { - return _body.ReadStringWithLengthPrefix(); + return _body.ReadString(); } - public byte[] ReadAuthenticationData() + public string ReadContentType() { - return _body.ReadWithLengthPrefix(); + return _body.ReadString(); } - public bool ReadRetainAvailable() + public byte[] ReadCorrelationData() { - return _body.ReadBoolean(); + return _body.ReadBinaryData(); } - public uint ReadSessionExpiryInterval() + public uint ReadMaximumPacketSize() { return _body.ReadFourByteInteger(); } - public ushort ReadReceiveMaximum() - { - return _body.ReadTwoByteInteger(); - } - public MqttQualityOfServiceLevel ReadMaximumQoS() { - byte value = _body.ReadByte(); + var value = _body.ReadByte(); if (value > 1) { throw new MqttProtocolViolationException($"Unexpected Maximum QoS value: {value}"); } - + return (MqttQualityOfServiceLevel)value; } - public string ReadAssignedClientIdentifier() + public uint ReadMessageExpiryInterval() { - return _body.ReadStringWithLengthPrefix(); + return _body.ReadFourByteInteger(); } - public string ReadServerReference() + public MqttPayloadFormatIndicator ReadPayloadFormatIndicator() { - return _body.ReadStringWithLengthPrefix(); + return (MqttPayloadFormatIndicator)_body.ReadByte(); } - public ushort ReadTopicAliasMaximum() + public string ReadReasonString() { - return _body.ReadTwoByteInteger(); + return _body.ReadString(); } - public uint ReadMaximumPacketSize() + public ushort ReadReceiveMaximum() { - return _body.ReadFourByteInteger(); + return _body.ReadTwoByteInteger(); } - public ushort ReadServerKeepAlive() + public string ReadResponseInformation() { - return _body.ReadTwoByteInteger(); + return _body.ReadString(); } - public string ReadResponseInformation() + public string ReadResponseTopic() { - return _body.ReadStringWithLengthPrefix(); + return _body.ReadString(); } - public bool ReadSharedSubscriptionAvailable() + public bool ReadRetainAvailable() { - return _body.ReadBoolean(); + return _body.ReadByte() == 1; } - public bool ReadSubscriptionIdentifiersAvailable() + public ushort ReadServerKeepAlive() { - return _body.ReadBoolean(); + return _body.ReadTwoByteInteger(); } - public bool ReadWildcardSubscriptionAvailable() + public string ReadServerReference() { - return _body.ReadBoolean(); + return _body.ReadString(); } - public uint ReadSubscriptionIdentifier() + public uint ReadSessionExpiryInterval() { - return _body.ReadVariableLengthInteger(); + return _body.ReadFourByteInteger(); } - public MqttPayloadFormatIndicator? ReadPayloadFormatIndicator() + public bool ReadSharedSubscriptionAvailable() { - return (MqttPayloadFormatIndicator)_body.ReadByte(); + return _body.ReadByte() == 1; } - public uint ReadMessageExpiryInterval() + public uint ReadSubscriptionIdentifier() { - return _body.ReadFourByteInteger(); + return _body.ReadVariableByteInteger(); } - public ushort ReadTopicAlias() + public bool ReadSubscriptionIdentifiersAvailable() { - return _body.ReadTwoByteInteger(); + return _body.ReadByte() == 1; } - public string ReadResponseTopic() + public ushort ReadTopicAlias() { - return _body.ReadStringWithLengthPrefix(); + return _body.ReadTwoByteInteger(); } - public byte[] ReadCorrelationData() + public ushort ReadTopicAliasMaximum() { - return _body.ReadWithLengthPrefix(); + return _body.ReadTwoByteInteger(); } - public string ReadContentType() + public bool ReadWildcardSubscriptionAvailable() { - return _body.ReadStringWithLengthPrefix(); + return _body.ReadByte() == 1; } public uint ReadWillDelayInterval() @@ -178,14 +202,14 @@ namespace MQTTnet.Formatter.V5 return _body.ReadFourByteInteger(); } - public bool RequestResponseInformation() + public bool RequestProblemInformation() { - return _body.ReadBoolean(); + return _body.ReadByte() == 1; } - public bool RequestProblemInformation() + public bool RequestResponseInformation() { - return _body.ReadBoolean(); + return _body.ReadByte() == 1; } public void ThrowInvalidPropertyIdException(Type type) @@ -193,4 +217,4 @@ namespace MQTTnet.Formatter.V5 throw new MqttProtocolViolationException($"Property ID '{CurrentPropertyId}' is not supported for package type '{type.Name}'."); } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Formatter/V5/MqttV5PropertiesWriter.cs b/Source/MQTTnet/Formatter/V5/MqttV5PropertiesWriter.cs new file mode 100644 index 0000000..3307b66 --- /dev/null +++ b/Source/MQTTnet/Formatter/V5/MqttV5PropertiesWriter.cs @@ -0,0 +1,351 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Formatter.V5 +{ + public sealed class MqttV5PropertiesWriter + { + readonly MqttBufferWriter _bufferWriter; + + public MqttV5PropertiesWriter(MqttBufferWriter bufferWriter) + { + _bufferWriter = bufferWriter ?? throw new ArgumentNullException(nameof(bufferWriter)); + } + + public int Length => _bufferWriter.Length; + + public void Reset() + { + _bufferWriter.Reset(0); + _bufferWriter.Cleanup(); + } + + public void WriteAssignedClientIdentifier(string value) + { + Write(MqttPropertyId.AssignedClientIdentifier, value); + } + + public void WriteAuthenticationData(byte[] value) + { + Write(MqttPropertyId.AuthenticationData, value); + } + + public void WriteAuthenticationMethod(string value) + { + Write(MqttPropertyId.AuthenticationMethod, value); + } + + public void WriteContentType(string value) + { + Write(MqttPropertyId.ContentType, value); + } + + public void WriteCorrelationData(byte[] value) + { + Write(MqttPropertyId.CorrelationData, value); + } + + public void WriteMaximumPacketSize(uint value) + { + // It is a Protocol Error to include the Maximum Packet Size more than once, or for the value to be set to zero. + if (value == 0) + { + return; + } + + WriteAsFourByteInteger(MqttPropertyId.MaximumPacketSize, value); + } + + public void WriteMaximumQoS(MqttQualityOfServiceLevel value) + { + // It is a Protocol Error to include Maximum QoS more than once, or to have a value other than 0 or 1. If the Maximum QoS is absent, the Client uses a Maximum QoS of 2. + if (value == MqttQualityOfServiceLevel.ExactlyOnce) + { + return; + } + + if (value == MqttQualityOfServiceLevel.AtLeastOnce) + { + Write(MqttPropertyId.MaximumQoS, true); + } + else + { + Write(MqttPropertyId.MaximumQoS, false); + } + } + + public void WriteMessageExpiryInterval(uint value) + { + // If absent, the Application Message does not expire. + // This library uses 0 to indicate no expiration. + WriteAsFourByteInteger(MqttPropertyId.MessageExpiryInterval, value); + } + + public void WritePayloadFormatIndicator(MqttPayloadFormatIndicator value) + { + // 0 (0x00) Byte Indicates that the Payload is unspecified bytes, which is equivalent to not sending a Payload Format Indicator. + if (value == MqttPayloadFormatIndicator.Unspecified) + { + return; + } + + Write(MqttPropertyId.PayloadFormatIndicator, (byte)value); + } + + public void WriteReasonString(string value) + { + Write(MqttPropertyId.ReasonString, value); + } + + public void WriteReceiveMaximum(ushort value) + { + // It is a Protocol Error to include the Receive Maximum value more than once or for it to have the value 0. + if (value == 0) + { + return; + } + + Write(MqttPropertyId.ReceiveMaximum, value); + } + + public void WriteRequestProblemInformation(bool value) + { + // If the Request Problem Information is absent, the value of 1 is used. + if (value) + { + return; + } + + Write(MqttPropertyId.RequestProblemInformation, false); + } + + public void WriteRequestResponseInformation(bool value) + { + // If the Request Response Information is absent, the value of 0 is used. + if (!value) + { + return; + } + + Write(MqttPropertyId.RequestResponseInformation, true); + } + + public void WriteResponseInformation(string value) + { + Write(MqttPropertyId.ResponseInformation, value); + } + + public void WriteResponseTopic(string value) + { + Write(MqttPropertyId.ResponseTopic, value); + } + + public void WriteRetainAvailable(bool value) + { + if (value) + { + // Absence of the flag means it is supported! + return; + } + + Write(MqttPropertyId.RetainAvailable, false); + } + + public void WriteServerKeepAlive(ushort value) + { + if (value == 0) + { + return; + } + + Write(MqttPropertyId.ServerKeepAlive, value); + } + + public void WriteServerReference(string value) + { + Write(MqttPropertyId.ServerReference, value); + } + + public void WriteSessionExpiryInterval(uint value) + { + // If the Session Expiry Interval is absent the value 0 is used. + if (value == 0) + { + return; + } + + WriteAsFourByteInteger(MqttPropertyId.SessionExpiryInterval, value); + } + + public void WriteSharedSubscriptionAvailable(bool value) + { + if (value) + { + // Absence of the flag means it is supported! + return; + } + + Write(MqttPropertyId.SharedSubscriptionAvailable, false); + } + + public void WriteSubscriptionIdentifier(uint value) + { + WriteAsVariableByteInteger(MqttPropertyId.SubscriptionIdentifier, value); + } + + public void WriteSubscriptionIdentifiers(ICollection value) + { + if (value == null) + { + return; + } + + foreach (var subscriptionIdentifier in value) + { + WriteAsVariableByteInteger(MqttPropertyId.SubscriptionIdentifier, subscriptionIdentifier); + } + } + + public void WriteSubscriptionIdentifiersAvailable(bool value) + { + if (value) + { + // Absence of the flag means it is supported! + return; + } + + Write(MqttPropertyId.SubscriptionIdentifiersAvailable, false); + } + + public void WriteTo(MqttBufferWriter target) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + target.WriteVariableByteInteger((uint)_bufferWriter.Length); + target.Write(_bufferWriter); + } + + public void WriteTopicAlias(ushort value) + { + // A Topic Alias of 0 is not permitted. A sender MUST NOT send a PUBLISH packet containing a Topic Alias which has the value 0. + if (value == 0) + { + return; + } + + Write(MqttPropertyId.TopicAlias, value); + } + + public void WriteTopicAliasMaximum(ushort value) + { + // If the Topic Alias Maximum property is absent, the default value is 0. + if (value == 0) + { + return; + } + + Write(MqttPropertyId.TopicAliasMaximum, value); + } + + public void WriteUserProperties(List userProperties) + { + if (userProperties == null || userProperties.Count == 0) + { + return; + } + + foreach (var property in userProperties) + { + _bufferWriter.WriteByte((byte)MqttPropertyId.UserProperty); + _bufferWriter.WriteString(property.Name); + _bufferWriter.WriteString(property.Value); + } + } + + public void WriteWildcardSubscriptionAvailable(bool value) + { + // If not present, then Wildcard Subscriptions are supported. + if (value) + { + return; + } + + Write(MqttPropertyId.WildcardSubscriptionAvailable, false); + } + + public void WriteWillDelayInterval(uint value) + { + // If the Will Delay Interval is absent, the default value is 0 and there is no delay before the Will Message is published. + if (value == 0) + { + return; + } + + WriteAsFourByteInteger(MqttPropertyId.WillDelayInterval, value); + } + + void Write(MqttPropertyId id, bool value) + { + _bufferWriter.WriteByte((byte)id); + _bufferWriter.WriteByte(value ? (byte)0x1 : (byte)0x0); + } + + void Write(MqttPropertyId id, byte value) + { + _bufferWriter.WriteByte((byte)id); + _bufferWriter.WriteByte(value); + } + + void Write(MqttPropertyId id, ushort value) + { + _bufferWriter.WriteByte((byte)id); + _bufferWriter.WriteTwoByteInteger(value); + } + + void Write(MqttPropertyId id, string value) + { + if (string.IsNullOrEmpty(value)) + { + return; + } + + _bufferWriter.WriteByte((byte)id); + _bufferWriter.WriteString(value); + } + + void Write(MqttPropertyId id, byte[] value) + { + if (value == null) + { + return; + } + + _bufferWriter.WriteByte((byte)id); + _bufferWriter.WriteBinaryData(value); + } + + void WriteAsFourByteInteger(MqttPropertyId id, uint value) + { + _bufferWriter.WriteByte((byte)id); + _bufferWriter.WriteByte((byte)(value >> 24)); + _bufferWriter.WriteByte((byte)(value >> 16)); + _bufferWriter.WriteByte((byte)(value >> 8)); + _bufferWriter.WriteByte((byte)value); + } + + void WriteAsVariableByteInteger(MqttPropertyId id, uint value) + { + _bufferWriter.WriteByte((byte)id); + _bufferWriter.WriteVariableByteInteger(value); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/IApplicationMessagePublisher.cs b/Source/MQTTnet/IApplicationMessagePublisher.cs deleted file mode 100644 index bf17718..0000000 --- a/Source/MQTTnet/IApplicationMessagePublisher.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using MQTTnet.Client.Publishing; - -namespace MQTTnet -{ - public interface IApplicationMessagePublisher - { - Task PublishAsync(MqttApplicationMessage applicationMessage, CancellationToken cancellationToken); - } -} diff --git a/Source/MQTTnet/IApplicationMessageReceiver.cs b/Source/MQTTnet/IApplicationMessageReceiver.cs deleted file mode 100644 index a44470d..0000000 --- a/Source/MQTTnet/IApplicationMessageReceiver.cs +++ /dev/null @@ -1,13 +0,0 @@ -using MQTTnet.Client.Receiving; - -namespace MQTTnet -{ - public interface IApplicationMessageReceiver - { - /// - /// Gets or sets the application message received handler that is fired every time a new message is received on the client's subscriptions. - /// Hint: Initialize handlers before you connect the client to avoid issues. - /// - IMqttApplicationMessageReceivedHandler ApplicationMessageReceivedHandler { get; set; } - } -} diff --git a/Source/MQTTnet/IMqttFactory.cs b/Source/MQTTnet/IMqttFactory.cs deleted file mode 100644 index 1c65f5c..0000000 --- a/Source/MQTTnet/IMqttFactory.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; -using MQTTnet.Client; -using MQTTnet.Diagnostics; -using MQTTnet.Diagnostics.Logger; -using MQTTnet.Server; - -namespace MQTTnet -{ - public interface IMqttFactory : IMqttClientFactory, IMqttServerFactory - { - IMqttNetLogger DefaultLogger { get; } - - IDictionary Properties { get; } - } -} diff --git a/Source/MQTTnet/Implementations/CrossPlatformSocket.cs b/Source/MQTTnet/Implementations/CrossPlatformSocket.cs index 86e0be1..79aadca 100644 --- a/Source/MQTTnet/Implementations/CrossPlatformSocket.cs +++ b/Source/MQTTnet/Implementations/CrossPlatformSocket.cs @@ -1,3 +1,7 @@ +// 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.IO; using System.Net; @@ -43,10 +47,16 @@ namespace MQTTnet.Implementations // We cannot use the _NoDelay_ property from the socket because there is an issue in .NET 4.5.2, 4.6. // The decompiled code is: this.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.Debug, value ? 1 : 0); // Which is wrong because the "NoDelay" should be set and not "Debug". - get => (int)_socket.GetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay) > 0; + get => (int)_socket.GetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay) != 0; set => _socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, value ? 1 : 0); } + public LingerOption LingerState + { + get => _socket.LingerState; + set => _socket.LingerState = value; + } + public bool DualMode { get => _socket.DualMode; @@ -58,7 +68,7 @@ namespace MQTTnet.Implementations get => _socket.ReceiveBufferSize; set => _socket.ReceiveBufferSize = value; } - + public int SendBufferSize { get => _socket.SendBufferSize; @@ -79,6 +89,8 @@ namespace MQTTnet.Implementations set => _socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, value ? 1 : 0); } + public bool IsConnected => _socket.Connected; + public async Task AcceptAsync() { try @@ -97,9 +109,14 @@ namespace MQTTnet.Implementations } } + public EndPoint LocalEndPoint => _socket.LocalEndPoint; + public void Bind(EndPoint localEndPoint) { - if (localEndPoint is null) throw new ArgumentNullException(nameof(localEndPoint)); + if (localEndPoint is null) + { + throw new ArgumentNullException(nameof(localEndPoint)); + } _socket.Bind(localEndPoint); } @@ -111,24 +128,31 @@ namespace MQTTnet.Implementations public async Task ConnectAsync(string host, int port, CancellationToken cancellationToken) { - if (host is null) throw new ArgumentNullException(nameof(host)); + if (host is null) + { + throw new ArgumentNullException(nameof(host)); + } + + cancellationToken.ThrowIfCancellationRequested(); try { _networkStream?.Dispose(); +#if NET5_0_OR_GREATER + await _socket.ConnectAsync(host, port, cancellationToken).ConfigureAwait(false); +#else // Workaround for: https://github.com/dotnet/corefx/issues/24430 using (cancellationToken.Register(_socketDisposeAction)) { - cancellationToken.ThrowIfCancellationRequested(); - #if NET452 || NET461 await Task.Factory.FromAsync(_socket.BeginConnect, _socket.EndConnect, host, port, null).ConfigureAwait(false); #else await _socket.ConnectAsync(host, port).ConfigureAwait(false); #endif - _networkStream = new NetworkStream(_socket, true); } +#endif + _networkStream = new NetworkStream(_socket, true); } catch (SocketException socketException) { @@ -137,6 +161,11 @@ namespace MQTTnet.Implementations throw new OperationCanceledException(); } + if (socketException.SocketErrorCode == SocketError.TimedOut) + { + throw new MqttCommunicationTimedOutException(); + } + throw new MqttCommunicationException($"Error while connecting with host '{host}:{port}'.", socketException); } catch (ObjectDisposedException) @@ -197,7 +226,7 @@ namespace MQTTnet.Implementations } #if NET452 || NET461 - class SocketWrapper + sealed class SocketWrapper { readonly Socket _socket; readonly ArraySegment _buffer; @@ -224,4 +253,4 @@ namespace MQTTnet.Implementations } #endif } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Implementations/MqttClientAdapterFactory.cs b/Source/MQTTnet/Implementations/MqttClientAdapterFactory.cs index 3d3d8ed..9d2cbd8 100644 --- a/Source/MQTTnet/Implementations/MqttClientAdapterFactory.cs +++ b/Source/MQTTnet/Implementations/MqttClientAdapterFactory.cs @@ -1,23 +1,19 @@ -using MQTTnet.Adapter; -using MQTTnet.Client.Options; +// 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; using MQTTnet.Formatter; using System; using MQTTnet.Channel; -using MQTTnet.Diagnostics.Logger; +using MQTTnet.Client; namespace MQTTnet.Implementations { - public class MqttClientAdapterFactory : IMqttClientAdapterFactory + public sealed class MqttClientAdapterFactory : IMqttClientAdapterFactory { - readonly IMqttNetLogger _logger; - - public MqttClientAdapterFactory(IMqttNetLogger logger) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - public IMqttChannelAdapter CreateClientAdapter(IMqttClientOptions options) + public IMqttChannelAdapter CreateClientAdapter(MqttClientOptions options, MqttPacketInspector packetInspector, IMqttNetLogger logger) { if (options == null) throw new ArgumentNullException(nameof(options)); @@ -42,8 +38,9 @@ namespace MQTTnet.Implementations } } - var packetFormatterAdapter = new MqttPacketFormatterAdapter(options.ProtocolVersion, new MqttPacketWriter()); - return new MqttChannelAdapter(channel, packetFormatterAdapter, options.PacketInspector, _logger); + var bufferWriter = new MqttBufferWriter(options.WriterBufferSize, options.WriterBufferSizeMax); + var packetFormatterAdapter = new MqttPacketFormatterAdapter(options.ProtocolVersion, bufferWriter); + return new MqttChannelAdapter(channel, packetFormatterAdapter, packetInspector, logger); } } } diff --git a/Source/MQTTnet/Implementations/MqttTcpChannel.Uwp.cs b/Source/MQTTnet/Implementations/MqttTcpChannel.Uwp.cs index 6bcb768..c43948f 100644 --- a/Source/MQTTnet/Implementations/MqttTcpChannel.Uwp.cs +++ b/Source/MQTTnet/Implementations/MqttTcpChannel.Uwp.cs @@ -1,3 +1,7 @@ +// 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. + #if WINDOWS_UWP using System; using System.Collections.Generic; @@ -12,7 +16,7 @@ using Windows.Networking; using Windows.Networking.Sockets; using Windows.Security.Cryptography.Certificates; using MQTTnet.Channel; -using MQTTnet.Client.Options; +using MQTTnet.Client; using MQTTnet.Server; namespace MQTTnet.Implementations @@ -26,13 +30,13 @@ namespace MQTTnet.Implementations Stream _readStream; Stream _writeStream; - public MqttTcpChannel(IMqttClientOptions clientOptions) + public MqttTcpChannel(MqttClientOptions clientOptions) { _options = (MqttClientTcpOptions)clientOptions.ChannelOptions; _bufferSize = _options.BufferSize; } - public MqttTcpChannel(StreamSocket socket, X509Certificate2 clientCertificate, IMqttServerOptions serverOptions) + public MqttTcpChannel(StreamSocket socket, X509Certificate2 clientCertificate, MqttServerOptions serverOptions) { _socket = socket ?? throw new ArgumentNullException(nameof(socket)); _bufferSize = serverOptions.DefaultEndpointOptions.BufferSize; diff --git a/Source/MQTTnet/Implementations/MqttTcpChannel.cs b/Source/MQTTnet/Implementations/MqttTcpChannel.cs index 800ab46..f3e8462 100644 --- a/Source/MQTTnet/Implementations/MqttTcpChannel.cs +++ b/Source/MQTTnet/Implementations/MqttTcpChannel.cs @@ -1,24 +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. + #if !WINDOWS_UWP -using MQTTnet.Channel; -using MQTTnet.Client.Options; using System; using System.IO; -using System.Linq; using System.Net.Security; using System.Net.Sockets; using System.Runtime.ExceptionServices; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; +using MQTTnet.Channel; +using MQTTnet.Client; using MQTTnet.Exceptions; namespace MQTTnet.Implementations { public sealed class MqttTcpChannel : IMqttChannel { - readonly IMqttClientOptions _clientOptions; - readonly MqttClientTcpOptions _tcpOptions; + readonly MqttClientOptions _clientOptions; readonly Action _disposeAction; + readonly MqttClientTcpOptions _tcpOptions; Stream _stream; @@ -27,7 +30,7 @@ namespace MQTTnet.Implementations _disposeAction = Dispose; } - public MqttTcpChannel(IMqttClientOptions clientOptions) : this() + public MqttTcpChannel(MqttClientOptions clientOptions) : this() { _clientOptions = clientOptions ?? throw new ArgumentNullException(nameof(clientOptions)); _tcpOptions = (MqttClientTcpOptions)clientOptions.ChannelOptions; @@ -45,12 +48,12 @@ namespace MQTTnet.Implementations ClientCertificate = clientCertificate; } + public X509Certificate2 ClientCertificate { get; } + public string Endpoint { get; private set; } public bool IsSecureConnection { get; } - public X509Certificate2 ClientCertificate { get; } - public async Task ConnectAsync(CancellationToken cancellationToken) { CrossPlatformSocket socket = null; @@ -67,9 +70,14 @@ namespace MQTTnet.Implementations socket.ReceiveBufferSize = _tcpOptions.BufferSize; socket.SendBufferSize = _tcpOptions.BufferSize; - socket.SendTimeout = (int)_clientOptions.CommunicationTimeout.TotalMilliseconds; + socket.SendTimeout = (int)_clientOptions.Timeout.TotalMilliseconds; socket.NoDelay = _tcpOptions.NoDelay; + if (socket.LingerState != null) + { + socket.LingerState = _tcpOptions.LingerState; + } + if (_tcpOptions.DualMode.HasValue) { // It is important to avoid setting the flag if no specific value is set by the user @@ -89,7 +97,7 @@ namespace MQTTnet.Implementations var sslStream = new SslStream(networkStream, false, InternalUserCertificateValidationCallback); try { -#if NETCOREAPP3_1 || NET5_0 +#if NETCOREAPP3_1 || NET5_0_OR_GREATER var sslOptions = new SslClientAuthenticationOptions { ApplicationProtocols = _tcpOptions.TlsOptions.ApplicationProtocols, @@ -101,12 +109,13 @@ namespace MQTTnet.Implementations await sslStream.AuthenticateAsClientAsync(sslOptions, cancellationToken).ConfigureAwait(false); #else - await sslStream.AuthenticateAsClientAsync(_tcpOptions.Server, LoadCertificates(), _tcpOptions.TlsOptions.SslProtocol, !_tcpOptions.TlsOptions.IgnoreCertificateRevocationErrors).ConfigureAwait(false); + await sslStream.AuthenticateAsClientAsync(_tcpOptions.Server, LoadCertificates(), _tcpOptions.TlsOptions.SslProtocol, + !_tcpOptions.TlsOptions.IgnoreCertificateRevocationErrors).ConfigureAwait(false); #endif } catch { -#if NETSTANDARD2_1 || NETCOREAPP3_1 || NET5_0 +#if NETSTANDARD2_1 || NETCOREAPP3_1 || NET5_0_OR_GREATER await sslStream.DisposeAsync().ConfigureAwait(false); #else sslStream.Dispose(); @@ -137,6 +146,30 @@ namespace MQTTnet.Implementations return Task.FromResult(0); } + public void Dispose() + { + // When the stream is disposed it will also close the socket and this will also dispose it. + // So there is no need to dispose the socket again. + // https://stackoverflow.com/questions/3601521/should-i-manually-dispose-the-socket-after-closing-it + try + { +#if !NETSTANDARD1_3 + _stream?.Close(); +#endif + _stream?.Dispose(); + } + catch (ObjectDisposedException) + { + } + catch (NullReferenceException) + { + } + finally + { + _stream = null; + } + } + public async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -219,42 +252,12 @@ namespace MQTTnet.Implementations } } - public void Dispose() - { - // When the stream is disposed it will also close the socket and this will also dispose it. - // So there is no need to dispose the socket again. - // https://stackoverflow.com/questions/3601521/should-i-manually-dispose-the-socket-after-closing-it - try - { - _stream?.Dispose(); - } - catch (ObjectDisposedException) - { - } - catch (NullReferenceException) - { - } - - _stream = null; - } - bool InternalUserCertificateValidationCallback(object sender, X509Certificate x509Certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { - #region OBSOLETE - -#pragma warning disable CS0618 // Type or member is obsolete - var certificateValidationCallback = _tcpOptions?.TlsOptions?.CertificateValidationCallback; -#pragma warning restore CS0618 // Type or member is obsolete - if (certificateValidationCallback != null) - { - return certificateValidationCallback(x509Certificate, chain, sslPolicyErrors, _clientOptions); - } - #endregion - var certificateValidationHandler = _tcpOptions?.TlsOptions?.CertificateValidationHandler; if (certificateValidationHandler != null) { - var context = new MqttClientCertificateValidationCallbackContext + var eventArgs = new MqttClientCertificateValidationEventArgs { Certificate = x509Certificate, Chain = chain, @@ -262,31 +265,10 @@ namespace MQTTnet.Implementations ClientOptions = _tcpOptions }; - return certificateValidationHandler(context); - } - - if (sslPolicyErrors == SslPolicyErrors.None) - { - return true; - } - - if (chain.ChainStatus.Any(c => c.Status == X509ChainStatusFlags.RevocationStatusUnknown || c.Status == X509ChainStatusFlags.Revoked || c.Status == X509ChainStatusFlags.OfflineRevocation)) - { - if (_tcpOptions?.TlsOptions?.IgnoreCertificateRevocationErrors != true) - { - return false; - } - } - - if (chain.ChainStatus.Any(c => c.Status == X509ChainStatusFlags.PartialChain)) - { - if (_tcpOptions?.TlsOptions?.IgnoreCertificateChainErrors != true) - { - return false; - } + return certificateValidationHandler(eventArgs); } - return _tcpOptions?.TlsOptions?.AllowUntrustedCertificates == true; + return sslPolicyErrors == SslPolicyErrors.None; } X509CertificateCollection LoadCertificates() @@ -306,4 +288,4 @@ namespace MQTTnet.Implementations } } } -#endif +#endif \ No newline at end of file diff --git a/Source/MQTTnet/Implementations/MqttTcpServerAdapter.Uwp.cs b/Source/MQTTnet/Implementations/MqttTcpServerAdapter.Uwp.cs index bda7773..3efe840 100644 --- a/Source/MQTTnet/Implementations/MqttTcpServerAdapter.Uwp.cs +++ b/Source/MQTTnet/Implementations/MqttTcpServerAdapter.Uwp.cs @@ -1,38 +1,40 @@ +// 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. + #if WINDOWS_UWP using Windows.Networking.Sockets; using MQTTnet.Adapter; -using MQTTnet.Diagnostics.Logger; using MQTTnet.Formatter; -using MQTTnet.Server; using System; using System.Runtime.InteropServices.WindowsRuntime; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; +using MQTTnet.Server; +using MQTTnet.Diagnostics; namespace MQTTnet.Implementations { public sealed class MqttTcpServerAdapter : IMqttServerAdapter { - readonly MqttNetSourceLogger _logger; - readonly IMqttNetLogger _rootLogger; - - IMqttServerOptions _options; + IMqttNetLogger _rootLogger; + MqttNetSourceLogger _logger; + + MqttServerOptions _options; StreamSocketListener _listener; - public MqttTcpServerAdapter(IMqttNetLogger logger) - { - _rootLogger = logger ?? throw new ArgumentNullException(nameof(logger)); - _logger = logger.WithSource(nameof(MqttTcpServerAdapter)); - } - public Func ClientHandler { get; set; } - public async Task StartAsync(IMqttServerOptions options) + public async Task StartAsync(MqttServerOptions options, IMqttNetLogger logger) { - _options = options ?? throw new ArgumentNullException(nameof(options)); - if (_listener != null) throw new InvalidOperationException("Server is already started."); + if (logger is null) throw new ArgumentNullException(nameof(logger)); + _rootLogger = logger; + _logger = logger.WithSource(nameof(MqttTcpServerAdapter)); + + _options = options ?? throw new ArgumentNullException(nameof(options)); + if (options.DefaultEndpointOptions.IsEnabled) { _listener = new StreamSocketListener(); @@ -42,7 +44,7 @@ namespace MQTTnet.Implementations _listener.Control.KeepAlive = true; _listener.Control.QualityOfService = SocketQualityOfService.LowLatency; _listener.ConnectionReceived += OnConnectionReceivedAsync; - + await _listener.BindServiceNameAsync(options.DefaultEndpointOptions.Port.ToString(), SocketProtectionLevel.PlainSocket); } @@ -88,8 +90,12 @@ namespace MQTTnet.Implementations _logger.Warning(exception, "Unable to convert UWP certificate to X509Certificate2."); } } - - using (var clientAdapter = new MqttChannelAdapter(new MqttTcpChannel(args.Socket, clientCertificate, _options), new MqttPacketFormatterAdapter(new MqttPacketWriter()), null, _rootLogger)) + + var bufferWriter = new MqttBufferWriter(4096, 65535); + var packetFormatterAdapter = new MqttPacketFormatterAdapter(bufferWriter); + var tcpChannel = new MqttTcpChannel(args.Socket, clientCertificate, _options); + + using (var clientAdapter = new MqttChannelAdapter(tcpChannel, packetFormatterAdapter, null, _rootLogger)) { await clientHandler(clientAdapter).ConfigureAwait(false); } @@ -112,7 +118,7 @@ namespace MQTTnet.Implementations args.Socket.Dispose(); } catch (Exception exception) - { + { _logger.Error(exception, "Error while cleaning up client connection."); } } diff --git a/Source/MQTTnet/Implementations/MqttTcpServerAdapter.cs b/Source/MQTTnet/Implementations/MqttTcpServerAdapter.cs index e3d495f..ba1ea40 100644 --- a/Source/MQTTnet/Implementations/MqttTcpServerAdapter.cs +++ b/Source/MQTTnet/Implementations/MqttTcpServerAdapter.cs @@ -1,4 +1,8 @@ -#if !WINDOWS_UWP +// 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. + +#if !WINDOWS_UWP using MQTTnet.Adapter; using MQTTnet.Diagnostics; using MQTTnet.Server; @@ -9,37 +13,31 @@ using System.Net.Sockets; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; -using MQTTnet.Diagnostics.Logger; namespace MQTTnet.Implementations { public sealed class MqttTcpServerAdapter : IMqttServerAdapter { readonly List _listeners = new List(); - readonly MqttNetSourceLogger _logger; - readonly IMqttNetLogger _rootLogger; + MqttServerOptions _serverOptions; CancellationTokenSource _cancellationTokenSource; - - public MqttTcpServerAdapter(IMqttNetLogger logger) - { - _rootLogger = logger ?? throw new ArgumentNullException(nameof(logger)); - _logger = logger.WithSource(nameof(MqttTcpServerAdapter)); - } - + public Func ClientHandler { get; set; } public bool TreatSocketOpeningErrorAsWarning { get; set; } - public Task StartAsync(IMqttServerOptions options) + 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, null, _cancellationTokenSource.Token); + RegisterListeners(options.DefaultEndpointOptions, null, logger, _cancellationTokenSource.Token); } if (options.TlsEndpointOptions?.IsEnabled == true) @@ -55,7 +53,7 @@ namespace MQTTnet.Implementations throw new InvalidOperationException("The certificate for TLS encryption must contain the private key."); } - RegisterListeners(options.TlsEndpointOptions, tlsCertificate, _cancellationTokenSource.Token); + RegisterListeners(options.TlsEndpointOptions, tlsCertificate, logger, _cancellationTokenSource.Token); } return PlatformAbstractionLayer.CompletedTask; @@ -92,11 +90,11 @@ namespace MQTTnet.Implementations } } - void RegisterListeners(MqttServerTcpEndpointBaseOptions options, X509Certificate2 tlsCertificate, CancellationToken cancellationToken) + void RegisterListeners(MqttServerTcpEndpointBaseOptions tcpEndpointOptions, X509Certificate2 tlsCertificate, IMqttNetLogger logger, CancellationToken cancellationToken) { - if (!options.BoundInterNetworkAddress.Equals(IPAddress.None)) + if (!tcpEndpointOptions.BoundInterNetworkAddress.Equals(IPAddress.None)) { - var listenerV4 = new MqttTcpServerListener(AddressFamily.InterNetwork, options, tlsCertificate, _rootLogger) + var listenerV4 = new MqttTcpServerListener(AddressFamily.InterNetwork, _serverOptions, tcpEndpointOptions, tlsCertificate, logger) { ClientHandler = OnClientAcceptedAsync }; @@ -107,9 +105,9 @@ namespace MQTTnet.Implementations } } - if (!options.BoundInterNetworkV6Address.Equals(IPAddress.None)) + if (!tcpEndpointOptions.BoundInterNetworkV6Address.Equals(IPAddress.None)) { - var listenerV6 = new MqttTcpServerListener(AddressFamily.InterNetworkV6, options, tlsCertificate, _rootLogger) + var listenerV6 = new MqttTcpServerListener(AddressFamily.InterNetworkV6, _serverOptions, tcpEndpointOptions, tlsCertificate, logger) { ClientHandler = OnClientAcceptedAsync }; diff --git a/Source/MQTTnet/Implementations/MqttTcpServerListener.cs b/Source/MQTTnet/Implementations/MqttTcpServerListener.cs index 8d5ac31..2f7fb1a 100644 --- a/Source/MQTTnet/Implementations/MqttTcpServerListener.cs +++ b/Source/MQTTnet/Implementations/MqttTcpServerListener.cs @@ -1,4 +1,8 @@ -#if !WINDOWS_UWP +// 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. + +#if !WINDOWS_UWP using MQTTnet.Adapter; using MQTTnet.Diagnostics; using MQTTnet.Formatter; @@ -12,7 +16,6 @@ using System.Net.Sockets; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; -using MQTTnet.Diagnostics.Logger; namespace MQTTnet.Implementations { @@ -21,6 +24,7 @@ namespace MQTTnet.Implementations readonly MqttNetSourceLogger _logger; readonly IMqttNetLogger _rootLogger; readonly AddressFamily _addressFamily; + readonly MqttServerOptions _serverOptions; readonly MqttServerTcpEndpointBaseOptions _options; readonly MqttServerTlsTcpEndpointOptions _tlsOptions; readonly X509Certificate2 _tlsCertificate; @@ -30,12 +34,14 @@ namespace MQTTnet.Implementations public MqttTcpServerListener( AddressFamily addressFamily, - MqttServerTcpEndpointBaseOptions options, + MqttServerOptions serverOptions, + MqttServerTcpEndpointBaseOptions tcpEndpointOptions, X509Certificate2 tlsCertificate, IMqttNetLogger logger) { _addressFamily = addressFamily; - _options = options; + _serverOptions = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions)); + _options = tcpEndpointOptions ?? throw new ArgumentNullException(nameof(tcpEndpointOptions)); _tlsCertificate = tlsCertificate; _rootLogger = logger; _logger = logger.WithSource(nameof(MqttTcpServerListener)); @@ -60,12 +66,11 @@ namespace MQTTnet.Implementations _localEndPoint = new IPEndPoint(boundIp, _options.Port); - _logger.Info("Starting TCP listener for {0} TLS={1}.", _localEndPoint, _tlsCertificate != null); + _logger.Info("Starting TCP listener (Endpoint='{0}', TLS={1}).", _localEndPoint, _tlsCertificate != null); _socket = new CrossPlatformSocket(_addressFamily); // 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; @@ -76,9 +81,22 @@ namespace MQTTnet.Implementations _socket.NoDelay = true; } + if (_options.LingerState != null) + { + _socket.LingerState = _options.LingerState; + } + _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; @@ -179,11 +197,11 @@ namespace MQTTnet.Implementations var clientHandler = ClientHandler; if (clientHandler != null) { - using (var clientAdapter = new MqttChannelAdapter( - new MqttTcpChannel(stream, remoteEndPoint, clientCertificate), - new MqttPacketFormatterAdapter(new MqttPacketWriter()), - null, - _rootLogger)) + 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, null, _rootLogger)) { await clientHandler(clientAdapter).ConfigureAwait(false); } diff --git a/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs b/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs index 8d6915f..7a3b4a9 100644 --- a/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs +++ b/Source/MQTTnet/Implementations/MqttWebSocketChannel.cs @@ -1,5 +1,8 @@ -using MQTTnet.Channel; -using MQTTnet.Client.Options; +// 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.Channel; using MQTTnet.Internal; using System; using System.Net; @@ -7,6 +10,7 @@ using System.Net.WebSockets; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; +using MQTTnet.Client; namespace MQTTnet.Implementations { @@ -172,7 +176,7 @@ namespace MQTTnet.Implementations clientWebSocket.Options.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => { // TODO: Find a way to add client options to same callback. Problem is that they have a different type. - var context = new MqttClientCertificateValidationCallbackContext + var context = new MqttClientCertificateValidationEventArgs { Certificate = certificate, Chain = chain, diff --git a/Source/MQTTnet/Implementations/PlatformAbstractionLayer.cs b/Source/MQTTnet/Implementations/PlatformAbstractionLayer.cs index 12f106d..11f3ecb 100644 --- a/Source/MQTTnet/Implementations/PlatformAbstractionLayer.cs +++ b/Source/MQTTnet/Implementations/PlatformAbstractionLayer.cs @@ -1,4 +1,8 @@ -using System; +// 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.Threading; using System.Threading.Tasks; @@ -16,6 +20,8 @@ namespace MQTTnet.Implementations public static byte[] EmptyByteArray { get; } = Array.Empty(); #endif + public static ArraySegment EmptyByteArraySegment { get; } = new ArraySegment(EmptyByteArray); + public static void Sleep(TimeSpan timeout) { #if !NETSTANDARD1_3 && !WINDOWS_UWP diff --git a/Source/MQTTnet/Internal/AsyncEvent.cs b/Source/MQTTnet/Internal/AsyncEvent.cs new file mode 100644 index 0000000..3edf7a7 --- /dev/null +++ b/Source/MQTTnet/Internal/AsyncEvent.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MQTTnet.Diagnostics; + +namespace MQTTnet.Internal +{ + public sealed class AsyncEvent where TEventArgs : EventArgs + { + readonly List> _handlers = new List>(); + + public bool HasHandlers => _handlers.Count > 0; + + public void AddHandler(Func handler) + { + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } + + _handlers.Add(new AsyncEventInvocator(null, handler)); + } + + public void AddHandler(Action handler) + { + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } + + _handlers.Add(new AsyncEventInvocator(handler, null)); + } + + public async Task InvokeAsync(TEventArgs eventArgs) + { + foreach (var handler in _handlers) + { + await handler.InvokeAsync(eventArgs).ConfigureAwait(false); + } + } + + public void RemoveHandler(Func handler) + { + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } + + _handlers.RemoveAll(h => h.WrapsHandler(handler)); + } + + public void RemoveHandler(Action handler) + { + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } + + _handlers.RemoveAll(h => h.WrapsHandler(handler)); + } + + public async Task TryInvokeAsync(TEventArgs eventArgs, MqttNetSourceLogger logger) + { + if (eventArgs == null) + { + throw new ArgumentNullException(nameof(eventArgs)); + } + + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + try + { + await InvokeAsync(eventArgs).ConfigureAwait(false); + } + catch (Exception exception) + { + logger.Warning(exception, $"Error while invoking event ({typeof(TEventArgs)})."); + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Internal/AsyncEventInvocator.cs b/Source/MQTTnet/Internal/AsyncEventInvocator.cs new file mode 100644 index 0000000..5554183 --- /dev/null +++ b/Source/MQTTnet/Internal/AsyncEventInvocator.cs @@ -0,0 +1,48 @@ +// 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.Threading.Tasks; +using MQTTnet.Implementations; + +namespace MQTTnet.Internal +{ + public readonly struct AsyncEventInvocator + { + readonly Action _handler; + readonly Func _asyncHandler; + + public AsyncEventInvocator(Action handler, Func asyncHandler) + { + _handler = handler; + _asyncHandler = asyncHandler; + } + + public bool WrapsHandler(Action handler) + { + return ReferenceEquals(_handler, handler); + } + + public bool WrapsHandler(Func handler) + { + return ReferenceEquals(_asyncHandler, handler); + } + + public Task InvokeAsync(TEventArgs eventArgs) + { + if (_handler != null) + { + _handler.Invoke(eventArgs); + return PlatformAbstractionLayer.CompletedTask; + } + + if (_asyncHandler != null) + { + return _asyncHandler.Invoke(eventArgs); + } + + throw new InvalidOperationException(); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Internal/AsyncLock.cs b/Source/MQTTnet/Internal/AsyncLock.cs index 6e1cef3..54b533c 100644 --- a/Source/MQTTnet/Internal/AsyncLock.cs +++ b/Source/MQTTnet/Internal/AsyncLock.cs @@ -1,4 +1,8 @@ -using System; +// 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.Threading; using System.Threading.Tasks; diff --git a/Source/MQTTnet/Internal/AsyncQueue.cs b/Source/MQTTnet/Internal/AsyncQueue.cs index 4e8d7a9..5605c0f 100644 --- a/Source/MQTTnet/Internal/AsyncQueue.cs +++ b/Source/MQTTnet/Internal/AsyncQueue.cs @@ -1,4 +1,8 @@ -using System; +// 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.Concurrent; using System.Threading; using System.Threading.Tasks; diff --git a/Source/MQTTnet/Internal/AsyncQueueDequeueResult.cs b/Source/MQTTnet/Internal/AsyncQueueDequeueResult.cs index 3e2b07b..6848e1f 100644 --- a/Source/MQTTnet/Internal/AsyncQueueDequeueResult.cs +++ b/Source/MQTTnet/Internal/AsyncQueueDequeueResult.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Internal +// 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.Internal { public class AsyncQueueDequeueResult { diff --git a/Source/MQTTnet/Internal/AsyncTaskCompletionSource.cs b/Source/MQTTnet/Internal/AsyncTaskCompletionSource.cs new file mode 100644 index 0000000..a77b56d --- /dev/null +++ b/Source/MQTTnet/Internal/AsyncTaskCompletionSource.cs @@ -0,0 +1,75 @@ +// 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.Threading.Tasks; + +namespace MQTTnet.Internal +{ + public sealed class AsyncTaskCompletionSource + { + readonly TaskCompletionSource _taskCompletionSource; + + public AsyncTaskCompletionSource() + { +#if NET452 + _taskCompletionSource = new TaskCompletionSource(); +#else + _taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); +#endif + } + + public Task Task => _taskCompletionSource.Task; + + public bool TrySetCanceled() + { +#if NET452 + // To prevent deadlocks it is required to call the _TrySetCanceled_ method + // from a new thread because the awaiting code will not(!) be executed in + // a new thread automatically (due to await). Furthermore _this_ thread will + // do it. But _this_ thread is also reading incoming packets -> deadlock. + // NET452 does not support RunContinuationsAsynchronously + System.Threading.Tasks.Task.Run(() => _taskCompletionSource.TrySetCanceled()); + return true; +#else + return _taskCompletionSource.TrySetCanceled(); +#endif + } + + public bool TrySetException(Exception exception) + { + if (exception == null) + { + throw new ArgumentNullException(nameof(exception)); + } + +#if NET452 + // To prevent deadlocks it is required to call the _TrySetException_ method + // from a new thread because the awaiting code will not(!) be executed in + // a new thread automatically (due to await). Furthermore _this_ thread will + // do it. But _this_ thread is also reading incoming packets -> deadlock. + // NET452 does not support RunContinuationsAsynchronously + System.Threading.Tasks.Task.Run(() => _taskCompletionSource.TrySetException(exception)); + return true; +#else + return _taskCompletionSource.TrySetException(exception); +#endif + } + + public bool TrySetResult(TResult result) + { +#if NET452 + // To prevent deadlocks it is required to call the _TrySetResult_ method + // from a new thread because the awaiting code will not(!) be executed in + // a new thread automatically (due to await). Furthermore _this_ thread will + // do it. But _this_ thread is also reading incoming packets -> deadlock. + // NET452 does not support RunContinuationsAsynchronously + System.Threading.Tasks.Task.Run(() => _taskCompletionSource.TrySetResult(result)); + return true; +#else + return _taskCompletionSource.TrySetResult(result); +#endif + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Internal/BlockingQueue.cs b/Source/MQTTnet/Internal/BlockingQueue.cs index 7ca5429..b31ed09 100644 --- a/Source/MQTTnet/Internal/BlockingQueue.cs +++ b/Source/MQTTnet/Internal/BlockingQueue.cs @@ -1,4 +1,8 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; using System.Collections.Generic; using System.Threading; diff --git a/Source/MQTTnet/Internal/Disposable.cs b/Source/MQTTnet/Internal/Disposable.cs index be733f2..0adb347 100644 --- a/Source/MQTTnet/Internal/Disposable.cs +++ b/Source/MQTTnet/Internal/Disposable.cs @@ -1,4 +1,8 @@ -using System; +// 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.Internal { diff --git a/Source/MQTTnet/Internal/MqttPacketBus.cs b/Source/MQTTnet/Internal/MqttPacketBus.cs new file mode 100644 index 0000000..e5919c1 --- /dev/null +++ b/Source/MQTTnet/Internal/MqttPacketBus.cs @@ -0,0 +1,140 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Packets; + +namespace MQTTnet.Internal +{ + public sealed class MqttPacketBus : IDisposable + { + readonly LinkedList[] _partitions = + { + new LinkedList(), + new LinkedList(), + new LinkedList() + }; + + readonly SemaphoreSlim _semaphore = new SemaphoreSlim(0); + readonly object _syncRoot = new object(); + + int _activePartition = (int)MqttPacketBusPartition.Health; + + public int ItemsCount + { + get + { + lock (_syncRoot) + { + return _partitions.Sum(p => p.Count); + } + } + } + + public void Clear() + { + lock (_syncRoot) + { + foreach (var partition in _partitions) + { + partition.Clear(); + } + } + } + + public async Task DequeueItemAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + lock (_syncRoot) + { + for (var i = 0; i < 3; i++) + { + MoveActivePartition(); + + if (_partitions[_activePartition].Count > 0) + { + var item = _partitions[_activePartition].First; + _partitions[_activePartition].RemoveFirst(); + return item.Value; + } + } + } + + // No partition contains data so that we have to wait and put + // the worker back to the thread pool. + await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + } + + cancellationToken.ThrowIfCancellationRequested(); + + throw new InvalidOperationException("MqttPacketBus is broken."); + } + + public void Dispose() + { + _semaphore?.Dispose(); + } + + public void DropFirstItem(MqttPacketBusPartition partition) + { + lock (_syncRoot) + { + var partitionInstance = _partitions[(int)partition]; + + if (partitionInstance.Any()) + { + partitionInstance.RemoveFirst(); + } + } + } + + public void EnqueueItem(MqttPacketBusItem item, MqttPacketBusPartition partition) + { + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + lock (_syncRoot) + { + _partitions[(int)partition].AddLast(item); + } + + _semaphore.Release(); + } + + public List ExportPackets(MqttPacketBusPartition partition) + { + lock (_syncRoot) + { + return _partitions[(int)partition].Select(i => i.Packet).ToList(); + } + } + + public int PartitionItemsCount(MqttPacketBusPartition partition) + { + lock (_syncRoot) + { + return _partitions[(int)partition].Count; + } + } + + void MoveActivePartition() + { + if (_activePartition >= _partitions.Length - 1) + { + _activePartition = 0; + } + else + { + _activePartition++; + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Internal/MqttPacketBusItem.cs b/Source/MQTTnet/Internal/MqttPacketBusItem.cs new file mode 100644 index 0000000..9122453 --- /dev/null +++ b/Source/MQTTnet/Internal/MqttPacketBusItem.cs @@ -0,0 +1,49 @@ +// 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.Threading.Tasks; +using MQTTnet.Packets; + +namespace MQTTnet.Internal +{ + public sealed class MqttPacketBusItem + { + readonly AsyncTaskCompletionSource _promise = new AsyncTaskCompletionSource(); + + public MqttPacketBusItem(MqttPacket packet) + { + Packet = packet ?? throw new ArgumentNullException(nameof(packet)); + } + + public MqttPacket Packet { get; } + + public event EventHandler Delivered; + + public Task WaitForDeliveryAsync() + { + return _promise.Task; + } + + public void MarkAsDelivered() + { + if (_promise.TrySetResult(0)) + { + Delivered?.Invoke(this, EventArgs.Empty); + } + } + + public void MarkAsFailed(Exception exception) + { + if (exception == null) throw new ArgumentNullException(nameof(exception)); + + _promise.TrySetException(exception); + } + + public void MarkAsCancelled() + { + _promise.TrySetCanceled(); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Internal/MqttPacketBusPartition.cs b/Source/MQTTnet/Internal/MqttPacketBusPartition.cs new file mode 100644 index 0000000..d870039 --- /dev/null +++ b/Source/MQTTnet/Internal/MqttPacketBusPartition.cs @@ -0,0 +1,15 @@ +// 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.Internal +{ + public enum MqttPacketBusPartition + { + Data, + + Control, + + Health + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Internal/MqttTaskTimeout.cs b/Source/MQTTnet/Internal/MqttTaskTimeout.cs index ba4ec7f..abd05c3 100644 --- a/Source/MQTTnet/Internal/MqttTaskTimeout.cs +++ b/Source/MQTTnet/Internal/MqttTaskTimeout.cs @@ -1,4 +1,8 @@ -using System; +// 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.Threading; using System.Threading.Tasks; using MQTTnet.Exceptions; @@ -30,29 +34,5 @@ namespace MQTTnet.Internal } } } - - public static async Task WaitAsync(Func> action, TimeSpan timeout, CancellationToken cancellationToken) - { - if (action == null) throw new ArgumentNullException(nameof(action)); - - using (var timeoutCts = new CancellationTokenSource(timeout)) - using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken)) - { - try - { - return await action(linkedCts.Token).ConfigureAwait(false); - } - catch (OperationCanceledException exception) - { - var timeoutReached = timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested; - if (timeoutReached) - { - throw new MqttCommunicationTimedOutException(exception); - } - - throw; - } - } - } } } diff --git a/Source/MQTTnet/Internal/TaskExtensions.cs b/Source/MQTTnet/Internal/TaskExtensions.cs index b10c3f5..d4c8c4d 100644 --- a/Source/MQTTnet/Internal/TaskExtensions.cs +++ b/Source/MQTTnet/Internal/TaskExtensions.cs @@ -1,6 +1,9 @@ -using MQTTnet.Diagnostics; +// 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; using System.Threading.Tasks; -using MQTTnet.Diagnostics.Logger; namespace MQTTnet.Internal { diff --git a/Source/MQTTnet/Internal/TestMqttChannel.cs b/Source/MQTTnet/Internal/TestMqttChannel.cs index 233a0cb..45c8bd8 100644 --- a/Source/MQTTnet/Internal/TestMqttChannel.cs +++ b/Source/MQTTnet/Internal/TestMqttChannel.cs @@ -1,4 +1,8 @@ -using System.IO; +// 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.IO; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; diff --git a/Source/MQTTnet/LowLevelClient/ILowLevelMqttClient.cs b/Source/MQTTnet/LowLevelClient/ILowLevelMqttClient.cs deleted file mode 100644 index 1734bf4..0000000 --- a/Source/MQTTnet/LowLevelClient/ILowLevelMqttClient.cs +++ /dev/null @@ -1,19 +0,0 @@ -using MQTTnet.Client.Options; -using MQTTnet.Packets; -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace MQTTnet.LowLevelClient -{ - public interface ILowLevelMqttClient : IDisposable - { - Task ConnectAsync(IMqttClientOptions options, CancellationToken cancellationToken); - - Task DisconnectAsync(CancellationToken cancellationToken); - - Task SendAsync(MqttBasePacket packet, CancellationToken cancellationToken); - - Task ReceiveAsync(CancellationToken cancellationToken); - } -} diff --git a/Source/MQTTnet/LowLevelClient/LowLevelMqttClient.cs b/Source/MQTTnet/LowLevelClient/LowLevelMqttClient.cs index 2fa0e7c..3e325c2 100644 --- a/Source/MQTTnet/LowLevelClient/LowLevelMqttClient.cs +++ b/Source/MQTTnet/LowLevelClient/LowLevelMqttClient.cs @@ -1,50 +1,64 @@ -using MQTTnet.Adapter; -using MQTTnet.Client.Options; -using MQTTnet.Diagnostics; -using MQTTnet.Packets; +// 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.Threading; using System.Threading.Tasks; -using MQTTnet.Diagnostics.Logger; +using MQTTnet.Adapter; +using MQTTnet.Client; +using MQTTnet.Diagnostics; +using MQTTnet.Exceptions; +using MQTTnet.Internal; +using MQTTnet.Packets; namespace MQTTnet.LowLevelClient { - public sealed class LowLevelMqttClient : ILowLevelMqttClient + public sealed class LowLevelMqttClient : IDisposable { - readonly MqttNetSourceLogger _logger; readonly IMqttClientAdapterFactory _clientAdapterFactory; + readonly AsyncEvent _inspectPacketEvent = new AsyncEvent(); + readonly MqttNetSourceLogger _logger; + + readonly IMqttNetLogger _rootLogger; IMqttChannelAdapter _adapter; - IMqttClientOptions _options; public LowLevelMqttClient(IMqttClientAdapterFactory clientAdapterFactory, IMqttNetLogger logger) { _clientAdapterFactory = clientAdapterFactory ?? throw new ArgumentNullException(nameof(clientAdapterFactory)); - if (logger is null) throw new ArgumentNullException(nameof(logger)); + _rootLogger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger.WithSource(nameof(LowLevelMqttClient)); } - bool IsConnected => _adapter != null; + public event Func InspectPackage + { + add => _inspectPacketEvent.AddHandler(value); + remove => _inspectPacketEvent.RemoveHandler(value); + } + + public bool IsConnected => _adapter != null; - public async Task ConnectAsync(IMqttClientOptions options, CancellationToken cancellationToken) + public async Task ConnectAsync(MqttClientOptions options, CancellationToken cancellationToken) { - if (options is null) throw new ArgumentNullException(nameof(options)); + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } if (_adapter != null) { throw new InvalidOperationException("Low level MQTT client is already connected. Disconnect first before connecting again."); } - var newAdapter = _clientAdapterFactory.CreateClientAdapter(options); + var newAdapter = _clientAdapterFactory.CreateClientAdapter(options, new MqttPacketInspector(_inspectPacketEvent, _rootLogger), _rootLogger); try { - _logger.Verbose("Trying to connect with server '{0}' (Timeout={1}).", options.ChannelOptions, options.CommunicationTimeout); - await newAdapter.ConnectAsync(options.CommunicationTimeout, cancellationToken).ConfigureAwait(false); + _logger.Verbose("Trying to connect with server '{0}'.", options.ChannelOptions); + await newAdapter.ConnectAsync(cancellationToken).ConfigureAwait(false); _logger.Verbose("Connection with server established."); - - _options = options; } catch (Exception) { @@ -57,72 +71,77 @@ namespace MQTTnet.LowLevelClient public async Task DisconnectAsync(CancellationToken cancellationToken) { - if (_adapter == null) - { - return; - } - - await SafeDisconnect(cancellationToken).ConfigureAwait(false); - _adapter = null; - } - - public async Task SendAsync(MqttBasePacket packet, CancellationToken cancellationToken) - { - if (packet is null) throw new ArgumentNullException(nameof(packet)); - - if (_adapter == null) + var adapter = _adapter; + if (adapter == null) { throw new InvalidOperationException("Low level MQTT client is not connected."); } try { - await _adapter.SendPacketAsync(packet, cancellationToken).ConfigureAwait(false); + await adapter.DisconnectAsync(cancellationToken).ConfigureAwait(false); } - catch (Exception) + catch { - await SafeDisconnect(cancellationToken).ConfigureAwait(false); + Dispose(); throw; } } - public async Task ReceiveAsync(CancellationToken cancellationToken) + public void Dispose() + { + _adapter?.Dispose(); + _adapter = null; + } + + public async Task ReceiveAsync(CancellationToken cancellationToken) { - if (_adapter == null) + var adapter = _adapter; + if (adapter == null) { throw new InvalidOperationException("Low level MQTT client is not connected."); } try { - return await _adapter.ReceivePacketAsync(cancellationToken).ConfigureAwait(false); + var receivedPacket = await adapter.ReceivePacketAsync(cancellationToken).ConfigureAwait(false); + if (receivedPacket == null) + { + // Graceful socket close. + throw new MqttCommunicationException("The connection is closed."); + } + + return receivedPacket; } - catch (Exception) + catch { - await SafeDisconnect(cancellationToken).ConfigureAwait(false); + Dispose(); throw; } } - public void Dispose() + public async Task SendAsync(MqttPacket packet, CancellationToken cancellationToken) { - _adapter?.Dispose(); - } + if (packet is null) + { + throw new ArgumentNullException(nameof(packet)); + } - async Task SafeDisconnect(CancellationToken cancellationToken) - { - try + var adapter = _adapter; + if (adapter == null) { - await _adapter.DisconnectAsync(_options.CommunicationTimeout, cancellationToken).ConfigureAwait(false); + throw new InvalidOperationException("Low level MQTT client is not connected."); } - catch (Exception exception) + + try { - _logger.Error(exception, "Error while disconnecting."); + await adapter.SendPacketAsync(packet, cancellationToken).ConfigureAwait(false); } - finally + catch { - _adapter.Dispose(); + Dispose(); + throw; } } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/MQTTnet.csproj b/Source/MQTTnet/MQTTnet.csproj index 8efd872..55304cd 100644 --- a/Source/MQTTnet/MQTTnet.csproj +++ b/Source/MQTTnet/MQTTnet.csproj @@ -1,53 +1,97 @@ - - - - netstandard1.3;netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0 - $(TargetFrameworks);net452;net461 - $(TargetFrameworks);uap10.0 - MQTTnet - MQTTnet - False - - - - - - false - false - true - true - snupkg - - - - false - UAP,Version=v10.0 - UAP - 10.0.18362.0 - 10.0.10240.0 - .NETCore - v5.0 - $(DefineConstants);WINDOWS_UWP - en - $(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets - - - - Full - - - - - - - - - - - - - - - + + + + + + + + @(ReleaseNotes, '%0a') + + + + + netstandard1.3;netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0 + $(TargetFrameworks);net452;net461 + $(TargetFrameworks);uap10.0 + 7.3 + + MQTTnet + MQTTnet + True + The contributors of MQTTnet + MQTTnet + MQTTnet is a high performance .NET library for MQTT based communication. It provides a MQTT client and a MQTT server (broker) and supports v3.1.0, v3.1.1 and v5.0.0 of the MQTT protocol. + The contributors of MQTTnet + MQTTnet + false + false + true + true + snupkg + Christian Kratky 2016-2022 + https://github.com/dotnet/MQTTnet + https://github.com/dotnet/MQTTnet.git + git + 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 + en-US + false + false + nuget.png + true + true + LICENSE + true + + + + false + UAP,Version=v10.0 + UAP + 10.0.18362.0 + 10.0.10240.0 + .NETCore + v5.0 + $(DefineConstants);WINDOWS_UWP + en + $(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets + + + + Full + true + + + + true + false + + + + + True + \ + + + + + + True + \ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Source/MQTTnet/MQTTnet.csproj.DotSettings b/Source/MQTTnet/MQTTnet.csproj.DotSettings new file mode 100644 index 0000000..4af6589 --- /dev/null +++ b/Source/MQTTnet/MQTTnet.csproj.DotSettings @@ -0,0 +1,19 @@ + + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True \ No newline at end of file diff --git a/Source/MQTTnet/MqttApplicationMessage.cs b/Source/MQTTnet/MqttApplicationMessage.cs index d1fe229..ff94737 100644 --- a/Source/MQTTnet/MqttApplicationMessage.cs +++ b/Source/MQTTnet/MqttApplicationMessage.cs @@ -1,112 +1,133 @@ -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; using MQTTnet.Packets; using MQTTnet.Protocol; namespace MQTTnet { - public class MqttApplicationMessage + public sealed class MqttApplicationMessage { /// - /// 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). + /// Gets or sets the content type. + /// The content type must be a UTF-8 encoded string. The content type value identifies the kind of UTF-8 encoded + /// payload. /// - public string Topic { get; set; } + public string ContentType { get; set; } /// - /// Gets or sets the payload. - /// The payload is the data bytes sent via the MQTT protocol. + /// Gets or sets the correlation data. + /// In order for the sender to know what sent message the response refers to it can also send correlation data with the + /// published message. + /// Hint: MQTT 5 feature only. /// - public byte[] Payload { get; set; } + public byte[] CorrelationData { get; set; } /// - /// Gets or sets the quality of service level. - /// The Quality of Service (QoS) level is an agreement between the sender of a message and the receiver of a message that defines the guarantee of delivery for a specific message. - /// There are 3 QoS levels in MQTT: - /// - At most once (0): Message gets delivered no time, once or multiple times. - /// - At least once (1): Message gets delivered at least once (one time or more often). - /// - Exactly once (2): Message gets delivered exactly once (It's ensured that the message only comes once). + /// If the DUP flag is set to 0, it indicates that this is the first occasion that the Client or Server has attempted + /// to send this MQTT PUBLISH Packet. + /// If the DUP flag is set to 1, it indicates that this might be re-delivery of an earlier attempt to send the Packet. + /// The DUP flag MUST be set to 1 by the Client or Server when it attempts to re-deliver a PUBLISH Packet + /// [MQTT-3.3.1.-1]. + /// The DUP flag MUST be set to 0 for all QoS 0 messages [MQTT-3.3.1-2]. /// - public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } + public bool Dup { get; set; } /// - /// Gets or sets a value indicating whether the message should be retained or not. - /// A retained message is a normal MQTT message with the retained flag set to true. - /// The broker stores the last retained message and the corresponding QoS for that topic. + /// Gets or sets the message expiry interval. + /// A client can set the message expiry interval in seconds for each PUBLISH message individually. + /// This interval defines the period of time that the broker stores the PUBLISH message for any matching subscribers + /// that are not currently connected. + /// When no message expiry interval is set, the broker must store the message for matching subscribers indefinitely. + /// When the retained=true option is set on the PUBLISH message, this interval also defines how long a message is + /// retained on a topic. + /// Hint: MQTT 5 feature only. /// - public bool Retain { get; set; } - + public uint MessageExpiryInterval { get; set; } + /// - /// If the DUP flag is set to 0, it indicates that this is the first occasion that the Client or Server has attempted to send this MQTT PUBLISH Packet. - /// If the DUP flag is set to 1, it indicates that this might be re-delivery of an earlier attempt to send the Packet. - /// The DUP flag MUST be set to 1 by the Client or Server when it attempts to re-deliver a PUBLISH Packet [MQTT-3.3.1.-1]. - /// The DUP flag MUST be set to 0 for all QoS 0 messages [MQTT-3.3.1-2]. + /// Gets or sets the payload. + /// The payload is the data bytes sent via the MQTT protocol. /// - public bool Dup { get; set; } + public byte[] Payload { get; set; } /// - /// Gets or sets the user properties. - /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT packet. - /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add metadata to MQTT messages and pass information between publisher, broker, and subscriber. - /// The feature is very similar to the HTTP header concept. - /// Hint: MQTT 5 feature only. + /// Gets or sets the payload format indicator. + /// The payload format indicator is part of any MQTT packet that can contain a payload. The indicator is an optional + /// byte value. + /// A value of 0 indicates an “unspecified byte stream”. + /// A value of 1 indicates a "UTF-8 encoded payload". + /// If no payload format indicator is provided, the default value is 0. + /// Hint: MQTT 5 feature only. /// - public List UserProperties { get; set; } + public MqttPayloadFormatIndicator PayloadFormatIndicator { get; set; } = MqttPayloadFormatIndicator.Unspecified; /// - /// Gets or sets the content type. - /// The content type must be a UTF-8 encoded string. The content type value identifies the kind of UTF-8 encoded payload. + /// Gets or sets the quality of service level. + /// The Quality of Service (QoS) level is an agreement between the sender of a message and the receiver of a message + /// that defines the guarantee of delivery for a specific message. + /// There are 3 QoS levels in MQTT: + /// - At most once (0): Message gets delivered no time, once or multiple times. + /// - At least once (1): Message gets delivered at least once (one time or more often). + /// - Exactly once (2): Message gets delivered exactly once (It's ensured that the message only comes once). /// - public string ContentType { get; set; } + public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } /// - /// Gets or sets the response topic. - /// In MQTT 5 the ability to publish a response topic was added in the publish message which allows you to implement the request/response pattern between clients that is common in web applications. - /// Hint: MQTT 5 feature only. + /// Gets or sets the response topic. + /// In MQTT 5 the ability to publish a response topic was added in the publish message which allows you to implement + /// the request/response pattern between clients that is common in web applications. + /// Hint: MQTT 5 feature only. /// public string ResponseTopic { get; set; } /// - /// Gets or sets the payload format indicator. - /// The payload format indicator is part of any MQTT packet that can contain a payload. The indicator is an optional byte value. - /// A value of 0 indicates an “unspecified byte stream”. - /// A value of 1 indicates a "UTF-8 encoded payload". - /// If no payload format indicator is provided, the default value is 0. - /// Hint: MQTT 5 feature only. + /// Gets or sets a value indicating whether the message should be retained or not. + /// A retained message is a normal MQTT message with the retained flag set to true. + /// The broker stores the last retained message and the corresponding QoS for that topic. /// - public MqttPayloadFormatIndicator? PayloadFormatIndicator { get; set; } + public bool Retain { get; set; } /// - /// Gets or sets the message expiry interval. - /// A client can set the message expiry interval in seconds for each PUBLISH message individually. - /// This interval defines the period of time that the broker stores the PUBLISH message for any matching subscribers that are not currently connected. - /// When no message expiry interval is set, the broker must store the message for matching subscribers indefinitely. - /// When the retained=true option is set on the PUBLISH message, this interval also defines how long a message is retained on a topic. - /// Hint: MQTT 5 feature only. + /// Gets or sets the subscription identifiers. + /// The client can specify a subscription identifier when subscribing. + /// The broker will establish and store the mapping relationship between this subscription and subscription identifier + /// when successfully create or modify subscription. + /// The broker will return the subscription identifier associated with this PUBLISH packet and the PUBLISH packet to + /// the client when need to forward PUBLISH packets matching this subscription to this client. + /// Hint: MQTT 5 feature only. /// - public uint? MessageExpiryInterval { get; set; } + public List SubscriptionIdentifiers { get; set; } /// - /// Gets or sets the topic alias. - /// Topic aliases were introduced are a mechanism for reducing the size of published packets by reducing the size of the topic field. - /// Hint: MQTT 5 feature only. + /// 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). /// - public ushort? TopicAlias { get; set; } + public string Topic { get; set; } /// - /// Gets or sets the correlation data. - /// In order for the sender to know what sent message the response refers to it can also send correlation data with the published message. - /// Hint: MQTT 5 feature only. + /// Gets or sets the topic alias. + /// Topic aliases were introduced are a mechanism for reducing the size of published packets by reducing the size of + /// the topic field. + /// A value of 0 indicates no topic alias is used. + /// Hint: MQTT 5 feature only. /// - public byte[] CorrelationData { get; set; } + public ushort TopicAlias { get; set; } /// - /// Gets or sets the subscription identifiers. - /// The client can specify a subscription identifier when subscribing. - /// The broker will establish and store the mapping relationship between this subscription and subscription identifier when successfully create or modify subscription. - /// The broker will return the subscription identifier associated with this PUBLISH packet and the PUBLISH packet to the client when need to forward PUBLISH packets matching this subscription to this client. - /// Hint: MQTT 5 feature only. + /// Gets or sets the user properties. + /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT + /// packet. + /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add + /// metadata to MQTT messages and pass information between publisher, broker, and subscriber. + /// The feature is very similar to the HTTP header concept. + /// Hint: MQTT 5 feature only. /// - public List SubscriptionIdentifiers { get; set; } + public List UserProperties { get; set; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/MqttApplicationMessageBuilder.cs b/Source/MQTTnet/MqttApplicationMessageBuilder.cs index cc63a29..2f1674e 100644 --- a/Source/MQTTnet/MqttApplicationMessageBuilder.cs +++ b/Source/MQTTnet/MqttApplicationMessageBuilder.cs @@ -1,4 +1,7 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + using System.Collections.Generic; using System.IO; using System.Linq; @@ -9,108 +12,100 @@ using MQTTnet.Protocol; namespace MQTTnet { - public class MqttApplicationMessageBuilder + public sealed class MqttApplicationMessageBuilder { - /// - /// The quality of service level. - /// The Quality of Service (QoS) level is an agreement between the sender of a message and the receiver of a message that defines the guarantee of delivery for a specific message. - /// There are 3 QoS levels in MQTT: - /// - At most once (0): Message gets delivered no time, once or multiple times. - /// - At least once (1): Message gets delivered at least once (one time or more often). - /// - Exactly once (2): Message gets delivered exactly once (It's ensured that the message only comes once). - /// - MqttQualityOfServiceLevel _qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce; - - /// - /// 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). - /// - string _topic; - - /// - /// The payload. - /// The payload is the data bytes sent via the MQTT protocol. - /// - byte[] _payload; - - /// - /// A value indicating whether the message should be retained or not. - /// A retained message is a normal MQTT message with the retained flag set to true. - /// The broker stores the last retained message and the corresponding QoS for that topic. - /// - bool _retain; - - bool _dup; - - /// - /// The content type. - /// The content type must be a UTF-8 encoded string. The content type value identifies the kind of UTF-8 encoded payload. - /// string _contentType; - - /// - /// The response topic. - /// In MQTT 5 the ability to publish a response topic was added in the publish message which allows you to implement the request/response pattern between clients that is common in web applications. - /// Hint: MQTT 5 feature only. - /// + byte[] _correlationData; + uint _messageExpiryInterval; + byte[] _payload; + MqttPayloadFormatIndicator _payloadFormatIndicator; + MqttQualityOfServiceLevel _qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce; string _responseTopic; + bool _retain; + List _subscriptionIdentifiers; + string _topic; + ushort _topicAlias; + List _userProperties; - /// - /// The correlation data. - /// In order for the sender to know what sent message the response refers to it can also send correlation data with the published message. - /// Hint: MQTT 5 feature only. - /// - byte[] _correlationData; + public MqttApplicationMessage Build() + { + if (_topicAlias == 0 && string.IsNullOrEmpty(_topic)) + { + throw new MqttProtocolViolationException("Topic or TopicAlias is not set."); + } - /// - /// The topic alias. - /// Topic aliases were introduced are a mechanism for reducing the size of published packets by reducing the size of the topic field. - /// Hint: MQTT 5 feature only. - /// - ushort? _topicAlias; + var applicationMessage = new MqttApplicationMessage + { + Topic = _topic, + Payload = _payload, + QualityOfServiceLevel = _qualityOfServiceLevel, + Retain = _retain, + ContentType = _contentType, + ResponseTopic = _responseTopic, + CorrelationData = _correlationData, + TopicAlias = _topicAlias, + SubscriptionIdentifiers = _subscriptionIdentifiers, + MessageExpiryInterval = _messageExpiryInterval, + PayloadFormatIndicator = _payloadFormatIndicator, + UserProperties = _userProperties + }; - /// - /// The subscription identifiers. - /// The client can specify a subscription identifier when subscribing. - /// The broker will establish and store the mapping relationship between this subscription and subscription identifier when successfully create or modify subscription. - /// The broker will return the subscription identifier associated with this PUBLISH packet and the PUBLISH packet to the client when need to forward PUBLISH packets matching this subscription to this client. - /// Hint: MQTT 5 feature only. - /// - List _subscriptionIdentifiers; + return applicationMessage; + } /// - /// The message expiry interval. - /// A client can set the message expiry interval in seconds for each PUBLISH message individually. - /// This interval defines the period of time that the broker stores the PUBLISH message for any matching subscribers that are not currently connected. - /// When no message expiry interval is set, the broker must store the message for matching subscribers indefinitely. - /// When the retained=true option is set on the PUBLISH message, this interval also defines how long a message is retained on a topic. - /// Hint: MQTT 5 feature only. + /// Adds the content type to the message. + /// Hint: MQTT 5 feature only. /// - uint? _messageExpiryInterval; + /// + /// A new instance of the + /// + /// class. + /// + public MqttApplicationMessageBuilder WithContentType(string contentType) + { + _contentType = contentType; + return this; + } /// - /// The payload format indicator. - /// The payload format indicator is part of any MQTT packet that can contain a payload. The indicator is an optional byte value. - /// A value of 0 indicates an “unspecified byte stream”. - /// A value of 1 indicates a "UTF-8 encoded payload". - /// If no payload format indicator is provided, the default value is 0. - /// Hint: MQTT 5 feature only. + /// Adds the correlation data to the message. + /// Hint: MQTT 5 feature only. /// - MqttPayloadFormatIndicator? _payloadFormatIndicator; + /// + /// The correlation data. + /// + /// + /// A new instance of the + /// + /// class. + /// + public MqttApplicationMessageBuilder WithCorrelationData(byte[] correlationData) + { + _correlationData = correlationData; + return this; + } /// - /// 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. + /// Adds the message expiry interval in seconds to the message. + /// Hint: MQTT 5 feature only. /// - /// Hint: MQTT 5 feature only. - List _userProperties; - - public MqttApplicationMessageBuilder WithTopic(string topic) + /// + /// The message expiry interval. + /// + /// + /// A new instance of the + /// + /// class. + /// + public MqttApplicationMessageBuilder WithMessageExpiryInterval(uint messageExpiryInterval) { - _topic = topic; + _messageExpiryInterval = messageExpiryInterval; return this; } @@ -168,9 +163,9 @@ namespace MQTTnet { break; } + totalRead += bytesRead; - } - while (totalRead < length); + } while (totalRead < length); } return this; @@ -188,87 +183,55 @@ namespace MQTTnet return this; } - public MqttApplicationMessageBuilder WithQualityOfServiceLevel(MqttQualityOfServiceLevel qualityOfServiceLevel) - { - _qualityOfServiceLevel = qualityOfServiceLevel; - return this; - } - - public MqttApplicationMessageBuilder WithQualityOfServiceLevel(int qualityOfServiceLevel) - { - if (qualityOfServiceLevel < 0 || qualityOfServiceLevel > 2) - { - throw new ArgumentOutOfRangeException(nameof(qualityOfServiceLevel)); - } - - return WithQualityOfServiceLevel((MqttQualityOfServiceLevel)qualityOfServiceLevel); - } - - public MqttApplicationMessageBuilder WithRetainFlag(bool value = true) - { - _retain = value; - return this; - } - - public MqttApplicationMessageBuilder WithDupFlag(bool value = true) - { - _dup = value; - return this; - } - - public MqttApplicationMessageBuilder WithAtLeastOnceQoS() - { - _qualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce; - return this; - } - - public MqttApplicationMessageBuilder WithAtMostOnceQoS() - { - _qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce; - return this; - } - - public MqttApplicationMessageBuilder WithExactlyOnceQoS() - { - _qualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce; - return this; - } - /// - /// Adds the user property to the message. - /// Hint: MQTT 5 feature only. + /// Adds the payload format indicator to the message. + /// Hint: MQTT 5 feature only. /// - /// The property name. - /// The property value. - /// A new instance of the class. - public MqttApplicationMessageBuilder WithUserProperty(string name, string value) + /// + /// The payload format indicator. + /// + /// + /// A new instance of the + /// + /// class. + /// + public MqttApplicationMessageBuilder WithPayloadFormatIndicator(MqttPayloadFormatIndicator payloadFormatIndicator) { - if (_userProperties == null) - { - _userProperties = new List(); - } - - _userProperties.Add(new MqttUserProperty(name, value)); + _payloadFormatIndicator = payloadFormatIndicator; return this; } /// - /// Adds the content type to the message. - /// Hint: MQTT 5 feature only. + /// The quality of service level. + /// The Quality of Service (QoS) level is an agreement between the sender of a message and the receiver of a message + /// that defines the guarantee of delivery for a specific message. + /// There are 3 QoS levels in MQTT: + /// - At most once (0): Message gets delivered no time, once or multiple times. + /// - At least once (1): Message gets delivered at least once (one time or more often). + /// - Exactly once (2): Message gets delivered exactly once (It's ensured that the message only comes once). /// - /// A new instance of the class. - public MqttApplicationMessageBuilder WithContentType(string contentType) + public MqttApplicationMessageBuilder WithQualityOfServiceLevel(MqttQualityOfServiceLevel qualityOfServiceLevel) { - _contentType = contentType; + _qualityOfServiceLevel = qualityOfServiceLevel; return this; } /// - /// Adds the response topic to the message. - /// Hint: MQTT 5 feature only. + /// Adds the response topic to the message. + /// Hint: MQTT 5 feature only. /// - /// The response topic. - /// A new instance of the class. + /// + /// The response topic. + /// + /// + /// A new instance of the + /// + /// class. + /// public MqttApplicationMessageBuilder WithResponseTopic(string responseTopic) { _responseTopic = responseTopic; @@ -276,35 +239,30 @@ namespace MQTTnet } /// - /// Adds the correlation data to the message. - /// Hint: MQTT 5 feature only. - /// - /// The correlation data. - /// A new instance of the class. - public MqttApplicationMessageBuilder WithCorrelationData(byte[] correlationData) - { - _correlationData = correlationData; - return this; - } - - /// - /// Adds the topic alias to the message. - /// Hint: MQTT 5 feature only. + /// A value indicating whether the message should be retained or not. + /// A retained message is a normal MQTT message with the retained flag set to true. + /// The broker stores the last retained message and the corresponding QoS for that topic. /// - /// The topic alias. - /// A new instance of the class. - public MqttApplicationMessageBuilder WithTopicAlias(ushort topicAlias) + public MqttApplicationMessageBuilder WithRetainFlag(bool value = true) { - _topicAlias = topicAlias; + _retain = value; return this; } /// - /// Adds the subscription identifier to the message. - /// Hint: MQTT 5 feature only. + /// Adds the subscription identifier to the message. + /// Hint: MQTT 5 feature only. /// - /// The subscription identifier. - /// A new instance of the class. + /// + /// The subscription identifier. + /// + /// + /// A new instance of the + /// + /// class. + /// public MqttApplicationMessageBuilder WithSubscriptionIdentifier(uint subscriptionIdentifier) { if (_subscriptionIdentifiers == null) @@ -317,64 +275,65 @@ namespace MQTTnet } /// - /// Adds the message expiry interval in seconds to the message. - /// Hint: MQTT 5 feature only. + /// 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). /// - /// The message expiry interval. - /// A new instance of the class. - public MqttApplicationMessageBuilder WithMessageExpiryInterval(uint messageExpiryInterval) + public MqttApplicationMessageBuilder WithTopic(string topic) { - _messageExpiryInterval = messageExpiryInterval; + _topic = topic; return this; } /// - /// Adds the payload format indicator to the message. - /// Hint: MQTT 5 feature only. + /// Adds the topic alias to the message. + /// Hint: MQTT 5 feature only. /// - /// The payload format indicator. - /// A new instance of the class. - public MqttApplicationMessageBuilder WithPayloadFormatIndicator(MqttPayloadFormatIndicator payloadFormatIndicator) + /// + /// The topic alias. + /// + /// + /// A new instance of the + /// + /// class. + /// + public MqttApplicationMessageBuilder WithTopicAlias(ushort topicAlias) { - _payloadFormatIndicator = payloadFormatIndicator; + _topicAlias = topicAlias; return this; } - public MqttApplicationMessage Build() + /// + /// Adds the user property to the message. + /// Hint: MQTT 5 feature only. + /// + /// + /// The property name. + /// + /// + /// The property value. + /// + /// + /// A new instance of the + /// + /// class. + /// + public MqttApplicationMessageBuilder WithUserProperty(string name, string value) { - if (!_topicAlias.HasValue && string.IsNullOrEmpty(_topic)) - { - throw new MqttProtocolViolationException("Topic or TopicAlias is not set."); - } - - if (_topicAlias == 0) - { - throw new MqttProtocolViolationException("A Topic Alias of 0 is not permitted. A sender MUST NOT send a PUBLISH packet containing a Topic Alias which has the value 0 [MQTT-3.3.2-8]."); - } - - if (_qualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce && _dup) + if (_userProperties == null) { - throw new MqttProtocolViolationException("The DUP flag MUST be set to 0 for all QoS 0 messages [MQTT-3.3.1-2]."); + _userProperties = new List(); } - var applicationMessage = new MqttApplicationMessage - { - Topic = _topic, - Payload = _payload, - QualityOfServiceLevel = _qualityOfServiceLevel, - Retain = _retain, - Dup = _dup, - ContentType = _contentType, - ResponseTopic = _responseTopic, - CorrelationData = _correlationData, - TopicAlias = _topicAlias, - SubscriptionIdentifiers = _subscriptionIdentifiers, - MessageExpiryInterval = _messageExpiryInterval, - PayloadFormatIndicator = _payloadFormatIndicator, - UserProperties = _userProperties - }; - - return applicationMessage; + _userProperties.Add(new MqttUserProperty(name, value)); + return this; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/MqttApplicationMessageExtensions.cs b/Source/MQTTnet/MqttApplicationMessageExtensions.cs index dac9998..1bfd69a 100644 --- a/Source/MQTTnet/MqttApplicationMessageExtensions.cs +++ b/Source/MQTTnet/MqttApplicationMessageExtensions.cs @@ -1,4 +1,8 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; using System.Text; namespace MQTTnet diff --git a/Source/MQTTnet/MqttFactory.cs b/Source/MQTTnet/MqttFactory.cs index 6a03659..17ab896 100644 --- a/Source/MQTTnet/MqttFactory.cs +++ b/Source/MQTTnet/MqttFactory.cs @@ -1,4 +1,8 @@ -using MQTTnet.Adapter; +// 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.Client; using MQTTnet.Implementations; using MQTTnet.LowLevelClient; @@ -6,14 +10,12 @@ using MQTTnet.Server; using System; using System.Collections.Generic; using System.Linq; -using MQTTnet.Client.Options; -using MQTTnet.Client.Subscribing; -using MQTTnet.Client.Unsubscribing; -using MQTTnet.Diagnostics.Logger; +using MQTTnet.Diagnostics; +using MqttClient = MQTTnet.Client.MqttClient; namespace MQTTnet { - public sealed class MqttFactory : IMqttFactory + public sealed class MqttFactory { IMqttClientAdapterFactory _clientAdapterFactory; @@ -24,44 +26,45 @@ namespace MQTTnet public MqttFactory(IMqttNetLogger logger) { DefaultLogger = logger ?? throw new ArgumentNullException(nameof(logger)); - _clientAdapterFactory = new MqttClientAdapterFactory(logger); + + _clientAdapterFactory = new MqttClientAdapterFactory(); } public IMqttNetLogger DefaultLogger { get; } - public IList> DefaultServerAdapters { get; } = new List> + public IList> DefaultServerAdapters { get; } = new List> { - factory => new MqttTcpServerAdapter(factory.DefaultLogger) + factory => new MqttTcpServerAdapter() }; public IDictionary Properties { get; } = new Dictionary(); - public IMqttFactory UseClientAdapterFactory(IMqttClientAdapterFactory clientAdapterFactory) + public MqttFactory UseClientAdapterFactory(IMqttClientAdapterFactory clientAdapterFactory) { _clientAdapterFactory = clientAdapterFactory ?? throw new ArgumentNullException(nameof(clientAdapterFactory)); return this; } - public ILowLevelMqttClient CreateLowLevelMqttClient() + public LowLevelMqttClient CreateLowLevelMqttClient() { return CreateLowLevelMqttClient(DefaultLogger); } - public ILowLevelMqttClient CreateLowLevelMqttClient(IMqttNetLogger logger) + public LowLevelMqttClient CreateLowLevelMqttClient(IMqttNetLogger logger) { if (logger == null) throw new ArgumentNullException(nameof(logger)); return new LowLevelMqttClient(_clientAdapterFactory, logger); } - public ILowLevelMqttClient CreateLowLevelMqttClient(IMqttClientAdapterFactory clientAdapterFactory) + public LowLevelMqttClient CreateLowLevelMqttClient(IMqttClientAdapterFactory clientAdapterFactory) { if (clientAdapterFactory == null) throw new ArgumentNullException(nameof(clientAdapterFactory)); return new LowLevelMqttClient(_clientAdapterFactory, DefaultLogger); } - public ILowLevelMqttClient CreateLowLevelMqttClient(IMqttNetLogger logger, IMqttClientAdapterFactory clientAdapterFactory) + public LowLevelMqttClient CreateLowLevelMqttClient(IMqttNetLogger logger, IMqttClientAdapterFactory clientAdapterFactory) { if (logger == null) throw new ArgumentNullException(nameof(logger)); if (clientAdapterFactory == null) throw new ArgumentNullException(nameof(clientAdapterFactory)); @@ -69,26 +72,26 @@ namespace MQTTnet return new LowLevelMqttClient(_clientAdapterFactory, logger); } - public IMqttClient CreateMqttClient() + public MqttClient CreateMqttClient() { return CreateMqttClient(DefaultLogger); } - public IMqttClient CreateMqttClient(IMqttNetLogger logger) + public MqttClient CreateMqttClient(IMqttNetLogger logger) { if (logger == null) throw new ArgumentNullException(nameof(logger)); return new MqttClient(_clientAdapterFactory, logger); } - public IMqttClient CreateMqttClient(IMqttClientAdapterFactory clientAdapterFactory) + public MqttClient CreateMqttClient(IMqttClientAdapterFactory clientAdapterFactory) { if (clientAdapterFactory == null) throw new ArgumentNullException(nameof(clientAdapterFactory)); return new MqttClient(clientAdapterFactory, DefaultLogger); } - public IMqttClient CreateMqttClient(IMqttNetLogger logger, IMqttClientAdapterFactory clientAdapterFactory) + public MqttClient CreateMqttClient(IMqttNetLogger logger, IMqttClientAdapterFactory clientAdapterFactory) { if (logger == null) throw new ArgumentNullException(nameof(logger)); if (clientAdapterFactory == null) throw new ArgumentNullException(nameof(clientAdapterFactory)); @@ -96,42 +99,47 @@ namespace MQTTnet return new MqttClient(clientAdapterFactory, logger); } - public IMqttServer CreateMqttServer() + public MqttServer CreateMqttServer(MqttServerOptions options) { - return CreateMqttServer(DefaultLogger); + return CreateMqttServer(options, DefaultLogger); } - public IMqttServer CreateMqttServer(IMqttNetLogger logger) + 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(serverAdapters, logger); + return CreateMqttServer(options, serverAdapters, logger); } - public IMqttServer CreateMqttServer(IEnumerable serverAdapters, IMqttNetLogger logger) + public MqttServer CreateMqttServer(MqttServerOptions options, IEnumerable serverAdapters, IMqttNetLogger logger) { if (serverAdapters == null) throw new ArgumentNullException(nameof(serverAdapters)); if (logger == null) throw new ArgumentNullException(nameof(logger)); - return new MqttServer(serverAdapters, logger); + return new MqttServer(options, serverAdapters, logger); } - public IMqttServer CreateMqttServer(IEnumerable serverAdapters) + public MqttServer CreateMqttServer(MqttServerOptions options, IEnumerable serverAdapters) { if (serverAdapters == null) throw new ArgumentNullException(nameof(serverAdapters)); - return new MqttServer(serverAdapters, DefaultLogger); + return new MqttServer(options, serverAdapters, DefaultLogger); + } + + public MqttServerOptionsBuilder CreateServerOptionsBuilder() + { + return new MqttServerOptionsBuilder(); } public MqttClientOptionsBuilder CreateClientOptionsBuilder() { return new MqttClientOptionsBuilder(); } - - public MqttServerOptionsBuilder CreateServerOptionsBuilder() + + public MqttClientDisconnectOptionsBuilder CreateClientDisconnectOptionsBuilder() { - return new MqttServerOptionsBuilder(); + return new MqttClientDisconnectOptionsBuilder(); } public MqttClientSubscribeOptionsBuilder CreateSubscribeOptionsBuilder() diff --git a/Source/MQTTnet/MqttTopicFilter.cs b/Source/MQTTnet/MqttTopicFilter.cs deleted file mode 100644 index 214aa32..0000000 --- a/Source/MQTTnet/MqttTopicFilter.cs +++ /dev/null @@ -1,65 +0,0 @@ -using MQTTnet.Protocol; -using System; - -namespace MQTTnet -{ - [Obsolete("Use MqttTopicFilter instead. It is just a renamed version to align with general namings in this lib.")] - public class TopicFilter : MqttTopicFilter - { - } - - // TODO: Consider using struct instead. - public class MqttTopicFilter - { - /// - /// 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). - /// - public string Topic { get; set; } - - /// - /// Gets or sets the quality of service level. - /// The Quality of Service (QoS) level is an agreement between the sender of a message and the receiver of a message that defines the guarantee of delivery for a specific message. - /// There are 3 QoS levels in MQTT: - /// - At most once (0): Message gets delivered no time, once or multiple times. - /// - At least once (1): Message gets delivered at least once (one time or more often). - /// - Exactly once (2): Message gets delivered exactly once (It's ensured that the message only comes once). - /// - public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } - - /// - /// Gets or sets a value indicating whether the sender will not receive its own published application messages. - /// - /// Hint: MQTT 5 feature only. - public bool NoLocal { get; set; } - - /// - /// Gets or sets a value indicating whether messages are retained as published or not. - /// Hint: MQTT 5 feature only. - /// - public bool RetainAsPublished { get; set; } - - /// - /// Gets or sets the retain handling. - /// Hint: MQTT 5 feature only. - /// - public MqttRetainHandling RetainHandling { get; set; } - - public override string ToString() - { - return string.Concat( - "TopicFilter: [Topic=", - Topic, - "] [QualityOfServiceLevel=", - QualityOfServiceLevel, - "] [NoLocal=", - NoLocal, - "] [RetainAsPublished=", - RetainAsPublished, - "] [RetainHandling=", - RetainHandling, - "]"); - } - } -} \ No newline at end of file diff --git a/Source/MQTTnet/MqttTopicFilterBuilder.cs b/Source/MQTTnet/MqttTopicFilterBuilder.cs index 055f655..8163178 100644 --- a/Source/MQTTnet/MqttTopicFilterBuilder.cs +++ b/Source/MQTTnet/MqttTopicFilterBuilder.cs @@ -1,15 +1,15 @@ -using MQTTnet.Exceptions; +// 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.Protocol; using System; +using MQTTnet.Packets; namespace MQTTnet { - [Obsolete("Use MqttTopicFilterBuilder instead. It is just a renamed version to align with general namings in this lib.")] - public class TopicFilterBuilder : MqttTopicFilterBuilder - { - } - - public class MqttTopicFilterBuilder + public sealed class MqttTopicFilterBuilder { /// /// The quality of service level. @@ -60,39 +60,18 @@ namespace MQTTnet _qualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce; return this; } - - [Obsolete("Please use overload with _bool_ instead.")] - public MqttTopicFilterBuilder WithNoLocal(bool? value = true) - { - _noLocal = value == true; - return this; - } public MqttTopicFilterBuilder WithNoLocal(bool value = true) { _noLocal = value; return this; } - - [Obsolete("Please use overload with _bool_ instead.")] - public MqttTopicFilterBuilder WithRetainAsPublished(bool? value = true) - { - _retainAsPublished = value == true; - return this; - } public MqttTopicFilterBuilder WithRetainAsPublished(bool value = true) { _retainAsPublished = value; return this; } - - [Obsolete("Please use overload with _MqttRetainHandling_ instead.")] - public MqttTopicFilterBuilder WithRetainHandling(MqttRetainHandling? value) - { - _retainHandling = value ?? MqttRetainHandling.SendAtSubscribe; - return this; - } public MqttTopicFilterBuilder WithRetainHandling(MqttRetainHandling value) { diff --git a/Source/MQTTnet/MqttTopicFilterCompareResult.cs b/Source/MQTTnet/MqttTopicFilterCompareResult.cs new file mode 100644 index 0000000..5ce59a5 --- /dev/null +++ b/Source/MQTTnet/MqttTopicFilterCompareResult.cs @@ -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. + +namespace MQTTnet +{ + public enum MqttTopicFilterCompareResult + { + NoMatch, + + IsMatch, + + FilterInvalid, + + TopicInvalid + } +} \ No newline at end of file diff --git a/Source/MQTTnet/MqttTopicFilterComparer.cs b/Source/MQTTnet/MqttTopicFilterComparer.cs new file mode 100644 index 0000000..72285a8 --- /dev/null +++ b/Source/MQTTnet/MqttTopicFilterComparer.cs @@ -0,0 +1,178 @@ +// 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 +{ + public static class MqttTopicFilterComparer + { + public const char LevelSeparator = '/'; + public const char MultiLevelWildcard = '#'; + public const char SingleLevelWildcard = '+'; + public const char ReservedTopicPrefix = '$'; + + public static unsafe MqttTopicFilterCompareResult Compare(string topic, string filter) + { + if (string.IsNullOrEmpty(topic)) + { + return MqttTopicFilterCompareResult.TopicInvalid; + } + + if (string.IsNullOrEmpty(filter)) + { + return MqttTopicFilterCompareResult.FilterInvalid; + } + + var filterOffset = 0; + var filterLength = filter.Length; + + var topicOffset = 0; + var topicLength = topic.Length; + + fixed (char* topicPointer = topic) + fixed (char* filterPointer = filter) + { + if (filterLength > topicLength) + { + // It is impossible to create a filter which is longer than the actual topic. + // The only way this can happen is when the last char is a wildcard char. + // sensor/7/temperature >> sensor/7/temperature = Equal + // sensor/+/temperature >> sensor/7/temperature = Equal + // sensor/7/+ >> sensor/7/temperature = Shorter + // sensor/# >> sensor/7/temperature = Shorter + var lastFilterChar = filterPointer[filterLength - 1]; + if (lastFilterChar != MultiLevelWildcard && lastFilterChar != SingleLevelWildcard) + { + return MqttTopicFilterCompareResult.NoMatch; + } + } + + var isMultiLevelFilter = filterPointer[filterLength - 1] == MultiLevelWildcard; + var isReservedTopic = topicPointer[0] == ReservedTopicPrefix; + + if (isReservedTopic && filterLength == 1 && isMultiLevelFilter) + { + // It is not allowed to receive i.e. '$foo/bar' with filter '#'. + return MqttTopicFilterCompareResult.NoMatch; + } + + if (isReservedTopic && filterPointer[0] == SingleLevelWildcard) + { + // It is not allowed to receive i.e. '$SYS/monitor/Clients' with filter '+/monitor/Clients'. + return MqttTopicFilterCompareResult.NoMatch; + } + + if (filterLength == 1 && isMultiLevelFilter) + { + // Filter '#' matches basically everything. + return MqttTopicFilterCompareResult.IsMatch; + } + + // Go through the filter char by char. + while (filterOffset < filterLength && topicOffset < topicLength) + { + // Check if the current char is a multi level wildcard. The char is only allowed + // at the very las position. + if (filterPointer[filterOffset] == MultiLevelWildcard && filterOffset != filterLength - 1) + { + return MqttTopicFilterCompareResult.FilterInvalid; + } + + if (filterPointer[filterOffset] == topicPointer[topicOffset]) + { + if (topicOffset == topicLength - 1) + { + // Check for e.g. "foo" matching "foo/#" + if (filterOffset == filterLength - 3 && filterPointer[filterOffset + 1] == LevelSeparator && isMultiLevelFilter) + { + return MqttTopicFilterCompareResult.IsMatch; + } + + // Check for e.g. "foo/" matching "foo/#" + if (filterOffset == filterLength - 2 && filterPointer[filterOffset] == LevelSeparator && isMultiLevelFilter) + { + return MqttTopicFilterCompareResult.IsMatch; + } + } + + filterOffset++; + topicOffset++; + + // Check if the end was reached and i.e. "foo/bar" matches "foo/bar" + if (filterOffset == filterLength && topicOffset == topicLength) + { + return MqttTopicFilterCompareResult.IsMatch; + } + + var endOfTopic = topicOffset == topicLength; + + if (endOfTopic && filterOffset == filterLength - 1 && filterPointer[filterOffset] == SingleLevelWildcard) + { + if (filterOffset > 0 && filterPointer[filterOffset - 1] != LevelSeparator) + { + return MqttTopicFilterCompareResult.FilterInvalid; + } + + return MqttTopicFilterCompareResult.IsMatch; + } + } + else + { + if (filterPointer[filterOffset] == SingleLevelWildcard) + { + // Check for invalid "+foo" or "a/+foo" subscription + if (filterOffset > 0 && filterPointer[filterOffset - 1] != LevelSeparator) + { + return MqttTopicFilterCompareResult.FilterInvalid; + } + + // Check for bad "foo+" or "foo+/a" subscription + if (filterOffset < filterLength - 1 && filterPointer[filterOffset + 1] != LevelSeparator) + { + return MqttTopicFilterCompareResult.FilterInvalid; + } + + filterOffset++; + while (topicOffset < topicLength && topicPointer[topicOffset] != LevelSeparator) + { + topicOffset++; + } + + if (topicOffset == topicLength && filterOffset == filterLength) + { + return MqttTopicFilterCompareResult.IsMatch; + } + } + else if (filterPointer[filterOffset] == MultiLevelWildcard) + { + if (filterOffset > 0 && filterPointer[filterOffset - 1] != LevelSeparator) + { + return MqttTopicFilterCompareResult.FilterInvalid; + } + + if (filterOffset + 1 != filterLength) + { + return MqttTopicFilterCompareResult.FilterInvalid; + } + + return MqttTopicFilterCompareResult.IsMatch; + } + else + { + // Check for e.g. "foo/bar" matching "foo/+/#". + if (filterOffset > 0 && filterOffset + 2 == filterLength && topicOffset == topicLength && filterPointer[filterOffset - 1] == SingleLevelWildcard && + filterPointer[filterOffset] == LevelSeparator && isMultiLevelFilter) + { + return MqttTopicFilterCompareResult.IsMatch; + } + + return MqttTopicFilterCompareResult.NoMatch; + } + } + } + } + + return MqttTopicFilterCompareResult.NoMatch; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/PacketDispatcher/IMqttPacketAwaitable.cs b/Source/MQTTnet/PacketDispatcher/IMqttPacketAwaitable.cs index 06e8660..a467edb 100644 --- a/Source/MQTTnet/PacketDispatcher/IMqttPacketAwaitable.cs +++ b/Source/MQTTnet/PacketDispatcher/IMqttPacketAwaitable.cs @@ -1,4 +1,8 @@ -using MQTTnet.Packets; +// 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 System; namespace MQTTnet.PacketDispatcher @@ -7,7 +11,7 @@ namespace MQTTnet.PacketDispatcher { MqttPacketAwaitableFilter Filter { get; } - void Complete(MqttBasePacket packet); + void Complete(MqttPacket packet); void Fail(Exception exception); diff --git a/Source/MQTTnet/PacketDispatcher/MqttPacketAwaitable.cs b/Source/MQTTnet/PacketDispatcher/MqttPacketAwaitable.cs index de0df74..895e2b5 100644 --- a/Source/MQTTnet/PacketDispatcher/MqttPacketAwaitable.cs +++ b/Source/MQTTnet/PacketDispatcher/MqttPacketAwaitable.cs @@ -1,14 +1,19 @@ -using MQTTnet.Exceptions; +// 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; using System; using System.Threading; using System.Threading.Tasks; +using MQTTnet.Internal; namespace MQTTnet.PacketDispatcher { - public sealed class MqttPacketAwaitable : IMqttPacketAwaitable where TPacket : MqttBasePacket + public sealed class MqttPacketAwaitable : IMqttPacketAwaitable where TPacket : MqttPacket { - readonly TaskCompletionSource _taskCompletionSource; + readonly AsyncTaskCompletionSource _promise = new AsyncTaskCompletionSource(); readonly MqttPacketDispatcher _owningPacketDispatcher; public MqttPacketAwaitable(ushort packetIdentifier, MqttPacketDispatcher owningPacketDispatcher) @@ -20,70 +25,36 @@ namespace MQTTnet.PacketDispatcher }; _owningPacketDispatcher = owningPacketDispatcher ?? throw new ArgumentNullException(nameof(owningPacketDispatcher)); -#if NET452 - _taskCompletionSource = new TaskCompletionSource(); -#else - _taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); -#endif } public MqttPacketAwaitableFilter Filter { get; } - public async Task WaitOneAsync(TimeSpan timeout) + public async Task WaitOneAsync(CancellationToken cancellationToken) { - using (var timeoutToken = new CancellationTokenSource(timeout)) + using (cancellationToken.Register(() => Fail(new MqttCommunicationTimedOutException()))) { - using (timeoutToken.Token.Register(() => Fail(new MqttCommunicationTimedOutException()))) - { - var packet = await _taskCompletionSource.Task.ConfigureAwait(false); - return (TPacket)packet; - } + var packet = await _promise.Task.ConfigureAwait(false); + return (TPacket)packet; } } - public void Complete(MqttBasePacket packet) + public void Complete(MqttPacket packet) { if (packet == null) throw new ArgumentNullException(nameof(packet)); -#if NET452 - // To prevent deadlocks it is required to call the _TrySetResult_ method - // from a new thread because the awaiting code will not(!) be executed in - // a new thread automatically (due to await). Furthermore _this_ thread will - // do it. But _this_ thread is also reading incoming packets -> deadlock. - // NET452 does not support RunContinuationsAsynchronously - Task.Run(() => _taskCompletionSource.TrySetResult(packet)); -#else - _taskCompletionSource.TrySetResult(packet); -#endif + _promise.TrySetResult(packet); } public void Fail(Exception exception) { if (exception == null) throw new ArgumentNullException(nameof(exception)); -#if NET452 - // To prevent deadlocks it is required to call the _TrySetResult_ method - // from a new thread because the awaiting code will not(!) be executed in - // a new thread automatically (due to await). Furthermore _this_ thread will - // do it. But _this_ thread is also reading incoming packets -> deadlock. - // NET452 does not support RunContinuationsAsynchronously - Task.Run(() => _taskCompletionSource.TrySetException(exception)); -#else - _taskCompletionSource.TrySetException(exception); -#endif + + _promise.TrySetException(exception); } public void Cancel() { -#if NET452 - // To prevent deadlocks it is required to call the _TrySetResult_ method - // from a new thread because the awaiting code will not(!) be executed in - // a new thread automatically (due to await). Furthermore _this_ thread will - // do it. But _this_ thread is also reading incoming packets -> deadlock. - // NET452 does not support RunContinuationsAsynchronously - Task.Run(() => _taskCompletionSource.TrySetCanceled()); -#else - _taskCompletionSource.TrySetCanceled(); -#endif + _promise.TrySetCanceled(); } public void Dispose() diff --git a/Source/MQTTnet/PacketDispatcher/MqttPacketAwaitableFilter.cs b/Source/MQTTnet/PacketDispatcher/MqttPacketAwaitableFilter.cs index c1110db..ab94969 100644 --- a/Source/MQTTnet/PacketDispatcher/MqttPacketAwaitableFilter.cs +++ b/Source/MQTTnet/PacketDispatcher/MqttPacketAwaitableFilter.cs @@ -1,4 +1,8 @@ -using System; +// 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.PacketDispatcher { diff --git a/Source/MQTTnet/PacketDispatcher/MqttPacketDispatcher.cs b/Source/MQTTnet/PacketDispatcher/MqttPacketDispatcher.cs index 2d537c6..dae92f7 100644 --- a/Source/MQTTnet/PacketDispatcher/MqttPacketDispatcher.cs +++ b/Source/MQTTnet/PacketDispatcher/MqttPacketDispatcher.cs @@ -1,3 +1,7 @@ +// 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 System; using System.Collections.Generic; @@ -36,12 +40,12 @@ namespace MQTTnet.PacketDispatcher } } - public bool TryDispatch(MqttBasePacket packet) + public bool TryDispatch(MqttPacket packet) { if (packet == null) throw new ArgumentNullException(nameof(packet)); ushort identifier = 0; - if (packet is IMqttPacketWithIdentifier packetWithIdentifier) + if (packet is MqttPacketWithIdentifier packetWithIdentifier) { identifier = packetWithIdentifier.PacketIdentifier; } @@ -76,7 +80,7 @@ namespace MQTTnet.PacketDispatcher return awaitables.Count > 0; } - public MqttPacketAwaitable AddAwaitable(ushort packetIdentifier) where TResponsePacket : MqttBasePacket + public MqttPacketAwaitable AddAwaitable(ushort packetIdentifier) where TResponsePacket : MqttPacket { var awaitable = new MqttPacketAwaitable(packetIdentifier, this); diff --git a/Source/MQTTnet/Packets/IMqttPacketWithIdentifier.cs b/Source/MQTTnet/Packets/IMqttPacketWithIdentifier.cs deleted file mode 100644 index 5f7f8e9..0000000 --- a/Source/MQTTnet/Packets/IMqttPacketWithIdentifier.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MQTTnet.Packets -{ - public interface IMqttPacketWithIdentifier - { - ushort PacketIdentifier { get; set; } - } -} diff --git a/Source/MQTTnet/Packets/MqttAuthPacket.cs b/Source/MQTTnet/Packets/MqttAuthPacket.cs index a792570..e29c751 100644 --- a/Source/MQTTnet/Packets/MqttAuthPacket.cs +++ b/Source/MQTTnet/Packets/MqttAuthPacket.cs @@ -1,14 +1,23 @@ -using MQTTnet.Protocol; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using MQTTnet.Protocol; namespace MQTTnet.Packets { - /// - /// Added in MQTTv5.0.0. - /// - public sealed class MqttAuthPacket : MqttBasePacket + /// Added in MQTTv5.0.0. + public sealed class MqttAuthPacket : MqttPacket { + public byte[] AuthenticationData { get; set; } + + public string AuthenticationMethod { get; set; } + public MqttAuthenticateReasonCode ReasonCode { get; set; } - public MqttAuthPacketProperties Properties { get; set; } + public string ReasonString { get; set; } + + public List UserProperties { get; set; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttAuthPacketProperties.cs b/Source/MQTTnet/Packets/MqttAuthPacketProperties.cs deleted file mode 100644 index d4910af..0000000 --- a/Source/MQTTnet/Packets/MqttAuthPacketProperties.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; - -namespace MQTTnet.Packets -{ - public sealed class MqttAuthPacketProperties - { - public string AuthenticationMethod { get; set; } - - public byte[] AuthenticationData { get; set; } - - public string ReasonString { get; set; } - - public List UserProperties { get; set; } - } -} diff --git a/Source/MQTTnet/Packets/MqttBasePacket.cs b/Source/MQTTnet/Packets/MqttBasePacket.cs deleted file mode 100644 index d964056..0000000 --- a/Source/MQTTnet/Packets/MqttBasePacket.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace MQTTnet.Packets -{ - public abstract class MqttBasePacket - { - } -} diff --git a/Source/MQTTnet/Packets/MqttConnAckPacket.cs b/Source/MQTTnet/Packets/MqttConnAckPacket.cs index b8db02d..3ae7bc6 100644 --- a/Source/MQTTnet/Packets/MqttConnAckPacket.cs +++ b/Source/MQTTnet/Packets/MqttConnAckPacket.cs @@ -1,29 +1,66 @@ -using MQTTnet.Protocol; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using MQTTnet.Protocol; namespace MQTTnet.Packets { - public sealed class MqttConnAckPacket : MqttBasePacket + public sealed class MqttConnAckPacket : MqttPacket { - public MqttConnectReturnCode ReturnCode { get; set; } - /// - /// Added in MQTT 3.1.1. + /// Added in MQTTv5. /// - public bool IsSessionPresent { get; set; } - + public string AssignedClientIdentifier { get; set; } + + public byte[] AuthenticationData { get; set; } + + public string AuthenticationMethod { get; set; } + /// - /// Added in MQTT 5.0.0. + /// Added in MQTTv3.1.1. /// - public MqttConnectReasonCode ReasonCode { get; set; } + public bool IsSessionPresent { get; set; } + + public uint MaximumPacketSize { get; set; } + + public MqttQualityOfServiceLevel MaximumQoS { get; set; } /// - /// Added in MQTT 5.0.0. + /// Added in MQTTv5. /// - public MqttConnAckPacketProperties Properties { get; set; } = new MqttConnAckPacketProperties(); + public MqttConnectReasonCode ReasonCode { get; set; } + + public string ReasonString { get; set; } + + public ushort ReceiveMaximum { get; set; } + + public string ResponseInformation { get; set; } + + public bool RetainAvailable { get; set; } + + public MqttConnectReturnCode ReturnCode { get; set; } + + public ushort ServerKeepAlive { get; set; } + + public string ServerReference { get; set; } + public uint SessionExpiryInterval { get; set; } + + public bool SharedSubscriptionAvailable { get; set; } + + public bool SubscriptionIdentifiersAvailable { get; set; } + + public ushort TopicAliasMaximum { get; set; } + + public List UserProperties { get; set; } + + public bool WildcardSubscriptionAvailable { get; set; } + public override string ToString() { - return string.Concat("ConnAck: [ReturnCode=", ReturnCode, "] [ReasonCode=", ReasonCode, "] [IsSessionPresent=", IsSessionPresent, "]"); + return $"ConnAck: [ReturnCode={ReturnCode}] [ReasonCode={ReasonCode}] [IsSessionPresent={IsSessionPresent}]"; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttConnAckPacketProperties.cs b/Source/MQTTnet/Packets/MqttConnAckPacketProperties.cs deleted file mode 100644 index 519512b..0000000 --- a/Source/MQTTnet/Packets/MqttConnAckPacketProperties.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Generic; -using MQTTnet.Protocol; - -namespace MQTTnet.Packets -{ - public sealed class MqttConnAckPacketProperties - { - public uint? SessionExpiryInterval { get; set; } - - public ushort? ReceiveMaximum { get; set; } - - public MqttQualityOfServiceLevel? MaximumQoS { get; set; } - - public bool RetainAvailable { get; set; } - - public uint? MaximumPacketSize { get; set; } - - public string AssignedClientIdentifier { get; set; } - - public ushort TopicAliasMaximum { get; set; } - - public string ReasonString { get; set; } - - public List UserProperties { get; set; } - - public bool WildcardSubscriptionAvailable { get; set; } - - public bool SubscriptionIdentifiersAvailable { get; set; } - - public bool SharedSubscriptionAvailable { get; set; } - - public ushort? ServerKeepAlive { get; set; } - - public string ResponseInformation { get; set; } - - public string ServerReference { get; set; } - - public string AuthenticationMethod { get; set; } - - public byte[] AuthenticationData { get; set; } - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttConnectPacket.cs b/Source/MQTTnet/Packets/MqttConnectPacket.cs index 6130262..a543f5b 100644 --- a/Source/MQTTnet/Packets/MqttConnectPacket.cs +++ b/Source/MQTTnet/Packets/MqttConnectPacket.cs @@ -1,25 +1,68 @@ -namespace MQTTnet.Packets +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using MQTTnet.Protocol; + +namespace MQTTnet.Packets { - public sealed class MqttConnectPacket : MqttBasePacket + public sealed class MqttConnectPacket : MqttPacket { + public byte[] AuthenticationData { get; set; } + + public string AuthenticationMethod { get; set; } + + /// + /// Also called "Clean Start" in MQTTv5. + /// + public bool CleanSession { get; set; } + public string ClientId { get; set; } - public string Username { get; set; } + public byte[] WillCorrelationData { get; set; } + + public ushort KeepAlivePeriod { get; set; } + + public uint MaximumPacketSize { get; set; } public byte[] Password { get; set; } - public ushort KeepAlivePeriod { get; set; } + public ushort ReceiveMaximum { get; set; } - // Also called "Clean Start" in MQTTv5. - public bool CleanSession { get; set; } + public bool RequestProblemInformation { get; set; } + + public bool RequestResponseInformation { get; set; } + + public string WillResponseTopic { get; set; } + + public uint SessionExpiryInterval { get; set; } + + public ushort TopicAliasMaximum { get; set; } + + public string Username { get; set; } + + public List UserProperties { get; set; } + + public string WillContentType { get; set; } + + public uint WillDelayInterval { get; set; } + + public bool WillFlag { get; set; } + + public byte[] WillMessage { get; set; } + + public uint WillMessageExpiryInterval { get; set; } + + public MqttPayloadFormatIndicator WillPayloadFormatIndicator { get; set; } = MqttPayloadFormatIndicator.Unspecified; - public MqttApplicationMessage WillMessage { get; set; } + public MqttQualityOfServiceLevel WillQoS { get; set; } = MqttQualityOfServiceLevel.AtMostOnce; - #region Added in MQTTv5.0.0 + public bool WillRetain { get; set; } - public MqttConnectPacketProperties Properties { get; set; } + public string WillTopic { get; set; } - #endregion + public List WillUserProperties { get; set; } public override string ToString() { @@ -30,7 +73,7 @@ passwordText = "****"; } - return string.Concat("Connect: [ClientId=", ClientId, "] [Username=", Username, "] [Password=", passwordText, "] [KeepAlivePeriod=", KeepAlivePeriod, "] [CleanSession=", CleanSession, "]"); + return $"Connect: [ClientId={ClientId}] [Username={Username}] [Password={passwordText}] [KeepAlivePeriod={KeepAlivePeriod}] [CleanSession={CleanSession}]"; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttConnectPacketProperties.cs b/Source/MQTTnet/Packets/MqttConnectPacketProperties.cs deleted file mode 100644 index b9bd81e..0000000 --- a/Source/MQTTnet/Packets/MqttConnectPacketProperties.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; - -namespace MQTTnet.Packets -{ - public sealed class MqttConnectPacketProperties - { - public uint? WillDelayInterval { get; set; } - - public uint? SessionExpiryInterval { get; set; } - - public string AuthenticationMethod { get; set; } - - public byte[] AuthenticationData { get; set; } - - public bool? RequestProblemInformation { get; set; } - - public bool? RequestResponseInformation { get; set; } - - public ushort? ReceiveMaximum { get; set; } - - public ushort? TopicAliasMaximum { get; set; } - - public uint? MaximumPacketSize { get; set; } - - public List UserProperties { get; set; } - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttDisconnectPacket.cs b/Source/MQTTnet/Packets/MqttDisconnectPacket.cs index d28e57b..ea39e11 100644 --- a/Source/MQTTnet/Packets/MqttDisconnectPacket.cs +++ b/Source/MQTTnet/Packets/MqttDisconnectPacket.cs @@ -1,20 +1,42 @@ -using MQTTnet.Protocol; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using MQTTnet.Protocol; namespace MQTTnet.Packets { - public sealed class MqttDisconnectPacket : MqttBasePacket + public sealed class MqttDisconnectPacket : MqttPacket { - #region Added in MQTTv5 + /// + /// Added in MQTTv5. + /// + public MqttDisconnectReasonCode ReasonCode { get; set; } = MqttDisconnectReasonCode.NormalDisconnection; + + /// + /// Added in MQTTv5. + /// + public string ReasonString { get; set; } - public MqttDisconnectReasonCode? ReasonCode { get; set; } + /// + /// Added in MQTTv5. + /// + public string ServerReference { get; set; } - public MqttDisconnectPacketProperties Properties { get; set; } + /// + /// Added in MQTTv5. + /// + public uint SessionExpiryInterval { get; set; } - #endregion + /// + /// Added in MQTTv5. + /// + public List UserProperties { get; set; } public override string ToString() { - return string.Concat("Disconnect: [ReasonCode=", ReasonCode, "]"); + return $"Disconnect: [ReasonCode={ReasonCode}]"; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttDisconnectPacketProperties.cs b/Source/MQTTnet/Packets/MqttDisconnectPacketProperties.cs deleted file mode 100644 index c4f8f79..0000000 --- a/Source/MQTTnet/Packets/MqttDisconnectPacketProperties.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; - -namespace MQTTnet.Packets -{ - public sealed class MqttDisconnectPacketProperties - { - public uint? SessionExpiryInterval { get; set; } - - public string ReasonString { get; set; } - - public List UserProperties { get; set; } - - public string ServerReference { get; set; } - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttPacket.cs b/Source/MQTTnet/Packets/MqttPacket.cs new file mode 100644 index 0000000..9d0af89 --- /dev/null +++ b/Source/MQTTnet/Packets/MqttPacket.cs @@ -0,0 +1,10 @@ +// 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.Packets +{ + public abstract class MqttPacket + { + } +} diff --git a/Source/MQTTnet/Packets/MqttPacketWithIdentifier.cs b/Source/MQTTnet/Packets/MqttPacketWithIdentifier.cs new file mode 100644 index 0000000..99caa7b --- /dev/null +++ b/Source/MQTTnet/Packets/MqttPacketWithIdentifier.cs @@ -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.Packets +{ + public abstract class MqttPacketWithIdentifier : MqttPacket + { + public ushort PacketIdentifier { get; set; } + } +} diff --git a/Source/MQTTnet/Packets/MqttPingReqPacket.cs b/Source/MQTTnet/Packets/MqttPingReqPacket.cs index 18a3a16..9ca4764 100644 --- a/Source/MQTTnet/Packets/MqttPingReqPacket.cs +++ b/Source/MQTTnet/Packets/MqttPingReqPacket.cs @@ -1,6 +1,10 @@ -namespace MQTTnet.Packets +// 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.Packets { - public sealed class MqttPingReqPacket : MqttBasePacket + public sealed class MqttPingReqPacket : MqttPacket { // This is a minor performance improvement. public static readonly MqttPingReqPacket Instance = new MqttPingReqPacket(); diff --git a/Source/MQTTnet/Packets/MqttPingRespPacket.cs b/Source/MQTTnet/Packets/MqttPingRespPacket.cs index 8ddc349..d895061 100644 --- a/Source/MQTTnet/Packets/MqttPingRespPacket.cs +++ b/Source/MQTTnet/Packets/MqttPingRespPacket.cs @@ -1,6 +1,10 @@ -namespace MQTTnet.Packets +// 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.Packets { - public sealed class MqttPingRespPacket : MqttBasePacket + public sealed class MqttPingRespPacket : MqttPacket { // This is a minor performance improvement. public static readonly MqttPingRespPacket Instance = new MqttPingRespPacket(); diff --git a/Source/MQTTnet/Packets/MqttPubAckPacket.cs b/Source/MQTTnet/Packets/MqttPubAckPacket.cs index dbd8d71..97053b3 100644 --- a/Source/MQTTnet/Packets/MqttPubAckPacket.cs +++ b/Source/MQTTnet/Packets/MqttPubAckPacket.cs @@ -1,22 +1,32 @@ -using MQTTnet.Protocol; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using MQTTnet.Protocol; namespace MQTTnet.Packets { - public sealed class MqttPubAckPacket : MqttBasePacket, IMqttPacketWithIdentifier + public sealed class MqttPubAckPacket : MqttPacketWithIdentifier { - public ushort PacketIdentifier { get; set; } - - #region Added in MQTTv5 - - public MqttPubAckReasonCode? ReasonCode { get; set; } + /// + /// Added in MQTTv5. + /// + public MqttPubAckReasonCode ReasonCode { get; set; } = MqttPubAckReasonCode.Success; - public MqttPubAckPacketProperties Properties { get; set; } + /// + /// Added in MQTTv5. + /// + public string ReasonString { get; set; } - #endregion + /// + /// Added in MQTTv5. + /// + public List UserProperties { get; set; } public override string ToString() { - return string.Concat("PubAck: [PacketIdentifier=", PacketIdentifier, "] [ReasonCode=", ReasonCode, "]"); + return $"PubAck: [PacketIdentifier={PacketIdentifier}] [ReasonCode={ReasonCode}]"; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttPubAckPacketProperties.cs b/Source/MQTTnet/Packets/MqttPubAckPacketProperties.cs deleted file mode 100644 index 58cb057..0000000 --- a/Source/MQTTnet/Packets/MqttPubAckPacketProperties.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; - -namespace MQTTnet.Packets -{ - public sealed class MqttPubAckPacketProperties - { - public string ReasonString { get; set; } - - public List UserProperties { get; set; } - } -} diff --git a/Source/MQTTnet/Packets/MqttPubCompPacket.cs b/Source/MQTTnet/Packets/MqttPubCompPacket.cs index 573b0eb..ec4dd83 100644 --- a/Source/MQTTnet/Packets/MqttPubCompPacket.cs +++ b/Source/MQTTnet/Packets/MqttPubCompPacket.cs @@ -1,22 +1,32 @@ -using MQTTnet.Protocol; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using MQTTnet.Protocol; namespace MQTTnet.Packets { - public sealed class MqttPubCompPacket : MqttBasePacket, IMqttPacketWithIdentifier + public sealed class MqttPubCompPacket : MqttPacketWithIdentifier { - public ushort PacketIdentifier { get; set; } - - #region Added in MQTTv5 - - public MqttPubCompReasonCode? ReasonCode { get; set; } + /// + /// Added in MQTTv5. + /// + public MqttPubCompReasonCode ReasonCode { get; set; } = MqttPubCompReasonCode.Success; - public MqttPubCompPacketProperties Properties { get; set; } + /// + /// Added in MQTTv5. + /// + public string ReasonString { get; set; } - #endregion + /// + /// Added in MQTTv5. + /// + public List UserProperties { get; set; } public override string ToString() { - return string.Concat("PubComp: [PacketIdentifier=", PacketIdentifier, "] [ReasonCode=", ReasonCode, "]"); + return $"PubComp: [PacketIdentifier={PacketIdentifier}] [ReasonCode={ReasonCode}]"; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttPubCompPacketProperties.cs b/Source/MQTTnet/Packets/MqttPubCompPacketProperties.cs deleted file mode 100644 index 9d2e1a7..0000000 --- a/Source/MQTTnet/Packets/MqttPubCompPacketProperties.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; - -namespace MQTTnet.Packets -{ - public sealed class MqttPubCompPacketProperties - { - public string ReasonString { get; set; } - - public List UserProperties { get; set; } - } -} diff --git a/Source/MQTTnet/Packets/MqttPubRecPacket.cs b/Source/MQTTnet/Packets/MqttPubRecPacket.cs index 7e704be..3e080e7 100644 --- a/Source/MQTTnet/Packets/MqttPubRecPacket.cs +++ b/Source/MQTTnet/Packets/MqttPubRecPacket.cs @@ -1,22 +1,32 @@ -using MQTTnet.Protocol; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using MQTTnet.Protocol; namespace MQTTnet.Packets { - public sealed class MqttPubRecPacket : MqttBasePacket, IMqttPacketWithIdentifier + public sealed class MqttPubRecPacket : MqttPacketWithIdentifier { - public ushort PacketIdentifier { get; set; } - - #region Added in MQTTv5 - - public MqttPubRecReasonCode? ReasonCode { get; set; } + /// + /// Added in MQTTv5. + /// + public MqttPubRecReasonCode ReasonCode { get; set; } = MqttPubRecReasonCode.Success; - public MqttPubRecPacketProperties Properties { get; set; } + /// + /// Added in MQTTv5. + /// + public string ReasonString { get; set; } - #endregion + /// + /// Added in MQTTv5. + /// + public List UserProperties { get; set; } public override string ToString() { - return string.Concat("PubRec: [PacketIdentifier=", PacketIdentifier, "] [ReasonCode=", ReasonCode, "]"); + return $"PubRec: [PacketIdentifier={PacketIdentifier}] [ReasonCode={ReasonCode}]"; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttPubRecPacketProperties.cs b/Source/MQTTnet/Packets/MqttPubRecPacketProperties.cs deleted file mode 100644 index 2d302e8..0000000 --- a/Source/MQTTnet/Packets/MqttPubRecPacketProperties.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; - -namespace MQTTnet.Packets -{ - public sealed class MqttPubRecPacketProperties - { - public string ReasonString { get; set; } - - public List UserProperties { get; set; } - } -} diff --git a/Source/MQTTnet/Packets/MqttPubRelPacket.cs b/Source/MQTTnet/Packets/MqttPubRelPacket.cs index 810c3ba..9631da3 100644 --- a/Source/MQTTnet/Packets/MqttPubRelPacket.cs +++ b/Source/MQTTnet/Packets/MqttPubRelPacket.cs @@ -1,22 +1,32 @@ -using MQTTnet.Protocol; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using MQTTnet.Protocol; namespace MQTTnet.Packets { - public sealed class MqttPubRelPacket : MqttBasePacket, IMqttPacketWithIdentifier + public sealed class MqttPubRelPacket : MqttPacketWithIdentifier { - public ushort PacketIdentifier { get; set; } - - #region Added in MQTTv5 - - public MqttPubRelReasonCode? ReasonCode { get; set; } + /// + /// Added in MQTTv5. + /// + public MqttPubRelReasonCode ReasonCode { get; set; } = MqttPubRelReasonCode.Success; - public MqttPubRelPacketProperties Properties { get; set; } + /// + /// Added in MQTTv5. + /// + public string ReasonString { get; set; } - #endregion + /// + /// Added in MQTTv5. + /// + public List UserProperties { get; set; } public override string ToString() { - return string.Concat("PubRel: [PacketIdentifier=", PacketIdentifier, "] [ReasonCode=", ReasonCode, "]"); + return $"PubRel: [PacketIdentifier={PacketIdentifier}] [ReasonCode={ReasonCode}]"; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttPubRelPacketProperties.cs b/Source/MQTTnet/Packets/MqttPubRelPacketProperties.cs deleted file mode 100644 index 9cd610b..0000000 --- a/Source/MQTTnet/Packets/MqttPubRelPacketProperties.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; - -namespace MQTTnet.Packets -{ - public sealed class MqttPubRelPacketProperties - { - public string ReasonString { get; set; } - - public List UserProperties { get; set; } - } -} diff --git a/Source/MQTTnet/Packets/MqttPublishPacket.cs b/Source/MQTTnet/Packets/MqttPublishPacket.cs index 9fabdf9..10aad7b 100644 --- a/Source/MQTTnet/Packets/MqttPublishPacket.cs +++ b/Source/MQTTnet/Packets/MqttPublishPacket.cs @@ -1,30 +1,44 @@ -using MQTTnet.Protocol; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using MQTTnet.Protocol; namespace MQTTnet.Packets { - public sealed class MqttPublishPacket : MqttBasePacket, IMqttPacketWithIdentifier + public sealed class MqttPublishPacket : MqttPacketWithIdentifier { - public ushort PacketIdentifier { get; set; } - - public bool Retain { get; set; } + public string ContentType { get; set; } - public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } + public byte[] CorrelationData { get; set; } public bool Dup { get; set; } - public string Topic { get; set; } + public uint MessageExpiryInterval { get; set; } public byte[] Payload { get; set; } - #region Added in MQTTv5 + public MqttPayloadFormatIndicator PayloadFormatIndicator { get; set; } = MqttPayloadFormatIndicator.Unspecified; + + public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } = MqttQualityOfServiceLevel.AtMostOnce; + + public string ResponseTopic { get; set; } + + public bool Retain { get; set; } + + public List SubscriptionIdentifiers { get; set; } + + public string Topic { get; set; } - public MqttPublishPacketProperties Properties { get; set; } + public ushort TopicAlias { get; set; } - #endregion + public List UserProperties { get; set; } public override string ToString() { - return string.Concat("Publish: [Topic=", Topic, "] [Payload.Length=", Payload?.Length, "] [QoSLevel=", QualityOfServiceLevel, "] [Dup=", Dup, "] [Retain=", Retain, "] [PacketIdentifier=", PacketIdentifier, "]"); + return + $"Publish: [Topic={Topic}] [Payload.Length={Payload?.Length}] [QoSLevel={QualityOfServiceLevel}] [Dup={Dup}] [Retain={Retain}] [PacketIdentifier={PacketIdentifier}]"; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttPublishPacketProperties.cs b/Source/MQTTnet/Packets/MqttPublishPacketProperties.cs deleted file mode 100644 index 076edf5..0000000 --- a/Source/MQTTnet/Packets/MqttPublishPacketProperties.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; -using MQTTnet.Protocol; - -namespace MQTTnet.Packets -{ - public sealed class MqttPublishPacketProperties - { - public MqttPayloadFormatIndicator? PayloadFormatIndicator { get; set; } - - public uint? MessageExpiryInterval { get; set; } - - public ushort? TopicAlias { get; set; } - - public string ResponseTopic { get; set; } - - public byte[] CorrelationData { get; set; } - - public List UserProperties { get; set; } - - public List SubscriptionIdentifiers { get; set; } - - public string ContentType { get; set; } - } -} diff --git a/Source/MQTTnet/Packets/MqttSubAckPacket.cs b/Source/MQTTnet/Packets/MqttSubAckPacket.cs index e3848a9..e9887c8 100644 --- a/Source/MQTTnet/Packets/MqttSubAckPacket.cs +++ b/Source/MQTTnet/Packets/MqttSubAckPacket.cs @@ -1,29 +1,35 @@ -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; using System.Linq; using MQTTnet.Protocol; namespace MQTTnet.Packets { - public sealed class MqttSubAckPacket : MqttBasePacket, IMqttPacketWithIdentifier + public sealed class MqttSubAckPacket : MqttPacketWithIdentifier { - public ushort PacketIdentifier { get; set; } - - public List ReturnCodes { get; set; } = new List(); - - #region Added in MQTTv5.0.0 - - public List ReasonCodes { get; } = new List(); + /// + /// Reason Code is used in MQTTv5.0.0 and backward compatible to v.3.1.1. Return Code is used in MQTTv3.1.1 + /// + public List ReasonCodes { get; set; } - public MqttSubAckPacketProperties Properties { get; set; } + /// + /// Added in MQTTv5. + /// + public string ReasonString { get; set; } - #endregion + /// + /// Added in MQTTv5. + /// + public List UserProperties { get; set; } public override string ToString() { - var returnCodesText = string.Join(",", ReturnCodes.Select(f => f.ToString())); var reasonCodesText = string.Join(",", ReasonCodes.Select(f => f.ToString())); - return string.Concat("SubAck: [PacketIdentifier=", PacketIdentifier, "] [ReturnCodes=", returnCodesText, "] [ReasonCode=", reasonCodesText, "]"); + return $"SubAck: [PacketIdentifier={PacketIdentifier}] [ReasonCode={reasonCodesText}]"; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttSubAckPacketProperties.cs b/Source/MQTTnet/Packets/MqttSubAckPacketProperties.cs deleted file mode 100644 index a8a2f57..0000000 --- a/Source/MQTTnet/Packets/MqttSubAckPacketProperties.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; - -namespace MQTTnet.Packets -{ - public sealed class MqttSubAckPacketProperties - { - public string ReasonString { get; set; } - - public List UserProperties { get; set; } - } -} diff --git a/Source/MQTTnet/Packets/MqttSubscribePacket.cs b/Source/MQTTnet/Packets/MqttSubscribePacket.cs index 3743c9c..fdbbaeb 100644 --- a/Source/MQTTnet/Packets/MqttSubscribePacket.cs +++ b/Source/MQTTnet/Packets/MqttSubscribePacket.cs @@ -1,23 +1,30 @@ -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; using System.Linq; namespace MQTTnet.Packets { - public sealed class MqttSubscribePacket : MqttBasePacket, IMqttPacketWithIdentifier + public sealed class MqttSubscribePacket : MqttPacketWithIdentifier { - public ushort PacketIdentifier { get; set; } + /// + /// It is a Protocol Error if the Subscription Identifier has a value of 0. + /// + public uint SubscriptionIdentifier { get; set; } public List TopicFilters { get; set; } = new List(); /// - /// Added in MQTT V5. + /// Added in MQTTv5. /// - public MqttSubscribePacketProperties Properties { get; set; } = new MqttSubscribePacketProperties(); - + public List UserProperties { get; set; } + public override string ToString() { var topicFiltersText = string.Join(",", TopicFilters.Select(f => f.Topic + "@" + f.QualityOfServiceLevel)); - return string.Concat("Subscribe: [PacketIdentifier=", PacketIdentifier, "] [TopicFilters=", topicFiltersText, "]"); + return $"Subscribe: [PacketIdentifier={PacketIdentifier}] [TopicFilters={topicFiltersText}]"; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttSubscribePacketProperties.cs b/Source/MQTTnet/Packets/MqttSubscribePacketProperties.cs deleted file mode 100644 index 8455f89..0000000 --- a/Source/MQTTnet/Packets/MqttSubscribePacketProperties.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; - -namespace MQTTnet.Packets -{ - public sealed class MqttSubscribePacketProperties - { - /// - /// It is a Protocol Error if the Subscription Identifier has a value of 0. - /// - public uint SubscriptionIdentifier { get; set; } - - public List UserProperties { get; set; } = new List(); - } -} diff --git a/Source/MQTTnet/Packets/MqttTopicFilter.cs b/Source/MQTTnet/Packets/MqttTopicFilter.cs new file mode 100644 index 0000000..8b9d806 --- /dev/null +++ b/Source/MQTTnet/Packets/MqttTopicFilter.cs @@ -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.Protocol; + +namespace MQTTnet.Packets +{ + public sealed class MqttTopicFilter + { + /// + /// Gets or sets a value indicating whether the sender will not receive its own published application messages. + /// + /// Hint: MQTT 5 feature only. + public bool NoLocal { get; set; } + + /// + /// Gets or sets the quality of service level. + /// The Quality of Service (QoS) level is an agreement between the sender of a message and the receiver of a message + /// that defines the guarantee of delivery for a specific message. + /// There are 3 QoS levels in MQTT: + /// - At most once (0): Message gets delivered no time, once or multiple times. + /// - At least once (1): Message gets delivered at least once (one time or more often). + /// - Exactly once (2): Message gets delivered exactly once (It's ensured that the message only comes once). + /// + public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } + + /// + /// Gets or sets a value indicating whether messages are retained as published or not. + /// Hint: MQTT 5 feature only. + /// + public bool RetainAsPublished { get; set; } + + /// + /// Gets or sets the retain handling. + /// Hint: MQTT 5 feature only. + /// + public MqttRetainHandling RetainHandling { get; set; } + + /// + /// 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). + /// + public string Topic { get; set; } + + public override string ToString() + { + return + $"TopicFilter: [Topic={Topic}] [QualityOfServiceLevel={QualityOfServiceLevel}] [NoLocal={NoLocal}] [RetainAsPublished={RetainAsPublished}] [RetainHandling={RetainHandling}]"; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttUnsubAckPacket.cs b/Source/MQTTnet/Packets/MqttUnsubAckPacket.cs index 394da48..1d2d32f 100644 --- a/Source/MQTTnet/Packets/MqttUnsubAckPacket.cs +++ b/Source/MQTTnet/Packets/MqttUnsubAckPacket.cs @@ -1,26 +1,39 @@ -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; using System.Linq; using MQTTnet.Protocol; namespace MQTTnet.Packets { - public sealed class MqttUnsubAckPacket : MqttBasePacket, IMqttPacketWithIdentifier + public sealed class MqttUnsubAckPacket : MqttPacketWithIdentifier { - public ushort PacketIdentifier { get; set; } - - #region Added in MQTTv5 - - public MqttUnsubAckPacketProperties Properties { get; set; } + /// + /// Added in MQTTv5. + /// + public List ReasonCodes { get; set; } - public List ReasonCodes { get; set; } = new List(); + /// + /// Added in MQTTv5. + /// + public string ReasonString { get; set; } - #endregion + /// + /// Added in MQTTv5. + /// + public List UserProperties { get; set; } public override string ToString() { - var reasonCodesText = string.Join(",", ReasonCodes.Select(f => f.ToString())); + var reasonCodesText = string.Empty; + if (ReasonCodes != null) + { + reasonCodesText = string.Join(",", ReasonCodes?.Select(f => f.ToString())); + } - return string.Concat("UnsubAck: [PacketIdentifier=", PacketIdentifier, "] [ReasonCodes=", reasonCodesText, "]"); + return $"UnsubAck: [PacketIdentifier={PacketIdentifier}] [ReasonCodes={reasonCodesText}] [ReasonString={ReasonString}]"; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttUnsubAckPacketProperties.cs b/Source/MQTTnet/Packets/MqttUnsubAckPacketProperties.cs deleted file mode 100644 index 89ac074..0000000 --- a/Source/MQTTnet/Packets/MqttUnsubAckPacketProperties.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; - -namespace MQTTnet.Packets -{ - public sealed class MqttUnsubAckPacketProperties - { - public string ReasonString { get; set; } - - public List UserProperties { get; set; } - } -} diff --git a/Source/MQTTnet/Packets/MqttUnsubscribePacket.cs b/Source/MQTTnet/Packets/MqttUnsubscribePacket.cs index 76a9d37..cddb3b4 100644 --- a/Source/MQTTnet/Packets/MqttUnsubscribePacket.cs +++ b/Source/MQTTnet/Packets/MqttUnsubscribePacket.cs @@ -1,23 +1,24 @@ -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; namespace MQTTnet.Packets { - public sealed class MqttUnsubscribePacket : MqttBasePacket, IMqttPacketWithIdentifier + public sealed class MqttUnsubscribePacket : MqttPacketWithIdentifier { - public ushort PacketIdentifier { get; set; } - public List TopicFilters { get; set; } = new List(); - #region Added in MQTTv5 - - public MqttUnsubscribePacketProperties Properties { get; set; } - - #endregion + /// + /// Added in MQTTv5. + /// + public List UserProperties { get; set; } public override string ToString() { var topicFiltersText = string.Join(",", TopicFilters); - return string.Concat("Unsubscribe: [PacketIdentifier=", PacketIdentifier, "] [TopicFilters=", topicFiltersText, "]"); + return $"Unsubscribe: [PacketIdentifier={PacketIdentifier}] [TopicFilters={topicFiltersText}]"; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Packets/MqttUnsubscribePacketProperties.cs b/Source/MQTTnet/Packets/MqttUnsubscribePacketProperties.cs deleted file mode 100644 index d98a4f9..0000000 --- a/Source/MQTTnet/Packets/MqttUnsubscribePacketProperties.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace MQTTnet.Packets -{ - public sealed class MqttUnsubscribePacketProperties - { - public List UserProperties { get; set; } - } -} diff --git a/Source/MQTTnet/Packets/MqttUserProperty.cs b/Source/MQTTnet/Packets/MqttUserProperty.cs index f2d2fa1..f54c821 100644 --- a/Source/MQTTnet/Packets/MqttUserProperty.cs +++ b/Source/MQTTnet/Packets/MqttUserProperty.cs @@ -1,4 +1,8 @@ -using System; +// 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.Packets { @@ -14,11 +18,6 @@ namespace MQTTnet.Packets public string Value { get; } - public override int GetHashCode() - { - return Name.GetHashCode() ^ Value.GetHashCode(); - } - public override bool Equals(object other) { return Equals(other as MqttUserProperty); @@ -36,8 +35,17 @@ namespace MQTTnet.Packets return true; } - return string.Equals(Name, other.Name, StringComparison.Ordinal) && - string.Equals(Value, other.Value, StringComparison.Ordinal); + return string.Equals(Name, other.Name, StringComparison.Ordinal) && string.Equals(Value, other.Value, StringComparison.Ordinal); + } + + public override int GetHashCode() + { + return Name.GetHashCode() ^ Value.GetHashCode(); + } + + public override string ToString() + { + return $"{Name} = {Value}"; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Properties/AssemblyInfo.cs b/Source/MQTTnet/Properties/AssemblyInfo.cs index 1e21222..9722982 100644 --- a/Source/MQTTnet/Properties/AssemblyInfo.cs +++ b/Source/MQTTnet/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ -using System.Runtime.CompilerServices; +// 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. -#if DEBUG -[assembly:InternalsVisibleTo("MQTTnet.Core.Tests")] -#endif diff --git a/Source/MQTTnet/Protocol/MqttAuthenticateReasonCode.cs b/Source/MQTTnet/Protocol/MqttAuthenticateReasonCode.cs index 48d7948..1f59e1d 100644 --- a/Source/MQTTnet/Protocol/MqttAuthenticateReasonCode.cs +++ b/Source/MQTTnet/Protocol/MqttAuthenticateReasonCode.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Protocol +// 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.Protocol { public enum MqttAuthenticateReasonCode { diff --git a/Source/MQTTnet/Protocol/MqttConnectReasonCode.cs b/Source/MQTTnet/Protocol/MqttConnectReasonCode.cs index cad4d77..74977df 100644 --- a/Source/MQTTnet/Protocol/MqttConnectReasonCode.cs +++ b/Source/MQTTnet/Protocol/MqttConnectReasonCode.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Protocol +// 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.Protocol { public enum MqttConnectReasonCode { diff --git a/Source/MQTTnet/Protocol/MqttConnectReasonCodeConverter.cs b/Source/MQTTnet/Protocol/MqttConnectReasonCodeConverter.cs deleted file mode 100644 index d195b68..0000000 --- a/Source/MQTTnet/Protocol/MqttConnectReasonCodeConverter.cs +++ /dev/null @@ -1,91 +0,0 @@ -using MQTTnet.Exceptions; - -namespace MQTTnet.Protocol -{ - public class MqttConnectReasonCodeConverter - { - public MqttConnectReturnCode ToConnectReturnCode(MqttConnectReasonCode reasonCode) - { - switch (reasonCode) - { - case MqttConnectReasonCode.Success: - { - return MqttConnectReturnCode.ConnectionAccepted; - } - - case MqttConnectReasonCode.NotAuthorized: - { - return MqttConnectReturnCode.ConnectionRefusedNotAuthorized; - } - - case MqttConnectReasonCode.BadUserNameOrPassword: - { - return MqttConnectReturnCode.ConnectionRefusedBadUsernameOrPassword; - } - - case MqttConnectReasonCode.ClientIdentifierNotValid: - { - return MqttConnectReturnCode.ConnectionRefusedIdentifierRejected; - } - - case MqttConnectReasonCode.UnsupportedProtocolVersion: - { - return MqttConnectReturnCode.ConnectionRefusedUnacceptableProtocolVersion; - } - - case MqttConnectReasonCode.ServerUnavailable: - case MqttConnectReasonCode.ServerBusy: - case MqttConnectReasonCode.ServerMoved: - { - return MqttConnectReturnCode.ConnectionRefusedServerUnavailable; - } - - default: - { - throw new MqttProtocolViolationException("Unable to convert connect reason code (MQTTv5) to return code (MQTTv3)."); - } - } - } - - public MqttConnectReasonCode ToConnectReasonCode(MqttConnectReturnCode returnCode) - { - switch (returnCode) - { - case MqttConnectReturnCode.ConnectionAccepted: - { - return MqttConnectReasonCode.Success; - } - - case MqttConnectReturnCode.ConnectionRefusedUnacceptableProtocolVersion: - { - return MqttConnectReasonCode.UnsupportedProtocolVersion; - } - - case MqttConnectReturnCode.ConnectionRefusedBadUsernameOrPassword: - { - return MqttConnectReasonCode.BadUserNameOrPassword; - } - - case MqttConnectReturnCode.ConnectionRefusedIdentifierRejected: - { - return MqttConnectReasonCode.ClientIdentifierNotValid; - } - - case MqttConnectReturnCode.ConnectionRefusedServerUnavailable: - { - return MqttConnectReasonCode.ServerUnavailable; - } - - case MqttConnectReturnCode.ConnectionRefusedNotAuthorized: - { - return MqttConnectReasonCode.NotAuthorized; - } - - default: - { - throw new MqttProtocolViolationException("Unable to convert connect reason code (MQTTv5) to return code (MQTTv3)."); - } - } - } - } -} diff --git a/Source/MQTTnet/Protocol/MqttConnectReturnCode.cs b/Source/MQTTnet/Protocol/MqttConnectReturnCode.cs index bffc4dc..0687fe7 100644 --- a/Source/MQTTnet/Protocol/MqttConnectReturnCode.cs +++ b/Source/MQTTnet/Protocol/MqttConnectReturnCode.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Protocol +// 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.Protocol { public enum MqttConnectReturnCode { diff --git a/Source/MQTTnet/Protocol/MqttControlPacketType.cs b/Source/MQTTnet/Protocol/MqttControlPacketType.cs index 12dce63..22b48e0 100644 --- a/Source/MQTTnet/Protocol/MqttControlPacketType.cs +++ b/Source/MQTTnet/Protocol/MqttControlPacketType.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Protocol +// 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.Protocol { public enum MqttControlPacketType { diff --git a/Source/MQTTnet/Protocol/MqttDisconnectReasonCode.cs b/Source/MQTTnet/Protocol/MqttDisconnectReasonCode.cs index c1d2e3d..b3307f7 100644 --- a/Source/MQTTnet/Protocol/MqttDisconnectReasonCode.cs +++ b/Source/MQTTnet/Protocol/MqttDisconnectReasonCode.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Protocol +// 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.Protocol { public enum MqttDisconnectReasonCode { diff --git a/Source/MQTTnet/Protocol/MqttPayloadFormatIndicator.cs b/Source/MQTTnet/Protocol/MqttPayloadFormatIndicator.cs index bf776a1..a4446c4 100644 --- a/Source/MQTTnet/Protocol/MqttPayloadFormatIndicator.cs +++ b/Source/MQTTnet/Protocol/MqttPayloadFormatIndicator.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Protocol +// 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.Protocol { public enum MqttPayloadFormatIndicator { diff --git a/Source/MQTTnet/Protocol/MqttPropertyID.cs b/Source/MQTTnet/Protocol/MqttPropertyId.cs similarity index 78% rename from Source/MQTTnet/Protocol/MqttPropertyID.cs rename to Source/MQTTnet/Protocol/MqttPropertyId.cs index 1fc08ce..bce6c23 100644 --- a/Source/MQTTnet/Protocol/MqttPropertyID.cs +++ b/Source/MQTTnet/Protocol/MqttPropertyId.cs @@ -1,7 +1,13 @@ -namespace MQTTnet.Protocol +// 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.Protocol { public enum MqttPropertyId { + None = 0, + PayloadFormatIndicator = 1, MessageExpiryInterval = 2, ContentType = 3, diff --git a/Source/MQTTnet/Protocol/MqttPubAckReasonCode.cs b/Source/MQTTnet/Protocol/MqttPubAckReasonCode.cs index b0580d6..9eded18 100644 --- a/Source/MQTTnet/Protocol/MqttPubAckReasonCode.cs +++ b/Source/MQTTnet/Protocol/MqttPubAckReasonCode.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Protocol +// 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.Protocol { public enum MqttPubAckReasonCode { diff --git a/Source/MQTTnet/Protocol/MqttPubCompReasonCode.cs b/Source/MQTTnet/Protocol/MqttPubCompReasonCode.cs index 9c23927..464b90a 100644 --- a/Source/MQTTnet/Protocol/MqttPubCompReasonCode.cs +++ b/Source/MQTTnet/Protocol/MqttPubCompReasonCode.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Protocol +// 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.Protocol { public enum MqttPubCompReasonCode { diff --git a/Source/MQTTnet/Protocol/MqttPubRecReasonCode.cs b/Source/MQTTnet/Protocol/MqttPubRecReasonCode.cs index b64eac2..9299b64 100644 --- a/Source/MQTTnet/Protocol/MqttPubRecReasonCode.cs +++ b/Source/MQTTnet/Protocol/MqttPubRecReasonCode.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Protocol +// 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.Protocol { public enum MqttPubRecReasonCode { diff --git a/Source/MQTTnet/Protocol/MqttPubRelReasonCode.cs b/Source/MQTTnet/Protocol/MqttPubRelReasonCode.cs index d71da68..5bca4b4 100644 --- a/Source/MQTTnet/Protocol/MqttPubRelReasonCode.cs +++ b/Source/MQTTnet/Protocol/MqttPubRelReasonCode.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Protocol +// 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.Protocol { public enum MqttPubRelReasonCode { diff --git a/Source/MQTTnet/Protocol/MqttQualityOfServiceLevel.cs b/Source/MQTTnet/Protocol/MqttQualityOfServiceLevel.cs index 579f72e..e4134b7 100644 --- a/Source/MQTTnet/Protocol/MqttQualityOfServiceLevel.cs +++ b/Source/MQTTnet/Protocol/MqttQualityOfServiceLevel.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Protocol +// 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.Protocol { public enum MqttQualityOfServiceLevel { diff --git a/Source/MQTTnet/Protocol/MqttRetainHandling.cs b/Source/MQTTnet/Protocol/MqttRetainHandling.cs index 4f3fb46..7604ea5 100644 --- a/Source/MQTTnet/Protocol/MqttRetainHandling.cs +++ b/Source/MQTTnet/Protocol/MqttRetainHandling.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Protocol +// 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.Protocol { public enum MqttRetainHandling { diff --git a/Source/MQTTnet/Protocol/MqttSubscribeReasonCode.cs b/Source/MQTTnet/Protocol/MqttSubscribeReasonCode.cs index e3f2c0f..48cc7f8 100644 --- a/Source/MQTTnet/Protocol/MqttSubscribeReasonCode.cs +++ b/Source/MQTTnet/Protocol/MqttSubscribeReasonCode.cs @@ -1,11 +1,18 @@ -namespace MQTTnet.Protocol +// 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.Protocol { public enum MqttSubscribeReasonCode { - GrantedQoS0 = 0, - GrantedQoS1 = 1, - GrantedQoS2 = 2, - UnspecifiedError = 128, + // Compatible with MQTTv3.1.1. + GrantedQoS0 = 0x00, + GrantedQoS1 = 0x01, + GrantedQoS2 = 0x02, + UnspecifiedError = 0x80, + + // New in MQTTv5. ImplementationSpecificError = 131, NotAuthorized = 135, TopicFilterInvalid = 143, diff --git a/Source/MQTTnet/Protocol/MqttSubscribeReturnCode.cs b/Source/MQTTnet/Protocol/MqttSubscribeReturnCode.cs index b256962..da569b6 100644 --- a/Source/MQTTnet/Protocol/MqttSubscribeReturnCode.cs +++ b/Source/MQTTnet/Protocol/MqttSubscribeReturnCode.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Protocol +// 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.Protocol { public enum MqttSubscribeReturnCode { diff --git a/Source/MQTTnet/Protocol/MqttTopicValidator.cs b/Source/MQTTnet/Protocol/MqttTopicValidator.cs index b8f0295..f9c9283 100644 --- a/Source/MQTTnet/Protocol/MqttTopicValidator.cs +++ b/Source/MQTTnet/Protocol/MqttTopicValidator.cs @@ -1,4 +1,8 @@ -using System; +// 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.Exceptions; namespace MQTTnet.Protocol @@ -9,17 +13,10 @@ namespace MQTTnet.Protocol { if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); - if (!applicationMessage.TopicAlias.HasValue) + if (applicationMessage.TopicAlias == 0) { ThrowIfInvalid(applicationMessage.Topic); } - else - { - if (applicationMessage.TopicAlias.Value == 0) - { - throw new MqttProtocolViolationException("The topic alias cannot be 0."); - } - } } public static void ThrowIfInvalid(string topic) @@ -50,7 +47,11 @@ namespace MQTTnet.Protocol throw new MqttProtocolViolationException("Topic should not be empty."); } - if (topic.IndexOf("#") != -1 && topic.IndexOf("#") != topic.Length - 1) throw new MqttProtocolViolationException("The character '#' is only allowed as last character"); + var indexOfHash = topic.IndexOf("#", StringComparison.Ordinal); + if (indexOfHash != -1 && indexOfHash != topic.Length - 1) + { + throw new MqttProtocolViolationException("The character '#' is only allowed as last character"); + } } } } diff --git a/Source/MQTTnet/Protocol/MqttUnsubscribeReasonCode.cs b/Source/MQTTnet/Protocol/MqttUnsubscribeReasonCode.cs index af06283..cd85c8e 100644 --- a/Source/MQTTnet/Protocol/MqttUnsubscribeReasonCode.cs +++ b/Source/MQTTnet/Protocol/MqttUnsubscribeReasonCode.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Protocol +// 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.Protocol { public enum MqttUnsubscribeReasonCode { diff --git a/Source/MQTTnet/Server/Events/ApplicationMessageNotConsumedEventArgs.cs b/Source/MQTTnet/Server/Events/ApplicationMessageNotConsumedEventArgs.cs new file mode 100644 index 0000000..28b0e72 --- /dev/null +++ b/Source/MQTTnet/Server/Events/ApplicationMessageNotConsumedEventArgs.cs @@ -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 ApplicationMessageNotConsumedEventArgs : EventArgs + { + /// + /// Gets the application message which was not consumed by any client. + /// + public MqttApplicationMessage ApplicationMessage { get; internal set; } + + /// + /// Gets the ID of the client which has sent the affected application message. + /// + public string SenderId { get; internal set; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/MqttServerClientConnectedEventArgs.cs b/Source/MQTTnet/Server/Events/ClientConnectedEventArgs.cs similarity index 75% rename from Source/MQTTnet/Server/MqttServerClientConnectedEventArgs.cs rename to Source/MQTTnet/Server/Events/ClientConnectedEventArgs.cs index 21e8df3..2a8845e 100644 --- a/Source/MQTTnet/Server/MqttServerClientConnectedEventArgs.cs +++ b/Source/MQTTnet/Server/Events/ClientConnectedEventArgs.cs @@ -1,9 +1,13 @@ -using System; +// 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.Formatter; namespace MQTTnet.Server { - public sealed class MqttServerClientConnectedEventArgs : EventArgs + public sealed class ClientConnectedEventArgs : EventArgs { /// /// Gets the client identifier of the connected client. diff --git a/Source/MQTTnet/Server/MqttServerClientDisconnectedEventArgs.cs b/Source/MQTTnet/Server/Events/ClientDisconnectedEventArgs.cs similarity index 62% rename from Source/MQTTnet/Server/MqttServerClientDisconnectedEventArgs.cs rename to Source/MQTTnet/Server/Events/ClientDisconnectedEventArgs.cs index 77ad076..aeac413 100644 --- a/Source/MQTTnet/Server/MqttServerClientDisconnectedEventArgs.cs +++ b/Source/MQTTnet/Server/Events/ClientDisconnectedEventArgs.cs @@ -1,8 +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. + using System; namespace MQTTnet.Server { - public sealed class MqttServerClientDisconnectedEventArgs : EventArgs + public sealed class ClientDisconnectedEventArgs : EventArgs { /// /// Gets the client identifier. diff --git a/Source/MQTTnet/Server/MqttServerClientSubscribedTopicEventArgs.cs b/Source/MQTTnet/Server/Events/ClientSubscribedTopicEventArgs.cs similarity index 62% rename from Source/MQTTnet/Server/MqttServerClientSubscribedTopicEventArgs.cs rename to Source/MQTTnet/Server/Events/ClientSubscribedTopicEventArgs.cs index 23850b2..3410567 100644 --- a/Source/MQTTnet/Server/MqttServerClientSubscribedTopicEventArgs.cs +++ b/Source/MQTTnet/Server/Events/ClientSubscribedTopicEventArgs.cs @@ -1,8 +1,13 @@ -using System; +// 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 MqttServerClientSubscribedTopicEventArgs : EventArgs + public sealed class ClientSubscribedTopicEventArgs : EventArgs { /// /// Gets the client identifier. diff --git a/Source/MQTTnet/Server/MqttServerClientUnsubscribedTopicEventArgs.cs b/Source/MQTTnet/Server/Events/ClientUnsubscribedTopicEventArgs.cs similarity index 64% rename from Source/MQTTnet/Server/MqttServerClientUnsubscribedTopicEventArgs.cs rename to Source/MQTTnet/Server/Events/ClientUnsubscribedTopicEventArgs.cs index 341e1f4..9a9f73b 100644 --- a/Source/MQTTnet/Server/MqttServerClientUnsubscribedTopicEventArgs.cs +++ b/Source/MQTTnet/Server/Events/ClientUnsubscribedTopicEventArgs.cs @@ -1,8 +1,12 @@ -using System; +// 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 MqttServerClientUnsubscribedTopicEventArgs : EventArgs + public sealed class ClientUnsubscribedTopicEventArgs : EventArgs { /// /// Gets the client identifier. diff --git a/Source/MQTTnet/Server/Events/InterceptingPacketEventArgs.cs b/Source/MQTTnet/Server/Events/InterceptingPacketEventArgs.cs new file mode 100644 index 0000000..3f2661c --- /dev/null +++ b/Source/MQTTnet/Server/Events/InterceptingPacketEventArgs.cs @@ -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; +using System.Threading; +using MQTTnet.Packets; + +namespace MQTTnet.Server +{ + public sealed class InterceptingPacketEventArgs : EventArgs + { + /// + /// Gets the client ID which has sent the packet or will receive the packet. + /// + public string ClientId { get; internal set; } + + /// + /// Gets the endpoint of the sending or receiving client. + /// + public string Endpoint { get; internal set; } + + /// + /// Gets or sets the MQTT packet which was received or will be sent. + /// + public MqttPacket Packet { get; set; } + + /// + /// Gets or sets whether the packet should be processed or not. + /// + public bool ProcessPacket { get; set; } = true; + + /// + /// Gets the cancellation token from the connection managing thread. + /// Use this in further event processing. + /// + public CancellationToken CancellationToken { get; internal set; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/Events/InterceptingPublishEventArgs.cs b/Source/MQTTnet/Server/Events/InterceptingPublishEventArgs.cs new file mode 100644 index 0000000..d677d11 --- /dev/null +++ b/Source/MQTTnet/Server/Events/InterceptingPublishEventArgs.cs @@ -0,0 +1,44 @@ +// 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; + +namespace MQTTnet.Server +{ + public sealed class InterceptingPublishEventArgs : EventArgs + { + /// + /// Gets the client identifier. + /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. + /// + public string ClientId { get; internal set; } + + public MqttApplicationMessage ApplicationMessage { get; set; } + + /// + /// Gets or sets a key/value collection that can be used to share data within the scope of this session. + /// + public IDictionary SessionItems { get; internal set; } + + /// + /// Gets the response which will be sent to the client via the PUBACK etc. packets. + /// + public PublishResponse Response { get; } = new PublishResponse(); + + /// + /// Gets or sets whether the publish should be processed internally. + /// + public bool ProcessPublish { get; set; } = true; + + public bool CloseConnection { get; set; } + + /// + /// Gets the cancellation token which can indicate that the client connection gets down. + /// + public CancellationToken CancellationToken { get; internal set; } + } +} diff --git a/Source/MQTTnet/Server/Events/InterceptingSubscriptionEventArgs.cs b/Source/MQTTnet/Server/Events/InterceptingSubscriptionEventArgs.cs new file mode 100644 index 0000000..8b173f1 --- /dev/null +++ b/Source/MQTTnet/Server/Events/InterceptingSubscriptionEventArgs.cs @@ -0,0 +1,69 @@ +// 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 + { + /// + /// Gets the cancellation token which can indicate that the client connection gets down. + /// + public CancellationToken CancellationToken { get; internal set; } + + /// + /// Gets the client identifier. + /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. + /// + public string ClientId { get; internal set; } + + /// + /// Gets or sets whether the broker should close the client connection. + /// + public bool CloseConnection { get; set; } + + /// + /// 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. + /// + public bool ProcessSubscription { get; set; } = true; + + /// + /// Gets or sets the reason string which will be sent to the client in the SUBACK packet. + /// + public string ReasonString { get; set; } + + /// + /// Gets the response which will be sent to the client via the SUBACK packet. + /// + public SubscribeResponse Response { get; } = new SubscribeResponse(); + + /// + /// Gets the current client session. + /// + public MqttSessionStatus Session { get; internal set; } + + /// + /// Gets or sets a key/value collection that can be used to share data within the scope of this session. + /// + public IDictionary SessionItems { get; internal set; } + + /// + /// Gets or sets the topic filter. + /// The topic filter can contain topics and wildcards. + /// + public MqttTopicFilter TopicFilter { get; set; } + + /// + /// Gets or sets the user properties. + /// + public List UserProperties { get; set; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/Events/InterceptingUnsubscriptionEventArgs.cs b/Source/MQTTnet/Server/Events/InterceptingUnsubscriptionEventArgs.cs new file mode 100644 index 0000000..2c78563 --- /dev/null +++ b/Source/MQTTnet/Server/Events/InterceptingUnsubscriptionEventArgs.cs @@ -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.Collections.Generic; +using System.Threading; + +namespace MQTTnet.Server +{ + public sealed class InterceptingUnsubscriptionEventArgs : EventArgs + { + /// + /// Gets the client identifier. + /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. + /// + public string ClientId { get; internal set; } + + /// + /// 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). + /// + public string Topic { get; internal set; } + + /// + /// Gets or sets a key/value collection that can be used to share data within the scope of this session. + /// + public IDictionary SessionItems { get; internal set; } + + /// + /// Gets the response which will be sent to the client via the UNSUBACK pocket. + /// + public UnsubscribeResponse Response { get; } = new UnsubscribeResponse(); + + /// + /// 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. + /// + public bool ProcessUnsubscription { get; set; } = true; + + /// + /// Gets or sets whether the broker should close the client connection. + /// + public bool CloseConnection { get; set; } + + /// + /// Gets the cancellation token which can indicate that the client connection gets down. + /// + public CancellationToken CancellationToken { get; internal set; } + } +} diff --git a/Source/MQTTnet/Server/Events/LoadingRetainedMessagesEventArgs.cs b/Source/MQTTnet/Server/Events/LoadingRetainedMessagesEventArgs.cs new file mode 100644 index 0000000..12aa046 --- /dev/null +++ b/Source/MQTTnet/Server/Events/LoadingRetainedMessagesEventArgs.cs @@ -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 LoadedRetainedMessages { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/Events/PreparingSessionEventArgs.cs b/Source/MQTTnet/Server/Events/PreparingSessionEventArgs.cs new file mode 100644 index 0000000..68b8248 --- /dev/null +++ b/Source/MQTTnet/Server/Events/PreparingSessionEventArgs.cs @@ -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 System; +using System.Collections.Generic; +using MQTTnet.Packets; + +namespace MQTTnet.Server +{ + public sealed class PreparingSessionEventArgs : EventArgs + { + public string Id { get; internal 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 List Subscriptions { get; } = new List(); + + public List PublishPackets { get; } = new List(); + + public IDictionary Items { get; set; } + + /// + /// Gets the last will message. + /// In MQTT, you use the last will message feature to notify other clients about an ungracefully disconnected client. + /// + // TODO: Use single properties. No entire will message. + MqttApplicationMessage WillMessage { get; set; } + + /// + /// Gets the will delay interval. + /// This is the time between the client disconnect and the time the will message will be sent. + /// + uint? WillDelayInterval { get; set; } + + DateTime? SessionExpiryTimestamp { get; set; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/Events/RetainedMessageChangedEventArgs.cs b/Source/MQTTnet/Server/Events/RetainedMessageChangedEventArgs.cs new file mode 100644 index 0000000..1d436fc --- /dev/null +++ b/Source/MQTTnet/Server/Events/RetainedMessageChangedEventArgs.cs @@ -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 System; +using System.Collections.Generic; + +namespace MQTTnet.Server +{ + public sealed class RetainedMessageChangedEventArgs : EventArgs + { + public string ClientId { get; internal set; } + + public MqttApplicationMessage ChangedRetainedMessage { get; internal set; } + + public List StoredRetainedMessages { get; internal set; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/Events/SessionDeletedEventArgs.cs b/Source/MQTTnet/Server/Events/SessionDeletedEventArgs.cs new file mode 100644 index 0000000..844f7c1 --- /dev/null +++ b/Source/MQTTnet/Server/Events/SessionDeletedEventArgs.cs @@ -0,0 +1,16 @@ +// 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 SessionDeletedEventArgs : EventArgs + { + /// + /// Gets the ID of the session. + /// + public string Id { get; internal set; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/Events/ValidatingConnectionEventArgs.cs b/Source/MQTTnet/Server/Events/ValidatingConnectionEventArgs.cs new file mode 100644 index 0000000..6126280 --- /dev/null +++ b/Source/MQTTnet/Server/Events/ValidatingConnectionEventArgs.cs @@ -0,0 +1,184 @@ +// 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.Implementations; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Server +{ + public sealed class ValidatingConnectionEventArgs : EventArgs + { + readonly IMqttChannelAdapter _clientAdapter; + readonly MqttConnectPacket _connectPacket; + + public ValidatingConnectionEventArgs(MqttConnectPacket connectPacket, IMqttChannelAdapter clientAdapter) + { + _connectPacket = connectPacket ?? throw new ArgumentNullException(nameof(connectPacket)); + _clientAdapter = clientAdapter ?? throw new ArgumentNullException(nameof(clientAdapter)); + } + + /// + /// Gets or sets the assigned client identifier. + /// MQTTv5 only. + /// + public string AssignedClientIdentifier { get; set; } + + /// + /// Gets or sets the authentication data. + /// Hint: MQTT 5 feature only. + /// + public byte[] AuthenticationData => _connectPacket.AuthenticationData; + + /// + /// Gets or sets the authentication method. + /// Hint: MQTT 5 feature only. + /// + public string AuthenticationMethod => _connectPacket.AuthenticationMethod; + + /// + /// 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. + /// + public bool? CleanSession => _connectPacket.CleanSession; + + public X509Certificate2 ClientCertificate => _clientAdapter.ClientCertificate; + + /// + /// Gets the client identifier. + /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. + /// + public string ClientId => _connectPacket.ClientId; + + public string Endpoint => _clientAdapter.Endpoint; + + public bool IsSecureConnection => _clientAdapter.IsSecureConnection; + + /// + /// 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. + /// + public ushort? KeepAlivePeriod => _connectPacket.KeepAlivePeriod; + + /// + /// A value of 0 indicates that the value is not used. + /// + public uint MaximumPacketSize => _connectPacket.MaximumPacketSize; + + public string Password => Encoding.UTF8.GetString(RawPassword ?? PlatformAbstractionLayer.EmptyByteArray); + + public MqttProtocolVersion ProtocolVersion => _clientAdapter.PacketFormatterAdapter.ProtocolVersion; + + public byte[] RawPassword => _connectPacket.Password; + + /// + /// 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. + /// MQTTv5 only. + /// + public MqttConnectReasonCode ReasonCode { get; set; } = MqttConnectReasonCode.Success; + + public string ReasonString { get; set; } + + /// + /// 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. + /// + public ushort ReceiveMaximum => _connectPacket.ReceiveMaximum; + + /// + /// Gets the request problem information. + /// Hint: MQTT 5 feature only. + /// + public bool RequestProblemInformation => _connectPacket.RequestProblemInformation; + + /// + /// Gets the request response information. + /// Hint: MQTT 5 feature only. + /// + public bool RequestResponseInformation => _connectPacket.RequestResponseInformation; + + /// + /// Gets or sets the response authentication data. + /// MQTTv5 only. + /// + public byte[] ResponseAuthenticationData { get; set; } + + /// + /// 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. + /// Hint: MQTT 5 feature only. + /// + public List ResponseUserProperties { get; set; } + + /// + /// 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. + /// MQTTv5 only. + /// + public string ServerReference { get; set; } + + /// + /// Gets the session expiry interval. + /// The time after a session expires when it's not actively used. + /// A value of 0 means no expiation. + /// + public uint SessionExpiryInterval => _connectPacket.SessionExpiryInterval; + + /// + /// Gets or sets a key/value collection that can be used to share data within the scope of this session. + /// + public IDictionary SessionItems { get; internal set; } + + /// + /// 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. + /// + public ushort TopicAliasMaximum => _connectPacket.TopicAliasMaximum; + + public string Username => _connectPacket.Username; + + /// + /// Gets or sets the user properties. + /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT + /// packet. + /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add + /// metadata to MQTT messages and pass information between publisher, broker, and subscriber. + /// The feature is very similar to the HTTP header concept. + /// Hint: MQTT 5 feature only. + /// + public List UserProperties => _connectPacket.UserProperties; + + /// + /// 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. + /// + public uint WillDelayInterval => _connectPacket.WillDelayInterval; + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/GetSubscribedMessagesFilter.cs b/Source/MQTTnet/Server/GetSubscribedMessagesFilter.cs deleted file mode 100644 index 004efea..0000000 --- a/Source/MQTTnet/Server/GetSubscribedMessagesFilter.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MQTTnet.Server -{ - public sealed class GetSubscribedMessagesFilter - { - public bool IsNewSubscription { get; set; } - - public MqttTopicFilter TopicFilter { get; set; } - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Server/IMqttClientSession.cs b/Source/MQTTnet/Server/IMqttClientSession.cs deleted file mode 100644 index f522df6..0000000 --- a/Source/MQTTnet/Server/IMqttClientSession.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Threading.Tasks; - -namespace MQTTnet.Server -{ - public interface IMqttClientSession - { - /// - /// Gets the client identifier. - /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. - /// - string ClientId { get; } - - Task StopAsync(); - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Server/IMqttRetainedMessagesManager.cs b/Source/MQTTnet/Server/IMqttRetainedMessagesManager.cs deleted file mode 100644 index 4b85a7e..0000000 --- a/Source/MQTTnet/Server/IMqttRetainedMessagesManager.cs +++ /dev/null @@ -1,20 +0,0 @@ -using MQTTnet.Diagnostics; -using System.Collections.Generic; -using System.Threading.Tasks; -using MQTTnet.Diagnostics.Logger; - -namespace MQTTnet.Server -{ - public interface IMqttRetainedMessagesManager - { - Task Start(IMqttServerOptions options, IMqttNetLogger logger); - - Task LoadMessagesAsync(); - - Task ClearMessagesAsync(); - - Task HandleMessageAsync(string clientId, MqttApplicationMessage applicationMessage); - - Task> GetMessagesAsync(); - } -} diff --git a/Source/MQTTnet/Server/IMqttServer.cs b/Source/MQTTnet/Server/IMqttServer.cs deleted file mode 100644 index 4b920cf..0000000 --- a/Source/MQTTnet/Server/IMqttServer.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using MQTTnet.Server.Status; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace MQTTnet.Server -{ - public interface IMqttServer : IApplicationMessageReceiver, IApplicationMessagePublisher, IDisposable - { - bool IsStarted { get; } - IMqttServerStartedHandler StartedHandler { get; set; } - IMqttServerStoppedHandler StoppedHandler { get; set; } - - IMqttServerClientConnectedHandler ClientConnectedHandler { get; set; } - IMqttServerClientDisconnectedHandler ClientDisconnectedHandler { get; set; } - IMqttServerClientSubscribedTopicHandler ClientSubscribedTopicHandler { get; set; } - IMqttServerClientUnsubscribedTopicHandler ClientUnsubscribedTopicHandler { get; set; } - - IMqttServerOptions Options { get; } - - Task> GetClientStatusAsync(); - Task> GetSessionStatusAsync(); - - Task> GetRetainedApplicationMessagesAsync(); - Task ClearRetainedApplicationMessagesAsync(); - - Task SubscribeAsync(string clientId, ICollection topicFilters); - Task UnsubscribeAsync(string clientId, ICollection topicFilters); - - Task StartAsync(IMqttServerOptions options); - Task StopAsync(); - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Server/IMqttServerApplicationMessageInterceptor.cs b/Source/MQTTnet/Server/IMqttServerApplicationMessageInterceptor.cs deleted file mode 100644 index 64fe047..0000000 --- a/Source/MQTTnet/Server/IMqttServerApplicationMessageInterceptor.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; - -namespace MQTTnet.Server -{ - public interface IMqttServerApplicationMessageInterceptor - { - Task InterceptApplicationMessagePublishAsync(MqttApplicationMessageInterceptorContext context); - } -} diff --git a/Source/MQTTnet/Server/IMqttServerCertificateCredentials.cs b/Source/MQTTnet/Server/IMqttServerCertificateCredentials.cs deleted file mode 100644 index acaff87..0000000 --- a/Source/MQTTnet/Server/IMqttServerCertificateCredentials.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MQTTnet.Server -{ - public interface IMqttServerCertificateCredentials - { - string Password { get; } - } -} diff --git a/Source/MQTTnet/Server/IMqttServerClientConnectedHandler.cs b/Source/MQTTnet/Server/IMqttServerClientConnectedHandler.cs deleted file mode 100644 index b507e41..0000000 --- a/Source/MQTTnet/Server/IMqttServerClientConnectedHandler.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; - -namespace MQTTnet.Server -{ - public interface IMqttServerClientConnectedHandler - { - Task HandleClientConnectedAsync(MqttServerClientConnectedEventArgs eventArgs); - } -} diff --git a/Source/MQTTnet/Server/IMqttServerClientDisconnectedHandler.cs b/Source/MQTTnet/Server/IMqttServerClientDisconnectedHandler.cs deleted file mode 100644 index 3614d2d..0000000 --- a/Source/MQTTnet/Server/IMqttServerClientDisconnectedHandler.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; - -namespace MQTTnet.Server -{ - public interface IMqttServerClientDisconnectedHandler - { - Task HandleClientDisconnectedAsync(MqttServerClientDisconnectedEventArgs eventArgs); - } -} diff --git a/Source/MQTTnet/Server/IMqttServerClientMessageQueueInterceptor.cs b/Source/MQTTnet/Server/IMqttServerClientMessageQueueInterceptor.cs deleted file mode 100644 index c6f97a7..0000000 --- a/Source/MQTTnet/Server/IMqttServerClientMessageQueueInterceptor.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; - -namespace MQTTnet.Server -{ - public interface IMqttServerClientMessageQueueInterceptor - { - Task InterceptClientMessageQueueEnqueueAsync(MqttClientMessageQueueInterceptorContext context); - } -} diff --git a/Source/MQTTnet/Server/IMqttServerClientSubscribedTopicHandler.cs b/Source/MQTTnet/Server/IMqttServerClientSubscribedTopicHandler.cs deleted file mode 100644 index 6f82bb3..0000000 --- a/Source/MQTTnet/Server/IMqttServerClientSubscribedTopicHandler.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; - -namespace MQTTnet.Server -{ - public interface IMqttServerClientSubscribedTopicHandler - { - Task HandleClientSubscribedTopicAsync(MqttServerClientSubscribedTopicEventArgs eventArgs); - } -} diff --git a/Source/MQTTnet/Server/IMqttServerClientUnsubscribedTopicHandler.cs b/Source/MQTTnet/Server/IMqttServerClientUnsubscribedTopicHandler.cs deleted file mode 100644 index 077a80f..0000000 --- a/Source/MQTTnet/Server/IMqttServerClientUnsubscribedTopicHandler.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; - -namespace MQTTnet.Server -{ - public interface IMqttServerClientUnsubscribedTopicHandler - { - Task HandleClientUnsubscribedTopicAsync(MqttServerClientUnsubscribedTopicEventArgs eventArgs); - } -} diff --git a/Source/MQTTnet/Server/IMqttServerConnectionValidator.cs b/Source/MQTTnet/Server/IMqttServerConnectionValidator.cs deleted file mode 100644 index 68b5be6..0000000 --- a/Source/MQTTnet/Server/IMqttServerConnectionValidator.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; - -namespace MQTTnet.Server -{ - public interface IMqttServerConnectionValidator - { - Task ValidateConnectionAsync(MqttConnectionValidatorContext context); - } -} diff --git a/Source/MQTTnet/Server/IMqttServerFactory.cs b/Source/MQTTnet/Server/IMqttServerFactory.cs deleted file mode 100644 index a612c86..0000000 --- a/Source/MQTTnet/Server/IMqttServerFactory.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using MQTTnet.Adapter; -using MQTTnet.Diagnostics; -using MQTTnet.Diagnostics.Logger; - -namespace MQTTnet.Server -{ - public interface IMqttServerFactory - { - IList> DefaultServerAdapters { get; } - - IMqttServer CreateMqttServer(); - - IMqttServer CreateMqttServer(IMqttNetLogger logger); - - IMqttServer CreateMqttServer(IEnumerable adapters); - - IMqttServer CreateMqttServer(IEnumerable adapters, IMqttNetLogger logger); - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Server/IMqttServerOptions.cs b/Source/MQTTnet/Server/IMqttServerOptions.cs deleted file mode 100644 index f5cad0d..0000000 --- a/Source/MQTTnet/Server/IMqttServerOptions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -namespace MQTTnet.Server -{ - public interface IMqttServerOptions - { - /// - /// Gets the client identifier. - /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. - /// - string ClientId { get; } - - bool EnablePersistentSessions { get; } - - int MaxPendingMessagesPerClient { get; } - MqttPendingMessagesOverflowStrategy PendingMessagesOverflowStrategy { get; } - - TimeSpan DefaultCommunicationTimeout { get; } - TimeSpan KeepAliveMonitorInterval { get; } - - IMqttServerConnectionValidator ConnectionValidator { get; } - IMqttServerSubscriptionInterceptor SubscriptionInterceptor { get; } - IMqttServerUnsubscriptionInterceptor UnsubscriptionInterceptor { get; } - IMqttServerApplicationMessageInterceptor ApplicationMessageInterceptor { get; } - IMqttServerClientMessageQueueInterceptor ClientMessageQueueInterceptor { get; } - IMqttServerApplicationMessageInterceptor UndeliveredMessageInterceptor { get; } - - MqttServerTcpEndpointOptions DefaultEndpointOptions { get; } - MqttServerTlsTcpEndpointOptions TlsEndpointOptions { get; } - - IMqttServerStorage Storage { get; } - - IMqttRetainedMessagesManager RetainedMessagesManager { get; } - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Server/IMqttServerPersistedSession.cs b/Source/MQTTnet/Server/IMqttServerPersistedSession.cs deleted file mode 100644 index a7b015e..0000000 --- a/Source/MQTTnet/Server/IMqttServerPersistedSession.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace MQTTnet.Server -{ - public interface IMqttServerPersistedSession - { - /// - /// Gets the client identifier. - /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. - /// - string ClientId { get; } - - IDictionary Items { get; } - - IList Subscriptions { get; } - - /// - /// Gets the last will message. - /// In MQTT, you use the last will message feature to notify other clients about an ungracefully disconnected client. - /// - MqttApplicationMessage WillMessage { get; } - - /// - /// Gets the will delay interval. - /// This is the time between the client disconnect and the time the will message will be sent. - /// - uint? WillDelayInterval { get; } - - DateTime? SessionExpiryTimestamp { get; } - - IList PendingApplicationMessages { get; } - } -} diff --git a/Source/MQTTnet/Server/IMqttServerPersistedSessionsStorage.cs b/Source/MQTTnet/Server/IMqttServerPersistedSessionsStorage.cs deleted file mode 100644 index fa2a4cb..0000000 --- a/Source/MQTTnet/Server/IMqttServerPersistedSessionsStorage.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace MQTTnet.Server -{ - public interface IMqttServerPersistedSessionsStorage - { - Task> LoadPersistedSessionsAsync(); - } -} diff --git a/Source/MQTTnet/Server/IMqttServerStartedHandler.cs b/Source/MQTTnet/Server/IMqttServerStartedHandler.cs deleted file mode 100644 index e6155f1..0000000 --- a/Source/MQTTnet/Server/IMqttServerStartedHandler.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace MQTTnet.Server -{ - public interface IMqttServerStartedHandler - { - Task HandleServerStartedAsync(EventArgs eventArgs); - } -} diff --git a/Source/MQTTnet/Server/IMqttServerStoppedHandler.cs b/Source/MQTTnet/Server/IMqttServerStoppedHandler.cs deleted file mode 100644 index 34df323..0000000 --- a/Source/MQTTnet/Server/IMqttServerStoppedHandler.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace MQTTnet.Server -{ - public interface IMqttServerStoppedHandler - { - Task HandleServerStoppedAsync(EventArgs eventArgs); - } -} diff --git a/Source/MQTTnet/Server/IMqttServerStorage.cs b/Source/MQTTnet/Server/IMqttServerStorage.cs deleted file mode 100644 index da6e6e2..0000000 --- a/Source/MQTTnet/Server/IMqttServerStorage.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace MQTTnet.Server -{ - public interface IMqttServerStorage - { - Task SaveRetainedMessagesAsync(IList messages); - - Task> LoadRetainedMessagesAsync(); - } -} diff --git a/Source/MQTTnet/Server/IMqttServerSubscriptionInterceptor.cs b/Source/MQTTnet/Server/IMqttServerSubscriptionInterceptor.cs deleted file mode 100644 index a7ce95e..0000000 --- a/Source/MQTTnet/Server/IMqttServerSubscriptionInterceptor.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; - -namespace MQTTnet.Server -{ - public interface IMqttServerSubscriptionInterceptor - { - Task InterceptSubscriptionAsync(MqttSubscriptionInterceptorContext context); - } -} diff --git a/Source/MQTTnet/Server/IMqttServerUnsubscriptionInterceptor.cs b/Source/MQTTnet/Server/IMqttServerUnsubscriptionInterceptor.cs deleted file mode 100644 index 9669383..0000000 --- a/Source/MQTTnet/Server/IMqttServerUnsubscriptionInterceptor.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; - -namespace MQTTnet.Server -{ - public interface IMqttServerUnsubscriptionInterceptor - { - Task InterceptUnsubscriptionAsync(MqttUnsubscriptionInterceptorContext context); - } -} diff --git a/Source/MQTTnet/Server/InjectedMqttApplicationMessage.cs b/Source/MQTTnet/Server/InjectedMqttApplicationMessage.cs new file mode 100644 index 0000000..0708cee --- /dev/null +++ b/Source/MQTTnet/Server/InjectedMqttApplicationMessage.cs @@ -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 System; + +namespace MQTTnet.Server +{ + public sealed class InjectedMqttApplicationMessage + { + public InjectedMqttApplicationMessage(MqttApplicationMessage applicationMessage) + { + ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage)); + } + + public string SenderClientId { get; set; } + + public MqttApplicationMessage ApplicationMessage { get; set; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/Internal/CheckSubscriptionsResult.cs b/Source/MQTTnet/Server/Internal/CheckSubscriptionsResult.cs index b00b208..7c20e8d 100644 --- a/Source/MQTTnet/Server/Internal/CheckSubscriptionsResult.cs +++ b/Source/MQTTnet/Server/Internal/CheckSubscriptionsResult.cs @@ -1,11 +1,15 @@ -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; using MQTTnet.Protocol; -namespace MQTTnet.Server.Internal +namespace MQTTnet.Server { - public struct CheckSubscriptionsResult + public sealed class CheckSubscriptionsResult { - public static CheckSubscriptionsResult NotSubscribed = new CheckSubscriptionsResult(); + public static CheckSubscriptionsResult NotSubscribed { get; } = new CheckSubscriptionsResult(); public bool IsSubscribed { get; set; } @@ -13,14 +17,6 @@ namespace MQTTnet.Server.Internal public List SubscriptionIdentifiers { get; set; } - /// - /// Gets or sets the quality of service level. - /// The Quality of Service (QoS) level is an agreement between the sender of a message and the receiver of a message that defines the guarantee of delivery for a specific message. - /// There are 3 QoS levels in MQTT: - /// - At most once (0): Message gets delivered no time, once or multiple times. - /// - At least once (1): Message gets delivered at least once (one time or more often). - /// - Exactly once (2): Message gets delivered exactly once (It's ensured that the message only comes once). - /// public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } } } diff --git a/Source/MQTTnet/Server/Internal/ISubscriptionChangedNotification.cs b/Source/MQTTnet/Server/Internal/ISubscriptionChangedNotification.cs new file mode 100644 index 0000000..01b13f6 --- /dev/null +++ b/Source/MQTTnet/Server/Internal/ISubscriptionChangedNotification.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MQTTnet.Server +{ + public interface ISubscriptionChangedNotification + { + void OnSubscriptionsAdded(MqttSession clientSession, List subscriptionsTopics); + + void OnSubscriptionsRemoved(MqttSession clientSession, List subscriptionTopics); + } +} diff --git a/Source/MQTTnet/Server/Internal/MqttClient.cs b/Source/MQTTnet/Server/Internal/MqttClient.cs new file mode 100644 index 0000000..fd4c7f6 --- /dev/null +++ b/Source/MQTTnet/Server/Internal/MqttClient.cs @@ -0,0 +1,533 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Adapter; +using MQTTnet.Client; +using MQTTnet.Diagnostics; +using MQTTnet.Exceptions; +using MQTTnet.Formatter; +using MQTTnet.Internal; +using MQTTnet.PacketDispatcher; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Server +{ + public sealed class MqttClient + { + readonly Dictionary _topicAlias = new Dictionary(); + readonly MqttPacketDispatcher _packetDispatcher = new MqttPacketDispatcher(); + readonly MqttPacketFactories _packetFactories = new MqttPacketFactories(); + readonly MqttApplicationMessageFactory _applicationMessageFactory = new MqttApplicationMessageFactory(); + + readonly MqttClientSessionsManager _sessionsManager; + readonly MqttNetSourceLogger _logger; + + readonly MqttServerOptions _serverOptions; + readonly MqttServerEventContainer _eventContainer; + readonly MqttConnectPacket _connectPacket; + + CancellationTokenSource _cancellationToken; + + public MqttClient( + MqttConnectPacket connectPacket, + IMqttChannelAdapter channelAdapter, + MqttSession session, + MqttServerOptions serverOptions, + MqttServerEventContainer eventContainer, + MqttClientSessionsManager sessionsManager, + IMqttNetLogger logger) + { + _serverOptions = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions)); + _eventContainer = eventContainer; + _sessionsManager = sessionsManager ?? throw new ArgumentNullException(nameof(sessionsManager)); + + ChannelAdapter = channelAdapter ?? throw new ArgumentNullException(nameof(channelAdapter)); + Endpoint = channelAdapter.Endpoint; + + Session = session ?? throw new ArgumentNullException(nameof(session)); + _connectPacket = connectPacket ?? throw new ArgumentNullException(nameof(connectPacket)); + + if (logger == null) throw new ArgumentNullException(nameof(logger)); + _logger = logger.WithSource(nameof(MqttClient)); + } + + public string Id => _connectPacket.ClientId; + + public string Endpoint { get; } + + public IMqttChannelAdapter ChannelAdapter { get; } + + public MqttClientStatistics Statistics { get; } = new MqttClientStatistics(); + + public bool IsRunning { get; private set; } + + public ushort KeepAlivePeriod => _connectPacket.KeepAlivePeriod; + + public MqttSession Session { get; } + + public bool IsTakenOver { get; set; } + + public bool IsCleanDisconnect { get; private set; } + + public async Task StopAsync(MqttDisconnectReasonCode reason) + { + IsRunning = false; + + if (reason == MqttDisconnectReasonCode.SessionTakenOver || reason == MqttDisconnectReasonCode.KeepAliveTimeout) + { + // Is 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(reason).ConfigureAwait(false); + } + + StopInternal(); + } + + public void ResetStatistics() + { + ChannelAdapter.ResetStatistics(); + } + + public async Task RunAsync() + { + _logger.Info("Client '{0}': Session started.", Id); + + Session.LatestConnectPacket = _connectPacket; + Session.WillMessageSent = false; + + using (var cancellationToken = new CancellationTokenSource()) + { + _cancellationToken = cancellationToken; + + try + { + Task.Run(() => SendPacketsLoop(cancellationToken.Token), cancellationToken.Token).RunInBackground(_logger); + + IsRunning = true; + + await ReceivePackagesLoop(cancellationToken.Token).ConfigureAwait(false); + } + finally + { + IsRunning = false; + + _cancellationToken = null; + cancellationToken.Cancel(); + } + } + + _packetDispatcher.CancelAll(); + + if (!IsTakenOver && !IsCleanDisconnect && Session.LatestConnectPacket.WillFlag && !Session.WillMessageSent) + { + var willPublishPacket = _packetFactories.Publish.Create(Session.LatestConnectPacket); + var willApplicationMessage = _applicationMessageFactory.Create(willPublishPacket); + + _= _sessionsManager.DispatchApplicationMessage(Id, willApplicationMessage); + 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); + } + + async Task InterceptPacketAsync(MqttPacket packet, CancellationToken cancellationToken) + { + if (!_eventContainer.InterceptingOutboundPacketEvent.HasHandlers) + { + return packet; + } + + var interceptingPacketEventArgs = new InterceptingPacketEventArgs + { + ClientId = Id, + Endpoint = Endpoint, + Packet = packet, + CancellationToken = cancellationToken + }; + + await _eventContainer.InterceptingOutboundPacketEvent.InvokeAsync(interceptingPacketEventArgs).ConfigureAwait(false); + + if (!interceptingPacketEventArgs.ProcessPacket || packet == null) + { + return null; + } + + return interceptingPacketEventArgs.Packet; + } + + async Task ReceivePackagesLoop(CancellationToken cancellationToken) + { + 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) + { + var packet = await ChannelAdapter.ReceivePacketAsync(cancellationToken).ConfigureAwait(false); + + if (packet == null) + { + return; + } + + var processPacket = true; + + if (_eventContainer.InterceptingInboundPacketEvent.HasHandlers) + { + var interceptingPacketEventArgs = new InterceptingPacketEventArgs + { + ClientId = Id, + Endpoint = Endpoint, + Packet = packet, + CancellationToken = cancellationToken + }; + + await _eventContainer.InterceptingInboundPacketEvent.InvokeAsync(interceptingPacketEventArgs).ConfigureAwait(false); + packet = interceptingPacketEventArgs.Packet; + processPacket = interceptingPacketEventArgs.ProcessPacket; + } + + if (!processPacket || packet == null) + { + // Restart the receiving process to get the next packet ignoring the current one.. + continue; + } + + Statistics.HandleReceivedPacket(packet); + + if (packet is MqttPublishPacket publishPacket) + { + await HandleIncomingPublishPacket(publishPacket, cancellationToken).ConfigureAwait(false); + } + else if (packet is MqttPubAckPacket pubAckPacket) + { + Session.AcknowledgePublishPacket(pubAckPacket.PacketIdentifier); + } + else if (packet is MqttPubCompPacket pubCompPacket) + { + Session.AcknowledgePublishPacket(pubCompPacket.PacketIdentifier); + } + else if (packet is MqttPubRecPacket pubRecPacket) + { + HandleIncomingPubRecPacket(pubRecPacket); + } + else if (packet is MqttPubRelPacket pubRelPacket) + { + HandleIncomingPubRelPacket(pubRelPacket); + } + else if (packet is MqttSubscribePacket subscribePacket) + { + await HandleIncomingSubscribePacket(subscribePacket, cancellationToken).ConfigureAwait(false); + } + else if (packet is MqttUnsubscribePacket unsubscribePacket) + { + await HandleIncomingUnsubscribePacket(unsubscribePacket, cancellationToken).ConfigureAwait(false); + } + else if (packet is MqttPingReqPacket) + { + // See: The Server MUST send a PINGRESP packet in response to a PINGREQ packet [MQTT-3.12.4-1]. + //await SendPacketAsync(MqttPingRespPacket.Instance, cancellationToken).ConfigureAwait(false); + Session.EnqueuePacket(new MqttPacketBusItem(MqttPingRespPacket.Instance)); + } + else if (packet 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 (packet is MqttDisconnectPacket) + { + IsCleanDisconnect = true; + return; + } + else + { + if (!_packetDispatcher.TryDispatch(packet)) + { + throw new MqttProtocolViolationException($"Received packet '{packet}' at an unexpected time."); + } + } + } + } + catch (OperationCanceledException) + { + } + catch (Exception exception) + { + if (exception is MqttCommunicationException) + { + _logger.Warning(exception, "Client '{0}': Communication exception while receiving client packets.", Id); + } + else + { + _logger.Error(exception, "Client '{0}': Error while receiving client packets.", Id); + } + } + } + + async Task SendPacketsLoop(CancellationToken cancellationToken) + { + MqttPacketBusItem packetBusItem = null; + + try + { + while (!cancellationToken.IsCancellationRequested) + { + 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; + } + + try + { + await SendPacketAsync(packetBusItem.Packet, cancellationToken).ConfigureAwait(false); + packetBusItem.MarkAsDelivered(); + } + catch (OperationCanceledException) + { + packetBusItem.MarkAsCancelled(); + } + catch (Exception exception) + { + packetBusItem.MarkAsFailed(exception); + } + } + } + catch (OperationCanceledException) + { + } + catch (Exception exception) + { + if (exception is MqttCommunicationTimedOutException) + { + _logger.Warning(exception, "Client '{0}': Sending publish packet failed: Timeout.", Id); + } + else if (exception is MqttCommunicationException) + { + _logger.Warning(exception, "Client '{0}': Sending publish packet failed: 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.EnqueuePacket(new MqttPacketBusItem(publishPacket)); + } + } + + StopInternal(); + } + } + + void StopInternal() + { + try + { + _cancellationToken?.Cancel(); + } + catch (ObjectDisposedException) + { + // This can happen when connections are created and dropped very quickly. + // It is not an issue if the cancellation token cannot be cancelled multiple times. + } + } + + void HandleIncomingPubRecPacket(MqttPubRecPacket pubRecPacket) + { + var pubRelPacket = _packetFactories.PubRel.Create(pubRecPacket, MqttApplicationMessageReceivedReasonCode.Success); + Session.EnqueuePacket(new MqttPacketBusItem(pubRelPacket)); + } + + void HandleIncomingPubRelPacket(MqttPubRelPacket pubRelPacket) + { + var pubCompPacket = _packetFactories.PubComp.Create(pubRelPacket, MqttApplicationMessageReceivedReasonCode.Success); + Session.EnqueuePacket(new MqttPacketBusItem(pubCompPacket)); + } + + async Task HandleIncomingSubscribePacket(MqttSubscribePacket subscribePacket, CancellationToken cancellationToken) + { + var subscribeResult = await Session.SubscriptionsManager.Subscribe(subscribePacket, cancellationToken).ConfigureAwait(false); + + var subAckPacket = _packetFactories.SubAck.Create(subscribePacket, subscribeResult); + + Session.EnqueuePacket(new MqttPacketBusItem(subAckPacket)); + + if (subscribeResult.CloseConnection) + { + StopInternal(); + return; + } + + if (subscribeResult.RetainedMessages != null) + { + foreach (var retainedApplicationMessage in subscribeResult.RetainedMessages) + { + var publishPacket = _packetFactories.Publish.Create(retainedApplicationMessage.ApplicationMessage); + Session.EnqueuePacket(new MqttPacketBusItem(publishPacket)); + } + } + } + + async Task HandleIncomingUnsubscribePacket(MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken) + { + var unsubscribeResult = await Session.SubscriptionsManager.Unsubscribe(unsubscribePacket, cancellationToken).ConfigureAwait(false); + + var unsubAckPacket = _packetFactories.UnsubAck.Create(unsubscribePacket, unsubscribeResult); + + Session.EnqueuePacket(new MqttPacketBusItem(unsubAckPacket)); + + if (unsubscribeResult.CloseConnection) + { + StopInternal(); + } + } + + async Task HandleIncomingPublishPacket(MqttPublishPacket publishPacket, CancellationToken cancellationToken) + { + HandleTopicAlias(publishPacket); + + InterceptingPublishEventArgs interceptingPublishEventArgs = null; + var applicationMessage = _applicationMessageFactory.Create(publishPacket); + var closeConnection = false; + var processPublish = true; + + if (_eventContainer.InterceptingPublishEvent.HasHandlers) + { + interceptingPublishEventArgs = new InterceptingPublishEventArgs + { + ClientId = Id, + ApplicationMessage = applicationMessage, + SessionItems = Session.Items, + ProcessPublish = true, + CloseConnection = false, + CancellationToken = cancellationToken + }; + + 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; + } + + if (closeConnection) + { + await StopAsync(MqttDisconnectReasonCode.UnspecifiedError); + return; + } + + if (processPublish && applicationMessage != null) + { + await _sessionsManager.DispatchApplicationMessage(Id, applicationMessage).ConfigureAwait(false); + } + + switch (publishPacket.QualityOfServiceLevel) + { + case MqttQualityOfServiceLevel.AtMostOnce: + { + // Do nothing since QoS 0 has no ack at all! + break; + } + case MqttQualityOfServiceLevel.AtLeastOnce: + { + var pubAckPacket = _packetFactories.PubAck.Create(publishPacket, interceptingPublishEventArgs); + Session.EnqueuePacket(new MqttPacketBusItem(pubAckPacket)); + break; + } + case MqttQualityOfServiceLevel.ExactlyOnce: + { + var pubRecPacket = _packetFactories.PubRec.Create(publishPacket, interceptingPublishEventArgs); + Session.EnqueuePacket(new MqttPacketBusItem(pubRecPacket)); + break; + } + default: + { + throw new MqttCommunicationException("Received a not supported QoS level."); + } + } + } + + 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 TrySendDisconnectPacket(MqttDisconnectReasonCode reasonCode) + { + try + { + var disconnectPacket = _packetFactories.Disconnect.Create(reasonCode); + + 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, reasonCode); + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/Internal/MqttClientConnection.cs b/Source/MQTTnet/Server/Internal/MqttClientConnection.cs deleted file mode 100644 index 6081f27..0000000 --- a/Source/MQTTnet/Server/Internal/MqttClientConnection.cs +++ /dev/null @@ -1,495 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MQTTnet.Adapter; -using MQTTnet.Client; -using MQTTnet.Client.Disconnecting; -using MQTTnet.Diagnostics.Logger; -using MQTTnet.Exceptions; -using MQTTnet.Formatter; -using MQTTnet.Implementations; -using MQTTnet.Internal; -using MQTTnet.PacketDispatcher; -using MQTTnet.Packets; -using MQTTnet.Protocol; -using MQTTnet.Server.Status; - -namespace MQTTnet.Server.Internal -{ - public sealed class MqttClientConnection - { - readonly Dictionary _topicAlias = new Dictionary(); - readonly MqttPacketIdentifierProvider _packetIdentifierProvider = new MqttPacketIdentifierProvider(); - readonly MqttPacketDispatcher _packetDispatcher = new MqttPacketDispatcher(); - - readonly MqttClientSessionsManager _sessionsManager; - - readonly IMqttChannelAdapter _channelAdapter; - readonly MqttNetSourceLogger _logger; - readonly IMqttServerOptions _serverOptions; - - readonly string _endpoint; - readonly MqttConnectPacket _connectPacket; - - CancellationTokenSource _cancellationToken; - - public MqttClientConnection( - MqttConnectPacket connectPacket, - IMqttChannelAdapter channelAdapter, - MqttClientSession session, - IMqttServerOptions serverOptions, - MqttClientSessionsManager sessionsManager, - IMqttNetLogger logger) - { - _serverOptions = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions)); - _sessionsManager = sessionsManager ?? throw new ArgumentNullException(nameof(sessionsManager)); - - _channelAdapter = channelAdapter ?? throw new ArgumentNullException(nameof(channelAdapter)); - _endpoint = channelAdapter.Endpoint; - - Session = session ?? throw new ArgumentNullException(nameof(session)); - _connectPacket = connectPacket ?? throw new ArgumentNullException(nameof(connectPacket)); - - if (logger == null) throw new ArgumentNullException(nameof(logger)); - _logger = logger.WithSource(nameof(MqttClientConnection)); - } - - public string ClientId => _connectPacket.ClientId; - - public string Endpoint => _endpoint; - - public MqttClientConnectionStatistics Statistics { get; } = new MqttClientConnectionStatistics(); - - public bool IsRunning { get; private set; } - - public ushort KeepAlivePeriod => _connectPacket.KeepAlivePeriod; - - public bool IsReadingPacket => _channelAdapter.IsReadingPacket; - - public MqttClientSession Session { get; } - - public bool IsTakenOver { get; set; } - - public bool IsCleanDisconnect { get; private set; } - - public async Task StopAsync(MqttClientDisconnectReason reason) - { - IsRunning = false; - - if (reason == MqttClientDisconnectReason.SessionTakenOver || reason == MqttClientDisconnectReason.KeepAliveTimeout) - { - // Is 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(reason).ConfigureAwait(false); - } - - StopInternal(); - } - - public void ResetStatistics() - { - _channelAdapter.ResetStatistics(); - } - - public void FillClientStatus(MqttClientStatus clientStatus) - { - clientStatus.ClientId = ClientId; - clientStatus.Endpoint = _endpoint; - - clientStatus.ProtocolVersion = _channelAdapter.PacketFormatterAdapter.ProtocolVersion; - clientStatus.BytesSent = _channelAdapter.BytesSent; - clientStatus.BytesReceived = _channelAdapter.BytesReceived; - - Statistics.FillClientStatus(clientStatus); - } - - public async Task RunAsync() - { - _logger.Info("Client '{0}': Session started.", ClientId); - - Session.WillMessage = _connectPacket.WillMessage; - - using (var cancellationToken = new CancellationTokenSource()) - { - _cancellationToken = cancellationToken; - - try - { - Task.Run(() => SendPacketsLoop(cancellationToken.Token), cancellationToken.Token).RunInBackground(_logger); - - Session.IsCleanSession = false; - - IsRunning = true; - - await ReceivePackagesLoop(cancellationToken.Token).ConfigureAwait(false); - } - finally - { - IsRunning = false; - - cancellationToken.Cancel(); - _cancellationToken = null; - } - } - - _packetDispatcher.CancelAll(); - - if (!IsTakenOver && !IsCleanDisconnect && Session.WillMessage != null) - { - _sessionsManager.DispatchApplicationMessage(Session.WillMessage, this); - Session.WillMessage = null; - } - - _logger.Info("Client '{0}': Connection stopped.", ClientId); - } - - Task SendPacketAsync(MqttBasePacket packet, CancellationToken cancellationToken) - { - return _channelAdapter.SendPacketAsync(packet, cancellationToken).ContinueWith(task => { Statistics.HandleSentPacket(packet); }, cancellationToken); - } - - async Task ReceivePackagesLoop(CancellationToken cancellationToken) - { - 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) - { - var packet = await _channelAdapter.ReceivePacketAsync(cancellationToken).ConfigureAwait(false); - if (packet == null) - { - // The client has closed the connection gracefully. - return; - } - - Statistics.HandleReceivedPacket(packet); - - if (packet is MqttPublishPacket publishPacket) - { - await HandleIncomingPublishPacket(publishPacket, cancellationToken).ConfigureAwait(false); - } - else if (packet is MqttPubRelPacket pubRelPacket) - { - await HandleIncomingPubRelPacket(pubRelPacket, cancellationToken).ConfigureAwait(false); - } - else if (packet is MqttSubscribePacket subscribePacket) - { - await HandleIncomingSubscribePacket(subscribePacket, cancellationToken).ConfigureAwait(false); - } - else if (packet is MqttUnsubscribePacket unsubscribePacket) - { - await HandleIncomingUnsubscribePacket(unsubscribePacket, cancellationToken).ConfigureAwait(false); - } - else if (packet is MqttPingReqPacket) - { - // See: The Server MUST send a PINGRESP packet in response to a PINGREQ packet [MQTT-3.12.4-1]. - await SendPacketAsync(MqttPingRespPacket.Instance, cancellationToken).ConfigureAwait(false); - } - else if (packet 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 (packet is MqttDisconnectPacket) - { - IsCleanDisconnect = true; - return; - } - else - { - if (!_packetDispatcher.TryDispatch(packet)) - { - throw new MqttProtocolViolationException($"Received packet '{packet}' at an unexpected time."); - } - } - } - } - catch (OperationCanceledException) - { - } - catch (Exception exception) - { - if (exception is MqttCommunicationException) - { - _logger.Warning(exception, "Client '{0}': Communication exception while receiving client packets.", ClientId); - } - else - { - _logger.Error(exception, "Client '{0}': Error while receiving client packets.", ClientId); - } - } - } - - async Task SendPacketsLoop(CancellationToken cancellationToken) - { - MqttQueuedApplicationMessage queuedApplicationMessage = null; - MqttPublishPacket publishPacket = null; - - try - { - while (!cancellationToken.IsCancellationRequested) - { - queuedApplicationMessage = await Session.ApplicationMessagesQueue.Dequeue(cancellationToken).ConfigureAwait(false); - - // Also check the cancellation token here because the dequeue is blocking and may take some time. - if (cancellationToken.IsCancellationRequested) - { - return; - } - - if (queuedApplicationMessage == null) - { - continue; - } - - publishPacket = await CreatePublishPacket(queuedApplicationMessage).ConfigureAwait(false); - if (publishPacket == null) - { - continue; - } - - if (publishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce) - { - await SendPacketAsync(publishPacket, cancellationToken).ConfigureAwait(false); - } - else if (publishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtLeastOnce) - { - using (var awaitable = _packetDispatcher.AddAwaitable(publishPacket.PacketIdentifier)) - { - await SendPacketAsync(publishPacket, cancellationToken).ConfigureAwait(false); - await awaitable.WaitOneAsync(_serverOptions.DefaultCommunicationTimeout).ConfigureAwait(false); - } - } - else if (publishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.ExactlyOnce) - { - using (var awaitableRec = _packetDispatcher.AddAwaitable(publishPacket.PacketIdentifier)) - using (var awaitableComp = _packetDispatcher.AddAwaitable(publishPacket.PacketIdentifier)) - { - await SendPacketAsync(publishPacket, cancellationToken).ConfigureAwait(false); - var pubRecPacket = await awaitableRec.WaitOneAsync(_serverOptions.DefaultCommunicationTimeout).ConfigureAwait(false); - - var pubRelPacket = _channelAdapter.PacketFormatterAdapter.DataConverter.CreatePubRelPacket(pubRecPacket, MqttApplicationMessageReceivedReasonCode.Success); - await SendPacketAsync(pubRelPacket, cancellationToken).ConfigureAwait(false); - - await awaitableComp.WaitOneAsync(_serverOptions.DefaultCommunicationTimeout).ConfigureAwait(false); - } - } - - _logger.Verbose("Client '{0}': Queued application message sent.", ClientId); - } - } - catch (OperationCanceledException) - { - } - catch (Exception exception) - { - if (exception is MqttCommunicationTimedOutException) - { - _logger.Warning(exception, "Client '{0}': Sending publish packet failed: Timeout.", ClientId); - } - else if (exception is MqttCommunicationException) - { - _logger.Warning(exception, "Client '{0}': Sending publish packet failed: Communication exception.", ClientId); - } - else - { - _logger.Error(exception, "Client '{0}': Sending publish packet failed.", ClientId); - } - - if (publishPacket?.QualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) - { - if (queuedApplicationMessage != null) - { - queuedApplicationMessage.IsDuplicate = true; - Session.ApplicationMessagesQueue.Enqueue(queuedApplicationMessage); - } - } - - StopInternal(); - } - } - - async Task CreatePublishPacket(MqttQueuedApplicationMessage queuedApplicationMessage) - { - var publishPacket = _channelAdapter.PacketFormatterAdapter.DataConverter.CreatePublishPacket(queuedApplicationMessage.ApplicationMessage); - publishPacket.QualityOfServiceLevel = queuedApplicationMessage.SubscriptionQualityOfServiceLevel; - - if (_channelAdapter.PacketFormatterAdapter.ProtocolVersion == MqttProtocolVersion.V500) - { - publishPacket.Properties.SubscriptionIdentifiers = queuedApplicationMessage.SubscriptionIdentifiers; - } - - // Set the retain flag to true according to [MQTT-3.3.1-8] and [MQTT-3.3.1-9]. - publishPacket.Retain = queuedApplicationMessage.IsRetainedMessage; - - publishPacket = await InvokeClientMessageQueueInterceptor(publishPacket, queuedApplicationMessage).ConfigureAwait(false); - if (publishPacket == null) - { - // The interceptor has decided that the message is not relevant and will be fully ignored. - return null; - } - - if (publishPacket.QualityOfServiceLevel > 0) - { - publishPacket.PacketIdentifier = _packetIdentifierProvider.GetNextPacketIdentifier(); - } - - return publishPacket; - } - - void StopInternal() - { - _cancellationToken?.Cancel(); - } - - Task HandleIncomingPubRelPacket(MqttPubRelPacket pubRelPacket, CancellationToken cancellationToken) - { - var pubCompPacket = _channelAdapter.PacketFormatterAdapter.DataConverter.CreatePubCompPacket(pubRelPacket, MqttApplicationMessageReceivedReasonCode.Success); - return SendPacketAsync(pubCompPacket, cancellationToken); - } - - async Task HandleIncomingSubscribePacket(MqttSubscribePacket subscribePacket, CancellationToken cancellationToken) - { - var subscribeResult = await Session.SubscriptionsManager.Subscribe(subscribePacket).ConfigureAwait(false); - var subAckPacket = _channelAdapter.PacketFormatterAdapter.DataConverter.CreateSubAckPacket(subscribePacket, subscribeResult); - - await SendPacketAsync(subAckPacket, cancellationToken).ConfigureAwait(false); - - if (subscribeResult.CloseConnection) - { - StopInternal(); - return; - } - - foreach (var retainedApplicationMessage in subscribeResult.RetainedApplicationMessages) - { - Session.ApplicationMessagesQueue.Enqueue(retainedApplicationMessage); - } - } - - async Task HandleIncomingUnsubscribePacket(MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken) - { - var reasonCodes = await Session.SubscriptionsManager.Unsubscribe(unsubscribePacket).ConfigureAwait(false); - var unsubAckPacket = _channelAdapter.PacketFormatterAdapter.DataConverter.CreateUnsubAckPacket(unsubscribePacket, reasonCodes); - - await SendPacketAsync(unsubAckPacket, cancellationToken).ConfigureAwait(false); - } - - Task HandleIncomingPublishPacket(MqttPublishPacket publishPacket, CancellationToken cancellationToken) - { - HandleTopicAlias(publishPacket); - - var applicationMessage = _channelAdapter.PacketFormatterAdapter.DataConverter.CreateApplicationMessage(publishPacket); - _sessionsManager.DispatchApplicationMessage(applicationMessage, this); - - switch (publishPacket.QualityOfServiceLevel) - { - case MqttQualityOfServiceLevel.AtMostOnce: - { - return PlatformAbstractionLayer.CompletedTask; - } - case MqttQualityOfServiceLevel.AtLeastOnce: - { - var pubAckPacket = _channelAdapter.PacketFormatterAdapter.DataConverter.CreatePubAckPacket(publishPacket, MqttApplicationMessageReceivedReasonCode.Success); - return SendPacketAsync(pubAckPacket, cancellationToken); - } - case MqttQualityOfServiceLevel.ExactlyOnce: - { - var pubRecPacket = _channelAdapter.PacketFormatterAdapter.DataConverter.CreatePubRecPacket(publishPacket, MqttApplicationMessageReceivedReasonCode.Success); - return SendPacketAsync(pubRecPacket, cancellationToken); - } - default: - { - throw new MqttCommunicationException("Received a not supported QoS level."); - } - } - } - - void HandleTopicAlias(MqttPublishPacket publishPacket) - { - if (publishPacket.Properties?.TopicAlias == null) - { - return; - } - - var topicAlias = publishPacket.Properties.TopicAlias.Value; - - lock (_topicAlias) - { - if (!string.IsNullOrEmpty(publishPacket.Topic)) - { - _topicAlias[topicAlias] = publishPacket.Topic; - } - else - { - if (_topicAlias.TryGetValue(topicAlias, out var topic)) - { - publishPacket.Topic = topic; - } - else - { - } - } - } - } - - async Task TrySendDisconnectPacket(MqttClientDisconnectReason reason) - { - try - { - var disconnectOptions = new MqttClientDisconnectOptions - { - ReasonCode = reason, - ReasonString = reason.ToString() - }; - - var disconnectPacket = _channelAdapter.PacketFormatterAdapter.DataConverter.CreateDisconnectPacket(disconnectOptions); - - 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 (Reason = {1}).", ClientId, reason); - } - } - - async Task InvokeClientMessageQueueInterceptor(MqttPublishPacket publishPacket, MqttQueuedApplicationMessage queuedApplicationMessage) - { - if (_serverOptions.ClientMessageQueueInterceptor == null) - { - return publishPacket; - } - - var context = new MqttClientMessageQueueInterceptorContext - { - SenderClientId = queuedApplicationMessage.SenderClientId, - ReceiverClientId = ClientId, - ApplicationMessage = queuedApplicationMessage.ApplicationMessage, - SubscriptionQualityOfServiceLevel = queuedApplicationMessage.SubscriptionQualityOfServiceLevel - }; - - if (_serverOptions.ClientMessageQueueInterceptor != null) - { - await _serverOptions.ClientMessageQueueInterceptor.InterceptClientMessageQueueEnqueueAsync(context).ConfigureAwait(false); - } - - if (!context.AcceptEnqueue || context.ApplicationMessage == null) - { - return null; - } - - publishPacket.Topic = context.ApplicationMessage.Topic; - publishPacket.Payload = context.ApplicationMessage.Payload; - publishPacket.QualityOfServiceLevel = context.SubscriptionQualityOfServiceLevel; - - return publishPacket; - } - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Server/Internal/MqttClientConnectionStatistics.cs b/Source/MQTTnet/Server/Internal/MqttClientConnectionStatistics.cs deleted file mode 100644 index c9c16d1..0000000 --- a/Source/MQTTnet/Server/Internal/MqttClientConnectionStatistics.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.Threading; -using MQTTnet.Packets; -using MQTTnet.Server.Status; - -namespace MQTTnet.Server.Internal -{ - public sealed class MqttClientConnectionStatistics - { - readonly DateTime _connectedTimestamp; - - DateTime _lastNonKeepAlivePacketReceivedTimestamp; - DateTime _lastPacketReceivedTimestamp; - DateTime _lastPacketSentTimestamp; - - // Start with 1 because the CONNACK packet is not counted here. - long _receivedPacketsCount = 1; - - // Start with 1 because the CONNECT packet is not counted here. - long _sentPacketsCount = 1; - - long _receivedApplicationMessagesCount; - long _sentApplicationMessagesCount; - - public MqttClientConnectionStatistics() - { - _connectedTimestamp = DateTime.UtcNow; - - _lastPacketReceivedTimestamp = _connectedTimestamp; - _lastPacketSentTimestamp = _connectedTimestamp; - - _lastNonKeepAlivePacketReceivedTimestamp = _connectedTimestamp; - } - - /// - /// Timestamp of the last package that has been received from the client ("sent" from the client's perspective) - /// - public DateTime LastPacketSentTimestamp => _lastPacketSentTimestamp; - - /// - /// Timestamp of the last package that has been sent to the client ("received" from the client's perspective) - /// - public DateTime LastPacketReceivedTimestamp => _lastPacketReceivedTimestamp; - - - public void HandleReceivedPacket(MqttBasePacket packet) - { - if (packet == null) throw new ArgumentNullException(nameof(packet)); - - // This class is tracking all values from Clients perspective! - _lastPacketSentTimestamp = DateTime.UtcNow; - - Interlocked.Increment(ref _sentPacketsCount); - - if (packet is MqttPublishPacket) - { - Interlocked.Increment(ref _sentApplicationMessagesCount); - } - - if (!(packet is MqttPingReqPacket || packet is MqttPingRespPacket)) - { - _lastNonKeepAlivePacketReceivedTimestamp = _lastPacketReceivedTimestamp; - } - } - - public void HandleSentPacket(MqttBasePacket packet) - { - if (packet == null) throw new ArgumentNullException(nameof(packet)); - - // This class is tracking all values from Clients perspective! - _lastPacketReceivedTimestamp = DateTime.UtcNow; - - Interlocked.Increment(ref _receivedPacketsCount); - - if (packet is MqttPublishPacket) - { - Interlocked.Increment(ref _receivedApplicationMessagesCount); - } - } - - public void FillClientStatus(MqttClientStatus clientStatus) - { - if (clientStatus == null) throw new ArgumentNullException(nameof(clientStatus)); - - clientStatus.ConnectedTimestamp = _connectedTimestamp; - - clientStatus.ReceivedPacketsCount = Interlocked.Read(ref _receivedPacketsCount); - clientStatus.SentPacketsCount = Interlocked.Read(ref _sentPacketsCount); - - clientStatus.ReceivedApplicationMessagesCount = Interlocked.Read(ref _receivedApplicationMessagesCount); - clientStatus.SentApplicationMessagesCount = Interlocked.Read(ref _sentApplicationMessagesCount); - - clientStatus.LastPacketReceivedTimestamp = _lastPacketReceivedTimestamp; - clientStatus.LastPacketSentTimestamp = _lastPacketSentTimestamp; - - clientStatus.LastNonKeepAlivePacketReceivedTimestamp = _lastNonKeepAlivePacketReceivedTimestamp; - } - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Server/Internal/MqttClientSession.cs b/Source/MQTTnet/Server/Internal/MqttClientSession.cs deleted file mode 100644 index a943f5a..0000000 --- a/Source/MQTTnet/Server/Internal/MqttClientSession.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Collections.Generic; -using MQTTnet.Server.Status; - -namespace MQTTnet.Server.Internal -{ - public sealed class MqttClientSession : IDisposable - { - readonly DateTime _createdTimestamp = DateTime.UtcNow; - - public MqttClientSession( - string clientId, - IDictionary items, - MqttServerEventDispatcher eventDispatcher, - IMqttServerOptions serverOptions, - IMqttRetainedMessagesManager retainedMessagesManager, - bool isPersistent - ) - { - ClientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); - Items = items ?? throw new ArgumentNullException(nameof(items)); - IsPersistent = isPersistent; - SubscriptionsManager = new MqttClientSubscriptionsManager(this, serverOptions, eventDispatcher, retainedMessagesManager); - ApplicationMessagesQueue = new MqttClientSessionApplicationMessagesQueue(serverOptions); - } - - public string ClientId { get; } - - public bool IsCleanSession { get; set; } = true; - - /// - /// Session should persist if CleanSession was set to false (Mqtt3) or if SessionExpiryInterval != 0 (Mqtt5) - /// - public bool IsPersistent { get; set; } - - public MqttApplicationMessage WillMessage { get; set; } - - public MqttClientSubscriptionsManager SubscriptionsManager { get; } - - public MqttClientSessionApplicationMessagesQueue ApplicationMessagesQueue { get; } - - /// - /// Gets or sets a key/value collection that can be used to share data within the scope of this session. - /// - public IDictionary Items { get; } - - public void FillSessionStatus(MqttSessionStatus status) - { - status.ClientId = ClientId; - status.CreatedTimestamp = _createdTimestamp; - status.PendingApplicationMessagesCount = ApplicationMessagesQueue.Count; - status.Items = Items; - } - - public void Dispose() - { - ApplicationMessagesQueue?.Dispose(); - } - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Server/Internal/MqttClientSessionApplicationMessagesQueue.cs b/Source/MQTTnet/Server/Internal/MqttClientSessionApplicationMessagesQueue.cs deleted file mode 100644 index da49c5b..0000000 --- a/Source/MQTTnet/Server/Internal/MqttClientSessionApplicationMessagesQueue.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using MQTTnet.Internal; - -namespace MQTTnet.Server.Internal -{ - public sealed class MqttClientSessionApplicationMessagesQueue : IDisposable - { - readonly AsyncQueue _messageQueue = new AsyncQueue(); - - readonly IMqttServerOptions _options; - - public MqttClientSessionApplicationMessagesQueue(IMqttServerOptions options) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - } - - public int Count => _messageQueue.Count; - - public void Enqueue(MqttQueuedApplicationMessage queuedApplicationMessage) - { - if (queuedApplicationMessage == null) throw new ArgumentNullException(nameof(queuedApplicationMessage)); - - lock (_messageQueue) - { - if (_messageQueue.Count >= _options.MaxPendingMessagesPerClient) - { - if (_options.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropNewMessage) - { - return; - } - - if (_options.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage) - { - _messageQueue.TryDequeue(); - } - } - - _messageQueue.Enqueue(queuedApplicationMessage); - } - } - - public async Task Dequeue(CancellationToken cancellationToken) - { - var dequeueResult = await _messageQueue.TryDequeueAsync(cancellationToken).ConfigureAwait(false); - if (!dequeueResult.IsSuccess) - { - return null; - } - - return dequeueResult.Item; - } - - public void Clear() - { - _messageQueue.Clear(); - } - - public void Dispose() - { - _messageQueue?.Dispose(); - } - } -} diff --git a/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs b/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs index 471e9df..8517888 100644 --- a/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs +++ b/Source/MQTTnet/Server/Internal/MqttClientSessionsManager.cs @@ -1,503 +1,463 @@ +// 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.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using MQTTnet.Adapter; -using MQTTnet.Client.Disconnecting; using MQTTnet.Diagnostics; -using MQTTnet.Diagnostics.Logger; using MQTTnet.Exceptions; using MQTTnet.Formatter; using MQTTnet.Internal; using MQTTnet.Packets; using MQTTnet.Protocol; -using MQTTnet.Server.Status; -namespace MQTTnet.Server.Internal +namespace MQTTnet.Server { - public sealed class MqttClientSessionsManager : IDisposable + public sealed class MqttClientSessionsManager : ISubscriptionChangedNotification, IDisposable { - readonly BlockingCollection _messageQueue = - new BlockingCollection(); + readonly Dictionary _clients = new Dictionary(4096); readonly AsyncLock _createConnectionSyncRoot = new AsyncLock(); + readonly MqttServerEventContainer _eventContainer; + readonly MqttNetSourceLogger _logger; + readonly MqttServerOptions _options; + readonly MqttPacketFactories _packetFactories = new MqttPacketFactories(); - readonly Dictionary _clientConnections = - new Dictionary(4096); - - readonly Dictionary - _clientSessions = new Dictionary(4096); - - readonly IDictionary _serverSessionItems = new ConcurrentDictionary(); + readonly MqttRetainedMessagesManager _retainedMessagesManager; + readonly IMqttNetLogger _rootLogger; - readonly MqttServerEventDispatcher _eventDispatcher; + // The _sessions dictionary contains all session, the _subscriberSessions hash set contains subscriber sessions only. + // See the MqttSubscription object for a detailed explanation. + readonly Dictionary _sessions = new Dictionary(4096); - readonly IMqttRetainedMessagesManager _retainedMessagesManager; - readonly IMqttServerOptions _options; - readonly MqttNetSourceLogger _logger; - readonly IMqttNetLogger _rootLogger; + readonly object _sessionsManagementLock = new object(); + readonly HashSet _subscriberSessions = new HashSet(); public MqttClientSessionsManager( - IMqttServerOptions options, - IMqttRetainedMessagesManager retainedMessagesManager, - MqttServerEventDispatcher eventDispatcher, + MqttServerOptions options, + MqttRetainedMessagesManager retainedMessagesManager, + MqttServerEventContainer eventContainer, IMqttNetLogger logger) { - if (logger == null) throw new ArgumentNullException(nameof(logger)); + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + _logger = logger.WithSource(nameof(MqttClientSessionsManager)); _rootLogger = logger; - _eventDispatcher = eventDispatcher ?? throw new ArgumentNullException(nameof(eventDispatcher)); _options = options ?? throw new ArgumentNullException(nameof(options)); - _retainedMessagesManager = retainedMessagesManager ?? - throw new ArgumentNullException(nameof(retainedMessagesManager)); + _retainedMessagesManager = retainedMessagesManager ?? throw new ArgumentNullException(nameof(retainedMessagesManager)); + _eventContainer = eventContainer ?? throw new ArgumentNullException(nameof(eventContainer)); } - public void Start(CancellationToken cancellationToken) + public async Task CloseAllConnectionsAsync() { - Task.Run(() => TryProcessQueuedApplicationMessagesAsync(cancellationToken), cancellationToken) - .RunInBackground(_logger); + List connections; + lock (_clients) + { + connections = _clients.Values.ToList(); + _clients.Clear(); + } + + foreach (var connection in connections) + { + await connection.StopAsync(MqttDisconnectReasonCode.NormalDisconnection).ConfigureAwait(false); + } } - async Task ReceiveConnectPacket(IMqttChannelAdapter channelAdapter, - CancellationToken cancellationToken) + public async Task DeleteSessionAsync(string clientId) { - try + MqttClient connection; + + lock (_clients) { - using (var timeoutToken = new CancellationTokenSource(_options.DefaultCommunicationTimeout)) - using (var effectiveCancellationToken = - CancellationTokenSource.CreateLinkedTokenSource(timeoutToken.Token, cancellationToken)) + _clients.TryGetValue(clientId, out connection); + } + + MqttSession session; + + lock (_sessionsManagementLock) + { + _sessions.TryGetValue(clientId, out session); + _sessions.Remove(clientId); + + if (session != null) { - var firstPacket = await channelAdapter.ReceivePacketAsync(effectiveCancellationToken.Token) - .ConfigureAwait(false); + _subscriberSessions.Remove(session); + } + } - if (firstPacket is MqttConnectPacket connectPacket) - { - return connectPacket; - } + try + { + if (connection != null) + { + await connection.StopAsync(MqttDisconnectReasonCode.NormalDisconnection).ConfigureAwait(false); } } - catch (OperationCanceledException) + catch (Exception exception) { - _logger.Warning("Client '{0}': Connected but did not sent a CONNECT packet.", channelAdapter.Endpoint); + _logger.Error(exception, $"Error while deleting session '{clientId}'."); } - catch (MqttCommunicationTimedOutException) + + try { - _logger.Warning("Client '{0}': Connected but did not sent a CONNECT packet.", channelAdapter.Endpoint); + if (_eventContainer.SessionDeletedEvent.HasHandlers) + { + var eventArgs = new SessionDeletedEventArgs + { + Id = session?.Id + }; + + await _eventContainer.SessionDeletedEvent.TryInvokeAsync(eventArgs, _logger).ConfigureAwait(false); + } + } + catch (Exception exception) + { + _logger.Error(exception, $"Error while executing session deleted event for session '{clientId}'."); } - _logger.Warning("Client '{0}': First received packet was no 'CONNECT' packet [MQTT-3.1.0-1].", - channelAdapter.Endpoint); - return null; + session?.Dispose(); + + _logger.Verbose("Session for client '{0}' deleted.", clientId); } - public async Task HandleClientConnectionAsync(IMqttChannelAdapter channelAdapter, - CancellationToken cancellationToken) + public async Task DispatchApplicationMessage(string senderId, MqttApplicationMessage applicationMessage) { - MqttClientConnection clientConnection = null; - try { - var connectPacket = await ReceiveConnectPacket(channelAdapter, cancellationToken).ConfigureAwait(false); - if (connectPacket == null) + if (applicationMessage.Retain) { - // Nothing was received in time etc. - return; + await _retainedMessagesManager.UpdateMessage(senderId, applicationMessage).ConfigureAwait(false); } - MqttConnAckPacket connAckPacket; - - var connectionValidatorContext = - await ValidateConnection(connectPacket, channelAdapter).ConfigureAwait(false); - if (connectionValidatorContext.ReasonCode != MqttConnectReasonCode.Success) + var deliveryCount = 0; + List subscriberSessions; + lock (_sessionsManagementLock) { - // Send failure response here without preparing a session! - connAckPacket = - channelAdapter.PacketFormatterAdapter.DataConverter.CreateConnAckPacket( - connectionValidatorContext); - await channelAdapter.SendPacketAsync(connAckPacket, cancellationToken).ConfigureAwait(false); - return; + // only subscriber clients are of interest here. + subscriberSessions = _subscriberSessions.ToList(); } - connAckPacket = - channelAdapter.PacketFormatterAdapter.DataConverter.CreateConnAckPacket(connectionValidatorContext); + // Calculate application message topic hash once for subscription checks + MqttSubscription.CalculateTopicHash(applicationMessage.Topic, out var topicHash, out _, out _); - // Pass connAckPacket so that IsSessionPresent flag can be set if the client session already exists - clientConnection = await CreateClientConnection(connectPacket, connAckPacket, channelAdapter, - connectionValidatorContext).ConfigureAwait(false); + foreach (var session in subscriberSessions) + { + var checkSubscriptionsResult = session.SubscriptionsManager.CheckSubscriptions( + applicationMessage.Topic, + topicHash, + applicationMessage.QualityOfServiceLevel, + senderId); - await channelAdapter.SendPacketAsync(connAckPacket, cancellationToken).ConfigureAwait(false); + if (!checkSubscriptionsResult.IsSubscribed) + { + continue; + } - await _eventDispatcher.SafeNotifyClientConnectedAsync(connectPacket, channelAdapter) - .ConfigureAwait(false); + var newPublishPacket = _packetFactories.Publish.Create(applicationMessage); + newPublishPacket.QualityOfServiceLevel = checkSubscriptionsResult.QualityOfServiceLevel; + newPublishPacket.SubscriptionIdentifiers = checkSubscriptionsResult.SubscriptionIdentifiers; - await clientConnection.RunAsync().ConfigureAwait(false); - } - catch (OperationCanceledException) - { - } - catch (Exception exception) - { - _logger.Error(exception, exception.Message); - } - finally - { - if (clientConnection != null) - { - if (clientConnection.ClientId != null) + if (newPublishPacket.QualityOfServiceLevel > 0) { - // in case it is a takeover _clientConnections already contains the new connection - if (!clientConnection.IsTakenOver) - { - lock (_clientConnections) - { - _clientConnections.Remove(clientConnection.ClientId); - } - - if ((!_options.EnablePersistentSessions) || (!clientConnection.Session.IsPersistent)) - { - await DeleteSessionAsync(clientConnection.ClientId).ConfigureAwait(false); - } - } + newPublishPacket.PacketIdentifier = session.PacketIdentifierProvider.GetNextPacketIdentifier(); } - var endpoint = clientConnection.Endpoint; - - if (clientConnection.ClientId != null && !clientConnection.IsTakenOver) + if (checkSubscriptionsResult.RetainAsPublished) { - // The event is fired at a separate place in case of a handover! - await _eventDispatcher.SafeNotifyClientDisconnectedAsync( - clientConnection.ClientId, - clientConnection.IsCleanDisconnect - ? MqttClientDisconnectType.Clean - : MqttClientDisconnectType.NotClean, - endpoint).ConfigureAwait(false); + // Transfer the original retain state from the publisher. This is a MQTTv5 feature. + newPublishPacket.Retain = applicationMessage.Retain; + } + else + { + newPublishPacket.Retain = false; } - } - await channelAdapter.DisconnectAsync(_options.DefaultCommunicationTimeout, CancellationToken.None) - .ConfigureAwait(false); - } - } + session.EnqueuePacket(new MqttPacketBusItem(newPublishPacket)); + deliveryCount++; - public async Task CloseAllConnectionsAsync() - { - List connections; - lock (_clientConnections) - { - connections = _clientConnections.Values.ToList(); - _clientConnections.Clear(); - } + _logger.Verbose("Client '{0}': Queued PUBLISH packet with topic '{1}'.", session.Id, applicationMessage.Topic); + } - foreach (var connection in connections) - { - await connection.StopAsync(MqttClientDisconnectReason.NormalDisconnection).ConfigureAwait(false); + await FireApplicationMessageNotConsumedEvent(applicationMessage, deliveryCount, senderId); } - } - - public List GetConnections() - { - lock (_clientConnections) + catch (Exception exception) { - return _clientConnections.Values.ToList(); + _logger.Error(exception, "Unhandled exception while processing next queued application message."); } } - public Task> GetClientStatusAsync() + public void Dispose() { - var result = new List(); + _createConnectionSyncRoot?.Dispose(); - lock (_clientConnections) + lock (_sessionsManagementLock) { - foreach (var connection in _clientConnections.Values) + foreach (var sessionItem in _sessions) { - var clientStatus = new MqttClientStatus(connection); - connection.FillClientStatus(clientStatus); - - var sessionStatus = new MqttSessionStatus(connection.Session, this); - connection.Session.FillSessionStatus(sessionStatus); - clientStatus.Session = sessionStatus; - - result.Add(clientStatus); + sessionItem.Value.Dispose(); } } - - return Task.FromResult((IList) result); } - public Task> GetSessionStatusAsync() + public MqttClient GetClient(string id) { - var result = new List(); - - lock (_clientSessions) + lock (_clients) { - foreach (var session in _clientSessions.Values) + if (!_clients.TryGetValue(id, out var client)) { - var sessionStatus = new MqttSessionStatus(session, this); - session.FillSessionStatus(sessionStatus); - - result.Add(sessionStatus); + throw new InvalidOperationException($"Client with ID '{id}' not found."); } - } - - return Task.FromResult((IList) result); - } - - public void DispatchApplicationMessage(MqttApplicationMessage applicationMessage, MqttClientConnection sender) - { - if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); - - _messageQueue.Add(new MqttPendingApplicationMessage(applicationMessage, sender)); - } - - public async Task SubscribeAsync(string clientId, ICollection topicFilters) - { - if (clientId == null) throw new ArgumentNullException(nameof(clientId)); - if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); - - if (topicFilters is null) throw new ArgumentNullException(nameof(topicFilters)); - - var fakeSubscribePacket = new MqttSubscribePacket - { - TopicFilters = topicFilters.ToList() - }; - - var clientSession = GetClientSession(clientId); - var subscribeResult = await clientSession.SubscriptionsManager.Subscribe(fakeSubscribePacket) - .ConfigureAwait(false); - - foreach (var retainedApplicationMessage in subscribeResult.RetainedApplicationMessages) - { - clientSession.ApplicationMessagesQueue.Enqueue(retainedApplicationMessage); + return client; } } - public Task UnsubscribeAsync(string clientId, ICollection topicFilters) + public List GetClients() { - if (clientId == null) throw new ArgumentNullException(nameof(clientId)); - if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); - - var fakeUnsubscribePacket = new MqttUnsubscribePacket + lock (_clients) { - TopicFilters = topicFilters.ToList() - }; - - return GetClientSession(clientId).SubscriptionsManager.Unsubscribe(fakeUnsubscribePacket); + return _clients.Values.ToList(); + } } - public async Task DeleteSessionAsync(string clientId) + public Task> GetClientStatusAsync() { - MqttClientConnection connection; - MqttClientSession session; - - lock (_clientConnections) - { - _clientConnections.TryGetValue(clientId, out connection); - } + var result = new List(); - lock (_clientSessions) + lock (_clients) { - _clientSessions.TryGetValue(clientId, out session); - _clientSessions.Remove(clientId); - } + foreach (var connection in _clients.Values) + { + var clientStatus = new MqttClientStatus(connection) + { + Session = new MqttSessionStatus(connection.Session) + }; - if (connection != null) - { - await connection.StopAsync(MqttClientDisconnectReason.NormalDisconnection).ConfigureAwait(false); + result.Add(clientStatus); + } } - session?.Dispose(); - - _logger.Verbose("Session for client '{0}' deleted.", clientId); + return Task.FromResult((IList)result); } - public void Dispose() + public Task> GetSessionStatusAsync() { - _messageQueue?.Dispose(); - } + var result = new List(); - async Task TryProcessQueuedApplicationMessagesAsync(CancellationToken cancellationToken) - { - // Make sure all queued messages are proccessed befor server stops. - while (!cancellationToken.IsCancellationRequested || _messageQueue.Any()) + lock (_sessionsManagementLock) { - try - { - await TryProcessNextQueuedApplicationMessage().ConfigureAwait(false); - } - catch (OperationCanceledException) - { - } - catch (Exception exception) + foreach (var sessionItem in _sessions) { - _logger.Error(exception, "Unhandled exception while processing queued application messages."); + var sessionStatus = new MqttSessionStatus(sessionItem.Value); + result.Add(sessionStatus); } } + + return Task.FromResult((IList)result); } - async Task TryProcessNextQueuedApplicationMessage() + public async Task HandleClientConnectionAsync(IMqttChannelAdapter channelAdapter, CancellationToken cancellationToken) { + MqttClient client = null; + try { - MqttPendingApplicationMessage pendingApplicationMessage; - try - { - pendingApplicationMessage = _messageQueue.Take(); - } - catch (ArgumentNullException) + var connectPacket = await ReceiveConnectPacket(channelAdapter, cancellationToken).ConfigureAwait(false); + if (connectPacket == null) { + // Nothing was received in time etc. return; } - catch (ObjectDisposedException) + + var validatingConnectionEventArgs = await ValidateConnection(connectPacket, channelAdapter).ConfigureAwait(false); + var connAckPacket = _packetFactories.ConnAck.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; } - var clientConnection = pendingApplicationMessage.Sender; - var senderClientId = clientConnection?.ClientId ?? _options.ClientId; - var applicationMessage = pendingApplicationMessage.ApplicationMessage; + // Pass connAckPacket so that IsSessionPresent flag can be set if the client session already exists. + client = await CreateClientConnection(connectPacket, connAckPacket, channelAdapter, validatingConnectionEventArgs).ConfigureAwait(false); + + await client.SendPacketAsync(connAckPacket, cancellationToken).ConfigureAwait(false); - var interceptor = _options.ApplicationMessageInterceptor; - if (interceptor != null) + if (_eventContainer.ClientConnectedEvent.HasHandlers) { - var interceptorContext = - await InterceptApplicationMessageAsync(interceptor, clientConnection, applicationMessage) - .ConfigureAwait(false); - if (interceptorContext != null) + var eventArgs = new ClientConnectedEventArgs { - if (interceptorContext.CloseConnection) + ClientId = connectPacket.ClientId, + UserName = connectPacket.Username, + ProtocolVersion = channelAdapter.PacketFormatterAdapter.ProtocolVersion, + Endpoint = channelAdapter.Endpoint + }; + + await _eventContainer.ClientConnectedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + } + + await client.RunAsync().ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + catch (Exception exception) + { + _logger.Error(exception, exception.Message); + } + finally + { + if (client != null) + { + if (client.Id != null) + { + // in case it is a takeover _clientConnections already contains the new connection + if (!client.IsTakenOver) { - if (clientConnection != null) + lock (_clients) { - await clientConnection.StopAsync(MqttClientDisconnectReason.NormalDisconnection) - .ConfigureAwait(false); + _clients.Remove(client.Id); + } + + if (!_options.EnablePersistentSessions || !client.Session.IsPersistent) + { + await DeleteSessionAsync(client.Id).ConfigureAwait(false); } } + } + + var endpoint = client.Endpoint; - if (interceptorContext.ApplicationMessage == null || !interceptorContext.AcceptPublish) + if (client.Id != null && !client.IsTakenOver && _eventContainer.ClientDisconnectedEvent.HasHandlers) + { + var eventArgs = new ClientDisconnectedEventArgs { - return; - } + ClientId = client.Id, + DisconnectType = client.IsCleanDisconnect ? MqttClientDisconnectType.Clean : MqttClientDisconnectType.NotClean, + Endpoint = endpoint + }; - applicationMessage = interceptorContext.ApplicationMessage; + await _eventContainer.ClientDisconnectedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); } } - await _eventDispatcher.SafeNotifyApplicationMessageReceivedAsync(senderClientId, applicationMessage) - .ConfigureAwait(false); - - if (applicationMessage.Retain) + using (var timeout = new CancellationTokenSource(_options.DefaultCommunicationTimeout)) { - await _retainedMessagesManager.HandleMessageAsync(senderClientId, applicationMessage) - .ConfigureAwait(false); + await channelAdapter.DisconnectAsync(timeout.Token).ConfigureAwait(false); } + } + } - var deliveryCount = 0; - List sessions; - lock (_clientSessions) + public void OnSubscriptionsAdded(MqttSession clientSession, List topics) + { + lock (_sessionsManagementLock) + { + if (!clientSession.HasSubscribedTopics) { - sessions = _clientSessions.Values.ToList(); + // first subscribed topic + _subscriberSessions.Add(clientSession); } - foreach (var clientSession in sessions) + foreach (var topic in topics) { - var checkSubscriptionsResult = clientSession.SubscriptionsManager.CheckSubscriptions( - applicationMessage.Topic, - applicationMessage.QualityOfServiceLevel, - senderClientId); - - if (!checkSubscriptionsResult.IsSubscribed) - { - continue; - } - - _logger.Verbose("Client '{0}': Queued application message with topic '{1}'.", - clientSession.ClientId, applicationMessage.Topic); - - var queuedApplicationMessage = new MqttQueuedApplicationMessage - { - ApplicationMessage = applicationMessage, - SubscriptionQualityOfServiceLevel = checkSubscriptionsResult.QualityOfServiceLevel, - SubscriptionIdentifiers = checkSubscriptionsResult.SubscriptionIdentifiers - }; - - if (checkSubscriptionsResult.RetainAsPublished) - { - // Transfer the original retain state from the publisher. - // This is a MQTTv5 feature. - queuedApplicationMessage.IsRetainedMessage = applicationMessage.Retain; - } - - clientSession.ApplicationMessagesQueue.Enqueue(queuedApplicationMessage); - deliveryCount++; + clientSession.AddSubscribedTopic(topic); } + } + } - if (deliveryCount == 0) + public void OnSubscriptionsRemoved(MqttSession clientSession, List subscriptionTopics) + { + lock (_sessionsManagementLock) + { + foreach (var subscriptionTopic in subscriptionTopics) { - var undeliveredMessageInterceptor = _options.UndeliveredMessageInterceptor; - if (undeliveredMessageInterceptor == null) - { - return; - } + clientSession.RemoveSubscribedTopic(subscriptionTopic); + } - // The delegate signature is the same as for regular message interceptor. So the call is fine and just uses a different interceptor. - await InterceptApplicationMessageAsync(undeliveredMessageInterceptor, clientConnection, - applicationMessage).ConfigureAwait(false); + if (!clientSession.HasSubscribedTopics) + { + // last subscription removed + _subscriberSessions.Remove(clientSession); } } - catch (Exception exception) + } + + public void Start() + { + if (!_options.EnablePersistentSessions) { - _logger.Error(exception, "Unhandled exception while processing next queued application message."); + _sessions.Clear(); } } - async Task ValidateConnection(MqttConnectPacket connectPacket, - IMqttChannelAdapter channelAdapter) + public async Task SubscribeAsync(string clientId, ICollection topicFilters) { - var context = new MqttConnectionValidatorContext(connectPacket, channelAdapter) + if (clientId == null) { - SessionItems = new ConcurrentDictionary() - }; - - var connectionValidator = _options.ConnectionValidator; + throw new ArgumentNullException(nameof(clientId)); + } - if (connectionValidator == null) + if (topicFilters == null) { - context.ReasonCode = MqttConnectReasonCode.Success; - return context; + throw new ArgumentNullException(nameof(topicFilters)); } - await connectionValidator.ValidateConnectionAsync(context).ConfigureAwait(false); + var fakeSubscribePacket = new MqttSubscribePacket(); + fakeSubscribePacket.TopicFilters.AddRange(topicFilters); - // Check the client ID and set a random one if supported. - if (string.IsNullOrEmpty(connectPacket.ClientId) && - channelAdapter.PacketFormatterAdapter.ProtocolVersion == MqttProtocolVersion.V500) + var clientSession = GetClientSession(clientId); + + var subscribeResult = await clientSession.SubscriptionsManager.Subscribe(fakeSubscribePacket, CancellationToken.None).ConfigureAwait(false); + + if (subscribeResult.RetainedMessages != null) { - connectPacket.ClientId = context.AssignedClientIdentifier; + foreach (var retainedApplicationMessage in subscribeResult.RetainedMessages) + { + var publishPacket = _packetFactories.Publish.Create(retainedApplicationMessage.ApplicationMessage); + clientSession.EnqueuePacket(new MqttPacketBusItem(publishPacket)); + } } + } - if (string.IsNullOrEmpty(connectPacket.ClientId)) + public Task UnsubscribeAsync(string clientId, ICollection topicFilters) + { + if (clientId == null) { - context.ReasonCode = MqttConnectReasonCode.ClientIdentifierNotValid; + throw new ArgumentNullException(nameof(clientId)); } - return context; + if (topicFilters == null) + { + throw new ArgumentNullException(nameof(topicFilters)); + } + + var fakeUnsubscribePacket = new MqttUnsubscribePacket(); + fakeUnsubscribePacket.TopicFilters.AddRange(topicFilters); + + return GetClientSession(clientId).SubscriptionsManager.Unsubscribe(fakeUnsubscribePacket, CancellationToken.None); } - async Task CreateClientConnection( + async Task CreateClientConnection( MqttConnectPacket connectPacket, MqttConnAckPacket connAckPacket, IMqttChannelAdapter channelAdapter, - MqttConnectionValidatorContext context) + ValidatingConnectionEventArgs validatingConnectionEventArgs) { - MqttClientConnection connection; + MqttClient connection; bool sessionShouldPersist; - if (context.ProtocolVersion == MqttProtocolVersion.V500) + if (validatingConnectionEventArgs.ProtocolVersion == 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]. @@ -507,7 +467,7 @@ namespace MQTTnet.Server.Internal // in each time it connects. // Persist if SessionExpiryInterval != 0, but may start with a clean session - sessionShouldPersist = context.SessionExpiryInterval.GetValueOrDefault() != 0; + sessionShouldPersist = validatingConnectionEventArgs.SessionExpiryInterval != 0; } else { @@ -522,20 +482,19 @@ namespace MQTTnet.Server.Internal using (await _createConnectionSyncRoot.WaitAsync(CancellationToken.None).ConfigureAwait(false)) { - MqttClientSession session; - lock (_clientSessions) + MqttSession session; + lock (_sessionsManagementLock) { - if (!_clientSessions.TryGetValue(connectPacket.ClientId, out session)) + if (!_sessions.TryGetValue(connectPacket.ClientId, out session)) { - _logger.Verbose("Created a new session for client '{0}'.", connectPacket.ClientId); - session = CreateSession(connectPacket.ClientId, context.SessionItems, sessionShouldPersist); + session = CreateSession(connectPacket.ClientId, validatingConnectionEventArgs.SessionItems, sessionShouldPersist); } else { if (connectPacket.CleanSession) { _logger.Verbose("Deleting existing session of client '{0}'.", connectPacket.ClientId); - session = CreateSession(connectPacket.ClientId, context.SessionItems, sessionShouldPersist); + session = CreateSession(connectPacket.ClientId, validatingConnectionEventArgs.SessionItems, sessionShouldPersist); } else { @@ -543,74 +502,90 @@ namespace MQTTnet.Server.Internal // Session persistence could change for MQTT 5 clients that reconnect with different SessionExpiryInterval session.IsPersistent = sessionShouldPersist; connAckPacket.IsSessionPresent = true; + session.Recover(); } } - _clientSessions[connectPacket.ClientId] = session; + _sessions[connectPacket.ClientId] = session; } - MqttClientConnection existingConnection; + 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.InvokeAsync(preparingSessionEventArgs).ConfigureAwait(false); + } - lock (_clientConnections) + MqttClient existing; + + lock (_clients) { - _clientConnections.TryGetValue(connectPacket.ClientId, out existingConnection); + _clients.TryGetValue(connectPacket.ClientId, out existing); connection = CreateConnection(connectPacket, channelAdapter, session); - _clientConnections[connectPacket.ClientId] = connection; + _clients[connectPacket.ClientId] = connection; } - if (existingConnection != null) + if (existing != null) { - await _eventDispatcher.SafeNotifyClientDisconnectedAsync(existingConnection.ClientId, - MqttClientDisconnectType.Takeover, existingConnection.Endpoint); + existing.IsTakenOver = true; + await existing.StopAsync(MqttDisconnectReasonCode.SessionTakenOver).ConfigureAwait(false); - existingConnection.IsTakenOver = true; - await existingConnection.StopAsync(MqttClientDisconnectReason.SessionTakenOver) - .ConfigureAwait(false); + if (_eventContainer.ClientConnectedEvent.HasHandlers) + { + var eventArgs = new ClientDisconnectedEventArgs + { + ClientId = existing.Id, + DisconnectType = MqttClientDisconnectType.Takeover, + Endpoint = existing.Endpoint + }; + + await _eventContainer.ClientDisconnectedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + } } } return connection; } - async Task InterceptApplicationMessageAsync( - IMqttServerApplicationMessageInterceptor interceptor, - MqttClientConnection clientConnection, - MqttApplicationMessage applicationMessage) + MqttClient CreateConnection(MqttConnectPacket connectPacket, IMqttChannelAdapter channelAdapter, MqttSession session) { - string senderClientId; - IDictionary sessionItems; + return new MqttClient(connectPacket, channelAdapter, session, _options, _eventContainer, this, _rootLogger); + } - var messageIsFromServer = clientConnection == null; - if (messageIsFromServer) + MqttSession CreateSession(string clientId, IDictionary sessionItems, bool isPersistent) + { + _logger.Verbose("Created a new session for client '{0}'.", clientId); + + return new MqttSession(clientId, isPersistent, sessionItems, _options, _eventContainer, _retainedMessagesManager, this); + } + + async Task FireApplicationMessageNotConsumedEvent(MqttApplicationMessage applicationMessage, int deliveryCount, string senderId) + { + if (deliveryCount > 0) { - senderClientId = _options.ClientId; - sessionItems = _serverSessionItems; + return; } - else + + if (!_eventContainer.ApplicationMessageNotConsumedEvent.HasHandlers) { - senderClientId = clientConnection.ClientId; - sessionItems = clientConnection.Session.Items; + return; } - var interceptorContext = new MqttApplicationMessageInterceptorContext + var eventArgs = new ApplicationMessageNotConsumedEventArgs { - ClientId = senderClientId, - SessionItems = sessionItems, - AcceptPublish = true, ApplicationMessage = applicationMessage, - CloseConnection = false + SenderId = senderId }; - await interceptor.InterceptApplicationMessagePublishAsync(interceptorContext).ConfigureAwait(false); - return interceptorContext; + await _eventContainer.ApplicationMessageNotConsumedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); } - MqttClientSession GetClientSession(string clientId) + MqttSession GetClientSession(string clientId) { - lock (_clientSessions) + lock (_sessionsManagementLock) { - if (!_clientSessions.TryGetValue(clientId, out var session)) + if (!_sessions.TryGetValue(clientId, out var session)) { throw new InvalidOperationException($"Client session '{clientId}' is unknown."); } @@ -619,28 +594,54 @@ namespace MQTTnet.Server.Internal } } - MqttClientConnection CreateConnection(MqttConnectPacket connectPacket, IMqttChannelAdapter channelAdapter, - MqttClientSession session) + async Task ReceiveConnectPacket(IMqttChannelAdapter channelAdapter, CancellationToken cancellationToken) { - return new MqttClientConnection( - connectPacket, - channelAdapter, - session, - _options, - this, - _rootLogger); + 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; } - MqttClientSession CreateSession(string clientId, IDictionary sessionItems, bool isPersistent) + async Task ValidateConnection(MqttConnectPacket connectPacket, IMqttChannelAdapter channelAdapter) { - return new MqttClientSession( - clientId, - sessionItems, - _eventDispatcher, - _options, - _retainedMessagesManager, - isPersistent - ); + var context = new ValidatingConnectionEventArgs(connectPacket, channelAdapter) + { + SessionItems = new ConcurrentDictionary() + }; + + await _eventContainer.ValidatingConnectionEvent.InvokeAsync(context).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 = context.AssignedClientIdentifier; + } + + if (string.IsNullOrEmpty(connectPacket.ClientId)) + { + context.ReasonCode = MqttConnectReasonCode.ClientIdentifierNotValid; + } + + return context; } } } \ No newline at end of file diff --git a/Source/MQTTnet/Server/Internal/MqttClientStatistics.cs b/Source/MQTTnet/Server/Internal/MqttClientStatistics.cs new file mode 100644 index 0000000..a0d6c07 --- /dev/null +++ b/Source/MQTTnet/Server/Internal/MqttClientStatistics.cs @@ -0,0 +1,95 @@ +// 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.Threading; +using MQTTnet.Packets; + +namespace MQTTnet.Server +{ + public sealed class MqttClientStatistics + { + // Start with 1 because the CONNACK packet is not counted here. + long _receivedPacketsCount = 1; + + // Start with 1 because the CONNECT packet is not counted here. + long _sentPacketsCount = 1; + + long _receivedApplicationMessagesCount; + long _sentApplicationMessagesCount; + + public MqttClientStatistics() + { + ConnectedTimestamp = DateTime.UtcNow; + + LastPacketReceivedTimestamp = ConnectedTimestamp; + LastPacketSentTimestamp = ConnectedTimestamp; + + LastNonKeepAlivePacketReceivedTimestamp = ConnectedTimestamp; + } + + public DateTime ConnectedTimestamp { get; } + + /// + /// Timestamp of the last package that has been sent to the client ("received" from the client's perspective) + /// + public DateTime LastPacketReceivedTimestamp { get; private set; } + + /// + /// Timestamp of the last package that has been received from the client ("sent" from the client's perspective) + /// + public DateTime LastPacketSentTimestamp { get; private set; } + + public DateTime LastNonKeepAlivePacketReceivedTimestamp { get; private set; } + + public long SentApplicationMessagesCount => Interlocked.Read(ref _sentApplicationMessagesCount); + + public long ReceivedApplicationMessagesCount => Interlocked.Read(ref _receivedApplicationMessagesCount); + + public long SentPacketsCount => Interlocked.Read(ref _sentPacketsCount); + + public long ReceivedPacketsCount => Interlocked.Read(ref _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 _sentPacketsCount); + + if (packet is MqttPublishPacket) + { + Interlocked.Increment(ref _sentApplicationMessagesCount); + } + + if (!(packet is MqttPingReqPacket || packet is MqttPingRespPacket)) + { + LastNonKeepAlivePacketReceivedTimestamp = LastPacketReceivedTimestamp; + } + } + + 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 _receivedPacketsCount); + + if (packet is MqttPublishPacket) + { + Interlocked.Increment(ref _receivedApplicationMessagesCount); + } + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/Internal/MqttClientSubscriptionsManager.cs b/Source/MQTTnet/Server/Internal/MqttClientSubscriptionsManager.cs index b6c1c39..b1174fe 100644 --- a/Source/MQTTnet/Server/Internal/MqttClientSubscriptionsManager.cs +++ b/Source/MQTTnet/Server/Internal/MqttClientSubscriptionsManager.cs @@ -1,4 +1,8 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -6,313 +10,524 @@ using System.Threading.Tasks; using MQTTnet.Packets; using MQTTnet.Protocol; -namespace MQTTnet.Server.Internal +namespace MQTTnet.Server { - public sealed class MqttClientSubscriptionsManager + public sealed class MqttClientSubscriptionsManager : IDisposable { - // We use a reader writer lock because the subscriptions are only read most of the time. - // Since writing is done by multiple threads (server API or connection thread), we cannot avoid locking - // completely by swapping references etc. - readonly ReaderWriterLockSlim _subscriptionsLock = new ReaderWriterLockSlim(); - readonly Dictionary _subscriptions = new Dictionary(4096); - readonly MqttClientSession _clientSession; - readonly IMqttServerOptions _options; - readonly MqttServerEventDispatcher _eventDispatcher; - readonly IMqttRetainedMessagesManager _retainedMessagesManager; + static readonly List EmptySubscriptionIdentifiers = new List(); + + readonly MqttServerEventContainer _eventContainer; + readonly Dictionary> _noWildcardSubscriptionsByTopicHash = new Dictionary>(); + 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 _subscriptions = new Dictionary(); + + // Use subscription lock to maintain consistency across subscriptions and topic hash dictionaries + readonly SemaphoreSlim _subscriptionsLock = new SemaphoreSlim(1); + readonly Dictionary _wildcardSubscriptionsByTopicHash = new Dictionary(); public MqttClientSubscriptionsManager( - MqttClientSession clientSession, - IMqttServerOptions serverOptions, - MqttServerEventDispatcher eventDispatcher, - IMqttRetainedMessagesManager retainedMessagesManager) + MqttSession session, + MqttServerEventContainer eventContainer, + MqttRetainedMessagesManager retainedMessagesManager, + ISubscriptionChangedNotification subscriptionChangedNotification) { - _clientSession = clientSession ?? throw new ArgumentNullException(nameof(clientSession)); - _options = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions)); - _eventDispatcher = eventDispatcher ?? throw new ArgumentNullException(nameof(eventDispatcher)); + _session = session ?? throw new ArgumentNullException(nameof(session)); + _eventContainer = eventContainer ?? throw new ArgumentNullException(nameof(eventContainer)); _retainedMessagesManager = retainedMessagesManager ?? throw new ArgumentNullException(nameof(retainedMessagesManager)); + _subscriptionChangedNotification = subscriptionChangedNotification; } - public async Task Subscribe(MqttSubscribePacket subscribePacket) + public CheckSubscriptionsResult CheckSubscriptions(string topic, ulong topicHash, MqttQualityOfServiceLevel applicationMessageQoSLevel, string senderClientId) { - if (subscribePacket == null) throw new ArgumentNullException(nameof(subscribePacket)); + var possibleSubscriptions = new List(); - var retainedApplicationMessages = await _retainedMessagesManager.GetMessagesAsync().ConfigureAwait(false); - var result = new SubscribeResult(); + // Check for possible subscriptions. They might have collisions but this is fine. + _subscriptionsLock.Wait(); + try + { + if (_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var noWildcardSubscriptions)) + { + possibleSubscriptions.AddRange(noWildcardSubscriptions.ToList()); + } - // The topic filters are order by its QoS so that the higher QoS will win over a - // lower one. - foreach (var originalTopicFilter in subscribePacket.TopicFilters.OrderByDescending(f => f.QualityOfServiceLevel)) + foreach (var wcs in _wildcardSubscriptionsByTopicHash) + { + var wildcardSubscriptions = wcs.Value; + var subscriptionHash = wcs.Key; + var subscriptionHashMask = wildcardSubscriptions.HashMask; + + if ((topicHash & subscriptionHashMask) == subscriptionHash) + { + possibleSubscriptions.AddRange(wildcardSubscriptions.Subscriptions.ToList()); + } + } + } + finally { - var interceptorContext = await InterceptSubscribe(originalTopicFilter).ConfigureAwait(false); - var finalTopicFilter = interceptorContext?.TopicFilter ?? originalTopicFilter; - var acceptSubscription = interceptorContext?.AcceptSubscription ?? true; - var closeConnection = interceptorContext?.CloseConnection ?? false; + _subscriptionsLock.Release(); + } + + // 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(senderClientId, _session.Id); + var maxQoSLevel = -1; // Not subscribed. - if (string.IsNullOrEmpty(finalTopicFilter.Topic) || !acceptSubscription) + HashSet subscriptionIdentifiers = null; + var retainAsPublished = false; + + foreach (var subscription in possibleSubscriptions) + { + if (subscription.NoLocal && senderIsReceiver) { - result.ReturnCodes.Add(MqttSubscribeReturnCode.Failure); - result.ReasonCodes.Add(MqttSubscribeReasonCode.UnspecifiedError); + // This is a MQTTv5 feature! + continue; } - else + + if (MqttTopicFilterComparer.Compare(topic, subscription.Topic) != MqttTopicFilterCompareResult.IsMatch) { - result.ReturnCodes.Add(ConvertToSubscribeReturnCode(finalTopicFilter.QualityOfServiceLevel)); - result.ReasonCodes.Add(ConvertToSubscribeReasonCode(finalTopicFilter.QualityOfServiceLevel)); + continue; } - if (closeConnection) + if (subscription.RetainAsPublished) { - result.CloseConnection = true; + // This is a MQTTv5 feature! + retainAsPublished = true; } - if (!acceptSubscription || string.IsNullOrEmpty(finalTopicFilter.Topic)) + if ((int)subscription.GrantedQualityOfServiceLevel > maxQoSLevel) { - continue; + maxQoSLevel = (int)subscription.GrantedQualityOfServiceLevel; } - var subscription = CreateSubscription(finalTopicFilter, subscribePacket); + if (subscription.Identifier > 0) + { + if (subscriptionIdentifiers == null) + { + subscriptionIdentifiers = new HashSet(); + } - await _eventDispatcher.SafeNotifyClientSubscribedTopicAsync(_clientSession.ClientId, finalTopicFilter).ConfigureAwait(false); + subscriptionIdentifiers.Add(subscription.Identifier); + } + } - FilterRetainedApplicationMessages(retainedApplicationMessages, subscription, result); + 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 = applicationMessageQoSLevel + }; + + // 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)applicationMessageQoSLevel) + { + result.QualityOfServiceLevel = (MqttQualityOfServiceLevel)maxQoSLevel; } return result; } - public async Task> Unsubscribe(MqttUnsubscribePacket unsubscribePacket) + public void Dispose() { - if (unsubscribePacket == null) throw new ArgumentNullException(nameof(unsubscribePacket)); + _subscriptionsLock.Dispose(); + } - var reasonCodes = new List(); + public async Task Subscribe(MqttSubscribePacket subscribePacket, CancellationToken cancellationToken) + { + if (subscribePacket == null) + { + throw new ArgumentNullException(nameof(subscribePacket)); + } + + var retainedApplicationMessages = await _retainedMessagesManager.GetMessages().ConfigureAwait(false); + var result = new SubscribeResult + { + ReasonCodes = new List(subscribePacket.TopicFilters.Count) + }; + + var addedSubscriptions = new List(); - foreach (var topicFilter in unsubscribePacket.TopicFilters) + // The topic filters are order by its QoS so that the higher QoS will win over a + // lower one. + foreach (var originalTopicFilter in subscribePacket.TopicFilters.OrderByDescending(f => f.QualityOfServiceLevel)) { - var interceptorContext = await InterceptUnsubscribe(topicFilter).ConfigureAwait(false); - if (interceptorContext != null && !interceptorContext.AcceptUnsubscription) + var subscriptionEventArgs = await InterceptSubscribe(originalTopicFilter, cancellationToken).ConfigureAwait(false); + var finalTopicFilter = subscriptionEventArgs.TopicFilter; + var processSubscription = subscriptionEventArgs.ProcessSubscription && subscriptionEventArgs.Response.ReasonCode <= MqttSubscribeReasonCode.GrantedQoS2; + + result.UserProperties = subscriptionEventArgs.UserProperties; + result.ReasonString = subscriptionEventArgs.ReasonString; + result.ReasonCodes.Add(subscriptionEventArgs.Response.ReasonCode); + + if (subscriptionEventArgs.CloseConnection) { - reasonCodes.Add(MqttUnsubscribeReasonCode.ImplementationSpecificError); - continue; + // 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; } - _subscriptionsLock.EnterWriteLock(); - try + if (!processSubscription || string.IsNullOrEmpty(finalTopicFilter.Topic)) { - reasonCodes.Add(_subscriptions.Remove(topicFilter) - ? MqttUnsubscribeReasonCode.Success - : MqttUnsubscribeReasonCode.NoSubscriptionExisted); + continue; } - finally + + var createSubscriptionResult = CreateSubscription(finalTopicFilter, subscribePacket.SubscriptionIdentifier, subscriptionEventArgs.Response.ReasonCode); + + addedSubscriptions.Add(finalTopicFilter.Topic); + + if (_eventContainer.ClientSubscribedTopicEvent.HasHandlers) { - _subscriptionsLock.ExitWriteLock(); + var eventArgs = new ClientSubscribedTopicEventArgs + { + ClientId = _session.Id, + TopicFilter = finalTopicFilter + }; + + await _eventContainer.ClientSubscribedTopicEvent.InvokeAsync(eventArgs).ConfigureAwait(false); } - } - foreach (var topicFilter in unsubscribePacket.TopicFilters) - { - await _eventDispatcher.SafeNotifyClientUnsubscribedTopicAsync(_clientSession.ClientId, topicFilter).ConfigureAwait(false); + FilterRetainedApplicationMessages(retainedApplicationMessages, createSubscriptionResult, result); } - return reasonCodes; + _subscriptionChangedNotification?.OnSubscriptionsAdded(_session, addedSubscriptions); + + return result; } - public CheckSubscriptionsResult CheckSubscriptions(string topic, MqttQualityOfServiceLevel qosLevel, string senderClientId) + public async Task Unsubscribe(MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken) { - List subscriptions; - _subscriptionsLock.EnterReadLock(); - try - { - subscriptions = _subscriptions.Values.ToList(); - } - finally + if (unsubscribePacket == null) { - _subscriptionsLock.ExitReadLock(); + throw new ArgumentNullException(nameof(unsubscribePacket)); } - var senderIsReceiver = string.Equals(senderClientId, _clientSession.ClientId); + var result = new MqttUnsubscribeResult(); - var qosLevels = new HashSet(); - var subscriptionIdentifiers = new HashSet(); - var retainAsPublished = false; + var removedSubscriptions = new List(); - foreach (var subscription in subscriptions) + await _subscriptionsLock.WaitAsync(cancellationToken).ConfigureAwait(false); + try { - if (subscription.NoLocal && senderIsReceiver) + foreach (var topicFilter in unsubscribePacket.TopicFilters) { - // This is a MQTTv5 feature! - continue; + _subscriptions.TryGetValue(topicFilter, out var existingSubscription); + + var interceptorContext = await InterceptUnsubscribe(topicFilter, existingSubscription, cancellationToken).ConfigureAwait(false); + var acceptUnsubscription = interceptorContext.Response.ReasonCode == MqttUnsubscribeReasonCode.Success; + + result.ReasonCodes.Add(interceptorContext.Response.ReasonCode); + + if (interceptorContext.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 (interceptorContext.ProcessUnsubscription) + { + _subscriptions.Remove(topicFilter); + + // must remove subscription object from topic hash dictionary also + + if (existingSubscription.TopicHasWildcard) + { + if (_wildcardSubscriptionsByTopicHash.TryGetValue(existingSubscription.TopicHash, out var subs)) + { + subs.Subscriptions.Remove(existingSubscription); + if (subs.Subscriptions.Count == 0) + { + _wildcardSubscriptionsByTopicHash.Remove(existingSubscription.TopicHash); + } + } + } + else + { + if (_noWildcardSubscriptionsByTopicHash.TryGetValue(existingSubscription.TopicHash, out var subs)) + { + subs.Remove(existingSubscription); + if (subs.Count == 0) + { + _noWildcardSubscriptionsByTopicHash.Remove(existingSubscription.TopicHash); + } + } + } + + removedSubscriptions.Add(topicFilter); + } } + } + finally + { + _subscriptionsLock.Release(); - if (subscription.RetainAsPublished) + if (_subscriptionChangedNotification != null) { - // This is a MQTTv5 feature! - retainAsPublished = true; + _subscriptionChangedNotification.OnSubscriptionsRemoved(_session, removedSubscriptions); } + } - if (!MqttTopicFilterComparer.IsMatch(topic, subscription.Topic)) + if (_eventContainer.ClientUnsubscribedTopicEvent.HasHandlers) + { + foreach (var topicFilter in unsubscribePacket.TopicFilters) { - continue; - } + var eventArgs = new ClientUnsubscribedTopicEventArgs + { + ClientId = _session.Id, + TopicFilter = topicFilter + }; - qosLevels.Add(subscription.QualityOfServiceLevel); - - if (subscription.Identifier > 0) - { - subscriptionIdentifiers.Add(subscription.Identifier); + await _eventContainer.ClientUnsubscribedTopicEvent.InvokeAsync(eventArgs).ConfigureAwait(false); } } - if (qosLevels.Count == 0) - { - return CheckSubscriptionsResult.NotSubscribed; - } - - return new CheckSubscriptionsResult - { - IsSubscribed = true, - RetainAsPublished = retainAsPublished, - SubscriptionIdentifiers = subscriptionIdentifiers.ToList(), - QualityOfServiceLevel = GetEffectiveQoS(qosLevel, qosLevels) - }; + return result; } - Subscription CreateSubscription(MqttTopicFilter topicFilter, MqttSubscribePacket subscribePacket) + CreateSubscriptionResult CreateSubscription(MqttTopicFilter topicFilter, uint subscriptionIdentifier, MqttSubscribeReasonCode reasonCode) { - var subscription = new Subscription + MqttQualityOfServiceLevel grantedQualityOfServiceLevel; + + if (reasonCode == MqttSubscribeReasonCode.GrantedQoS0) { - Topic = topicFilter.Topic, - NoLocal = topicFilter.NoLocal, - RetainHandling = topicFilter.RetainHandling, - RetainAsPublished = topicFilter.RetainAsPublished, - QualityOfServiceLevel = topicFilter.QualityOfServiceLevel, - Identifier = subscribePacket.Properties?.SubscriptionIdentifier ?? 0 - }; + 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; - _subscriptionsLock.EnterWriteLock(); + // Add to subscriptions and maintain topic hash dictionaries + + _subscriptionsLock.Wait(); try { - subscription.IsNewSubscription = !_subscriptions.ContainsKey(topicFilter.Topic); + MqttSubscription.CalculateTopicHash(topicFilter.Topic, out var topicHash, out var topicHashMask, 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 subs)) + { + subs.Subscriptions.Remove(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(topicHashMask); + _wildcardSubscriptionsByTopicHash.Add(topicHash, subscriptions); + } + + subscriptions.Subscriptions.Add(subscription); + } + else + { + if (!_noWildcardSubscriptionsByTopicHash.TryGetValue(topicHash, out var subscriptions)) + { + subscriptions = new HashSet(); + _noWildcardSubscriptionsByTopicHash.Add(topicHash, subscriptions); + } + + subscriptions.Add(subscription); + } } finally { - _subscriptionsLock.ExitWriteLock(); + _subscriptionsLock.Release(); } - return subscription; + return new CreateSubscriptionResult + { + IsNewSubscription = isNewSubscription, + Subscription = subscription + }; } - static void FilterRetainedApplicationMessages(IList retainedApplicationMessages, Subscription subscription, SubscribeResult subscribeResult) + static void FilterRetainedApplicationMessages( + IList retainedApplicationMessages, + CreateSubscriptionResult createSubscriptionResult, + SubscribeResult subscribeResult) { - for (var i = retainedApplicationMessages.Count - 1; i >= 0; i--) + for (var index = retainedApplicationMessages.Count - 1; index >= 0; index--) { - var retainedApplicationMessage = retainedApplicationMessages[i]; + var retainedApplicationMessage = retainedApplicationMessages[index]; if (retainedApplicationMessage == null) { continue; } - if (subscription.RetainHandling == MqttRetainHandling.DoNotSendOnSubscribe) + if (createSubscriptionResult.Subscription.RetainHandling == MqttRetainHandling.DoNotSendOnSubscribe) { // This is a MQTT V5+ feature. continue; } - if (subscription.RetainHandling == MqttRetainHandling.SendAtSubscribeIfNewSubscriptionOnly && !subscription.IsNewSubscription) + if (createSubscriptionResult.Subscription.RetainHandling == MqttRetainHandling.SendAtSubscribeIfNewSubscriptionOnly && !createSubscriptionResult.IsNewSubscription) { // This is a MQTT V5+ feature. continue; } - if (!MqttTopicFilterComparer.IsMatch(retainedApplicationMessage.Topic, subscription.Topic)) + if (MqttTopicFilterComparer.Compare(retainedApplicationMessage.Topic, createSubscriptionResult.Subscription.Topic) != MqttTopicFilterCompareResult.IsMatch) { continue; } - var queuedApplicationMessage = new MqttQueuedApplicationMessage + var retainedMessageMatch = new MqttRetainedMessageMatch { ApplicationMessage = retainedApplicationMessage, - IsRetainedMessage = true, - SubscriptionQualityOfServiceLevel = subscription.QualityOfServiceLevel + SubscriptionQualityOfServiceLevel = createSubscriptionResult.Subscription.GrantedQualityOfServiceLevel }; - if (subscription.Identifier > 0) + if (subscribeResult.RetainedMessages == null) { - queuedApplicationMessage.SubscriptionIdentifiers = new List { subscription.Identifier }; + subscribeResult.RetainedMessages = new List(); } - subscribeResult.RetainedApplicationMessages.Add(queuedApplicationMessage); + subscribeResult.RetainedMessages.Add(retainedMessageMatch); - retainedApplicationMessages[i] = null; + // Clear the retained message from the list because the client should receive every message only + // one time even if multiple subscriptions affect them. + retainedApplicationMessages[index] = null; } } - static MqttSubscribeReturnCode ConvertToSubscribeReturnCode(MqttQualityOfServiceLevel qualityOfServiceLevel) + async Task InterceptSubscribe(MqttTopicFilter topicFilter, CancellationToken cancellationToken) { - return (MqttSubscribeReturnCode)(int)qualityOfServiceLevel; - } - - static MqttSubscribeReasonCode ConvertToSubscribeReasonCode(MqttQualityOfServiceLevel qualityOfServiceLevel) - { - return (MqttSubscribeReasonCode)(int)qualityOfServiceLevel; - } - - async Task InterceptSubscribe(MqttTopicFilter topicFilter) - { - var interceptor = _options.SubscriptionInterceptor; - if (interceptor == null) - { - return null; - } - - var context = new MqttSubscriptionInterceptorContext + var eventArgs = new InterceptingSubscriptionEventArgs { - ClientId = _clientSession.ClientId, + ClientId = _session.Id, TopicFilter = topicFilter, - SessionItems = _clientSession.Items + SessionItems = _session.Items, + Session = new MqttSessionStatus(_session), + CancellationToken = cancellationToken }; - await interceptor.InterceptSubscriptionAsync(context).ConfigureAwait(false); - - return context; - } - - async Task InterceptUnsubscribe(string topicFilter) - { - var interceptor = _options.UnsubscriptionInterceptor; - if (interceptor == null) + if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce) { - return null; + eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.GrantedQoS0; } - - var context = new MqttUnsubscriptionInterceptorContext + else if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtLeastOnce) { - ClientId = _clientSession.ClientId, - Topic = topicFilter, - SessionItems = _clientSession.Items - }; + eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.GrantedQoS1; + } + else if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.ExactlyOnce) + { + eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.GrantedQoS2; + } - await interceptor.InterceptUnsubscriptionAsync(context).ConfigureAwait(false); + if (topicFilter.Topic.StartsWith("$share/")) + { + eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.SharedSubscriptionsNotSupported; + } + else + { + await _eventContainer.InterceptingSubscriptionEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + } - return context; + return eventArgs; } - static MqttQualityOfServiceLevel GetEffectiveQoS(MqttQualityOfServiceLevel qosLevel, HashSet subscribedQoSLevels) + async Task InterceptUnsubscribe(string topicFilter, MqttSubscription mqttSubscription, CancellationToken cancellationToken) { - MqttQualityOfServiceLevel effectiveQoS; - if (subscribedQoSLevels.Contains(qosLevel)) + var clientUnsubscribingTopicEventArgs = new InterceptingUnsubscriptionEventArgs { - effectiveQoS = qosLevel; - } - else if (subscribedQoSLevels.Count == 1) + ClientId = _session.Id, + Topic = topicFilter, + SessionItems = _session.Items, + CancellationToken = cancellationToken + }; + + if (mqttSubscription == null) { - effectiveQoS = subscribedQoSLevels.First(); + clientUnsubscribingTopicEventArgs.Response.ReasonCode = MqttUnsubscribeReasonCode.NoSubscriptionExisted; } else { - effectiveQoS = subscribedQoSLevels.Max(); + clientUnsubscribingTopicEventArgs.Response.ReasonCode = MqttUnsubscribeReasonCode.Success; } - return effectiveQoS; + await _eventContainer.InterceptingUnsubscriptionEvent.InvokeAsync(clientUnsubscribingTopicEventArgs).ConfigureAwait(false); + + return clientUnsubscribingTopicEventArgs; + } + + public sealed class CreateSubscriptionResult + { + public bool IsNewSubscription { get; set; } + + public MqttSubscription Subscription { get; set; } } } } \ No newline at end of file diff --git a/Source/MQTTnet/Server/Internal/MqttRetainedMessagesManager.cs b/Source/MQTTnet/Server/Internal/MqttRetainedMessagesManager.cs index ebe8ee2..3673734 100644 --- a/Source/MQTTnet/Server/Internal/MqttRetainedMessagesManager.cs +++ b/Source/MQTTnet/Server/Internal/MqttRetainedMessagesManager.cs @@ -1,51 +1,48 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using MQTTnet.Diagnostics; -using MQTTnet.Diagnostics.Logger; using MQTTnet.Implementations; using MQTTnet.Internal; -namespace MQTTnet.Server.Internal +namespace MQTTnet.Server { - public sealed class MqttRetainedMessagesManager : IMqttRetainedMessagesManager + public sealed class MqttRetainedMessagesManager { - readonly AsyncLock _storageAccessLock = new AsyncLock(); readonly Dictionary _messages = new Dictionary(4096); + readonly AsyncLock _storageAccessLock = new AsyncLock(); - MqttNetSourceLogger _logger; - IMqttServerOptions _options; - - // TODO: Get rid of the logger here! - public Task Start(IMqttServerOptions options, IMqttNetLogger logger) + 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)); - - _options = options ?? throw new ArgumentNullException(nameof(options)); - return PlatformAbstractionLayer.CompletedTask; } - - public async Task LoadMessagesAsync() + + public async Task Start() { - if (_options.Storage == null) - { - return; - } - try { - var retainedMessages = await _options.Storage.LoadRetainedMessagesAsync().ConfigureAwait(false); - + var eventArgs = new LoadingRetainedMessagesEventArgs(); + await _eventContainer.LoadingRetainedMessagesEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + lock (_messages) { _messages.Clear(); - if (retainedMessages != null) + if (eventArgs.LoadedRetainedMessages != null) { - foreach (var retainedMessage in retainedMessages) + foreach (var retainedMessage in eventArgs.LoadedRetainedMessages) { _messages[retainedMessage.Topic] = retainedMessage; } @@ -58,9 +55,12 @@ namespace MQTTnet.Server.Internal } } - public async Task HandleMessageAsync(string clientId, MqttApplicationMessage applicationMessage) + public async Task UpdateMessage(string clientId, MqttApplicationMessage applicationMessage) { - if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + if (applicationMessage == null) + { + throw new ArgumentNullException(nameof(applicationMessage)); + } try { @@ -103,12 +103,16 @@ namespace MQTTnet.Server.Internal if (saveIsRequired) { - if (_options.Storage != null) + using (await _storageAccessLock.WaitAsync(CancellationToken.None).ConfigureAwait(false)) { - using (await _storageAccessLock.WaitAsync(CancellationToken.None).ConfigureAwait(false)) + var eventArgs = new RetainedMessageChangedEventArgs { - await _options.Storage.SaveRetainedMessagesAsync(messagesForSave).ConfigureAwait(false); - } + ClientId = clientId, + ChangedRetainedMessage = applicationMessage, + StoredRetainedMessages = messagesForSave + }; + + await _eventContainer.RetainedMessageChangedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); } } } @@ -118,27 +122,25 @@ namespace MQTTnet.Server.Internal } } - public Task> GetMessagesAsync() + public Task> GetMessages() { lock (_messages) { - return Task.FromResult((IList)_messages.Values.ToList()); + var result = new List(_messages.Values); + return Task.FromResult((IList)result); } } - public async Task ClearMessagesAsync() + public async Task ClearMessages() { lock (_messages) { _messages.Clear(); } - if (_options.Storage != null) + using (await _storageAccessLock.WaitAsync(CancellationToken.None).ConfigureAwait(false)) { - using (await _storageAccessLock.WaitAsync(CancellationToken.None).ConfigureAwait(false)) - { - await _options.Storage.SaveRetainedMessagesAsync(new List()).ConfigureAwait(false); - } + await _eventContainer.RetainedMessagesClearedEvent.InvokeAsync(EventArgs.Empty).ConfigureAwait(false); } } } diff --git a/Source/MQTTnet/Server/Internal/MqttServerEventContainer.cs b/Source/MQTTnet/Server/Internal/MqttServerEventContainer.cs new file mode 100644 index 0000000..f42d23d --- /dev/null +++ b/Source/MQTTnet/Server/Internal/MqttServerEventContainer.cs @@ -0,0 +1,48 @@ +// 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.Internal; + +namespace MQTTnet.Server +{ + public sealed class MqttServerEventContainer + { + public AsyncEvent InterceptingSubscriptionEvent { get; } = new AsyncEvent(); + + public AsyncEvent InterceptingUnsubscriptionEvent { get; } = new AsyncEvent(); + + public AsyncEvent InterceptingPublishEvent { get; } = new AsyncEvent(); + + public AsyncEvent ValidatingConnectionEvent { get; } = new AsyncEvent(); + + public AsyncEvent ClientConnectedEvent { get; } = new AsyncEvent(); + + public AsyncEvent ClientDisconnectedEvent { get; } = new AsyncEvent(); + + public AsyncEvent ClientSubscribedTopicEvent { get; } = new AsyncEvent(); + + public AsyncEvent ClientUnsubscribedTopicEvent { get; } = new AsyncEvent(); + + public AsyncEvent PreparingSessionEvent { get; } = new AsyncEvent(); + + public AsyncEvent SessionDeletedEvent { get; } = new AsyncEvent(); + + public AsyncEvent ApplicationMessageNotConsumedEvent { get; } = new AsyncEvent(); + + public AsyncEvent RetainedMessageChangedEvent { get; } = new AsyncEvent(); + + public AsyncEvent LoadingRetainedMessagesEvent { get; } = new AsyncEvent(); + + public AsyncEvent RetainedMessagesClearedEvent { get; } = new AsyncEvent(); + + public AsyncEvent InterceptingInboundPacketEvent { get; } = new AsyncEvent(); + + public AsyncEvent InterceptingOutboundPacketEvent { get; } = new AsyncEvent(); + + public AsyncEvent StartedEvent { get; } = new AsyncEvent(); + + public AsyncEvent StoppedEvent { get; } = new AsyncEvent(); + } +} diff --git a/Source/MQTTnet/Server/Internal/MqttServerEventDispatcher.cs b/Source/MQTTnet/Server/Internal/MqttServerEventDispatcher.cs deleted file mode 100644 index 97b9025..0000000 --- a/Source/MQTTnet/Server/Internal/MqttServerEventDispatcher.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System; -using System.Threading.Tasks; -using MQTTnet.Adapter; -using MQTTnet.Client.Receiving; -using MQTTnet.Diagnostics; -using MQTTnet.Diagnostics.Logger; -using MQTTnet.Packets; - -namespace MQTTnet.Server.Internal -{ - public sealed class MqttServerEventDispatcher - { - readonly MqttNetSourceLogger _logger; - - public MqttServerEventDispatcher(IMqttNetLogger logger) - { - if (logger is null) throw new ArgumentNullException(nameof(logger)); - - _logger = logger.WithSource(nameof(MqttServerEventDispatcher)); - } - - public IMqttServerClientConnectedHandler ClientConnectedHandler { get; set; } - - public IMqttServerClientDisconnectedHandler ClientDisconnectedHandler { get; set; } - - public IMqttServerClientSubscribedTopicHandler ClientSubscribedTopicHandler { get; set; } - - public IMqttServerClientUnsubscribedTopicHandler ClientUnsubscribedTopicHandler { get; set; } - - public IMqttApplicationMessageReceivedHandler ApplicationMessageReceivedHandler { get; set; } - - public async Task SafeNotifyClientConnectedAsync(MqttConnectPacket connectPacket, IMqttChannelAdapter channelAdapter) - { - try - { - var handler = ClientConnectedHandler; - if (handler == null) - { - return; - } - - await handler.HandleClientConnectedAsync(new MqttServerClientConnectedEventArgs - { - ClientId = connectPacket.ClientId, - UserName = connectPacket.Username, - ProtocolVersion = channelAdapter.PacketFormatterAdapter.ProtocolVersion, - Endpoint = channelAdapter.Endpoint - }).ConfigureAwait(false); - } - catch (Exception exception) - { - _logger.Error(exception, "Error while handling custom 'ClientConnected' event."); - } - } - - public async Task SafeNotifyClientDisconnectedAsync(string clientId, MqttClientDisconnectType disconnectType, string endpoint) - { - try - { - var handler = ClientDisconnectedHandler; - if (handler == null) - { - return; - } - - await handler.HandleClientDisconnectedAsync(new MqttServerClientDisconnectedEventArgs - { - ClientId = clientId, - DisconnectType = disconnectType, - Endpoint = endpoint - }).ConfigureAwait(false); - } - catch (Exception exception) - { - _logger.Error(exception, "Error while handling custom 'ClientDisconnected' event."); - } - } - - public async Task SafeNotifyClientSubscribedTopicAsync(string clientId, MqttTopicFilter topicFilter) - { - try - { - var handler = ClientSubscribedTopicHandler; - if (handler == null) - { - return; - } - - await handler.HandleClientSubscribedTopicAsync(new MqttServerClientSubscribedTopicEventArgs - { - ClientId = clientId, - TopicFilter = topicFilter - }).ConfigureAwait(false); - } - catch (Exception exception) - { - _logger.Error(exception, "Error while handling custom 'ClientSubscribedTopic' event."); - } - } - - public async Task SafeNotifyClientUnsubscribedTopicAsync(string clientId, string topicFilter) - { - try - { - var handler = ClientUnsubscribedTopicHandler; - if (handler == null) - { - return; - } - - await handler.HandleClientUnsubscribedTopicAsync(new MqttServerClientUnsubscribedTopicEventArgs - { - ClientId = clientId, - TopicFilter = topicFilter - }).ConfigureAwait(false); - } - catch (Exception exception) - { - _logger.Error(exception, "Error while handling custom 'ClientUnsubscribedTopic' event."); - } - } - - public async Task SafeNotifyApplicationMessageReceivedAsync(string senderClientId, MqttApplicationMessage applicationMessage) - { - try - { - var handler = ApplicationMessageReceivedHandler; - if (handler == null) - { - return; - } - - await handler.HandleApplicationMessageReceivedAsync(new MqttApplicationMessageReceivedEventArgs(senderClientId, applicationMessage, null, null)).ConfigureAwait(false); - } - catch (Exception exception) - { - _logger.Error(exception, "Error while handling custom 'ApplicationMessageReceived' event."); - } - } - } -} diff --git a/Source/MQTTnet/Server/Internal/MqttServerKeepAliveMonitor.cs b/Source/MQTTnet/Server/Internal/MqttServerKeepAliveMonitor.cs index 0eb073c..76deb05 100644 --- a/Source/MQTTnet/Server/Internal/MqttServerKeepAliveMonitor.cs +++ b/Source/MQTTnet/Server/Internal/MqttServerKeepAliveMonitor.cs @@ -1,21 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + using System; using System.Threading; using System.Threading.Tasks; -using MQTTnet.Client.Disconnecting; using MQTTnet.Diagnostics; -using MQTTnet.Diagnostics.Logger; using MQTTnet.Implementations; using MQTTnet.Internal; +using MQTTnet.Protocol; -namespace MQTTnet.Server.Internal +namespace MQTTnet.Server { public sealed class MqttServerKeepAliveMonitor { - readonly IMqttServerOptions _options; + readonly MqttServerOptions _options; readonly MqttClientSessionsManager _sessionsManager; readonly MqttNetSourceLogger _logger; - public MqttServerKeepAliveMonitor(IMqttServerOptions options, MqttClientSessionsManager sessionsManager, IMqttNetLogger logger) + public MqttServerKeepAliveMonitor(MqttServerOptions options, MqttClientSessionsManager sessionsManager, IMqttNetLogger logger) { _options = options ?? throw new ArgumentNullException(nameof(options)); _sessionsManager = sessionsManager ?? throw new ArgumentNullException(nameof(sessionsManager)); @@ -41,7 +44,7 @@ namespace MQTTnet.Server.Internal while (!cancellationToken.IsCancellationRequested) { - TryMaintainConnections(); + TryProcessClients(); PlatformAbstractionLayer.Sleep(_options.KeepAliveMonitorInterval); } } @@ -58,16 +61,16 @@ namespace MQTTnet.Server.Internal } } - void TryMaintainConnections() + void TryProcessClients() { var now = DateTime.UtcNow; - foreach (var connection in _sessionsManager.GetConnections()) + foreach (var client in _sessionsManager.GetClients()) { - TryMaintainConnection(connection, now); + TryProcessClient(client, now); } } - void TryMaintainConnection(MqttClientConnection connection, DateTime now) + void TryProcessClient(MqttClient connection, DateTime now) { try { @@ -83,7 +86,7 @@ namespace MQTTnet.Server.Internal return; } - if (connection.IsReadingPacket) + if (connection.ChannelAdapter.IsReadingPacket) { // The connection is currently reading a (large) packet. So it is obviously // doing something and thus "connected". @@ -102,18 +105,18 @@ namespace MQTTnet.Server.Internal return; } - _logger.Warning("Client '{0}': Did not receive any packet or keep alive signal.", connection.ClientId); + _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. - var _ = connection.StopAsync(MqttClientDisconnectReason.KeepAliveTimeout); + var _ = connection.StopAsync(MqttDisconnectReasonCode.KeepAliveTimeout); } catch (Exception exception) { - _logger.Error(exception, "Client {0}: Unhandled exception while checking keep alive timeouts.", connection.ClientId); + _logger.Error(exception, "Client {0}: Unhandled exception while checking keep alive timeouts.", connection.Id); } } } diff --git a/Source/MQTTnet/Server/Internal/MqttSession.cs b/Source/MQTTnet/Server/Internal/MqttSession.cs new file mode 100644 index 0000000..9bb8337 --- /dev/null +++ b/Source/MQTTnet/Server/Internal/MqttSession.cs @@ -0,0 +1,161 @@ +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Client; +using MQTTnet.Internal; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Server +{ + public sealed class MqttSession : IDisposable + { + readonly MqttClientSessionsManager _clientSessionsManager; + readonly MqttPacketBus _packetBus = new MqttPacketBus(); + + readonly MqttServerOptions _serverOptions; + + readonly Dictionary _unacknowledgedPublishPackets = new Dictionary(); + + // Bookkeeping to know if this is a subscribing client; lazy intialize later. + HashSet _subscribedTopics; + + public MqttSession( + string clientId, + bool isPersistent, + IDictionary items, + MqttServerOptions serverOptions, + MqttServerEventContainer eventContainer, + MqttRetainedMessagesManager retainedMessagesManager, + MqttClientSessionsManager clientSessionsManager) + { + Id = clientId ?? throw new ArgumentNullException(nameof(clientId)); + IsPersistent = isPersistent; + Items = items ?? throw new ArgumentNullException(nameof(items)); + + _serverOptions = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions)); + _clientSessionsManager = clientSessionsManager ?? throw new ArgumentNullException(nameof(clientSessionsManager)); + + SubscriptionsManager = new MqttClientSubscriptionsManager(this, eventContainer, retainedMessagesManager, clientSessionsManager); + } + + public DateTime CreatedTimestamp { get; } = DateTime.UtcNow; + + public bool HasSubscribedTopics => _subscribedTopics != null && _subscribedTopics.Count > 0; + + public string Id { get; } + + /// + /// Session should persist if CleanSession was set to false (Mqtt3) or if SessionExpiryInterval != 0 (Mqtt5) + /// + public bool IsPersistent { get; set; } + + public IDictionary Items { get; } + + public MqttConnectPacket LatestConnectPacket { get; set; } + + public MqttPacketIdentifierProvider PacketIdentifierProvider { get; } = new MqttPacketIdentifierProvider(); + + public long PendingDataPacketsCount => _packetBus.PartitionItemsCount(MqttPacketBusPartition.Data); + + public MqttClientSubscriptionsManager SubscriptionsManager { get; } + + public bool WillMessageSent { get; set; } + + public void AcknowledgePublishPacket(ushort packetIdentifier) + { + _unacknowledgedPublishPackets.Remove(packetIdentifier); + } + + public void AddSubscribedTopic(string topic) + { + if (_subscribedTopics == null) + { + _subscribedTopics = new HashSet(); + } + + _subscribedTopics.Add(topic); + } + + public Task DeleteAsync() + { + return _clientSessionsManager.DeleteSessionAsync(Id); + } + + public Task DequeuePacketAsync(CancellationToken cancellationToken) + { + return _packetBus.DequeueItemAsync(cancellationToken); + } + + public void Dispose() + { + _packetBus?.Dispose(); + SubscriptionsManager.Dispose(); + } + + public void EnqueuePacket(MqttPacketBusItem packetBusItem) + { + if (packetBusItem == null) + { + throw new ArgumentNullException(nameof(packetBusItem)); + } + + if (_packetBus.ItemsCount >= _serverOptions.MaxPendingMessagesPerClient) + { + if (_serverOptions.PendingMessagesOverflowStrategy == MqttPendingMessagesOverflowStrategy.DropNewMessage) + { + return; + } + + 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. + _packetBus.DropFirstItem(MqttPacketBusPartition.Data); + } + } + + if (packetBusItem.Packet is MqttPublishPacket publishPacket) + { + if (publishPacket.QualityOfServiceLevel > MqttQualityOfServiceLevel.AtMostOnce) + { + _unacknowledgedPublishPackets[publishPacket.PacketIdentifier] = publishPacket; + } + + _packetBus.EnqueueItem(packetBusItem, MqttPacketBusPartition.Data); + } + else if (packetBusItem.Packet is MqttPingReqPacket || packetBusItem.Packet is MqttPingRespPacket) + { + _packetBus.EnqueueItem(packetBusItem, MqttPacketBusPartition.Health); + } + else + { + _packetBus.EnqueueItem(packetBusItem, MqttPacketBusPartition.Control); + } + } + + public void Recover() + { + // TODO: Keep the bus and only insert pending items again. + // TODO: Check if packet identifier must be restarted or not. + _packetBus.Clear(); + + foreach (var publishPacket in _unacknowledgedPublishPackets.Values.ToList()) + { + EnqueuePacket(new MqttPacketBusItem(publishPacket)); + } + } + + public void RemoveSubscribedTopic(string topic) + { + _subscribedTopics?.Remove(topic); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/Internal/MqttSubscription.cs b/Source/MQTTnet/Server/Internal/MqttSubscription.cs new file mode 100644 index 0000000..ec53242 --- /dev/null +++ b/Source/MQTTnet/Server/Internal/MqttSubscription.cs @@ -0,0 +1,309 @@ +// 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 +{ +/* +* 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 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; + + CalculateTopicHash(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; } + + public static void CalculateTopicHash(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; + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/Internal/MqttTopicFilterComparer.cs b/Source/MQTTnet/Server/Internal/MqttTopicFilterComparer.cs deleted file mode 100644 index 1fe976e..0000000 --- a/Source/MQTTnet/Server/Internal/MqttTopicFilterComparer.cs +++ /dev/null @@ -1,128 +0,0 @@ -using System; - -namespace MQTTnet.Server.Internal -{ - public static class MqttTopicFilterComparer - { - const char LevelSeparator = '/'; - const char MultiLevelWildcard = '#'; - const char SingleLevelWildcard = '+'; - - public static bool IsMatch(string topic, string filter) - { - if (topic == null) throw new ArgumentNullException(nameof(topic)); - if (filter == null) throw new ArgumentNullException(nameof(filter)); - - var sPos = 0; - var sLen = filter.Length; - var tPos = 0; - var tLen = topic.Length; - - while (sPos < sLen && tPos < tLen) - { - if (filter[sPos] == topic[tPos]) - { - if (tPos == tLen - 1) - { - // Check for e.g. foo matching foo/# - if (sPos == sLen - 3 - && filter[sPos + 1] == LevelSeparator - && filter[sPos + 2] == MultiLevelWildcard) - { - return true; - } - // Check for e.g. foo/ matching foo/# - if (sPos == sLen - 2 - && filter[sPos] == LevelSeparator - && filter[sPos + 1] == MultiLevelWildcard) - { - return true; - } - } - - sPos++; - tPos++; - - if (sPos == sLen && tPos == tLen) - { - return true; - } - - if (tPos == tLen && sPos == sLen - 1 && filter[sPos] == SingleLevelWildcard) - { - if (sPos > 0 && filter[sPos - 1] != LevelSeparator) - { - // Invalid filter string - return false; - } - - return true; - } - } - else - { - if (filter[sPos] == SingleLevelWildcard) - { - // Check for bad "+foo" or "a/+foo" subscription - if (sPos > 0 && filter[sPos - 1] != LevelSeparator) - { - // Invalid filter string - return false; - } - - // Check for bad "foo+" or "foo+/a" subscription - if (sPos < sLen - 1 && filter[sPos + 1] != LevelSeparator) - { - // Invalid filter string - return false; - } - - sPos++; - while (tPos < tLen && topic[tPos] != LevelSeparator) - { - tPos++; - } - - if (tPos == tLen && sPos == sLen) - { - return true; - } - } - else if (filter[sPos] == MultiLevelWildcard) - { - if (sPos > 0 && filter[sPos - 1] != LevelSeparator) - { - // Invalid filter string - return false; - } - - if (sPos + 1 != sLen) - { - // Invalid filter string - return false; - } - - return true; - } - else - { - // Check for e.g. foo/bar matching foo/+/# - if (sPos > 0 - && sPos + 2 == sLen - && tPos == tLen - && filter[sPos - 1] == SingleLevelWildcard - && filter[sPos] == LevelSeparator - && filter[sPos + 1] == MultiLevelWildcard) - { - return true; - } - - return false; - } - } - } - - return false; - } - } -} diff --git a/Source/MQTTnet/Server/Internal/MqttUnsubscribeResult.cs b/Source/MQTTnet/Server/Internal/MqttUnsubscribeResult.cs new file mode 100644 index 0000000..9e482dc --- /dev/null +++ b/Source/MQTTnet/Server/Internal/MqttUnsubscribeResult.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using MQTTnet.Protocol; + +namespace MQTTnet.Server +{ + public sealed class MqttUnsubscribeResult + { + public List ReasonCodes { get; } = new List(128); + + public bool CloseConnection { get; set; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/Internal/SubscribeResult.cs b/Source/MQTTnet/Server/Internal/SubscribeResult.cs index 2a3f070..beff649 100644 --- a/Source/MQTTnet/Server/Internal/SubscribeResult.cs +++ b/Source/MQTTnet/Server/Internal/SubscribeResult.cs @@ -1,16 +1,23 @@ -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using MQTTnet.Packets; using MQTTnet.Protocol; -namespace MQTTnet.Server.Internal +namespace MQTTnet.Server { public sealed class SubscribeResult { - public List ReturnCodes { get; } = new List(128); + public bool CloseConnection { get; set; } - public List ReasonCodes { get; } = new List(128); + public List ReasonCodes { get; set; } - public List RetainedApplicationMessages { get; } = new List(1024); - - public bool CloseConnection { get; set; } + public string ReasonString { get; set; } + + public List RetainedMessages { get; set; } + + public List UserProperties { get; set; } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/Internal/Subscription.cs b/Source/MQTTnet/Server/Internal/Subscription.cs deleted file mode 100644 index 965cce6..0000000 --- a/Source/MQTTnet/Server/Internal/Subscription.cs +++ /dev/null @@ -1,21 +0,0 @@ -using MQTTnet.Protocol; - -namespace MQTTnet.Server.Internal -{ - public sealed class Subscription - { - public string Topic { get; set; } - - public bool NoLocal { get; set; } - - public MqttRetainHandling RetainHandling { get; set; } - - public bool RetainAsPublished { get; set; } - - public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } - - public uint Identifier { get; set; } - - public bool IsNewSubscription { get; set; } - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Server/Internal/TopicHashMaskSubscriptions.cs b/Source/MQTTnet/Server/Internal/TopicHashMaskSubscriptions.cs new file mode 100644 index 0000000..e9a35e8 --- /dev/null +++ b/Source/MQTTnet/Server/Internal/TopicHashMaskSubscriptions.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace MQTTnet.Server +{ + /// + /// Helper class that stores the topic hash mask common to all contained Subscriptions for direct access. + /// + public sealed class TopicHashMaskSubscriptions + { + public TopicHashMaskSubscriptions(ulong hashMask) + { + HashMask = hashMask; + } + + public ulong HashMask { get; } + + public HashSet Subscriptions { get; } = new HashSet(); + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/MqttApplicationMessageInterceptorContext.cs b/Source/MQTTnet/Server/MqttApplicationMessageInterceptorContext.cs deleted file mode 100644 index 1ab4ee4..0000000 --- a/Source/MQTTnet/Server/MqttApplicationMessageInterceptorContext.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; - -namespace MQTTnet.Server -{ - public sealed class MqttApplicationMessageInterceptorContext - { - /// - /// Gets the client identifier. - /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. - /// - public string ClientId { get; internal set; } - - public MqttApplicationMessage ApplicationMessage { get; set; } - - /// - /// Gets or sets a key/value collection that can be used to share data within the scope of this session. - /// - public IDictionary SessionItems { get; internal set; } - - public bool AcceptPublish { get; set; } = true; - - public bool CloseConnection { get; set; } - } -} diff --git a/Source/MQTTnet/Server/MqttClientDisconnectType.cs b/Source/MQTTnet/Server/MqttClientDisconnectType.cs index c4d6f59..7f738c8 100644 --- a/Source/MQTTnet/Server/MqttClientDisconnectType.cs +++ b/Source/MQTTnet/Server/MqttClientDisconnectType.cs @@ -1,4 +1,8 @@ -namespace MQTTnet.Server +// 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 { diff --git a/Source/MQTTnet/Server/MqttClientMessageQueueInterceptorContext.cs b/Source/MQTTnet/Server/MqttClientMessageQueueInterceptorContext.cs deleted file mode 100644 index 39b08ff..0000000 --- a/Source/MQTTnet/Server/MqttClientMessageQueueInterceptorContext.cs +++ /dev/null @@ -1,25 +0,0 @@ -using MQTTnet.Protocol; - -namespace MQTTnet.Server -{ - public sealed class MqttClientMessageQueueInterceptorContext - { - public string SenderClientId { get; internal set; } - - public string ReceiverClientId { get; internal set; } - - public MqttApplicationMessage ApplicationMessage { get; set; } - - public bool AcceptEnqueue { get; set; } = true; - - /// - /// Gets or sets the supscription quality of service level. - /// The Quality of Service (QoS) level is an agreement between the sender of a message and the receiver of a message that defines the guarantee of delivery for a specific message. - /// There are 3 QoS levels in MQTT: - /// - At most once (0): Message gets delivered no time, once or multiple times. - /// - At least once (1): Message gets delivered at least once (one time or more often). - /// - Exactly once (2): Message gets delivered exactly once (It's ensured that the message only comes once). - /// - public MqttQualityOfServiceLevel SubscriptionQualityOfServiceLevel { get; set; } - } -} diff --git a/Source/MQTTnet/Server/MqttClientMessageQueueInterceptorDelegate.cs b/Source/MQTTnet/Server/MqttClientMessageQueueInterceptorDelegate.cs deleted file mode 100644 index 9d1118d..0000000 --- a/Source/MQTTnet/Server/MqttClientMessageQueueInterceptorDelegate.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Threading.Tasks; -using MQTTnet.Implementations; - -namespace MQTTnet.Server -{ - public sealed class MqttClientMessageQueueInterceptorDelegate : IMqttServerClientMessageQueueInterceptor - { - readonly Func _callback; - - public MqttClientMessageQueueInterceptorDelegate(Action callback) - { - if (callback == null) throw new ArgumentNullException(nameof(callback)); - - _callback = context => - { - callback(context); - return PlatformAbstractionLayer.CompletedTask; - }; - } - - public MqttClientMessageQueueInterceptorDelegate(Func callback) - { - _callback = callback ?? throw new ArgumentNullException(nameof(callback)); - } - - public Task InterceptClientMessageQueueEnqueueAsync(MqttClientMessageQueueInterceptorContext context) - { - return _callback(context); - } - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Server/MqttConnectionValidatorContext.cs b/Source/MQTTnet/Server/MqttConnectionValidatorContext.cs deleted file mode 100644 index c0eb8a2..0000000 --- a/Source/MQTTnet/Server/MqttConnectionValidatorContext.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using MQTTnet.Adapter; -using MQTTnet.Formatter; -using MQTTnet.Implementations; -using MQTTnet.Packets; -using MQTTnet.Protocol; - -namespace MQTTnet.Server -{ - public sealed class MqttConnectionValidatorContext - { - readonly MqttConnectPacket _connectPacket; - readonly IMqttChannelAdapter _clientAdapter; - - public MqttConnectionValidatorContext(MqttConnectPacket connectPacket, IMqttChannelAdapter clientAdapter) - { - _connectPacket = connectPacket ?? throw new ArgumentNullException(nameof(connectPacket)); - _clientAdapter = clientAdapter ?? throw new ArgumentNullException(nameof(clientAdapter)); - } - - /// - /// Gets the client identifier. - /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. - /// - public string ClientId => _connectPacket.ClientId; - - public string Endpoint => _clientAdapter.Endpoint; - - public bool IsSecureConnection => _clientAdapter.IsSecureConnection; - - public X509Certificate2 ClientCertificate => _clientAdapter.ClientCertificate; - - public MqttProtocolVersion ProtocolVersion => _clientAdapter.PacketFormatterAdapter.ProtocolVersion; - - public string Username => _connectPacket.Username; - - public byte[] RawPassword => _connectPacket.Password; - - public string Password => Encoding.UTF8.GetString(RawPassword ?? PlatformAbstractionLayer.EmptyByteArray); - - /// - /// Gets or sets the will delay interval. - /// This is the time between the client disconnect and the time the will message will be sent. - /// - public MqttApplicationMessage WillMessage => _connectPacket.WillMessage; - - /// - /// 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. - /// - public bool? CleanSession => _connectPacket.CleanSession; - - /// - /// 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. - /// - public ushort? KeepAlivePeriod => _connectPacket.KeepAlivePeriod; - - /// - /// Gets or sets the user properties. - /// In MQTT 5, user properties are basic UTF-8 string key-value pairs that you can append to almost every type of MQTT packet. - /// As long as you don’t exceed the maximum message size, you can use an unlimited number of user properties to add metadata to MQTT messages and pass information between publisher, broker, and subscriber. - /// The feature is very similar to the HTTP header concept. - /// Hint: MQTT 5 feature only. - /// - public List UserProperties => _connectPacket.Properties?.UserProperties; - - /// - /// Gets or sets the authentication data. - /// Hint: MQTT 5 feature only. - /// - public byte[] AuthenticationData => _connectPacket.Properties?.AuthenticationData; - - /// - /// Gets or sets the authentication method. - /// Hint: MQTT 5 feature only. - /// - public string AuthenticationMethod => _connectPacket.Properties?.AuthenticationMethod; - - public uint? MaximumPacketSize => _connectPacket.Properties?.MaximumPacketSize; - - /// - /// Gets or sets the receive maximum. - /// This gives the maximum length of the receive messages. - /// - public ushort? ReceiveMaximum => _connectPacket.Properties?.ReceiveMaximum; - - /// - /// Gets or sets the topic alias maximum. - /// This gives the maximum length of the topic alias. - /// - public ushort TopicAliasMaximum => _connectPacket.Properties?.TopicAliasMaximum ?? 0; - - /// - /// Gets the request problem information. - /// Hint: MQTT 5 feature only. - /// - public bool? RequestProblemInformation => _connectPacket.Properties?.RequestProblemInformation; - - /// - /// Gets the request response information. - /// Hint: MQTT 5 feature only. - /// - public bool? RequestResponseInformation => _connectPacket.Properties?.RequestResponseInformation; - - /// - /// Gets the session expiry interval. - /// The time after a session expires when it's not actively used. - /// - public uint? SessionExpiryInterval => _connectPacket.Properties?.SessionExpiryInterval; - - /// - /// Gets or sets the will delay interval. - /// This is the time between the client disconnect and the time the will message will be sent. - /// - public uint? WillDelayInterval => _connectPacket.Properties?.WillDelayInterval; - - /// - /// Gets or sets a key/value collection that can be used to share data within the scope of this session. - /// - public IDictionary SessionItems { get; internal set; } - - /// - /// This is used for MQTTv3 only. - /// - [Obsolete("Use ReasonCode instead. It is MQTTv5 only but will be converted to a valid ReturnCode.")] - public MqttConnectReturnCode ReturnCode - { - get => new MqttConnectReasonCodeConverter().ToConnectReturnCode(ReasonCode); - set => ReasonCode = new MqttConnectReasonCodeConverter().ToConnectReasonCode(value); - } - - /// - /// 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. - /// MQTTv5 only. - /// - public MqttConnectReasonCode ReasonCode { get; set; } = MqttConnectReasonCode.Success; - - /// - /// 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. - /// Hint: MQTT 5 feature only. - /// - public List ResponseUserProperties { get; set; } - - /// - /// Gets or sets the response authentication data. - /// MQTTv5 only. - /// - public byte[] ResponseAuthenticationData { get; set; } - - /// - /// Gets or sets the assigned client identifier. - /// MQTTv5 only. - /// - public string AssignedClientIdentifier { get; set; } - - public string ReasonString { get; set; } - - /// - /// 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. - /// MQTTv5 only. - /// - public string ServerReference { get; set; } - } -} diff --git a/Source/MQTTnet/Server/MqttPendingApplicationMessage.cs b/Source/MQTTnet/Server/MqttPendingApplicationMessage.cs deleted file mode 100644 index 52ed9c7..0000000 --- a/Source/MQTTnet/Server/MqttPendingApplicationMessage.cs +++ /dev/null @@ -1,17 +0,0 @@ -using MQTTnet.Server.Internal; - -namespace MQTTnet.Server -{ - public sealed class MqttPendingApplicationMessage - { - public MqttPendingApplicationMessage(MqttApplicationMessage applicationMessage, MqttClientConnection sender) - { - Sender = sender; - ApplicationMessage = applicationMessage; - } - - public MqttClientConnection Sender { get; } - - public MqttApplicationMessage ApplicationMessage { get; } - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Server/MqttPendingMessagesOverflowStrategy.cs b/Source/MQTTnet/Server/MqttPendingMessagesOverflowStrategy.cs deleted file mode 100644 index 1601487..0000000 --- a/Source/MQTTnet/Server/MqttPendingMessagesOverflowStrategy.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MQTTnet.Server -{ - public enum MqttPendingMessagesOverflowStrategy - { - DropOldestQueuedMessage, - DropNewMessage - } -} diff --git a/Source/MQTTnet/Server/MqttQueuedApplicationMessage.cs b/Source/MQTTnet/Server/MqttQueuedApplicationMessage.cs deleted file mode 100644 index b31ea84..0000000 --- a/Source/MQTTnet/Server/MqttQueuedApplicationMessage.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; -using MQTTnet.Protocol; - -namespace MQTTnet.Server -{ - public sealed class MqttQueuedApplicationMessage - { - public MqttApplicationMessage ApplicationMessage { get; set; } - - public string SenderClientId { get; set; } - - public bool IsRetainedMessage { get; set; } - - public List SubscriptionIdentifiers { get; set; } - - public bool IsDuplicate { get; set; } - - /// - /// Gets or sets the subscription quality of service level. - /// The Quality of Service (QoS) level is an agreement between the sender of a message and the receiver of a message that defines the guarantee of delivery for a specific message. - /// There are 3 QoS levels in MQTT: - /// - At most once (0): Message gets delivered no time, once or multiple times. - /// - At least once (1): Message gets delivered at least once (one time or more often). - /// - Exactly once (2): Message gets delivered exactly once (It's ensured that the message only comes once). - /// - public MqttQualityOfServiceLevel SubscriptionQualityOfServiceLevel { get; set; } - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Server/MqttRetainedMessageMatch.cs b/Source/MQTTnet/Server/MqttRetainedMessageMatch.cs new file mode 100644 index 0000000..a54e5b7 --- /dev/null +++ b/Source/MQTTnet/Server/MqttRetainedMessageMatch.cs @@ -0,0 +1,15 @@ +// 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 MqttApplicationMessage ApplicationMessage { get; set; } + + public MqttQualityOfServiceLevel SubscriptionQualityOfServiceLevel { get; set; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/MqttServer.cs b/Source/MQTTnet/Server/MqttServer.cs index 54827e2..4084e8f 100644 --- a/Source/MQTTnet/Server/MqttServer.cs +++ b/Source/MQTTnet/Server/MqttServer.cs @@ -1,192 +1,243 @@ -using MQTTnet.Adapter; -using MQTTnet.Client.Publishing; -using MQTTnet.Client.Receiving; -using MQTTnet.Diagnostics; -using MQTTnet.Exceptions; -using MQTTnet.Protocol; -using MQTTnet.Server.Status; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using MQTTnet.Diagnostics.Logger; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; using MQTTnet.Implementations; using MQTTnet.Internal; -using MQTTnet.Server.Internal; +using MQTTnet.Packets; +using MQTTnet.Protocol; namespace MQTTnet.Server { - public class MqttServer : Disposable, IMqttServer + public class MqttServer : Disposable { - readonly MqttServerEventDispatcher _eventDispatcher; + readonly MqttServerEventContainer _eventContainer = new MqttServerEventContainer(); + readonly ICollection _adapters; - readonly IMqttNetLogger _rootLogger; readonly MqttNetSourceLogger _logger; - - MqttClientSessionsManager _clientSessionsManager; - IMqttRetainedMessagesManager _retainedMessagesManager; - MqttServerKeepAliveMonitor _keepAliveMonitor; + readonly MqttServerOptions _options; + readonly IMqttNetLogger _rootLogger; + readonly MqttRetainedMessagesManager _retainedMessagesManager; + readonly MqttServerKeepAliveMonitor _keepAliveMonitor; + readonly MqttClientSessionsManager _clientSessionsManager; + CancellationTokenSource _cancellationTokenSource; - - public MqttServer(IEnumerable adapters, IMqttNetLogger logger) + + public MqttServer(MqttServerOptions options, IEnumerable adapters, IMqttNetLogger logger) { - if (adapters == null) throw new ArgumentNullException(nameof(adapters)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + + if (adapters == null) + { + throw new ArgumentNullException(nameof(adapters)); + } + _adapters = adapters.ToList(); - if (logger == null) throw new ArgumentNullException(nameof(logger)); + _rootLogger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger.WithSource(nameof(MqttServer)); - _rootLogger = logger; - _eventDispatcher = new MqttServerEventDispatcher(logger); + _retainedMessagesManager = new MqttRetainedMessagesManager(_eventContainer, _rootLogger); + _clientSessionsManager = new MqttClientSessionsManager(options, _retainedMessagesManager, _eventContainer, _rootLogger); + _keepAliveMonitor = new MqttServerKeepAliveMonitor(options, _clientSessionsManager, _rootLogger); } - public bool IsStarted => _cancellationTokenSource != null; + public event Func ApplicationMessageNotConsumedAsync + { + add => _eventContainer.ApplicationMessageNotConsumedEvent.AddHandler(value); + remove => _eventContainer.ApplicationMessageNotConsumedEvent.RemoveHandler(value); + } - public IMqttServerStartedHandler StartedHandler { get; set; } + public event Func ClientConnectedAsync + { + add => _eventContainer.ClientConnectedEvent.AddHandler(value); + remove => _eventContainer.ClientConnectedEvent.RemoveHandler(value); + } - public IMqttServerStoppedHandler StoppedHandler { get; set; } + public event Func ClientDisconnectedAsync + { + add => _eventContainer.ClientDisconnectedEvent.AddHandler(value); + remove => _eventContainer.ClientDisconnectedEvent.RemoveHandler(value); + } - public IMqttServerClientConnectedHandler ClientConnectedHandler + public event Func ClientSubscribedTopicAsync { - get => _eventDispatcher.ClientConnectedHandler; - set => _eventDispatcher.ClientConnectedHandler = value; + add => _eventContainer.ClientSubscribedTopicEvent.AddHandler(value); + remove => _eventContainer.ClientSubscribedTopicEvent.RemoveHandler(value); } - public IMqttServerClientDisconnectedHandler ClientDisconnectedHandler + public event Func ClientUnsubscribedTopicAsync { - get => _eventDispatcher.ClientDisconnectedHandler; - set => _eventDispatcher.ClientDisconnectedHandler = value; + add => _eventContainer.ClientUnsubscribedTopicEvent.AddHandler(value); + remove => _eventContainer.ClientUnsubscribedTopicEvent.RemoveHandler(value); } - public IMqttServerClientSubscribedTopicHandler ClientSubscribedTopicHandler + public event Func InterceptingInboundPacketAsync { - get => _eventDispatcher.ClientSubscribedTopicHandler; - set => _eventDispatcher.ClientSubscribedTopicHandler = value; + add => _eventContainer.InterceptingInboundPacketEvent.AddHandler(value); + remove => _eventContainer.InterceptingInboundPacketEvent.RemoveHandler(value); } - public IMqttServerClientUnsubscribedTopicHandler ClientUnsubscribedTopicHandler + public event Func InterceptingOutboundPacketAsync { - get => _eventDispatcher.ClientUnsubscribedTopicHandler; - set => _eventDispatcher.ClientUnsubscribedTopicHandler = value; + add => _eventContainer.InterceptingOutboundPacketEvent.AddHandler(value); + remove => _eventContainer.InterceptingOutboundPacketEvent.RemoveHandler(value); } - public IMqttApplicationMessageReceivedHandler ApplicationMessageReceivedHandler + public event Func InterceptingPublishAsync { - get => _eventDispatcher.ApplicationMessageReceivedHandler; - set => _eventDispatcher.ApplicationMessageReceivedHandler = value; + add => _eventContainer.InterceptingPublishEvent.AddHandler(value); + remove => _eventContainer.InterceptingPublishEvent.RemoveHandler(value); } - public IMqttServerOptions Options { get; private set; } + public event Func InterceptingSubscriptionAsync + { + add => _eventContainer.InterceptingSubscriptionEvent.AddHandler(value); + remove => _eventContainer.InterceptingSubscriptionEvent.RemoveHandler(value); + } - public Task> GetClientStatusAsync() + public event Func InterceptingUnsubscriptionAsync { - ThrowIfDisposed(); - ThrowIfNotStarted(); + add => _eventContainer.InterceptingUnsubscriptionEvent.AddHandler(value); + remove => _eventContainer.InterceptingUnsubscriptionEvent.RemoveHandler(value); + } - return _clientSessionsManager.GetClientStatusAsync(); + public event Func LoadingRetainedMessageAsync + { + add => _eventContainer.LoadingRetainedMessagesEvent.AddHandler(value); + remove => _eventContainer.LoadingRetainedMessagesEvent.RemoveHandler(value); } - public Task> GetSessionStatusAsync() + public event Func PreparingSessionAsync { - ThrowIfDisposed(); - ThrowIfNotStarted(); + add => _eventContainer.PreparingSessionEvent.AddHandler(value); + remove => _eventContainer.PreparingSessionEvent.RemoveHandler(value); + } - return _clientSessionsManager.GetSessionStatusAsync(); + public event Func RetainedMessageChangedAsync + { + add => _eventContainer.RetainedMessageChangedEvent.AddHandler(value); + remove => _eventContainer.RetainedMessageChangedEvent.RemoveHandler(value); } - public Task> GetRetainedApplicationMessagesAsync() + public event Func RetainedMessagesClearedAsync { - ThrowIfDisposed(); - ThrowIfNotStarted(); + add => _eventContainer.RetainedMessagesClearedEvent.AddHandler(value); + remove => _eventContainer.RetainedMessagesClearedEvent.RemoveHandler(value); + } - return _retainedMessagesManager.GetMessagesAsync(); + public event Func SessionDeletedAsync + { + add => _eventContainer.SessionDeletedEvent.AddHandler(value); + remove => _eventContainer.SessionDeletedEvent.RemoveHandler(value); } - public Task ClearRetainedApplicationMessagesAsync() + public event Func StartedAsync { - ThrowIfDisposed(); - ThrowIfNotStarted(); + add => _eventContainer.StartedEvent.AddHandler(value); + remove => _eventContainer.StartedEvent.RemoveHandler(value); + } - return _retainedMessagesManager?.ClearMessagesAsync() ?? PlatformAbstractionLayer.CompletedTask; + public event Func StoppedAsync + { + add => _eventContainer.StoppedEvent.AddHandler(value); + remove => _eventContainer.StoppedEvent.RemoveHandler(value); } - public Task SubscribeAsync(string clientId, ICollection topicFilters) + public event Func ValidatingConnectionAsync { - if (clientId == null) throw new ArgumentNullException(nameof(clientId)); - if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + add => _eventContainer.ValidatingConnectionEvent.AddHandler(value); + remove => _eventContainer.ValidatingConnectionEvent.RemoveHandler(value); + } - foreach (var topicFilter in topicFilters) + public bool IsStarted => _cancellationTokenSource != null; + + public Task DeleteRetainedMessagesAsync() + { + ThrowIfNotStarted(); + + return _retainedMessagesManager?.ClearMessages() ?? PlatformAbstractionLayer.CompletedTask; + } + + public Task DisconnectClientAsync(string id, MqttDisconnectReasonCode reasonCode) + { + if (id == null) { - MqttTopicValidator.ThrowIfInvalidSubscribe(topicFilter.Topic); + throw new ArgumentNullException(nameof(id)); } - ThrowIfDisposed(); + ThrowIfNotStarted(); + + return _clientSessionsManager.GetClient(id).StopAsync(reasonCode); + } + + public Task> GetClientsAsync() + { ThrowIfNotStarted(); - return _clientSessionsManager.SubscribeAsync(clientId, topicFilters); + return _clientSessionsManager.GetClientStatusAsync(); } - public Task UnsubscribeAsync(string clientId, ICollection topicFilters) + public Task> GetRetainedMessagesAsync() { - if (clientId == null) throw new ArgumentNullException(nameof(clientId)); - if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + ThrowIfNotStarted(); - ThrowIfDisposed(); + return _retainedMessagesManager.GetMessages(); + } + + public Task> GetSessionsAsync() + { ThrowIfNotStarted(); - return _clientSessionsManager.UnsubscribeAsync(clientId, topicFilters); + return _clientSessionsManager.GetSessionStatusAsync(); } - public Task PublishAsync(MqttApplicationMessage applicationMessage, CancellationToken cancellationToken) + public Task InjectApplicationMessage(InjectedMqttApplicationMessage injectedApplicationMessage) { - if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + if (injectedApplicationMessage == null) + { + throw new ArgumentNullException(nameof(injectedApplicationMessage)); + } - ThrowIfDisposed(); + if (injectedApplicationMessage.ApplicationMessage == null) + { + throw new ArgumentNullException(nameof(injectedApplicationMessage.ApplicationMessage)); + } - MqttTopicValidator.ThrowIfInvalid(applicationMessage.Topic); + MqttTopicValidator.ThrowIfInvalid(injectedApplicationMessage.ApplicationMessage.Topic); ThrowIfNotStarted(); - _clientSessionsManager.DispatchApplicationMessage(applicationMessage, null); - - return Task.FromResult(new MqttClientPublishResult()); + return _clientSessionsManager.DispatchApplicationMessage(injectedApplicationMessage.SenderClientId, injectedApplicationMessage.ApplicationMessage); } - public async Task StartAsync(IMqttServerOptions options) + public async Task StartAsync() { - ThrowIfDisposed(); ThrowIfStarted(); - Options = options ?? throw new ArgumentNullException(nameof(options)); - _cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = _cancellationTokenSource.Token; - _retainedMessagesManager = Options.RetainedMessagesManager ?? throw new MqttConfigurationException("options.RetainedMessagesManager should not be null."); - - await _retainedMessagesManager.Start(Options, _rootLogger).ConfigureAwait(false); - await _retainedMessagesManager.LoadMessagesAsync().ConfigureAwait(false); - - _clientSessionsManager = new MqttClientSessionsManager(Options, _retainedMessagesManager, _eventDispatcher, _rootLogger); - _clientSessionsManager.Start(cancellationToken); - - _keepAliveMonitor = new MqttServerKeepAliveMonitor(Options, _clientSessionsManager, _rootLogger); + 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).ConfigureAwait(false); + await adapter.StartAsync(_options, _rootLogger).ConfigureAwait(false); } + await _eventContainer.StartedEvent.InvokeAsync(EventArgs.Empty).ConfigureAwait(false); + _logger.Info("Started."); - - var startedHandler = StartedHandler; - if (startedHandler != null) - { - await startedHandler.HandleServerStartedAsync(EventArgs.Empty).ConfigureAwait(false); - } } public async Task StopAsync() @@ -201,31 +252,76 @@ namespace MQTTnet.Server _cancellationTokenSource.Cancel(false); await _clientSessionsManager.CloseAllConnectionsAsync().ConfigureAwait(false); - + foreach (var adapter in _adapters) { adapter.ClientHandler = null; await adapter.StopAsync().ConfigureAwait(false); } - - _logger.Info("Stopped."); } finally { - _clientSessionsManager?.Dispose(); - _clientSessionsManager = null; - _cancellationTokenSource?.Dispose(); _cancellationTokenSource = null; + } + + await _eventContainer.StoppedEvent.InvokeAsync(EventArgs.Empty).ConfigureAwait(false); + + _logger.Info("Stopped."); + } - _retainedMessagesManager = null; + public Task SubscribeAsync(string clientId, ICollection topicFilters) + { + if (clientId == null) + { + throw new ArgumentNullException(nameof(clientId)); } - var stoppedHandler = StoppedHandler; - if (stoppedHandler != null) + if (topicFilters == null) { - await stoppedHandler.HandleServerStoppedAsync(EventArgs.Empty).ConfigureAwait(false); + 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 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(null, retainedMessage); } protected override void Dispose(bool disposing) @@ -248,20 +344,24 @@ namespace MQTTnet.Server return _clientSessionsManager.HandleClientConnectionAsync(channelAdapter, cancellationToken); } - void ThrowIfStarted() + void ThrowIfNotStarted() { - if (_cancellationTokenSource != null) + ThrowIfDisposed(); + + if (_cancellationTokenSource == null) { - throw new InvalidOperationException("The MQTT server is already started."); + throw new InvalidOperationException("The MQTT server is not started."); } } - void ThrowIfNotStarted() + void ThrowIfStarted() { - if (_cancellationTokenSource == null) + ThrowIfDisposed(); + + if (_cancellationTokenSource != null) { - throw new InvalidOperationException("The MQTT server is not started."); + throw new InvalidOperationException("The MQTT server is already started."); } } } -} +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/MqttServerApplicationMessageInterceptorDelegate.cs b/Source/MQTTnet/Server/MqttServerApplicationMessageInterceptorDelegate.cs deleted file mode 100644 index e946399..0000000 --- a/Source/MQTTnet/Server/MqttServerApplicationMessageInterceptorDelegate.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Threading.Tasks; -using MQTTnet.Implementations; - -namespace MQTTnet.Server -{ - public sealed class MqttServerApplicationMessageInterceptorDelegate : IMqttServerApplicationMessageInterceptor - { - readonly Func _callback; - - public MqttServerApplicationMessageInterceptorDelegate(Action callback) - { - if (callback == null) throw new ArgumentNullException(nameof(callback)); - - _callback = context => - { - callback(context); - return PlatformAbstractionLayer.CompletedTask; - }; - } - - public MqttServerApplicationMessageInterceptorDelegate(Func callback) - { - _callback = callback ?? throw new ArgumentNullException(nameof(callback)); - } - - public Task InterceptApplicationMessagePublishAsync(MqttApplicationMessageInterceptorContext context) - { - return _callback(context); - } - } -} diff --git a/Source/MQTTnet/Server/MqttServerCertificateCredentials.cs b/Source/MQTTnet/Server/MqttServerCertificateCredentials.cs deleted file mode 100644 index 05b6c5f..0000000 --- a/Source/MQTTnet/Server/MqttServerCertificateCredentials.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MQTTnet.Server -{ - public class MqttServerCertificateCredentials : IMqttServerCertificateCredentials - { - public string Password { get; set; } - } -} diff --git a/Source/MQTTnet/Server/MqttServerClientConnectedHandlerDelegate.cs b/Source/MQTTnet/Server/MqttServerClientConnectedHandlerDelegate.cs deleted file mode 100644 index a416ea9..0000000 --- a/Source/MQTTnet/Server/MqttServerClientConnectedHandlerDelegate.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Threading.Tasks; -using MQTTnet.Implementations; - -namespace MQTTnet.Server -{ - public sealed class MqttServerClientConnectedHandlerDelegate : IMqttServerClientConnectedHandler - { - readonly Func _handler; - - public MqttServerClientConnectedHandlerDelegate(Action handler) - { - if (handler == null) throw new ArgumentNullException(nameof(handler)); - - _handler = eventArgs => - { - handler(eventArgs); - return PlatformAbstractionLayer.CompletedTask; - }; - } - - public MqttServerClientConnectedHandlerDelegate(Func handler) - { - _handler = handler ?? throw new ArgumentNullException(nameof(handler)); - } - - public Task HandleClientConnectedAsync(MqttServerClientConnectedEventArgs eventArgs) - { - return _handler(eventArgs); - } - } -} diff --git a/Source/MQTTnet/Server/MqttServerClientDisconnectedHandlerDelegate.cs b/Source/MQTTnet/Server/MqttServerClientDisconnectedHandlerDelegate.cs deleted file mode 100644 index 2a03df9..0000000 --- a/Source/MQTTnet/Server/MqttServerClientDisconnectedHandlerDelegate.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Threading.Tasks; -using MQTTnet.Implementations; - -namespace MQTTnet.Server -{ - public sealed class MqttServerClientDisconnectedHandlerDelegate : IMqttServerClientDisconnectedHandler - { - readonly Func _handler; - - public MqttServerClientDisconnectedHandlerDelegate(Action handler) - { - if (handler == null) throw new ArgumentNullException(nameof(handler)); - - _handler = eventArgs => - { - handler(eventArgs); - return PlatformAbstractionLayer.CompletedTask; - }; - } - - public MqttServerClientDisconnectedHandlerDelegate(Func handler) - { - _handler = handler ?? throw new ArgumentNullException(nameof(handler)); - } - - public Task HandleClientDisconnectedAsync(MqttServerClientDisconnectedEventArgs eventArgs) - { - return _handler(eventArgs); - } - } -} diff --git a/Source/MQTTnet/Server/MqttServerClientSubscribedTopicHandlerDelegate.cs b/Source/MQTTnet/Server/MqttServerClientSubscribedTopicHandlerDelegate.cs deleted file mode 100644 index 6dbf4a3..0000000 --- a/Source/MQTTnet/Server/MqttServerClientSubscribedTopicHandlerDelegate.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Threading.Tasks; -using MQTTnet.Implementations; - -namespace MQTTnet.Server -{ - [Obsolete("Use MqttServerClientSubscribedTopicHandlerDelegate instead. This will be removed in a future version.")] - public sealed class MqttServerClientSubscribedHandlerDelegate : MqttServerClientSubscribedTopicHandlerDelegate - { - public MqttServerClientSubscribedHandlerDelegate(Action handler) : base(handler) - { - } - - public MqttServerClientSubscribedHandlerDelegate(Func handler) : base(handler) - { - } - } - - public class MqttServerClientSubscribedTopicHandlerDelegate : IMqttServerClientSubscribedTopicHandler - { - readonly Func _handler; - - public MqttServerClientSubscribedTopicHandlerDelegate(Action handler) - { - if (handler == null) throw new ArgumentNullException(nameof(handler)); - - _handler = eventArgs => - { - handler(eventArgs); - return PlatformAbstractionLayer.CompletedTask; - }; - } - - public MqttServerClientSubscribedTopicHandlerDelegate(Func handler) - { - _handler = handler ?? throw new ArgumentNullException(nameof(handler)); - } - - public Task HandleClientSubscribedTopicAsync(MqttServerClientSubscribedTopicEventArgs eventArgs) - { - return _handler(eventArgs); - } - } -} diff --git a/Source/MQTTnet/Server/MqttServerClientUnsubscribedTopicHandlerDelegate.cs b/Source/MQTTnet/Server/MqttServerClientUnsubscribedTopicHandlerDelegate.cs deleted file mode 100644 index c93636c..0000000 --- a/Source/MQTTnet/Server/MqttServerClientUnsubscribedTopicHandlerDelegate.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Threading.Tasks; -using MQTTnet.Implementations; - -namespace MQTTnet.Server -{ - public sealed class MqttServerClientUnsubscribedTopicHandlerDelegate : IMqttServerClientUnsubscribedTopicHandler - { - readonly Func _handler; - - public MqttServerClientUnsubscribedTopicHandlerDelegate(Action handler) - { - if (handler == null) throw new ArgumentNullException(nameof(handler)); - - _handler = eventArgs => - { - handler(eventArgs); - return PlatformAbstractionLayer.CompletedTask; - }; - } - - public MqttServerClientUnsubscribedTopicHandlerDelegate(Func handler) - { - _handler = handler ?? throw new ArgumentNullException(nameof(handler)); - } - - public Task HandleClientUnsubscribedTopicAsync(MqttServerClientUnsubscribedTopicEventArgs eventArgs) - { - return _handler(eventArgs); - } - } -} diff --git a/Source/MQTTnet/Server/MqttServerConnectionValidatorDelegate.cs b/Source/MQTTnet/Server/MqttServerConnectionValidatorDelegate.cs deleted file mode 100644 index b6a51a1..0000000 --- a/Source/MQTTnet/Server/MqttServerConnectionValidatorDelegate.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Threading.Tasks; -using MQTTnet.Implementations; - -namespace MQTTnet.Server -{ - public sealed class MqttServerConnectionValidatorDelegate : IMqttServerConnectionValidator - { - readonly Func _callback; - - public MqttServerConnectionValidatorDelegate(Action callback) - { - if (callback == null) throw new ArgumentNullException(nameof(callback)); - - _callback = context => - { - callback(context); - return PlatformAbstractionLayer.CompletedTask; - }; - } - - public MqttServerConnectionValidatorDelegate(Func callback) - { - _callback = callback ?? throw new ArgumentNullException(nameof(callback)); - } - - public Task ValidateConnectionAsync(MqttConnectionValidatorContext context) - { - return _callback(context); - } - } -} diff --git a/Source/MQTTnet/Server/MqttServerExtensions.cs b/Source/MQTTnet/Server/MqttServerExtensions.cs index b0282b3..c027c59 100644 --- a/Source/MQTTnet/Server/MqttServerExtensions.cs +++ b/Source/MQTTnet/Server/MqttServerExtensions.cs @@ -1,112 +1,18 @@ -using MQTTnet.Client.Publishing; -using MQTTnet.Client.Receiving; +// 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; using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using MQTTnet.Packets; namespace MQTTnet.Server { public static class MqttServerExtensions { - public static IMqttServer UseClientConnectedHandler(this IMqttServer server, Func handler) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - - if (handler == null) - { - return server.UseClientConnectedHandler((IMqttServerClientConnectedHandler)null); - } - - return server.UseClientConnectedHandler(new MqttServerClientConnectedHandlerDelegate(handler)); - } - - public static IMqttServer UseClientConnectedHandler(this IMqttServer server, Action handler) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - - if (handler == null) - { - return server.UseClientConnectedHandler((IMqttServerClientConnectedHandler)null); - } - - return server.UseClientConnectedHandler(new MqttServerClientConnectedHandlerDelegate(handler)); - } - - public static IMqttServer UseClientConnectedHandler(this IMqttServer server, IMqttServerClientConnectedHandler handler) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - - server.ClientConnectedHandler = handler; - return server; - } - - public static IMqttServer UseClientDisconnectedHandler(this IMqttServer server, Func handler) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - - if (handler == null) - { - return server.UseClientDisconnectedHandler((IMqttServerClientDisconnectedHandler)null); - } - - return server.UseClientDisconnectedHandler(new MqttServerClientDisconnectedHandlerDelegate(handler)); - } - - public static IMqttServer UseClientDisconnectedHandler(this IMqttServer server, Action handler) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - - if (handler == null) - { - return server.UseClientDisconnectedHandler((IMqttServerClientDisconnectedHandler)null); - } - - return server.UseClientDisconnectedHandler(new MqttServerClientDisconnectedHandlerDelegate(handler)); - } - - public static IMqttServer UseClientDisconnectedHandler(this IMqttServer server, IMqttServerClientDisconnectedHandler handler) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - - server.ClientDisconnectedHandler = handler; - return server; - } - - public static IMqttServer UseApplicationMessageReceivedHandler(this IMqttServer server, Func handler) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - - if (handler == null) - { - return server.UseApplicationMessageReceivedHandler((IMqttApplicationMessageReceivedHandler)null); - } - - return server.UseApplicationMessageReceivedHandler(new MqttApplicationMessageReceivedHandlerDelegate(handler)); - } - - public static IMqttServer UseApplicationMessageReceivedHandler(this IMqttServer server, Action handler) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - - if (handler == null) - { - return server.UseApplicationMessageReceivedHandler((IMqttApplicationMessageReceivedHandler)null); - } - - return server.UseApplicationMessageReceivedHandler(new MqttApplicationMessageReceivedHandlerDelegate(handler)); - } - - public static IMqttServer UseApplicationMessageReceivedHandler(this IMqttServer server, IMqttApplicationMessageReceivedHandler handler) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - - server.ApplicationMessageReceivedHandler = handler; - return server; - } - - public static Task SubscribeAsync(this IMqttServer server, string clientId, params MqttTopicFilter[] topicFilters) + 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)); @@ -114,17 +20,8 @@ namespace MQTTnet.Server return server.SubscribeAsync(clientId, topicFilters); } - - public static Task SubscribeAsync(this IMqttServer server, string clientId, string topic, MqttQualityOfServiceLevel qualityOfServiceLevel) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - if (clientId == null) throw new ArgumentNullException(nameof(clientId)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); - - return server.SubscribeAsync(clientId, new MqttTopicFilterBuilder().WithTopic(topic).WithQualityOfServiceLevel(qualityOfServiceLevel).Build()); - } - - public static Task SubscribeAsync(this IMqttServer server, string clientId, string topic) + + 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)); @@ -132,102 +29,5 @@ namespace MQTTnet.Server return server.SubscribeAsync(clientId, new MqttTopicFilterBuilder().WithTopic(topic).Build()); } - - public static Task UnsubscribeAsync(this IMqttServer server, string clientId, params string[] 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.UnsubscribeAsync(clientId, topicFilters); - } - - public static async Task PublishAsync(this IMqttServer server, IEnumerable applicationMessages) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - if (applicationMessages == null) throw new ArgumentNullException(nameof(applicationMessages)); - - foreach (var applicationMessage in applicationMessages) - { - await server.PublishAsync(applicationMessage).ConfigureAwait(false); - } - } - - public static Task PublishAsync(this IMqttServer server, MqttApplicationMessage applicationMessage) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); - - return server.PublishAsync(applicationMessage, CancellationToken.None); - } - - public static async Task PublishAsync(this IMqttServer server, params MqttApplicationMessage[] applicationMessages) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - if (applicationMessages == null) throw new ArgumentNullException(nameof(applicationMessages)); - - foreach (var applicationMessage in applicationMessages) - { - await server.PublishAsync(applicationMessage, CancellationToken.None).ConfigureAwait(false); - } - } - - public static Task PublishAsync(this IMqttServer server, string topic) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); - - return server.PublishAsync(builder => builder - .WithTopic(topic)); - } - - public static Task PublishAsync(this IMqttServer server, string topic, string payload) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); - - return server.PublishAsync(builder => builder - .WithTopic(topic) - .WithPayload(payload)); - } - - public static Task PublishAsync(this IMqttServer server, string topic, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); - - return server.PublishAsync(builder => builder - .WithTopic(topic) - .WithPayload(payload) - .WithQualityOfServiceLevel(qualityOfServiceLevel)); - } - - public static Task PublishAsync(this IMqttServer server, string topic, string payload, MqttQualityOfServiceLevel qualityOfServiceLevel, bool retain) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - if (topic == null) throw new ArgumentNullException(nameof(topic)); - - return server.PublishAsync(builder => builder - .WithTopic(topic) - .WithPayload(payload) - .WithQualityOfServiceLevel(qualityOfServiceLevel) - .WithRetainFlag(retain)); - } - - public static Task PublishAsync(this IMqttServer server, Func builder, CancellationToken cancellationToken) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - - var message = builder(new MqttApplicationMessageBuilder()).Build(); - return server.PublishAsync(message, cancellationToken); - } - - public static Task PublishAsync(this IMqttServer server, Func builder) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - - var message = builder(new MqttApplicationMessageBuilder()).Build(); - return server.PublishAsync(message, CancellationToken.None); - } } } diff --git a/Source/MQTTnet/Server/MqttServerMultiThreadedApplicationMessageInterceptorDelegate.cs b/Source/MQTTnet/Server/MqttServerMultiThreadedApplicationMessageInterceptorDelegate.cs deleted file mode 100644 index bd7b6ec..0000000 --- a/Source/MQTTnet/Server/MqttServerMultiThreadedApplicationMessageInterceptorDelegate.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Threading.Tasks; -using MQTTnet.Implementations; -using MQTTnet.Internal; - -namespace MQTTnet.Server -{ - public sealed class MqttServerMultiThreadedApplicationMessageInterceptorDelegate : IMqttServerApplicationMessageInterceptor - { - readonly Func _callback; - - public MqttServerMultiThreadedApplicationMessageInterceptorDelegate(Action callback) - { - if (callback == null) throw new ArgumentNullException(nameof(callback)); - - _callback = context => - { - callback(context); - return PlatformAbstractionLayer.CompletedTask; - }; - } - - public Func ExceptionHandler { get; set; } - - public MqttServerMultiThreadedApplicationMessageInterceptorDelegate(Func callback) - { - _callback = callback ?? throw new ArgumentNullException(nameof(callback)); - } - - public Task InterceptApplicationMessagePublishAsync(MqttApplicationMessageInterceptorContext context) - { - Task.Run(async () => - { - try - { - await _callback.Invoke(context).ConfigureAwait(false); - } - catch (Exception exception) - { - var exceptionHandler = ExceptionHandler; - if (exceptionHandler != null) - { - await exceptionHandler.Invoke(context, exception).ConfigureAwait(false); - } - } - }).RunInBackground(); - - return PlatformAbstractionLayer.CompletedTask; - } - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Server/MqttServerOptions.cs b/Source/MQTTnet/Server/MqttServerOptions.cs deleted file mode 100644 index 2823365..0000000 --- a/Source/MQTTnet/Server/MqttServerOptions.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using MQTTnet.Server.Internal; - -namespace MQTTnet.Server -{ - public sealed class MqttServerOptions : IMqttServerOptions - { - public MqttServerTcpEndpointOptions DefaultEndpointOptions { get; } = new MqttServerTcpEndpointOptions(); - - public MqttServerTlsTcpEndpointOptions TlsEndpointOptions { get; } = new MqttServerTlsTcpEndpointOptions(); - - /// - /// 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. - /// - public string ClientId { get; set; } - - public bool EnablePersistentSessions { get; set; } - - public int MaxPendingMessagesPerClient { get; set; } = 250; - - public MqttPendingMessagesOverflowStrategy PendingMessagesOverflowStrategy { get; set; } = MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage; - - public TimeSpan DefaultCommunicationTimeout { get; set; } = TimeSpan.FromSeconds(15); - - public TimeSpan KeepAliveMonitorInterval { get; set; } = TimeSpan.FromMilliseconds(500); - - public IMqttServerConnectionValidator ConnectionValidator { get; set; } - - public IMqttServerApplicationMessageInterceptor ApplicationMessageInterceptor { get; set; } - - public IMqttServerClientMessageQueueInterceptor ClientMessageQueueInterceptor { get; set; } - - public IMqttServerSubscriptionInterceptor SubscriptionInterceptor { get; set; } - - public IMqttServerUnsubscriptionInterceptor UnsubscriptionInterceptor { get; set; } - - public IMqttServerApplicationMessageInterceptor UndeliveredMessageInterceptor { get; set; } - - public IMqttServerStorage Storage { get; set; } - - public IMqttRetainedMessagesManager RetainedMessagesManager { get; set; } = new MqttRetainedMessagesManager(); - } -} diff --git a/Source/MQTTnet/Server/MqttServerStartedHandlerDelegate.cs b/Source/MQTTnet/Server/MqttServerStartedHandlerDelegate.cs deleted file mode 100644 index ad33184..0000000 --- a/Source/MQTTnet/Server/MqttServerStartedHandlerDelegate.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Threading.Tasks; -using MQTTnet.Implementations; - -namespace MQTTnet.Server -{ - public sealed class MqttServerStartedHandlerDelegate : IMqttServerStartedHandler - { - readonly Func _handler; - - public MqttServerStartedHandlerDelegate(Action handler) - { - if (handler == null) throw new ArgumentNullException(nameof(handler)); - - _handler = eventArgs => - { - handler(eventArgs); - return PlatformAbstractionLayer.CompletedTask; - }; - } - - public MqttServerStartedHandlerDelegate(Func handler) - { - _handler = handler ?? throw new ArgumentNullException(nameof(handler)); - } - - public Task HandleServerStartedAsync(EventArgs eventArgs) - { - return _handler(eventArgs); - } - } -} diff --git a/Source/MQTTnet/Server/MqttServerStoppedHandlerDelegate.cs b/Source/MQTTnet/Server/MqttServerStoppedHandlerDelegate.cs deleted file mode 100644 index 8a9a37e..0000000 --- a/Source/MQTTnet/Server/MqttServerStoppedHandlerDelegate.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Threading.Tasks; -using MQTTnet.Implementations; - -namespace MQTTnet.Server -{ - public sealed class MqttServerStoppedHandlerDelegate : IMqttServerStoppedHandler - { - readonly Func _handler; - - public MqttServerStoppedHandlerDelegate(Action handler) - { - if (handler == null) throw new ArgumentNullException(nameof(handler)); - - _handler = eventArgs => - { - handler(eventArgs); - return PlatformAbstractionLayer.CompletedTask; - }; - } - - public MqttServerStoppedHandlerDelegate(Func handler) - { - _handler = handler ?? throw new ArgumentNullException(nameof(handler)); - } - - public Task HandleServerStoppedAsync(EventArgs eventArgs) - { - return _handler(eventArgs); - } - } -} diff --git a/Source/MQTTnet/Server/MqttServerSubscriptionInterceptorDelegate.cs b/Source/MQTTnet/Server/MqttServerSubscriptionInterceptorDelegate.cs deleted file mode 100644 index 88d6823..0000000 --- a/Source/MQTTnet/Server/MqttServerSubscriptionInterceptorDelegate.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Threading.Tasks; -using MQTTnet.Implementations; - -namespace MQTTnet.Server -{ - public sealed class MqttServerSubscriptionInterceptorDelegate : IMqttServerSubscriptionInterceptor - { - readonly Func _callback; - - public MqttServerSubscriptionInterceptorDelegate(Action callback) - { - if (callback == null) throw new ArgumentNullException(nameof(callback)); - - _callback = context => - { - callback(context); - return PlatformAbstractionLayer.CompletedTask; - }; - } - - public MqttServerSubscriptionInterceptorDelegate(Func callback) - { - _callback = callback ?? throw new ArgumentNullException(nameof(callback)); - } - - public Task InterceptSubscriptionAsync(MqttSubscriptionInterceptorContext context) - { - return _callback(context); - } - } -} diff --git a/Source/MQTTnet/Server/MqttServerTcpEndpointOptions.cs b/Source/MQTTnet/Server/MqttServerTcpEndpointOptions.cs deleted file mode 100644 index 0eb6dc2..0000000 --- a/Source/MQTTnet/Server/MqttServerTcpEndpointOptions.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MQTTnet.Server -{ - public class MqttServerTcpEndpointOptions : MqttServerTcpEndpointBaseOptions - { - public MqttServerTcpEndpointOptions() - { - IsEnabled = true; - Port = 1883; - } - } -} diff --git a/Source/MQTTnet/Server/MqttServerTlsTcpEndpointOptions.cs b/Source/MQTTnet/Server/MqttServerTlsTcpEndpointOptions.cs deleted file mode 100644 index 25f61bd..0000000 --- a/Source/MQTTnet/Server/MqttServerTlsTcpEndpointOptions.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Security.Authentication; -using MQTTnet.Certificates; - -namespace MQTTnet.Server -{ - public class MqttServerTlsTcpEndpointOptions : MqttServerTcpEndpointBaseOptions - { - ICertificateProvider _certificateProvider; - - public MqttServerTlsTcpEndpointOptions() - { - Port = 8883; - } - - [Obsolete("Please use CertificateProvider with 'BlobCertificateProvider' instead.")] - public byte[] Certificate { get; set; } - - [Obsolete("Please use CertificateProvider with 'BlobCertificateProvider' including password property instead.")] - public IMqttServerCertificateCredentials CertificateCredentials { get; set; } - -#if !WINDOWS_UWP - public System.Net.Security.RemoteCertificateValidationCallback RemoteCertificateValidationCallback { get; set; } -#endif - public ICertificateProvider CertificateProvider - { - get - { - // Backward compatibility only. Gets converted to auto property when - // obsolete properties are removed. - if (_certificateProvider != null) - { - return _certificateProvider; - } - - if (Certificate == null) - { - return null; - } - - return new BlobCertificateProvider(Certificate) - { - Password = CertificateCredentials?.Password - }; - } - - set => _certificateProvider = value; - } - - public bool ClientCertificateRequired { get; set; } - - public bool CheckCertificateRevocation { get; set; } - - public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls12; - } -} diff --git a/Source/MQTTnet/Server/MqttSubscriptionInterceptorContext.cs b/Source/MQTTnet/Server/MqttSubscriptionInterceptorContext.cs deleted file mode 100644 index 025a650..0000000 --- a/Source/MQTTnet/Server/MqttSubscriptionInterceptorContext.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; - -namespace MQTTnet.Server -{ - public sealed class MqttSubscriptionInterceptorContext - { - /// - /// Gets the client identifier. - /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. - /// - public string ClientId { get; internal set; } - - /// - /// Gets or sets the topic filter. - /// The topic filter can contain topics and wildcards. - /// - public MqttTopicFilter TopicFilter { get; set; } - - /// - /// Gets or sets a key/value collection that can be used to share data within the scope of this session. - /// - public IDictionary SessionItems { get; internal set; } - - public bool AcceptSubscription { get; set; } = true; - - public bool CloseConnection { get; set; } - } -} diff --git a/Source/MQTTnet/Server/MqttUnsubscriptionInterceptorContext.cs b/Source/MQTTnet/Server/MqttUnsubscriptionInterceptorContext.cs deleted file mode 100644 index 99b1f1f..0000000 --- a/Source/MQTTnet/Server/MqttUnsubscriptionInterceptorContext.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Collections.Generic; - -namespace MQTTnet.Server -{ - public sealed class MqttUnsubscriptionInterceptorContext - { - /// - /// Gets the client identifier. - /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. - /// - public string ClientId { get; internal set; } - - /// - /// 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). - /// - public string Topic { get; internal set; } - - /// - /// Gets or sets a key/value collection that can be used to share data within the scope of this session. - /// - public IDictionary SessionItems { get; internal set; } - - public bool AcceptUnsubscription { get; set; } = true; - - public bool CloseConnection { get; set; } - } -} diff --git a/Source/MQTTnet/Server/Options/IMqttServerCertificateCredentials.cs b/Source/MQTTnet/Server/Options/IMqttServerCertificateCredentials.cs new file mode 100644 index 0000000..d0b7563 --- /dev/null +++ b/Source/MQTTnet/Server/Options/IMqttServerCertificateCredentials.cs @@ -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; } + } +} diff --git a/Source/MQTTnet/Server/Options/MqttPendingMessagesOverflowStrategy.cs b/Source/MQTTnet/Server/Options/MqttPendingMessagesOverflowStrategy.cs new file mode 100644 index 0000000..e9dada1 --- /dev/null +++ b/Source/MQTTnet/Server/Options/MqttPendingMessagesOverflowStrategy.cs @@ -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 + } +} diff --git a/Source/MQTTnet/Server/Options/MqttServerCertificateCredentials.cs b/Source/MQTTnet/Server/Options/MqttServerCertificateCredentials.cs new file mode 100644 index 0000000..f0ce001 --- /dev/null +++ b/Source/MQTTnet/Server/Options/MqttServerCertificateCredentials.cs @@ -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; } + } +} diff --git a/Source/MQTTnet/Server/Options/MqttServerOptions.cs b/Source/MQTTnet/Server/Options/MqttServerOptions.cs new file mode 100644 index 0000000..59e46ab --- /dev/null +++ b/Source/MQTTnet/Server/Options/MqttServerOptions.cs @@ -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 TimeSpan KeepAliveMonitorInterval { get; set; } = TimeSpan.FromMilliseconds(500); + + public int MaxPendingMessagesPerClient { get; set; } = 250; + + public MqttPendingMessagesOverflowStrategy PendingMessagesOverflowStrategy { get; set; } = MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage; + + public MqttServerTlsTcpEndpointOptions TlsEndpointOptions { get; } = new MqttServerTlsTcpEndpointOptions(); + + /// + /// 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. + /// + public int WriterBufferSize { get; set; } = 4096; + + /// + /// 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. + /// + public int WriterBufferSizeMax { get; set; } = 65535; + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/MqttServerOptionsBuilder.cs b/Source/MQTTnet/Server/Options/MqttServerOptionsBuilder.cs similarity index 54% rename from Source/MQTTnet/Server/MqttServerOptionsBuilder.cs rename to Source/MQTTnet/Server/Options/MqttServerOptionsBuilder.cs index 386c40f..b6a50cc 100644 --- a/Source/MQTTnet/Server/MqttServerOptionsBuilder.cs +++ b/Source/MQTTnet/Server/Options/MqttServerOptionsBuilder.cs @@ -1,4 +1,8 @@ -using System; +// 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; @@ -140,103 +144,37 @@ namespace MQTTnet.Server return this; } #endif - - public MqttServerOptionsBuilder WithStorage(IMqttServerStorage value) - { - _options.Storage = value; - return this; - } - - public MqttServerOptionsBuilder WithRetainedMessagesManager(IMqttRetainedMessagesManager value) - { - _options.RetainedMessagesManager = value; - return this; - } - - public MqttServerOptionsBuilder WithConnectionValidator(IMqttServerConnectionValidator value) - { - _options.ConnectionValidator = value; - return this; - } - - public MqttServerOptionsBuilder WithConnectionValidator(Action value) - { - _options.ConnectionValidator = new MqttServerConnectionValidatorDelegate(value); - return this; - } - public MqttServerOptionsBuilder WithConnectionValidator(Func value) - { - _options.ConnectionValidator = new MqttServerConnectionValidatorDelegate(value); - return this; - } + // public MqttServerOptionsBuilder WithApplicationMessageInterceptor(IMqttServerApplicationMessageInterceptor value) + // { + // _options.ApplicationMessageInterceptor = value; + // return this; + // } + // + // public MqttServerOptionsBuilder WithApplicationMessageInterceptor(Action value) + // { + // _options.ApplicationMessageInterceptor = new MqttServerApplicationMessageInterceptorDelegate(value); + // return this; + // } + // + // public MqttServerOptionsBuilder WithApplicationMessageInterceptor(Func value) + // { + // _options.ApplicationMessageInterceptor = new MqttServerApplicationMessageInterceptorDelegate(value); + // return this; + // } + + // public MqttServerOptionsBuilder WithMultiThreadedApplicationMessageInterceptor(Action value) + // { + // _options.ApplicationMessageInterceptor = new MqttServerMultiThreadedApplicationMessageInterceptorDelegate(value); + // return this; + // } + // + // public MqttServerOptionsBuilder WithMultiThreadedApplicationMessageInterceptor(Func value) + // { + // _options.ApplicationMessageInterceptor = new MqttServerMultiThreadedApplicationMessageInterceptorDelegate(value); + // return this; + // } - public MqttServerOptionsBuilder WithApplicationMessageInterceptor(IMqttServerApplicationMessageInterceptor value) - { - _options.ApplicationMessageInterceptor = value; - return this; - } - - public MqttServerOptionsBuilder WithApplicationMessageInterceptor(Action value) - { - _options.ApplicationMessageInterceptor = new MqttServerApplicationMessageInterceptorDelegate(value); - return this; - } - - public MqttServerOptionsBuilder WithApplicationMessageInterceptor(Func value) - { - _options.ApplicationMessageInterceptor = new MqttServerApplicationMessageInterceptorDelegate(value); - return this; - } - - public MqttServerOptionsBuilder WithMultiThreadedApplicationMessageInterceptor(Action value) - { - _options.ApplicationMessageInterceptor = new MqttServerMultiThreadedApplicationMessageInterceptorDelegate(value); - return this; - } - - public MqttServerOptionsBuilder WithMultiThreadedApplicationMessageInterceptor(Func value) - { - _options.ApplicationMessageInterceptor = new MqttServerMultiThreadedApplicationMessageInterceptorDelegate(value); - return this; - } - - public MqttServerOptionsBuilder WithClientMessageQueueInterceptor(IMqttServerClientMessageQueueInterceptor value) - { - _options.ClientMessageQueueInterceptor = value; - return this; - } - - public MqttServerOptionsBuilder WithClientMessageQueueInterceptor(Action value) - { - _options.ClientMessageQueueInterceptor = new MqttClientMessageQueueInterceptorDelegate(value); - return this; - } - - public MqttServerOptionsBuilder WithSubscriptionInterceptor(IMqttServerSubscriptionInterceptor value) - { - _options.SubscriptionInterceptor = value; - return this; - } - - public MqttServerOptionsBuilder WithUnsubscriptionInterceptor(IMqttServerUnsubscriptionInterceptor value) - { - _options.UnsubscriptionInterceptor = value; - return this; - } - - public MqttServerOptionsBuilder WithSubscriptionInterceptor(Action value) - { - _options.SubscriptionInterceptor = new MqttServerSubscriptionInterceptorDelegate(value); - return this; - } - - public MqttServerOptionsBuilder WithUndeliveredMessageInterceptor(Action value) - { - _options.UndeliveredMessageInterceptor = new MqttServerApplicationMessageInterceptorDelegate(value); - return this; - } - public MqttServerOptionsBuilder WithDefaultEndpointReuseAddress() { _options.DefaultEndpointOptions.ReuseAddress = true; @@ -249,22 +187,22 @@ namespace MQTTnet.Server return this; } - public MqttServerOptionsBuilder WithPersistentSessions() - { - _options.EnablePersistentSessions = true; - return this; - } - - /// - /// Gets or sets the client ID which is used when publishing messages from the server directly. - /// - public MqttServerOptionsBuilder WithClientId(string value) + public MqttServerOptionsBuilder WithPersistentSessions(bool value = true) { - _options.ClientId = value; + _options.EnablePersistentSessions = value; return this; } - - public IMqttServerOptions Build() + + // /// + // /// Gets or sets the client ID which is used when publishing messages from the server directly. + // /// + // public MqttServerOptionsBuilder WithClientId(string value) + // { + // _options.ClientId = value; + // return this; + // } + + public MqttServerOptions Build() { return _options; } diff --git a/Source/MQTTnet/Server/MqttServerTcpEndpointBaseOptions.cs b/Source/MQTTnet/Server/Options/MqttServerTcpEndpointBaseOptions.cs similarity index 56% rename from Source/MQTTnet/Server/MqttServerTcpEndpointBaseOptions.cs rename to Source/MQTTnet/Server/Options/MqttServerTcpEndpointBaseOptions.cs index 7f305fe..feab55b 100644 --- a/Source/MQTTnet/Server/MqttServerTcpEndpointBaseOptions.cs +++ b/Source/MQTTnet/Server/Options/MqttServerTcpEndpointBaseOptions.cs @@ -1,4 +1,9 @@ -using System.Net; +// 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 { @@ -8,10 +13,12 @@ namespace MQTTnet.Server public int Port { get; set; } - public int ConnectionBacklog { get; set; } = 10; + public int ConnectionBacklog { get; set; } = 100; public bool NoDelay { get; set; } = true; + public LingerOption LingerState { get; set; } = new LingerOption(true, 0); + #if WINDOWS_UWP public int BufferSize { get; set; } = 4096; #endif @@ -21,7 +28,7 @@ namespace MQTTnet.Server public IPAddress BoundInterNetworkV6Address { get; set; } = IPAddress.IPv6Any; /// - /// This requires admin permissions on Linux. + /// This requires admin permissions on Linux. /// public bool ReuseAddress { get; set; } } diff --git a/Source/MQTTnet/Server/Options/MqttServerTcpEndpointOptions.cs b/Source/MQTTnet/Server/Options/MqttServerTcpEndpointOptions.cs new file mode 100644 index 0000000..0437da0 --- /dev/null +++ b/Source/MQTTnet/Server/Options/MqttServerTcpEndpointOptions.cs @@ -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; + } + } +} diff --git a/Source/MQTTnet/Server/Options/MqttServerTlsTcpEndpointOptions.cs b/Source/MQTTnet/Server/Options/MqttServerTlsTcpEndpointOptions.cs new file mode 100644 index 0000000..ddab4ff --- /dev/null +++ b/Source/MQTTnet/Server/Options/MqttServerTlsTcpEndpointOptions.cs @@ -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.Security.Authentication; +using MQTTnet.Certificates; + +namespace MQTTnet.Server +{ + public sealed class MqttServerTlsTcpEndpointOptions : MqttServerTcpEndpointBaseOptions + { + public MqttServerTlsTcpEndpointOptions() + { + Port = 8883; + } + +#if !WINDOWS_UWP + public System.Net.Security.RemoteCertificateValidationCallback RemoteCertificateValidationCallback { get; set; } +#endif + public ICertificateProvider CertificateProvider { get; set; } + + public bool ClientCertificateRequired { get; set; } + + public bool CheckCertificateRevocation { get; set; } + + public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls12; + } +} diff --git a/Source/MQTTnet/Server/PublishResponse.cs b/Source/MQTTnet/Server/PublishResponse.cs new file mode 100644 index 0000000..d6c7c11 --- /dev/null +++ b/Source/MQTTnet/Server/PublishResponse.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Server +{ + public sealed class PublishResponse + { + public MqttPubAckReasonCode ReasonCode { get; set; } = MqttPubAckReasonCode.Success; + + public string ReasonString { get; set; } + + public List UserProperties { get; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/Status/IMqttClientStatus.cs b/Source/MQTTnet/Server/Status/IMqttClientStatus.cs deleted file mode 100644 index 29fb2d9..0000000 --- a/Source/MQTTnet/Server/Status/IMqttClientStatus.cs +++ /dev/null @@ -1,45 +0,0 @@ -using MQTTnet.Formatter; -using System; -using System.Threading.Tasks; - -namespace MQTTnet.Server.Status -{ - public interface IMqttClientStatus - { - /// - /// Gets the client identifier. - /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. - /// - string ClientId { get; } - - string Endpoint { get; } - - MqttProtocolVersion ProtocolVersion { get; } - - DateTime ConnectedTimestamp { get; set; } - - DateTime LastPacketReceivedTimestamp { get; } - - DateTime LastPacketSentTimestamp { get; set; } - - DateTime LastNonKeepAlivePacketReceivedTimestamp { get; } - - long ReceivedApplicationMessagesCount { get; } - - long SentApplicationMessagesCount { get; } - - long ReceivedPacketsCount { get; } - - long SentPacketsCount { get; } - - IMqttSessionStatus Session { get; } - - long BytesSent { get; } - - long BytesReceived { get; } - - Task DisconnectAsync(); - - void ResetStatistics(); - } -} diff --git a/Source/MQTTnet/Server/Status/IMqttSessionStatus.cs b/Source/MQTTnet/Server/Status/IMqttSessionStatus.cs deleted file mode 100644 index 9f1cd2b..0000000 --- a/Source/MQTTnet/Server/Status/IMqttSessionStatus.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace MQTTnet.Server.Status -{ - public interface IMqttSessionStatus - { - /// - /// Gets the client identifier. - /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. - /// - string ClientId { get; } - - /// - /// Gets the count of messages which are not yet sent to the client but already queued. - /// - long PendingApplicationMessagesCount { get; } - - IDictionary Items { get; } - - Task ClearPendingApplicationMessagesAsync(); - - Task DeleteAsync(); - } -} \ No newline at end of file diff --git a/Source/MQTTnet/Server/Status/MqttClientStatus.cs b/Source/MQTTnet/Server/Status/MqttClientStatus.cs index 1770cde..6707c9b 100644 --- a/Source/MQTTnet/Server/Status/MqttClientStatus.cs +++ b/Source/MQTTnet/Server/Status/MqttClientStatus.cs @@ -1,60 +1,63 @@ -using MQTTnet.Formatter; +// 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.Threading.Tasks; -using MQTTnet.Client.Disconnecting; -using MQTTnet.Server.Internal; +using MQTTnet.Formatter; +using MQTTnet.Protocol; -namespace MQTTnet.Server.Status +namespace MQTTnet.Server { - public sealed class MqttClientStatus : IMqttClientStatus + public sealed class MqttClientStatus { - readonly MqttClientConnection _connection; + readonly MqttClient _client; - public MqttClientStatus(MqttClientConnection connection) + public MqttClientStatus(MqttClient client) { - _connection = connection ?? throw new ArgumentNullException(nameof(connection)); + _client = client ?? throw new ArgumentNullException(nameof(client)); } /// /// 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. /// - public string ClientId { get; set; } + public string Id => _client.Id; - public string Endpoint { get; set; } + public string Endpoint => _client.Endpoint; - public MqttProtocolVersion ProtocolVersion { get; set; } + public MqttProtocolVersion ProtocolVersion => _client.ChannelAdapter.PacketFormatterAdapter.ProtocolVersion; - public DateTime ConnectedTimestamp { get; set; } - - public DateTime LastPacketReceivedTimestamp { get; set; } - - public DateTime LastPacketSentTimestamp { get; set; } + public DateTime ConnectedTimestamp => _client.Statistics.ConnectedTimestamp; + + public DateTime LastPacketReceivedTimestamp => _client.Statistics.LastPacketReceivedTimestamp; + + public DateTime LastPacketSentTimestamp => _client.Statistics.LastPacketSentTimestamp; - public DateTime LastNonKeepAlivePacketReceivedTimestamp { get; set; } + public DateTime LastNonKeepAlivePacketReceivedTimestamp => _client.Statistics.LastNonKeepAlivePacketReceivedTimestamp; - public long ReceivedApplicationMessagesCount { get; set; } + public long ReceivedApplicationMessagesCount => _client.Statistics.ReceivedApplicationMessagesCount; - public long SentApplicationMessagesCount { get; set; } + public long SentApplicationMessagesCount => _client.Statistics.SentApplicationMessagesCount; - public long ReceivedPacketsCount { get; set; } + public long ReceivedPacketsCount => _client.Statistics.ReceivedPacketsCount; - public long SentPacketsCount { get; set; } + public long SentPacketsCount => _client.Statistics.SentPacketsCount; - public IMqttSessionStatus Session { get; set; } + public MqttSessionStatus Session { get; set; } - public long BytesSent { get; set; } + public long BytesSent => _client.ChannelAdapter.BytesSent; - public long BytesReceived { get; set; } + public long BytesReceived => _client.ChannelAdapter.BytesReceived; public Task DisconnectAsync() { - return _connection.StopAsync(MqttClientDisconnectReason.NormalDisconnection); + return _client.StopAsync(MqttDisconnectReasonCode.NormalDisconnection); } public void ResetStatistics() { - _connection.ResetStatistics(); + _client.ResetStatistics(); } } } diff --git a/Source/MQTTnet/Server/Status/MqttSessionStatus.cs b/Source/MQTTnet/Server/Status/MqttSessionStatus.cs index e4bd478..7dfd641 100644 --- a/Source/MQTTnet/Server/Status/MqttSessionStatus.cs +++ b/Source/MQTTnet/Server/Status/MqttSessionStatus.cs @@ -1,45 +1,62 @@ -using System; -using System.Collections.Generic; +// 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.Tasks; -using MQTTnet.Server.Internal; +using MQTTnet.Formatter; +using MQTTnet.Implementations; +using MQTTnet.Internal; -namespace MQTTnet.Server.Status +namespace MQTTnet.Server { - public sealed class MqttSessionStatus : IMqttSessionStatus + public sealed class MqttSessionStatus { - readonly MqttClientSession _session; - readonly MqttClientSessionsManager _sessionsManager; + readonly MqttSession _session; - public MqttSessionStatus(MqttClientSession session, MqttClientSessionsManager sessionsManager) + public MqttSessionStatus(MqttSession session) { _session = session ?? throw new ArgumentNullException(nameof(session)); - _sessionsManager = sessionsManager ?? throw new ArgumentNullException(nameof(sessionsManager)); } + + public string Id => _session.Id; + + public long PendingApplicationMessagesCount => _session.PendingDataPacketsCount; - /// - /// 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. - /// - public string ClientId { get; set; } + public DateTime CreatedTimestamp => _session.CreatedTimestamp; - public long PendingApplicationMessagesCount { get; set; } + public IDictionary Items => _session.Items; + + public Task EnqueueApplicationMessageAsync(MqttApplicationMessage applicationMessage) + { + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + + var publishPacketFactory = new MqttPublishPacketFactory(); + _session.EnqueuePacket(new MqttPacketBusItem(publishPacketFactory.Create(applicationMessage))); + + return PlatformAbstractionLayer.CompletedTask; + } - public DateTime CreatedTimestamp { get; set; } + public Task DeliverApplicationMessageAsync(MqttApplicationMessage applicationMessage) + { + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + + var publishPacketFactory = new MqttPublishPacketFactory(); + var packetBusItem = new MqttPacketBusItem(publishPacketFactory.Create(applicationMessage)); + _session.EnqueuePacket(packetBusItem); - /// - /// This items can be used by the library user in order to store custom information. - /// - public IDictionary Items { get; set; } + return packetBusItem.WaitForDeliveryAsync(); + } public Task DeleteAsync() { - return _sessionsManager.DeleteSessionAsync(ClientId); + return _session.DeleteAsync(); } - - public Task ClearPendingApplicationMessagesAsync() + + public Task ClearApplicationMessagesQueueAsync() { - _session.ApplicationMessagesQueue.Clear(); - return Task.FromResult(0); + throw new NotImplementedException(); } } } diff --git a/Source/MQTTnet/Server/SubscribeResponse.cs b/Source/MQTTnet/Server/SubscribeResponse.cs new file mode 100644 index 0000000..5b4959a --- /dev/null +++ b/Source/MQTTnet/Server/SubscribeResponse.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Server +{ + public sealed class SubscribeResponse + { + /// + /// Gets or sets the reason code which is sent to the client. + /// The subscription is skipped when the value is not GrantedQoS_. + /// MQTTv5 only. + /// + public MqttSubscribeReasonCode ReasonCode { get; set; } + + public List UserProperties { get; } = new List(); + + public string ReasonString { get; set; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/Server/UnsubscribeResponse.cs b/Source/MQTTnet/Server/UnsubscribeResponse.cs new file mode 100644 index 0000000..990bde5 --- /dev/null +++ b/Source/MQTTnet/Server/UnsubscribeResponse.cs @@ -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 System.Collections.Generic; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Server +{ + public sealed class UnsubscribeResponse + { + /// + /// Gets or sets the reason code which is sent to the client. + /// MQTTv5 only. + /// + public MqttUnsubscribeReasonCode ReasonCode { get; set; } + + public List UserProperties { get; } = new List(); + + public string ReasonString { get; set; } + } +} \ No newline at end of file diff --git a/Source/MQTTnet/codeSigningKey.pfx b/Source/MQTTnet/codeSigningKey.pfx deleted file mode 100644 index 9374a5dbc11acc2a3a2c507ad3281f755fcbbf45..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1764 zcmZXTcU0477sr1Y{u02E3Rsy6RF6!P5EC4TViG6<5|(8)6bdht4Pn**2~%VhDG)+r zRiF|;Rs(232}_WnBFNAn7@#7v0YUKX&z|>fPfyQ1_qpHuxzByh^T)kd7AS(l5Lgz- z1*NnSO%pfw!BDUw7MO-)fhiD9z_Jic|0p5Kkt{?hgl|D!0LuKYYp*m6Uc`boVOj7x ztTHJ5e+)goABkoX7*ju@%aL$6Y6!`KXGW>@w~Y-`14Vm$%t|J4kDqTZ6P+L4WA}$u z7G|cVzWO=rPM5wfIP-qtyt0lB$q9RZfl;2jH`j5_?H+F9>;~s$TY3gKglNc)7o8NO ze7LbE7vwGTl9VM=d7UKxRL#pd*=>j^2Jg!@OuS4?H+PszXH25Hvpu&B?YF0YC?2ia z+c&>@k2C6vei%8?-AgF9bj+B4I@jH@=B!s{pvnx1wN+0Re(2V7_ge?B>q zdHFIZ!gba3v|lxO;k{Ktous{`Tpu|>`6?-G4*zzsWxE>Vm%$<#P%2#|JnccIM@Gqd zu%^q)oXAITT6lZCy11?0+W2A<2)kju-CK_iCw7+hx$(4#6%wtm4UD_1$L&-yeOC1D zmu3aaXob)(G#136n~%L@2{jtY#{_RJg60lcTq+bM*J^Wp1$|~~>_y@66^1u&x1eXS zRL|PzXw*-N3Vxa7wlGgKJJiPOnwGfS+>U|9#YTA=tch(b{C>^w`GnH-F`6bV>NAP6 z(cu++Wu*^^YW(6UY~b)Kfu>97t4@NW-IbZWvAa&ROV=VtB^)iEv1e!4sc#)eUr1Nv z5v%-6b!#dA#_&$CS7%yR^_&pYgk7;{oG&UcmcgTazem_<<^YeCsRNmfSkStrT?e zS||EX*T5ariA5od^Pu_9SyT4-4idp#zIeEDdB8tyZZL-GpsZyp{I?z5?=WEz_0kH0xwYk_S$89R1TJ>r>J#A#5eptXD0belP2bQGR8iAa&O zHyoOzGlQiC{?4rp>Up{Euzr-RTR=2uUg|4vUU{gha4rG~C3YjItMBMNlp`7{q0 zWZ5(p98}3`bh?R*UvIrI7WnzB8Ykd<@d5MU)`l~za(|88==QO@{Kfm78_oTOp>}l2 zYgtdWF#dj^N|);q&cXzZ{ktJGdv$DwlyQ=;!>1b8+Pv0%5*l!MiRnRK6;yIwM5w=E z!aGh-ZaCyO2JWn-^J7$tVwc2uk2>|M6PFrc_-77iW?)aEi&C&)Nho#UYYz4-_6QaQ z?$O?lfGZ;9Ruv;2rrE~ZwaktU*j$VFvWM4^kCgM2S4msW`lZOXqIU#UV`58>!D@UO G;6DJc77}Ox diff --git a/Tests/MQTTnet.AspNetCore.Tests/MQTTnet.AspNetCore.Tests.csproj b/Tests/MQTTnet.AspNetCore.Tests/MQTTnet.AspNetCore.Tests.csproj deleted file mode 100644 index 24efb9b..0000000 --- a/Tests/MQTTnet.AspNetCore.Tests/MQTTnet.AspNetCore.Tests.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - netcoreapp3.1;net461;net5.0 - false - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Tests/MQTTnet.AspNetCore.Tests/MqttPacketSerializerTests.cs b/Tests/MQTTnet.AspNetCore.Tests/MqttPacketSerializerTests.cs deleted file mode 100644 index 2d389ab..0000000 --- a/Tests/MQTTnet.AspNetCore.Tests/MqttPacketSerializerTests.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Formatter; -using MQTTnet.Tests; - -namespace MQTTnet.AspNetCore.Tests -{ - [TestClass] - public class MqttPacketSerializerTestsWithSpanBasedReader : MqttPacketSerializer_Tests - { - protected override IMqttPacketBodyReader ReaderFactory(byte[] data) - { - var result = new SpanBasedMqttPacketBodyReader(); - result.SetBuffer(data); - return result; - } - - protected override IMqttPacketWriter WriterFactory() - { - return new SpanBasedMqttPacketWriter(); - } - } -} diff --git a/Tests/MQTTnet.AspNetCore.Tests/SpanBasedMqttPacketWriterTests.cs b/Tests/MQTTnet.AspNetCore.Tests/SpanBasedMqttPacketWriterTests.cs deleted file mode 100644 index 8e54887..0000000 --- a/Tests/MQTTnet.AspNetCore.Tests/SpanBasedMqttPacketWriterTests.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Formatter; -using MQTTnet.Tests; - -namespace MQTTnet.AspNetCore.Tests -{ - [TestClass] - public class SpanBasedMqttPacketWriterTests : MqttPacketWriter_Tests - { - protected override IMqttPacketWriter WriterFactory() - { - return new SpanBasedMqttPacketWriter(); - } - } -} diff --git a/Tests/MQTTnet.Benchmarks/App.config b/Tests/MQTTnet.Benchmarks/App.config deleted file mode 100644 index 662f695..0000000 --- a/Tests/MQTTnet.Benchmarks/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Tests/MQTTnet.Benchmarks/Configurations/RuntimeCompareConfig.cs b/Tests/MQTTnet.Benchmarks/Configurations/RuntimeCompareConfig.cs deleted file mode 100644 index d9f5909..0000000 --- a/Tests/MQTTnet.Benchmarks/Configurations/RuntimeCompareConfig.cs +++ /dev/null @@ -1,16 +0,0 @@ -using BenchmarkDotNet.Environments; -using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Toolchains.CsProj; - -namespace MQTTnet.Benchmarks.Configurations -{ - public class RuntimeCompareConfig : BaseConfig - { - public RuntimeCompareConfig() - { - AddJob(Job.Default.WithRuntime(ClrRuntime.Net472)); - AddJob(Job.Default.WithRuntime(CoreRuntime.Core22).WithToolchain(CsProjCoreToolchain.NetCoreApp22)); - } - - } -} diff --git a/Tests/MQTTnet.Benchmarks/MQTTnet.Benchmarks.csproj b/Tests/MQTTnet.Benchmarks/MQTTnet.Benchmarks.csproj deleted file mode 100644 index 7551874..0000000 --- a/Tests/MQTTnet.Benchmarks/MQTTnet.Benchmarks.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - Exe - Full - net461;netcoreapp2.1;net5.0 - netcoreapp2.1 - 7.2 - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Tests/MQTTnet.Benchmarks/TopicFilterComparerBenchmark.cs b/Tests/MQTTnet.Benchmarks/TopicFilterComparerBenchmark.cs deleted file mode 100644 index e6a09aa..0000000 --- a/Tests/MQTTnet.Benchmarks/TopicFilterComparerBenchmark.cs +++ /dev/null @@ -1,90 +0,0 @@ -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Jobs; -using System; -using MQTTnet.Server.Internal; - -namespace MQTTnet.Benchmarks -{ - [SimpleJob(RuntimeMoniker.Net461)] - [RPlotExporter] - [MemoryDiagnoser] - public class TopicFilterComparerBenchmark - { - static readonly char[] TopicLevelSeparator = { '/' }; - - [GlobalSetup] - public void Setup() - { - } - - [Benchmark] - public void MqttTopicFilterComparer_10000_StringSplitMethod() - { - for (var i = 0; i < 10000; i++) - { - LegacyMethodByStringSplit("sport/tennis/player1", "sport/#"); - LegacyMethodByStringSplit("sport/tennis/player1/ranking", "sport/#/ranking"); - LegacyMethodByStringSplit("sport/tennis/player1/score/wimbledon", "sport/+/player1/#"); - LegacyMethodByStringSplit("sport/tennis/player1", "sport/tennis/+"); - LegacyMethodByStringSplit("/finance", "+/+"); - LegacyMethodByStringSplit("/finance", "/+"); - LegacyMethodByStringSplit("/finance", "+"); - } - } - - [Benchmark] - public void MqttTopicFilterComparer_10000_LoopMethod() - { - for (var i = 0; i < 10000; i++) - { - MqttTopicFilterComparer.IsMatch("sport/tennis/player1", "sport/#"); - MqttTopicFilterComparer.IsMatch("sport/tennis/player1/ranking", "sport/#/ranking"); - MqttTopicFilterComparer.IsMatch("sport/tennis/player1/score/wimbledon", "sport/+/player1/#"); - MqttTopicFilterComparer.IsMatch("sport/tennis/player1", "sport/tennis/+"); - MqttTopicFilterComparer.IsMatch("/finance", "+/+"); - MqttTopicFilterComparer.IsMatch("/finance", "/+"); - MqttTopicFilterComparer.IsMatch("/finance", "+"); - } - } - - static bool LegacyMethodByStringSplit(string topic, string filter) - { - if (topic == null) throw new ArgumentNullException(nameof(topic)); - if (filter == null) throw new ArgumentNullException(nameof(filter)); - - if (string.Equals(topic, filter, StringComparison.Ordinal)) - { - return true; - } - - var fragmentsTopic = topic.Split(TopicLevelSeparator, StringSplitOptions.None); - var fragmentsFilter = filter.Split(TopicLevelSeparator, StringSplitOptions.None); - - // # > In either case it MUST be the last character specified in the Topic Filter [MQTT-4.7.1-2]. - for (var i = 0; i < fragmentsFilter.Length; i++) - { - if (fragmentsFilter[i] == "+") - { - continue; - } - - if (fragmentsFilter[i] == "#") - { - return true; - } - - if (i >= fragmentsTopic.Length) - { - return false; - } - - if (!string.Equals(fragmentsFilter[i], fragmentsTopic[i], StringComparison.Ordinal)) - { - return false; - } - } - - return fragmentsTopic.Length == fragmentsFilter.Length; - } - } -} diff --git a/Tests/MQTTnet.Core.Tests/Client/LowLevelMqttClient_Tests.cs b/Tests/MQTTnet.Core.Tests/Client/LowLevelMqttClient_Tests.cs deleted file mode 100644 index 814adca..0000000 --- a/Tests/MQTTnet.Core.Tests/Client/LowLevelMqttClient_Tests.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Client.Options; -using MQTTnet.Exceptions; -using MQTTnet.LowLevelClient; -using MQTTnet.Packets; -using MQTTnet.Protocol; -using MQTTnet.Tests.Mockups; - -namespace MQTTnet.Tests.Client -{ - [TestClass] - public class LowLevelMqttClient_Tests - { - public TestContext TestContext { get; set; } - - [TestMethod] - [ExpectedException(typeof(MqttCommunicationException))] - public async Task Connect_To_Not_Existing_Server() - { - var client = new MqttFactory().CreateLowLevelMqttClient(); - var options = new MqttClientOptionsBuilder() - .WithTcpServer("localhost") - .Build(); - - await client.ConnectAsync(options, CancellationToken.None).ConfigureAwait(false); - } - - [TestMethod] - public async Task Connect_And_Disconnect() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer(); - - var factory = new MqttFactory(); - var lowLevelClient = factory.CreateLowLevelMqttClient(); - - await lowLevelClient.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build(), CancellationToken.None); - - await lowLevelClient.DisconnectAsync(CancellationToken.None); - } - } - - [TestMethod] - public async Task Authenticate() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer(); - - var factory = new MqttFactory(); - var lowLevelClient = factory.CreateLowLevelMqttClient(); - - await lowLevelClient.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build(), CancellationToken.None); - - var receivedPacket = await Authenticate(lowLevelClient).ConfigureAwait(false); - - await lowLevelClient.DisconnectAsync(CancellationToken.None).ConfigureAwait(false); - - Assert.IsNotNull(receivedPacket); - Assert.AreEqual(MqttConnectReturnCode.ConnectionAccepted, receivedPacket.ReturnCode); - } - } - - [TestMethod] - public async Task Subscribe() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - await testEnvironment.StartServer(); - - var factory = new MqttFactory(); - var lowLevelClient = factory.CreateLowLevelMqttClient(); - - await lowLevelClient.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build(), CancellationToken.None); - - await Authenticate(lowLevelClient).ConfigureAwait(false); - - var receivedPacket = await Subscribe(lowLevelClient, "a").ConfigureAwait(false); - - await lowLevelClient.DisconnectAsync(CancellationToken.None).ConfigureAwait(false); - - Assert.IsNotNull(receivedPacket); - Assert.AreEqual(MqttSubscribeReturnCode.SuccessMaximumQoS0, receivedPacket.ReturnCodes[0]); - } - } - - [TestMethod] - public async Task Loose_Connection() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - testEnvironment.ServerPort = 8364; - var server = await testEnvironment.StartServer(); - var client = await testEnvironment.ConnectLowLevelClient(o => o.WithCommunicationTimeout(TimeSpan.Zero)); - - await Authenticate(client).ConfigureAwait(false); - - await server.StopAsync(); - - await Task.Delay(1000); - - try - { - await client.SendAsync(MqttPingReqPacket.Instance, CancellationToken.None).ConfigureAwait(false); - await client.SendAsync(MqttPingReqPacket.Instance, CancellationToken.None).ConfigureAwait(false); - } - catch (MqttCommunicationException exception) - { - Assert.IsTrue(exception.InnerException is SocketException); - return; - } - catch - { - Assert.Fail("Wrong exception type thrown."); - } - - Assert.Fail("This MUST fail"); - } - } - - async Task Authenticate(ILowLevelMqttClient client) - { - await client.SendAsync(new MqttConnectPacket - { - CleanSession = true, - ClientId = TestContext.TestName, - Username = "user", - Password = Encoding.UTF8.GetBytes("pass") - }, - CancellationToken.None).ConfigureAwait(false); - - return await client.ReceiveAsync(CancellationToken.None).ConfigureAwait(false) as MqttConnAckPacket; - } - - async Task Subscribe(ILowLevelMqttClient client, string topic) - { - await client.SendAsync(new MqttSubscribePacket - { - PacketIdentifier = 1, - TopicFilters = new List - { - new MqttTopicFilter - { - Topic = topic - } - } - }, - CancellationToken.None).ConfigureAwait(false); - - return await client.ReceiveAsync(CancellationToken.None).ConfigureAwait(false) as MqttSubAckPacket; - } - } -} diff --git a/Tests/MQTTnet.Core.Tests/Extensions/MqttPacketWriterExtensions.cs b/Tests/MQTTnet.Core.Tests/Extensions/MqttPacketWriterExtensions.cs deleted file mode 100644 index 1e0473b..0000000 --- a/Tests/MQTTnet.Core.Tests/Extensions/MqttPacketWriterExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using MQTTnet.Formatter; -using MQTTnet.Protocol; - -namespace MQTTnet.Tests.Extensions -{ - public static class MqttPacketWriterExtensions - { - public static byte[] AddMqttHeader(this IMqttPacketWriter writer, MqttControlPacketType header, byte[] body) - { - writer.Write(MqttPacketWriter.BuildFixedHeader(header)); - writer.WriteVariableLengthInteger((uint)body.Length); - writer.Write(body, 0, body.Length); - return writer.GetBuffer(); - } - } -} diff --git a/Tests/MQTTnet.Core.Tests/MQTTnet.Tests.csproj b/Tests/MQTTnet.Core.Tests/MQTTnet.Tests.csproj deleted file mode 100644 index 63244d6..0000000 --- a/Tests/MQTTnet.Core.Tests/MQTTnet.Tests.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - netcoreapp3.1;net461;net5.0 - false - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/Mockups/MqttPacketAsserts.cs b/Tests/MQTTnet.Core.Tests/Mockups/MqttPacketAsserts.cs deleted file mode 100644 index 4fbbdc0..0000000 --- a/Tests/MQTTnet.Core.Tests/Mockups/MqttPacketAsserts.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Packets; - -namespace MQTTnet.Tests.Mockups -{ - public sealed class MqttPacketAsserts - { - public void AssertIsConnectPacket(MqttBasePacket packet) - { - Assert.AreEqual(packet.GetType(), typeof(MqttConnectPacket)); - } - - public void AssertIsConnAckPacket(MqttBasePacket packet) - { - Assert.AreEqual(packet.GetType(), typeof(MqttConnAckPacket)); - } - } -} \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/Mockups/TestApplicationMessageReceivedHandler.cs b/Tests/MQTTnet.Core.Tests/Mockups/TestApplicationMessageReceivedHandler.cs deleted file mode 100644 index eda3cc3..0000000 --- a/Tests/MQTTnet.Core.Tests/Mockups/TestApplicationMessageReceivedHandler.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Client.Receiving; -using MQTTnet.Implementations; - -namespace MQTTnet.Tests.Mockups -{ - public sealed class TestApplicationMessageReceivedHandler : IMqttApplicationMessageReceivedHandler - { - readonly List _messageReceivedEventArgs = new List(); - - public Task HandleApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs) - { - lock (_messageReceivedEventArgs) - { - _messageReceivedEventArgs.Add(eventArgs.ApplicationMessage); - } - - return PlatformAbstractionLayer.CompletedTask; - } - - public List ReceivedApplicationMessages - { - get - { - lock (_messageReceivedEventArgs) - { - return _messageReceivedEventArgs.ToList(); - } - } - } - - public void AssertReceivedCountEquals(int expectedCount) - { - lock (_messageReceivedEventArgs) - { - Assert.AreEqual(expectedCount, _messageReceivedEventArgs.Count); - } - } - } -} \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/Mockups/TestClientWrapper.cs b/Tests/MQTTnet.Core.Tests/Mockups/TestClientWrapper.cs deleted file mode 100644 index bbe4a2f..0000000 --- a/Tests/MQTTnet.Core.Tests/Mockups/TestClientWrapper.cs +++ /dev/null @@ -1,101 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Client; -using MQTTnet.Client.Connecting; -using MQTTnet.Client.Disconnecting; -using MQTTnet.Client.ExtendedAuthenticationExchange; -using MQTTnet.Client.Options; -using MQTTnet.Client.Publishing; -using MQTTnet.Client.Receiving; -using MQTTnet.Client.Subscribing; -using MQTTnet.Client.Unsubscribing; -using System.Threading; -using System.Threading.Tasks; - -namespace MQTTnet.Tests.Mockups -{ - public sealed class TestClientWrapper : IMqttClient - { - public TestClientWrapper(IMqttClient implementation, TestContext testContext) - { - Implementation = implementation; - TestContext = testContext; - } - - public IMqttClient Implementation { get; } - - public TestContext TestContext { get; } - - public bool IsConnected => Implementation.IsConnected; - - public IMqttClientOptions Options => Implementation.Options; - - public IMqttClientConnectedHandler ConnectedHandler - { - get => Implementation.ConnectedHandler; - set => Implementation.ConnectedHandler = value; - } - - public IMqttClientDisconnectedHandler DisconnectedHandler - { - get => Implementation.DisconnectedHandler; - set => Implementation.DisconnectedHandler = value; - } - - public IMqttApplicationMessageReceivedHandler ApplicationMessageReceivedHandler - { - get => Implementation.ApplicationMessageReceivedHandler; - set => Implementation.ApplicationMessageReceivedHandler = value; - } - - public Task ConnectAsync(IMqttClientOptions options, CancellationToken cancellationToken) - { - if (TestContext != null) - { - var clientOptions = (MqttClientOptions)options; - - var existingClientId = clientOptions.ClientId; - if (existingClientId != null && !existingClientId.StartsWith(TestContext.TestName)) - { - clientOptions.ClientId = TestContext.TestName + existingClientId; - } - } - - return Implementation.ConnectAsync(options, cancellationToken); - } - - public Task DisconnectAsync(MqttClientDisconnectOptions options, CancellationToken cancellationToken) - { - return Implementation.DisconnectAsync(options, cancellationToken); - } - - public void Dispose() - { - Implementation.Dispose(); - } - - public Task PingAsync(CancellationToken cancellationToken) - { - return Implementation.PingAsync(cancellationToken); - } - - public Task PublishAsync(MqttApplicationMessage applicationMessage, CancellationToken cancellationToken) - { - return Implementation.PublishAsync(applicationMessage, cancellationToken); - } - - public Task SendExtendedAuthenticationExchangeDataAsync(MqttExtendedAuthenticationExchangeData data, CancellationToken cancellationToken) - { - return Implementation.SendExtendedAuthenticationExchangeDataAsync(data, cancellationToken); - } - - public Task SubscribeAsync(MqttClientSubscribeOptions options, CancellationToken cancellationToken) - { - return Implementation.SubscribeAsync(options, cancellationToken); - } - - public Task UnsubscribeAsync(MqttClientUnsubscribeOptions options, CancellationToken cancellationToken) - { - return Implementation.UnsubscribeAsync(options, cancellationToken); - } - } -} \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/Mockups/TestEnvironment.cs b/Tests/MQTTnet.Core.Tests/Mockups/TestEnvironment.cs deleted file mode 100644 index 3d00abc..0000000 --- a/Tests/MQTTnet.Core.Tests/Mockups/TestEnvironment.cs +++ /dev/null @@ -1,305 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Client; -using MQTTnet.Client.Options; -using MQTTnet.Server; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MQTTnet.Diagnostics.Logger; -using MQTTnet.Extensions.Rpc; -using MQTTnet.Extensions.Rpc.Options; -using MQTTnet.Formatter; -using MQTTnet.LowLevelClient; - -namespace MQTTnet.Tests.Mockups -{ - public sealed class TestEnvironment : IDisposable - { - readonly MqttProtocolVersion _protocolVersion; - readonly List _lowLevelClients = new List(); - readonly List _clients = new List(); - readonly List _serverErrors = new List(); - readonly List _clientErrors = new List(); - - readonly List _exceptions = new List(); - - public MqttFactory Factory { get; } = new MqttFactory(); - - public IMqttServer Server { get; private set; } - - public bool IgnoreClientLogErrors { get; set; } - - public bool IgnoreServerLogErrors { get; set; } - - public int ServerPort { get; set; } = 1888; - - public MqttNetEventLogger ServerLogger { get; } = new MqttNetEventLogger("server"); - - public MqttNetEventLogger ClientLogger { get; } = new MqttNetEventLogger("client"); - - public TestContext TestContext { get; } - - public TestEnvironment() : this(null) - { - } - - public TestEnvironment(TestContext testContext, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V311) - { - _protocolVersion = protocolVersion; - TestContext = testContext; - - ServerLogger.LogMessagePublished += (s, e) => - { - if (Debugger.IsAttached) - { - Debug.WriteLine(e.LogMessage.ToString()); - } - - if (e.LogMessage.Level == MqttNetLogLevel.Error) - { - lock (_serverErrors) - { - _serverErrors.Add(e.LogMessage.ToString()); - } - } - }; - - ClientLogger.LogMessagePublished += (s, e) => - { - if (Debugger.IsAttached) - { - Debug.WriteLine(e.LogMessage.ToString()); - } - - if (e.LogMessage.Level == MqttNetLogLevel.Error) - { - lock (_clientErrors) - { - _clientErrors.Add(e.LogMessage.ToString()); - } - } - }; - } - - public IMqttClient CreateClient() - { - lock (_clients) - { - var client = Factory.CreateMqttClient(ClientLogger); - _clients.Add(client); - - return new TestClientWrapper(client, TestContext); - } - } - - public Task ConnectClient() - { - return ConnectClient(Factory.CreateClientOptionsBuilder().WithProtocolVersion(_protocolVersion)); - } - - public async Task ConnectClient(Action optionsBuilder) - { - if (optionsBuilder == null) throw new ArgumentNullException(nameof(optionsBuilder)); - - var options = Factory.CreateClientOptionsBuilder() - .WithProtocolVersion(_protocolVersion) - .WithTcpServer("127.0.0.1", ServerPort); - - optionsBuilder.Invoke(options); - - var client = CreateClient(); - await client.ConnectAsync(options.Build()).ConfigureAwait(false); - - return client; - } - - public async Task ConnectClient(MqttClientOptionsBuilder options) - { - if (options == null) throw new ArgumentNullException(nameof(options)); - - options = options.WithTcpServer("127.0.0.1", ServerPort); - - var client = CreateClient(); - await client.ConnectAsync(options.Build()).ConfigureAwait(false); - - return client; - } - - public async Task ConnectClient(IMqttClientOptions options) - { - if (options == null) throw new ArgumentNullException(nameof(options)); - - var client = CreateClient(); - await client.ConnectAsync(options).ConfigureAwait(false); - - return client; - } - - public ILowLevelMqttClient CreateLowLevelClient() - { - lock (_clients) - { - var client = Factory.CreateLowLevelMqttClient(ClientLogger); - _lowLevelClients.Add(client); - - return client; - } - } - - public Task ConnectLowLevelClient() - { - return ConnectLowLevelClient(o => { }); - } - - public async Task ConnectLowLevelClient(Action optionsBuilder) - { - if (optionsBuilder == null) throw new ArgumentNullException(nameof(optionsBuilder)); - - var options = new MqttClientOptionsBuilder(); - options = options.WithTcpServer("127.0.0.1", ServerPort); - optionsBuilder.Invoke(options); - - var client = CreateLowLevelClient(); - await client.ConnectAsync(options.Build(), CancellationToken.None).ConfigureAwait(false); - - return client; - } - - public async Task ConnectRpcClient(IMqttRpcClientOptions options) - { - return new MqttRpcClient(await ConnectClient(), options); - } - - public IMqttServer CreateServer() - { - if (Server != null) - { - throw new InvalidOperationException("Server already started."); - } - - Server = new TestServerWrapper(Factory.CreateMqttServer(ServerLogger), TestContext, this); - return Server; - } - - public Task StartServer() - { - return StartServer(Factory.CreateServerOptionsBuilder()); - } - - public async Task StartServer(MqttServerOptionsBuilder options) - { - var server = CreateServer(); - - options.WithDefaultEndpointPort(ServerPort); - options.WithMaxPendingMessagesPerClient(int.MaxValue); - - await server.StartAsync(options.Build()); - - return server; - } - - public async Task StartServer(Action options) - { - var server = CreateServer(); - - var optionsBuilder = Factory.CreateServerOptionsBuilder(); - optionsBuilder.WithDefaultEndpointPort(ServerPort); - optionsBuilder.WithMaxPendingMessagesPerClient(int.MaxValue); - - options?.Invoke(optionsBuilder); - - await server.StartAsync(optionsBuilder.Build()); - - return server; - } - - public TestApplicationMessageReceivedHandler CreateApplicationMessageHandler(IMqttClient mqttClient) - { - if (mqttClient == null) throw new ArgumentNullException(nameof(mqttClient)); - - var handler = new TestApplicationMessageReceivedHandler(); - if (mqttClient.ApplicationMessageReceivedHandler != null) - { - throw new InvalidOperationException("ApplicationMessageReceivedHandler is already set."); - } - - mqttClient.ApplicationMessageReceivedHandler = handler; - - return handler; - } - - public void ThrowIfLogErrors() - { - lock (_serverErrors) - { - if (!IgnoreServerLogErrors && _serverErrors.Count > 0) - { - throw new Exception($"Server had {_serverErrors.Count} errors (${string.Join(Environment.NewLine, _serverErrors)})."); - } - } - - lock (_clientErrors) - { - if (!IgnoreClientLogErrors && _clientErrors.Count > 0) - { - throw new Exception($"Client(s) had {_clientErrors.Count} errors (${string.Join(Environment.NewLine, _clientErrors)})."); - } - } - } - - public void TrackException(Exception exception) - { - lock (_exceptions) - { - _exceptions.Add(exception); - } - } - - public void Dispose() - { - foreach (var mqttClient in _clients) - { - try - { - mqttClient.DisconnectAsync().GetAwaiter().GetResult(); - } - catch - { - // This can happen when the test already disconnected the client. - } - finally - { - mqttClient?.Dispose(); - } - } - - foreach (var lowLevelMqttClient in _lowLevelClients) - { - lowLevelMqttClient.Dispose(); - } - - try - { - Server?.StopAsync().GetAwaiter().GetResult(); - } - catch - { - // This can happen when the test already stopped the server. - } - finally - { - Server?.Dispose(); - } - - ThrowIfLogErrors(); - - if (_exceptions.Any()) - { - throw new Exception($"{_exceptions.Count} exceptions tracked.\r\n" + string.Join(Environment.NewLine, _exceptions)); - } - } - } -} \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/Mockups/TestMqttCommunicationAdapter.cs b/Tests/MQTTnet.Core.Tests/Mockups/TestMqttCommunicationAdapter.cs deleted file mode 100644 index 2a5be2e..0000000 --- a/Tests/MQTTnet.Core.Tests/Mockups/TestMqttCommunicationAdapter.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Security.Cryptography.X509Certificates; -using System.Threading; -using System.Threading.Tasks; -using MQTTnet.Adapter; -using MQTTnet.Formatter; -using MQTTnet.Packets; - -namespace MQTTnet.Tests.Mockups -{ - public class TestMqttCommunicationAdapter : IMqttChannelAdapter - { - readonly BlockingCollection _incomingPackets = new BlockingCollection(); - - public TestMqttCommunicationAdapter Partner { get; set; } - - public string Endpoint { get; } = string.Empty; - - public bool IsSecureConnection { get; } = false; - - public X509Certificate2 ClientCertificate { get; } - - public MqttPacketFormatterAdapter PacketFormatterAdapter { get; } = new MqttPacketFormatterAdapter(MqttProtocolVersion.V311); - - public long BytesSent { get; } - public long BytesReceived { get; } - - public bool IsReadingPacket { get; } - - public void Dispose() - { - } - - public Task ConnectAsync(TimeSpan timeout, CancellationToken cancellationToken) - { - return Task.FromResult(0); - } - - public Task DisconnectAsync(TimeSpan timeout, CancellationToken cancellationToken) - { - return Task.FromResult(0); - } - - public Task SendPacketAsync(MqttBasePacket packet, CancellationToken cancellationToken) - { - ThrowIfPartnerIsNull(); - - Partner.EnqueuePacketInternal(packet); - - return Task.FromResult(0); - } - - public Task ReceivePacketAsync(CancellationToken cancellationToken) - { - ThrowIfPartnerIsNull(); - - return Task.Run(() => - { - try - { - return _incomingPackets.Take(cancellationToken); - } - catch - { - return null; - } - }, cancellationToken); - } - - public void ResetStatistics() - { - } - - private void EnqueuePacketInternal(MqttBasePacket packet) - { - if (packet == null) throw new ArgumentNullException(nameof(packet)); - - _incomingPackets.Add(packet); - } - - private void ThrowIfPartnerIsNull() - { - if (Partner == null) - { - throw new InvalidOperationException("Partner is not set."); - } - } - } -} diff --git a/Tests/MQTTnet.Core.Tests/Mockups/TestMqttCommunicationAdapterFactory.cs b/Tests/MQTTnet.Core.Tests/Mockups/TestMqttCommunicationAdapterFactory.cs deleted file mode 100644 index 371090c..0000000 --- a/Tests/MQTTnet.Core.Tests/Mockups/TestMqttCommunicationAdapterFactory.cs +++ /dev/null @@ -1,20 +0,0 @@ -using MQTTnet.Adapter; -using MQTTnet.Client.Options; - -namespace MQTTnet.Tests.Mockups -{ - public class TestMqttCommunicationAdapterFactory : IMqttClientAdapterFactory - { - readonly IMqttChannelAdapter _adapter; - - public TestMqttCommunicationAdapterFactory(IMqttChannelAdapter adapter) - { - _adapter = adapter; - } - - public IMqttChannelAdapter CreateClientAdapter(IMqttClientOptions options) - { - return _adapter; - } - } -} diff --git a/Tests/MQTTnet.Core.Tests/Mockups/TestServerStorage.cs b/Tests/MQTTnet.Core.Tests/Mockups/TestServerStorage.cs deleted file mode 100644 index 55ca6bb..0000000 --- a/Tests/MQTTnet.Core.Tests/Mockups/TestServerStorage.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using MQTTnet.Server; - -namespace MQTTnet.Tests.Mockups -{ - public class TestServerStorage : IMqttServerStorage - { - public IList Messages = new List(); - - public Task SaveRetainedMessagesAsync(IList messages) - { - Messages = messages; - return Task.CompletedTask; - } - - public Task> LoadRetainedMessagesAsync() - { - return Task.FromResult(Messages); - } - } -} diff --git a/Tests/MQTTnet.Core.Tests/Mockups/TestServerWrapper.cs b/Tests/MQTTnet.Core.Tests/Mockups/TestServerWrapper.cs deleted file mode 100644 index 50168a5..0000000 --- a/Tests/MQTTnet.Core.Tests/Mockups/TestServerWrapper.cs +++ /dev/null @@ -1,141 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Client.Publishing; -using MQTTnet.Client.Receiving; -using MQTTnet.Server; -using MQTTnet.Server.Status; -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace MQTTnet.Tests.Mockups -{ - public sealed class TestServerWrapper : IMqttServer - { - public TestServerWrapper(IMqttServer implementation, TestContext testContext, TestEnvironment testEnvironment) - { - Implementation = implementation; - TestContext = testContext; - TestEnvironment = testEnvironment; - } - - public IMqttServer Implementation { get; } - public TestContext TestContext { get; } - public TestEnvironment TestEnvironment { get; } - - public bool IsStarted { get; } - - public IMqttServerStartedHandler StartedHandler - { - get => Implementation.StartedHandler; - set => Implementation.StartedHandler = value; - } - - public IMqttServerStoppedHandler StoppedHandler - { - get => Implementation.StoppedHandler; - set => Implementation.StoppedHandler = value; - } - - public IMqttServerClientConnectedHandler ClientConnectedHandler - { - get => Implementation.ClientConnectedHandler; - set => Implementation.ClientConnectedHandler = value; - } - - public IMqttServerClientDisconnectedHandler ClientDisconnectedHandler - { - get => Implementation.ClientDisconnectedHandler; - set => Implementation.ClientDisconnectedHandler = value; - } - - public IMqttServerClientSubscribedTopicHandler ClientSubscribedTopicHandler - { - get => Implementation.ClientSubscribedTopicHandler; - set => Implementation.ClientSubscribedTopicHandler = value; - } - - public IMqttServerClientUnsubscribedTopicHandler ClientUnsubscribedTopicHandler - { - get => Implementation.ClientUnsubscribedTopicHandler; - set => Implementation.ClientUnsubscribedTopicHandler = value; - } - - public IMqttServerOptions Options => Implementation.Options; - - public IMqttApplicationMessageReceivedHandler ApplicationMessageReceivedHandler - { - get => Implementation.ApplicationMessageReceivedHandler; - set => Implementation.ApplicationMessageReceivedHandler = value; - } - - public Task ClearRetainedApplicationMessagesAsync() - { - return Implementation.ClearRetainedApplicationMessagesAsync(); - } - - public Task> GetClientStatusAsync() - { - return Implementation.GetClientStatusAsync(); - } - - public Task> GetRetainedApplicationMessagesAsync() - { - return Implementation.GetRetainedApplicationMessagesAsync(); - } - - public Task> GetSessionStatusAsync() - { - return Implementation.GetSessionStatusAsync(); - } - - public Task PublishAsync(MqttApplicationMessage applicationMessage, CancellationToken cancellationToken) - { - return Implementation.PublishAsync(applicationMessage, cancellationToken); - } - - public Task StartAsync(IMqttServerOptions options) - { - if (TestContext != null) - { - var serverOptions = (MqttServerOptions)options; - - if (serverOptions.ConnectionValidator == null) - { - serverOptions.ConnectionValidator = new MqttServerConnectionValidatorDelegate(ConnectionValidator); - } - } - - return Implementation.StartAsync(options); - } - - public Task StopAsync() - { - return Implementation.StopAsync(); - } - - public Task SubscribeAsync(string clientId, ICollection topicFilters) - { - return Implementation.SubscribeAsync(clientId, topicFilters); - } - - public Task UnsubscribeAsync(string clientId, ICollection topicFilters) - { - return Implementation.UnsubscribeAsync(clientId, topicFilters); - } - - public void Dispose() - { - Implementation.Dispose(); - } - - void ConnectionValidator(MqttConnectionValidatorContext ctx) - { - if (!ctx.ClientId.StartsWith(TestContext.TestName)) - { - TestEnvironment.TrackException(new InvalidOperationException($"Invalid client ID used ({ctx.ClientId}). It must start with UnitTest name.")); - ctx.ReasonCode = Protocol.MqttConnectReasonCode.ClientIdentifierNotValid; - } - } - } -} \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/MqttApplicationMessage_Tests.cs b/Tests/MQTTnet.Core.Tests/MqttApplicationMessage_Tests.cs deleted file mode 100644 index 05d0907..0000000 --- a/Tests/MQTTnet.Core.Tests/MqttApplicationMessage_Tests.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Extensions; -using MQTTnet.Packets; -using System.Collections.Generic; - -namespace MQTTnet.Tests -{ - [TestClass] - public class MqttApplicationMessage_Tests - { - [TestMethod] - public void GetUserProperty_Test() - { - var message = new MqttApplicationMessage - { - UserProperties = new List - { - new MqttUserProperty("foo", "bar"), - new MqttUserProperty("value", "1011"), - new MqttUserProperty("CASE", "insensitive") - } - }; - - Assert.AreEqual("bar", message.GetUserProperty("foo")); - //Assert.AreEqual(1011, message.GetUserProperty("value")); - Assert.AreEqual(null, message.GetUserProperty("case")); - Assert.AreEqual(null, message.GetUserProperty("nonExists")); - //Assert.AreEqual(null, message.GetUserProperty("nonExists")); - //Assert.ThrowsException(() => message.GetUserProperty("nonExists")); - } - } -} diff --git a/Tests/MQTTnet.Core.Tests/MqttPacketReader_Tests.cs b/Tests/MQTTnet.Core.Tests/MqttPacketReader_Tests.cs deleted file mode 100644 index c160f4d..0000000 --- a/Tests/MQTTnet.Core.Tests/MqttPacketReader_Tests.cs +++ /dev/null @@ -1,24 +0,0 @@ -//using System.IO; -//using System.Threading; -//using System.Threading.Tasks; -//using Microsoft.VisualStudio.TestTools.UnitTesting; -//using MQTTnet.Formatter; -//using MQTTnet.Internal; - -//namespace MQTTnet.Tests -//{ -// [TestClass] -// public class MqttPacketReader_Tests -// { -// [TestMethod] -// public async Task MqttPacketReader_EmptyStream() -// { -// var fixedHeader = new byte[2]; -// var reader = new MqttPacketReader(new TestMqttChannel(new MemoryStream())); -// var readResult = await reader.ReadFixedHeaderAsync(fixedHeader, CancellationToken.None); - -// Assert.IsTrue(readResult.ConnectionClosed); -// } -// } -//} -// TODO: Fix diff --git a/Tests/MQTTnet.Core.Tests/MqttPacketWriter_Tests.cs b/Tests/MQTTnet.Core.Tests/MqttPacketWriter_Tests.cs deleted file mode 100644 index ad08d52..0000000 --- a/Tests/MQTTnet.Core.Tests/MqttPacketWriter_Tests.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Formatter; - -namespace MQTTnet.Tests -{ - [TestClass] - public class MqttPacketWriter_Tests - { - protected virtual IMqttPacketWriter WriterFactory() - { - return new MqttPacketWriter(); - } - - [TestMethod] - public void WritePacket() - { - var writer = WriterFactory(); - Assert.AreEqual(0, writer.Length); - - writer.WriteWithLengthPrefix("1234567890"); - Assert.AreEqual(10 + 2, writer.Length); - - writer.WriteWithLengthPrefix(new byte[300]); - Assert.AreEqual(300 + 2 + 12, writer.Length); - - writer.WriteWithLengthPrefix(new byte[5000]); - Assert.AreEqual(5000 + 2 + 300 + 2 + 12, writer.Length); - } - } -} diff --git a/Tests/MQTTnet.Core.Tests/Protocol_Tests.cs b/Tests/MQTTnet.Core.Tests/Protocol_Tests.cs deleted file mode 100644 index ef9e945..0000000 --- a/Tests/MQTTnet.Core.Tests/Protocol_Tests.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Formatter; - -namespace MQTTnet.Tests -{ - [TestClass] - public class Protocol_Tests - { - [TestMethod] - public void Encode_Four_Byte_Integer() - { - for (uint i = 0; i < 268435455; i++) - { - var writer = new MqttPacketWriter(); - writer.WriteVariableLengthInteger(i); - var buffer = writer.GetBuffer(); - - var reader = new MqttPacketBodyReader(buffer, 0, writer.Length); - var checkValue = reader.ReadVariableLengthInteger(); - - Assert.AreEqual(i, checkValue); - } - } - } -} diff --git a/Tests/MQTTnet.Core.Tests/Server/MqttSubscriptionsManager_Tests.cs b/Tests/MQTTnet.Core.Tests/Server/MqttSubscriptionsManager_Tests.cs deleted file mode 100644 index 2a6f58f..0000000 --- a/Tests/MQTTnet.Core.Tests/Server/MqttSubscriptionsManager_Tests.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System.Collections.Concurrent; -using System.Threading.Tasks; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Packets; -using MQTTnet.Protocol; -using MQTTnet.Server; -using MQTTnet.Server.Internal; -using MQTTnet.Tests.Mockups; - -namespace MQTTnet.Tests.Server -{ - [TestClass] - public class MqttSubscriptionsManager_Tests - { - [TestMethod] - public async Task MqttSubscriptionsManager_SubscribeSingleSuccess() - { - var s = CreateSession(); - - var sm = new MqttClientSubscriptionsManager(s, new MqttServerOptions(), new MqttServerEventDispatcher(new TestLogger()), new MqttRetainedMessagesManager()); - - var sp = new MqttSubscribePacket(); - sp.TopicFilters.Add(new MqttTopicFilterBuilder().WithTopic("A/B/C").Build()); - - await sm.Subscribe(sp); - - var result = sm.CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.AtMostOnce, ""); - Assert.IsTrue(result.IsSubscribed); - Assert.AreEqual(result.QualityOfServiceLevel, MqttQualityOfServiceLevel.AtMostOnce); - } - - [TestMethod] - public async Task MqttSubscriptionsManager_SubscribeDifferentQoSSuccess() - { - var s = CreateSession(); - - var sm = new MqttClientSubscriptionsManager(s, new MqttServerOptions(), new MqttServerEventDispatcher(new TestLogger()), new MqttRetainedMessagesManager()); - - var sp = new MqttSubscribePacket(); - sp.TopicFilters.Add(new MqttTopicFilter { Topic = "A/B/C", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }); - - await sm.Subscribe(sp); - - var result = sm.CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.ExactlyOnce, ""); - Assert.IsTrue(result.IsSubscribed); - Assert.AreEqual(result.QualityOfServiceLevel, MqttQualityOfServiceLevel.AtMostOnce); - } - - [TestMethod] - public async Task MqttSubscriptionsManager_SubscribeTwoTimesSuccess() - { - var s = CreateSession(); - - var sm = new MqttClientSubscriptionsManager(s, new MqttServerOptions(), new MqttServerEventDispatcher(new TestLogger()), new MqttRetainedMessagesManager()); - - var sp = new MqttSubscribePacket(); - sp.TopicFilters.Add(new MqttTopicFilter { Topic = "#", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce }); - sp.TopicFilters.Add(new MqttTopicFilter { Topic = "A/B/C", QualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce }); - - await sm.Subscribe(sp); - - var result = sm.CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.ExactlyOnce, ""); - Assert.IsTrue(result.IsSubscribed); - Assert.AreEqual(result.QualityOfServiceLevel, MqttQualityOfServiceLevel.AtLeastOnce); - } - - [TestMethod] - public async Task MqttSubscriptionsManager_SubscribeSingleNoSuccess() - { - var s = CreateSession(); - - var sm = new MqttClientSubscriptionsManager(s, new MqttServerOptions(), new MqttServerEventDispatcher(new TestLogger()), new MqttRetainedMessagesManager()); - - var sp = new MqttSubscribePacket(); - sp.TopicFilters.Add(new MqttTopicFilterBuilder().WithTopic("A/B/C").Build()); - - await sm.Subscribe(sp); - - Assert.IsFalse(sm.CheckSubscriptions("A/B/X", MqttQualityOfServiceLevel.AtMostOnce, "").IsSubscribed); - } - - [TestMethod] - public async Task MqttSubscriptionsManager_SubscribeAndUnsubscribeSingle() - { - var s = CreateSession(); - - var sm = new MqttClientSubscriptionsManager(s, new MqttServerOptions(), new MqttServerEventDispatcher(new TestLogger()), new MqttRetainedMessagesManager()); - - var sp = new MqttSubscribePacket(); - sp.TopicFilters.Add(new MqttTopicFilterBuilder().WithTopic("A/B/C").Build()); - - await sm.Subscribe(sp); - - Assert.IsTrue(sm.CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.AtMostOnce, "").IsSubscribed); - - var up = new MqttUnsubscribePacket(); - up.TopicFilters.Add("A/B/C"); - await sm.Unsubscribe(up); - - Assert.IsFalse(sm.CheckSubscriptions("A/B/C", MqttQualityOfServiceLevel.AtMostOnce, "").IsSubscribed); - } - - MqttClientSession CreateSession() - { - return new MqttClientSession( - "", - new ConcurrentDictionary(), - new MqttServerEventDispatcher(new TestLogger()), - new MqttServerOptions(), - new MqttRetainedMessagesManager(), - false); - } - } -} diff --git a/Tests/MQTTnet.Core.Tests/Server/Session_Tests.cs b/Tests/MQTTnet.Core.Tests/Server/Session_Tests.cs deleted file mode 100644 index c502edf..0000000 --- a/Tests/MQTTnet.Core.Tests/Server/Session_Tests.cs +++ /dev/null @@ -1,174 +0,0 @@ -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Client; -using MQTTnet.Client.Options; -using MQTTnet.Client.Subscribing; -using MQTTnet.Server; -using MQTTnet.Tests.Mockups; - -namespace MQTTnet.Tests.Server -{ - [TestClass] - public class Session_Tests - { - public TestContext TestContext { get; set; } - - [TestMethod] - public async Task Set_Session_Item() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - var serverOptions = new MqttServerOptionsBuilder() - .WithConnectionValidator(delegate (MqttConnectionValidatorContext context) - { - // Don't validate anything. Just set some session items. - context.SessionItems["can_subscribe_x"] = true; - context.SessionItems["default_payload"] = "Hello World"; - }) - .WithSubscriptionInterceptor(delegate (MqttSubscriptionInterceptorContext context) - { - if (context.TopicFilter.Topic == "x") - { - context.AcceptSubscription = context.SessionItems["can_subscribe_x"] as bool? == true; - } - }) - .WithApplicationMessageInterceptor(delegate (MqttApplicationMessageInterceptorContext context) - { - context.ApplicationMessage.Payload = Encoding.UTF8.GetBytes(context.SessionItems["default_payload"] as string); - }); - - await testEnvironment.StartServer(serverOptions); - - string receivedPayload = null; - - var client = await testEnvironment.ConnectClient(); - client.UseApplicationMessageReceivedHandler(delegate (MqttApplicationMessageReceivedEventArgs args) - { - receivedPayload = args.ApplicationMessage.ConvertPayloadToString(); - }); - - var subscribeResult = await client.SubscribeAsync("x"); - - Assert.AreEqual(MqttClientSubscribeResultCode.GrantedQoS0, subscribeResult.Items[0].ResultCode); - - var client2 = await testEnvironment.ConnectClient(); - await client2.PublishAsync("x"); - - await Task.Delay(1000); - - Assert.AreEqual("Hello World", receivedPayload); - } - } - - [TestMethod] - public async Task Get_Session_Items_In_Status() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - var serverOptions = new MqttServerOptionsBuilder() - .WithConnectionValidator(delegate (MqttConnectionValidatorContext context) - { - // Don't validate anything. Just set some session items. - context.SessionItems["can_subscribe_x"] = true; - context.SessionItems["default_payload"] = "Hello World"; - }); - - await testEnvironment.StartServer(serverOptions); - - var client = await testEnvironment.ConnectClient(); - - var sessionStatus = await testEnvironment.Server.GetSessionStatusAsync(); - var session = sessionStatus.First(); - - Assert.AreEqual(true, session.Items["can_subscribe_x"]); - } - } - - - [TestMethod] - public async Task Manage_Session_MaxParallel() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - testEnvironment.IgnoreClientLogErrors = true; - var serverOptions = new MqttServerOptionsBuilder(); - await testEnvironment.StartServer(serverOptions); - - var options = new MqttClientOptionsBuilder().WithClientId("1"); - - var clients = await Task.WhenAll(Enumerable.Range(0, 10) - .Select(i => TryConnect(testEnvironment, options))); - - var connectedClients = clients.Where(c => c?.IsConnected ?? false).ToList(); - - Assert.AreEqual(1, connectedClients.Count); - } - } - - [TestMethod] - public async Task Clean_Session_Persistence() - { - using (var testEnvironment = new TestEnvironment(TestContext)) - { - // Create server with persistent sessions enabled - - await testEnvironment.StartServer(o => o.WithPersistentSessions()); - - const string ClientId = "Client1"; - - // Create client with clean session and long session expiry interval - - var client1 = await testEnvironment.ConnectClient(o => o - .WithProtocolVersion(Formatter.MqttProtocolVersion.V311) - .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) - .WithSessionExpiryInterval(9999) // not relevant for v311 but testing impact - .WithCleanSession(true) // start and end with clean session - .WithClientId(ClientId) - .Build() - ); - - // Disconnect; empty session should be removed from server - - await client1.DisconnectAsync(); - - // Simulate some time delay between connections - - await Task.Delay(1000); - - // Reconnect the same client ID without clean session - - var client2 = testEnvironment.CreateClient(); - var options = testEnvironment.Factory.CreateClientOptionsBuilder() - .WithProtocolVersion(Formatter.MqttProtocolVersion.V311) - .WithTcpServer("127.0.0.1", testEnvironment.ServerPort) - .WithSessionExpiryInterval(9999) // not relevant for v311 but testing impact - .WithCleanSession(false) // see if there is a session - .WithClientId(ClientId) - .Build(); - - - var result = await client2.ConnectAsync(options).ConfigureAwait(false); - - await client2.DisconnectAsync(); - - // Session should NOT be present for MQTT v311 and initial CleanSession == true - - Assert.IsTrue(!result.IsSessionPresent, "Session present"); - } - } - - async Task TryConnect(TestEnvironment testEnvironment, MqttClientOptionsBuilder options) - { - try - { - return await testEnvironment.ConnectClient(options); - } - catch (System.Exception) - { - return null; - } - } - } -} diff --git a/Tests/MQTTnet.Core.Tests/Server/Shared_Subscriptions_Tests.cs b/Tests/MQTTnet.Core.Tests/Server/Shared_Subscriptions_Tests.cs deleted file mode 100644 index d510e7a..0000000 --- a/Tests/MQTTnet.Core.Tests/Server/Shared_Subscriptions_Tests.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Client; -using MQTTnet.Formatter; - -namespace MQTTnet.Tests.Server -{ - [TestClass] - public sealed class Shared_Subscriptions_Tests : BaseTestClass - { - [TestMethod] - public async Task Server_Reports_Shared_Subscriptions_Not_Supported() - { - using (var testEnvironment = CreateTestEnvironment(MqttProtocolVersion.V500)) - { - await testEnvironment.StartServer(); - - var client = testEnvironment.CreateClient(); - var connectResult = await client.ConnectAsync(testEnvironment.Factory.CreateClientOptionsBuilder() - .WithProtocolVersion(MqttProtocolVersion.V500) - .WithTcpServer("127.0.0.1", testEnvironment.ServerPort).Build()); - - Assert.IsFalse(connectResult.SharedSubscriptionAvailable); - } - } - } -} \ No newline at end of file diff --git a/Tests/MQTTnet.Core.Tests/Server/TopicFilterComparer_Tests.cs b/Tests/MQTTnet.Core.Tests/Server/TopicFilterComparer_Tests.cs deleted file mode 100644 index e885ff0..0000000 --- a/Tests/MQTTnet.Core.Tests/Server/TopicFilterComparer_Tests.cs +++ /dev/null @@ -1,114 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MQTTnet.Server.Internal; - -namespace MQTTnet.Tests.Server -{ - [TestClass] - public class TopicFilterComparer_Tests - { - [TestMethod] - public void TopicFilterComparer_Plus_Match_With_Separator_Only() - { - //A Topic Name or Topic Filter consisting only of the ‘/’ character is valid - CompareAndAssert("A", "+", true); - } - - [TestMethod] - public void TopicFilterComparer_Hash_Match_With_Separator_Only() - { - //A Topic Name or Topic Filter consisting only of the ‘/’ character is valid - CompareAndAssert("/", "#", true); - } - - [TestMethod] - public void TopicFilterComparer_DirectMatch() - { - CompareAndAssert("A/B/C", "A/B/C", true); - } - - [TestMethod] - public void TopicFilterComparer_DirectNoMatch() - { - CompareAndAssert("A/B/X", "A/B/C", false); - } - - [TestMethod] - public void TopicFilterComparer_MiddleOneLevelWildcardMatch() - { - CompareAndAssert("A/B/C", "A/+/C", true); - } - - [TestMethod] - public void TopicFilterComparer_MiddleOneLevelWildcardNoMatch() - { - CompareAndAssert("A/B/C/D", "A/+/C", false); - } - - [TestMethod] - public void TopicFilterComparer_BeginningOneLevelWildcardMatch() - { - CompareAndAssert("A/B/C", "+/B/C", true); - } - - [TestMethod] - public void TopicFilterComparer_EndOneLevelWildcardMatch() - { - CompareAndAssert("A/B/C", "A/B/+", true); - } - - [TestMethod] - public void TopicFilterComparer_EndMultipleLevelsWildcardMatch() - { - CompareAndAssert("A/B/C", "A/#", true); - } - - [TestMethod] - public void TopicFilterComparer_EndMultipleLevelsWildcardNoMatch() - { - CompareAndAssert("A/B/C/D", "A/C/#", false); - } - - [TestMethod] - public void TopicFilterComparer_EndMultipleLevelsWildcardMatchEmptyLevel() - { - CompareAndAssert("A/", "A/#", true); - } - - [TestMethod] - public void TopicFilterComparer_AllLevelsWildcardMatch() - { - CompareAndAssert("A/B/C/D", "#", true); - } - - [TestMethod] - public void TopicFilterComparer_MultiLevel_Sport() - { - // Tests from official MQTT spec (4.7.1.2 Multi-level wildcard) - CompareAndAssert("sport/tennis/player1", "sport/tennis/player1/#", true); - CompareAndAssert("sport/tennis/player1/ranking", "sport/tennis/player1/#", true); - CompareAndAssert("sport/tennis/player1/score/wimbledon", "sport/tennis/player1/#", true); - - CompareAndAssert("sport/tennis/player1", "sport/tennis/+", true); - CompareAndAssert("sport/tennis/player2", "sport/tennis/+", true); - CompareAndAssert("sport/tennis/player1/ranking", "sport/tennis/+", false); - - CompareAndAssert("sport", "sport/#", true); - CompareAndAssert("sport", "sport/+", false); - CompareAndAssert("sport/", "sport/+", true); - } - - [TestMethod] - public void TopicFilterComparer_SingleLevel_Finance() - { - // Tests from official MQTT spec (4.7.1.3 Single level wildcard) - CompareAndAssert("/finance", "+/+", true); - CompareAndAssert("/finance", "/+", true); - CompareAndAssert("/finance", "+", false); - } - - private static void CompareAndAssert(string topic, string filter, bool expectedResult) - { - Assert.AreEqual(expectedResult, MqttTopicFilterComparer.IsMatch(topic, filter)); - } - } -} diff --git a/Tests/MQTTnet.Test.BlazorApp/Client/App.razor b/Tests/MQTTnet.Test.BlazorApp/Client/App.razor deleted file mode 100644 index 6f67a6e..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Client/App.razor +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - -

Sorry, there's nothing at this address.

-
-
-
diff --git a/Tests/MQTTnet.Test.BlazorApp/Client/MQTTnet.Test.BlazorApp.Client.csproj b/Tests/MQTTnet.Test.BlazorApp/Client/MQTTnet.Test.BlazorApp.Client.csproj deleted file mode 100644 index cd4f7bb..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Client/MQTTnet.Test.BlazorApp.Client.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - netstandard2.1 - 3.0 - - - - - - - - - - - - - - - diff --git a/Tests/MQTTnet.Test.BlazorApp/Client/Pages/Counter.razor b/Tests/MQTTnet.Test.BlazorApp/Client/Pages/Counter.razor deleted file mode 100644 index 57e55f6..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Client/Pages/Counter.razor +++ /dev/null @@ -1,27 +0,0 @@ -@page "/counter" -@using MQTTnet.Client.Options -@using System.Threading - -

Counter

- -

Current count: @currentCount

- - - -@code { - private int currentCount = 0; - - private async Task IncrementCount() - { - var mqttFactory = new MqttFactory(); - var client = mqttFactory.CreateMqttClient(); - - var options = new MqttClientOptionsBuilder() - .WithWebSocketServer("192.168.1.15:80/mqtt") - .Build(); - - await client.ConnectAsync(options, CancellationToken.None); - - currentCount++; - } -} diff --git a/Tests/MQTTnet.Test.BlazorApp/Client/Pages/FetchData.razor b/Tests/MQTTnet.Test.BlazorApp/Client/Pages/FetchData.razor deleted file mode 100644 index 371b573..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Client/Pages/FetchData.razor +++ /dev/null @@ -1,46 +0,0 @@ -@page "/fetchdata" -@using MQTTnet.Test.BlazorApp.Shared -@inject HttpClient Http - -

Weather forecast

- -

This component demonstrates fetching data from the server.

- -@if (forecasts == null) -{ -

Loading...

-} -else -{ - - - - - - - - - - - @foreach (var forecast in forecasts) - { - - - - - - - } - -
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
-} - -@code { - private WeatherForecast[] forecasts; - - protected override async Task OnInitializedAsync() - { - forecasts = await Http.GetFromJsonAsync("WeatherForecast"); - } - -} diff --git a/Tests/MQTTnet.Test.BlazorApp/Client/Pages/Index.razor b/Tests/MQTTnet.Test.BlazorApp/Client/Pages/Index.razor deleted file mode 100644 index e54d914..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Client/Pages/Index.razor +++ /dev/null @@ -1,7 +0,0 @@ -@page "/" - -

Hello, world!

- -Welcome to your new app. - - diff --git a/Tests/MQTTnet.Test.BlazorApp/Client/Program.cs b/Tests/MQTTnet.Test.BlazorApp/Client/Program.cs deleted file mode 100644 index 800427b..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Client/Program.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Components.WebAssembly.Hosting; -using Microsoft.Extensions.DependencyInjection; - -namespace MQTTnet.Test.BlazorApp.Client -{ - public static class Program - { - public static Task Main(string[] args) - { - var builder = WebAssemblyHostBuilder.CreateDefault(args); - builder.RootComponents.Add("app"); - - builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); - - return builder.Build().RunAsync(); - } - } -} diff --git a/Tests/MQTTnet.Test.BlazorApp/Client/Shared/MainLayout.razor b/Tests/MQTTnet.Test.BlazorApp/Client/Shared/MainLayout.razor deleted file mode 100644 index 0f4e22a..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Client/Shared/MainLayout.razor +++ /dev/null @@ -1,15 +0,0 @@ -@inherits LayoutComponentBase - - - -
-
- About -
- -
- @Body -
-
diff --git a/Tests/MQTTnet.Test.BlazorApp/Client/Shared/NavMenu.razor b/Tests/MQTTnet.Test.BlazorApp/Client/Shared/NavMenu.razor deleted file mode 100644 index 6ea3f2f..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Client/Shared/NavMenu.razor +++ /dev/null @@ -1,37 +0,0 @@ - - -
- -
- -@code { - private bool collapseNavMenu = true; - - private string NavMenuCssClass => collapseNavMenu ? "collapse" : null; - - private void ToggleNavMenu() - { - collapseNavMenu = !collapseNavMenu; - } -} diff --git a/Tests/MQTTnet.Test.BlazorApp/Client/Shared/SurveyPrompt.razor b/Tests/MQTTnet.Test.BlazorApp/Client/Shared/SurveyPrompt.razor deleted file mode 100644 index 0271409..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Client/Shared/SurveyPrompt.razor +++ /dev/null @@ -1,16 +0,0 @@ - - -@code { - // Demonstrates how a parent component can supply parameters - [Parameter] - public string Title { get; set; } -} diff --git a/Tests/MQTTnet.Test.BlazorApp/Client/_Imports.razor b/Tests/MQTTnet.Test.BlazorApp/Client/_Imports.razor deleted file mode 100644 index 77388ef..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Client/_Imports.razor +++ /dev/null @@ -1,9 +0,0 @@ -@using System.Net.Http -@using System.Net.Http.Json -@using Microsoft.AspNetCore.Components.Forms -@using Microsoft.AspNetCore.Components.Routing -@using Microsoft.AspNetCore.Components.Web -@using Microsoft.AspNetCore.Components.WebAssembly.Http -@using Microsoft.JSInterop -@using MQTTnet.Test.BlazorApp.Client -@using MQTTnet.Test.BlazorApp.Client.Shared diff --git a/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/app.css b/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/app.css deleted file mode 100644 index 4e4425c..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/app.css +++ /dev/null @@ -1,183 +0,0 @@ -@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); - -html, body { - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; -} - -a, .btn-link { - color: #0366d6; -} - -.btn-primary { - color: #fff; - background-color: #1b6ec2; - border-color: #1861ac; -} - -app { - position: relative; - display: flex; - flex-direction: column; -} - -.top-row { - height: 3.5rem; - display: flex; - align-items: center; -} - -.main { - flex: 1; -} - - .main .top-row { - background-color: #f7f7f7; - border-bottom: 1px solid #d6d5d5; - justify-content: flex-end; - } - - .main .top-row > a, .main .top-row .btn-link { - white-space: nowrap; - margin-left: 1.5rem; - } - -.main .top-row a:first-child { - overflow: hidden; - text-overflow: ellipsis; -} - -.sidebar { - background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); -} - - .sidebar .top-row { - background-color: rgba(0,0,0,0.4); - } - - .sidebar .navbar-brand { - font-size: 1.1rem; - } - - .sidebar .oi { - width: 2rem; - font-size: 1.1rem; - vertical-align: text-top; - top: -2px; - } - - .sidebar .nav-item { - font-size: 0.9rem; - padding-bottom: 0.5rem; - } - - .sidebar .nav-item:first-of-type { - padding-top: 1rem; - } - - .sidebar .nav-item:last-of-type { - padding-bottom: 1rem; - } - - .sidebar .nav-item a { - color: #d7d7d7; - border-radius: 4px; - height: 3rem; - display: flex; - align-items: center; - line-height: 3rem; - } - - .sidebar .nav-item a.active { - background-color: rgba(255,255,255,0.25); - color: white; - } - - .sidebar .nav-item a:hover { - background-color: rgba(255,255,255,0.1); - color: white; - } - -.content { - padding-top: 1.1rem; -} - -.navbar-toggler { - background-color: rgba(255, 255, 255, 0.1); -} - -.valid.modified:not([type=checkbox]) { - outline: 1px solid #26b050; -} - -.invalid { - outline: 1px solid red; -} - -.validation-message { - color: red; -} - -#blazor-error-ui { - background: lightyellow; - bottom: 0; - box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); - display: none; - left: 0; - padding: 0.6rem 1.25rem 0.7rem 1.25rem; - position: fixed; - width: 100%; - z-index: 1000; -} - -#blazor-error-ui .dismiss { - cursor: pointer; - position: absolute; - right: 0.75rem; - top: 0.5rem; -} - -@media (max-width: 767.98px) { - .main .top-row:not(.auth) { - display: none; - } - - .main .top-row.auth { - justify-content: space-between; - } - - .main .top-row a, .main .top-row .btn-link { - margin-left: 0; - } -} - -@media (min-width: 768px) { - app { - flex-direction: row; - } - - .sidebar { - width: 250px; - height: 100vh; - position: sticky; - top: 0; - } - - .main .top-row { - position: sticky; - top: 0; - } - - .main > div { - padding-left: 2rem !important; - padding-right: 1.5rem !important; - } - - .navbar-toggler { - display: none; - } - - .sidebar .collapse { - /* Never collapse the sidebar for wide screens */ - display: block; - } -} diff --git a/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/bootstrap/bootstrap.min.css b/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/bootstrap/bootstrap.min.css deleted file mode 100644 index 92e3fe8..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/bootstrap/bootstrap.min.css +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * Bootstrap v4.3.1 (https://getbootstrap.com/) - * Copyright 2011-2019 The Bootstrap Authors - * Copyright 2011-2019 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-break:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;color:#212529}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding-top:.375rem;padding-bottom:.375rem;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:center right calc(.375em + .1875rem);background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.form-control.is-valid~.valid-feedback,.form-control.is-valid~.valid-tooltip,.was-validated .form-control:valid~.valid-feedback,.was-validated .form-control:valid~.valid-tooltip{display:block}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc((1em + .75rem) * 3 / 4 + 1.75rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-select.is-valid~.valid-feedback,.custom-select.is-valid~.valid-tooltip,.was-validated .custom-select:valid~.valid-feedback,.was-validated .custom-select:valid~.valid-tooltip{display:block}.form-control-file.is-valid~.valid-feedback,.form-control-file.is-valid~.valid-tooltip,.was-validated .form-control-file:valid~.valid-feedback,.was-validated .form-control-file:valid~.valid-tooltip{display:block}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid~.valid-feedback,.custom-control-input.is-valid~.valid-tooltip,.was-validated .custom-control-input:valid~.valid-feedback,.was-validated .custom-control-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid~.valid-feedback,.custom-file-input.is-valid~.valid-tooltip,.was-validated .custom-file-input:valid~.valid-feedback,.was-validated .custom-file-input:valid~.valid-tooltip{display:block}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23dc3545' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E");background-repeat:no-repeat;background-position:center right calc(.375em + .1875rem);background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-control.is-invalid~.invalid-feedback,.form-control.is-invalid~.invalid-tooltip,.was-validated .form-control:invalid~.invalid-feedback,.was-validated .form-control:invalid~.invalid-tooltip{display:block}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc((1em + .75rem) * 3 / 4 + 1.75rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23dc3545' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-select.is-invalid~.invalid-feedback,.custom-select.is-invalid~.invalid-tooltip,.was-validated .custom-select:invalid~.invalid-feedback,.was-validated .custom-select:invalid~.invalid-tooltip{display:block}.form-control-file.is-invalid~.invalid-feedback,.form-control-file.is-invalid~.invalid-tooltip,.was-validated .form-control-file:invalid~.invalid-feedback,.was-validated .form-control-file:invalid~.invalid-tooltip{display:block}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid~.invalid-feedback,.custom-control-input.is-invalid~.invalid-tooltip,.was-validated .custom-control-input:invalid~.invalid-feedback,.was-validated .custom-control-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid~.invalid-feedback,.custom-file-input.is-invalid~.invalid-tooltip,.was-validated .custom-file-input:invalid~.invalid-feedback,.was-validated .custom-file-input:invalid~.invalid-tooltip{display:block}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;text-decoration:none}.btn-link:hover{color:#0056b3;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline;box-shadow:none}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50%/50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#007bff;background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:disabled~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:calc(1rem + .4rem);padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar>.container,.navbar>.container-fluid{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(.25rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{display:-ms-flexbox;display:flex;-ms-flex:1 0 0%;flex:1 0 0%;-ms-flex-direction:column;flex-direction:column;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:first-of-type) .card-header:first-child{border-radius:0}.accordion>.card:not(:first-of-type):not(:last-of-type){border-bottom:0;border-radius:0}.accordion>.card:first-of-type{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:last-of-type{border-top-left-radius:0;border-top-right-radius:0}.accordion>.card .card-header{margin-bottom:-1px}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:2;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:1;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}@media (min-width:576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-sm .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width:768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-md .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width:992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-lg .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width:1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-xl .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush .list-group-item:last-child{margin-bottom:-1px}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{margin-bottom:0;border-bottom:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal-dialog-scrollable{display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);content:""}.modal-dialog-centered.modal-dialog-scrollable{-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:.3rem;border-top-right-radius:.3rem}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:1rem;border-top:1px solid #dee2e6;border-bottom-right-radius:.3rem;border-bottom-left-radius:.3rem}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:0s .6s opacity}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50%/100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0056b3!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-break:break-word!important;overflow-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}} -/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/FONT-LICENSE b/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/FONT-LICENSE deleted file mode 100644 index a1dc03f..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/FONT-LICENSE +++ /dev/null @@ -1,86 +0,0 @@ -SIL OPEN FONT LICENSE Version 1.1 - -Copyright (c) 2014 Waybury - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/ICON-LICENSE b/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/ICON-LICENSE deleted file mode 100644 index 2199f4a..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/ICON-LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 Waybury - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file diff --git a/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/README.md b/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/README.md deleted file mode 100644 index 6b810e4..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/README.md +++ /dev/null @@ -1,114 +0,0 @@ -[Open Iconic v1.1.1](http://useiconic.com/open) -=========== - -### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons) - - - -## What's in Open Iconic? - -* 223 icons designed to be legible down to 8 pixels -* Super-light SVG files - 61.8 for the entire set -* SVG sprite—the modern replacement for icon fonts -* Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats -* Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats -* PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. - - -## Getting Started - -#### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections. - -### General Usage - -#### Using Open Iconic's SVGs - -We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). - -``` -icon name -``` - -#### Using Open Iconic's SVG Sprite - -Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. - -Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* - -``` - - - -``` - -Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. - -``` -.icon { - width: 16px; - height: 16px; -} -``` - -Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. - -``` -.icon-account-login { - fill: #f00; -} -``` - -To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). - -#### Using Open Iconic's Icon Font... - - -##### …with Bootstrap - -You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` - - -``` - -``` - - -``` - -``` - -##### …with Foundation - -You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` - -``` - -``` - - -``` - -``` - -##### …on its own - -You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` - -``` - -``` - -``` - -``` - - -## License - -### Icons - -All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). - -### Fonts - -All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). diff --git a/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css b/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css deleted file mode 100644 index 4664f2e..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css +++ /dev/null @@ -1 +0,0 @@ -@font-face{font-family:Icons;src:url(../fonts/open-iconic.eot);src:url(../fonts/open-iconic.eot?#iconic-sm) format('embedded-opentype'),url(../fonts/open-iconic.woff) format('woff'),url(../fonts/open-iconic.ttf) format('truetype'),url(../fonts/open-iconic.otf) format('opentype'),url(../fonts/open-iconic.svg#iconic-sm) format('svg');font-weight:400;font-style:normal}.oi{position:relative;top:1px;display:inline-block;speak:none;font-family:Icons;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.oi:empty:before{width:1em;text-align:center;box-sizing:content-box}.oi.oi-align-center:before{text-align:center}.oi.oi-align-left:before{text-align:left}.oi.oi-align-right:before{text-align:right}.oi.oi-flip-horizontal:before{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.oi.oi-flip-vertical:before{-webkit-transform:scale(1,-1);-ms-transform:scale(-1,1);transform:scale(1,-1)}.oi.oi-flip-horizontal-vertical:before{-webkit-transform:scale(-1,-1);-ms-transform:scale(-1,1);transform:scale(-1,-1)}.oi-account-login:before{content:'\e000'}.oi-account-logout:before{content:'\e001'}.oi-action-redo:before{content:'\e002'}.oi-action-undo:before{content:'\e003'}.oi-align-center:before{content:'\e004'}.oi-align-left:before{content:'\e005'}.oi-align-right:before{content:'\e006'}.oi-aperture:before{content:'\e007'}.oi-arrow-bottom:before{content:'\e008'}.oi-arrow-circle-bottom:before{content:'\e009'}.oi-arrow-circle-left:before{content:'\e00a'}.oi-arrow-circle-right:before{content:'\e00b'}.oi-arrow-circle-top:before{content:'\e00c'}.oi-arrow-left:before{content:'\e00d'}.oi-arrow-right:before{content:'\e00e'}.oi-arrow-thick-bottom:before{content:'\e00f'}.oi-arrow-thick-left:before{content:'\e010'}.oi-arrow-thick-right:before{content:'\e011'}.oi-arrow-thick-top:before{content:'\e012'}.oi-arrow-top:before{content:'\e013'}.oi-audio-spectrum:before{content:'\e014'}.oi-audio:before{content:'\e015'}.oi-badge:before{content:'\e016'}.oi-ban:before{content:'\e017'}.oi-bar-chart:before{content:'\e018'}.oi-basket:before{content:'\e019'}.oi-battery-empty:before{content:'\e01a'}.oi-battery-full:before{content:'\e01b'}.oi-beaker:before{content:'\e01c'}.oi-bell:before{content:'\e01d'}.oi-bluetooth:before{content:'\e01e'}.oi-bold:before{content:'\e01f'}.oi-bolt:before{content:'\e020'}.oi-book:before{content:'\e021'}.oi-bookmark:before{content:'\e022'}.oi-box:before{content:'\e023'}.oi-briefcase:before{content:'\e024'}.oi-british-pound:before{content:'\e025'}.oi-browser:before{content:'\e026'}.oi-brush:before{content:'\e027'}.oi-bug:before{content:'\e028'}.oi-bullhorn:before{content:'\e029'}.oi-calculator:before{content:'\e02a'}.oi-calendar:before{content:'\e02b'}.oi-camera-slr:before{content:'\e02c'}.oi-caret-bottom:before{content:'\e02d'}.oi-caret-left:before{content:'\e02e'}.oi-caret-right:before{content:'\e02f'}.oi-caret-top:before{content:'\e030'}.oi-cart:before{content:'\e031'}.oi-chat:before{content:'\e032'}.oi-check:before{content:'\e033'}.oi-chevron-bottom:before{content:'\e034'}.oi-chevron-left:before{content:'\e035'}.oi-chevron-right:before{content:'\e036'}.oi-chevron-top:before{content:'\e037'}.oi-circle-check:before{content:'\e038'}.oi-circle-x:before{content:'\e039'}.oi-clipboard:before{content:'\e03a'}.oi-clock:before{content:'\e03b'}.oi-cloud-download:before{content:'\e03c'}.oi-cloud-upload:before{content:'\e03d'}.oi-cloud:before{content:'\e03e'}.oi-cloudy:before{content:'\e03f'}.oi-code:before{content:'\e040'}.oi-cog:before{content:'\e041'}.oi-collapse-down:before{content:'\e042'}.oi-collapse-left:before{content:'\e043'}.oi-collapse-right:before{content:'\e044'}.oi-collapse-up:before{content:'\e045'}.oi-command:before{content:'\e046'}.oi-comment-square:before{content:'\e047'}.oi-compass:before{content:'\e048'}.oi-contrast:before{content:'\e049'}.oi-copywriting:before{content:'\e04a'}.oi-credit-card:before{content:'\e04b'}.oi-crop:before{content:'\e04c'}.oi-dashboard:before{content:'\e04d'}.oi-data-transfer-download:before{content:'\e04e'}.oi-data-transfer-upload:before{content:'\e04f'}.oi-delete:before{content:'\e050'}.oi-dial:before{content:'\e051'}.oi-document:before{content:'\e052'}.oi-dollar:before{content:'\e053'}.oi-double-quote-sans-left:before{content:'\e054'}.oi-double-quote-sans-right:before{content:'\e055'}.oi-double-quote-serif-left:before{content:'\e056'}.oi-double-quote-serif-right:before{content:'\e057'}.oi-droplet:before{content:'\e058'}.oi-eject:before{content:'\e059'}.oi-elevator:before{content:'\e05a'}.oi-ellipses:before{content:'\e05b'}.oi-envelope-closed:before{content:'\e05c'}.oi-envelope-open:before{content:'\e05d'}.oi-euro:before{content:'\e05e'}.oi-excerpt:before{content:'\e05f'}.oi-expand-down:before{content:'\e060'}.oi-expand-left:before{content:'\e061'}.oi-expand-right:before{content:'\e062'}.oi-expand-up:before{content:'\e063'}.oi-external-link:before{content:'\e064'}.oi-eye:before{content:'\e065'}.oi-eyedropper:before{content:'\e066'}.oi-file:before{content:'\e067'}.oi-fire:before{content:'\e068'}.oi-flag:before{content:'\e069'}.oi-flash:before{content:'\e06a'}.oi-folder:before{content:'\e06b'}.oi-fork:before{content:'\e06c'}.oi-fullscreen-enter:before{content:'\e06d'}.oi-fullscreen-exit:before{content:'\e06e'}.oi-globe:before{content:'\e06f'}.oi-graph:before{content:'\e070'}.oi-grid-four-up:before{content:'\e071'}.oi-grid-three-up:before{content:'\e072'}.oi-grid-two-up:before{content:'\e073'}.oi-hard-drive:before{content:'\e074'}.oi-header:before{content:'\e075'}.oi-headphones:before{content:'\e076'}.oi-heart:before{content:'\e077'}.oi-home:before{content:'\e078'}.oi-image:before{content:'\e079'}.oi-inbox:before{content:'\e07a'}.oi-infinity:before{content:'\e07b'}.oi-info:before{content:'\e07c'}.oi-italic:before{content:'\e07d'}.oi-justify-center:before{content:'\e07e'}.oi-justify-left:before{content:'\e07f'}.oi-justify-right:before{content:'\e080'}.oi-key:before{content:'\e081'}.oi-laptop:before{content:'\e082'}.oi-layers:before{content:'\e083'}.oi-lightbulb:before{content:'\e084'}.oi-link-broken:before{content:'\e085'}.oi-link-intact:before{content:'\e086'}.oi-list-rich:before{content:'\e087'}.oi-list:before{content:'\e088'}.oi-location:before{content:'\e089'}.oi-lock-locked:before{content:'\e08a'}.oi-lock-unlocked:before{content:'\e08b'}.oi-loop-circular:before{content:'\e08c'}.oi-loop-square:before{content:'\e08d'}.oi-loop:before{content:'\e08e'}.oi-magnifying-glass:before{content:'\e08f'}.oi-map-marker:before{content:'\e090'}.oi-map:before{content:'\e091'}.oi-media-pause:before{content:'\e092'}.oi-media-play:before{content:'\e093'}.oi-media-record:before{content:'\e094'}.oi-media-skip-backward:before{content:'\e095'}.oi-media-skip-forward:before{content:'\e096'}.oi-media-step-backward:before{content:'\e097'}.oi-media-step-forward:before{content:'\e098'}.oi-media-stop:before{content:'\e099'}.oi-medical-cross:before{content:'\e09a'}.oi-menu:before{content:'\e09b'}.oi-microphone:before{content:'\e09c'}.oi-minus:before{content:'\e09d'}.oi-monitor:before{content:'\e09e'}.oi-moon:before{content:'\e09f'}.oi-move:before{content:'\e0a0'}.oi-musical-note:before{content:'\e0a1'}.oi-paperclip:before{content:'\e0a2'}.oi-pencil:before{content:'\e0a3'}.oi-people:before{content:'\e0a4'}.oi-person:before{content:'\e0a5'}.oi-phone:before{content:'\e0a6'}.oi-pie-chart:before{content:'\e0a7'}.oi-pin:before{content:'\e0a8'}.oi-play-circle:before{content:'\e0a9'}.oi-plus:before{content:'\e0aa'}.oi-power-standby:before{content:'\e0ab'}.oi-print:before{content:'\e0ac'}.oi-project:before{content:'\e0ad'}.oi-pulse:before{content:'\e0ae'}.oi-puzzle-piece:before{content:'\e0af'}.oi-question-mark:before{content:'\e0b0'}.oi-rain:before{content:'\e0b1'}.oi-random:before{content:'\e0b2'}.oi-reload:before{content:'\e0b3'}.oi-resize-both:before{content:'\e0b4'}.oi-resize-height:before{content:'\e0b5'}.oi-resize-width:before{content:'\e0b6'}.oi-rss-alt:before{content:'\e0b7'}.oi-rss:before{content:'\e0b8'}.oi-script:before{content:'\e0b9'}.oi-share-boxed:before{content:'\e0ba'}.oi-share:before{content:'\e0bb'}.oi-shield:before{content:'\e0bc'}.oi-signal:before{content:'\e0bd'}.oi-signpost:before{content:'\e0be'}.oi-sort-ascending:before{content:'\e0bf'}.oi-sort-descending:before{content:'\e0c0'}.oi-spreadsheet:before{content:'\e0c1'}.oi-star:before{content:'\e0c2'}.oi-sun:before{content:'\e0c3'}.oi-tablet:before{content:'\e0c4'}.oi-tag:before{content:'\e0c5'}.oi-tags:before{content:'\e0c6'}.oi-target:before{content:'\e0c7'}.oi-task:before{content:'\e0c8'}.oi-terminal:before{content:'\e0c9'}.oi-text:before{content:'\e0ca'}.oi-thumb-down:before{content:'\e0cb'}.oi-thumb-up:before{content:'\e0cc'}.oi-timer:before{content:'\e0cd'}.oi-transfer:before{content:'\e0ce'}.oi-trash:before{content:'\e0cf'}.oi-underline:before{content:'\e0d0'}.oi-vertical-align-bottom:before{content:'\e0d1'}.oi-vertical-align-center:before{content:'\e0d2'}.oi-vertical-align-top:before{content:'\e0d3'}.oi-video:before{content:'\e0d4'}.oi-volume-high:before{content:'\e0d5'}.oi-volume-low:before{content:'\e0d6'}.oi-volume-off:before{content:'\e0d7'}.oi-warning:before{content:'\e0d8'}.oi-wifi:before{content:'\e0d9'}.oi-wrench:before{content:'\e0da'}.oi-x:before{content:'\e0db'}.oi-yen:before{content:'\e0dc'}.oi-zoom-in:before{content:'\e0dd'}.oi-zoom-out:before{content:'\e0de'} \ No newline at end of file diff --git a/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.eot b/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.eot deleted file mode 100644 index f98177dbf711863eff7c90f84d5d419d02d99ba8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28196 zcmdsfdwg8gedj&r&QluAL-W#Wq&pgEMvsv!&0Cf&+mau`20w)Dj4&8Iu59zN6=RG; z451+<)Ej~^SrrmCp$=hb!Zu?PlZ0v^rFqOYfzqruY1s`+ve{(Uv}w|M+teR4-tX_6 zJJQHDgm(Majx=-5J@?%6_?_SRz0Ykss3^zpP!y(cg+5#{t0IGvlZlxgLVa!|Pwg%0HwaAkJPsR_7CkF z{hz=5BS2$bQO4>H%uMR+@Bes%qU=0}`qqrY1!(P0t>lnf>u?>hCHF7DiD%jIRLs_gA0(b1L}rzgltYVrt?gc2Y5;9UDjQ z%B)P;{Yp$h?WOgkCosju&-Q&Abmg0GDQ~^0YA77V?+nuN;!-_LToFFdx5>D-3RhIC zNim@Y28=&kzxC#&OZZhTUDD)z++voc1{on3eJelI&j0@(PPn1`HTMH@R>gMK0^H#} z-APZ<6H9s`4L|t$XFtpR3vV~DpGXL)8ZghQI8nFC#;Gm~d%|gaTbMPC42!c1B?miM zn$?TN(kwg4=NH!N?1DZwr|Va=QM0@at3QmtSVbGuP_f*EuIqDh*>o`umty&fMPWVN zwOSy=lGa!#OKqKlS=4KL6^YiDEHv;MA!Dj|%KqdbXOLRkVPgo+>xM z`tdLxr03~jdXO4;l(4}>Kca7fS2gy1&DtubqsnG6amCcr?ZNni_*#ur)!una=lO+a z(W#N+^Oy#G-fw#XCIlD!Q7hD3IjwB$Uoy5LHCCk7M6R+q+PRlLC+2F#Og&0KX;fTm z9gRV6t=nO-P_Az=CG4l*~#0dwv=AFvG8)~&n&z! z>wcqjdUo&ccd;$(NdM=j`265c&L?J1yxG?F>}_{_wry>?^aan|yPK}R#cpg(b^$xz zf;Gl2?&aw=%jBtFht&{S}(z)fW6^mCJSIuQ@i4|p+ zx3$z#v51krkNGj$t;x!E@Z?f6a(ZZoC>r5@Ucl5$FlAy4?Q*}B&hb1!m&U%lE*Euc z#N62h7Dtl~c7f-y5Wr$VDS7_#wX$QaKmmSK`iqLyDz`g-`54&Z80Kl-ofTt{b;TI$ zT#%ThARiNAa&`dV8`oF>zV?w_b1QPe8_mRA%fyml9N}zE z_-m(6zyG|m?j+Mnf7=xbb%mHqB&x=o>~}ut(o3hDKA)2v)LFgfzUPV|zwQq${}Jm! zdvqS0#f$auxa~yCyx|1clRx73VPI)bD(DG&?EH&%UAHgnwu8I!`Kp(SFWc>Wqg^Ma zTe*j+Ez4Kzf`(q!&Qco{4bZc|i%U<6aYU6B7)Lx7;53d@W>5_ia)5Ny1_i;Fuu5e! z-gKnZ5^0T^BYvyJ8eYL}Z1AdPGrK^uOnkDgwNvdLC@Di@t#zMFFbngC*yBaZnjCxO zZVNwAs{vvUm;SyZn;h!w92-hzJ6O%btT}YL>chAEtV)iFcrVtkM#9EvCDS2-twqu&y5y= zw;q?%OgQCDn!(c|X=^MS%LcRltks{LOR&8^`AO+?V#}7fxh-2D&&;XX#mAnwc+n^T z?I3bku^;?ONNGpAEzQ9|wZK)t4otF{`3c3+*b1IhG!ph>Qy^76GG!OWj>gw*J9S{; z4GguD#dS*bxuJZ1h^DeJ+j4C4fm1qeo$MT>2@;LZAJ13vO*7V9&^G2tG7zXZ?FfUm z#SMB%w5<{KY9(%XvO$a>;P-@EExte!yNWhJc8Fzlj6qNMLkn-vTJq?^8$)^3(jB7q zK=I-s|H2zsK0QCgqux+AWHJJLC*aI54Qv=}8o8CR zZwEnEGeI;95)@8khtt_i7IdVSr-7d=zV}u=kyugRRIfhw zeDDVL_QJF74|wmnm%D6ymv^z?^V}7hzydG+3&|d1l55zYhOj3av4&o`Cs_*%Sec7K6kNmX1R1PD zYix+tfd4N`+-xrWgR9=NE#s(Rcb7VHTc13*dDZG`u2Vy5+-xoVUX3HO%~S7URi&d_ za|fSnjU2xwx0TQZaKH4&{58k8C}uC~%bS*!t{HKh8i(U_G87Y4V6Mbq6(WCwXB8|!8EMz7QHK&Z*mcFpc< z+RRN&4^&tAL+^tIcvp=oXtiyp&{<>WDx_onB*c$TJG+1&G7a-fJb(lhUsyZ?n4aYuiGF!~%5BNht zkLp&(Oy-jvTIYsHHM$C!I<(f1-`DJlUJRPI*qqTW+kTY1z~}7?FWT8-kChzvs)6UdU2dnB zx$Q4tyPa>#r3G#wn2l*V56=aR2F{ncODvttVSQ>#9gal)dghYmi{bh)=H+FHv=R)hRtN(5RM_@E0? z5kM8i9$Uerye_+vY3w_3_P#}l!_lo1O@m<2iy=ee^_*n$LO%GqY8Q0?Zgjgfu%~GcgW`lM%ck$vJ0hs4ShNL&iUr07ttjmJdpcTs@YpWWi zLeN`YSMXY|ok4QJ?b0l&5gLe$Y$tuGLVQ^KYqd>=*0HTNl+kS35%>Tm0`e`E!ED_IcN2j(%)=h7jWUMUO0+h zRRdK=F-j8tO~s;7T+L5ZJE`9#xx)%NSO@&}!yd9s-zo3*_M|@$v_@C3vckh1zbO=c zQz)I*Tce|GeeMd4hi+VZwk!ITF`O4lyst z4Y9otCo>pme1^Sp;8gd3{bk67rC&829rHZ0Sv4^W_lM?+#W|mfdf9!dfV9s|K;O|StI2k1ficm_+HH-M&Az?i*JgaZ@5^* zE(GBy_gO3&{S94&SP6KeFT!J~`_y882z_O7zCy_m6O~Qphe|_ZM`==gUbZ=u2Swa{ zc-fe%m1d0D?+|)|HxUHK2lEHO%w;$(wR`cy*WG%iYh_pcDb`1TTj~Ka=bd}qEvd|b zQ^m{sB3zJTR-u==fD1KM#C|~QSdzg!U=2oM?a81uk|lZ~xEUA=&kOD%%>%Gb(5GU} zTOiHa&bDc8$;Tnw1g$O1?*a*kxmaWcc5HS9ORvEu4`$0U9^0!Yn(iJ=IPSjNkr=(Z zDY5+W^zl3}LDjB$vt0K9RLLL5oR)B01*NRQyg(`CyrhZKYKCkpBzcJRl8dOC)PO3V zwaRCOc~t7^!d#+yVgv-}OF|o3m8R8-X8{D#>>(A*N?k%eEp2Xp{Og1~APhL#`%a==_CxDO?0Cstm3 z30%#eV0U(fut|VC7qL}fR)`ZvgHV2zC*{}rc8UrQR$o+3OBx1mZ zBw=TjS?FXCbR;9PLY)=VCY?28(R%*NYUev|5yJtCsjYSrP2lsA^AtqzGR9J<&#=SZlzmY*a6=bs1jPR3mA)Spy%lFF5 zROWpz3sBDaoT_RIIQP`UxG^?pxxq~=8DPB}F$ARVc7;st8!RO5cGmB4ZoCptXt$F* zCv5*@5{La6dkp?4(js8{AS3-dZwU(s)Cst!XwFM`ri$l@b{jSbv$P3IT0yOVSP=dS zw*x&V*WCoyCHggs=e+QPsqGa4jr6auy%nO1Ao}q)D@u%U$o8tSy3nH?Dvbl+CYu7R zr;${9Fe_A8p_~#-b)dOUM&F@rV13*8{M%o^J~;k`hJ4<8%LsADky~hvVqJxtWL9i& zd%G1Mt!u5vSyM$+o%}ek3E&T+d^?dS@rBYBXD1idLoy_TzhGTt(IHuqpa=xQPQX9) z0h)5@Nist!gP>qOtZ~ zMv}`QE9zVNwYYBcTms~PKGwK=(ESy}0lC<7k|w5-tgTAbC1>SlGFV{0;z+^k=% zP^`6tvGjFXO#;T4IOYvy2(y&V4OomZUoa&6Vs1-oEuS+>A1T9w;)~}99&%k-92Wn0 z#WQ5b|rc;Pr&qX~%&%}F#z(-avRX_b{G<+PY*7c;v8*q~hfsmb>XW+&kft>v*aLckMzT1J z?H52T$v0c|wF=q6AAu|`zT{OizHk$e;I$04CdhHNvo^$$PQGVNwOorbI=H7r;%%PvE>$cds9X%hLl`MJ6ID0UQ$ zMeHT$iSw|nEZP>KML>Fm^x}gE6TyOH{baI=g|o?MIs%(H=}Lgtd<{kFSU|8gs^G;wS0(6~;HoUQld?%1QRZPOq4L+V$^Kce3< zza;Al%6f$Xs zJ(ifhc0+%g-EIkP+x_5%O&`B;lgFbvI(tX2(;pCqr(#uYQ^?=!6x^22htq48xpO$v_M&$&HhkRZI$5SG*{TDTls&4?T2*ow$^%;=-wcMati4n z1CHQ>9wQCHD;N>p7-?idNGxoNs;bt2YwvLPeckc+x|?c4{(9F?>4DPUv%A;0{U0rT z_kOmD&oj?W>$p&VVcQqtdrO##R}$gZvxB^K55{&58Yt zJxOe?lC{aLO=P4@bLhDSp?60bYv?&Ikwm8{*lPk&G^LoJkdZLui?+rM>F(~;>w2o| zMK;_&(66yNkzdnZIw!7G&E(FlJ&^0YY17!o8++wN$M&_u>xQ?M7Ubo=DWd@UWC>?f zaBRpICMlP|)$9eavi2=$}kiDm__jweO@3rN;(HfCW16c9Drzu=v&AdeV|?K z)Hl>6;GWe_22rqia&JR(5=A5kv`TN7kZQ7Nx(gj9+tU~<`a?Zgk%=6%J-S;Vf)l z0Lt7Py8yV%l2=b$%8RSCQEe5x!D~D$o5J(-tk}HN7&Sr#rE{V&8p{&>vO=@mh5fr@ zQ*622sGaQeFjBNykn}REr5UPzt2F@U1^%tXhqD=YE_!)(NR36wpAto)W}`tTHWeJ$ z>Kc}gmd$AFZ|-gi@CbSTFbq6RJAy4%%b{gEY$%uTDdmFttp;N%I-l% z_DCo&{xE-elH$n7{aCg!AftazXDcW*!Ul!TUdgkhUm~V-!*`ujvXDvFDD7)ohgPl3 zWm1X0-gs9>w5?TZZfdBjTAsney4@_8{!`-jJF=) z!Ih4dvLfo`b6!xSXZ<1gZ}Sax-i2Gee9%xRy`{56px72K`EN^adc9{21=65bkhPMa zR}Dn3Al|?mA(VFLEopIu&Y`6UD>6tJS#HW#Rgp`MU*q7S=7Roe3s? zbg=ZL(wEq2hzDcPE1w=LJ;!!djFtF|h&6!Q0rm&jArNo?F@_L_;&0BWr8|IO@M|p5 zV^z@OMSa^7_Ik3gs==b^kpd(=UXG#yyApH&grKsGYS>(CXI*eP5|0)*5;5XqlEGv) z>GAT5Uhjg%i|r)ZqCAxW=_qVL;vCo@d{ur$1HGvFS~T1cs1i7rfLDhc3FNwt#^9_X z`3W{;p$@^_j3^24E}?yX_{*-JGFZvcEqWTGQ3FhTSQW5DIvH?aGyF zk3DtFNc2_PSEc&;QuIYu!pDfmBKavGX=2$iW)X~27!K12bis%qj}Q|O76PUUm*Ff- zh(K=yW32f=f-Gtf8ik+mT7n?g`{Fb;KX*699YJse1^RPncoAwWVN!L?8DcsO|&<8t7Kdq z`Q9J`nkB+!vSBC#S1)l1?-teTmXcyN2z!u8TG~Z)8QW1+P4O3{b27q$os{tyrP<}z zx7OA-`w?YU^oCs3PI!_{W{^hEMU?qN`~?|#F(>0GzkJ~2VzhR7p{k1)r2?m6sBWH{_0ElUbM_IgNLK-IGf3H)siHZ*NlW8BqDLfvrrdWs4Q)9dtse@ zdgUjCVS;eqtTrRor(4+x+}wGcodNd|HfhW?)@zo&Kqz^^fH7$!vL>6cBDm6s!HHpl z#=MPK9r)$MtSMq*b3{&d=aeH*<1sr~L&)!RxEiuaV}1e(iF*QComGb3c$)@#%l813 zpfU5g?P{nz=baV?-BPtdTWz*ha}(MUGZoWM{SRhCnFzkYoX}SJUdUO7!Q6JDaqr(o zLb8vfcTx_Lc_9mdGtxeS>Lq@OQ_38%N{X~2GqXscyW%7GGs(zgkD-Vgl572IYkT7z zkYbx4!@3a-Yf@}N*%Eqw7JY+R{MNh>gF=GJk+TUtTB4p;&mta7RDt|*^%O%D@{~bW zj5rfJQ`?DTU`|A(F)!2;bd*BO#H?&*-40?SRIJPwWee=&%AG603XhI~c)|FF{nSOFGh!?# z$5_gC)e2iJoat~E2P2Di)sxrX1@%rZu%q~ai52n-sVc2aS;J)k-@p zd;{Wy3fO83T!q5&L-ERaY7XE@%u(n#W=fLr#fwEffiJ}Ja(e<+LE<| zAKks(g4^Amu2r=T-DK~?6Q#RO-ipICub*04fAsAZ{tmxK*q(*0z{wFf2t!Mmg~HS< z>`uZ0#bj`lsuhmsPTqG=(;VIR-t}1S__ab%HRvO3wh`Qv~V zG&_H|9c+aQBq1r93w9*CE!)muNoGLTzeVug92sfn5XkrE$Maj-qZVJPLz8<%)fWDT zYO|`pyy$C&v*cMl#O}-w#qaIxfR$|J=B6QX#Ts!(SZYHyqH|Va4G|3|{NW@V%W!qt zet-|{BU!&P7E4MthFhYdjup5s;)wu1vE>0W{6qMs6irp&xM52#`!HY%^9b?-BDCbe zxT3yEmE)D3l9RN7s6GvaZ1A$ap@)-g-y;2CG(Ru%Kn)<@5P3$(YF{3Ys4sm1mF*`z zWJN{{f4O};u>=p;jThsI!xA9IeMQin>M|XGoeaHWV?;bj0bXenCTp2cMTEYoihVET z)k=SXLAtLHE$8)bgCWbk^CZ^uo50^ynC}X|!3)9CL!8!NHBV)%i$OWY;Q<)FNR5Mo z4G0$|PZum+RFegqHeo^SJ!b+lN01IFab2NDZcAX#&JK1aZhOSX=S_p1CPXYFPML>S z{t1QZBuJ+dieKX3Gqtx4c6JWlTKmkwgbd#yxGnlb7U3qvWdPWihk${mv|%2t;aZ_f zErt@qWwkU`(l?~sxh#bEA_&UDvxt>Oe1dPg3>+>wAcoRtAd+J3N%#cL(0DFAuU26n zES^bVhJ{)vSfFOi9XS8Yx-}iIfApF2kMsF8>z+9uIQIDYXFmEm@P_a}#%Khw&JNO3 z7{ZQ{X%IssbOJEqkCBHx!uFCK4rEXK<44fI@&%>k_5|L9(4Jeg2hEx^JvcAZChO9L zXUGK8BgJV18%zJ^ca5CMmp}G1PyqzQqs0E2t*dmW%(5p;&en#281ton$6v&pbEmcw=4n?au4S-Sy0OJ!_)R437?}-km!s`%H9AALC89lE}Q4u=a{lsF?svCed+$tOaa z7j01y!_E-)lp}n->@^&SN_b&c_#Gi1sao0GfB+13L7b4F;FcvjFxlAyXuB3Cz*OnS zLFh&Xup&LLHOAWIaWJ;Gp|13!8P;+CbFV)7;c4bB?f;u|8Jq=COLwx){kM8wdEn7k zcQE%~oIlrf&ql+pbLmMzUxg2m>^jTN?ub3@vBo@-2+8o<8-?zdFfJ=@giXjUz22DTppvsdH%LW6F|Deg9C$UdSM+ zp7x>W(CDkBH(v!RK|E#3)|M^z&|%-f{gIZfE&V6Q9)0!IN5@WzQ~pb9rV1&%>T3ZX z`D6q>&~aZGYfl21IG+XS6HKNw`!b@b?0XiT-D4M*6e4FY{oGzG+F64gv%yqkd`1Ny zq8KZR&sg-iQhbIXD9|A=I$A3-(&ZcZ!(Y^Fjs_FH{2%G9mVVYK`jKbF20-6h3|u3L3WtCZ?%+>khd2<9P#On9qR?tn zD3Q`R#3ncc!J<>KUS1s7Jz#gM>M!5}2?cAq2L`%pf+4FV@C#LS+sik_1<$|B-OC^4 zc~K&91~DqX1|25-$#%9k?h?EXv{($)X`)ya*weB@HV~>Po#eq8OdMbMCb%Whq zt->d?0gkZ?msD9O$U4ug~o53-O@Y zXY)D(L1$-uYkOUfV_X05!g^AJDrjj7EYO>jJw!`)Ub{9IZ>u7C6|__a{914>6a(r- zAdQtqM)(Y;zq%x0Tq$!HCGA(#kukJu`aN5E8$&hQ_ie8UH4b#7DV(;!5I-P$_+G5Y zv(FmA!*rt@$D7<<)0J}cuUXUYXkB@&h#z*4P$JCDMPmANCCx6lGA+BR*!x7Igsq!& zng~K&B|pbm9V?97=_G<(fuzEJJcu|49L9g*%a%Z~Sl_EX^8~_w^k+V=>UyvC#KSEs z5Zw;m{_<-o@%`vaFGcm&URL$!^UuTMWXKPK-uM^!eL^_$094|_*&whq>dvr}r|-VI zbncGvV~A$?O@8#qvtM}oZA8yf*&c}1D4`gv zO6G7O=P!87;&V8M?59KS=?E0SB7G~Uo{)jDpY!ktmHUC9gJandKaOyhDJ8*2JWXR; zqFYsXfeG=kfY(_q&NzA!ra&#WB5#Wz{F=hdkYX#IW}QF$Nb#xCUqAgCix$6p@7Pfc z;v+vS{pj@5%=eUDdgHZwzpNjH=DZ{aRDohqOagFMYYO@(FbTNpO_-?tUXFIb(H1*E zM`hE5{t_FW*KdC6zu)uF&mYv!KO+?APQyexUwY}Kd;a@VH|r1n{Gn&gOJ%!kC>3&` zSjRA6;Sq9MnD&ZP`jJv3l(dveW`K|@a{7}r4HRZ4Ni8Pn6tPJ#k9QV@o%CYqoRF@? z1&?-$bD~@TlI#PuIM0a~cyE=U8=wl{QDu`X+%lOkp)WQl+y+~I0)nr{TS`MM@i?dG z!Hu`OJ#Re$k`3kjUKFk-)zFzjPXGpqjQ0<5BRHvT`n68n1WDt$)8LXx794u=Jl9inhOTl zy4*tU3>eu#sT3Fv|_Nmk$>MddiLLcl?ftEQR)K?w&D2nwZuD7ZAh`NI%oX?s8k zMEAs_A-z8f?rCt%O1ysWHp@C9+BVuO+wo}IE^kwuTNAvv^5k5M&d#;BEuEgT8fWL0 z9aW)2tK^1}=hl|eE&K$b(ZW&u=HSjE^TXmVpU0gy%4kL=MS`L6Q%MJjmI&Jc^M!YV0ahT)5@ za9#<`svH+wRt?I;;PUeFb@@K~un?<%EPlC1B&DB=kR@r1F@m%gzFk>ER!6uB6>bv0 zWamU)Sd3)3EctQeU6GgcQ{XzSTRrG!5QiMChEIC=GQpYzT>vrtt^61r^j~-gzuVb` zAFm8Gt!h#=l(bPf|8ICxfYb;QiA3f8HDUKtEU^)LXy>qjibDbva|2t8qkJY%y!_+> zo&3h>Kcexv;0qLkSc@^b5Q8Z62^{^lvUdE$vSn);tt0S$=Tk_x-d*aFu!0Ro-Y9Op zM;sS`p0Y&W%WI9jRbE%@t+Ie$Zn?Z(pg^bE9+ zJX1I?X2i=u$_Bkf#13LZ;3nn>0eJ#+fP`L91YozIt)D|_xuBB&(Hm_1fDOI8MxOB( zGCOz#C^sFg!x=PeGCKZ1Co<gp2|!4jrbaSO6X!>?9ULbX+xTXvAmyQl}9%v~VI= z3!M8u(_J*DN5n14CUSX+?wpH_?oUJJiCINd(OXJh+ks_BR}#7t1V)I&!e15kkn~O@ot<>Ic)hij70o`d z$5cbTGh8|yZ?ffvN{0daPq(P5rQP=gIt%$7Pi?-Yg`I4&9r$qRpXgL5=4R-lEwC5Z z&PKGL;Guw-I3Xv6FR~bjNJXixr6V{?EQ}zK$$_4FBGB5oLYR=u#~x_PWUkePBgr`}zS=;U4%-t?Dj4?Q=CpUG}+675F7%!W>pkV-far zsGNdN2rIgXFUF}%kaB517sm6;&K|lz0Wlx9i0PzofhBucDgzcs`!|g>Tuce$Fc-)k zK!Nqpt_MFS-1Q(hI@u3M8X?0O+3IDm2HU%sVg<_U2YyKyZ9D6$#d$%&>K6MTM2V(V za47Nq3y5op{f}XPEUYJ0mqZ+5Rbxjf%)C+$0ZvpyN{nDm*z3`@P@M;xMetFn;L>IZ z8wblNZ?4Fbzl#nlzhLK+A}Re?Cc^K7lh&nXoMQed0&rwnBu$v~U^qVr|Ce~Aq&Fl{ zc0(%yk6aOtwY4-g7(9i}m(#l)psZmmBE>jlN=z9d8Rnlx%+s>8>a4xUr|?sHlYYdg ziWn^jq5W)?{KY6=#%omY)$MzrwCg%u(OG$<7^6WG0VjHA1-*3wa0)m1-DC^^oXB*6 zcMc$4h(@p+R+VrgF-XFSr3H|T1Q-khK^aaGJmqVG5z!q<>q&nRbO&)SkbB{)kHpAo z1eq88W)k$;6=L{^0e~qsM8N=XGo90gXe+{vmUIJpZ$KMpV;hdp3Y!M)_ZXCNyrKj& z0S4;`oiNA_(IJf}y-Idn{9nm!^>p9}5`n8g}>V zUrayz^{+gV{$l?8bb55puFaX}3@zx6u|0dn?kJrb+O=ZEu3wh*9|1d+{9F_%XFJ>6 zAZ!`*IyQe&kWexolH3mqGT90gLz3Vz%{5t^R3F>l)mM6}Dc=;rzVSX*dQr#$(5P?| z5hVt(sSYrJlWqR{?Xxg96*D6-wK{Y7L#b~VfIer zzOlAP7Mk|$iayeI{Y>M+!^!Xd6GQO!KQ+xrrT&F?_WiQxm?Z??tp^etdbtAaLlWc)xcYL#)OVvH1n*7eUFBOS(lA7c~Y z2IQT6?~!HXyAD|W6W!IHsK42@>i;O!z%+c8z28&0^cmqjR^UAl_=pNvLsh%<8D&)c z7}Zx><*HKN`22)XY&|}#it4`i7q*Ufty6iA@|D*VYWQAlm+O|(%KGK9_j;b{S3Xl& zm!5w=ZB#zQ&Z#x4Blyo$o9;7x(e%Ge z@0jD}A@g4Ilja{g{GwTJL#a3tQvK_O{*O0kr>aOb1>I2meR$p|~I<9pbbUfuaS7WJ}sJXx9$(nD~{GGGS zdDMBz`JD5I&XOzR+UnZp`k3n}*Ppp9?wotK`>6XQP) z-Rt!o^{eV9>OWfl#rhxAml{?z9BBAz!}lBBY`D7XE3jegVp>?=*qV+`US6knS)J0B4UWxp)&DplOZMN;nw(qoEY)`e{)Ba@p8&Okq zWAyRpUq(x@q1aUHSnS!@f9t60*w``K@k%EJ-V)#Zsd5032=w9NmwcF+>f1$LfnDs6 z7U}S?@}QAt@I3t&BTrEn|J%r`N*h~g=j5;%tTT#VU)}> zSRnqBk>{{x{8uBdDx=D;jJ!#yWj7mnv(m)wHS!iEz`m%A;1%36$|PR0O|RJ2lquyy z_}z|3p3V4bcq79>yq^0oUc;>^cZ-*CA3$!ScxCqyksijo!DdjFK>a?X9e~Xd{LLyW zVXIo9>@(_8D(m**rQiEd`yie>f_D}vBZp@ukId-W)Q7a~y_zD2wHmLmtW zjfV~%*?8#i{uwRN+oyFLIC5lm<%$*iP`Zywd+*%WdvN9m+NgNf_%+jq4q`=?y>I*$ zl-)9|yywVQV)R$ObX>zcG`v@-2X?m}%(4&p6dGDKu$9`bgGX*Ta{G+ludUSjd$K)= zzJAoYvN>h3qVnEvK;J!c_|97n9n|`J@uw+(-YnpC5Mx+2u|u;n2Ybr1lh~+SdI00R z+UKVz#3^9LnaWIfqmu>pDjVJySH-H8^~wf7XA>~z8s=a%piM63Mzm5b^D-avvjFTs zb*!E>uttV}2*j(kFb(lct$6=T8*67#7GoWF{c9KNhW)Gu@x&`wAKvbapb3^@X_kSM zpJM}TB~B-)0?GVe8ojwvlaOqwE^C880lpmR-lTvTbZT+rh@z^=v2G z#dfm~usj=QH?TeIMs^e1%Wh^9Y!dWyn(1tY?PL4d0d@=2t}A7qEw zo$Ls^iydWmvt#T->>l=EcAVYI?qeTe_p{$&A4R=}~ryJ;px8{wBWs(+ak*ctXb`wIIiJIh{RUt?cq-(WAYKW6jnKeCtD%j}!%PuMH$ zPuaKFx7l~tcUh7BC-!ITd+ht{RrVVDbM`v>3-E^j%+9g@!hXnp#Qu`~m2xFed4C_r zX@~v(8>f@ z^K^!%vpk*S=>eXemG|%WfGs83cc(#vc`*}9Ovq_#!@obuBGd!E+*&NRf@a!bd zPVwwC&+0ro!?XK%u8-&Xc`m_oNuEpbT$<-HJeTFU9M28#+$7IU@!T}e={z^XbNl!} zA0O!F0|`Emkm zHOZ%@_|!C?()rX3pW4T#`}lM}pHA@UB%e<4=`^3t@aZg{&hhC1K0V2&r}*?VpVs;G z44>Y|^**lmb3MWJB-c}1PjfxP^(@zOTp!>FWY?#-KFwiu)Mto(FudR2RY_h7N?a=_ zyYd^xHEqk+73YpE1TKJCP=e1W%5egj8?mFeloRAV??P{s?&NM!x< zXm4a005N+Y6@X4bOM5s*w%T8^-qJ!;x^~iM&?WzC9lcfYveKkp=s=Nir4{<3RTUKQmsl*>#sPK=L_ zHx^j;_;{qCY|qb(kM|VRxVAwnnA#^XAoIxfe8C(UE?6SN82)&HP4pB@@d(DH>1WJS z!y4U@ofoP`3d+QWg4z{E>4Y?vVhesuxa#NFn9G7tZ|J7SUocRb(1oMDj4G0iE*kj zv0e<&7JuGat&D6K?g}pg+8$pH_$t{7>&6g9Fxv@j!->cwErNiO(nydjXpIFdYa3NKRZDLrPK=)_eZU*Udc=*J`nOaMC z;c$0jE5PK#+`QdA1%Lbuqci|GQyPq)Q7Ns9pD|HdA3tNJv>|@RLTO|CjFr-+_!%3e zq4*g)rOk1rP}BV{7)T2S(u@W)4204!2102o2102B1EI7H1EI7X1EDmEflwO5Kq&3N zKq&2uYpVpFcf~P(_k=crMVO#Pn?zdZB&6z&7rMF&UDz&hVCp8I)K&LOWHJ{aI`y74 zfG<6Tp2am_fkM2i!2Epz%Dt6PS$=CpTuX~__Mr~jaOHLd6}alKs9XtrRnXe?Ly_E> z70i#B^kd!_=v5z?0M<_CdJ2hnZ*WylA^F>?0>h?JJ%y!E0_|F_wuyEoKzPlG6PqHN zKne1o*PwUUu1SVSN%Wrv2?+rE@h_?r>?7SXCwe2Aw(11h$}HX1dSx306WT;AtuR5G zdF_t;SGcBXjbFhF!5hYhiNM)FDA6B!jBLc#!YVG`C)m`iTT*d8GNDHb>d2%H8pB5> z8~6r`3`8wzXbaTZbVmBMRJYd ziuDeU8)Fc$e~xpta2BEhJE9 zQ@oHuGD=X}0Jv%!!L!P6x+YHOSQrIZH^-k>ly%5#L55N0+W7NKlw605DA`JNhH+~f z)uGIGszaF_REIKSRA&g8>!}W9c2XV6?4ml9*-drUBJ%;NLzz6)q0Bhdq09|bX9Sr& zREIJ*QXR_NM0F^$m+GuR=4PrxnF*>xnMtZcnW=aoy9nlKx+n~ySQoif$ju0RLh))` z?28w2i?#RDg{XZ%vdqYRqR@Tr+G9AMsVLf0GmB@H{k&9( z$MeMEdX%D4)$7*{jm=ME&&yC9P z5Iif6Z;~z1Ves>XqTo5s;51bGZ?#U*(Z8WluQScPTCKR04^gV`*3_0;xaw6`H2dQAVS%Dq4X|gY2a8zpT7?rYl=nrE^r*8M62n6<51-) zbynb5S0dELz_CRMSC3!?)zGWZ6^+q6Rmd)Y*8ZBUCJ<}6r;#h%J5x)=g(6r@tvg%QbyuGN*SfhP>NBf2*-2qU8YRMQ6|b} z;F$KM%Hy~<3adCsiN(GjYLsD{siZ5nVVe@DOMA2KAY~Rx2cd;R)a$P(!%7Qt%L)sk z@+zaU28|pPHEKq2X;IXiqOz$`nZ+~8GK)(eFN}&G6dToVYFXLL^xJNmg3>8eI%w9E zK{E==(8dTQUv@MLhxx@buqz6b&|WD*SrPXC?#a{f^yB2XXq?mKjKrag%Hx!QN(%nt zF~&G05e;>Du=J>LGs=p}rWY2(MWsi@4NMsr9~*~Smp7+esHiC8(M2gHqewnEbuuXM zABBsBrL&5PXGFyf!iMu=%xEE=ZeZ7e70)c3F)%nfq6_oCcYtzkr`1MTZzU9?0QF*CfW*)7K1+6`zJgVd<6P3we@&Yj6RAm~7d6y!czsZgF& zo>Jy1)yhJMn59aMvO;-UaVvGov&t%^L0PM;S2ie{lr73OrAgVTJg4k}8rZA6r0iE( zl>^Ev%3XlkfxQ4KXr?WRVk*Q!0#o@%6eoqB`XTXm>W>P>32 z+E?wT#;CWdgVb0xUQJY!)l@ZIyIlaY3g)!hB{L%Rm;@bYK8iw`jk3PtyUMRi`AuSjk-d8T6L>+>a*%9 zwLx90u2(mxo764pHnmCJslK58mwHYWaq$U>Ny#axX>qY}adGi+32}*WNpZ<>DRHTB zX>qx6d2#u11#yLOQ{rReWO4N=iyn=sX$fhGX-R3xX(?%`X=!P> zX?bb+X$5J8X;X4zbK`R3a}#nCbCYtDb5n9tbJKEjbMtcZa|?2(lt(<>luU@)VRFGVdQjl7ZR*+keSCC&&P*5m^=>NN#xgfg(Dn?P4flQWzP#8$% z84yb?u*F@_s&^~*fCcYWSAuxzK|ZTNKx;rk>p(<}Aft^Sq|G3utstiDAg3K5sAly! z^?7v{2y3^xN8PKwsJ^7`Q}?SaYODIPdO$s>zM>vd538@Luc>Y7Z`9XSkNSpsL_Mm$ zsUB0`Qr}kJQQuYHQ{PuVP>-u8)DP8@>TlKGsi)MB)ZeQgtA9}csD7e;s{Tp+O#NIv zt$v}NQU9#|Mg3C!O8r{>M*XY$t@@q%H}&soJ4pKxB9cDXsV`ZAzG-WYZlE4Bz2V*riE+Ww5zoU?HcV`t-IDkvuQmwyB4YS z(yr64*KW{m)Ou^b(j1yoi_-dNH)%I((b_FqU(KcU)B0;M+5qiVZJ;(tsnc%LVzoFe zUQ5stwInTBOVLubG%Z~ltlh3dEbSp}v^GW?tBupfYY%IWXxZAM+GARdHbI-HoFTb;Go)k{B$pqOQiQUI{pWUN>k4Jhe?yuQ9y1MILy6)TSM_%7{{hw|abi?Qy z=H2k}jrZO-{>I09NA}L>eYm&(S2zD^!LR_Y|9CP@b8P0uCiBZ3fs*P%i`a_?% zK1=)TxoO?a%cJK;ABz6*maA^L_m+jXeAxH;zLWcY?YhzRtZS#M#r37@d_Q}?n11*4 z%kHlsJ}nvp_nZLZXJ*{fZuxmt!r=nao__3rwyzhCR}d2C)`j zc8l85!WXxMv_$fce9w!IEG_;8c3(DM?9aAFFfY%cKeZ#v8`AR(_jF|0qr&{rBFFCX zN4tE{E-TOBG5Rl6Y)3_rBVsuInb#N1nAac8^ax+OSM}BKoDhB%EsAj>4%;~H;Gx(Y zv=^bm;moGyMGm^iaWU4Wb5!K0=#UNI!9slFJKcYI{Yx6Wct7)+9}FzCPuTe^Jm*d3 z?!p|ryKlZG4Equu8(^0 z?rlSuA(};~{m#1{?aPFPl|EBeJImnj@lxGq@a}dI;Sc9Cm|p)v{cg6Gotymk%u|Mc zy7<^GhKcU_5uyJpiT5ls4)XE#cSW|&uV2IUKfKRXBjVha*(#PUgy(d$+Wj>m$I4d< z4`Z7;5EM zsp7?2%zL4^P*jl{qh=Ytxrf@jykoN_o{btrMf%nwxW}tKq7JM~CNHu}0 zz8bok{tiZ;8fKh2rH^}~=nw2PJH6-B8*doC z#ivk3e`DO9VJwxU7Tq~+oN;QHe(Kc0vy5x_oAi%iprZ^CWq#m9}4 zr}WB=3wE$(*1US##*GFq`kg)VZhd3r>M~Z$iWihrRvIUV=`X&x&BKncBW15W{-O~v zXv=J0v@cp^zG!o{`-Zvv<#r}c;c;DzpVEI_J#EocHkB3CPj4_V6k>n*Z4TTO<_bN| z-k$y1RKuU*Ptm8oHv4UMobhyi1GaQ#@EXzGzW32Bqu2;0(!~wf(s4Ly%cFa#Ihsc) zr$WHZ=d(Imz2~zqhrZ}YS`lB3l~xanOr$4e8b~TIogqC_eSNS%^H$7Tys+93^TZy} zlQ9>T$*<{^ja3^RzUM3(8yhz|eVW%RdRk}h7E^iM@@J}7EvTEf!f=b8b{;K;h*qXA zK`;HnxF@n-ScDhS&f5cn#1mi%ZQrf}9WAM;S>p76YF*;4S?TDw!?M!tUg_jxthVp* z{1)4{EASMn^oQx;R2^bgI}c34*6?`!(P0# ztl9Alt9|+zX0(YumW5A>5HW2+Mpa2=5u3mY))($5*-^6Zsr}6Gt+MQ6FE;LIGTfFO zJJ#=G``Ig%d#iR#_(X*8X$vunL@#K{Y zbjIEj*Brgc@Q=3~{oy@+4P(a2)r=<-&(m0>^blHHoY0)?=7$HS-J4fb`WSoI=xDXD z*Gpf`+mrU;!{4!g8C;9|T4)Z}`7Ha`S0)}g^2#em9424KfD2-{cH+db4wvt+HK>`K%$s#4xy7*gcJA45kR1*_qsVdDy%xHSZgILS)QiRT z!|4;lQ&WczPj!kIi}~mtk_H}AQh*{oBvb<85VYbA@#1<#jb5;5`t(HwMok6tAJ$V( z3_tDg9rpSUTZ+pu{a6C0@38N%g%-k*Ej$*N*9As{00u8gKEyEC`BrmW=%Axjk04o( z;(+e*e;J^{Z6+1^z7%cIV$xag2T_m5dx44|AzSU{u*4XvBw?|{TD-Nq+0l_@kq^U{ zfd1S|9AXS6Vd5)e9W)=9P(ez>e z|D(Mp*1c_@1u+C`u;{}%N7--K{)Rmpwrtq4dG%h<_15ZjbJxvnC}#zR*TRlfy*}k7 zW6DbpH$KFS2p4fKhEEa~M=7nV-AAt!w8;O=${bg&8;w<)CKsg8Y+5B_kmY2H)wOZ8J_ zN5*a&W;Cr?zm{+Eh3oFxr)!th8j}v{{tCatKJ=kcL!GSOxWvH|_Lm=?|0-mpi-%)# z{eINjL!A*z|M4Rb)ECV#^?*H7CgD+Nh1?as~4BgDxtwR>sTAp zS=lq?wX=vkQC8CR^Y>Au}aih*=HkItHXx+ZAW&0uHgQ+9ESW*Zn?U<=ujnkCB& z(Q8EUR{fLH8GNt^XZXty8K0&bGs;D;hSJ^DO$|*A4cHk&c&6@Nx4M2kGngA=*XH0v3OCrvg+U32OFpu^X_o z$mz%eO991t?Ed*(JM+!A`r9F#E^Qv?0PtPPsddTw0z4>t!kO3R^$nzvuw~1ZFEs{= zk-F`RTLR?T$0CKB|ADUT9h}uP3+}32US|yCxXZh|ZdonvvVGxy01p~u4Ppx? zNfC$5%g;t~?Q19oQ$67OYpyv_gq_0`8WV;k4E06(fi`^6rm&OR1gwMtf1t>eeP$JW zx7+D*2lTTXpoe*T@ONmSwpV*QhjIY&Xk?0hV75F^BU)`L+M$| zI<{d=?ONkAXcF5iwQHBInTuik(VxW%PoZG(`Z;T##BAh%|4oHB2MUq@e$JmDOA*W7xUFP+GDlEWOyOfdHL#%VFtLHk0aL>oqb=3`X9YY`oNX3ayTy}Zsyu&)T zp?aO8!(mz1(6G+g;RsYDE&_zY3Y*xHyS?}$bVpVV0nCA6*)9Nv(#HAvb2FM}?0kYi zbLrMu+sd{Ze1sKC1gPdAYY6LNT9%lVt686%g%6+rwJYzzsyFxXZMQJg`i zjEA>1&&LJb%i4H&^BP<^bt;>OuW7~==EZ&Un{i>-Dco1QM#mLBTe$5(CenhV#3OHp=L5aC?6+aMr34S)3pyq!n`I|KN;uEi=E{~*l}_Y? zw|TRz!IRU&Pk`XO0qVnvl)u@oHmkhi3YDriJKK5zY+wQ+@I4jPA1vm%*N78@?CxR8cq+BKU#(3LsX4^f) zG>K-4;n-%1nH+mQ6WefXGo2h4P&5-7aA25i;}BP9To@>_pPkKrwrbTP!0L9vNd-&N`?Qt~w@PCkx#I#DJdxMt8^pU`x z@YlfjlAJ--gRCp(UU~q*8q%p@e$z#AngELs$>U5wF2LIX*)TqXM87GSr6LUJITK?> z#lV=IUQ5v053aofMZtk*i9&mN>8LwdoFRY@xE6o}?CVi~NN+N-62Nvu9}qQib}^|N z@SNvcJF=iqZ6ALbVPt^NDw_;Snu&(u8e+Y7 z^yqt?*;aP%fzijS48D4#zHZs(QudUQE%g=H$ugfUbT4xo-=Q&9w551k)wZhUCC@YC zV-U#4mJi>2^FwEwm3=t*%@K`;Sp9)Mw{}hwTMtb^TFk-SmNjfuO>K=a(Cf9bJ+qt3 z8p|4sS3bdvAztV-npz-vpoRppD-y79fgN`x4K{!awaQ!&U3>*v8(r$ziCR6G;Vc zQo%dPn7DG9HG&5wB^4Fv)zzY2tYKn?A=3Db;zpi^?M7^A4#sDQdcLN*!4UWRM@k$> zgc}q&Cg_u9CCO3~V~{6=5Zw7zDMO`iEkLtGWRR`kSsE@T09G(fgTz`=5fQP~gr@sDLbk-_3w#{RMI7`&7 zBvd7|MP|ZB-I-|OTbZxBulu_r z_4?{f3)cos-nEN1ET}gIefPm}{n#<~_lJ&+ezQLtJ=z#Ca^Sa++fUZdhscIQVTDm+ z;kqcc^IoEtIEk$%zYg+_9Ihl3f@03J9l)66a42P%NZZQumxE8sAwUIsEIAcI&+ zfBq={%|F3k63}^>gP6x|+j60z0q;f2+ijQ{lB&#UF0l!WypaTU(7F|^WkX<0qS*w| z55g)-$DCw~95w>o-T;gy*^;m?O))r5;v~o)*>(>bI5`x$$F>EYTNuMOj~C$tJdS^S zS2q*%EFJ?$K}tBnnA993lR)4~whvZqT{AcT+}2I_L#(=L*&DN7Jw3Ejhh%9)?)jhj!j`R za~D4U#NMg>9#}r1Cgm^lPBP&3-OU#ng{Z_R|cOV%&mcy#+d>77?Q#$W&f(GnMyP8Tf4RaEVX>j3uFRiR3V)hy+ysmzPK&k!bBIG|ja0!VOiJ~lMb%F6g-Mpa_JH^E3v0uo`fA7d4F7z) zIAE==U)12}h_N)(*Ecx%fuO4s-oAjV({~u_Ai=LW4ggDnzdcFQ0?JDa5AU<2yllAi zy#&$WC6VkCb9p%!(KPL_TrLy5!{JPdDOgTsCB^{0$szZqG*{H)ak2>6Z{1Rj8BJ6C~CDa}~hN7;aFXc0O;4N=;fPz08;5m@5i ziEsIL{96hgwXq}6Rk7a)q(j8U3M5BdJeKT4jE#*L2EIDjP!x?JRgK4|Z<1k9#V#-0 zBv()h9j#Doh@Zg5la6s3ErWlYB&3Tx6R>8`8rgcCm-W0muySs5YU6b z9-iPi{v*!@f*}Yi(U7#>f|gsrfWyuV zzW@6=R}8lY;_R1%+et$ZotX9t_94E*B+o8*H>wbDc*=l$J4%#9I6%^q*X`EV*EF(5 zEZK#;0n?8IquhQwp>9+Unt}WVtog;bfH(`SDq^|@2M}oj>qyR!;j(2===ysgP0%#a zk~iqmHKV6ANhFDgP{GsC#rBLa^E=|43vSC0{yD8WwT`)xuO7pX>EbCj z0bpnE+B;2-_iJaZQT{Zz4%tz|n_7`81?p9m|ifZNpOY2LQ2 z*~zw7Y@JnW{CGt#y={xwkFZ7OXrxJwG&xR}3=&W%kvyl6Ri?eoA0r+M;g4bYU~$tj zS$Rv1eN0XMoL^5fCQs7mEvlZwo-!j9>)ED;`nATvgZiF5C!cN2+h6eX$ozZ*f-vTi zdYh>pglUZa$tR3=&-kRcdD_Ou>nm&Lu*wyN{~GbObcgC08BBElB;)9q&#Hdgv~%^2 z^;@?Z2M+3M>l-$+^=1&_DOORvXr3`?l3rAlxj3)2VE>8_T3XD;>+4rGvIeu>a<**6 zat0{3h%KmI1{iTr900zh6}Lw4Re$^L9~s^rwrbyLM1joVbsZW#^5w&tH0klBCC`*R z^Hc+4W~c+`lp^&{HdL%%w0_a1xotH@Tg`7bz5DJJ#%om8&ZYrlZE{4FJ^Pt^D@Tno z=j#e1Ut7QW(otVNvdKM9EDi#{r%E;4da z3rYY@xgnv*r*jx80S&pKRZSO-vdI!|FO{y|V5S#xy^!(6$2s3($JW2L!@aC-3A`T&8#Gq! zp1X}5Wrq&oYunu2RgH$rt1qivT({J{^R*3cGQ@R*Nnrl=P~k*sLI`(ayRb)ogHzlj z6l^y+DZoLlD+~p$JE<&#PDPUa(h4N&B!?rd1Ww0vrzXydpIEiL>fqi5z<`>#~JpNFmqun z5f=~?X&jw3Bp+;5TpT$&nBm?2@BdxH!gW|N#p(ao!8fo zLXo&N#*3-4{ls^HJ0~xgI*Co9a6FtfK`R}Or5skPOV|VDwS4h%Lr~t&MID{3+s-l3 zkE_Q|yDvF7_&PAPz;&-ug=a3-DyJwz6a8zG7U(d`Gp)B*{y&pcqwc{rZ zzKb{OEiE6c*k7=}VEF@6fCSuv=?fNAvIVObtY#ZmuQr}_fBjwN$pJC?V~?@hUw!P= z$3A7RzG}dER1-u71^XY_{0N{ojC{yJf*}%jdv!mO%iyCjZ4onAO45_~%NLD|BFZd6 zU5YW|wnx~c$7eqL%DA0FSqhs`Q?jIFQ}xD0TbXhCgc;!;{xzHqCxHqf9c29bL>!_& z7q9t>#Yy|*M@CH_vD~nIw6k!-1eR@#AhBg-uTMWXX{&MG;j&LEpFRnRR3hDKTMI@_ zM?Mu@n>hZ#>6t8(J-BP42bz~2v&Q63$Oj-}Esnx|!tpiGF1gmt9NaiWFg2$rggM-2 zX>uYHis6ET#>%*o{Fgp;;~pGZkj~QC(Ea1yq2!%5ZySU?S(s2f#N==t|Lua!95k+c zd0mYwe|IDbAsq^)8js1g+kSu)BqtKZ1!GuZ!Tt9cybbUN6x*b1RVf>=nr8e=LRKt&Am7KttP~DM?F&vG2p-}FU}x!0mZE{a z0y+pCnED4ZCH0T#x0AVyBoiq#K2xfzTf#(zh_)9_*VFGC4;NmD5mcTWN)+2T2)>Yq zy=m_og}WZecxk$RY{LG#*D;U19%UCIrnHz#6Cc$r_{%5T7Ti|E-ZdhQeU zec!zF*O&fktS#nM@IZ2G~apy$t%;kLyig^3mVL6kMkbky1 z8j_tAZ=ADwmU{_Xz~&pa=R_51Raw{?xO`VG*j~9AxlV5$IPm712PThpu;R)&3ue`r zb$J!)p&DCRW7vjoU$D8dnVD559~kW{W^*cMEm%^6Rzb2=qRL85x>p*uy4Bk^%2rX$ zF?#ak(awlx;gf-98;X#k!3?vI%pA&zvzHbc-uZg%j{5DJ@Y%KTI2`;hR&B1_ zTv=bnN?GdEvg}FOlSbah#8pPAx5>&*@7mUOu+!_^JXZmQeN-eaDEtz+Nc@ai#Kxhxw(7?33w)iF4OAd_@m(VASU zPsLh+d7rat}dTRi8YyGAhNs4ca*Owf`7*4 zwYY0|iWmdLm

=q+oq7+tRRgr-9Vc(Lh=j6D4m!A>yC8%GnaP7{>EZ zX-pf@FJa{XJP#(u2LqqMU@wxK*gp@RI%Nz)Cil1@MXAUql8E#os&k%ZryhS}tU+!w z>9z16Hz-^mcBo!f4A~8e2ds3 z&cO2VMT!&rgg+8S7IJraDbK`0mQqOhIZ?*T#B+fQ(sxP4LH{J`Bc%*8f;>BtVQ{e! z?6*NAV;&_i^dFY)R`P{8C~r8&YP#5-_90GjzqEF28zgpiOJ6Iw)*QB5DSygpgG{yB zZk5V|mftjmV1|4Q4$mtp%5$Riygfy&4&Qi7>z+NWPTpM_oIu;KH$9OqtH`B%_d#Xi zu`OSI`oVV)B~VecE;QLvrv%j>=h`zIF8faA!5Dkq8bRA2Xw7wp0| zUi26%dOmDSx1!w>qVJ!gTE-uk^z!tVr?-?JVux7E)|Yp^yz9Wh7SEr4Jb@@APd9d1 zMbFnok0Zk7F)CK+=d(hWu^G=!+dgf3VawD*_npb+S1sZ_41SnL1mdRViczLztKEF3 z!Ib}`@_+&{5ft7b#Q~Tk6R%(tfJ=IS(rhouxu=P?orJU2_7X)O=+z1^A9<{4N?-DN zaSYpC5~(>AvQrsrm5OW#xf5s_i8M`jg6vbe806et>4vWU2lEDM1T$!UNMA}z^0FmF zMw(ngB#XBe?a6bT*Doel#v@(hm(K|ANF0XD7}#52DdbEM6XwW6EFlhYf!2`_IsGAr zvGa+ozam?R3$rCC!tFwC2Qrgvan%FD=*%{&x^Eb=P-5)1Ta*D|9a)jKK0^kC+42=> z!JCzHQQ5XNa5v3R4B*o!1RQRh)*&ul)~p~hEY13>QZ8uFw9K*bA{r46zR1YGilP8F_Xw6bMUB{ z4;CDs1S?3Q6;{|NA_2}?dW}b5wRPSHF;xI_I5h~`2B1DD1<8UKP{`$JzJZMTV4ClF zdxo74!5bpjhT)YM_%rYZ7~V(lV3~t%8|1dh1#d&%i4>h}cnJaTJMb8p^betuO{5zL z1o;jlv?E_qKrldh*U40Gw^d^tw}c^n3fsim%$gQ%s(^QIQ^nuJxOFA#N_NcKQNN>p z?Q@HEEZR}PuV+n0)7B=EYY4fL7H*E_2bpux#>%y`<$94cG#jQ+(IETWl3T^N3N(49 zqM~$RF*9J(pS5mb8`suvG}u{wuvtQ5yz5Y0-qhqoEVgMszaCxgnD<;sy;0%TE0$Nz zTTp@f#3sDn1S{EB)9wx~0vMMN3Z%mwvqYr8Lfm}?tb4Hfz}$UC>=eDBxNZiUei_US zx`G_fv*(vKR~vi2)645iYfEd5l`=~}7kXD>N5rI9LaEHfJoi!C%B8pj=uHj9}Wg(wmndeUV#b|UDAV)Y&Z zfRy$@;tUobDOdRinxhwthKBi)BZr3hXG3D%73QCBCPktaP@{Cg$kd|1Jw2_ql-0Ot z$udfp9|N957A(C3;!BBKy7ZDV+im`GmsvHI=OFiW*NVsS4-%vC_eJy zTTzdDBV(;_45D;|S^ACD*6fX>x}8hWbuh2E(~wM`(hKNhXc!NRyo zCB2kHNuPxO&1q73Gmx4u91RKw6Fm!rdXM2r)4zR-YcKF{#=9{dI{n*GhUar#sJ|7x z_M@5s_;x!RR{lV~@kX+K`1#j2yv^Xnee%!~hUbj_!2Ub8Wym^|tUtgMYbt+(`gv9M z6U;IGHQog*HpD^Eq8Ajf5&H`^&w*HC*y=ZLHh3#Ps5e(Xk0d7!`xe>Mv`28RX1x&u zoK5JoyBiRUV%38yvizpm2 z(`yYEB?A6Pd)Dw<1@@8ZPlS>dUZ6=L}CXP~r@~)LaVY#s)J) zo#8U3?Yby7y=LlzEGJec1TR@UoFsD4XG~Jq87{8}EK#Y!!h`-!ywnizg$~0Jm5P{Q zr-HsuJ)Au5ofDNWv)RHg7}T8y=LF!F;r7dI=pdSgO2fvhukr{I zF&schP6Qb_z)6U2Ai|0#Fgpvr1W9T~+DG!)KqOE>;pBorgdm(U5`tM-PLz^82;3`? zE_fROig4+E^3U$76@0Tz-CYxG})-B(dRFjKX-BUq$#7z9)MuHBw*zX$1g|K;fJT9{{6r9$S+^-e2tDf zpZ{-d2kQp+o$Ck7{@t@t{m%Dvu1oj-Cv9}T=l|mPN__^)g8TotAN*om=eoZ%*3NbQ zljHxbonLxRD!=R+o>7(s_E)R}`s#dN=i|=LtG(8ByuVbh^F4H|{?PS4D*I3Gy|k_W f%X4~$E_2;^J#ifP;CI~=<%5iE_!YyhznS - - - - -Created by FontForge 20120731 at Tue Jul 1 20:39:22 2014 - By P.J. Onori -Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf b/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf deleted file mode 100644 index fab604866cd5e55ef4525ea22e420c411f510b01..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28028 zcmdtKd3;;feJ6U)xmZaM3$bwn2@oW}1>CTclqiYRLQA$5Y6)oBGM7s&UL;16CB=~) zH%=W_6UVZgVeQ0|xQgR(6Hfyvk(0O_J9V>Qn%H&oG)e0>@i>{hWS-onNvmm7eN1S+ zzjH1~P?8h3Z~l59fphM;=bq(ve&@HJt1v}T9Lj@=s?4rmzvGs>e#M?e$-DSAY}wuu zPnxz(DGIB>^~Cf&le3EBXMcd}6Zj5KA3GXEIX;t*;HP5m?7n+mKJ@c`Tz^VYD(~Jm zd1MylPFz2T)UxmH5A7ZOe}4HVio)j=WvpiZ%%sNt@lV$&%8rY;pWcrG(tJLATS5ef5?>;m=`T3U~TdOF!ucQC(+%tJ%mOWkhLq)lj+7BL_yl3W< z|K$8OuAf04Cua{GIr?|bL{U+0Z%`D&^z7l8*&pAf{=TBzgX+qM@uk@--(Pw5FDd=Y zzv;PiF*WcaJFOVej)kLlWmcx_K_#l7Hdl-))s-Jiaq+Wt?>bHS=G)5KZ>d2Pj^cL) zspv_s6cktVJbfGVdn<57wHg$I5=3giAFkhi>*`hfDp#)t<$c^@rlkfMM*)4yKjpoZ zm;e7O&j~k_zvW&)&a7B2n1DOHt25zBxS|PHxb6pE|LkYEcj28n_7e#qH3-ZzD|Xba zuyCr&LatB>-zH{GA;V(qa?!?47iYCXp*YJ<^ZA9f8oR8`&1u?oZB#99!|V;=FIv_H zHB=}yp=sKjTsBRN!=aeIVp3RFXLZmQUKG&EInIE&niKmm!2v$!20ko9;D~#VS11nc$`+=KtG~yf>$N>ebwp;yRE`v zGH}Jv)#<|c{rH;oR1LoSw#IV{&!ba4$LBE(`n=!v1WX7n_@h>+xl&r**uQ0L1!}B7 zt%+QDbF_1>eooBQh?%++pHi_R?rNvaVp0_&7C-Jcx2Da0VHnH(`yji@Q4AK*~y%C}@R$UciWpw&Fz=BN&REs|Hb5 z;$@}9KzIq9aGHV#O5h8E}wr4JV`QcE{(tKyortc-Ac zv8~hc$>PQ3trZG48duddZHX0S*S59PQlWs6zK{7a+O3K5cJSm-tA>$kafivtXzwF&by768I+`}rql(K|3%uZ`sLDML~eis`agzI^b!&%^)q#exy z{uPQ>X;RvWcC-W=e9lS}(GIuYlzx?4YHksgUImQXzoMzdf+Q*$Kg_9fyOSJZs$*<<+E(%oGdnwYpO{(HB(_-7zv zf{W|>&!PC0imz2WsU5X!4}vIr{4C;UXb`h{hi!c4o#Kn{u+t~=S@!wOPZV$8Jb5y& z2B{D?Kb}81xtV=Fdw=ovEV7czOS)@RtV$L75Hy$i0P=${%0+O6L9*X{n_ULtT`Uma zcpe2nR-kN&c4Mx7aJ`5UC-`?oL-n;aHU{{!w7-%2v5+p0DI98!q+H=t!kzY;Lk8jw z9$!4Yk|kTp^6XKUi`{*~_MqmmFZ`|Dqdj=ZUUQlSi+|q{2y_IPLnLaD+1c-X(xDa4 z*gYOQJE*Z**8?vU0$$A%qWMuB6`;a#{Ho zt(sfqBHoMjtCFy>n+Y~b9K*m+LKs3S=}r*hvY}^>Jv{vG+rtlQg~72wVC>ju4rR7% z$sGF3*uqQggM&0jfww#&+H;~s;H}GHHxf>{6Grf~aLOFbL^J-3H)Hl@=HhJ6PkvH7 z8{f2PZf?^i$TM?l@X8ZUUAdwcfOZf$EZYxWC7`sT-KIvruTtPDUw=L zK&%PU2IwJhOkYnG7;3ptY2dV;w43plfJ`Z{ovO3g_gK62-G8vEK~3AYZ{eI3GQtww z@naTIz&YGdTO;7iFb!-NY#O#Y?0Lu^g&BK5+2eYB9kt&Chy zfn`Q4M6*FP82LQSjArinLqVwK=$geu>6<*q=jB~2_&j$6Ca}PZ|3b3InB*GPsR8WC zdaR*a?n&0fd}iig5CvB;D?tY9&>S72HQ@i#6f+u&|KzB3ZAsgz*zsapcJtE*H?CND z(=BR1jTz0wKd7>$x43E@tfF{qbN1lV&EbE1ts7D9GGDu?OG5h7FYwkgf$VxLUl*#P#m;wC zHy9Wj9BCPLIK2U%W3wr4q*}&xM$b{3ll^&h&^+u5hcn=JN7hh-m1 zUgY!Eg_o@Ci6@G-`&Hk0cZbvNW=`vi*luVYA0ZEs-s1)rt%np7R@|$dpbgX{mqGDrvr8pyH$VUJ#p{eOwmGZp&nc8YPIm z*Gqe^tGyMQPwYJa8z?`>2;_3sX zzCdyw-DiScxfm(eg1j!u3zB9pwPDrk6lbXw+0Ifwq8%#>vD54{>7}xcq{~ehO9(P< zALw#-N2Ix$ldJ~$!4UT~G4MeLq#}SSf<4y5q~rirF2v3jJ*|iQU?^1886#}I!lG_d zy_LnY6<*bzuBw=0M&@l~+a$}X0^=JH6Hh1O9908c; zM24g{$zMn|S**+aX1^KBA#1BaN`;`eysqH2ZYzW2g4@MeR3kJH8QJdA7^F_c%u#cc zmXKPcMWmFrIxV;^*H-~nwrliPJmz0iUom!V^aVD&sCQ=N^)>B~OnXf`8B7acfS?sM zmz3BmqjPhm|D_g7CAdXH6XO%~$OS3Oav@MHWMv=`v3~r7K+uWp8xx>F#1a-+V=~Qv zF`Fvw#f$dJO~t?4#4h8)Ub%1#ziJRv9mOb#dp8scdT}K`RcWVwm*fsJ=wJ=-+Y5Wh zGJU7C+glS}pWhtmVI_r!+kTVJ|0Z8Nt2IYPTY8;k8V}vL`9e!*w5``x2K!p@dCP@J zqnH~wX@C(UGlzwx3v(o{l^9}fkQ-uq0ZwKx(D*cab^n>pe(Nic3yZ&MI5y^bY@=#m zChiT)6$*16H3+kob7x;&O`PP)cwb`d*sjCS9UuZw1#tWlj0FyOKb%#EBWezp zhTw;O0^xfl3+sJ9S}43FdcO5a0lN@{qts`ip!YX)1!5)OjlKwvrS4OW{UP*~#rX;) zLrhdQof|3+jUA&&@p;+iP!1Gv*WqPju2dQ^X0J`?3GTQb93RXd05g{0xYX{I58ra< zxsHL3+B2+|0JqcwWX>adoK4B}{xgMZ`yyPBV^*P;I)DpR6~ul(>sW%pJYe>Rqpbslp0X^vu63MFpo-IU6@N$SCoJNeMx8o)D97z!m@tlv(mI$ z_AG!vnmwd~S*c6Nr=`uUyzkPujZ5P;`h{gy@;nS%@0}F40_I7`LvmCU{JmdUsjOGF zD6ZA^jT?rC1_x4ou{Mulf>DEz2bSiv6fL2=39bdS7w9i&4y4JXSQw%|!el_I9Z4Q$ zDG01&A!rFgAP3Afg8NXMc4GO(m%!D$adxC5fK3AAxq__%vqFqG8iev2JRu*qp@Q62 zfsQZ1C?)F0siXs&TJQ_8rz^0}Objx#D+!&*3+C6HBEhQw1xxi?E8e|SfZ(UwmBEXM z-nk+5LH4QfkP#RTmL(%kiReXDqq~HZ*U&u@<+Kk8UVSa)6Kpn4BkiDNptUIDJ=SY@ zkBcBzYMiV{WwxV*=RsldIPBMY8zuXlUxEGF<1E?hVZYXuO{sF?wJ0zat_j%kx*L8!tfj+p%JQRk~3}w^rf?yJY zV*aWYrv`*%%l5>JXW1UopyOI`2*sdC8Wo|OnqPt!t+O9|CrR+?>x$HS#99MhC8K(2 ztxNDSC)1fhPHLFk45>^sQo2`KrV{UaMSyb7V^>v+&%V1B#*MK-)2&Wo$pGuMh#??- z+z~K1Z#9v)+g`idzW#bVq1{gMoUr|qNgVcP>@oPGNQ;2&gN*d=zAY>uP$%G?qB$?& znJS(q+O69ljM647X$7?cVnO&T+z#}dTz3P!v*_0-o^!(wrnZ&|G}6Dq_LPY(g6PNI zDl5^)A=|6O>OzmUsWc9Nn`{cOo`#dH{)|vzg>p(T)qv(28GVPgfc0(R^Y45C`{3jk z>T)^vff3@4BL`@XVqJxtWK=AQ4deCDx>mdFRTV_l$&Uk@0RAA#w-SjGUnp%cc6wng zBttUz3)V#z9g-ypia;Rj1pHGUpea|MCNrcm2%6F;>`Bn~;(lO%I2D0PEi9;hV_O|{aD zG1j=HZ0Bz@2u7Al4yhUFui#VCE=icjV$D@;{Qkf@_DBwYjSE z@S!s+2@6-AIdr(Qs<<)W9Xp22I@sW81Nda{lRBinMQvcmvc4D} zLItj=PwpZ>n%0P559kRR$zm|JUk0@#-)zO#%47#`7_zwdl2=Xt!c9Pe*D}}|AjerQ zSP+{a>434-Yiz}?7I-fQ38W)|0rEo`T{eJzko;$_w15_n{Aa|Ner3bK;auwcn7 zxeVbVCyG*_N#y3{=jP@k*ikeVv6rAH&cn8{Xj_C90qGUeiw7c17z>i|lF2F>$|NGG zFl^?G=caFSZhrNtCbr30Jnv@h&bMy;*x_A!?!5cO^i{?EZD*nOm1baR{Lbv5ag7`~ zoA1lsvs+u;qCND-)US|#M873|N!As}KR)pK63>MEvy5i~s2TlB_7w8{(;Aj&1IcNN zAM~-r$Nn{PC0fHWl|TF5vZ0hKf0u0d-g2pwEq|L_`u^ogj2cV2#AB?2SJ*2o0=ED* zL{5Nvli2|hJ;Dug8es@&;u^Geaw7soNFmp*NZ3jGRS(Qa0oVHAJ**PA7H>2(F}oq$ zOy-CoQ%U@a#>sm~*h2PD$fRlZM11<@b$u;XtI5A**Td^JeEhZzE|+R+?;gEHdq^0b z3Ki820dJ#Sa9chfO08aR_L^Y{2RpcEEkB)iT#W{No=m1waKkbWTZrM=(#$fcZch%=s7o$M7zP?Z2(a; zB$=R);Sl8umil$6&d!xy{U7 zTUQUS8Qxr6ke7R>^aAXYC7e;gu_0d=q+9}5vm3<^{F*cC(ti4K+YnD2cX6hz4P z!uKNNd&!H<2{pmgL?(!72E_9eo zSG~XB4RmEhJ~vdTc1F5Iz6)NG+)&>wj$`oJ3_5Pd}~f^(Nh*@hrj7 z1gjn9B;`XFAPDnS$e(eAGO&FCD06e{GT<^xUOjOsFK*CArCIO>xBjqf3eVHCV)IgC z)Cd(6FN(%!EKBsu49#*U_V2b0(dBldRNYQLU(#_1KMyUGDW*?jv_%{gXX~s6RWmv zu4+v?2YNR>)Xx2Z#@@bq#+n*kRaHjMTE^5$lUwb7HQaAh(-zfgc3OR~RF&doVs1y+ zYOwn~7HDPFBkNgnMPpjER{0JDeIo;&8ne5-(Gd%^RaRHkR(Sm;V`Y`On!E3*XtG(D zN%d5jDt&6Cd~JwZQ#_fJ-TjR0kx*c~A^yrF#gUQwv1DUFM*E(|dMFi}xyUNZGLT0Id4ixx*U!xSYmhON8Q9@Isb_MOI zQfk3JD!$fO=e3)Nzajpi%y{b(9$e{YDJi0EKIaBSdfpp=|29`w<6gMa%?EXb(p|hj z1d45PlmE8(mfL+nS0HtI1^h{XUeyu3f_MXOgizX{x1_`sI)|1btjHi?WVtC_kpmw- zwit{nag?!sX^y-0lUF8{0{=MR_U%(oxug#5u4*_^P~05cHzr zYmrc$uR`El99|uAB#`Sm5{0vh#o}=cSo9X ziN3x>U{y!QDt1I90Tl4u>VbjPC!RT>C)$dwE0VpvN%|ry;iJc6k^JP7G_m9uGYQ5i z42LNMx?n_*M~Dds3jtGw%WxJZM4&fb^Xc-Z&@90ZE#n}xH|H^K?F2PgiU8cPzG*X;t<{~s@Ewc#f%^JAcM5Di|8`8 zt)i0RFNzmsgatb-<1vb}%dhXOu5I)p%B$7pyVM&>MF{e|PB~fa2F@KDSj3l;*s{#GqTM7HF%D=1OirTVkeS`pN&nEGQGf zH<%OJD%}g%OE8$*N;K~M+ek?Ek@QZ=K{797A#g_8M^L@QFL6qlBUVX~c4TH2DRftS z1b-$Ond~tXaYJ&gcXf4ltPN6Z17uhyqG1h+MJQWB&(EN5FpJ-r7h+IAP&slo!ADEf z^Tt`kgNZ7TUv8XYs6w97>53j_Vr6P8kqpd!*b?5bt9S~%0;F7}5P?W(7@-wX9l%d=znfr%CJ4UDvf z0&J@Ey?1+whJ!}P_Nt|w7QO*-LIrHK39dq6`Js5_95n~<#OEk<95W@!_{x=n7RMK2 zd8s`CD?jlZ8z-IvKWGYV0Z@q$6U`BC@J7k43WpDZLn-k5GBQOQAcsyg#4r*Ipio9c zP+$$N7F9%~gOi2PZd0A$HRN;fm=U9+Z&pMvM508voY3C|NIgC}UlXe^X}0PW9j;EB zW;EY2{`hNb&z+~i*UqTH*B;-s)r8xfu8tMeHqBsd#}mbSPv42dG;f?)T7UHI6#fpc zOW2-;t-#I^I0!>aiG{+{EbLCg0>xx-lp4&R%$|PWU@&Owy#L-OvL|mAf~roRAr4^Y z_z~mXO}wZx+En9mn8_apw4m8}L#<#dTp$Ta(Oj@2*=@;o21_yny8b=XdlV?<*`^&veDfVWp&KJeGyLt_=znKkl`P~Kc#4@ z499g_ddY_YQ55{%%4XPZk^pu>Y4Mg>6C}e||^>sa*Z2KnZ52N|HnG0$F z`G&|dLRS0Ictm~a3n*_t;UX(CV)#q#-_~f>Ap_1oY%e$hAj8a(^$`M0)JOvzCB)@7lNe+IIY1- zo=lq;gL3r412BA%8V3g(5H3WXE?B&%CiB@X!h+g;(Ew(SARSWTIs%W~6~~^P9c+)^ z^_Yjx8wT4Ah*(CPG7k;>8HMV^Nv9KvU;N;6)priIw-4S~{oKL04BsKRE&4jp z09c=gfI(1c!91En)k2qA3?+ukYH6&bZ%DawSqSkJ5R`@I5i5=O1kY9(I9#+r45iUP zB*og3@Clru@mxKxR$w12o=IT3g<2?Bpk~bJyY$?eRc&v4^tnq<^7&P3p1b5b@#LlF zKKcgmhVVezd;C~u8|f(wVMmD+h#?X>0T}j1$-^FId&mw4vM2uWBWPghg3?lZ0&fCn z&neo2W=)zNoR=wsdFjG6WPs_B;xzpA#sBsDdd}d?wo2 zxy~oXeDy!@moVoT`iN2=iZp{$KdYD@q7d+772=l>3u#7Jq#sw@4>KUdK*s*)*};K< zD=qs*TPD`sYBt+z%vTy%Ah5Hscqz^j$umjo(RKH4{n;~HnGa{`Ag*0*8Qs@1xo!{K z>rTr*H*RZ0%vka7lBW~Nr0s*K`pnO^GN+^oa?hy3My}H&3Nk`qUpOUBgK5&b3{E6+ z1b$sN1C6!8lia9u5RHvA)p}i3A|8Yh5rQ&ArxZ2i&@$Pmg~)GS)XhrwQ{d@{8!^!554>LAvO5K>rXuKdhv6bW;n7<)3zPK z9EB}PoDri~XFAj55uweCwy3afX9&4U5x#ErIu1m|-LNbCo{*2!V9DHo01S3noRFa4 zmL)qd+1Y()yBa6JRO!b-=tdf_B0aA;%39@dFt(?zrud^7*7o2FuRZ?ZY33~M`@4&2 zoCQ&fM_Bv5JKe87^!RJrnDehLUF^7Ty>8dJ`m~_0!iPw9on>ct#GZDUqb^B=WcclE zLQ5i36wFmZR>(p~#lDuOb@Vej1qc+vdV-@T(1@19Uc_KX*q1^@T3xM+_Gpm*MLTjc z2(jGH%jq^$TTovd-6P$T4r}T*LK2IFu@GcS@Ed6>R7H$mjpV0v3QWbukrt99M3;=z zIfCS4%8*R`;85Eh$RNqC)}hGI=xfEdUIQvYJY~w}rcL+JVc)@h;ik<^eW%ABf9X5yRtP?g%n=#HJ^ukG6EmyxUY=0CxJ|y&w}&`CR3b!1<_R2-3!m}wu(y%k+T+m zZY>n7tj>zrP}_RkjV>F=*m{c3SoFD4e1=87T0&n67J{Z=6Q)_163G85zB0H_ z(Au8}+P-+khxyz%%_9z{L=g$8nz%U7zo^<6@lATSdmFMx z=dG$^7oYz?@vE($YK=UsHGF;dO)NW7{HKxJpJ>gdK2|UKk!QvFLEoBmTqB7Jhkz08 z;EiX7I1r9d8V5om&}x$?k_S_^Uem`#Y=r0kg^X z3srSmOE<*@&%MXpYait~Q35z~@=dZ|1J0yBSuS+P9D>(@7K@?U4HT;ads=450zws` zlRP+siGytb_CG(cX0WrP*tznTr1iQwGKO|lpKDWheV}UV-mO)E z`u?^Qh11sQ;s<08&r4-__E|l6m~NEfcoSQzI+C`&Rjc}J%>y@!_+c9fCBocXAf``O z((HmO!?LTgy-zes*t$ul2_w{1@^hTkF~i86N+8%3NGkltgNSp$Vf?4QZ1NQfwcWwz zoJS=im`4^#ef% z$Fjp-9N{ieN`jAgn#Q)oYbum#!N+`Vd!;zz=!zSB)!2%>C5-TE3Nu5Bt$3ET|L`M) zXNrIO?CUI2`11W@$1sSG{IK|=v(GZmGg|S@*YE$bb_|;Hk{nP0nn*DTz};Yj-$Q{( zz+HFTK<#&Pvt}$20%^zDIukuy*M=p+L9mCer!h%P-&e-=Dcd zd-&&%Ja*|rBpHlgj|u+pQLG^Fgs0ZF-fP0 zO@ev6y&&wQSBe*fbS*A;q+Og71>FE3$v#kx^PGr*cUK6y0jdBVRWixKEt3ur`eK8^ zZLsMlAoyCWsW{XWi*bq`Tz|LI_4ZRB*-*~!M`06>G@)GEH8S_T(q2FxHq1xZ-*MKR z+Dd|UN{^ZLE``^G0$t{$BoUA^*&jm(}czG*v{jdvpQ*XlUZ*!1?F zZ|g~=dbWN0t)|8!3%Btt_g#2mV@s1UYkEa`}7TW_;u$D?h#yiIX# zP2f=Z$+;+Ci{KMi885SW&_!riG61xao5WJRr(K1GuPAc@k!@df< z3%=;Jt5;-`y)a9{Dk)=z;fpSFUJ1>r6c=1l4NAn|+VawM=|20g5UYPIez{8|#h;6i zC25S&gR~dEU0y?0N4N?VZVr2W9e@7{jA2)adP41?rJgqjDNB!`AOM`^3=%+y;A7fL%L+^HAY0{O1?gW7mBC+sS zg;MolS0cwW+7k1NNA#tF?!UXJZYP>`?JAVE^eRRW-GGoGzksjj8MI7=*yAdty{o?6`3 z+}LcNSuA^;WQ5+|)84wapH#SqzEiC_i_dx- zjS+`+ZbKP<$(S&knbTN=Jsm2i;1j}%F5-)EDifq!+RugY{F<|e4p2bM$0=euDO_O5 zUY1OQ1=9XaVGS2k!Z^$YvIkILEwt;w&k1)u2#!Yf1CmC_a7MOz8LYwfET&k2()xj4 z5=L7tc&c$;P_VkiJ_u1FDHR+_y#E5?T72IV*dGgPN!2A0hgj9vF$yy;*F&)9Dj_9? zF(>TxNK2r`h0P-Ps8n!ivxM}6<&-y;<;mYghm~Kn@=1{te=HN>_rXc)Vk1s5{}cf@ zGA)oMOnNY!AB6u)JW|pdk|;Z&6@f?g#G)-t4RtzCq4VYRZU-o97>h_T4w({DhDe6_ zrx5eBEUma;E$}J)6yKsBF{%Pa3qokUP$7RY%2)6j6?`@8ZYb@VMptxJ9x2AC(?r0D z-dRC!odBFd4PGZ10{|y7UErMqh!>&}EQeJ&+(-^8dK4Ji1iVaXO0NhL$H6hxHaHA#NfZiL> z0@~PuBecS%LHj)lr5vv)0Zo9xI!q@FGDCDoBSNoIAmYF_4-Y>~azSfk>LVYSQkx@n zHEVY6TvJn58|vr`*3ukF2(GC8qc_ghS~ZjFu20P^kE00*-yN+t;&?1_ zAL@M@ukB`etEERI*cM*gv-V3slWmsB; z*hOEK8nYN!M5Px6s4QY&04kWm!Y=nVt96?jFEJqLh)Ba?`@hECw1N}Yp?$x*s-k4u z6PkN8U5%Hfkq#gA>FyeK{EaWB9{u`P9!q^OcWF8`x_jrw^b5KcbkErC-DCF@FAnYO z>Dl?qlKvxLr;?wGBIPU>8ta5DgI>qxO$ZW7=0lSEVL>Kafuc(iJQ{RN7ADmv_I30Y z-)_h?1h8-1PZVDgasV_c+(bmm88%cvxwm2AvEJ{#OL$FRY15;&?SiL5a(5$gS(n{$yiNQiv|mJiq2XmbB6LtV%ZnFb z>e8>l6tQsyO~HCE`Z%MYC3qJ>TO<6Ou-m=2pHm1lh?%FL47`gAx(K)w!rD>^;rFx{ z_bvK84O?!7-}5`fZ*JRQcd04CA_RuK_IPd^Vor1)=su$*hNlmJHLdVl)RFQ1-KbT< znX)lb3|hy(c8qiw_kD~_gd31|_P38LE#Gy(YM<(?_)+Q($BO@@R07lRS@wQUc^A=0St)(r{b2RV>%P}q%j>+K{O@Y# zy~au9*WJSyMVX%7unzF6{JHXc`FO$4m(BOR>Xko3d7L#{_8gVH-)FCF>;L36jbRzA z%hwZm{o{l8$){wMTa^>algc-hpTqZfGn-lxVE@EzyqRbDX0Gx3_$T>`U}Med z4)vH?P=9H#8Fm>SFnrPQKMn61W5yxl9^=!-ADV)uoav`#pE+m#l=)}o%NCQR#?oOq zVVSeMX!*Y7rqtF@l3^cDs7b=m7|sWD<7`BVym{@Y&&Rs z#&)sFR5elcVAa!A->UitdyD;;{fzwu`w#6!N7}L3vDfi2$1{$-f2db8eJy$^Z|K7%jf zyV-Zx_oT1jd)MFWf3n6`^JL8%wQaR4YA0$xTKmP?AJi7>R@CjU`)b|y>)xunTyLvy zsb5jQqh70jp#JIlUo|KVS#Zz?8_qWr19br{@QJ`nfxm5RZd~1XTjQr1Uv2zlQ*+a? zrf&v^f+vD!gD(ev82nYJF?3t#Oz2yopElPu4>wOVpKAVU^Sj}i@agcY;h(nHTQ;`L zwmjYPot7)D$=3T?pKg6KVu-AdJQ?}xNHIDTor<1_J|F#WZ8dG{+h*HdZKuFn;+sEJ z_9GI3K3x2g4>MhPx5z87i~Y$W9UfL5*7FRWr~j(wDGKBN)$^*-!Ups_PD8RIdfuqm z*=O`T-k!r=g*3$sBoz}z$vlGv;=ky54r|8$t>;x`RQZ*jHz?KY4n1#F8rc1M-lX{0 z7nKp^Fy8h&sT{?xrUaEK)H#6sar_>|%!4>ja|q=}MS2+T z2Ae@y9QAvVwxPyR{LLx@uvPUad-b}M%DUak5tMeLg&EX?GCp#6X7cEa7M%J}aBKI* z?%4w(UQ9batSpXD>?kQfc>*z1;_Aj-rj5 zlxfismg1)ALkE!@&`T&)4xsD+(%&}n0gQg9m>13SZUK=#lu>z~(gnL)7iQUud=d>U z8`wZ_=fR@~j@~_^^#uoleO;NZcyAwSUEiFtSW!`Sp^L)+#sM*M>ZDu$261!d@R0+D z4hH+W@rUa}fanZH*R_0Nhh}FEc9mu)u~E7D5XO0<&reZ^Q^1Tfl^O6xCll;d7Q8X8 zf>kPOm34s524K!j%*Lufn;guEXr*fAW*+8cKG=b3SS_n#^$Y>PA9Iw!Sf-uimhgA*f1Mm zYuP%so^4>G>?XDmFD$;9-NH7rEo>{>#>Uuowu9|tyVwU{IODvpM#M>`C?% z`!xFudz$?R_F48h_6++Yc9wmfJUnc=!^5d1n*1oz7+3E^S%u4%ksW{ z-Z#nnrg+~p@6&kS4DZ{^$5T9>=J5=VXL-Dz$0vDwipQsUT;uT> z9^cCoy*$weuQE?0cp}LYDV|94M207_Jkie+lRPoS6Vp7Q@x%;I?B&T`p6uhvI8P>c zGRc!E1YPlDh9|Q;+0T=cJUPXa(>$s1f@<6PbJ`~=BX4XgXW~4Q;F%=PqgQ9Fd}@kMP4g*@PtEYDy?nZtPxtZZ zIG;}N=_H>{@#!?5&hY6hpYG?=lYDxLPfzn{jZe?;>AhU*w`~4l|1WJN*uYz)E%B3gjC&tIe>+`I0d_0_2w&rHW$Gh@sEVwS1 zH?&S-K*o`+xx6tvoHvDsG5qm7o9N0LVquIcsGT!T4F~Ct>^xsFl2<0y<<*W5N=JgH zf~U~(xn5)IscpH5t@V>*@|#un=G|;W9iN26)56 zlXFPd2MoSSKc1O1cJf5ZDb?O3z_inc)p6R#&A`I ztFF8Q%{T=}f`Gs@hMl*MOaxC&1oL(Ptt;=0ZQ7ALXVBJ;x8$p4!Y8`&uGpq+xlP+; zVSNbYZc$zxJEu5CcIM7G93y!)Ih=QN5`qG4htJvQrwTuL=EF*;ty^>F2x|eX;Zs;# z>b4^k#$%;?y}VD40PpGUIA*c|aRt$vF2nIrF6a%5O4FjRHJr-Oc@Vq02`8y|qBUpq9 zTC_=|`F298&RD*qGv9&j5(B1g07~6(zl0~VVWLyNwFdB|E8n%a2F#a_b>x}1S3tSD z94gCi^~8cHG0tApVe78nuAl-p92S);zOM>eyLKp?J=ep$m`NYzje*|qkqKb!WVS0G zk9GT3bmbGjt12*T8r73n3dPqN><(_Aoe2=$bn4WG@CHzV9OyOZ9ky$NAyN|kr$9n{ zz<&ITDtYTj=gg_@a4@*y6xvEJ-41rkHu46viCV$@1a0Qk+j3vwK{Z(a6}%9?P=mY~HN@&3D2JDSMB;$3hqQyx(+$sivU$77&VM~1hOELt5AbK}O zbQpwJ05n-qoVQ^227~Lv8>ll{t$qPAnt%>bWk;?%xB^U%Mywa2u_ch3T5)v~ZY{D^ zxlq?5*F;!f8H}+jKcJ6bq_i{>#CNX+Txlr>W8q*oL2W&#?uzm5bDhkCjkjX47^}Hd zymGNv)Gj@`tjPYLas1& zMK?By9OD`g3lQiEz|xCYmQXO-Y| zQ;g6tKMJsJjGb4MHOOp2hEe9`*m)*OZb3$rY^FNHxV44qP-ZLDq0Ba_LzywEGla}` zszaF_REIJ3CWBKf2?R|71YVQ|0s(nD@ zsOp`ueE(wAyXZnxy<6m{>OCSyRS(AU1B+D;(S@iwD{@rzgCa*&568X&|7J-t8t%+n zX7Xyw))T~Px)cc5g)s;q?2{nMQly?erx=GJFm%Y&vMl`uxQA7g=s8tcd#;5&vJJxG tBe`>`w)R|vu3oY{2>a6NN2Vb$p$g>T@pFo;#)kMsZl diff --git a/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.woff b/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.woff deleted file mode 100644 index f9309988aeab3868040d3b322658902098eba27f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14984 zcmZ8|b8seK(C!=Cwr#($lZ~BhY}>Y-jcwc5*vZBlYh&9^ZhqhW{ZvpRobEY2 zRim2jc2|&)0Du6#g(m`l^xtUf0|3Fv_;2t37YPYfIRF6U=Qof04SefskYWWDCf0Ax zvBgA?Sg zQ{3X4{N{ANb;56uL&kuESlGIFd~-hEx-kF%7M7U{z_qbA{?BgvJGPPkQ1m-q%+}E3 zdtHw2HU7t!7$h5R$XB`1U|?VZ2x4oEo(?{~<9cW^U`%1|L<`O49o%ya3Cchk?TQjvHN{6At8vTKtqH+gT24Lz@);yzA(}YXmPMtu?=J) zB`AsehXP=+al-fk06b49&+lmeAMwbpQMYtnkU%E5*g+%ehk}td81f)!!euyQg~T*2 z)@9npKco9a9KNs1`!r1D7wjizEmb+j<)@`LL%3o_S^DOxFhSl--hj14 zM#H5aHC`i!yXJ}d7a=RP@L93co8&-xe2dITtXa!y%MBkDB~oaSX8=|B+}p%5@uonM zn_)dskE5dgxwy$B7UDtO_s#N{dQ@IiYRc?**2_dj%d{C+ob@a*k&~f+QCmvu@MvPv zXAzzv=m(mV@f35IWRg%#BWNS#Yb*+XqhW64orn;jVCARAp6(CT+dJl6*AU;? zM*P*yjc8Zknkp&+s)x#G((ur2&&kDr+QHf9@3~dEGc~r>L7*Gzy1Zi26w8WWema4O9nUHF1Ay`VkG|KN;jIkW!y|Iqm z_{%A18!12g;hLL=>v$cmr4i55J7qcYXU=B~yAkp<@s~C6tv|V{8@vThN7>Ar*+kUT zG#R!Mo!W$4Nb=yBdJDs4I&6_7L__a`awb5B)C3Ey=!p>9V1OES1_-UBB15l>gAY6! zgAcgD1lD&~n=am~Xzs0?{DhP>B#)UnBu6*&eKAo@JpMbD(YyVmvxqj z&@&kK=UwrH$rMA@KCPr0_vdj`DwkaL#P-jJHm=bJ?i!1 z8}!q?ktnS3m!tlo1#^A;Kj@_YSVeWK>j|c&ToS7G_GF@PG48OmO z9f5EK30J^t+iqJy*#ApP50`b1Itps9p(Y}?<(r0xM8Llb@Vv_bC)p7#QQo3mf&A%)o+*0URgNCG za4$QHzx$SKgZ`gRt#R0@*1!twSlSHhsoh;QsLMm8r|!LTG;ZrmyWdoHUi$My zm|}07P^J|LaHp^NgRiGf&NR(l5NXAon_%#8@W<{J!y{jdzW4$&DU}1qKxKQX)8XSL z?2mV_=`AIG5HC-7@$7A6{NO&-ydr#n74Uj&pF-Z$8y{E$zC4yusOM~M_{>Se`eA&?^+`>z6+^^e z-9zRTW5i&l^d`h>3TNz)Nke3o@P4#IaDYO_;5OYM^K&LQe2?L@Z-9NqAh8)@a0oa2 zBgZE0*v2lzCWIB9Dg+PnN60WgJt9X9;>y;|Kz%P)#Ht|n&;k+1CZVGLZfL=$4YG(l)XI zh)7x3yd;LHCXIWu%}triolkzfz}&Mv;H7!jBuw@gw*s$C$eu=Qa`1sc z5B}ui$H!Ce4T7GYUs-(D)QtlbRq-=L`#jXs?`*z*GJpGBAOxgH)eXYY$Hg~AG4DOq z=I=cl`sYCiMJzXE)U-~?69#ZqtZ&+AQf<3#MTmlm%g{%Umm_j2vh91ay zqv1Eg^xKZrziV{;&zZQAcXh9BJ$2;6V~=dAB!U$EAp{B=FqE%)N^YkP%oiRBdy5yc}^m({p@zFIc>%w~m)m9mf}!-OfW5B#m6e+P`6X=P7dmh0oT$%qeiyr_JA?e>=;4&-SO=&B8d&53>ph7P{!2UjA~-<}+y zPd{`k0wz%CSu^`360$||g)I7cO(uA+j+wedG2^l`$+y$zR;9Uh)P|Z7YDCGkDr?Emz*2pk z=&{N3d}iyDCb5)=dbZCriD^F425+7nvY$^RexMM&Y@~fu_8dox`Rv=J+(Qc9 zWn-qPasT@eA02E~FvN~G5E{6FE|YOYXW<6Lr~;=-HsGPY*-BMa)A~nN0YuSZvNR`; z?3GZSJ9gTT=B1hQ>?q8Z$4Lc+-+cJDeA2{i2Y;$GDd|}~D%QeStOPVz3q!BG*3_3< zsN9j}+#54rC}E;sx!5Odt+_wQl@-R;EOL%rm7PhG84}(HzEmEj=aMrK zIbG|+mgHB(oqX}A(s99tu1a)pigk_tAoUw~m?aQ&b3GAeI>XD0@EuIa$5l*WS1n*g zVJzBC98rNH+I+s$#v@W|d9@)RcYCycT4=Se+q`R8J-~u{;9-d3WS5+P6N)5m6Yiaf zW5r-x?=Ll_GwMmLqv7bF{L`WyIobWu>Q~t8YF*XhO1GVnn(*7@JyIqu1`U@KGOlS7 zDkIuCSkaEPKx|W0eg3B=i?9iL1FUT5wishps-be9I&>pL2hh8|-SBPq^WaW#5tOE~ zT}eCEtSL~gqcqjWVd7I9gOLIKbVX?4W{OO%%C0HvcP#h>_@M-fc}T%}R9KJL<`U9V zXu1u!HS7X0Ez~@YB)L|YW@u9W5-|tHX@2Vd^Q|Yoj6j=D&m1~FnIk%im7$;J?kgN=T59<}6@^cfW2XSeDIy;+ z;ETOlaWdwo5OPoV_ct=W{O6{#XMgMJ$9oeE-~m`CjpUZsw{hJ#0gvO&c?Cy}%w9Ms zF1qLs5n#X6OVn!u32_b_qY`#EKw4CB&te~7XZY(jWdCXUQ92kuUn~8)qF)SI2<%X% z$*37c99~#|tO)1lveW3!TBbb0&BE?sJ2VN2b`;e?d02KJA-GD}T=1K%plNHtYUYXp zgJD%O29qwCKm_~M0K>`K8^SP{D*2gCTZu`SM9S}-Ykw9zDoswD2oi?2TS?0j|YT&|8hjXaQoPL@9w`)i%-M<8&28g z`*F!&y{zlqjf@rLrt~FRSN5BK<&28)W4m>{vp08~u*1zMt6=`$Tiv_$EYw^6mW-W< zt8zy&d5h9t;u3Jj2lY=`hj8Cq$z7Jwz83FVg8EUT_;y_|+qcUF=C!0ITJ*U22Lx;V! zcKoPS=n8#~`Z=P6J*6*B$?-V%RjyUCCvVVwdl4E(WA=YtevNLvY$%)5Bc}Fw#;j-I z0#n6dHjW;Da&pE??)2+d3EbXdopfMeK@6A7^s%KeI88UNE8A_UQz9pRg$VLmUKJVl z4I&pPU<9*3OS$nt9-xj5K$8UbcV(lbl*jMiig1b^fo^TkNqIjEk~>Q^*t@Y56IUj>ezm7Kz-yTs!n(QG%R6u)`W@o3~fE4rr$BH|lu!66Zt>E+mol2P_*O ziCJ0f=UY}ApdzPxn7#+JwBo&4_`u(lc$Y5=bBVwn<&r;>yAaRJ-31VEoTj>*61yyd zp3YVTLPv?QW5862ulNZ1OgO37-b6gtqu(;CiQAmQ# zCr+Ycyg+WEcZ!?X&fSUptp-8 zOKi8O!M8Q-*Qu1ps0AggluG*V^1Nk{%4)ki%nw(VY+snRW|#=(2QwJB9_$3%HZg&v zGierEtLuJ=$|~f4f4fwK5=?TPAjUyj8Yew=i=kkkgavOh6g$X3)xPOz)zymuI+`8M zw>dd|>IZAe!R{&|(y{JJk1V~blgfVPyc@hkWl%sl(2&%1_ zBayVylj>~>f=ABwi~c<+Iw4?r-Y>*Ha5S^04!G0F`%{@_*=~3GPH#N7wy(VW#9K~% z^A}g?O}_Q?lKt*@WTk_H-hSSv3-$^pR130pW(KZ(yEogRXYxqJ=3(mI^u9}QZvQ-a z((-M|R_NJHj9Leb)GgW74j^HIe+xHZ9kE0~@bpOQ{p$rbO7MWSD}JS|^sjCkYlGuC zUORP_Sk^=&Xl>}jo)cc3(U8>A$EKMhU3Op5&q?!5bIRWKQy#{mHJe~z zpD_@@wKexPN7*mrUJtXFETM6Et`^w$d}C!Oti(ItQxZ<}ac+wqpcwP31>V3Xy^R=>z5USMBZKK+o&=70h3Nk7J|rhq`+&2=kGz zbKt(1>sMjxt*%JtH0X1QUjjrO+!WGqJ~>^oI7Jo_J)Kc&*z0~air!w9jp!g4?wfgq zJL+up-MtWP-#IVzI~_ZIvZ7?AAS3Z;mPEnwP_cT! z*JJkw8oBTf-J3$s=O1WSr-_ar>?Lq(5SfWB(V-~fojAhaKW3_-Gv)6Cs%N6kHOpSA zcS_*;`P_me1{t2on+Vr1a$ReDFnK`uz3Z3nG7l^pUjIFTxC`QjIs zw*4v<4CwC+ww4{v+O69!bR4?vCk|s{UsX-Jfap8;>_AXh$l|f<;E74Cz!jC7G9IXy zRd53A1wnR`fLa1lq+bZjJc+3|#A70PRV!DqsMBI+{Y`^Fjxpas$8>UHzBCi7^C*i6 zK(hW0jN5kPJk|E<^L0~z;qgZas_$AoR&%@#wjhOvWDm=21DL3NucshN z&4&0NC>nxBdAUC#X!+LbzQ^kjjbhE1k1OVX7~$`<-c{$9+pA7>tr~|B)r7k3PQii)1bP3cLR~PA43g zv4&593)87tEg~Q62W|9|3QnF4m?e!IAcZS5Ibl^1YcsARB`ADY4@045znu~7a01Rh z>+l$JuFC|4z7hK3+kCD|DCv!`W2+C<_BhK-N=Y> zl~TeiuMqwCt^g2?J(W(R_x%hzZ2vT01(hBOkf{W6GNbOatvp{|VWfZ@Gaj%s85B1e z{1-eVWEKKhhEWhGjoh&iS!ze1fT3o7ow#1s4uhlLS<=;VminN4iuf0PSxB_tM4{Q*zUBpS#fqtC8M||{+PW- z5(wRsj(WEBgf#w`o)_kNV2gkk)eH-#tUQ@!r1^IZh&ZD0`?tbafwU1|CVhznf zNcNSz+~+>zhi)M#9b%<-D2l7HP?UKitR+ZD(RSuH;DtL1{iZh<2ucun!sawL z`=q-fJdKD;G+Bv51liqQ+tU(A>7MJhhOnA&5qu5Rl=-K7=a^Bc5AfVym}bjN8}a31 zSC+FQ2;YpbwsQh&KyheTK+B>WMu-W!SdTKbq+HdKtis?NxkRxZ$qSeOCGaBhz|Z(DEp*18 z1VY0=kluAfiGjwwj;QdjMMGCGU*OjKSx<7Ei}Qj)i@i@!ss5pK%B8wKW43@}FZc$1 z-YoNXL5^b2WSlRy4ve@Z5jq~L&dXc<&fA`H7{ix;`+e}9bh&Hz9biU!LH$`ro>n{E z60{dR1cz+zB{R$pgoATCvTD1<7#BtK@y^5If#X$}l~ytQCQx-!#mp8tbkW2!!BzcyD)40=2|*Yu0mzK2QhCp1h#(R@$2;3wHfiXgEyLjy>&XZ{&M zX|0LbwAC69Uagm>U>z2#~Po-F%98OE1a8pWC?$^=_E$3P3gIXP#XRT!S%HmE3Nof?Q8}oXNel$6zZ6o5zeox?V*DP z#;gc)w7}{?5S6x8>d);zSK@Bkb2cjyb4fpGEQY8yvG{d=<)f#aeV&c7cz}dINU$Mi z(%?!S-H5nn;V;BHL`q}2RFUQG#`yzUbSbPC|xe%Okxc%);L zG_IfQ50^C{^A+S3h12axEIV`>eqL^5>t|45rId@hnBdprP!y7Z)cQ%p(8ARJ5fkIp zsXBB>UB(p=2!Bb&w+Ydbzv(Zoq=hleRCOX?9E-CqQnFv*KyBvL5g10fl#6st3l1r^ z{nu}0VD+#h3EPFLP)&G6MVtXL zojBMIJEED*owWecK9Axcvs^)EyxTG6kCj#khg~RI92J@%q-I~YswpGSNItHCSVz-Z z$aI%XJe@qt>YU7K`DFEY%(uxUQNk=Y1!MdKB!^j3lDhl& zB*r^qUR%{ANk;qd1q6@ttEMdwk?leq$2=`&Sl6|!Y!1R}KfWg7%;x6J6}JEmGNXFm zg|_y^m62>BRdyx`Y%_8b#P`(XCq2~>tsGTcLL!`UA*V>h`1J*&%T zdIHFYXJMi^OA7M~hfB<*ZueY+JM&>+Qfs#=kiLtfx0Ft)66%I_u?evJL21EhB1K~o z`y+e<;GfX>bBQsII2~e7232`QBzVq9t<1BI9gB&3v^Ec(tsL>=LHPD(3RZhi>+eHu zd|8z;=K=UNDEvmBsN1(=_6jNRl;dDjM9kO}*MC(c^F3lY{V&6y`f`AQZw?~-MqNy@ zTjAUYNJv+3iVw0y+J$1+cV)GLRf00|eV_EtDGG}ZM`MgKy1E3@Y68%4IWb*yvmw;1 zW4+u|$L@h*3@+;&b&FewrGx#rG#a-Y6k`B#0lUWXJ{=|geA4hq+^u1speQWAISOkxN6G2HT#(@9Tx^dB9XN_J?3OOn|~ zl$aAWj7%vg4nFC>fH5@o+O&Bq=Yw0FizVKxE{rDu<>BtzXAf=xem*|A%c3k`_IB1; zS?QAC^M3G%gl?zt#n9;@+H;`p^q*0YcXU&pIoTNQ@}1(qL22#*r= zZZi_}Yy%6t5zSkDn-$(McjvFXR9jx!dN;Or+L1<0IbO;R%_-O(w+5pxh#!$=qJ4Y4 zYD|XROqif~U`MF-?cxEZyv;j173tj z-YY(e%y5_KiS|+MCa32c^uh!YtRyu#U+7JX-2>9+vtNsXrX)PoX~9gbOv0o7fgfj} zB`?g8I*)BLm-MV-8F|9RS6zfd%mWs5oU49T_0Hc?R!?L211om!o0F5?OCs*R=6-{c#%b^7GQ}uK~jPH z!qWw1S0j(t4IW+yW|v#OYAN)jCMFo4AluBz$FX=j+Sk*9N}jv6sek`8*blveRYyK6 z@$$QlJR0o@v$S+f-zsLw0nh#kUV&fD{$c1Ky*FirKmqzg+)FWg)*qYr#!&xh)r5FM zyIhdtLDGe=z-F!B!f`gKQ;5@DmkA~JFJ)}&q2vWU*3SVpi6R6uxf)tZkEGzFa5#xh zgxWZZW?URJ?Z)bcPP-?uZsE@O`(e|((Jc)+yo;i4MIL;)hlm(2w741^jymCajG}`Y z0+9`yJ4PswEoFzGwoK&Bt{R)>WKNgeyhyZZrCWq%%VuYWOSZTCmc7B@AINXaIYw>g zD(_7~W$3#FFPFybE@REcF<7d=>Bl!Qs|)m~SLEeCXQD;JBti`=eSRQFLEkCdcI{wy zZh^j@{zDOlr}L}zgS3@RiQBzf2Jwro|}z zp(8`DShFcww4*$ph=`Zv&Qf;2lWqEvw#uf03PUx5*6Zt_ixy%t9Lsse#_!)n3$--l zOf$;2nUJKM8%rIVj%qU1>XT_ym2MR4aaD{P*8oOSZgIqcWfWlkoR%D~ll0=66q}CTgR^m^OW6AzkH7eH)iozB+LoEQPHk( z#`+MS)QEj`X~>v7ZPYe^*p)Xt3}Ja0T^Df?O^X*F|EApS<~55@Q05SkK0sF+UD=#y zt7#A&M)vf*n^sI0F~cOr_VJvOH0Xd?%4c zS9%8jMQZ#au03wIpvh_4m~jGGx}6aI{d!htmWrf+Ec501JY=~N`(k@SGWn!aRsfxN){B8UN2djrCZY-c;VfAmwKt~0mYbZs}* zN)bzhWb*t}1j2|hWp6O^-@hIy=snZ+vUl(7haLy(cRSqP)j6yC>k9j)-0U_2f`oC* zDq6$j2-(gxSw{;!Dp96XDiCcn<=s}RfXP?}T|Y2spwLwsB6ETb1}TfF=R{7Hzpnh5 zA8mde1`9$mIOIAp6)$HGzWUmv@fqHkz82Ew-Q~St6-GJ%T zoE#?-c3l0~iaA9*ZHhlS4{FA<9Xf40OlkBmvD;}@=7o63Ay)&<*d*Y$1s;!ljpE;>z#T%*x>L7ZnjI45Ij{?bC*!?k!+qG ztdZ3sm+s_sl6t;4RC2XWn51!HZA6K~SFd{_-)wmP_l?z2qE~E~<2OIQ+O+`I`?nv4 zTY=XT@qB)6R50(?106eq%h-+tvkEe1h`*@lmM&+x3DEC^osEhDdqcgXu%ke2MH&Xk z1C-O3ZCc_QBqYIvgg?eabiv}wJFj##c2D8mmh`lixXcu@YxCQrG8!B!t|Fs3VzCQ; z9hr_t$>&PsMb)7~T9Gy2%f@h*+#5)SQ1_;4J^h9y10)bshZ z;l2nhm_6Q$h;b}ZWEkFj``_4Ccc@<0bZ^yIU;nEXlUv%4ty-&3ERH>Fs*hBk2V4(@zX=>s`_S;> znv9FMT_}=x6fgK5Eocs51k=oLfx-1*kl`Xt-`Wy>}^8>`FDC3BHmx0tiP7SUAm<*Y2o55|>ORCS?h9s0JBXbw;#Cph$cb&794ji= z+q>GiW^0_In6F@|`Go$PG?<~CdAy08(5Tw{%|4#eF}0z$P|{heEvSj_fb)BSxH5<| z05&!eJ_hd`J6pRTn3-`De*kX~6ob6;5$76=(raIQ zLf|D#m~aFvX;k~)4ngj9jDkYEH>=9Bl0Y4lFbo2hwZ;8SM5yle*pjPB#+xSFQmlZS zx-6>M44W~rAali^78Y#mRKbxFx=eMiUEa9z(ucTGd4XT}DvL>5sH(2)4?_+6KO;-8 zrn@NfBWJqrmF0aeV)74j{RNieoN=x1WWDtZBl&cYz_p4>6*bDFG3D`jit{?pN}=Kb zA$HRnUz77!U1Y__9o>Mc9eAhu-xJAe)|vDDd>|D0$V1~)51#MF`!ucYiH0PDBh7hd zP@~9L9U6_>0ITN)i|*;n^J#Cuv4^nl9;%&+iqY3>S?5D)G#pDe#$!hX0bHuh9I~vq zA2D4T@VATH2!##Rj~ya`D*lSE^NQsk@^8~~tHFwqGoQhqMQ94Y#*!-iK3j^ml#r&i zOqazq3pA5ARb?ZISzwF}DezJS|A=-F4_sjNEx`+yGyRH{IhD+PA05?2fF70oRRvbTyn=GafV{2>-SOR5)yp}dOVJQnupdB__2H{ zi%Re7Q-_+nW%M@Y$ImbA3k6IhfhQs^_th%;8QPSFoVu@2dYLVA7&B7wEV3z3DWY|4`dJ^1W>(H5b9w2ewH26TeK*KTVdYH@0yhXow`Vt zEiQb%wNti%zh@KY^!l}LTgdz&+oC$>Osld`vBzQUXWP=M-9c}NQL_(n4;71kn5XGo zmVOZ3ksQkzy(!yLlj|9MYY%lc=Ah@ZOz?K%F2w`tdy65K9JF()4*MSTo^&Wn?TB3P zh4PYQtzNI2laZ^V1u@2%VYXofo#$f9?} z{g5ky{arkjo0YZngdjFBkKC`Vo`@ZkWNC`C_ZF7g_;LQ^=gJK60isc0nfD||;QbLh zqm?XPW>-Ds0dZJbpO zb}am_%z^ldSG0U6@a*@mqlI3hkR}r6(>VCjfiSOI46I~*s;(97Ro)8+>zQ@jlv$49PArKvxkxgwBdB;#)2(4-!CdDVF!4L+<>%U)0rggTDio~bmuS8 z*DD7#>a9n~qz&fVQ)Srb$Y8w@3@3OW!=V6HjEqk8@ilHta1dF<-HO!0i~(!}5~#<= z!n4PX!FG>le~I^w5dGJxZstqGGH1pB;o}eE(Eh6Be7L8vtB>x7O+Oo_hROX4XeF%iNrNuDbMF%%Fj5&tjH zZ7s_!M;$vi4iUxIB2MrA(l$%5jD^&&(JiBh?Iq~B=emhrk`8_i{Ffx(xx%$@JBb4$SlNt~?WQ(N zrbFis>F-n+Ewf$L%LDR}95)U!ev7AlHLtPc>%(EeK6Xt72Nfmhq@VH#)l!BvMwO(w<36$uo$fW(#UmwvEP`o}J zPq{_b+bON@JG)PrK_|W_HmDM^PA|s$o1Y4khOl?^I?z#%nE! z{XC7pZ{9)DmQ?j7%D20V@pyT&Qdj#Tq9{+FAHx6pAWx)0Eu9L z5P*=4FobZ6NRH@+n21=7xPVTSv+KMKCW`On=9T!~!Jpg?S1Asw@0mRV42*4P_1jnSrl*M$yOvfC< ze8(ciO2@{;PRE|bp~m6EF~AAJsl@q<^NGucYk}L0JBj-b_Z|-(j~tH=PZiGu&krvf z?;0O~55)h8AAsM8|4D#LU_uZ>@SEVAkd#n}P=_#?aDecVh?K~UsE=5H*n_x`xQBR& z_?m=}M294iWQb&!6qi(l)POXKw3+ms44W*0Y=CT+9Fbg_+<`ose1!a!f}O&PBAa53 z5}Zw{%81H?s+?+r8k<^z+JSn2=DS1cf3GEvp@e?oJ^-k!K_hm=RJ*f~ zEPy^8)bGD}--KRiQ5NiBg;%7?zy1B=B*CHtc5B`!uGQRYFqnRBRXcLS z5pE{wla8bepSRui&#pNdE4gXH30(*{{GCl_2&(6MoneF?{$&T+Oa5g?MnXO=2THwJ zNyu0l{80#UvlT~tQNytW?0(Xc(S$a90`+1L4jIB^YnjWGh~q2PwiAbQyrJWIs()GM z-LTx|QI(~BF!yZyu3jYOyxi)d6q1}%F&nsTiNOoMg)@>4DswO zd7&f@=3|L%Ce-$h8rp+jmYY_uB#UFDQ4=Lb^GwKDnU=3`E4&nCwr*b=o=B|s^hs1R#V!agd6;mD@GGo*1m^2txCCYJ=jET}Lb#)NzldN#7*)#TZtJX7)bZh()DN<&DULB-z4J%ASOCDOS zi0&0yIg1V%+Atv2pu!%dK1bsWTZ|X)or9^6BWGs)3I=Y28W_*KeR-jvY4B^gK*h{y^sAn)+SUTnDOF`orBX|!{9+a4 zVtJ-&laFDBi^D=mo7d6d<;Dz!8i#DF~u*T d`d@*P)=+z2O9=Gccp2C_0H}G=_V0V@{{Zm~b;kez diff --git a/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/favicon.ico b/Tests/MQTTnet.Test.BlazorApp/Client/wwwroot/favicon.ico deleted file mode 100644 index a3a799985c43bc7309d701b2cad129023377dc71..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32038 zcmeHwX>eTEbtY7aYbrGrkNjgie?1jXjZ#zP%3n{}GObKv$BxI7Sl;Bwl5E+Qtj&t8 z*p|m4DO#HoJC-FyvNnp8NP<{Na0LMnTtO21(rBP}?EAiNjWgeO?z`{3ZoURUQlV2d zY1Pqv{m|X_oO91|?^z!6@@~od!@OH>&BN;>c@O+yUfy5w>LccTKJJ&`-k<%M^Zvi( z<$dKp=jCnNX5Qa+M_%6g|IEv~4R84q9|7E=|Ho(Wz3f-0wPjaRL;W*N^>q%^KGRr7 zxbjSORb_c&eO;oV_DZ7ua!sPH=0c+W;`vzJ#j~-x3uj};50#vqo*0w4!LUqs*UCh9 zvy2S%$#8$K4EOa&e@~aBS65_hc~Mpu=454VT2^KzWqEpBA=ME|O;1cn?8p<+{MKJf zbK#@1wzL44m$k(?85=Obido7=C|xWKe%66$z)NrzRwR>?hK?_bbwT z@Da?lBrBL}Zemo1@!9pYRau&!ld17h{f+UV0sY(R{ET$PBB|-=Nr@l-nY6w8HEAw* zRMIQU`24Jl_IFEPcS=_HdrOP5yf81z_?@M>83Vv65$QFr9nPg(wr`Ke8 zaY4ogdnMA*F7a4Q1_uXadTLUpCk;$ZPRRJ^sMOch;rlbvUGc1R9=u;dr9YANbQ<4Z z#P|Cp9BP$FXNPolgyr1XGt$^lFPF}rmBF5rj1Kh5%dforrP8W}_qJL$2qMBS-#%-|s#BPZBSETsn_EBYcr(W5dq( z@f%}C|iN7)YN`^)h7R?Cg}Do*w-!zwZb9=BMp%Wsh@nb22hA zA{`wa8Q;yz6S)zfo%sl08^GF`9csI9BlGnEy#0^Y3b);M+n<(}6jziM7nhe57a1rj zC@(2ISYBL^UtWChKzVWgf%4LW2Tqg_^7jMw`C$KvU+mcakFjV(BGAW9g%CzSyM;Df z143=mq0oxaK-H;o>F3~zJ<(3-j&?|QBn)WJfP#JR zRuA;`N?L83wQt78QIA$(Z)lGQY9r^SFal;LB^qi`8%8@y+mwcGsf~nv)bBy2S7z~9 z=;X@Gglk)^jpbNz?1;`!J3QUfAOp4U$Uxm5>92iT`mek#$>s`)M>;e4{#%HAAcb^8_Ax%ersk|}# z0bd;ZPu|2}18KtvmIo8`1@H~@2ejwo(5rFS`Z4&O{$$+ch2hC0=06Jh`@p+p8LZzY z&2M~8T6X^*X?yQ$3N5EzRv$(FtSxhW>>ABUyp!{484f8(%C1_y)3D%Qgfl_!sz`LTXOjR&L!zPA0qH_iNS!tY{!^2WfD%uT}P zI<~&?@&))5&hPPHVRl9);TPO>@UI2d!^ksb!$9T96V(F){puTsn(}qt_WXNw4VvHj zf;6A_XCvE`Z@}E-IOaG0rs>K>^=Sr&OgT_p;F@v0VCN0Y$r|Lw1?Wjt`AKK~RT*kJ z2>QPuVgLNcF+XKno;WBv$yj@d_WFJbl*#*V_Cwzo@%3n5%z4g21G*PVZ)wM5$A{klYozmGlB zT@u2+s}=f}25%IA!yNcXUr!!1)z(Nqbhojg0lv@7@0UlvUMT)*r;M$d0-t)Z?B1@qQk()o!4fqvfr_I0r7 zy1(NdkHEj#Yu{K>T#We#b#FD=c1XhS{hdTh9+8gy-vkcdkk*QS@y(xxEMb1w6z<^~ zYcETGfB#ibR#ql0EiD;PR$L&Vrh2uRv5t_$;NxC;>7_S5_OXxsi8udY3BUUdi55Sk zcyKM+PQ9YMA%D1kH1q48OFG(Gbl=FmV;yk8o>k%0$rJ8%-IYsHclnYuTskkaiCGkUlkMY~mx&K}XRlKIW;odWIeuKjtbc^8bBOTqK zjj(ot`_j?A6y_h%vxE9o*ntx#PGrnK7AljD_r58ylE*oy@{IY%+mA^!|2vW_`>`aC{#3`#3;D_$^S^cM zRcF+uTO2sICledvFgNMU@A%M)%8JbSLq{dD|2|2Sg8vvh_uV6*Q?F&rKaV{v_qz&y z`f;stIb?Cb2!Cg7CG91Bhu@D@RaIrq-+o+T2fwFu#|j>lD6ZS9-t^5cx>p|?flqUA z;Cgs#V)O#`Aw4$Kr)L5?|7f4izl!;n0jux}tEW$&&YBXz9o{+~HhoiYDJ`w5BVTl&ARya=M7zdy$FEe}iGBur8XE>rhLj&_yDk5D4n2GJZ07u7%zyAfNtOLn;)M?h*Py-Xtql5aJOtL4U8e|!t? z((sc6&OJXrPdVef^wZV&x=Z&~uA7^ix8rly^rEj?#d&~pQ{HN8Yq|fZ#*bXn-26P^ z5!)xRzYO9{u6vx5@q_{FE4#7BipS#{&J7*>y}lTyV94}dfE%Yk>@@pDe&F7J09(-0|wuI|$of-MRfK51#t@t2+U|*s=W; z!Y&t{dS%!4VEEi$efA!#<<7&04?kB}Soprd8*jYv;-Qj~h~4v>{XX~kjF+@Z7<t?^|i z#>_ag2i-CRAM8Ret^rZt*^K?`G|o>1o(mLkewxyA)38k93`<~4VFI?5VB!kBh%NNU zxb8K(^-MU1ImWQxG~nFB-Un;6n{lQz_FfsW9^H$Xcn{;+W^ZcG$0qLM#eNV=vGE@# z1~k&!h4@T|IiI<47@pS|i?Qcl=XZJL#$JKve;booMqDUYY{(xcdj6STDE=n?;fsS1 ze`h~Q{CT$K{+{t+#*I1=&&-UU8M&}AwAxD-rMa=e!{0gQXP@6azBq9(ji11uJF%@5 zCvV`#*?;ZguQ7o|nH%bm*s&jLej#@B35gy32ZAE0`Pz@#j6R&kN5w{O4~1rhDoU zEBdU)%Nl?8zi|DR((u|gg~r$aLYmGMyK%FO*qLvwxK5+cn*`;O`16c!&&XT{$j~5k zXb^fbh1GT-CI*Nj{-?r7HNg=e3E{6rxuluPXY z5Nm8ktc$o4-^SO0|Es_sp!A$8GVwOX+%)cH<;=u#R#nz;7QsHl;J@a{5NUAmAHq4D zIU5@jT!h?kUp|g~iN*!>jM6K!W5ar0v~fWrSHK@})@6Lh#h)C6F6@)&-+C3(zO! z8+kV|B7LctM3DpI*~EYo>vCj>_?x&H;>y0*vKwE0?vi$CLt zfSJB##P|M2dEUDBPKW=9cY-F;L;h3Fs4E2ERdN#NSL7ctAC z?-}_a{*L@GA7JHJudxtDVA{K5Yh*k(%#x4W7w+^ zcb-+ofbT5ieG+@QG2lx&7!MyE2JWDP@$k`M;0`*d+oQmJ2A^de!3c53HFcfW_Wtv< zKghQ;*FifmI}kE4dc@1y-u;@qs|V75Z^|Q0l0?teobTE8tGl@EB?k#q_wUjypJ*R zyEI=DJ^Z+d*&}B_xoWvs27LtH7972qqMxVFcX9}c&JbeNCXUZM0`nQIkf&C}&skSt z^9fw@b^Hb)!^hE2IJq~~GktG#ZWwWG<`@V&ckVR&r=JAO4YniJewVcG`HF;59}=bf zLyz0uxf6MhuSyH#-^!ZbHxYl^mmBVrx) zyrb8sQ*qBd_WXm9c~Of$&ZP$b^)<~0%nt#7y$1Jg$e}WCK>TeUB{P>|b1FAB?%K7>;XiOfd}JQ`|IP#Vf%kVy zXa4;XFZ+>n;F>uX&3|4zqWK2u3c<>q;tzjsb1;d{u;L$-hq3qe@82(ob<3qom#%`+ z;vzYAs7TIMl_O75BXu|r`Qhc4UT*vN$3Oo0kAC!{f2#HexDy|qUpgTF;k{o6|L>7l z=?`=*LXaow1o;oNNLXsGTrvC)$R&{m=94Tf+2iTT3Y_Or z-!;^0a{kyWtO4vksG_3cyc7HQ0~detf0+2+qxq(e1NS251N}w5iTSrM)`0p8rem!j zZ56hGD=pHI*B+dd)2B`%|9f0goozCSeXPw3 z+58k~sI02Yz#lOneJzYcG)EB0|F+ggC6D|B`6}d0khAK-gz7U3EGT|M_9$ZINqZjwf>P zJCZ=ogSoE`=yV5YXrcTQZx@Un(64*AlLiyxWnCJ9I<5Nc*eK6eV1Mk}ci0*NrJ=t| zCXuJG`#7GBbPceFtFEpl{(lTm`LX=B_!H+& z>$*Hf}}y zkt@nLXFG9%v**s{z&{H4e?aqp%&l#oU8lxUxk2o%K+?aAe6jLojA& z_|J0<-%u^<;NT*%4)n2-OdqfctSl6iCHE?W_Q2zpJken#_xUJlidzs249H=b#g z?}L4-Tnp6)t_5X?_$v)vz`s9@^BME2X@w<>sKZ3=B{%*B$T5Nj%6!-Hr;I!Scj`lH z&2dHFlOISwWJ&S2vf~@I4i~(0*T%OFiuX|eD*nd2utS4$1_JM?zmp>a#CsVy6Er^z zeNNZZDE?R3pM?>~e?H_N`C`hy%m4jb;6L#8=a7l>3eJS2LGgEUxsau-Yh9l~o7=Yh z2mYg3`m5*3Ik|lKQf~euzZlCWzaN&=vHuHtOwK!2@W6)hqq$Zm|7`Nmu%9^F6UH?+ z@2ii+=iJ;ZzhiUKu$QB()nKk3FooI>Jr_IjzY6=qxYy;&mvi7BlQ?t4kRjIhb|2q? zd^K~{-^cxjVSj?!Xs=Da5IHmFzRj!Kzh~b!?`P7c&T9s77VLYB?8_?F zauM^)p;qFG!9PHLfIsnt43UnmV?Wn?Ki7aXSosgq;f?MYUuSIYwOn(5vWhb{f%$pn z4ySN-z}_%7|B);A@PA5k*7kkdr4xZ@s{e9j+9w;*RFm;XPDQwx%~;8iBzSKTIGKO z{53ZZU*OLr@S5=k;?CM^i#zkxs3Sj%z0U`L%q`qM+tP zX$aL;*^g$7UyM2Go+_4A+f)IQcy^G$h2E zb?nT$XlgTEFJI8GN6NQf%-eVn9mPilRqUbT$pN-|;FEjq@Ao&TxpZg=mEgBHB zU@grU;&sfmqlO=6|G3sU;7t8rbK$?X0y_v9$^{X`m4jZ_BR|B|@?ZCLSPPEzz`w1n zP5nA;4(kQFKm%$enjkkBxM%Y}2si&d|62L)U(dCzCGn56HN+i#6|nV-TGIo0;W;`( zW-y=1KF4dp$$mC_|6}pbb>IHoKQeZajXQB>jVR?u`R>%l1o54?6NnS*arpVopdEF; zeC5J3*M0p`*8lif;!irrcjC?(uExejsi~>4wKYwstGY^N@KY}TujLx`S=Cu+T=!dx zKWlPm->I**E{A*q-Z^FFT5$G%7Ij0_*Mo4-y6~RmyTzUB&lfae(WZfO>um}mnsDXPEbau-!13!!xd!qh*{C)6&bz0j1I{>y$D-S)b*)JMCPk!=~KL&6Ngin0p6MCOxF2L_R9t8N!$2Wpced<#`y!F;w zKTi5V_kX&X09wAIJ#anfg9Dhn0s7(C6Nj3S-mVn(i|C6ZAVq0$hE)874co};g z^hR7pe4lU$P;*ggYc4o&UTQC%liCXooIfkI3TNaBV%t~FRr}yHu7kjQ2J*3;e%;iW zvDVCh8=G80KAeyhCuY2LjrC!Od1rvF7h}zszxGV)&!)6ChP5WAjv-zQAMNJIG!JHS zwl?pLxC-V5II#(hQ`l)ZAp&M0xd4%cxmco*MIk?{BD=BK`1vpc}D39|XlV z{c&0oGdDa~TL2FT4lh=~1NL5O-P~0?V2#ie`v^CnANfGUM!b4F=JkCwd7Q`c8Na2q zJGQQk^?6w}Vg9-{|2047((lAV84uN%sK!N2?V(!_1{{v6rdgZl56f0zDMQ+q)jKzzu^ztsVken;=DjAh6G`Cw`Q4G+BjS+n*=KI~^K{W=%t zbD-rN)O4|*Q~@<#@1Vx$E!0W9`B~IZeFn87sHMXD>$M%|Bh93rdGf1lKoX3K651t&nhsl= zXxG|%@8}Bbrlp_u#t*DZX<}_0Yb{A9*1Pd_)LtqNwy6xT4pZrOY{s?N4)pPwT(i#y zT%`lRi8U#Ken4fw>H+N`{f#FF?ZxFlLZg7z7#cr4X>id z{9kUD`d2=w_Zlb{^c`5IOxWCZ1k<0T1D1Z31IU0Q2edsZ1K0xv$pQVYq2KEp&#v#Z z?{m@Lin;*Str(C2sfF^L>{R3cjY`~#)m>Wm$Y|1fzeS0-$(Q^z@} zEO*vlb-^XK9>w&Ef^=Zzo-1AFSP#9zb~X5_+){$(eB4K z8gtW+nl{q+CTh+>v(gWrsP^DB*ge(~Q$AGxJ-eYc1isti%$%nM<_&Ev?%|??PK`$p z{f-PM{Ym8k<$$)(F9)tqzFJ?h&Dk@D?Dt{4CHKJWLs8$zy6+(R)pr@0ur)xY{=uXFFzH_> z-F^tN1y(2hG8V)GpDg%wW0Px_ep~nIjD~*HCSxDi0y`H!`V*~RHs^uQsb1*bK1qGpmd zB1m`Cjw0`nLBF2|umz+a#2X$c?Lj;M?Lj;MUp*d>7j~ayNAyj@SLpeH`)BgRH}byy zyQSat!;U{@O(<<2fp&oQkIy$z`_CQ-)O@RN;QD9T4y|wIJ^%U#(BF%=`i49}j!D-) zkOwPSJaG03SMkE~BzW}b_v>LA&y)EEYO6sbdnTX*$>UF|JhZ&^MSb4}Tgbne_4n+C zwI8U4i~PI>7a3{kVa8|))*%C0|K+bIbmV~a`|G#+`TU#g zXW;bWIcWsQi9c4X*RUDpIfyoPY)2bI-r9)xulm1CJDkQd6u+f)_N=w1ElgEBjprPF z3o?Ly0RVeY_{3~fPVckRMxe2lM8hj!B8F)JO z!`AP6>u>5Y&3o9t0QxBpNE=lJx#NyIbp1gD zzUYBIPYHIv9ngk-Zt~<)62^1Zs1LLYMh@_tP^I7EX-9)Ed0^@y{k65Gp0KRcTmMWw zU|+)qx{#q0SL+4q?Q`i0>COIIF8a0Cf&C`hbMj?LmG9K&iW-?PJt*u)38tTXAP>@R zZL6uH^!RYNq$p>PKz7f-zvg>OKXcZ8h!%Vo@{VUZp|+iUD_xb(N~G|6c#oQK^nHZU zKg#F6<)+`rf~k*Xjjye+syV{bwU2glMMMs-^ss4`bYaVroXzn`YQUd__UlZL_mLs z(vO}k!~(mi|L+(5&;>r<;|OHnbXBE78LruP;{yBxZ6y7K3)nMo-{6PCI7gQi6+rF_ zkPod!Z8n}q46ykrlQS|hVB(}(2Kf7BCZ>Vc;V>ccbk2~NGaf6wGQH@W9&?Zt3v(h*P4xDrN>ex7+jH*+Qg z%^jH$&+*!v{sQ!xkWN4+>|b}qGvEd6ANzgqoVy5Qfws}ef2QqF{iiR5{pT}PS&yjo z>lron#va-p=v;m>WB+XVz|o;UJFdjo5_!RRD|6W{4}A2a#bZv)gS_`b|KsSH)Sd_JIr%<%n06TX&t{&!H#{)?4W9hlJ`R1>FyugOh3=D_{einr zu(Wf`qTkvED+gEULO0I*Hs%f;&=`=X4;N8Ovf28x$A*11`dmfy2=$+PNqX>XcG`h% zJY&A6@&)*WT^rC(Caj}2+|X|6cICm5h0OK0cGB_!wEKFZJU)OQ+TZ1q2bTx9hxnq& z$9ee|f9|0M^)#E&Pr4)f?o&DMM4w>Ksb{hF(0|wh+5_{vPow{V%TFzU2za&gjttNi zIyR9qA56dX52Qbv2aY^g`U7R43-p`#sO1A=KS2aKgfR+Yu^bQ*i-qu z%0mP;Ap)B~zZgO9lG^`325gOf?iUHF{~7jyGC)3L(eL(SQ70VzR~wLN18tnx(Cz2~ zctBl1kI)wAe+cxWHw*NW-d;=pd+>+wd$a@GBju*wFvabSaPtHiT!o#QFC+wBVwYo3s=y;z1jM+M=Fj!FZM>UzpL-eZzOT( zhmZmEfWa=%KE#V3-ZK5#v!Hzd{zc^{ctF~- z>DT-U`}5!fk$aj24`#uGdB7r`>oX5tU|d*b|N3V1lXmv%MGrvE(dXG)^-J*LA>$LE z7kut4`zE)v{@Op|(|@i#c>tM!12FQh?}PfA0`Bp%=%*RiXVzLDXnXtE@4B)5uR}a> zbNU}q+712pIrM`k^odG8dKtG$zwHmQI^c}tfjx5?egx3!e%JRm_64e+>`Ra1IRfLb z1KQ`SxmH{cZfyVS5m(&`{V}Y4j6J{b17`h6KWqZ&hfc(oR zxM%w!$F(mKy05kY&lco3%zvLCxBW+t*rxO+i=qGMvobx0-<7`VUu)ka`){=ew+Ovt zg%52_{&UbkUA8aJPWsk)gYWV4`dnxI%s?7^fGpq{ZQuu=VH{-t7w~K%_E<8`zS;V- zKTho*>;UQQul^1GT^HCt@I-q?)&4!QDgBndn?3sNKYKCQFU4LGKJ$n@Je$&w9@E$X z^p@iJ(v&`1(tq~1zc>0Vow-KR&vm!GUzT?Eqgnc)leZ9p)-Z*C!zqb=-$XG0 z^!8RfuQs5s>Q~qcz92(a_Q+KH?C*vCTr~UdTiR`JGuNH8v(J|FTiSEcPrBpmHRtmd zI2Jng0J=bXK);YY^rM?jzn?~X-Pe`GbAy{D)Y6D&1GY-EBcy%Bq?bKh?A>DD9DD!p z?{q02wno2sraGUkZv5dx+J8)&K$)No43Zr(*S`FEdL!4C)}WE}vJd%{S6-3VUw>Wp z?Aasv`T0^%P$2vE?L+Qhj~qB~K%eW)xH(=b_jU}TLD&BP*Pc9hz@Z=e0nkpLkWl}> z_5J^i(9Z7$(XG9~I3sY)`OGZ#_L06+Dy4E>UstcP-rU@xJ$&rxvo!n1Ao`P~KLU-8 z{zDgN4-&A6N!kPSYbQ&7sLufi`YtE2uN$S?e&5n>Y4(q#|KP!cc1j)T^QrUXMPFaP z_SoYO8S8G}Z$?AL4`;pE?7J5K8yWqy23>cCT2{=-)+A$X^-I9=e!@J@A&-;Ufc)`H}c(VI&;0x zrrGv()5mjP%jXzS{^|29?bLNXS0bC%p!YXI!;O457rjCEEzMkGf~B3$T}dXBO23tP z+Ci>;5UoM?C@bU@f9G1^X3=ly&ZeFH<@|RnOG--A&)fd)AUgjw?%izq{p(KJ`EP0v z2mU)P!+3t@X14DA=E2RR-|p${GZ9ETX=d+kJRZL$nSa0daI@&oUUxnZg0xd_xu>Vz lzF#z5%kSKX?YLH3ll^(hI(_`L*t#Iva2Ede*Z;>H_ - - - - - - MQTTnet.Test.BlazorApp - - - - - - - Loading... - -

- An unhandled error has occurred. - Reload - 🗙 -
- - - - diff --git a/Tests/MQTTnet.Test.BlazorApp/Server/Controllers/WeatherForecastController.cs b/Tests/MQTTnet.Test.BlazorApp/Server/Controllers/WeatherForecastController.cs deleted file mode 100644 index b95a4bf..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Server/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,40 +0,0 @@ -using MQTTnet.Test.BlazorApp.Shared; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; - -namespace MQTTnet.Test.BlazorApp.Server.Controllers -{ - [ApiController] - [Route("[controller]")] - public class WeatherForecastController : ControllerBase - { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger logger; - - public WeatherForecastController(ILogger logger) - { - this.logger = logger; - } - - [HttpGet] - public IEnumerable Get() - { - var rng = new Random(); - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateTime.Now.AddDays(index), - TemperatureC = rng.Next(-20, 55), - Summary = Summaries[rng.Next(Summaries.Length)] - }) - .ToArray(); - } - } -} diff --git a/Tests/MQTTnet.Test.BlazorApp/Server/MQTTnet.Test.BlazorApp.Server.csproj b/Tests/MQTTnet.Test.BlazorApp/Server/MQTTnet.Test.BlazorApp.Server.csproj deleted file mode 100644 index 25d2fa8..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Server/MQTTnet.Test.BlazorApp.Server.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - netcoreapp3.1 - - - - - - - - - - - - - diff --git a/Tests/MQTTnet.Test.BlazorApp/Server/Pages/Error.cshtml b/Tests/MQTTnet.Test.BlazorApp/Server/Pages/Error.cshtml deleted file mode 100644 index 5b239dc..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Server/Pages/Error.cshtml +++ /dev/null @@ -1,27 +0,0 @@ -@page -@model MQTTnet.Test.BlazorApp.Server.Pages.ErrorModel -@{ - Layout = "_Layout"; - ViewData["Title"] = "Error"; -} - -

Error.

-

An error occurred while processing your request.

- -@if (Model.ShowRequestId) -{ -

- Request ID: @Model.RequestId -

-} - -

Development Mode

-

- Swapping to the Development environment displays detailed information about the error that occurred. -

-

- The Development environment shouldn't be enabled for deployed applications. - It can result in displaying sensitive information from exceptions to end users. - For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development - and restarting the app. -

diff --git a/Tests/MQTTnet.Test.BlazorApp/Server/Pages/Error.cshtml.cs b/Tests/MQTTnet.Test.BlazorApp/Server/Pages/Error.cshtml.cs deleted file mode 100644 index db458c6..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Server/Pages/Error.cshtml.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; -using Microsoft.Extensions.Logging; - -namespace MQTTnet.Test.BlazorApp.Server.Pages -{ - [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] - public class ErrorModel : PageModel - { - public string RequestId { get; set; } - - public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); - - private readonly ILogger _logger; - - public ErrorModel(ILogger logger) - { - _logger = logger; - } - - public void OnGet() - { - RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; - } - } -} diff --git a/Tests/MQTTnet.Test.BlazorApp/Server/Pages/Shared/_Layout.cshtml b/Tests/MQTTnet.Test.BlazorApp/Server/Pages/Shared/_Layout.cshtml deleted file mode 100644 index a369577..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Server/Pages/Shared/_Layout.cshtml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - @ViewBag.Title - - - - - -
-
- @RenderBody() -
-
- - - diff --git a/Tests/MQTTnet.Test.BlazorApp/Server/Program.cs b/Tests/MQTTnet.Test.BlazorApp/Server/Program.cs deleted file mode 100644 index e708574..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Server/Program.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace MQTTnet.Test.BlazorApp.Server -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } -} diff --git a/Tests/MQTTnet.Test.BlazorApp/Server/Startup.cs b/Tests/MQTTnet.Test.BlazorApp/Server/Startup.cs deleted file mode 100644 index e082beb..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Server/Startup.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.HttpsPolicy; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.ResponseCompression; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using System.Linq; - -namespace MQTTnet.Test.BlazorApp.Server -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureServices(IServiceCollection services) - { - - services.AddControllersWithViews(); - services.AddRazorPages(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - app.UseWebAssemblyDebugging(); - } - else - { - app.UseExceptionHandler("/Error"); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); - } - - app.UseHttpsRedirection(); - app.UseBlazorFrameworkFiles(); - app.UseStaticFiles(); - - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapRazorPages(); - endpoints.MapControllers(); - endpoints.MapFallbackToFile("index.html"); - }); - } - } -} diff --git a/Tests/MQTTnet.Test.BlazorApp/Server/appsettings.Development.json b/Tests/MQTTnet.Test.BlazorApp/Server/appsettings.Development.json deleted file mode 100644 index 8983e0f..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Server/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - } -} diff --git a/Tests/MQTTnet.Test.BlazorApp/Server/appsettings.json b/Tests/MQTTnet.Test.BlazorApp/Server/appsettings.json deleted file mode 100644 index ad75fee..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Server/appsettings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, -"AllowedHosts": "*" -} diff --git a/Tests/MQTTnet.Test.BlazorApp/Shared/MQTTnet.Test.BlazorApp.Shared.csproj b/Tests/MQTTnet.Test.BlazorApp/Shared/MQTTnet.Test.BlazorApp.Shared.csproj deleted file mode 100644 index d4c395e..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Shared/MQTTnet.Test.BlazorApp.Shared.csproj +++ /dev/null @@ -1,7 +0,0 @@ - - - - netstandard2.1 - - - diff --git a/Tests/MQTTnet.Test.BlazorApp/Shared/WeatherForecast.cs b/Tests/MQTTnet.Test.BlazorApp/Shared/WeatherForecast.cs deleted file mode 100644 index be8ca30..0000000 --- a/Tests/MQTTnet.Test.BlazorApp/Shared/WeatherForecast.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace MQTTnet.Test.BlazorApp.Shared -{ - public class WeatherForecast - { - public DateTime Date { get; set; } - - public int TemperatureC { get; set; } - - public string Summary { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - } -} diff --git a/Tests/MQTTnet.TestApp.AspNetCore2/MQTTnet.TestApp.AspNetCore2.csproj b/Tests/MQTTnet.TestApp.AspNetCore2/MQTTnet.TestApp.AspNetCore2.csproj deleted file mode 100644 index 8b5428b..0000000 --- a/Tests/MQTTnet.TestApp.AspNetCore2/MQTTnet.TestApp.AspNetCore2.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - net461;netcoreapp2.1;netcoreapp3.1;net5.0 - Latest - - - - - - - - - - - - - - - - - - - - diff --git a/Tests/MQTTnet.TestApp.AspNetCore2/Program.cs b/Tests/MQTTnet.TestApp.AspNetCore2/Program.cs deleted file mode 100644 index 1f65f7a..0000000 --- a/Tests/MQTTnet.TestApp.AspNetCore2/Program.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; -using MQTTnet.AspNetCore; -using System.Threading.Tasks; -using MQTTnet.AspNetCore.Extensions; - -namespace MQTTnet.TestApp.AspNetCore2 -{ - public static class Program - { - public static Task Main(string[] args) - { - return BuildWebHost(args).RunAsync(); - } - - private static IWebHost BuildWebHost(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseKestrel(o => - { - o.ListenAnyIP(1883, l => l.UseMqtt()); - o.ListenAnyIP(5000); // default http pipeline - }) - .UseStartup() - .Build(); - } -} diff --git a/Tests/MQTTnet.TestApp.AspNetCore2/Startup.cs b/Tests/MQTTnet.TestApp.AspNetCore2/Startup.cs deleted file mode 100644 index 6eb8bfb..0000000 --- a/Tests/MQTTnet.TestApp.AspNetCore2/Startup.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.IO; -using System.Reflection; -using System.Runtime.Versioning; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Logging; -using MQTTnet.AspNetCore; -using MQTTnet.AspNetCore.Extensions; -using MQTTnet.Server; - -namespace MQTTnet.TestApp.AspNetCore2 -{ - public class Startup - { - // In class _Startup_ of the ASP.NET Core 2.0 project. - - public void ConfigureServices(IServiceCollection services) - { - services - .AddHostedMqttServer(mqttServer => mqttServer.WithoutDefaultEndpoint()) - .AddMqttConnectionHandler() - .AddConnections(); - } - - // In class _Startup_ of the ASP.NET Core 3.1 project. -#if NETCOREAPP3_1 || NET5_0 - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapMqtt("/mqtt"); - }); -#else - public void Configure(IApplicationBuilder app, IHostingEnvironment env) - { - app.UseConnections(c => c.MapMqtt("/mqtt")); -#endif - - app.UseMqttServer(server => - { - server.StartedHandler = new MqttServerStartedHandlerDelegate(async args => - { - var frameworkName = GetType().Assembly.GetCustomAttribute()? - .FrameworkName; - - var msg = new MqttApplicationMessageBuilder() - .WithPayload($"Mqtt hosted on {frameworkName} is awesome") - .WithTopic("message"); - - while (true) - { - try - { - await server.PublishAsync(msg.Build()); - msg.WithPayload($"Mqtt hosted on {frameworkName} is still awesome at {DateTime.Now}"); - } - catch (Exception e) - { - Console.WriteLine(e); - } - finally - { - await Task.Delay(TimeSpan.FromSeconds(2)); - } - } - }); - }); - - app.Use((context, next) => - { - if (context.Request.Path == "/") - { - context.Request.Path = "/Index.html"; - } - - return next(); - }); - - app.UseStaticFiles(); - - - app.UseStaticFiles(new StaticFileOptions - { - RequestPath = "/node_modules", - FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "node_modules")) - }); - } - } -} diff --git a/Tests/MQTTnet.TestApp.AspNetCore2/package.json b/Tests/MQTTnet.TestApp.AspNetCore2/package.json deleted file mode 100644 index 9363db9..0000000 --- a/Tests/MQTTnet.TestApp.AspNetCore2/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "version": "1.0.0", - "name": "mqtt.test", - "private": true, - "devDependencies": {}, - "dependencies": { - "mqtt": "2.15.1", - "@types/node": "8.0.46", - "systemjs": "0.20.19", - "typescript": "2.3.4" - } -} diff --git a/Tests/MQTTnet.TestApp.AspNetCore2/tsconfig.json b/Tests/MQTTnet.TestApp.AspNetCore2/tsconfig.json deleted file mode 100644 index 5794ac5..0000000 --- a/Tests/MQTTnet.TestApp.AspNetCore2/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "noImplicitAny": false, - "noEmitOnError": true, - "removeComments": false, - "sourceMap": true, - "target": "es5" - }, - "exclude": [ - "node_modules" - ] -} diff --git a/Tests/MQTTnet.TestApp.AspNetCore2/wwwroot/Index.html b/Tests/MQTTnet.TestApp.AspNetCore2/wwwroot/Index.html deleted file mode 100644 index 9cc67e4..0000000 --- a/Tests/MQTTnet.TestApp.AspNetCore2/wwwroot/Index.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - MQTT test - - - - - -
-

- - - -
- -
-
    -
    - - - \ No newline at end of file diff --git a/Tests/MQTTnet.TestApp.AspNetCore2/wwwroot/app/app.ts b/Tests/MQTTnet.TestApp.AspNetCore2/wwwroot/app/app.ts deleted file mode 100644 index b716770..0000000 --- a/Tests/MQTTnet.TestApp.AspNetCore2/wwwroot/app/app.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { connect } from "mqtt"; - -var client = connect('ws://' + location.host + '/mqtt', - { - clientId: "client" + Math.floor(Math.random() * 6) + 1 - }); - -window.onbeforeunload = () => { - client.end(); -}; - -var publishButton = document.getElementById("publish"); -var topicInput = document.getElementById("topic"); -var msgInput = document.getElementById("msg"); -var stateParagraph = document.getElementById("state"); -var msgsList = document.getElementById("msgs"); - -publishButton.onclick = click => { - var topic = topicInput.value; - var msg = msgInput.value; - client.publish(topic, msg); -}; - -client.on('connect', () => { - client.subscribe('#', { qos: 0 }, (err, granted) => { - console.log(err); - }); - client.publish('presence', 'Hello mqtt'); - - stateParagraph.innerText = "connected"; - showMsg("[connect]"); -}); - -client.on("error", e => { - showMsg("error: " + e.message); -}); - -client.on("reconnect", () => { - stateParagraph.innerText = "reconnecting"; - showMsg("[reconnect]"); -}); - -client.on('message', (topic, message) => { - showMsg(topic + ": " + message.toString()); -}); - -function showMsg(msg: string) { - //console.log(msg); - - var node = document.createElement("LI"); - node.appendChild(document.createTextNode(msg)); - - msgsList.appendChild(node); - - if (msgsList.childElementCount > 50) { - msgsList.removeChild(msgsList.childNodes[0]); - } -} \ No newline at end of file diff --git a/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj b/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj deleted file mode 100644 index 124452e..0000000 --- a/Tests/MQTTnet.TestApp.NetCore/MQTTnet.TestApp.NetCore.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - Exe - Full - netcoreapp2.1;net5.0 - $(TargetFrameworks);net452;net461 - - - - RELEASE;NETCOREAPP2_1 - - - - - - - - - - - - - - - Always - - - - diff --git a/Tests/MQTTnet.TestApp.NetCore/Program.cs b/Tests/MQTTnet.TestApp.NetCore/Program.cs deleted file mode 100644 index a4a1f75..0000000 --- a/Tests/MQTTnet.TestApp.NetCore/Program.cs +++ /dev/null @@ -1,192 +0,0 @@ -using MQTTnet.Client.Options; -using MQTTnet.Diagnostics; -using MQTTnet.Server; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.IO; -using System.Net.Security; -using System.Threading; -using System.Threading.Tasks; -using MQTTnet.Client; -using MQTTnet.Diagnostics.Runtime; - -namespace MQTTnet.TestApp.NetCore -{ - public static class Program - { - public static void Main() - { - //MqttNetConsoleLogger.ForwardToConsole(); - - Console.WriteLine($"MQTTnet - TestApp.{TargetFrameworkProvider.TargetFramework}"); - Console.WriteLine("1 = Start client"); - Console.WriteLine("2 = Start server"); - Console.WriteLine("3 = Start performance test"); - Console.WriteLine("4 = Start managed client"); - Console.WriteLine("5 = Start public broker test"); - Console.WriteLine("6 = Start server & client"); - Console.WriteLine("7 = Client flow test"); - Console.WriteLine("8 = Start performance test (client only)"); - Console.WriteLine("9 = Start server (no trace)"); - Console.WriteLine("a = Start QoS 2 benchmark"); - Console.WriteLine("b = Start QoS 1 benchmark"); - Console.WriteLine("c = Start QoS 0 benchmark"); - Console.WriteLine("d = Start server with logging"); - - var pressedKey = Console.ReadKey(true); - if (pressedKey.KeyChar == '1') - { - Task.Run(ClientTest.RunAsync); - } - else if (pressedKey.KeyChar == '2') - { - Task.Run(ServerTest.RunAsync); - } - else if (pressedKey.KeyChar == '3') - { - Task.Run(PerformanceTest.RunClientAndServer); - } - else if (pressedKey.KeyChar == '4') - { - Task.Run(ManagedClientTest.RunAsync); - } - else if (pressedKey.KeyChar == '5') - { - Task.Run(PublicBrokerTest.RunAsync); - } - else if (pressedKey.KeyChar == '6') - { - Task.Run(ServerAndClientTest.RunAsync); - } - else if (pressedKey.KeyChar == '7') - { - Task.Run(ClientFlowTest.RunAsync); - } - else if (pressedKey.KeyChar == '8') - { - PerformanceTest.RunClientOnly(); - return; - } - else if (pressedKey.KeyChar == '9') - { - ServerTest.RunEmptyServer(); - return; - } - else if (pressedKey.KeyChar == 'a') - { - Task.Run(PerformanceTest.RunQoS2Test); - } - else if (pressedKey.KeyChar == 'b') - { - Task.Run(PerformanceTest.RunQoS1Test); - } - else if (pressedKey.KeyChar == 'c') - { - Task.Run(PerformanceTest.RunQoS0Test); - } - else if (pressedKey.KeyChar == 'd') - { - Task.Run(ServerTest.RunEmptyServerWithLogging); - } - - Thread.Sleep(Timeout.Infinite); - } - - static int _count; - - static async Task ClientTestWithHandlers() - { - //private static int _count = 0; - - var factory = new MqttFactory(); - var mqttClient = factory.CreateMqttClient(); - - var options = new MqttClientOptionsBuilder() - .WithClientId("mqttnetspeed") - .WithTcpServer("#serveraddress#") - .WithCredentials("#username#", "#password#") - .WithCleanSession() - .Build(); - - //mqttClient.ApplicationMessageReceived += (s, e) => // version 2.8.5 - mqttClient.UseApplicationMessageReceivedHandler(e => // version 3.0.0+ - { - Interlocked.Increment(ref _count); - }); - - //mqttClient.Connected += async (s, e) => // version 2.8.5 - mqttClient.UseConnectedHandler(async e => // version 3.0.0+ - { - Console.WriteLine("### CONNECTED WITH SERVER ###"); - await mqttClient.SubscribeAsync("topic/+", MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce); - Console.WriteLine("### SUBSCRIBED ###"); - }); - - await mqttClient.ConnectAsync(options); - - while (true) - { - Console.WriteLine($"{Interlocked.Exchange(ref _count, 0)}/s"); - await Task.Delay(TimeSpan.FromSeconds(1)); - } - - } - } - - public class RetainedMessageHandler : IMqttServerStorage - { - const string Filename = "C:\\MQTT\\RetainedMessages.json"; - - public Task SaveRetainedMessagesAsync(IList messages) - { - var directory = Path.GetDirectoryName(Filename); - if (!Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - - File.WriteAllText(Filename, JsonConvert.SerializeObject(messages)); - return Task.FromResult(0); - } - - public Task> LoadRetainedMessagesAsync() - { - IList retainedMessages; - if (File.Exists(Filename)) - { - var json = File.ReadAllText(Filename); - retainedMessages = JsonConvert.DeserializeObject>(json); - } - else - { - retainedMessages = new List(); - } - - return Task.FromResult(retainedMessages); - } - } - - public class WikiCode - { - public void Code() - { - //Validate certificate. - var options = new MqttClientOptionsBuilder() - .WithTls(new MqttClientOptionsBuilderTlsParameters - { - CertificateValidationHandler = context => - { - // TODO: Check conditions of certificate by using above context. - if (context.SslPolicyErrors == SslPolicyErrors.None) - { - return true; - } - - return false; - } - }) - .Build(); - } - } -} \ No newline at end of file diff --git a/Tests/MQTTnet.TestApp.NetCore/ServerTest.cs b/Tests/MQTTnet.TestApp.NetCore/ServerTest.cs deleted file mode 100644 index 21c47b7..0000000 --- a/Tests/MQTTnet.TestApp.NetCore/ServerTest.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System; -using System.Text; -using System.Threading.Tasks; -using MQTTnet.Client.Receiving; -using MQTTnet.Diagnostics; -using MQTTnet.Diagnostics.Logger; -using MQTTnet.Protocol; -using MQTTnet.Server; -using MQTTnet.Server.Internal; - -namespace MQTTnet.TestApp.NetCore -{ - public static class ServerTest - { - public static void RunEmptyServer() - { - var mqttServer = new MqttFactory().CreateMqttServer(); - mqttServer.StartAsync(new MqttServerOptions()).GetAwaiter().GetResult(); - - Console.WriteLine("Press any key to exit."); - Console.ReadLine(); - } - - public static void RunEmptyServerWithLogging() - { - var logger = new MqttNetEventLogger(); - MqttNetConsoleLogger.ForwardToConsole(logger); - - var mqttFactory = new MqttFactory(logger); - var mqttServer = mqttFactory.CreateMqttServer(); - mqttServer.StartAsync(new MqttServerOptions()).GetAwaiter().GetResult(); - - Console.WriteLine("Press any key to exit."); - Console.ReadLine(); - } - - public static async Task RunAsync() - { - try - { - var options = new MqttServerOptions - { - ConnectionValidator = new MqttServerConnectionValidatorDelegate(p => - { - if (p.ClientId == "SpecialClient") - { - if (p.Username != "USER" || p.Password != "PASS") - { - p.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword; - } - } - }), - - Storage = new RetainedMessageHandler(), - - ApplicationMessageInterceptor = new MqttServerApplicationMessageInterceptorDelegate(context => - { - if (MqttTopicFilterComparer.IsMatch(context.ApplicationMessage.Topic, "/myTopic/WithTimestamp/#")) - { - // Replace the payload with the timestamp. But also extending a JSON - // based payload with the timestamp is a suitable use case. - context.ApplicationMessage.Payload = Encoding.UTF8.GetBytes(DateTime.Now.ToString("O")); - } - - if (context.ApplicationMessage.Topic == "not_allowed_topic") - { - context.AcceptPublish = false; - context.CloseConnection = true; - } - }), - - SubscriptionInterceptor = new MqttServerSubscriptionInterceptorDelegate(context => - { - if (context.TopicFilter.Topic.StartsWith("admin/foo/bar") && context.ClientId != "theAdmin") - { - context.AcceptSubscription = false; - } - - if (context.TopicFilter.Topic.StartsWith("the/secret/stuff") && context.ClientId != "Imperator") - { - context.AcceptSubscription = false; - context.CloseConnection = true; - } - }) - }; - - // Extend the timestamp for all messages from clients. - // Protect several topics from being subscribed from every client. - - //var certificate = new X509Certificate(@"C:\certs\test\test.cer", ""); - //options.TlsEndpointOptions.Certificate = certificate.Export(X509ContentType.Cert); - //options.ConnectionBacklog = 5; - //options.DefaultEndpointOptions.IsEnabled = true; - //options.TlsEndpointOptions.IsEnabled = false; - - var mqttServer = new MqttFactory().CreateMqttServer(); - - mqttServer.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(e => - { - MqttNetConsoleLogger.PrintToConsole( - $"'{e.ClientId}' reported '{e.ApplicationMessage.Topic}' > '{Encoding.UTF8.GetString(e.ApplicationMessage.Payload ?? new byte[0])}'", - ConsoleColor.Magenta); - }); - - //options.ApplicationMessageInterceptor = c => - //{ - // if (c.ApplicationMessage.Payload == null || c.ApplicationMessage.Payload.Length == 0) - // { - // return; - // } - - // try - // { - // var content = JObject.Parse(Encoding.UTF8.GetString(c.ApplicationMessage.Payload)); - // var timestampProperty = content.Property("timestamp"); - // if (timestampProperty != null && timestampProperty.Value.Type == JTokenType.Null) - // { - // timestampProperty.Value = DateTime.Now.ToString("O"); - // c.ApplicationMessage.Payload = Encoding.UTF8.GetBytes(content.ToString()); - // } - // } - // catch (Exception) - // { - // } - //}; - - mqttServer.ClientConnectedHandler = new MqttServerClientConnectedHandlerDelegate(e => - { - Console.Write("Client disconnected event fired."); - }); - - await mqttServer.StartAsync(options); - - Console.WriteLine("Press any key to exit."); - Console.ReadLine(); - - await mqttServer.StopAsync(); - } - catch (Exception e) - { - Console.WriteLine(e); - } - - Console.ReadLine(); - } - } -}