From ade853da2387ca2c20fcd384266509b9c487af9b Mon Sep 17 00:00:00 2001 From: 1 <1@DESKTOP-2GSR655> Date: Thu, 7 Apr 2022 16:16:52 +0800 Subject: [PATCH] 212 --- BAP.MQTTnet.Test/BAP.MQTTnet.Test.csproj | 12 + .../Client/Client_Connection_Samples.cs | 275 + .../Client/Client_Publish_Samples.cs | 46 + .../Client/Client_Subscribe_Samples.cs | 83 + .../Controllers/HomeController.cs | 153 + BAP.MQTTnet.Test/Models/ErrorViewModel.cs | 11 + BAP.MQTTnet.Test/MqttClient.cs | 433 + BAP.MQTTnet.Test/Program.cs | 26 + BAP.MQTTnet.Test/Startup.cs | 53 + BAP.MQTTnet.Test/Views/Home/Index.cshtml | 8 + BAP.MQTTnet.Test/Views/Home/Privacy.cshtml | 6 + BAP.MQTTnet.Test/Views/Shared/Error.cshtml | 25 + BAP.MQTTnet.Test/Views/Shared/_Layout.cshtml | 48 + .../Shared/_ValidationScriptsPartial.cshtml | 2 + BAP.MQTTnet.Test/Views/_ViewImports.cshtml | 3 + BAP.MQTTnet.Test/Views/_ViewStart.cshtml | 3 + BAP.MQTTnet.Test/appsettings.Development.json | 9 + BAP.MQTTnet.Test/appsettings.json | 10 + BAP.MQTTnet.Test/wwwroot/css/site.css | 71 + BAP.MQTTnet.Test/wwwroot/favicon.ico | Bin 0 -> 32038 bytes .../wwwroot/lib/bootstrap/LICENSE | 22 + .../lib/bootstrap/dist/css/bootstrap-grid.css | 3719 ++++++ .../bootstrap/dist/css/bootstrap-grid.min.css | 7 + .../bootstrap/dist/css/bootstrap-reboot.css | 331 + .../dist/css/bootstrap-reboot.min.css | 8 + .../lib/bootstrap/dist/css/bootstrap.css | 10038 ++++++++++++++++ .../lib/bootstrap/dist/css/bootstrap.min.css | 7 + .../jquery-validation-unobtrusive/LICENSE.txt | 12 + .../wwwroot/lib/jquery-validation/LICENSE.md | 22 + .../wwwroot/lib/jquery/LICENSE.txt | 36 + .../BPA.AspNetCore.Test.csproj | 11 + .../Client/Client_Connection_Samples.cs | 275 + .../Client/Client_Publish_Samples.cs | 46 + .../Client/Client_Subscribe_Samples.cs | 83 + BPA.AspNetCore.Test/Pages/Error.cshtml | 26 + BPA.AspNetCore.Test/Pages/Error.cshtml.cs | 31 + BPA.AspNetCore.Test/Pages/Index.cshtml | 10 + BPA.AspNetCore.Test/Pages/Index.cshtml.cs | 25 + BPA.AspNetCore.Test/Pages/Privacy.cshtml | 8 + BPA.AspNetCore.Test/Pages/Privacy.cshtml.cs | 24 + .../Pages/Shared/_Layout.cshtml | 50 + .../Shared/_ValidationScriptsPartial.cshtml | 2 + BPA.AspNetCore.Test/Pages/_ViewImports.cshtml | 3 + BPA.AspNetCore.Test/Pages/_ViewStart.cshtml | 3 + BPA.AspNetCore.Test/Program.cs | 26 + BPA.AspNetCore.Test/Startup.cs | 56 + .../appsettings.Development.json | 9 + BPA.AspNetCore.Test/appsettings.json | 10 + BPA.AspNetCore.Test/wwwroot/css/site.css | 71 + BPA.AspNetCore.Test/wwwroot/favicon.ico | Bin 0 -> 32038 bytes .../wwwroot/lib/bootstrap/LICENSE | 22 + .../lib/bootstrap/dist/css/bootstrap-grid.css | 3719 ++++++ .../bootstrap/dist/css/bootstrap-grid.min.css | 7 + .../bootstrap/dist/css/bootstrap-reboot.css | 331 + .../dist/css/bootstrap-reboot.min.css | 8 + .../lib/bootstrap/dist/css/bootstrap.css | 10038 ++++++++++++++++ .../lib/bootstrap/dist/css/bootstrap.min.css | 7 + .../jquery-validation-unobtrusive/LICENSE.txt | 12 + .../wwwroot/lib/jquery-validation/LICENSE.md | 22 + .../wwwroot/lib/jquery/LICENSE.txt | 36 + MQTTnet.sln | 41 +- Samples/Client/Client_Subscribe_Samples.cs | 3 +- .../ApplicationBuilderExtensions.cs | 52 + .../AspNetMqttServerOptionsBuilder.cs | 19 + ...BPA - Backup (1).MQTTnet.AspNetCore.csproj | 61 + .../BPA - Backup.MQTTnet.AspNetCore.csproj | 61 + .../BPA.MQTTnet.AspNetCore.csproj | 75 + .../MqttClientConnectionContextFactory.cs | 38 + .../Client/Tcp/BufferExtensions.cs | 26 + .../Client/Tcp/DuplexPipe.cs | 45 + .../Client/Tcp/SocketAwaitable.cs | 74 + .../Client/Tcp/SocketReceiver.cs | 41 + .../Client/Tcp/SocketSender.cs | 105 + .../Client/Tcp/TcpConnection.cs | 272 + .../ConnectionBuilderExtensions.cs | 16 + .../ConnectionRouteBuilderExtensions.cs | 29 + .../EndpointRouterExtensions.cs | 25 + .../MqttConnectionContext.cs | 193 + .../MqttConnectionHandler.cs | 58 + .../MqttHostedServer.cs | 33 + .../MqttSubProtocolSelector.cs | 37 + .../MqttWebSocketServerAdapter.cs | 67 + .../ReaderExtensions.cs | 113 + .../ServiceCollectionExtensions.cs | 105 + .../Adapter/IMqttChannelAdapter.cs | 41 + .../Adapter/IMqttClientAdapterFactory.cs | 14 + .../BPA.MQTTnet/Adapter/IMqttServerAdapter.cs | 19 + .../BPA.MQTTnet/Adapter/MqttChannelAdapter.cs | 426 + .../Adapter/MqttConnectingFailedException.cs | 23 + .../Adapter/MqttPacketInspector.cs | 99 + .../BPA.MQTTnet/Adapter/ReceivedMqttPacket.cs | 26 + Source/BPA.MQTTnet/BPA.MQTTnet.csproj | 97 + .../Certificates/BlobCertificateProvider.cs | 32 + .../Certificates/ICertificateProvider.cs | 13 + .../Certificates/X509CertificateProvider.cs | 26 + Source/BPA.MQTTnet/Channel/IMqttChannel.cs | 24 + .../Connecting/MqttClientConnectResult.cs | 120 + .../Connecting/MqttClientConnectResultCode.cs | 32 + .../MqttClientConnectResultFactory.cs | 107 + .../MqttClientConnectedEventArgs.cs | 22 + .../MqttClientConnectingEventArgs.cs | 18 + .../MqttClientDisconnectOptions.cs | 21 + .../MqttClientDisconnectOptionsBuilder.cs | 33 + .../MqttClientDisconnectReason.cs | 40 + .../MqttClientDisconnectedEventArgs.cs | 29 + ...ttExtendedAuthenticationExchangeHandler.cs | 13 + ...ttExtendedAuthenticationExchangeContext.cs | 62 + .../MqttExtendedAuthenticationExchangeData.cs | 42 + Source/BPA.MQTTnet/Client/MqttClient.cs | 910 ++ .../Client/MqttClientConnectionStatus.cs | 14 + .../Client/MqttClientExtensions.cs | 140 + .../Client/MqttPacketIdentifierProvider.cs | 37 + .../Options/IMqttClientChannelOptions.cs | 11 + .../Options/IMqttClientCredentialsProvider.cs | 13 + ...qttClientCertificateValidationEventArgs.cs | 21 + .../Client/Options/MqttClientCredentials.cs | 28 + ...ientDefaultCertificateValidationHandler.cs | 40 + .../Client/Options/MqttClientOptions.cs | 182 + .../Options/MqttClientOptionsBuilder.cs | 395 + .../MqttClientOptionsBuilderTlsParameters.cs | 41 + ...ClientOptionsBuilderWebSocketParameters.cs | 16 + .../Client/Options/MqttClientTcpOptions.cs | 32 + .../Options/MqttClientTcpOptionsExtensions.cs | 23 + .../Client/Options/MqttClientTlsOptions.cs | 40 + .../Options/MqttClientWebSocketOptions.cs | 29 + .../MqttClientWebSocketProxyOptions.cs | 21 + .../Publishing/MqttClientPublishReasonCode.cs | 20 + .../Publishing/MqttClientPublishResult.cs | 38 + .../MqttClientPublishResultFactory.cs | 79 + ...MqttApplicationMessageReceivedEventArgs.cs | 86 + ...qttApplicationMessageReceivedReasonCode.cs | 20 + .../Subscribing/MqttClientSubscribeOptions.cs | 36 + .../MqttClientSubscribeOptionsBuilder.cs | 103 + .../Subscribing/MqttClientSubscribeResult.cs | 31 + .../MqttClientSubscribeResultCode.cs | 23 + .../MqttClientSubscribeResultFactory.cs | 57 + .../MqttClientSubscribeResultItem.cs | 23 + .../MqttClientUnsubscribeOptions.cs | 27 + .../MqttClientUnsubscribeOptionsBuilder.cs | 76 + .../MqttClientUnsubscribeResult.cs | 31 + .../MqttClientUnsubscribeResultCode.cs | 17 + .../MqttClientUnsubscribeResultFactory.cs | 62 + .../MqttClientUnsubscribeResultItem.cs | 21 + .../Diagnostics/Logger/IMqttNetLogger.cs | 15 + .../Diagnostics/Logger/MqttNetEventLogger.cs | 64 + .../Diagnostics/Logger/MqttNetLogLevel.cs | 17 + .../Diagnostics/Logger/MqttNetLogMessage.cs | 36 + .../MqttNetLogMessagePublishedEventArgs.cs | 18 + .../Diagnostics/Logger/MqttNetNullLogger.cs | 27 + .../Diagnostics/Logger/MqttNetSourceLogger.cs | 27 + .../Logger/MqttNetSourceLoggerExtensions.cs | 193 + .../InspectMqttPacketEventArgs.cs | 15 + .../MqttPacketFlowDirection.cs | 13 + .../Runtime/TargetFrameworkProvider.cs | 37 + .../Exceptions/MqttCommunicationException.cs | 25 + .../MqttCommunicationTimedOutException.cs | 19 + .../Exceptions/MqttConfigurationException.cs | 25 + .../MqttProtocolViolationException.cs | 16 + ...ttUnexpectedDisconnectReceivedException.cs | 33 + .../Formatter/IMqttPacketFormatter.cs | 16 + .../MqttApplicationMessageFactory.cs | 37 + .../BPA.MQTTnet/Formatter/MqttBufferReader.cs | 148 + .../BPA.MQTTnet/Formatter/MqttBufferWriter.cs | 284 + .../Formatter/MqttConnAckPacketFactory.cs | 41 + .../Formatter/MqttConnectPacketFactory.cs | 58 + .../MqttConnectReasonCodeConverter.cs | 96 + .../Formatter/MqttDisconnectPacketFactory.cs | 47 + .../BPA.MQTTnet/Formatter/MqttFixedHeader.cs | 22 + .../BPA.MQTTnet/Formatter/MqttPacketBuffer.cs | 65 + .../Formatter/MqttPacketFactories.cs | 32 + .../Formatter/MqttPacketFormatterAdapter.cs | 146 + .../Formatter/MqttProtocolVersion.cs | 15 + .../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 | 25 + .../Formatter/V3/MqttV3PacketFormatter.cs | 807 ++ .../Formatter/V5/MqttV5PacketDecoder.cs | 770 ++ .../Formatter/V5/MqttV5PacketEncoder.cs | 538 + .../Formatter/V5/MqttV5PacketFormatter.cs | 30 + .../Formatter/V5/MqttV5PropertiesReader.cs | 220 + .../Formatter/V5/MqttV5PropertiesWriter.cs | 351 + .../Implementations/CrossPlatformSocket.cs | 256 + .../MqttClientAdapterFactory.cs | 46 + .../Implementations/MqttTcpChannel.Uwp.cs | 200 + .../Implementations/MqttTcpChannel.cs | 291 + .../MqttTcpServerAdapter.Uwp.cs | 128 + .../Implementations/MqttTcpServerAdapter.cs | 134 + .../Implementations/MqttTcpServerListener.cs | 246 + .../Implementations/MqttWebSocketChannel.cs | 235 + .../PlatformAbstractionLayer.cs | 43 + Source/BPA.MQTTnet/Internal/AsyncEvent.cs | 88 + .../Internal/AsyncEventInvocator.cs | 48 + Source/BPA.MQTTnet/Internal/AsyncLock.cs | 86 + Source/BPA.MQTTnet/Internal/AsyncQueue.cs | 96 + .../Internal/AsyncQueueDequeueResult.cs | 19 + .../Internal/AsyncTaskCompletionSource.cs | 75 + Source/BPA.MQTTnet/Internal/BlockingQueue.cs | 127 + Source/BPA.MQTTnet/Internal/Disposable.cs | 41 + Source/BPA.MQTTnet/Internal/MqttPacketBus.cs | 140 + .../BPA.MQTTnet/Internal/MqttPacketBusItem.cs | 49 + .../Internal/MqttPacketBusPartition.cs | 15 + .../BPA.MQTTnet/Internal/MqttTaskTimeout.cs | 38 + Source/BPA.MQTTnet/Internal/TaskExtensions.cs | 23 + .../LowLevelClient/LowLevelMqttClient.cs | 147 + Source/BPA.MQTTnet/MQTTnet.csproj.DotSettings | 19 + Source/BPA.MQTTnet/MqttApplicationMessage.cs | 133 + .../MqttApplicationMessageBuilder.cs | 339 + .../MqttApplicationMessageExtensions.cs | 29 + Source/BPA.MQTTnet/MqttFactory.cs | 165 + Source/BPA.MQTTnet/MqttTopicFilterBuilder.cs | 98 + .../MqttTopicFilterCompareResult.cs | 17 + Source/BPA.MQTTnet/MqttTopicFilterComparer.cs | 178 + .../PacketDispatcher/IMqttPacketAwaitable.cs | 20 + .../PacketDispatcher/MqttPacketAwaitable.cs | 65 + .../MqttPacketAwaitableFilter.cs | 15 + .../PacketDispatcher/MqttPacketDispatcher.cs | 105 + Source/BPA.MQTTnet/Packets/MqttAuthPacket.cs | 23 + .../BPA.MQTTnet/Packets/MqttConnAckPacket.cs | 66 + .../BPA.MQTTnet/Packets/MqttConnectPacket.cs | 79 + .../Packets/MqttDisconnectPacket.cs | 42 + Source/BPA.MQTTnet/Packets/MqttPacket.cs | 10 + .../Packets/MqttPacketWithIdentifier.cs | 11 + .../BPA.MQTTnet/Packets/MqttPingReqPacket.cs | 17 + .../BPA.MQTTnet/Packets/MqttPingRespPacket.cs | 17 + .../BPA.MQTTnet/Packets/MqttPubAckPacket.cs | 32 + .../BPA.MQTTnet/Packets/MqttPubCompPacket.cs | 32 + .../BPA.MQTTnet/Packets/MqttPubRecPacket.cs | 32 + .../BPA.MQTTnet/Packets/MqttPubRelPacket.cs | 32 + .../BPA.MQTTnet/Packets/MqttPublishPacket.cs | 44 + .../BPA.MQTTnet/Packets/MqttSubAckPacket.cs | 35 + .../Packets/MqttSubscribePacket.cs | 30 + Source/BPA.MQTTnet/Packets/MqttTopicFilter.cs | 55 + .../BPA.MQTTnet/Packets/MqttUnsubAckPacket.cs | 39 + .../Packets/MqttUnsubscribePacket.cs | 24 + .../BPA.MQTTnet/Packets/MqttUserProperty.cs | 51 + Source/BPA.MQTTnet/Properties/AssemblyInfo.cs | 4 + .../Protocol/MqttAuthenticateReasonCode.cs | 13 + .../Protocol/MqttConnectReasonCode.cs | 32 + .../Protocol/MqttConnectReturnCode.cs | 16 + .../Protocol/MqttControlPacketType.cs | 25 + .../Protocol/MqttDisconnectReasonCode.cs | 39 + .../Protocol/MqttPayloadFormatIndicator.cs | 12 + Source/BPA.MQTTnet/Protocol/MqttPropertyId.cs | 39 + .../Protocol/MqttPubAckReasonCode.cs | 19 + .../Protocol/MqttPubCompReasonCode.cs | 12 + .../Protocol/MqttPubRecReasonCode.cs | 19 + .../Protocol/MqttPubRelReasonCode.cs | 12 + .../Protocol/MqttQualityOfServiceLevel.cs | 13 + .../Protocol/MqttRetainHandling.cs | 15 + .../Protocol/MqttSubscribeReasonCode.cs | 25 + .../Protocol/MqttSubscribeReturnCode.cs | 14 + .../Protocol/MqttTopicValidator.cs | 57 + .../Protocol/MqttUnsubscribeReasonCode.cs | 17 + .../ApplicationMessageNotConsumedEventArgs.cs | 21 + .../Server/Events/ClientConnectedEventArgs.cs | 33 + .../Events/ClientDisconnectedEventArgs.cs | 21 + .../Events/ClientSubscribedTopicEventArgs.cs | 24 + .../ClientUnsubscribedTopicEventArgs.cs | 23 + .../Events/InterceptingPacketEventArgs.cs | 39 + .../Events/InterceptingPublishEventArgs.cs | 43 + .../InterceptingSubscriptionEventArgs.cs | 69 + .../InterceptingUnsubscriptionEventArgs.cs | 55 + .../LoadingRetainedMessagesEventArgs.cs | 14 + .../Events/PreparingSessionEventArgs.cs | 50 + .../Events/RetainedMessageChangedEventArgs.cs | 18 + .../Server/Events/SessionDeletedEventArgs.cs | 16 + .../Events/ValidatingConnectionEventArgs.cs | 184 + .../Server/InjectedMqttApplicationMessage.cs | 20 + .../Internal/CheckSubscriptionsResult.cs | 22 + .../ISubscriptionChangedNotification.cs | 11 + .../BPA.MQTTnet/Server/Internal/MqttClient.cs | 533 + .../Internal/MqttClientSessionsManager.cs | 647 + .../Server/Internal/MqttClientStatistics.cs | 95 + .../MqttClientSubscriptionsManager.cs | 533 + .../Internal/MqttRetainedMessagesManager.cs | 147 + .../Internal/MqttServerEventContainer.cs | 48 + .../Internal/MqttServerKeepAliveMonitor.cs | 123 + .../Server/Internal/MqttSession.cs | 161 + .../Server/Internal/MqttSubscription.cs | 309 + .../Server/Internal/MqttUnsubscribeResult.cs | 16 + .../Server/Internal/SubscribeResult.cs | 23 + .../Internal/TopicHashMaskSubscriptions.cs | 19 + .../Server/MqttClientDisconnectType.cs | 13 + .../Server/MqttRetainedMessageMatch.cs | 15 + Source/BPA.MQTTnet/Server/MqttServer.cs | 367 + .../Server/MqttServerExtensions.cs | 31 + .../IMqttServerCertificateCredentials.cs | 11 + .../MqttPendingMessagesOverflowStrategy.cs | 13 + .../MqttServerCertificateCredentials.cs | 11 + .../Server/Options/MqttServerOptions.cs | 39 + .../Options/MqttServerOptionsBuilder.cs | 210 + .../MqttServerTcpEndpointBaseOptions.cs | 35 + .../Options/MqttServerTcpEndpointOptions.cs | 14 + .../MqttServerTlsTcpEndpointOptions.cs | 28 + Source/BPA.MQTTnet/Server/PublishResponse.cs | 19 + .../Server/Status/MqttClientStatus.cs | 63 + .../Server/Status/MqttSessionStatus.cs | 62 + .../BPA.MQTTnet/Server/SubscribeResponse.cs | 24 + .../BPA.MQTTnet/Server/UnsubscribeResponse.cs | 23 + 306 files changed, 49987 insertions(+), 10 deletions(-) create mode 100644 BAP.MQTTnet.Test/BAP.MQTTnet.Test.csproj create mode 100644 BAP.MQTTnet.Test/Client/Client_Connection_Samples.cs create mode 100644 BAP.MQTTnet.Test/Client/Client_Publish_Samples.cs create mode 100644 BAP.MQTTnet.Test/Client/Client_Subscribe_Samples.cs create mode 100644 BAP.MQTTnet.Test/Controllers/HomeController.cs create mode 100644 BAP.MQTTnet.Test/Models/ErrorViewModel.cs create mode 100644 BAP.MQTTnet.Test/MqttClient.cs create mode 100644 BAP.MQTTnet.Test/Program.cs create mode 100644 BAP.MQTTnet.Test/Startup.cs create mode 100644 BAP.MQTTnet.Test/Views/Home/Index.cshtml create mode 100644 BAP.MQTTnet.Test/Views/Home/Privacy.cshtml create mode 100644 BAP.MQTTnet.Test/Views/Shared/Error.cshtml create mode 100644 BAP.MQTTnet.Test/Views/Shared/_Layout.cshtml create mode 100644 BAP.MQTTnet.Test/Views/Shared/_ValidationScriptsPartial.cshtml create mode 100644 BAP.MQTTnet.Test/Views/_ViewImports.cshtml create mode 100644 BAP.MQTTnet.Test/Views/_ViewStart.cshtml create mode 100644 BAP.MQTTnet.Test/appsettings.Development.json create mode 100644 BAP.MQTTnet.Test/appsettings.json create mode 100644 BAP.MQTTnet.Test/wwwroot/css/site.css create mode 100644 BAP.MQTTnet.Test/wwwroot/favicon.ico create mode 100644 BAP.MQTTnet.Test/wwwroot/lib/bootstrap/LICENSE create mode 100644 BAP.MQTTnet.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css create mode 100644 BAP.MQTTnet.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css create mode 100644 BAP.MQTTnet.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css create mode 100644 BAP.MQTTnet.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css create mode 100644 BAP.MQTTnet.Test/wwwroot/lib/bootstrap/dist/css/bootstrap.css create mode 100644 BAP.MQTTnet.Test/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css create mode 100644 BAP.MQTTnet.Test/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt create mode 100644 BAP.MQTTnet.Test/wwwroot/lib/jquery-validation/LICENSE.md create mode 100644 BAP.MQTTnet.Test/wwwroot/lib/jquery/LICENSE.txt create mode 100644 BPA.AspNetCore.Test/BPA.AspNetCore.Test.csproj create mode 100644 BPA.AspNetCore.Test/Client/Client_Connection_Samples.cs create mode 100644 BPA.AspNetCore.Test/Client/Client_Publish_Samples.cs create mode 100644 BPA.AspNetCore.Test/Client/Client_Subscribe_Samples.cs create mode 100644 BPA.AspNetCore.Test/Pages/Error.cshtml create mode 100644 BPA.AspNetCore.Test/Pages/Error.cshtml.cs create mode 100644 BPA.AspNetCore.Test/Pages/Index.cshtml create mode 100644 BPA.AspNetCore.Test/Pages/Index.cshtml.cs create mode 100644 BPA.AspNetCore.Test/Pages/Privacy.cshtml create mode 100644 BPA.AspNetCore.Test/Pages/Privacy.cshtml.cs create mode 100644 BPA.AspNetCore.Test/Pages/Shared/_Layout.cshtml create mode 100644 BPA.AspNetCore.Test/Pages/Shared/_ValidationScriptsPartial.cshtml create mode 100644 BPA.AspNetCore.Test/Pages/_ViewImports.cshtml create mode 100644 BPA.AspNetCore.Test/Pages/_ViewStart.cshtml create mode 100644 BPA.AspNetCore.Test/Program.cs create mode 100644 BPA.AspNetCore.Test/Startup.cs create mode 100644 BPA.AspNetCore.Test/appsettings.Development.json create mode 100644 BPA.AspNetCore.Test/appsettings.json create mode 100644 BPA.AspNetCore.Test/wwwroot/css/site.css create mode 100644 BPA.AspNetCore.Test/wwwroot/favicon.ico create mode 100644 BPA.AspNetCore.Test/wwwroot/lib/bootstrap/LICENSE create mode 100644 BPA.AspNetCore.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css create mode 100644 BPA.AspNetCore.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css create mode 100644 BPA.AspNetCore.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css create mode 100644 BPA.AspNetCore.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css create mode 100644 BPA.AspNetCore.Test/wwwroot/lib/bootstrap/dist/css/bootstrap.css create mode 100644 BPA.AspNetCore.Test/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css create mode 100644 BPA.AspNetCore.Test/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt create mode 100644 BPA.AspNetCore.Test/wwwroot/lib/jquery-validation/LICENSE.md create mode 100644 BPA.AspNetCore.Test/wwwroot/lib/jquery/LICENSE.txt create mode 100644 Source/BPA.MQTTnet.AspnetCore/ApplicationBuilderExtensions.cs create mode 100644 Source/BPA.MQTTnet.AspnetCore/AspNetMqttServerOptionsBuilder.cs create mode 100644 Source/BPA.MQTTnet.AspnetCore/BPA - Backup (1).MQTTnet.AspNetCore.csproj create mode 100644 Source/BPA.MQTTnet.AspnetCore/BPA - Backup.MQTTnet.AspNetCore.csproj create mode 100644 Source/BPA.MQTTnet.AspnetCore/BPA.MQTTnet.AspNetCore.csproj create mode 100644 Source/BPA.MQTTnet.AspnetCore/Client/MqttClientConnectionContextFactory.cs create mode 100644 Source/BPA.MQTTnet.AspnetCore/Client/Tcp/BufferExtensions.cs create mode 100644 Source/BPA.MQTTnet.AspnetCore/Client/Tcp/DuplexPipe.cs create mode 100644 Source/BPA.MQTTnet.AspnetCore/Client/Tcp/SocketAwaitable.cs create mode 100644 Source/BPA.MQTTnet.AspnetCore/Client/Tcp/SocketReceiver.cs create mode 100644 Source/BPA.MQTTnet.AspnetCore/Client/Tcp/SocketSender.cs create mode 100644 Source/BPA.MQTTnet.AspnetCore/Client/Tcp/TcpConnection.cs create mode 100644 Source/BPA.MQTTnet.AspnetCore/ConnectionBuilderExtensions.cs create mode 100644 Source/BPA.MQTTnet.AspnetCore/ConnectionRouteBuilderExtensions.cs create mode 100644 Source/BPA.MQTTnet.AspnetCore/EndpointRouterExtensions.cs create mode 100644 Source/BPA.MQTTnet.AspnetCore/MqttConnectionContext.cs create mode 100644 Source/BPA.MQTTnet.AspnetCore/MqttConnectionHandler.cs create mode 100644 Source/BPA.MQTTnet.AspnetCore/MqttHostedServer.cs create mode 100644 Source/BPA.MQTTnet.AspnetCore/MqttSubProtocolSelector.cs create mode 100644 Source/BPA.MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs create mode 100644 Source/BPA.MQTTnet.AspnetCore/ReaderExtensions.cs create mode 100644 Source/BPA.MQTTnet.AspnetCore/ServiceCollectionExtensions.cs create mode 100644 Source/BPA.MQTTnet/Adapter/IMqttChannelAdapter.cs create mode 100644 Source/BPA.MQTTnet/Adapter/IMqttClientAdapterFactory.cs create mode 100644 Source/BPA.MQTTnet/Adapter/IMqttServerAdapter.cs create mode 100644 Source/BPA.MQTTnet/Adapter/MqttChannelAdapter.cs create mode 100644 Source/BPA.MQTTnet/Adapter/MqttConnectingFailedException.cs create mode 100644 Source/BPA.MQTTnet/Adapter/MqttPacketInspector.cs create mode 100644 Source/BPA.MQTTnet/Adapter/ReceivedMqttPacket.cs create mode 100644 Source/BPA.MQTTnet/BPA.MQTTnet.csproj create mode 100644 Source/BPA.MQTTnet/Certificates/BlobCertificateProvider.cs create mode 100644 Source/BPA.MQTTnet/Certificates/ICertificateProvider.cs create mode 100644 Source/BPA.MQTTnet/Certificates/X509CertificateProvider.cs create mode 100644 Source/BPA.MQTTnet/Channel/IMqttChannel.cs create mode 100644 Source/BPA.MQTTnet/Client/Connecting/MqttClientConnectResult.cs create mode 100644 Source/BPA.MQTTnet/Client/Connecting/MqttClientConnectResultCode.cs create mode 100644 Source/BPA.MQTTnet/Client/Connecting/MqttClientConnectResultFactory.cs create mode 100644 Source/BPA.MQTTnet/Client/Connecting/MqttClientConnectedEventArgs.cs create mode 100644 Source/BPA.MQTTnet/Client/Connecting/MqttClientConnectingEventArgs.cs create mode 100644 Source/BPA.MQTTnet/Client/Disconnecting/MqttClientDisconnectOptions.cs create mode 100644 Source/BPA.MQTTnet/Client/Disconnecting/MqttClientDisconnectOptionsBuilder.cs create mode 100644 Source/BPA.MQTTnet/Client/Disconnecting/MqttClientDisconnectReason.cs create mode 100644 Source/BPA.MQTTnet/Client/Disconnecting/MqttClientDisconnectedEventArgs.cs create mode 100644 Source/BPA.MQTTnet/Client/ExtendedAuthenticationExchange/IMqttExtendedAuthenticationExchangeHandler.cs create mode 100644 Source/BPA.MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeContext.cs create mode 100644 Source/BPA.MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeData.cs create mode 100644 Source/BPA.MQTTnet/Client/MqttClient.cs create mode 100644 Source/BPA.MQTTnet/Client/MqttClientConnectionStatus.cs create mode 100644 Source/BPA.MQTTnet/Client/MqttClientExtensions.cs create mode 100644 Source/BPA.MQTTnet/Client/MqttPacketIdentifierProvider.cs create mode 100644 Source/BPA.MQTTnet/Client/Options/IMqttClientChannelOptions.cs create mode 100644 Source/BPA.MQTTnet/Client/Options/IMqttClientCredentialsProvider.cs create mode 100644 Source/BPA.MQTTnet/Client/Options/MqttClientCertificateValidationEventArgs.cs create mode 100644 Source/BPA.MQTTnet/Client/Options/MqttClientCredentials.cs create mode 100644 Source/BPA.MQTTnet/Client/Options/MqttClientDefaultCertificateValidationHandler.cs create mode 100644 Source/BPA.MQTTnet/Client/Options/MqttClientOptions.cs create mode 100644 Source/BPA.MQTTnet/Client/Options/MqttClientOptionsBuilder.cs create mode 100644 Source/BPA.MQTTnet/Client/Options/MqttClientOptionsBuilderTlsParameters.cs create mode 100644 Source/BPA.MQTTnet/Client/Options/MqttClientOptionsBuilderWebSocketParameters.cs create mode 100644 Source/BPA.MQTTnet/Client/Options/MqttClientTcpOptions.cs create mode 100644 Source/BPA.MQTTnet/Client/Options/MqttClientTcpOptionsExtensions.cs create mode 100644 Source/BPA.MQTTnet/Client/Options/MqttClientTlsOptions.cs create mode 100644 Source/BPA.MQTTnet/Client/Options/MqttClientWebSocketOptions.cs create mode 100644 Source/BPA.MQTTnet/Client/Options/MqttClientWebSocketProxyOptions.cs create mode 100644 Source/BPA.MQTTnet/Client/Publishing/MqttClientPublishReasonCode.cs create mode 100644 Source/BPA.MQTTnet/Client/Publishing/MqttClientPublishResult.cs create mode 100644 Source/BPA.MQTTnet/Client/Publishing/MqttClientPublishResultFactory.cs create mode 100644 Source/BPA.MQTTnet/Client/Receiving/MqttApplicationMessageReceivedEventArgs.cs create mode 100644 Source/BPA.MQTTnet/Client/Receiving/MqttApplicationMessageReceivedReasonCode.cs create mode 100644 Source/BPA.MQTTnet/Client/Subscribing/MqttClientSubscribeOptions.cs create mode 100644 Source/BPA.MQTTnet/Client/Subscribing/MqttClientSubscribeOptionsBuilder.cs create mode 100644 Source/BPA.MQTTnet/Client/Subscribing/MqttClientSubscribeResult.cs create mode 100644 Source/BPA.MQTTnet/Client/Subscribing/MqttClientSubscribeResultCode.cs create mode 100644 Source/BPA.MQTTnet/Client/Subscribing/MqttClientSubscribeResultFactory.cs create mode 100644 Source/BPA.MQTTnet/Client/Subscribing/MqttClientSubscribeResultItem.cs create mode 100644 Source/BPA.MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeOptions.cs create mode 100644 Source/BPA.MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeOptionsBuilder.cs create mode 100644 Source/BPA.MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResult.cs create mode 100644 Source/BPA.MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultCode.cs create mode 100644 Source/BPA.MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultFactory.cs create mode 100644 Source/BPA.MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultItem.cs create mode 100644 Source/BPA.MQTTnet/Diagnostics/Logger/IMqttNetLogger.cs create mode 100644 Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetEventLogger.cs create mode 100644 Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetLogLevel.cs create mode 100644 Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetLogMessage.cs create mode 100644 Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetLogMessagePublishedEventArgs.cs create mode 100644 Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetNullLogger.cs create mode 100644 Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetSourceLogger.cs create mode 100644 Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetSourceLoggerExtensions.cs create mode 100644 Source/BPA.MQTTnet/Diagnostics/PacketInspection/InspectMqttPacketEventArgs.cs create mode 100644 Source/BPA.MQTTnet/Diagnostics/PacketInspection/MqttPacketFlowDirection.cs create mode 100644 Source/BPA.MQTTnet/Diagnostics/Runtime/TargetFrameworkProvider.cs create mode 100644 Source/BPA.MQTTnet/Exceptions/MqttCommunicationException.cs create mode 100644 Source/BPA.MQTTnet/Exceptions/MqttCommunicationTimedOutException.cs create mode 100644 Source/BPA.MQTTnet/Exceptions/MqttConfigurationException.cs create mode 100644 Source/BPA.MQTTnet/Exceptions/MqttProtocolViolationException.cs create mode 100644 Source/BPA.MQTTnet/Exceptions/MqttUnexpectedDisconnectReceivedException.cs create mode 100644 Source/BPA.MQTTnet/Formatter/IMqttPacketFormatter.cs create mode 100644 Source/BPA.MQTTnet/Formatter/MqttApplicationMessageFactory.cs create mode 100644 Source/BPA.MQTTnet/Formatter/MqttBufferReader.cs create mode 100644 Source/BPA.MQTTnet/Formatter/MqttBufferWriter.cs create mode 100644 Source/BPA.MQTTnet/Formatter/MqttConnAckPacketFactory.cs create mode 100644 Source/BPA.MQTTnet/Formatter/MqttConnectPacketFactory.cs create mode 100644 Source/BPA.MQTTnet/Formatter/MqttConnectReasonCodeConverter.cs create mode 100644 Source/BPA.MQTTnet/Formatter/MqttDisconnectPacketFactory.cs create mode 100644 Source/BPA.MQTTnet/Formatter/MqttFixedHeader.cs create mode 100644 Source/BPA.MQTTnet/Formatter/MqttPacketBuffer.cs create mode 100644 Source/BPA.MQTTnet/Formatter/MqttPacketFactories.cs create mode 100644 Source/BPA.MQTTnet/Formatter/MqttPacketFormatterAdapter.cs create mode 100644 Source/BPA.MQTTnet/Formatter/MqttProtocolVersion.cs create mode 100644 Source/BPA.MQTTnet/Formatter/MqttPubAckPacketFactory.cs create mode 100644 Source/BPA.MQTTnet/Formatter/MqttPubCompPacketFactory.cs create mode 100644 Source/BPA.MQTTnet/Formatter/MqttPubRecPacketFactory.cs create mode 100644 Source/BPA.MQTTnet/Formatter/MqttPubRelPacketFactory.cs create mode 100644 Source/BPA.MQTTnet/Formatter/MqttPublishPacketFactory.cs create mode 100644 Source/BPA.MQTTnet/Formatter/MqttSubAckPacketFactory.cs create mode 100644 Source/BPA.MQTTnet/Formatter/MqttSubscribePacketFactory.cs create mode 100644 Source/BPA.MQTTnet/Formatter/MqttUnsubAckPacketFactory.cs create mode 100644 Source/BPA.MQTTnet/Formatter/MqttUnsubscribePacketFactory.cs create mode 100644 Source/BPA.MQTTnet/Formatter/ReadFixedHeaderResult.cs create mode 100644 Source/BPA.MQTTnet/Formatter/V3/MqttV3PacketFormatter.cs create mode 100644 Source/BPA.MQTTnet/Formatter/V5/MqttV5PacketDecoder.cs create mode 100644 Source/BPA.MQTTnet/Formatter/V5/MqttV5PacketEncoder.cs create mode 100644 Source/BPA.MQTTnet/Formatter/V5/MqttV5PacketFormatter.cs create mode 100644 Source/BPA.MQTTnet/Formatter/V5/MqttV5PropertiesReader.cs create mode 100644 Source/BPA.MQTTnet/Formatter/V5/MqttV5PropertiesWriter.cs create mode 100644 Source/BPA.MQTTnet/Implementations/CrossPlatformSocket.cs create mode 100644 Source/BPA.MQTTnet/Implementations/MqttClientAdapterFactory.cs create mode 100644 Source/BPA.MQTTnet/Implementations/MqttTcpChannel.Uwp.cs create mode 100644 Source/BPA.MQTTnet/Implementations/MqttTcpChannel.cs create mode 100644 Source/BPA.MQTTnet/Implementations/MqttTcpServerAdapter.Uwp.cs create mode 100644 Source/BPA.MQTTnet/Implementations/MqttTcpServerAdapter.cs create mode 100644 Source/BPA.MQTTnet/Implementations/MqttTcpServerListener.cs create mode 100644 Source/BPA.MQTTnet/Implementations/MqttWebSocketChannel.cs create mode 100644 Source/BPA.MQTTnet/Implementations/PlatformAbstractionLayer.cs create mode 100644 Source/BPA.MQTTnet/Internal/AsyncEvent.cs create mode 100644 Source/BPA.MQTTnet/Internal/AsyncEventInvocator.cs create mode 100644 Source/BPA.MQTTnet/Internal/AsyncLock.cs create mode 100644 Source/BPA.MQTTnet/Internal/AsyncQueue.cs create mode 100644 Source/BPA.MQTTnet/Internal/AsyncQueueDequeueResult.cs create mode 100644 Source/BPA.MQTTnet/Internal/AsyncTaskCompletionSource.cs create mode 100644 Source/BPA.MQTTnet/Internal/BlockingQueue.cs create mode 100644 Source/BPA.MQTTnet/Internal/Disposable.cs create mode 100644 Source/BPA.MQTTnet/Internal/MqttPacketBus.cs create mode 100644 Source/BPA.MQTTnet/Internal/MqttPacketBusItem.cs create mode 100644 Source/BPA.MQTTnet/Internal/MqttPacketBusPartition.cs create mode 100644 Source/BPA.MQTTnet/Internal/MqttTaskTimeout.cs create mode 100644 Source/BPA.MQTTnet/Internal/TaskExtensions.cs create mode 100644 Source/BPA.MQTTnet/LowLevelClient/LowLevelMqttClient.cs create mode 100644 Source/BPA.MQTTnet/MQTTnet.csproj.DotSettings create mode 100644 Source/BPA.MQTTnet/MqttApplicationMessage.cs create mode 100644 Source/BPA.MQTTnet/MqttApplicationMessageBuilder.cs create mode 100644 Source/BPA.MQTTnet/MqttApplicationMessageExtensions.cs create mode 100644 Source/BPA.MQTTnet/MqttFactory.cs create mode 100644 Source/BPA.MQTTnet/MqttTopicFilterBuilder.cs create mode 100644 Source/BPA.MQTTnet/MqttTopicFilterCompareResult.cs create mode 100644 Source/BPA.MQTTnet/MqttTopicFilterComparer.cs create mode 100644 Source/BPA.MQTTnet/PacketDispatcher/IMqttPacketAwaitable.cs create mode 100644 Source/BPA.MQTTnet/PacketDispatcher/MqttPacketAwaitable.cs create mode 100644 Source/BPA.MQTTnet/PacketDispatcher/MqttPacketAwaitableFilter.cs create mode 100644 Source/BPA.MQTTnet/PacketDispatcher/MqttPacketDispatcher.cs create mode 100644 Source/BPA.MQTTnet/Packets/MqttAuthPacket.cs create mode 100644 Source/BPA.MQTTnet/Packets/MqttConnAckPacket.cs create mode 100644 Source/BPA.MQTTnet/Packets/MqttConnectPacket.cs create mode 100644 Source/BPA.MQTTnet/Packets/MqttDisconnectPacket.cs create mode 100644 Source/BPA.MQTTnet/Packets/MqttPacket.cs create mode 100644 Source/BPA.MQTTnet/Packets/MqttPacketWithIdentifier.cs create mode 100644 Source/BPA.MQTTnet/Packets/MqttPingReqPacket.cs create mode 100644 Source/BPA.MQTTnet/Packets/MqttPingRespPacket.cs create mode 100644 Source/BPA.MQTTnet/Packets/MqttPubAckPacket.cs create mode 100644 Source/BPA.MQTTnet/Packets/MqttPubCompPacket.cs create mode 100644 Source/BPA.MQTTnet/Packets/MqttPubRecPacket.cs create mode 100644 Source/BPA.MQTTnet/Packets/MqttPubRelPacket.cs create mode 100644 Source/BPA.MQTTnet/Packets/MqttPublishPacket.cs create mode 100644 Source/BPA.MQTTnet/Packets/MqttSubAckPacket.cs create mode 100644 Source/BPA.MQTTnet/Packets/MqttSubscribePacket.cs create mode 100644 Source/BPA.MQTTnet/Packets/MqttTopicFilter.cs create mode 100644 Source/BPA.MQTTnet/Packets/MqttUnsubAckPacket.cs create mode 100644 Source/BPA.MQTTnet/Packets/MqttUnsubscribePacket.cs create mode 100644 Source/BPA.MQTTnet/Packets/MqttUserProperty.cs create mode 100644 Source/BPA.MQTTnet/Properties/AssemblyInfo.cs create mode 100644 Source/BPA.MQTTnet/Protocol/MqttAuthenticateReasonCode.cs create mode 100644 Source/BPA.MQTTnet/Protocol/MqttConnectReasonCode.cs create mode 100644 Source/BPA.MQTTnet/Protocol/MqttConnectReturnCode.cs create mode 100644 Source/BPA.MQTTnet/Protocol/MqttControlPacketType.cs create mode 100644 Source/BPA.MQTTnet/Protocol/MqttDisconnectReasonCode.cs create mode 100644 Source/BPA.MQTTnet/Protocol/MqttPayloadFormatIndicator.cs create mode 100644 Source/BPA.MQTTnet/Protocol/MqttPropertyId.cs create mode 100644 Source/BPA.MQTTnet/Protocol/MqttPubAckReasonCode.cs create mode 100644 Source/BPA.MQTTnet/Protocol/MqttPubCompReasonCode.cs create mode 100644 Source/BPA.MQTTnet/Protocol/MqttPubRecReasonCode.cs create mode 100644 Source/BPA.MQTTnet/Protocol/MqttPubRelReasonCode.cs create mode 100644 Source/BPA.MQTTnet/Protocol/MqttQualityOfServiceLevel.cs create mode 100644 Source/BPA.MQTTnet/Protocol/MqttRetainHandling.cs create mode 100644 Source/BPA.MQTTnet/Protocol/MqttSubscribeReasonCode.cs create mode 100644 Source/BPA.MQTTnet/Protocol/MqttSubscribeReturnCode.cs create mode 100644 Source/BPA.MQTTnet/Protocol/MqttTopicValidator.cs create mode 100644 Source/BPA.MQTTnet/Protocol/MqttUnsubscribeReasonCode.cs create mode 100644 Source/BPA.MQTTnet/Server/Events/ApplicationMessageNotConsumedEventArgs.cs create mode 100644 Source/BPA.MQTTnet/Server/Events/ClientConnectedEventArgs.cs create mode 100644 Source/BPA.MQTTnet/Server/Events/ClientDisconnectedEventArgs.cs create mode 100644 Source/BPA.MQTTnet/Server/Events/ClientSubscribedTopicEventArgs.cs create mode 100644 Source/BPA.MQTTnet/Server/Events/ClientUnsubscribedTopicEventArgs.cs create mode 100644 Source/BPA.MQTTnet/Server/Events/InterceptingPacketEventArgs.cs create mode 100644 Source/BPA.MQTTnet/Server/Events/InterceptingPublishEventArgs.cs create mode 100644 Source/BPA.MQTTnet/Server/Events/InterceptingSubscriptionEventArgs.cs create mode 100644 Source/BPA.MQTTnet/Server/Events/InterceptingUnsubscriptionEventArgs.cs create mode 100644 Source/BPA.MQTTnet/Server/Events/LoadingRetainedMessagesEventArgs.cs create mode 100644 Source/BPA.MQTTnet/Server/Events/PreparingSessionEventArgs.cs create mode 100644 Source/BPA.MQTTnet/Server/Events/RetainedMessageChangedEventArgs.cs create mode 100644 Source/BPA.MQTTnet/Server/Events/SessionDeletedEventArgs.cs create mode 100644 Source/BPA.MQTTnet/Server/Events/ValidatingConnectionEventArgs.cs create mode 100644 Source/BPA.MQTTnet/Server/InjectedMqttApplicationMessage.cs create mode 100644 Source/BPA.MQTTnet/Server/Internal/CheckSubscriptionsResult.cs create mode 100644 Source/BPA.MQTTnet/Server/Internal/ISubscriptionChangedNotification.cs create mode 100644 Source/BPA.MQTTnet/Server/Internal/MqttClient.cs create mode 100644 Source/BPA.MQTTnet/Server/Internal/MqttClientSessionsManager.cs create mode 100644 Source/BPA.MQTTnet/Server/Internal/MqttClientStatistics.cs create mode 100644 Source/BPA.MQTTnet/Server/Internal/MqttClientSubscriptionsManager.cs create mode 100644 Source/BPA.MQTTnet/Server/Internal/MqttRetainedMessagesManager.cs create mode 100644 Source/BPA.MQTTnet/Server/Internal/MqttServerEventContainer.cs create mode 100644 Source/BPA.MQTTnet/Server/Internal/MqttServerKeepAliveMonitor.cs create mode 100644 Source/BPA.MQTTnet/Server/Internal/MqttSession.cs create mode 100644 Source/BPA.MQTTnet/Server/Internal/MqttSubscription.cs create mode 100644 Source/BPA.MQTTnet/Server/Internal/MqttUnsubscribeResult.cs create mode 100644 Source/BPA.MQTTnet/Server/Internal/SubscribeResult.cs create mode 100644 Source/BPA.MQTTnet/Server/Internal/TopicHashMaskSubscriptions.cs create mode 100644 Source/BPA.MQTTnet/Server/MqttClientDisconnectType.cs create mode 100644 Source/BPA.MQTTnet/Server/MqttRetainedMessageMatch.cs create mode 100644 Source/BPA.MQTTnet/Server/MqttServer.cs create mode 100644 Source/BPA.MQTTnet/Server/MqttServerExtensions.cs create mode 100644 Source/BPA.MQTTnet/Server/Options/IMqttServerCertificateCredentials.cs create mode 100644 Source/BPA.MQTTnet/Server/Options/MqttPendingMessagesOverflowStrategy.cs create mode 100644 Source/BPA.MQTTnet/Server/Options/MqttServerCertificateCredentials.cs create mode 100644 Source/BPA.MQTTnet/Server/Options/MqttServerOptions.cs create mode 100644 Source/BPA.MQTTnet/Server/Options/MqttServerOptionsBuilder.cs create mode 100644 Source/BPA.MQTTnet/Server/Options/MqttServerTcpEndpointBaseOptions.cs create mode 100644 Source/BPA.MQTTnet/Server/Options/MqttServerTcpEndpointOptions.cs create mode 100644 Source/BPA.MQTTnet/Server/Options/MqttServerTlsTcpEndpointOptions.cs create mode 100644 Source/BPA.MQTTnet/Server/PublishResponse.cs create mode 100644 Source/BPA.MQTTnet/Server/Status/MqttClientStatus.cs create mode 100644 Source/BPA.MQTTnet/Server/Status/MqttSessionStatus.cs create mode 100644 Source/BPA.MQTTnet/Server/SubscribeResponse.cs create mode 100644 Source/BPA.MQTTnet/Server/UnsubscribeResponse.cs diff --git a/BAP.MQTTnet.Test/BAP.MQTTnet.Test.csproj b/BAP.MQTTnet.Test/BAP.MQTTnet.Test.csproj new file mode 100644 index 0000000..fb30fdb --- /dev/null +++ b/BAP.MQTTnet.Test/BAP.MQTTnet.Test.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp3.1 + + + + + + + + diff --git a/BAP.MQTTnet.Test/Client/Client_Connection_Samples.cs b/BAP.MQTTnet.Test/Client/Client_Connection_Samples.cs new file mode 100644 index 0000000..1efc0fe --- /dev/null +++ b/BAP.MQTTnet.Test/Client/Client_Connection_Samples.cs @@ -0,0 +1,275 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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.Client; +using MQTTnet.Formatter; +using System; +using System.Security.Authentication; +using System.Threading; +using System.Threading.Tasks; + +namespace BPA.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("10.2.1.21",1883).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/BAP.MQTTnet.Test/Client/Client_Publish_Samples.cs b/BAP.MQTTnet.Test/Client/Client_Publish_Samples.cs new file mode 100644 index 0000000..a191236 --- /dev/null +++ b/BAP.MQTTnet.Test/Client/Client_Publish_Samples.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using MQTTnet.Client; +using System; +using System.Threading; +using System.Threading.Tasks; + +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("10.2.1.21", 1883) + .Build(); + + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + + var applicationMessage = new MqttApplicationMessageBuilder() + .WithTopic("mqttnet/samples/topic/2") + .WithPayload("19.asssssssssss5") + .Build(); + + await mqttClient.PublishAsync(applicationMessage, CancellationToken.None); + + Console.WriteLine("MQTT application message is published."); + } + } + } +} \ No newline at end of file diff --git a/BAP.MQTTnet.Test/Client/Client_Subscribe_Samples.cs b/BAP.MQTTnet.Test/Client/Client_Subscribe_Samples.cs new file mode 100644 index 0000000..dcc462a --- /dev/null +++ b/BAP.MQTTnet.Test/Client/Client_Subscribe_Samples.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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.Threading; +using System.Threading.Tasks; + +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("10.2.1.21",1883) + .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/BAP.MQTTnet.Test/Controllers/HomeController.cs b/BAP.MQTTnet.Test/Controllers/HomeController.cs new file mode 100644 index 0000000..a3e590d --- /dev/null +++ b/BAP.MQTTnet.Test/Controllers/HomeController.cs @@ -0,0 +1,153 @@ +using BAP.MQTTnet.Test.Models; +using BPA.MQTTnet.Samples.Client; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using MQTTnet; +using MQTTnet.Client; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace BAP.MQTTnet.Test.Controllers +{ + public class HomeController : Controller + { + private readonly ILogger _logger; + + public HomeController(ILogger logger) + { + // Client_Connection_Samples.Connect_Client(); + // Task.WaitAll(Publish_Application_Message()); + Handle_Received_Application_Message(); + _logger = logger; + } + + public IActionResult Index() + { + return View(); + } + + public IActionResult Privacy() + { + return View(); + } + + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public IActionResult Error() + { + return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); + } + + 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("10.2.1.21", 1883) + .Build(); + + await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); + + var applicationMessage = new MqttApplicationMessageBuilder() + .WithTopic("mqttnet/samples/topic/2") + .WithPayload("19.asssssssssss5") + .Build(); + + await mqttClient.PublishAsync(applicationMessage, CancellationToken.None); + + Console.WriteLine("MQTT application message is published."); + } + } + + public 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("10.2.1.21", 1883) + .WithCredentials("client1","222") + .WithTimeout(TimeSpan.FromMilliseconds(200)) + .WithClientId(Guid.NewGuid().ToString().Substring(0,5)) + .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. + + + var p= await mqttClient.ConnectAsync(mqttClientOptions); + + //mqttClient.UseConnectedHandler(async e => + //{ + // Console.WriteLine("### CONNECTED WITH SERVER ###"); + + // // Subscribe to a topic + // await mqttClient.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("my/topic").Build()); + + // Console.WriteLine("### SUBSCRIBED ###"); + //}); + + var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder() + .WithTopicFilter(f => { f.WithTopic("mqttnet/samples/topic/2"); }) + .Build(); + + mqttClient.ApplicationMessageReceivedAsync += e => + { + var a = e.ApplicationMessage.Topic; + Console.WriteLine("Received application message."); + // e.DumpToConsole(); + + return Task.CompletedTask; + }; + mqttClient.DisconnectedAsync+=async e => + { + Console.WriteLine("与服务器之间的连接断开了,正在尝试重新连接"); + await Task.Delay(TimeSpan.FromSeconds(5)); + try + { + // 重新连接 + await mqttClient.ConnectAsync(mqttClientOptions); + } + catch (Exception ex) + { + Console.WriteLine($"重新连接服务器失败:{ex}"); + } + + //return Task.CompletedTask; + }; + await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None); + //using (var timeoutToken = new CancellationTokenSource(TimeSpan.FromSeconds(1))) + //{ + + //} + + // var data = await mqttClient.SubscribeAsync(mqttSubscribeOptions); + + //Console.WriteLine("MQTT client subscribed to topic."); + + //Console.WriteLine("Press enter to exit."); + Console.ReadLine(); + } + } + } +} diff --git a/BAP.MQTTnet.Test/Models/ErrorViewModel.cs b/BAP.MQTTnet.Test/Models/ErrorViewModel.cs new file mode 100644 index 0000000..c3f8585 --- /dev/null +++ b/BAP.MQTTnet.Test/Models/ErrorViewModel.cs @@ -0,0 +1,11 @@ +using System; + +namespace BAP.MQTTnet.Test.Models +{ + public class ErrorViewModel + { + public string RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + } +} diff --git a/BAP.MQTTnet.Test/MqttClient.cs b/BAP.MQTTnet.Test/MqttClient.cs new file mode 100644 index 0000000..55d59ca --- /dev/null +++ b/BAP.MQTTnet.Test/MqttClient.cs @@ -0,0 +1,433 @@ + +using MQTTnet; +using System; +using System.Text; + +namespace MQTT +{ + public class MqttClient + { + #region Field Area + + /// + /// Mqtt factory + /// + private MqttFactory _factory; + /// + /// Mqtt client + /// + private IMqttClient _mqttClient; + /// + /// Mqtt 配置信息 + /// + private MqttClientConfig _mqttClientConfig; + /// + /// Mqtt options + /// + private IMqttClientOptions _options; + + #endregion + + #region CTOR + + /// + /// 默认启动IP为127.0.0.1 端口为1883 + /// + public MqttClient() + { + _mqttClientConfig = new MqttClientConfig + { + ServerIp = "127.0.0.1", + Port = 1883 + }; + Init(); + + // + // 调用示例 + /// + /// //var client = new MqttClient(s => + /// { + /// s.Port = 1883; + /// s.ServerIp = "127.0.0.1"; + /// s.UserName = "mqtt-test"; + /// s.Password = "mqtt-test"; + /// s.ReciveMsgCallback = s => + /// { + /// Console.WriteLine(s.Payload_UTF8); + /// }; + /// }, true); + /// client.Subscribe("/TopicName/"); + /// + /// 客户端配置信息 + /// 直接启动 + public MqttClient(Action config, bool autoStart = false) + { + _mqttClientConfig = new MqttClientConfig(); + config(_mqttClientConfig); + Init(); + if (autoStart) + { + Start(); + } + } + + + #endregion + + /// + /// 获取MqttClient实例 + /// + /// 调用示例 + /// //var client = MqttClient.Instance(s => + /// { + /// s.Port = 1883; + /// s.ServerIp = "127.0.0.1"; + /// s.UserName = "mqtt-test"; + /// s.Password = "mqtt-test"; + /// s.ReciveMsgCallback = s => + /// { + /// Console.WriteLine(s.Payload_UTF8); + /// }; + /// }, true); + /// client.Subscribe("/TopicName/"); + /// + /// 客户端配置信息 + /// 直接启动 + /// + public static MqttClient Instance(Action config, bool autoStart = false) + => new MqttClient(config, autoStart); + + /// + /// 初始化注册 + /// + private void Init() + { + try + { + _factory = new MqttFactory(); + + _mqttClient = _factory.CreateMqttClient(); + + _options = new MqttClientOptionsBuilder() + WithTcpServer(_mqttClientConfig.ServerIp, _mqttClientConfig.Port) + WithCredentials(_mqttClientConfig.UserName, _mqttClientConfig.Password) + WithClientId(_mqttClientConfig.ClientId) + Build(); + + //消息回调 + _mqttClient.UseApplicationMessageReceivedHandler(ReciveMsg); + } + catch (Exception exp) + { + if (_mqttClientConfig.Exception is null) + { + throw exp; + } + _mqttClientConfig.Exception(exp); + } + } + + + #region 内部事件转换处理 + + /// + /// 消息接收回调 + /// + /// + /// + private void ReciveMsg(MqttApplicationMessageReceivedEventArgs e) + { + if (_mqttClientConfig.ReciveMsgCallback != null) + { + _mqttClientConfig.ReciveMsgCallback(new MqttClientReciveMsg + { + Topic = e.ApplicationMessage.Topic, + Payload_UTF8 = Encoding.UTF8.GetString(e.ApplicationMessage.Payload), + Payload = e.ApplicationMessage.Payload, + Qos = e.ApplicationMessage.QualityOfServiceLevel, + Retain = e.ApplicationMessage.Retain, + }); + } + } + + /// + /// 订阅 + /// + /// + public async void Subscribe(string topicName) + { + topicName = topicName.Trim(); + if (string.IsNullOrEmpty(topicName)) + { + throw new Exception("订阅主题不能为空!"); + } + + if (!_mqttClient.IsConnected) + { + throw new Exception("MQTT客户端尚未连接!请先启动连接"); + } + await _mqttClient.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic(topicName).Build()); + } + + /// + /// 取消订阅 + /// + /// + public async void Unsubscribe(string topicName) + { + topicName = topicName.Trim(); + if (string.IsNullOrEmpty(topicName)) + { + throw new Exception("订阅主题不能为空!"); + } + + if (!_mqttClient.IsConnected) + { + throw new Exception("MQTT客户端尚未连接!请先启动连接"); + } + await _mqttClient.UnsubscribeAsync(topicName); + } + + /// + /// 重连机制 + /// + private void ReConnected() + { + if (_mqttClient.IsConnected) + { + return; + } + for (int i = 0; i < 10; i++) + { + //重连机制 + _mqttClient.UseDisconnectedHandler(async e => + { + try + { + await Task.Delay(TimeSpan.FromSeconds(_mqttClientConfig.ReconneTime)); + await _mqttClient.ConnectAsync(_options); + return; + } + catch (Exception exp) + { + if (_mqttClientConfig.Exception is null) + { + throw exp; + } + _mqttClientConfig.Exception(exp); + } + }); + } + } + + + #endregion + + + #region 消息发送 + + /// + /// 发送消息,含有重连机制,如掉线会自动重连 + /// + /// + private async Task PublishAsync(string topicName, MqttApplicationMessageBuilder MessageBuilder, PublicQos qos = 0) + { + 233 string topic = topicName.Trim(); + 234 if (string.IsNullOrEmpty(topic)) + 235 { + 236 throw new Exception("主题不能为空!"); + } + ReConnected(); + + MessageBuilder.WithTopic(topic).WithRetainFlag(); + if (qos == PublicQos.Qos_0) + { + MessageBuilder.WithAtLeastOnceQoS(); + } + else if (qos == PublicQos.Qos_1) + { + MessageBuilder.WithAtMostOnceQoS(); + } + else + { + MessageBuilder.WithExactlyOnceQoS(); + } + var Message = MessageBuilder.Build(); + try + { + await _mqttClient.PublishAsync(Message); + } + catch (Exception e) + { + if (_mqttClientConfig.Exception is null) + { + throw e; + } + _mqttClientConfig.Exception(e); + } + + } + + /// + /// 发送消息,含有重连机制,如掉线会自动重连 + /// + /// 文字消息 + public async Task Publish(string topicName, string message, PublicQos qos = 0) + { + await PublishAsync(topicName, new MqttApplicationMessageBuilder() + .WithPayload(message), qos); + } + /// + /// 发送消息,含有重连机制,如掉线会自动重连 + /// + /// 消息流 + public async void Publish(string topicName, Stream message, PublicQos qos = 0) + => await PublishAsync(topicName, new MqttApplicationMessageBuilder() + .WithPayload(message), qos); + + /// + /// 发送消息,含有重连机制,如掉线会自动重连 + /// + /// Byte消息 + public async void Publish(string topicName, IEnumerable message, PublicQos qos = 0) + => await PublishAsync(topicName, new MqttApplicationMessageBuilder() + .WithPayload(message), qos); + + /// + /// 发送消息,含有重连机制,如掉线会自动重连 + /// + /// Byte消息 + public async void Publish(string topicName, byte[] message, PublicQos qos = 0) + => await PublishAsync(topicName, new MqttApplicationMessageBuilder() + .WithPayload(message), qos); + + + #endregion + + /// + /// 启动服务 + /// + /// + public async Task Start() + => await _mqttClient.ConnectAsync(_options); + + /// + /// 停止服务 + /// + /// + public async Task Stop() + => await _mqttClient.DisconnectAsync(new MqttClientDisconnectOptions { ReasonCode = MqttClientDisconnectReason.NormalDisconnection }, CancellationToken.None); + + } + public class MqttClientConfig + { + private string _serverIp; + /// + /// 服务器IP + /// + public string ServerIp + { + get => _serverIp; + set + { + if (string.IsNullOrEmpty(value.Trim())) + { + throw new ArgumentException("ServerIp can't be null or empty!"); + } + _serverIp = value; + } + } + private int _port; + /// + /// 服务器端口 + /// + public int Port + { + get => _port; + set + { + if (value <= 0) + { + throw new ArgumentException("Port can't below the zero!"); + } + _port = value; + } + } + + /// + /// 用户名 + /// + public string UserName { get; set; } + /// + /// 密码 + /// + public string Password { get; set; } + + private string _clientId; + /// + /// 唯一用户ID,默认使用Guid + /// + public string ClientId + { + get + { + _clientId = _clientId ?? Guid.NewGuid().ToString(); + return _clientId; + } + set => _clientId = value; + } + /// + /// 客户端掉线重连时间,单位/s,默认5s + /// + public double ReconneTime { get; set; } = 5; + /// + /// 异常回调,默认为空,为空抛异常 + /// + public Action Exception = null; + + /// + /// 接收消息回调,默认不接收 + /// + public Action ReciveMsgCallback = null; + } + public class MqttClientReciveMsg + { + /// + /// 主题 + /// + public string Topic { get; set; } + /// + /// UTF-8格式下的 负载/消息 + /// + public string Payload_UTF8 { get; set; } + /// + /// 原始 负载/消息 + /// + public byte[] Payload { get; set; } + /// + /// Qos + /// + public MqttQualityOfServiceLevel Qos { get; set; } + /// + /// 保留 + /// + public bool Retain { get; set; } + } + public enum PublicQos + { + /// + /// //At most once,至多一次 + /// + Qos_0, + /// + /// //At least once,至少一次 + /// + Qos_1, + /// + /// //QoS2,Exactly once + /// + Qos_2, + } + } +} + diff --git a/BAP.MQTTnet.Test/Program.cs b/BAP.MQTTnet.Test/Program.cs new file mode 100644 index 0000000..c2d7012 --- /dev/null +++ b/BAP.MQTTnet.Test/Program.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BAP.MQTTnet.Test +{ + 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/BAP.MQTTnet.Test/Startup.cs b/BAP.MQTTnet.Test/Startup.cs new file mode 100644 index 0000000..f0de8d4 --- /dev/null +++ b/BAP.MQTTnet.Test/Startup.cs @@ -0,0 +1,53 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BAP.MQTTnet.Test +{ + 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. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllersWithViews(); + } + + // 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(); + } + else + { + app.UseExceptionHandler("/Home/Error"); + } + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/BAP.MQTTnet.Test/Views/Home/Index.cshtml b/BAP.MQTTnet.Test/Views/Home/Index.cshtml new file mode 100644 index 0000000..d2d19bd --- /dev/null +++ b/BAP.MQTTnet.Test/Views/Home/Index.cshtml @@ -0,0 +1,8 @@ +@{ + ViewData["Title"] = "Home Page"; +} + +
+

Welcome

+

Learn about building Web apps with ASP.NET Core.

+
diff --git a/BAP.MQTTnet.Test/Views/Home/Privacy.cshtml b/BAP.MQTTnet.Test/Views/Home/Privacy.cshtml new file mode 100644 index 0000000..af4fb19 --- /dev/null +++ b/BAP.MQTTnet.Test/Views/Home/Privacy.cshtml @@ -0,0 +1,6 @@ +@{ + ViewData["Title"] = "Privacy Policy"; +} +

@ViewData["Title"]

+ +

Use this page to detail your site's privacy policy.

diff --git a/BAP.MQTTnet.Test/Views/Shared/Error.cshtml b/BAP.MQTTnet.Test/Views/Shared/Error.cshtml new file mode 100644 index 0000000..a1e0478 --- /dev/null +++ b/BAP.MQTTnet.Test/Views/Shared/Error.cshtml @@ -0,0 +1,25 @@ +@model ErrorViewModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

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

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more 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/BAP.MQTTnet.Test/Views/Shared/_Layout.cshtml b/BAP.MQTTnet.Test/Views/Shared/_Layout.cshtml new file mode 100644 index 0000000..8e26146 --- /dev/null +++ b/BAP.MQTTnet.Test/Views/Shared/_Layout.cshtml @@ -0,0 +1,48 @@ + + + + + + @ViewData["Title"] - BAP.MQTTnet.Test + + + + +
+ +
+
+
+ @RenderBody() +
+
+ +
+
+ © 2022 - BAP.MQTTnet.Test - Privacy +
+
+ + + + @RenderSection("Scripts", required: false) + + diff --git a/BAP.MQTTnet.Test/Views/Shared/_ValidationScriptsPartial.cshtml b/BAP.MQTTnet.Test/Views/Shared/_ValidationScriptsPartial.cshtml new file mode 100644 index 0000000..5a16d80 --- /dev/null +++ b/BAP.MQTTnet.Test/Views/Shared/_ValidationScriptsPartial.cshtml @@ -0,0 +1,2 @@ + + diff --git a/BAP.MQTTnet.Test/Views/_ViewImports.cshtml b/BAP.MQTTnet.Test/Views/_ViewImports.cshtml new file mode 100644 index 0000000..ffc705c --- /dev/null +++ b/BAP.MQTTnet.Test/Views/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using BAP.MQTTnet.Test +@using BAP.MQTTnet.Test.Models +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/BAP.MQTTnet.Test/Views/_ViewStart.cshtml b/BAP.MQTTnet.Test/Views/_ViewStart.cshtml new file mode 100644 index 0000000..a5f1004 --- /dev/null +++ b/BAP.MQTTnet.Test/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/BAP.MQTTnet.Test/appsettings.Development.json b/BAP.MQTTnet.Test/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/BAP.MQTTnet.Test/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/BAP.MQTTnet.Test/appsettings.json b/BAP.MQTTnet.Test/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/BAP.MQTTnet.Test/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/BAP.MQTTnet.Test/wwwroot/css/site.css b/BAP.MQTTnet.Test/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/BAP.MQTTnet.Test/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/BAP.MQTTnet.Test/wwwroot/favicon.ico b/BAP.MQTTnet.Test/wwwroot/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a3a799985c43bc7309d701b2cad129023377dc71 GIT binary patch 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_ .col, +.no-gutters > [class*="col-"] { + padding-right: 0; + padding-left: 0; +} + +.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col, +.col-auto, .col-sm-1, .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-10, .col-sm-11, .col-sm-12, .col-sm, +.col-sm-auto, .col-md-1, .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-10, .col-md-11, .col-md-12, .col-md, +.col-md-auto, .col-lg-1, .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-10, .col-lg-11, .col-lg-12, .col-lg, +.col-lg-auto, .col-xl-1, .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-10, .col-xl-11, .col-xl-12, .col-xl, +.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%; + } +} + +.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; + } +} + +.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; + } +} + +.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: 0.25rem !important; +} + +.mt-1, +.my-1 { + margin-top: 0.25rem !important; +} + +.mr-1, +.mx-1 { + margin-right: 0.25rem !important; +} + +.mb-1, +.my-1 { + margin-bottom: 0.25rem !important; +} + +.ml-1, +.mx-1 { + margin-left: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.mt-2, +.my-2 { + margin-top: 0.5rem !important; +} + +.mr-2, +.mx-2 { + margin-right: 0.5rem !important; +} + +.mb-2, +.my-2 { + margin-bottom: 0.5rem !important; +} + +.ml-2, +.mx-2 { + margin-left: 0.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: 0.25rem !important; +} + +.pt-1, +.py-1 { + padding-top: 0.25rem !important; +} + +.pr-1, +.px-1 { + padding-right: 0.25rem !important; +} + +.pb-1, +.py-1 { + padding-bottom: 0.25rem !important; +} + +.pl-1, +.px-1 { + padding-left: 0.25rem !important; +} + +.p-2 { + padding: 0.5rem !important; +} + +.pt-2, +.py-2 { + padding-top: 0.5rem !important; +} + +.pr-2, +.px-2 { + padding-right: 0.5rem !important; +} + +.pb-2, +.py-2 { + padding-bottom: 0.5rem !important; +} + +.pl-2, +.px-2 { + padding-left: 0.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: -0.25rem !important; +} + +.mt-n1, +.my-n1 { + margin-top: -0.25rem !important; +} + +.mr-n1, +.mx-n1 { + margin-right: -0.25rem !important; +} + +.mb-n1, +.my-n1 { + margin-bottom: -0.25rem !important; +} + +.ml-n1, +.mx-n1 { + margin-left: -0.25rem !important; +} + +.m-n2 { + margin: -0.5rem !important; +} + +.mt-n2, +.my-n2 { + margin-top: -0.5rem !important; +} + +.mr-n2, +.mx-n2 { + margin-right: -0.5rem !important; +} + +.mb-n2, +.my-n2 { + margin-bottom: -0.5rem !important; +} + +.ml-n2, +.mx-n2 { + margin-left: -0.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: 0.25rem !important; + } + .mt-sm-1, + .my-sm-1 { + margin-top: 0.25rem !important; + } + .mr-sm-1, + .mx-sm-1 { + margin-right: 0.25rem !important; + } + .mb-sm-1, + .my-sm-1 { + margin-bottom: 0.25rem !important; + } + .ml-sm-1, + .mx-sm-1 { + margin-left: 0.25rem !important; + } + .m-sm-2 { + margin: 0.5rem !important; + } + .mt-sm-2, + .my-sm-2 { + margin-top: 0.5rem !important; + } + .mr-sm-2, + .mx-sm-2 { + margin-right: 0.5rem !important; + } + .mb-sm-2, + .my-sm-2 { + margin-bottom: 0.5rem !important; + } + .ml-sm-2, + .mx-sm-2 { + margin-left: 0.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: 0.25rem !important; + } + .pt-sm-1, + .py-sm-1 { + padding-top: 0.25rem !important; + } + .pr-sm-1, + .px-sm-1 { + padding-right: 0.25rem !important; + } + .pb-sm-1, + .py-sm-1 { + padding-bottom: 0.25rem !important; + } + .pl-sm-1, + .px-sm-1 { + padding-left: 0.25rem !important; + } + .p-sm-2 { + padding: 0.5rem !important; + } + .pt-sm-2, + .py-sm-2 { + padding-top: 0.5rem !important; + } + .pr-sm-2, + .px-sm-2 { + padding-right: 0.5rem !important; + } + .pb-sm-2, + .py-sm-2 { + padding-bottom: 0.5rem !important; + } + .pl-sm-2, + .px-sm-2 { + padding-left: 0.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: -0.25rem !important; + } + .mt-sm-n1, + .my-sm-n1 { + margin-top: -0.25rem !important; + } + .mr-sm-n1, + .mx-sm-n1 { + margin-right: -0.25rem !important; + } + .mb-sm-n1, + .my-sm-n1 { + margin-bottom: -0.25rem !important; + } + .ml-sm-n1, + .mx-sm-n1 { + margin-left: -0.25rem !important; + } + .m-sm-n2 { + margin: -0.5rem !important; + } + .mt-sm-n2, + .my-sm-n2 { + margin-top: -0.5rem !important; + } + .mr-sm-n2, + .mx-sm-n2 { + margin-right: -0.5rem !important; + } + .mb-sm-n2, + .my-sm-n2 { + margin-bottom: -0.5rem !important; + } + .ml-sm-n2, + .mx-sm-n2 { + margin-left: -0.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: 0.25rem !important; + } + .mt-md-1, + .my-md-1 { + margin-top: 0.25rem !important; + } + .mr-md-1, + .mx-md-1 { + margin-right: 0.25rem !important; + } + .mb-md-1, + .my-md-1 { + margin-bottom: 0.25rem !important; + } + .ml-md-1, + .mx-md-1 { + margin-left: 0.25rem !important; + } + .m-md-2 { + margin: 0.5rem !important; + } + .mt-md-2, + .my-md-2 { + margin-top: 0.5rem !important; + } + .mr-md-2, + .mx-md-2 { + margin-right: 0.5rem !important; + } + .mb-md-2, + .my-md-2 { + margin-bottom: 0.5rem !important; + } + .ml-md-2, + .mx-md-2 { + margin-left: 0.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: 0.25rem !important; + } + .pt-md-1, + .py-md-1 { + padding-top: 0.25rem !important; + } + .pr-md-1, + .px-md-1 { + padding-right: 0.25rem !important; + } + .pb-md-1, + .py-md-1 { + padding-bottom: 0.25rem !important; + } + .pl-md-1, + .px-md-1 { + padding-left: 0.25rem !important; + } + .p-md-2 { + padding: 0.5rem !important; + } + .pt-md-2, + .py-md-2 { + padding-top: 0.5rem !important; + } + .pr-md-2, + .px-md-2 { + padding-right: 0.5rem !important; + } + .pb-md-2, + .py-md-2 { + padding-bottom: 0.5rem !important; + } + .pl-md-2, + .px-md-2 { + padding-left: 0.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: -0.25rem !important; + } + .mt-md-n1, + .my-md-n1 { + margin-top: -0.25rem !important; + } + .mr-md-n1, + .mx-md-n1 { + margin-right: -0.25rem !important; + } + .mb-md-n1, + .my-md-n1 { + margin-bottom: -0.25rem !important; + } + .ml-md-n1, + .mx-md-n1 { + margin-left: -0.25rem !important; + } + .m-md-n2 { + margin: -0.5rem !important; + } + .mt-md-n2, + .my-md-n2 { + margin-top: -0.5rem !important; + } + .mr-md-n2, + .mx-md-n2 { + margin-right: -0.5rem !important; + } + .mb-md-n2, + .my-md-n2 { + margin-bottom: -0.5rem !important; + } + .ml-md-n2, + .mx-md-n2 { + margin-left: -0.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: 0.25rem !important; + } + .mt-lg-1, + .my-lg-1 { + margin-top: 0.25rem !important; + } + .mr-lg-1, + .mx-lg-1 { + margin-right: 0.25rem !important; + } + .mb-lg-1, + .my-lg-1 { + margin-bottom: 0.25rem !important; + } + .ml-lg-1, + .mx-lg-1 { + margin-left: 0.25rem !important; + } + .m-lg-2 { + margin: 0.5rem !important; + } + .mt-lg-2, + .my-lg-2 { + margin-top: 0.5rem !important; + } + .mr-lg-2, + .mx-lg-2 { + margin-right: 0.5rem !important; + } + .mb-lg-2, + .my-lg-2 { + margin-bottom: 0.5rem !important; + } + .ml-lg-2, + .mx-lg-2 { + margin-left: 0.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: 0.25rem !important; + } + .pt-lg-1, + .py-lg-1 { + padding-top: 0.25rem !important; + } + .pr-lg-1, + .px-lg-1 { + padding-right: 0.25rem !important; + } + .pb-lg-1, + .py-lg-1 { + padding-bottom: 0.25rem !important; + } + .pl-lg-1, + .px-lg-1 { + padding-left: 0.25rem !important; + } + .p-lg-2 { + padding: 0.5rem !important; + } + .pt-lg-2, + .py-lg-2 { + padding-top: 0.5rem !important; + } + .pr-lg-2, + .px-lg-2 { + padding-right: 0.5rem !important; + } + .pb-lg-2, + .py-lg-2 { + padding-bottom: 0.5rem !important; + } + .pl-lg-2, + .px-lg-2 { + padding-left: 0.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: -0.25rem !important; + } + .mt-lg-n1, + .my-lg-n1 { + margin-top: -0.25rem !important; + } + .mr-lg-n1, + .mx-lg-n1 { + margin-right: -0.25rem !important; + } + .mb-lg-n1, + .my-lg-n1 { + margin-bottom: -0.25rem !important; + } + .ml-lg-n1, + .mx-lg-n1 { + margin-left: -0.25rem !important; + } + .m-lg-n2 { + margin: -0.5rem !important; + } + .mt-lg-n2, + .my-lg-n2 { + margin-top: -0.5rem !important; + } + .mr-lg-n2, + .mx-lg-n2 { + margin-right: -0.5rem !important; + } + .mb-lg-n2, + .my-lg-n2 { + margin-bottom: -0.5rem !important; + } + .ml-lg-n2, + .mx-lg-n2 { + margin-left: -0.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: 0.25rem !important; + } + .mt-xl-1, + .my-xl-1 { + margin-top: 0.25rem !important; + } + .mr-xl-1, + .mx-xl-1 { + margin-right: 0.25rem !important; + } + .mb-xl-1, + .my-xl-1 { + margin-bottom: 0.25rem !important; + } + .ml-xl-1, + .mx-xl-1 { + margin-left: 0.25rem !important; + } + .m-xl-2 { + margin: 0.5rem !important; + } + .mt-xl-2, + .my-xl-2 { + margin-top: 0.5rem !important; + } + .mr-xl-2, + .mx-xl-2 { + margin-right: 0.5rem !important; + } + .mb-xl-2, + .my-xl-2 { + margin-bottom: 0.5rem !important; + } + .ml-xl-2, + .mx-xl-2 { + margin-left: 0.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: 0.25rem !important; + } + .pt-xl-1, + .py-xl-1 { + padding-top: 0.25rem !important; + } + .pr-xl-1, + .px-xl-1 { + padding-right: 0.25rem !important; + } + .pb-xl-1, + .py-xl-1 { + padding-bottom: 0.25rem !important; + } + .pl-xl-1, + .px-xl-1 { + padding-left: 0.25rem !important; + } + .p-xl-2 { + padding: 0.5rem !important; + } + .pt-xl-2, + .py-xl-2 { + padding-top: 0.5rem !important; + } + .pr-xl-2, + .px-xl-2 { + padding-right: 0.5rem !important; + } + .pb-xl-2, + .py-xl-2 { + padding-bottom: 0.5rem !important; + } + .pl-xl-2, + .px-xl-2 { + padding-left: 0.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: -0.25rem !important; + } + .mt-xl-n1, + .my-xl-n1 { + margin-top: -0.25rem !important; + } + .mr-xl-n1, + .mx-xl-n1 { + margin-right: -0.25rem !important; + } + .mb-xl-n1, + .my-xl-n1 { + margin-bottom: -0.25rem !important; + } + .ml-xl-n1, + .mx-xl-n1 { + margin-left: -0.25rem !important; + } + .m-xl-n2 { + margin: -0.5rem !important; + } + .mt-xl-n2, + .my-xl-n2 { + margin-top: -0.5rem !important; + } + .mr-xl-n2, + .mx-xl-n2 { + margin-right: -0.5rem !important; + } + .mb-xl-n2, + .my-xl-n2 { + margin-bottom: -0.5rem !important; + } + .ml-xl-n2, + .mx-xl-n2 { + margin-left: -0.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; + } +} +/*# sourceMappingURL=bootstrap-grid.css.map */ \ No newline at end of file diff --git a/BAP.MQTTnet.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css b/BAP.MQTTnet.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css new file mode 100644 index 0000000..e5e74f7 --- /dev/null +++ b/BAP.MQTTnet.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap Grid 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) + */html{box-sizing:border-box;-ms-overflow-style:scrollbar}*,::after,::before{box-sizing:inherit}.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%}}.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}}.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}}.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}} +/*# sourceMappingURL=bootstrap-grid.min.css.map */ \ No newline at end of file diff --git a/BAP.MQTTnet.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css b/BAP.MQTTnet.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css new file mode 100644 index 0000000..09cf986 --- /dev/null +++ b/BAP.MQTTnet.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css @@ -0,0 +1,331 @@ +/*! + * Bootstrap Reboot 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) + * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) + */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + line-height: 1.15; + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +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: 0.5rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +abbr[title], +abbr[data-original-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; +} + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; +} + +ol ol, +ul ul, +ol ul, +ul ol { + 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]):hover, a:not([href]):not([tabindex]):focus { + color: inherit; + text-decoration: none; +} + +a:not([href]):not([tabindex]):focus { + outline: 0; +} + +pre, +code, +kbd, +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: 0.75rem; + padding-bottom: 0.75rem; + color: #6c757d; + text-align: left; + caption-side: bottom; +} + +th { + text-align: inherit; +} + +label { + display: inline-block; + margin-bottom: 0.5rem; +} + +button { + border-radius: 0; +} + +button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; +} + +input, +button, +select, +optgroup, +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; +} + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +button:not(:disabled), +[type="button"]:not(:disabled), +[type="reset"]:not(:disabled), +[type="submit"]:not(:disabled) { + cursor: pointer; +} + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + padding: 0; + border-style: none; +} + +input[type="radio"], +input[type="checkbox"] { + box-sizing: border-box; + padding: 0; +} + +input[type="date"], +input[type="time"], +input[type="datetime-local"], +input[type="month"] { + -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; +} +/*# sourceMappingURL=bootstrap-reboot.css.map */ \ No newline at end of file diff --git a/BAP.MQTTnet.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css b/BAP.MQTTnet.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css new file mode 100644 index 0000000..c804b3b --- /dev/null +++ b/BAP.MQTTnet.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css @@ -0,0 +1,8 @@ +/*! + * Bootstrap Reboot 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) + * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) + */*,::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} +/*# sourceMappingURL=bootstrap-reboot.min.css.map */ \ No newline at end of file diff --git a/BAP.MQTTnet.Test/wwwroot/lib/bootstrap/dist/css/bootstrap.css b/BAP.MQTTnet.Test/wwwroot/lib/bootstrap/dist/css/bootstrap.css new file mode 100644 index 0000000..8f47589 --- /dev/null +++ b/BAP.MQTTnet.Test/wwwroot/lib/bootstrap/dist/css/bootstrap.css @@ -0,0 +1,10038 @@ +/*! + * 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; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + line-height: 1.15; + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +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: 0.5rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +abbr[title], +abbr[data-original-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; +} + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; +} + +ol ol, +ul ul, +ol ul, +ul ol { + 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]):hover, a:not([href]):not([tabindex]):focus { + color: inherit; + text-decoration: none; +} + +a:not([href]):not([tabindex]):focus { + outline: 0; +} + +pre, +code, +kbd, +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: 0.75rem; + padding-bottom: 0.75rem; + color: #6c757d; + text-align: left; + caption-side: bottom; +} + +th { + text-align: inherit; +} + +label { + display: inline-block; + margin-bottom: 0.5rem; +} + +button { + border-radius: 0; +} + +button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; +} + +input, +button, +select, +optgroup, +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; +} + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +button:not(:disabled), +[type="button"]:not(:disabled), +[type="reset"]:not(:disabled), +[type="submit"]:not(:disabled) { + cursor: pointer; +} + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + padding: 0; + border-style: none; +} + +input[type="radio"], +input[type="checkbox"] { + box-sizing: border-box; + padding: 0; +} + +input[type="date"], +input[type="time"], +input[type="datetime-local"], +input[type="month"] { + -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: 0.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, 0.1); +} + +small, +.small { + font-size: 80%; + font-weight: 400; +} + +mark, +.mark { + padding: 0.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: 0.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: 0.25rem; + background-color: #fff; + border: 1px solid #dee2e6; + border-radius: 0.25rem; + max-width: 100%; + height: auto; +} + +.figure { + display: inline-block; +} + +.figure-img { + margin-bottom: 0.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: 0.2rem 0.4rem; + font-size: 87.5%; + color: #fff; + background-color: #212529; + border-radius: 0.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-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col, +.col-auto, .col-sm-1, .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-10, .col-sm-11, .col-sm-12, .col-sm, +.col-sm-auto, .col-md-1, .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-10, .col-md-11, .col-md-12, .col-md, +.col-md-auto, .col-lg-1, .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-10, .col-lg-11, .col-lg-12, .col-lg, +.col-lg-auto, .col-xl-1, .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-10, .col-xl-11, .col-xl-12, .col-xl, +.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 th, +.table td { + padding: 0.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 th, +.table-sm td { + padding: 0.3rem; +} + +.table-bordered { + border: 1px solid #dee2e6; +} + +.table-bordered th, +.table-bordered td { + border: 1px solid #dee2e6; +} + +.table-bordered thead th, +.table-bordered thead td { + border-bottom-width: 2px; +} + +.table-borderless th, +.table-borderless td, +.table-borderless thead th, +.table-borderless tbody + tbody { + border: 0; +} + +.table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(0, 0, 0, 0.05); +} + +.table-hover tbody tr:hover { + color: #212529; + background-color: rgba(0, 0, 0, 0.075); +} + +.table-primary, +.table-primary > th, +.table-primary > td { + background-color: #b8daff; +} + +.table-primary th, +.table-primary td, +.table-primary thead th, +.table-primary tbody + tbody { + 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 > th, +.table-secondary > td { + background-color: #d6d8db; +} + +.table-secondary th, +.table-secondary td, +.table-secondary thead th, +.table-secondary tbody + tbody { + 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 > th, +.table-success > td { + background-color: #c3e6cb; +} + +.table-success th, +.table-success td, +.table-success thead th, +.table-success tbody + tbody { + 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 > th, +.table-info > td { + background-color: #bee5eb; +} + +.table-info th, +.table-info td, +.table-info thead th, +.table-info tbody + tbody { + 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 > th, +.table-warning > td { + background-color: #ffeeba; +} + +.table-warning th, +.table-warning td, +.table-warning thead th, +.table-warning tbody + tbody { + 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 > th, +.table-danger > td { + background-color: #f5c6cb; +} + +.table-danger th, +.table-danger td, +.table-danger thead th, +.table-danger tbody + tbody { + 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 > th, +.table-light > td { + background-color: #fdfdfe; +} + +.table-light th, +.table-light td, +.table-light thead th, +.table-light tbody + tbody { + 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 > th, +.table-dark > td { + background-color: #c6c8ca; +} + +.table-dark th, +.table-dark td, +.table-dark thead th, +.table-dark tbody + tbody { + 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 > th, +.table-active > td { + background-color: rgba(0, 0, 0, 0.075); +} + +.table-hover .table-active:hover { + background-color: rgba(0, 0, 0, 0.075); +} + +.table-hover .table-active:hover > td, +.table-hover .table-active:hover > th { + background-color: rgba(0, 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 th, +.table-dark td, +.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, 0.05); +} + +.table-dark.table-hover tbody tr:hover { + color: #fff; + background-color: rgba(255, 255, 255, 0.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 + 0.75rem + 2px); + padding: 0.375rem 0.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: 0.25rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.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 0.2rem rgba(0, 123, 255, 0.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(0.375rem + 1px); + padding-bottom: calc(0.375rem + 1px); + margin-bottom: 0; + font-size: inherit; + line-height: 1.5; +} + +.col-form-label-lg { + padding-top: calc(0.5rem + 1px); + padding-bottom: calc(0.5rem + 1px); + font-size: 1.25rem; + line-height: 1.5; +} + +.col-form-label-sm { + padding-top: calc(0.25rem + 1px); + padding-bottom: calc(0.25rem + 1px); + font-size: 0.875rem; + line-height: 1.5; +} + +.form-control-plaintext { + display: block; + width: 100%; + padding-top: 0.375rem; + padding-bottom: 0.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-sm, .form-control-plaintext.form-control-lg { + padding-right: 0; + padding-left: 0; +} + +.form-control-sm { + height: calc(1.5em + 0.5rem + 2px); + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; +} + +.form-control-lg { + height: calc(1.5em + 1rem + 2px); + padding: 0.5rem 1rem; + font-size: 1.25rem; + line-height: 1.5; + border-radius: 0.3rem; +} + +select.form-control[size], select.form-control[multiple] { + height: auto; +} + +textarea.form-control { + height: auto; +} + +.form-group { + margin-bottom: 1rem; +} + +.form-text { + display: block; + margin-top: 0.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: 0.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: 0.75rem; +} + +.form-check-inline .form-check-input { + position: static; + margin-top: 0; + margin-right: 0.3125rem; + margin-left: 0; +} + +.valid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 80%; + color: #28a745; +} + +.valid-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: 0.25rem 0.5rem; + margin-top: .1rem; + font-size: 0.875rem; + line-height: 1.5; + color: #fff; + background-color: rgba(40, 167, 69, 0.9); + border-radius: 0.25rem; +} + +.was-validated .form-control:valid, .form-control.is-valid { + border-color: #28a745; + padding-right: calc(1.5em + 0.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(0.375em + 0.1875rem); + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} + +.was-validated .form-control:valid:focus, .form-control.is-valid:focus { + border-color: #28a745; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.was-validated .form-control:valid ~ .valid-feedback, +.was-validated .form-control:valid ~ .valid-tooltip, .form-control.is-valid ~ .valid-feedback, +.form-control.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated textarea.form-control:valid, textarea.form-control.is-valid { + padding-right: calc(1.5em + 0.75rem); + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); +} + +.was-validated .custom-select:valid, .custom-select.is-valid { + border-color: #28a745; + padding-right: calc((1em + 0.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 0.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(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} + +.was-validated .custom-select:valid:focus, .custom-select.is-valid:focus { + border-color: #28a745; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.was-validated .custom-select:valid ~ .valid-feedback, +.was-validated .custom-select:valid ~ .valid-tooltip, .custom-select.is-valid ~ .valid-feedback, +.custom-select.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .form-control-file:valid ~ .valid-feedback, +.was-validated .form-control-file:valid ~ .valid-tooltip, .form-control-file.is-valid ~ .valid-feedback, +.form-control-file.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label { + color: #28a745; +} + +.was-validated .form-check-input:valid ~ .valid-feedback, +.was-validated .form-check-input:valid ~ .valid-tooltip, .form-check-input.is-valid ~ .valid-feedback, +.form-check-input.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .custom-control-input:valid ~ .custom-control-label, .custom-control-input.is-valid ~ .custom-control-label { + color: #28a745; +} + +.was-validated .custom-control-input:valid ~ .custom-control-label::before, .custom-control-input.is-valid ~ .custom-control-label::before { + border-color: #28a745; +} + +.was-validated .custom-control-input:valid ~ .valid-feedback, +.was-validated .custom-control-input:valid ~ .valid-tooltip, .custom-control-input.is-valid ~ .valid-feedback, +.custom-control-input.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before, .custom-control-input.is-valid:checked ~ .custom-control-label::before { + border-color: #34ce57; + background-color: #34ce57; +} + +.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before, .custom-control-input.is-valid:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before { + border-color: #28a745; +} + +.was-validated .custom-file-input:valid ~ .custom-file-label, .custom-file-input.is-valid ~ .custom-file-label { + border-color: #28a745; +} + +.was-validated .custom-file-input:valid ~ .valid-feedback, +.was-validated .custom-file-input:valid ~ .valid-tooltip, .custom-file-input.is-valid ~ .valid-feedback, +.custom-file-input.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .custom-file-input:valid:focus ~ .custom-file-label, .custom-file-input.is-valid:focus ~ .custom-file-label { + border-color: #28a745; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.invalid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 80%; + color: #dc3545; +} + +.invalid-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: 0.25rem 0.5rem; + margin-top: .1rem; + font-size: 0.875rem; + line-height: 1.5; + color: #fff; + background-color: rgba(220, 53, 69, 0.9); + border-radius: 0.25rem; +} + +.was-validated .form-control:invalid, .form-control.is-invalid { + border-color: #dc3545; + padding-right: calc(1.5em + 0.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(0.375em + 0.1875rem); + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} + +.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + +.was-validated .form-control:invalid ~ .invalid-feedback, +.was-validated .form-control:invalid ~ .invalid-tooltip, .form-control.is-invalid ~ .invalid-feedback, +.form-control.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid { + padding-right: calc(1.5em + 0.75rem); + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); +} + +.was-validated .custom-select:invalid, .custom-select.is-invalid { + border-color: #dc3545; + padding-right: calc((1em + 0.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 0.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(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} + +.was-validated .custom-select:invalid:focus, .custom-select.is-invalid:focus { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + +.was-validated .custom-select:invalid ~ .invalid-feedback, +.was-validated .custom-select:invalid ~ .invalid-tooltip, .custom-select.is-invalid ~ .invalid-feedback, +.custom-select.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .form-control-file:invalid ~ .invalid-feedback, +.was-validated .form-control-file:invalid ~ .invalid-tooltip, .form-control-file.is-invalid ~ .invalid-feedback, +.form-control-file.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label { + color: #dc3545; +} + +.was-validated .form-check-input:invalid ~ .invalid-feedback, +.was-validated .form-check-input:invalid ~ .invalid-tooltip, .form-check-input.is-invalid ~ .invalid-feedback, +.form-check-input.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .custom-control-input:invalid ~ .custom-control-label, .custom-control-input.is-invalid ~ .custom-control-label { + color: #dc3545; +} + +.was-validated .custom-control-input:invalid ~ .custom-control-label::before, .custom-control-input.is-invalid ~ .custom-control-label::before { + border-color: #dc3545; +} + +.was-validated .custom-control-input:invalid ~ .invalid-feedback, +.was-validated .custom-control-input:invalid ~ .invalid-tooltip, .custom-control-input.is-invalid ~ .invalid-feedback, +.custom-control-input.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before, .custom-control-input.is-invalid:checked ~ .custom-control-label::before { + border-color: #e4606d; + background-color: #e4606d; +} + +.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before, .custom-control-input.is-invalid:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + +.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before { + border-color: #dc3545; +} + +.was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label { + border-color: #dc3545; +} + +.was-validated .custom-file-input:invalid ~ .invalid-feedback, +.was-validated .custom-file-input:invalid ~ .invalid-tooltip, .custom-file-input.is-invalid ~ .invalid-feedback, +.custom-file-input.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .custom-file-input:invalid:focus ~ .custom-file-label, .custom-file-input.is-invalid:focus ~ .custom-file-label { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.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 .input-group, + .form-inline .custom-select { + 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: 0.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: 0.375rem 0.75rem; + font-size: 1rem; + line-height: 1.5; + border-radius: 0.25rem; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.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 0.2rem rgba(0, 123, 255, 0.25); +} + +.btn.disabled, .btn:disabled { + opacity: 0.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 0.2rem rgba(38, 143, 255, 0.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 0.2rem rgba(38, 143, 255, 0.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 0.2rem rgba(130, 138, 145, 0.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 0.2rem rgba(130, 138, 145, 0.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 0.2rem rgba(72, 180, 97, 0.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 0.2rem rgba(72, 180, 97, 0.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 0.2rem rgba(58, 176, 195, 0.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 0.2rem rgba(58, 176, 195, 0.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 0.2rem rgba(222, 170, 12, 0.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 0.2rem rgba(222, 170, 12, 0.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 0.2rem rgba(225, 83, 97, 0.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 0.2rem rgba(225, 83, 97, 0.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 0.2rem rgba(216, 217, 219, 0.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 0.2rem rgba(216, 217, 219, 0.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 0.2rem rgba(82, 88, 93, 0.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 0.2rem rgba(82, 88, 93, 0.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 0.2rem rgba(0, 123, 255, 0.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 0.2rem rgba(0, 123, 255, 0.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 0.2rem rgba(108, 117, 125, 0.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 0.2rem rgba(108, 117, 125, 0.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 0.2rem rgba(40, 167, 69, 0.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 0.2rem rgba(40, 167, 69, 0.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 0.2rem rgba(23, 162, 184, 0.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 0.2rem rgba(23, 162, 184, 0.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 0.2rem rgba(255, 193, 7, 0.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 0.2rem rgba(255, 193, 7, 0.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 0.2rem rgba(220, 53, 69, 0.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 0.2rem rgba(220, 53, 69, 0.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 0.2rem rgba(248, 249, 250, 0.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 0.2rem rgba(248, 249, 250, 0.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 0.2rem rgba(52, 58, 64, 0.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 0.2rem rgba(52, 58, 64, 0.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-lg, .btn-group-lg > .btn { + padding: 0.5rem 1rem; + font-size: 1.25rem; + line-height: 1.5; + border-radius: 0.3rem; +} + +.btn-sm, .btn-group-sm > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; +} + +.btn-block { + display: block; + width: 100%; +} + +.btn-block + .btn-block { + margin-top: 0.5rem; +} + +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} + +.fade { + transition: opacity 0.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 0.35s ease; +} + +@media (prefers-reduced-motion: reduce) { + .collapsing { + transition: none; + } +} + +.dropup, +.dropright, +.dropdown, +.dropleft { + position: relative; +} + +.dropdown-toggle { + white-space: nowrap; +} + +.dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid; + border-right: 0.3em solid transparent; + border-bottom: 0; + border-left: 0.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: 0.5rem 0; + margin: 0.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, 0.15); + border-radius: 0.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: 0.125rem; +} + +.dropup .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0; + border-right: 0.3em solid transparent; + border-bottom: 0.3em solid; + border-left: 0.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: 0.125rem; +} + +.dropright .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0; + border-bottom: 0.3em solid transparent; + border-left: 0.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: 0.125rem; +} + +.dropleft .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; +} + +.dropleft .dropdown-toggle::after { + display: none; +} + +.dropleft .dropdown-toggle::before { + display: inline-block; + margin-right: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0.3em solid; + border-bottom: 0.3em solid transparent; +} + +.dropleft .dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropleft .dropdown-toggle::before { + vertical-align: 0; +} + +.dropdown-menu[x-placement^="top"], .dropdown-menu[x-placement^="right"], .dropdown-menu[x-placement^="bottom"], .dropdown-menu[x-placement^="left"] { + right: auto; + bottom: auto; +} + +.dropdown-divider { + height: 0; + margin: 0.5rem 0; + overflow: hidden; + border-top: 1px solid #e9ecef; +} + +.dropdown-item { + display: block; + width: 100%; + padding: 0.25rem 1.5rem; + clear: both; + font-weight: 400; + color: #212529; + text-align: inherit; + white-space: nowrap; + background-color: transparent; + border: 0; +} + +.dropdown-item:hover, .dropdown-item:focus { + 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: 0.5rem 1.5rem; + margin-bottom: 0; + font-size: 0.875rem; + color: #6c757d; + white-space: nowrap; +} + +.dropdown-item-text { + display: block; + padding: 0.25rem 1.5rem; + color: #212529; +} + +.btn-group, +.btn-group-vertical { + position: relative; + display: -ms-inline-flexbox; + display: inline-flex; + vertical-align: middle; +} + +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + -ms-flex: 1 1 auto; + flex: 1 1 auto; +} + +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover { + z-index: 1; +} + +.btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active, +.btn-group-vertical > .btn:focus, +.btn-group-vertical > .btn:active, +.btn-group-vertical > .btn.active { + 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:not(:first-child), +.btn-group > .btn-group:not(:first-child) { + margin-left: -1px; +} + +.btn-group > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group > .btn-group:not(:last-child) > .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.btn-group > .btn:not(:first-child), +.btn-group > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.dropdown-toggle-split { + padding-right: 0.5625rem; + padding-left: 0.5625rem; +} + +.dropdown-toggle-split::after, +.dropup .dropdown-toggle-split::after, +.dropright .dropdown-toggle-split::after { + margin-left: 0; +} + +.dropleft .dropdown-toggle-split::before { + margin-right: 0; +} + +.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split { + padding-right: 0.375rem; + padding-left: 0.375rem; +} + +.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split { + padding-right: 0.75rem; + padding-left: 0.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:not(:first-child), +.btn-group-vertical > .btn-group:not(:first-child) { + margin-top: -1px; +} + +.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group-vertical > .btn-group:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.btn-group-vertical > .btn:not(:first-child), +.btn-group-vertical > .btn-group:not(:first-child) > .btn { + 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="radio"], +.btn-group-toggle > .btn input[type="checkbox"], +.btn-group-toggle > .btn-group > .btn input[type="radio"], +.btn-group-toggle > .btn-group > .btn input[type="checkbox"] { + 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 > .form-control, +.input-group > .form-control-plaintext, +.input-group > .custom-select, +.input-group > .custom-file { + position: relative; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + width: 1%; + margin-bottom: 0; +} + +.input-group > .form-control + .form-control, +.input-group > .form-control + .custom-select, +.input-group > .form-control + .custom-file, +.input-group > .form-control-plaintext + .form-control, +.input-group > .form-control-plaintext + .custom-select, +.input-group > .form-control-plaintext + .custom-file, +.input-group > .custom-select + .form-control, +.input-group > .custom-select + .custom-select, +.input-group > .custom-select + .custom-file, +.input-group > .custom-file + .form-control, +.input-group > .custom-file + .custom-select, +.input-group > .custom-file + .custom-file { + margin-left: -1px; +} + +.input-group > .form-control:focus, +.input-group > .custom-select:focus, +.input-group > .custom-file .custom-file-input:focus ~ .custom-file-label { + z-index: 3; +} + +.input-group > .custom-file .custom-file-input:focus { + z-index: 4; +} + +.input-group > .form-control:not(:last-child), +.input-group > .custom-select:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group > .form-control:not(:first-child), +.input-group > .custom-select: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-prepend, +.input-group-append { + display: -ms-flexbox; + display: flex; +} + +.input-group-prepend .btn, +.input-group-append .btn { + position: relative; + z-index: 2; +} + +.input-group-prepend .btn:focus, +.input-group-append .btn:focus { + z-index: 3; +} + +.input-group-prepend .btn + .btn, +.input-group-prepend .btn + .input-group-text, +.input-group-prepend .input-group-text + .input-group-text, +.input-group-prepend .input-group-text + .btn, +.input-group-append .btn + .btn, +.input-group-append .btn + .input-group-text, +.input-group-append .input-group-text + .input-group-text, +.input-group-append .input-group-text + .btn { + 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: 0.375rem 0.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: 0.25rem; +} + +.input-group-text input[type="radio"], +.input-group-text input[type="checkbox"] { + margin-top: 0; +} + +.input-group-lg > .form-control:not(textarea), +.input-group-lg > .custom-select { + height: calc(1.5em + 1rem + 2px); +} + +.input-group-lg > .form-control, +.input-group-lg > .custom-select, +.input-group-lg > .input-group-prepend > .input-group-text, +.input-group-lg > .input-group-append > .input-group-text, +.input-group-lg > .input-group-prepend > .btn, +.input-group-lg > .input-group-append > .btn { + padding: 0.5rem 1rem; + font-size: 1.25rem; + line-height: 1.5; + border-radius: 0.3rem; +} + +.input-group-sm > .form-control:not(textarea), +.input-group-sm > .custom-select { + height: calc(1.5em + 0.5rem + 2px); +} + +.input-group-sm > .form-control, +.input-group-sm > .custom-select, +.input-group-sm > .input-group-prepend > .input-group-text, +.input-group-sm > .input-group-append > .input-group-text, +.input-group-sm > .input-group-prepend > .btn, +.input-group-sm > .input-group-append > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; +} + +.input-group-lg > .custom-select, +.input-group-sm > .custom-select { + padding-right: 1.75rem; +} + +.input-group > .input-group-prepend > .btn, +.input-group > .input-group-prepend > .input-group-text, +.input-group > .input-group-append:not(:last-child) > .btn, +.input-group > .input-group-append:not(:last-child) > .input-group-text, +.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) { + 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:not(:first-child) > .btn, +.input-group > .input-group-prepend:not(:first-child) > .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) { + 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 0.2rem rgba(0, 123, 255, 0.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: 0.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: 0.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: 0.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, 0.5); +} + +.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before { + background-color: rgba(0, 123, 255, 0.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, 0.5); +} + +.custom-switch { + padding-left: 2.25rem; +} + +.custom-switch .custom-control-label::before { + left: -2.25rem; + width: 1.75rem; + pointer-events: all; + border-radius: 0.5rem; +} + +.custom-switch .custom-control-label::after { + top: calc(0.25rem + 2px); + left: calc(-2.25rem + 2px); + width: calc(1rem - 4px); + height: calc(1rem - 4px); + background-color: #adb5bd; + border-radius: 0.5rem; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out; + transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.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(0.75rem); + transform: translateX(0.75rem); +} + +.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before { + background-color: rgba(0, 123, 255, 0.5); +} + +.custom-select { + display: inline-block; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + padding: 0.375rem 1.75rem 0.375rem 0.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 0.75rem center/8px 10px; + background-color: #fff; + border: 1px solid #ced4da; + border-radius: 0.25rem; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.custom-select:focus { + border-color: #80bdff; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-select:focus::-ms-value { + color: #495057; + background-color: #fff; +} + +.custom-select[multiple], .custom-select[size]:not([size="1"]) { + height: auto; + padding-right: 0.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 + 0.5rem + 2px); + padding-top: 0.25rem; + padding-bottom: 0.25rem; + padding-left: 0.5rem; + font-size: 0.875rem; +} + +.custom-select-lg { + height: calc(1.5em + 1rem + 2px); + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1rem; + font-size: 1.25rem; +} + +.custom-file { + position: relative; + display: inline-block; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + margin-bottom: 0; +} + +.custom-file-input { + position: relative; + z-index: 2; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + margin: 0; + opacity: 0; +} + +.custom-file-input:focus ~ .custom-file-label { + border-color: #80bdff; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.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 + 0.75rem + 2px); + padding: 0.375rem 0.75rem; + font-weight: 400; + line-height: 1.5; + color: #495057; + background-color: #fff; + border: 1px solid #ced4da; + border-radius: 0.25rem; +} + +.custom-file-label::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + z-index: 3; + display: block; + height: calc(1.5em + 0.75rem); + padding: 0.375rem 0.75rem; + line-height: 1.5; + color: #495057; + content: "Browse"; + background-color: #e9ecef; + border-left: inherit; + border-radius: 0 0.25rem 0.25rem 0; +} + +.custom-range { + width: 100%; + height: calc(1rem + 0.4rem); + padding: 0; + background-color: transparent; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.custom-range:focus { + outline: none; +} + +.custom-range:focus::-webkit-slider-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-range:focus::-moz-range-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-range:focus::-ms-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-range::-moz-focus-outer { + border: 0; +} + +.custom-range::-webkit-slider-thumb { + width: 1rem; + height: 1rem; + margin-top: -0.25rem; + background-color: #007bff; + border: 0; + border-radius: 1rem; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.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: 0.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 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.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: 0.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: 0.2rem; + margin-left: 0.2rem; + background-color: #007bff; + border: 0; + border-radius: 1rem; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.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: 0.5rem; + color: transparent; + cursor: pointer; + background-color: transparent; + border-color: transparent; + border-width: 0.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 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.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: 0.5rem 1rem; +} + +.nav-link:hover, .nav-link:focus { + 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: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus { + border-color: #e9ecef #e9ecef #dee2e6; +} + +.nav-tabs .nav-link.disabled { + color: #6c757d; + background-color: transparent; + border-color: transparent; +} + +.nav-tabs .nav-link.active, +.nav-tabs .nav-item.show .nav-link { + 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: 0.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: 0.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: 0.3125rem; + padding-bottom: 0.3125rem; + margin-right: 1rem; + font-size: 1.25rem; + line-height: inherit; + white-space: nowrap; +} + +.navbar-brand:hover, .navbar-brand:focus { + 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: 0.5rem; + padding-bottom: 0.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: 0.25rem 0.75rem; + font-size: 1.25rem; + line-height: 1; + background-color: transparent; + border: 1px solid transparent; + border-radius: 0.25rem; +} + +.navbar-toggler:hover, .navbar-toggler:focus { + 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: 0.5rem; + padding-left: 0.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: 0.5rem; + padding-left: 0.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: 0.5rem; + padding-left: 0.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: 0.5rem; + padding-left: 0.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: 0.5rem; + padding-left: 0.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, 0.9); +} + +.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-light .navbar-nav .nav-link { + color: rgba(0, 0, 0, 0.5); +} + +.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus { + color: rgba(0, 0, 0, 0.7); +} + +.navbar-light .navbar-nav .nav-link.disabled { + color: rgba(0, 0, 0, 0.3); +} + +.navbar-light .navbar-nav .show > .nav-link, +.navbar-light .navbar-nav .active > .nav-link, +.navbar-light .navbar-nav .nav-link.show, +.navbar-light .navbar-nav .nav-link.active { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-light .navbar-toggler { + color: rgba(0, 0, 0, 0.5); + border-color: rgba(0, 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, 0.5); +} + +.navbar-light .navbar-text a { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-light .navbar-text a:hover, .navbar-light .navbar-text a:focus { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-dark .navbar-brand { + color: #fff; +} + +.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus { + color: #fff; +} + +.navbar-dark .navbar-nav .nav-link { + color: rgba(255, 255, 255, 0.5); +} + +.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus { + color: rgba(255, 255, 255, 0.75); +} + +.navbar-dark .navbar-nav .nav-link.disabled { + color: rgba(255, 255, 255, 0.25); +} + +.navbar-dark .navbar-nav .show > .nav-link, +.navbar-dark .navbar-nav .active > .nav-link, +.navbar-dark .navbar-nav .nav-link.show, +.navbar-dark .navbar-nav .nav-link.active { + color: #fff; +} + +.navbar-dark .navbar-toggler { + color: rgba(255, 255, 255, 0.5); + border-color: rgba(255, 255, 255, 0.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, 0.5); +} + +.navbar-dark .navbar-text a { + color: #fff; +} + +.navbar-dark .navbar-text a:hover, .navbar-dark .navbar-text a:focus { + 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, 0.125); + border-radius: 0.25rem; +} + +.card > hr { + margin-right: 0; + margin-left: 0; +} + +.card > .list-group:first-child .list-group-item:first-child { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.card > .list-group:last-child .list-group-item:last-child { + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.card-body { + -ms-flex: 1 1 auto; + flex: 1 1 auto; + padding: 1.25rem; +} + +.card-title { + margin-bottom: 0.75rem; +} + +.card-subtitle { + margin-top: -0.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: 0.75rem 1.25rem; + margin-bottom: 0; + background-color: rgba(0, 0, 0, 0.03); + border-bottom: 1px solid rgba(0, 0, 0, 0.125); +} + +.card-header:first-child { + border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0; +} + +.card-header + .list-group .list-group-item:first-child { + border-top: 0; +} + +.card-footer { + padding: 0.75rem 1.25rem; + background-color: rgba(0, 0, 0, 0.03); + border-top: 1px solid rgba(0, 0, 0, 0.125); +} + +.card-footer:last-child { + border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px); +} + +.card-header-tabs { + margin-right: -0.625rem; + margin-bottom: -0.75rem; + margin-left: -0.625rem; + border-bottom: 0; +} + +.card-header-pills { + margin-right: -0.625rem; + margin-left: -0.625rem; +} + +.card-img-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: 1.25rem; +} + +.card-img { + width: 100%; + border-radius: calc(0.25rem - 1px); +} + +.card-img-top { + width: 100%; + border-top-left-radius: calc(0.25rem - 1px); + border-top-right-radius: calc(0.25rem - 1px); +} + +.card-img-bottom { + width: 100%; + border-bottom-right-radius: calc(0.25rem - 1px); + border-bottom-left-radius: calc(0.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-img-top, + .card-group > .card:not(:last-child) .card-header { + border-top-right-radius: 0; + } + .card-group > .card:not(:last-child) .card-img-bottom, + .card-group > .card:not(:last-child) .card-footer { + 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-img-top, + .card-group > .card:not(:first-child) .card-header { + border-top-left-radius: 0; + } + .card-group > .card:not(:first-child) .card-img-bottom, + .card-group > .card:not(:first-child) .card-footer { + border-bottom-left-radius: 0; + } +} + +.card-columns .card { + margin-bottom: 0.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: 0.75rem 1rem; + margin-bottom: 1rem; + list-style: none; + background-color: #e9ecef; + border-radius: 0.25rem; +} + +.breadcrumb-item + .breadcrumb-item { + padding-left: 0.5rem; +} + +.breadcrumb-item + .breadcrumb-item::before { + display: inline-block; + padding-right: 0.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: 0.25rem; +} + +.page-link { + position: relative; + display: block; + padding: 0.5rem 0.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 0.2rem rgba(0, 123, 255, 0.25); +} + +.page-item:first-child .page-link { + margin-left: 0; + border-top-left-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.page-item:last-child .page-link { + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.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: 0.75rem 1.5rem; + font-size: 1.25rem; + line-height: 1.5; +} + +.pagination-lg .page-item:first-child .page-link { + border-top-left-radius: 0.3rem; + border-bottom-left-radius: 0.3rem; +} + +.pagination-lg .page-item:last-child .page-link { + border-top-right-radius: 0.3rem; + border-bottom-right-radius: 0.3rem; +} + +.pagination-sm .page-link { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; +} + +.pagination-sm .page-item:first-child .page-link { + border-top-left-radius: 0.2rem; + border-bottom-left-radius: 0.2rem; +} + +.pagination-sm .page-item:last-child .page-link { + border-top-right-radius: 0.2rem; + border-bottom-right-radius: 0.2rem; +} + +.badge { + display: inline-block; + padding: 0.25em 0.4em; + font-size: 75%; + font-weight: 700; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25rem; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +@media (prefers-reduced-motion: reduce) { + .badge { + transition: none; + } +} + +a.badge:hover, a.badge:focus { + text-decoration: none; +} + +.badge:empty { + display: none; +} + +.btn .badge { + position: relative; + top: -1px; +} + +.badge-pill { + padding-right: 0.6em; + padding-left: 0.6em; + border-radius: 10rem; +} + +.badge-primary { + color: #fff; + background-color: #007bff; +} + +a.badge-primary:hover, a.badge-primary:focus { + color: #fff; + background-color: #0062cc; +} + +a.badge-primary:focus, a.badge-primary.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); +} + +.badge-secondary { + color: #fff; + background-color: #6c757d; +} + +a.badge-secondary:hover, a.badge-secondary:focus { + color: #fff; + background-color: #545b62; +} + +a.badge-secondary:focus, a.badge-secondary.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); +} + +.badge-success { + color: #fff; + background-color: #28a745; +} + +a.badge-success:hover, a.badge-success:focus { + color: #fff; + background-color: #1e7e34; +} + +a.badge-success:focus, a.badge-success.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); +} + +.badge-info { + color: #fff; + background-color: #17a2b8; +} + +a.badge-info:hover, a.badge-info:focus { + color: #fff; + background-color: #117a8b; +} + +a.badge-info:focus, a.badge-info.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); +} + +.badge-warning { + color: #212529; + background-color: #ffc107; +} + +a.badge-warning:hover, a.badge-warning:focus { + color: #212529; + background-color: #d39e00; +} + +a.badge-warning:focus, a.badge-warning.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); +} + +.badge-danger { + color: #fff; + background-color: #dc3545; +} + +a.badge-danger:hover, a.badge-danger:focus { + color: #fff; + background-color: #bd2130; +} + +a.badge-danger:focus, a.badge-danger.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); +} + +.badge-light { + color: #212529; + background-color: #f8f9fa; +} + +a.badge-light:hover, a.badge-light:focus { + color: #212529; + background-color: #dae0e5; +} + +a.badge-light:focus, a.badge-light.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); +} + +.badge-dark { + color: #fff; + background-color: #343a40; +} + +a.badge-dark:hover, a.badge-dark:focus { + color: #fff; + background-color: #1d2124; +} + +a.badge-dark:focus, a.badge-dark.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); +} + +.jumbotron { + padding: 2rem 1rem; + margin-bottom: 2rem; + background-color: #e9ecef; + border-radius: 0.3rem; +} + +@media (min-width: 576px) { + .jumbotron { + padding: 4rem 2rem; + } +} + +.jumbotron-fluid { + padding-right: 0; + padding-left: 0; + border-radius: 0; +} + +.alert { + position: relative; + padding: 0.75rem 1.25rem; + margin-bottom: 1rem; + border: 1px solid transparent; + border-radius: 0.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: 0.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: 0.75rem; + background-color: #e9ecef; + border-radius: 0.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 0.6s ease; +} + +@media (prefers-reduced-motion: reduce) { + .progress-bar { + transition: none; + } +} + +.progress-bar-striped { + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.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:hover, .list-group-item-action:focus { + 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: 0.75rem 1.25rem; + margin-bottom: -1px; + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.125); +} + +.list-group-item:first-child { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.list-group-item:last-child { + margin-bottom: 0; + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.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: 0.25rem; + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; +} + +.list-group-horizontal .list-group-item:last-child { + margin-right: 0; + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.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: 0.25rem; + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-sm .list-group-item:last-child { + margin-right: 0; + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.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: 0.25rem; + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-md .list-group-item:last-child { + margin-right: 0; + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.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: 0.25rem; + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-lg .list-group-item:last-child { + margin-right: 0; + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.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: 0.25rem; + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-xl .list-group-item:last-child { + margin-right: 0; + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.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:hover, .list-group-item-primary.list-group-item-action:focus { + 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:hover, .list-group-item-secondary.list-group-item-action:focus { + 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:hover, .list-group-item-success.list-group-item-action:focus { + 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:hover, .list-group-item-info.list-group-item-action:focus { + 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:hover, .list-group-item-warning.list-group-item-action:focus { + 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:hover, .list-group-item-danger.list-group-item-action:focus { + 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:hover, .list-group-item-light.list-group-item-action:focus { + 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:hover, .list-group-item-dark.list-group-item-action:focus { + 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):hover, .close:not(:disabled):not(.disabled):focus { + 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: 0.875rem; + background-color: rgba(255, 255, 255, 0.85); + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.1); + box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + opacity: 0; + border-radius: 0.25rem; +} + +.toast:not(:last-child) { + margin-bottom: 0.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: 0.25rem 0.75rem; + color: #6c757d; + background-color: rgba(255, 255, 255, 0.85); + background-clip: padding-box; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +.toast-body { + padding: 0.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: 0.5rem; + pointer-events: none; +} + +.modal.fade .modal-dialog { + transition: -webkit-transform 0.3s ease-out; + transition: transform 0.3s ease-out; + transition: transform 0.3s ease-out, -webkit-transform 0.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-header, +.modal-dialog-scrollable .modal-footer { + -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, 0.2); + border-radius: 0.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: 0.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: 0.3rem; + border-top-right-radius: 0.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: 0.3rem; + border-bottom-left-radius: 0.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: 0.875rem; + word-wrap: break-word; + opacity: 0; +} + +.tooltip.show { + opacity: 0.9; +} + +.tooltip .arrow { + position: absolute; + display: block; + width: 0.8rem; + height: 0.4rem; +} + +.tooltip .arrow::before { + position: absolute; + content: ""; + border-color: transparent; + border-style: solid; +} + +.bs-tooltip-top, .bs-tooltip-auto[x-placement^="top"] { + padding: 0.4rem 0; +} + +.bs-tooltip-top .arrow, .bs-tooltip-auto[x-placement^="top"] .arrow { + bottom: 0; +} + +.bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^="top"] .arrow::before { + top: 0; + border-width: 0.4rem 0.4rem 0; + border-top-color: #000; +} + +.bs-tooltip-right, .bs-tooltip-auto[x-placement^="right"] { + padding: 0 0.4rem; +} + +.bs-tooltip-right .arrow, .bs-tooltip-auto[x-placement^="right"] .arrow { + left: 0; + width: 0.4rem; + height: 0.8rem; +} + +.bs-tooltip-right .arrow::before, .bs-tooltip-auto[x-placement^="right"] .arrow::before { + right: 0; + border-width: 0.4rem 0.4rem 0.4rem 0; + border-right-color: #000; +} + +.bs-tooltip-bottom, .bs-tooltip-auto[x-placement^="bottom"] { + padding: 0.4rem 0; +} + +.bs-tooltip-bottom .arrow, .bs-tooltip-auto[x-placement^="bottom"] .arrow { + top: 0; +} + +.bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^="bottom"] .arrow::before { + bottom: 0; + border-width: 0 0.4rem 0.4rem; + border-bottom-color: #000; +} + +.bs-tooltip-left, .bs-tooltip-auto[x-placement^="left"] { + padding: 0 0.4rem; +} + +.bs-tooltip-left .arrow, .bs-tooltip-auto[x-placement^="left"] .arrow { + right: 0; + width: 0.4rem; + height: 0.8rem; +} + +.bs-tooltip-left .arrow::before, .bs-tooltip-auto[x-placement^="left"] .arrow::before { + left: 0; + border-width: 0.4rem 0 0.4rem 0.4rem; + border-left-color: #000; +} + +.tooltip-inner { + max-width: 200px; + padding: 0.25rem 0.5rem; + color: #fff; + text-align: center; + background-color: #000; + border-radius: 0.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: 0.875rem; + word-wrap: break-word; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0.3rem; +} + +.popover .arrow { + position: absolute; + display: block; + width: 1rem; + height: 0.5rem; + margin: 0 0.3rem; +} + +.popover .arrow::before, .popover .arrow::after { + position: absolute; + display: block; + content: ""; + border-color: transparent; + border-style: solid; +} + +.bs-popover-top, .bs-popover-auto[x-placement^="top"] { + margin-bottom: 0.5rem; +} + +.bs-popover-top > .arrow, .bs-popover-auto[x-placement^="top"] > .arrow { + bottom: calc((0.5rem + 1px) * -1); +} + +.bs-popover-top > .arrow::before, .bs-popover-auto[x-placement^="top"] > .arrow::before { + bottom: 0; + border-width: 0.5rem 0.5rem 0; + border-top-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-top > .arrow::after, .bs-popover-auto[x-placement^="top"] > .arrow::after { + bottom: 1px; + border-width: 0.5rem 0.5rem 0; + border-top-color: #fff; +} + +.bs-popover-right, .bs-popover-auto[x-placement^="right"] { + margin-left: 0.5rem; +} + +.bs-popover-right > .arrow, .bs-popover-auto[x-placement^="right"] > .arrow { + left: calc((0.5rem + 1px) * -1); + width: 0.5rem; + height: 1rem; + margin: 0.3rem 0; +} + +.bs-popover-right > .arrow::before, .bs-popover-auto[x-placement^="right"] > .arrow::before { + left: 0; + border-width: 0.5rem 0.5rem 0.5rem 0; + border-right-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-right > .arrow::after, .bs-popover-auto[x-placement^="right"] > .arrow::after { + left: 1px; + border-width: 0.5rem 0.5rem 0.5rem 0; + border-right-color: #fff; +} + +.bs-popover-bottom, .bs-popover-auto[x-placement^="bottom"] { + margin-top: 0.5rem; +} + +.bs-popover-bottom > .arrow, .bs-popover-auto[x-placement^="bottom"] > .arrow { + top: calc((0.5rem + 1px) * -1); +} + +.bs-popover-bottom > .arrow::before, .bs-popover-auto[x-placement^="bottom"] > .arrow::before { + top: 0; + border-width: 0 0.5rem 0.5rem 0.5rem; + border-bottom-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-bottom > .arrow::after, .bs-popover-auto[x-placement^="bottom"] > .arrow::after { + top: 1px; + border-width: 0 0.5rem 0.5rem 0.5rem; + border-bottom-color: #fff; +} + +.bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^="bottom"] .popover-header::before { + position: absolute; + top: 0; + left: 50%; + display: block; + width: 1rem; + margin-left: -0.5rem; + content: ""; + border-bottom: 1px solid #f7f7f7; +} + +.bs-popover-left, .bs-popover-auto[x-placement^="left"] { + margin-right: 0.5rem; +} + +.bs-popover-left > .arrow, .bs-popover-auto[x-placement^="left"] > .arrow { + right: calc((0.5rem + 1px) * -1); + width: 0.5rem; + height: 1rem; + margin: 0.3rem 0; +} + +.bs-popover-left > .arrow::before, .bs-popover-auto[x-placement^="left"] > .arrow::before { + right: 0; + border-width: 0.5rem 0 0.5rem 0.5rem; + border-left-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-left > .arrow::after, .bs-popover-auto[x-placement^="left"] > .arrow::after { + right: 1px; + border-width: 0.5rem 0 0.5rem 0.5rem; + border-left-color: #fff; +} + +.popover-header { + padding: 0.5rem 0.75rem; + margin-bottom: 0; + font-size: 1rem; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-top-left-radius: calc(0.3rem - 1px); + border-top-right-radius: calc(0.3rem - 1px); +} + +.popover-header:empty { + display: none; +} + +.popover-body { + padding: 0.5rem 0.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 0.6s ease-in-out; + transition: transform 0.6s ease-in-out; + transition: transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out; +} + +@media (prefers-reduced-motion: reduce) { + .carousel-item { + transition: none; + } +} + +.carousel-item.active, +.carousel-item-next, +.carousel-item-prev { + display: block; +} + +.carousel-item-next:not(.carousel-item-left), +.active.carousel-item-right { + -webkit-transform: translateX(100%); + transform: translateX(100%); +} + +.carousel-item-prev:not(.carousel-item-right), +.active.carousel-item-left { + -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.active, +.carousel-fade .carousel-item-next.carousel-item-left, +.carousel-fade .carousel-item-prev.carousel-item-right { + z-index: 1; + opacity: 1; +} + +.carousel-fade .active.carousel-item-left, +.carousel-fade .active.carousel-item-right { + z-index: 0; + opacity: 0; + transition: 0s 0.6s opacity; +} + +@media (prefers-reduced-motion: reduce) { + .carousel-fade .active.carousel-item-left, + .carousel-fade .active.carousel-item-right { + transition: none; + } +} + +.carousel-control-prev, +.carousel-control-next { + 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: 0.5; + transition: opacity 0.15s ease; +} + +@media (prefers-reduced-motion: reduce) { + .carousel-control-prev, + .carousel-control-next { + transition: none; + } +} + +.carousel-control-prev:hover, .carousel-control-prev:focus, +.carousel-control-next:hover, +.carousel-control-next:focus { + color: #fff; + text-decoration: none; + outline: 0; + opacity: 0.9; +} + +.carousel-control-prev { + left: 0; +} + +.carousel-control-next { + right: 0; +} + +.carousel-control-prev-icon, +.carousel-control-next-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 0.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: 0.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: 0.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:hover, a.bg-primary:focus, +button.bg-primary:hover, +button.bg-primary:focus { + background-color: #0062cc !important; +} + +.bg-secondary { + background-color: #6c757d !important; +} + +a.bg-secondary:hover, a.bg-secondary:focus, +button.bg-secondary:hover, +button.bg-secondary:focus { + background-color: #545b62 !important; +} + +.bg-success { + background-color: #28a745 !important; +} + +a.bg-success:hover, a.bg-success:focus, +button.bg-success:hover, +button.bg-success:focus { + background-color: #1e7e34 !important; +} + +.bg-info { + background-color: #17a2b8 !important; +} + +a.bg-info:hover, a.bg-info:focus, +button.bg-info:hover, +button.bg-info:focus { + background-color: #117a8b !important; +} + +.bg-warning { + background-color: #ffc107 !important; +} + +a.bg-warning:hover, a.bg-warning:focus, +button.bg-warning:hover, +button.bg-warning:focus { + background-color: #d39e00 !important; +} + +.bg-danger { + background-color: #dc3545 !important; +} + +a.bg-danger:hover, a.bg-danger:focus, +button.bg-danger:hover, +button.bg-danger:focus { + background-color: #bd2130 !important; +} + +.bg-light { + background-color: #f8f9fa !important; +} + +a.bg-light:hover, a.bg-light:focus, +button.bg-light:hover, +button.bg-light:focus { + background-color: #dae0e5 !important; +} + +.bg-dark { + background-color: #343a40 !important; +} + +a.bg-dark:hover, a.bg-dark:focus, +button.bg-dark:hover, +button.bg-dark:focus { + 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: 0.2rem !important; +} + +.rounded { + border-radius: 0.25rem !important; +} + +.rounded-top { + border-top-left-radius: 0.25rem !important; + border-top-right-radius: 0.25rem !important; +} + +.rounded-right { + border-top-right-radius: 0.25rem !important; + border-bottom-right-radius: 0.25rem !important; +} + +.rounded-bottom { + border-bottom-right-radius: 0.25rem !important; + border-bottom-left-radius: 0.25rem !important; +} + +.rounded-left { + border-top-left-radius: 0.25rem !important; + border-bottom-left-radius: 0.25rem !important; +} + +.rounded-lg { + border-radius: 0.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 iframe, +.embed-responsive embed, +.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 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; +} + +.shadow { + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; +} + +.shadow-lg { + box-shadow: 0 1rem 3rem rgba(0, 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: 0.25rem !important; +} + +.mt-1, +.my-1 { + margin-top: 0.25rem !important; +} + +.mr-1, +.mx-1 { + margin-right: 0.25rem !important; +} + +.mb-1, +.my-1 { + margin-bottom: 0.25rem !important; +} + +.ml-1, +.mx-1 { + margin-left: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.mt-2, +.my-2 { + margin-top: 0.5rem !important; +} + +.mr-2, +.mx-2 { + margin-right: 0.5rem !important; +} + +.mb-2, +.my-2 { + margin-bottom: 0.5rem !important; +} + +.ml-2, +.mx-2 { + margin-left: 0.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: 0.25rem !important; +} + +.pt-1, +.py-1 { + padding-top: 0.25rem !important; +} + +.pr-1, +.px-1 { + padding-right: 0.25rem !important; +} + +.pb-1, +.py-1 { + padding-bottom: 0.25rem !important; +} + +.pl-1, +.px-1 { + padding-left: 0.25rem !important; +} + +.p-2 { + padding: 0.5rem !important; +} + +.pt-2, +.py-2 { + padding-top: 0.5rem !important; +} + +.pr-2, +.px-2 { + padding-right: 0.5rem !important; +} + +.pb-2, +.py-2 { + padding-bottom: 0.5rem !important; +} + +.pl-2, +.px-2 { + padding-left: 0.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: -0.25rem !important; +} + +.mt-n1, +.my-n1 { + margin-top: -0.25rem !important; +} + +.mr-n1, +.mx-n1 { + margin-right: -0.25rem !important; +} + +.mb-n1, +.my-n1 { + margin-bottom: -0.25rem !important; +} + +.ml-n1, +.mx-n1 { + margin-left: -0.25rem !important; +} + +.m-n2 { + margin: -0.5rem !important; +} + +.mt-n2, +.my-n2 { + margin-top: -0.5rem !important; +} + +.mr-n2, +.mx-n2 { + margin-right: -0.5rem !important; +} + +.mb-n2, +.my-n2 { + margin-bottom: -0.5rem !important; +} + +.ml-n2, +.mx-n2 { + margin-left: -0.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: 0.25rem !important; + } + .mt-sm-1, + .my-sm-1 { + margin-top: 0.25rem !important; + } + .mr-sm-1, + .mx-sm-1 { + margin-right: 0.25rem !important; + } + .mb-sm-1, + .my-sm-1 { + margin-bottom: 0.25rem !important; + } + .ml-sm-1, + .mx-sm-1 { + margin-left: 0.25rem !important; + } + .m-sm-2 { + margin: 0.5rem !important; + } + .mt-sm-2, + .my-sm-2 { + margin-top: 0.5rem !important; + } + .mr-sm-2, + .mx-sm-2 { + margin-right: 0.5rem !important; + } + .mb-sm-2, + .my-sm-2 { + margin-bottom: 0.5rem !important; + } + .ml-sm-2, + .mx-sm-2 { + margin-left: 0.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: 0.25rem !important; + } + .pt-sm-1, + .py-sm-1 { + padding-top: 0.25rem !important; + } + .pr-sm-1, + .px-sm-1 { + padding-right: 0.25rem !important; + } + .pb-sm-1, + .py-sm-1 { + padding-bottom: 0.25rem !important; + } + .pl-sm-1, + .px-sm-1 { + padding-left: 0.25rem !important; + } + .p-sm-2 { + padding: 0.5rem !important; + } + .pt-sm-2, + .py-sm-2 { + padding-top: 0.5rem !important; + } + .pr-sm-2, + .px-sm-2 { + padding-right: 0.5rem !important; + } + .pb-sm-2, + .py-sm-2 { + padding-bottom: 0.5rem !important; + } + .pl-sm-2, + .px-sm-2 { + padding-left: 0.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: -0.25rem !important; + } + .mt-sm-n1, + .my-sm-n1 { + margin-top: -0.25rem !important; + } + .mr-sm-n1, + .mx-sm-n1 { + margin-right: -0.25rem !important; + } + .mb-sm-n1, + .my-sm-n1 { + margin-bottom: -0.25rem !important; + } + .ml-sm-n1, + .mx-sm-n1 { + margin-left: -0.25rem !important; + } + .m-sm-n2 { + margin: -0.5rem !important; + } + .mt-sm-n2, + .my-sm-n2 { + margin-top: -0.5rem !important; + } + .mr-sm-n2, + .mx-sm-n2 { + margin-right: -0.5rem !important; + } + .mb-sm-n2, + .my-sm-n2 { + margin-bottom: -0.5rem !important; + } + .ml-sm-n2, + .mx-sm-n2 { + margin-left: -0.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: 0.25rem !important; + } + .mt-md-1, + .my-md-1 { + margin-top: 0.25rem !important; + } + .mr-md-1, + .mx-md-1 { + margin-right: 0.25rem !important; + } + .mb-md-1, + .my-md-1 { + margin-bottom: 0.25rem !important; + } + .ml-md-1, + .mx-md-1 { + margin-left: 0.25rem !important; + } + .m-md-2 { + margin: 0.5rem !important; + } + .mt-md-2, + .my-md-2 { + margin-top: 0.5rem !important; + } + .mr-md-2, + .mx-md-2 { + margin-right: 0.5rem !important; + } + .mb-md-2, + .my-md-2 { + margin-bottom: 0.5rem !important; + } + .ml-md-2, + .mx-md-2 { + margin-left: 0.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: 0.25rem !important; + } + .pt-md-1, + .py-md-1 { + padding-top: 0.25rem !important; + } + .pr-md-1, + .px-md-1 { + padding-right: 0.25rem !important; + } + .pb-md-1, + .py-md-1 { + padding-bottom: 0.25rem !important; + } + .pl-md-1, + .px-md-1 { + padding-left: 0.25rem !important; + } + .p-md-2 { + padding: 0.5rem !important; + } + .pt-md-2, + .py-md-2 { + padding-top: 0.5rem !important; + } + .pr-md-2, + .px-md-2 { + padding-right: 0.5rem !important; + } + .pb-md-2, + .py-md-2 { + padding-bottom: 0.5rem !important; + } + .pl-md-2, + .px-md-2 { + padding-left: 0.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: -0.25rem !important; + } + .mt-md-n1, + .my-md-n1 { + margin-top: -0.25rem !important; + } + .mr-md-n1, + .mx-md-n1 { + margin-right: -0.25rem !important; + } + .mb-md-n1, + .my-md-n1 { + margin-bottom: -0.25rem !important; + } + .ml-md-n1, + .mx-md-n1 { + margin-left: -0.25rem !important; + } + .m-md-n2 { + margin: -0.5rem !important; + } + .mt-md-n2, + .my-md-n2 { + margin-top: -0.5rem !important; + } + .mr-md-n2, + .mx-md-n2 { + margin-right: -0.5rem !important; + } + .mb-md-n2, + .my-md-n2 { + margin-bottom: -0.5rem !important; + } + .ml-md-n2, + .mx-md-n2 { + margin-left: -0.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: 0.25rem !important; + } + .mt-lg-1, + .my-lg-1 { + margin-top: 0.25rem !important; + } + .mr-lg-1, + .mx-lg-1 { + margin-right: 0.25rem !important; + } + .mb-lg-1, + .my-lg-1 { + margin-bottom: 0.25rem !important; + } + .ml-lg-1, + .mx-lg-1 { + margin-left: 0.25rem !important; + } + .m-lg-2 { + margin: 0.5rem !important; + } + .mt-lg-2, + .my-lg-2 { + margin-top: 0.5rem !important; + } + .mr-lg-2, + .mx-lg-2 { + margin-right: 0.5rem !important; + } + .mb-lg-2, + .my-lg-2 { + margin-bottom: 0.5rem !important; + } + .ml-lg-2, + .mx-lg-2 { + margin-left: 0.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: 0.25rem !important; + } + .pt-lg-1, + .py-lg-1 { + padding-top: 0.25rem !important; + } + .pr-lg-1, + .px-lg-1 { + padding-right: 0.25rem !important; + } + .pb-lg-1, + .py-lg-1 { + padding-bottom: 0.25rem !important; + } + .pl-lg-1, + .px-lg-1 { + padding-left: 0.25rem !important; + } + .p-lg-2 { + padding: 0.5rem !important; + } + .pt-lg-2, + .py-lg-2 { + padding-top: 0.5rem !important; + } + .pr-lg-2, + .px-lg-2 { + padding-right: 0.5rem !important; + } + .pb-lg-2, + .py-lg-2 { + padding-bottom: 0.5rem !important; + } + .pl-lg-2, + .px-lg-2 { + padding-left: 0.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: -0.25rem !important; + } + .mt-lg-n1, + .my-lg-n1 { + margin-top: -0.25rem !important; + } + .mr-lg-n1, + .mx-lg-n1 { + margin-right: -0.25rem !important; + } + .mb-lg-n1, + .my-lg-n1 { + margin-bottom: -0.25rem !important; + } + .ml-lg-n1, + .mx-lg-n1 { + margin-left: -0.25rem !important; + } + .m-lg-n2 { + margin: -0.5rem !important; + } + .mt-lg-n2, + .my-lg-n2 { + margin-top: -0.5rem !important; + } + .mr-lg-n2, + .mx-lg-n2 { + margin-right: -0.5rem !important; + } + .mb-lg-n2, + .my-lg-n2 { + margin-bottom: -0.5rem !important; + } + .ml-lg-n2, + .mx-lg-n2 { + margin-left: -0.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: 0.25rem !important; + } + .mt-xl-1, + .my-xl-1 { + margin-top: 0.25rem !important; + } + .mr-xl-1, + .mx-xl-1 { + margin-right: 0.25rem !important; + } + .mb-xl-1, + .my-xl-1 { + margin-bottom: 0.25rem !important; + } + .ml-xl-1, + .mx-xl-1 { + margin-left: 0.25rem !important; + } + .m-xl-2 { + margin: 0.5rem !important; + } + .mt-xl-2, + .my-xl-2 { + margin-top: 0.5rem !important; + } + .mr-xl-2, + .mx-xl-2 { + margin-right: 0.5rem !important; + } + .mb-xl-2, + .my-xl-2 { + margin-bottom: 0.5rem !important; + } + .ml-xl-2, + .mx-xl-2 { + margin-left: 0.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: 0.25rem !important; + } + .pt-xl-1, + .py-xl-1 { + padding-top: 0.25rem !important; + } + .pr-xl-1, + .px-xl-1 { + padding-right: 0.25rem !important; + } + .pb-xl-1, + .py-xl-1 { + padding-bottom: 0.25rem !important; + } + .pl-xl-1, + .px-xl-1 { + padding-left: 0.25rem !important; + } + .p-xl-2 { + padding: 0.5rem !important; + } + .pt-xl-2, + .py-xl-2 { + padding-top: 0.5rem !important; + } + .pr-xl-2, + .px-xl-2 { + padding-right: 0.5rem !important; + } + .pb-xl-2, + .py-xl-2 { + padding-bottom: 0.5rem !important; + } + .pl-xl-2, + .px-xl-2 { + padding-left: 0.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: -0.25rem !important; + } + .mt-xl-n1, + .my-xl-n1 { + margin-top: -0.25rem !important; + } + .mr-xl-n1, + .mx-xl-n1 { + margin-right: -0.25rem !important; + } + .mb-xl-n1, + .my-xl-n1 { + margin-bottom: -0.25rem !important; + } + .ml-xl-n1, + .mx-xl-n1 { + margin-left: -0.25rem !important; + } + .m-xl-n2 { + margin: -0.5rem !important; + } + .mt-xl-n2, + .my-xl-n2 { + margin-top: -0.5rem !important; + } + .mr-xl-n2, + .mx-xl-n2 { + margin-right: -0.5rem !important; + } + .mb-xl-n2, + .my-xl-n2 { + margin-bottom: -0.5rem !important; + } + .ml-xl-n2, + .mx-xl-n2 { + margin-left: -0.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:hover, a.text-primary:focus { + color: #0056b3 !important; +} + +.text-secondary { + color: #6c757d !important; +} + +a.text-secondary:hover, a.text-secondary:focus { + color: #494f54 !important; +} + +.text-success { + color: #28a745 !important; +} + +a.text-success:hover, a.text-success:focus { + color: #19692c !important; +} + +.text-info { + color: #17a2b8 !important; +} + +a.text-info:hover, a.text-info:focus { + color: #0f6674 !important; +} + +.text-warning { + color: #ffc107 !important; +} + +a.text-warning:hover, a.text-warning:focus { + color: #ba8b00 !important; +} + +.text-danger { + color: #dc3545 !important; +} + +a.text-danger:hover, a.text-danger:focus { + color: #a71d2a !important; +} + +.text-light { + color: #f8f9fa !important; +} + +a.text-light:hover, a.text-light:focus { + color: #cbd3da !important; +} + +.text-dark { + color: #343a40 !important; +} + +a.text-dark:hover, a.text-dark:focus { + color: #121416 !important; +} + +.text-body { + color: #212529 !important; +} + +.text-muted { + color: #6c757d !important; +} + +.text-black-50 { + color: rgba(0, 0, 0, 0.5) !important; +} + +.text-white-50 { + color: rgba(255, 255, 255, 0.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 { + *, + *::before, + *::after { + 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; + } + pre, + blockquote { + border: 1px solid #adb5bd; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + p, + h2, + h3 { + 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 th, + .table-bordered td { + border: 1px solid #dee2e6 !important; + } + .table-dark { + color: inherit; + } + .table-dark th, + .table-dark td, + .table-dark thead th, + .table-dark tbody + tbody { + border-color: #dee2e6; + } + .table .thead-dark th { + color: inherit; + border-color: #dee2e6; + } +} +/*# sourceMappingURL=bootstrap.css.map */ \ No newline at end of file diff --git a/BAP.MQTTnet.Test/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css b/BAP.MQTTnet.Test/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css new file mode 100644 index 0000000..92e3fe8 --- /dev/null +++ b/BAP.MQTTnet.Test/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css @@ -0,0 +1,7 @@ +/*! + * 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/BAP.MQTTnet.Test/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt b/BAP.MQTTnet.Test/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt new file mode 100644 index 0000000..0bdc196 --- /dev/null +++ b/BAP.MQTTnet.Test/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt @@ -0,0 +1,12 @@ +Copyright (c) .NET Foundation. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +these files except in compliance with the License. You may obtain a copy of the +License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. diff --git a/BAP.MQTTnet.Test/wwwroot/lib/jquery-validation/LICENSE.md b/BAP.MQTTnet.Test/wwwroot/lib/jquery-validation/LICENSE.md new file mode 100644 index 0000000..dc377cc --- /dev/null +++ b/BAP.MQTTnet.Test/wwwroot/lib/jquery-validation/LICENSE.md @@ -0,0 +1,22 @@ +The MIT License (MIT) +===================== + +Copyright Jörn Zaefferer + +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. diff --git a/BAP.MQTTnet.Test/wwwroot/lib/jquery/LICENSE.txt b/BAP.MQTTnet.Test/wwwroot/lib/jquery/LICENSE.txt new file mode 100644 index 0000000..e4e5e00 --- /dev/null +++ b/BAP.MQTTnet.Test/wwwroot/lib/jquery/LICENSE.txt @@ -0,0 +1,36 @@ +Copyright JS Foundation and other contributors, https://js.foundation/ + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/jquery/jquery + +The following license applies to all parts of this software except as +documented below: + +==== + +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. + +==== + +All files located in the node_modules and external directories are +externally maintained libraries used by this software which have their +own licenses; we recommend you read them, as their terms may differ from +the terms above. diff --git a/BPA.AspNetCore.Test/BPA.AspNetCore.Test.csproj b/BPA.AspNetCore.Test/BPA.AspNetCore.Test.csproj new file mode 100644 index 0000000..db21383 --- /dev/null +++ b/BPA.AspNetCore.Test/BPA.AspNetCore.Test.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.1 + + + + + + + diff --git a/BPA.AspNetCore.Test/Client/Client_Connection_Samples.cs b/BPA.AspNetCore.Test/Client/Client_Connection_Samples.cs new file mode 100644 index 0000000..1efc0fe --- /dev/null +++ b/BPA.AspNetCore.Test/Client/Client_Connection_Samples.cs @@ -0,0 +1,275 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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.Client; +using MQTTnet.Formatter; +using System; +using System.Security.Authentication; +using System.Threading; +using System.Threading.Tasks; + +namespace BPA.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("10.2.1.21",1883).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/BPA.AspNetCore.Test/Client/Client_Publish_Samples.cs b/BPA.AspNetCore.Test/Client/Client_Publish_Samples.cs new file mode 100644 index 0000000..bb6f5b0 --- /dev/null +++ b/BPA.AspNetCore.Test/Client/Client_Publish_Samples.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using MQTTnet.Client; +using System; +using System.Threading; +using System.Threading.Tasks; + +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/BPA.AspNetCore.Test/Client/Client_Subscribe_Samples.cs b/BPA.AspNetCore.Test/Client/Client_Subscribe_Samples.cs new file mode 100644 index 0000000..e66c0f6 --- /dev/null +++ b/BPA.AspNetCore.Test/Client/Client_Subscribe_Samples.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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.Threading; +using System.Threading.Tasks; + +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("10.2.1.21",1883) + .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/BPA.AspNetCore.Test/Pages/Error.cshtml b/BPA.AspNetCore.Test/Pages/Error.cshtml new file mode 100644 index 0000000..6f92b95 --- /dev/null +++ b/BPA.AspNetCore.Test/Pages/Error.cshtml @@ -0,0 +1,26 @@ +@page +@model ErrorModel +@{ + 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/BPA.AspNetCore.Test/Pages/Error.cshtml.cs b/BPA.AspNetCore.Test/Pages/Error.cshtml.cs new file mode 100644 index 0000000..356a4b1 --- /dev/null +++ b/BPA.AspNetCore.Test/Pages/Error.cshtml.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; + +namespace BPA.AspNetCore.Test.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/BPA.AspNetCore.Test/Pages/Index.cshtml b/BPA.AspNetCore.Test/Pages/Index.cshtml new file mode 100644 index 0000000..b5f0c15 --- /dev/null +++ b/BPA.AspNetCore.Test/Pages/Index.cshtml @@ -0,0 +1,10 @@ +@page +@model IndexModel +@{ + ViewData["Title"] = "Home page"; +} + +
+

Welcome

+

Learn about building Web apps with ASP.NET Core.

+
diff --git a/BPA.AspNetCore.Test/Pages/Index.cshtml.cs b/BPA.AspNetCore.Test/Pages/Index.cshtml.cs new file mode 100644 index 0000000..d3532bb --- /dev/null +++ b/BPA.AspNetCore.Test/Pages/Index.cshtml.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BPA.AspNetCore.Test.Pages +{ + public class IndexModel : PageModel + { + private readonly ILogger _logger; + + public IndexModel(ILogger logger) + { + _logger = logger; + } + + public void OnGet() + { + + } + } +} diff --git a/BPA.AspNetCore.Test/Pages/Privacy.cshtml b/BPA.AspNetCore.Test/Pages/Privacy.cshtml new file mode 100644 index 0000000..46ba966 --- /dev/null +++ b/BPA.AspNetCore.Test/Pages/Privacy.cshtml @@ -0,0 +1,8 @@ +@page +@model PrivacyModel +@{ + ViewData["Title"] = "Privacy Policy"; +} +

@ViewData["Title"]

+ +

Use this page to detail your site's privacy policy.

diff --git a/BPA.AspNetCore.Test/Pages/Privacy.cshtml.cs b/BPA.AspNetCore.Test/Pages/Privacy.cshtml.cs new file mode 100644 index 0000000..5bf85a1 --- /dev/null +++ b/BPA.AspNetCore.Test/Pages/Privacy.cshtml.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BPA.AspNetCore.Test.Pages +{ + public class PrivacyModel : PageModel + { + private readonly ILogger _logger; + + public PrivacyModel(ILogger logger) + { + _logger = logger; + } + + public void OnGet() + { + } + } +} diff --git a/BPA.AspNetCore.Test/Pages/Shared/_Layout.cshtml b/BPA.AspNetCore.Test/Pages/Shared/_Layout.cshtml new file mode 100644 index 0000000..59ec03b --- /dev/null +++ b/BPA.AspNetCore.Test/Pages/Shared/_Layout.cshtml @@ -0,0 +1,50 @@ + + + + + + @ViewData["Title"] - BPA.AspNetCore.Test + + + + +
+ +
+
+
+ @RenderBody() +
+
+ +
+
+ © 2022 - BPA.AspNetCore.Test - Privacy +
+
+ + + + + + @RenderSection("Scripts", required: false) + + diff --git a/BPA.AspNetCore.Test/Pages/Shared/_ValidationScriptsPartial.cshtml b/BPA.AspNetCore.Test/Pages/Shared/_ValidationScriptsPartial.cshtml new file mode 100644 index 0000000..5a16d80 --- /dev/null +++ b/BPA.AspNetCore.Test/Pages/Shared/_ValidationScriptsPartial.cshtml @@ -0,0 +1,2 @@ + + diff --git a/BPA.AspNetCore.Test/Pages/_ViewImports.cshtml b/BPA.AspNetCore.Test/Pages/_ViewImports.cshtml new file mode 100644 index 0000000..e58f49d --- /dev/null +++ b/BPA.AspNetCore.Test/Pages/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using BPA.AspNetCore.Test +@namespace BPA.AspNetCore.Test.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/BPA.AspNetCore.Test/Pages/_ViewStart.cshtml b/BPA.AspNetCore.Test/Pages/_ViewStart.cshtml new file mode 100644 index 0000000..a5f1004 --- /dev/null +++ b/BPA.AspNetCore.Test/Pages/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/BPA.AspNetCore.Test/Program.cs b/BPA.AspNetCore.Test/Program.cs new file mode 100644 index 0000000..63942b4 --- /dev/null +++ b/BPA.AspNetCore.Test/Program.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BPA.AspNetCore.Test +{ + 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/BPA.AspNetCore.Test/Startup.cs b/BPA.AspNetCore.Test/Startup.cs new file mode 100644 index 0000000..f50df87 --- /dev/null +++ b/BPA.AspNetCore.Test/Startup.cs @@ -0,0 +1,56 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BPA.AspNetCore.Test +{ + 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. + public void ConfigureServices(IServiceCollection services) + { + 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(); + } + 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.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapRazorPages(); + }); + } + } +} diff --git a/BPA.AspNetCore.Test/appsettings.Development.json b/BPA.AspNetCore.Test/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/BPA.AspNetCore.Test/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/BPA.AspNetCore.Test/appsettings.json b/BPA.AspNetCore.Test/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/BPA.AspNetCore.Test/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/BPA.AspNetCore.Test/wwwroot/css/site.css b/BPA.AspNetCore.Test/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/BPA.AspNetCore.Test/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/BPA.AspNetCore.Test/wwwroot/favicon.ico b/BPA.AspNetCore.Test/wwwroot/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a3a799985c43bc7309d701b2cad129023377dc71 GIT binary patch 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_ .col, +.no-gutters > [class*="col-"] { + padding-right: 0; + padding-left: 0; +} + +.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col, +.col-auto, .col-sm-1, .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-10, .col-sm-11, .col-sm-12, .col-sm, +.col-sm-auto, .col-md-1, .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-10, .col-md-11, .col-md-12, .col-md, +.col-md-auto, .col-lg-1, .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-10, .col-lg-11, .col-lg-12, .col-lg, +.col-lg-auto, .col-xl-1, .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-10, .col-xl-11, .col-xl-12, .col-xl, +.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%; + } +} + +.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; + } +} + +.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; + } +} + +.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: 0.25rem !important; +} + +.mt-1, +.my-1 { + margin-top: 0.25rem !important; +} + +.mr-1, +.mx-1 { + margin-right: 0.25rem !important; +} + +.mb-1, +.my-1 { + margin-bottom: 0.25rem !important; +} + +.ml-1, +.mx-1 { + margin-left: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.mt-2, +.my-2 { + margin-top: 0.5rem !important; +} + +.mr-2, +.mx-2 { + margin-right: 0.5rem !important; +} + +.mb-2, +.my-2 { + margin-bottom: 0.5rem !important; +} + +.ml-2, +.mx-2 { + margin-left: 0.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: 0.25rem !important; +} + +.pt-1, +.py-1 { + padding-top: 0.25rem !important; +} + +.pr-1, +.px-1 { + padding-right: 0.25rem !important; +} + +.pb-1, +.py-1 { + padding-bottom: 0.25rem !important; +} + +.pl-1, +.px-1 { + padding-left: 0.25rem !important; +} + +.p-2 { + padding: 0.5rem !important; +} + +.pt-2, +.py-2 { + padding-top: 0.5rem !important; +} + +.pr-2, +.px-2 { + padding-right: 0.5rem !important; +} + +.pb-2, +.py-2 { + padding-bottom: 0.5rem !important; +} + +.pl-2, +.px-2 { + padding-left: 0.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: -0.25rem !important; +} + +.mt-n1, +.my-n1 { + margin-top: -0.25rem !important; +} + +.mr-n1, +.mx-n1 { + margin-right: -0.25rem !important; +} + +.mb-n1, +.my-n1 { + margin-bottom: -0.25rem !important; +} + +.ml-n1, +.mx-n1 { + margin-left: -0.25rem !important; +} + +.m-n2 { + margin: -0.5rem !important; +} + +.mt-n2, +.my-n2 { + margin-top: -0.5rem !important; +} + +.mr-n2, +.mx-n2 { + margin-right: -0.5rem !important; +} + +.mb-n2, +.my-n2 { + margin-bottom: -0.5rem !important; +} + +.ml-n2, +.mx-n2 { + margin-left: -0.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: 0.25rem !important; + } + .mt-sm-1, + .my-sm-1 { + margin-top: 0.25rem !important; + } + .mr-sm-1, + .mx-sm-1 { + margin-right: 0.25rem !important; + } + .mb-sm-1, + .my-sm-1 { + margin-bottom: 0.25rem !important; + } + .ml-sm-1, + .mx-sm-1 { + margin-left: 0.25rem !important; + } + .m-sm-2 { + margin: 0.5rem !important; + } + .mt-sm-2, + .my-sm-2 { + margin-top: 0.5rem !important; + } + .mr-sm-2, + .mx-sm-2 { + margin-right: 0.5rem !important; + } + .mb-sm-2, + .my-sm-2 { + margin-bottom: 0.5rem !important; + } + .ml-sm-2, + .mx-sm-2 { + margin-left: 0.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: 0.25rem !important; + } + .pt-sm-1, + .py-sm-1 { + padding-top: 0.25rem !important; + } + .pr-sm-1, + .px-sm-1 { + padding-right: 0.25rem !important; + } + .pb-sm-1, + .py-sm-1 { + padding-bottom: 0.25rem !important; + } + .pl-sm-1, + .px-sm-1 { + padding-left: 0.25rem !important; + } + .p-sm-2 { + padding: 0.5rem !important; + } + .pt-sm-2, + .py-sm-2 { + padding-top: 0.5rem !important; + } + .pr-sm-2, + .px-sm-2 { + padding-right: 0.5rem !important; + } + .pb-sm-2, + .py-sm-2 { + padding-bottom: 0.5rem !important; + } + .pl-sm-2, + .px-sm-2 { + padding-left: 0.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: -0.25rem !important; + } + .mt-sm-n1, + .my-sm-n1 { + margin-top: -0.25rem !important; + } + .mr-sm-n1, + .mx-sm-n1 { + margin-right: -0.25rem !important; + } + .mb-sm-n1, + .my-sm-n1 { + margin-bottom: -0.25rem !important; + } + .ml-sm-n1, + .mx-sm-n1 { + margin-left: -0.25rem !important; + } + .m-sm-n2 { + margin: -0.5rem !important; + } + .mt-sm-n2, + .my-sm-n2 { + margin-top: -0.5rem !important; + } + .mr-sm-n2, + .mx-sm-n2 { + margin-right: -0.5rem !important; + } + .mb-sm-n2, + .my-sm-n2 { + margin-bottom: -0.5rem !important; + } + .ml-sm-n2, + .mx-sm-n2 { + margin-left: -0.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: 0.25rem !important; + } + .mt-md-1, + .my-md-1 { + margin-top: 0.25rem !important; + } + .mr-md-1, + .mx-md-1 { + margin-right: 0.25rem !important; + } + .mb-md-1, + .my-md-1 { + margin-bottom: 0.25rem !important; + } + .ml-md-1, + .mx-md-1 { + margin-left: 0.25rem !important; + } + .m-md-2 { + margin: 0.5rem !important; + } + .mt-md-2, + .my-md-2 { + margin-top: 0.5rem !important; + } + .mr-md-2, + .mx-md-2 { + margin-right: 0.5rem !important; + } + .mb-md-2, + .my-md-2 { + margin-bottom: 0.5rem !important; + } + .ml-md-2, + .mx-md-2 { + margin-left: 0.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: 0.25rem !important; + } + .pt-md-1, + .py-md-1 { + padding-top: 0.25rem !important; + } + .pr-md-1, + .px-md-1 { + padding-right: 0.25rem !important; + } + .pb-md-1, + .py-md-1 { + padding-bottom: 0.25rem !important; + } + .pl-md-1, + .px-md-1 { + padding-left: 0.25rem !important; + } + .p-md-2 { + padding: 0.5rem !important; + } + .pt-md-2, + .py-md-2 { + padding-top: 0.5rem !important; + } + .pr-md-2, + .px-md-2 { + padding-right: 0.5rem !important; + } + .pb-md-2, + .py-md-2 { + padding-bottom: 0.5rem !important; + } + .pl-md-2, + .px-md-2 { + padding-left: 0.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: -0.25rem !important; + } + .mt-md-n1, + .my-md-n1 { + margin-top: -0.25rem !important; + } + .mr-md-n1, + .mx-md-n1 { + margin-right: -0.25rem !important; + } + .mb-md-n1, + .my-md-n1 { + margin-bottom: -0.25rem !important; + } + .ml-md-n1, + .mx-md-n1 { + margin-left: -0.25rem !important; + } + .m-md-n2 { + margin: -0.5rem !important; + } + .mt-md-n2, + .my-md-n2 { + margin-top: -0.5rem !important; + } + .mr-md-n2, + .mx-md-n2 { + margin-right: -0.5rem !important; + } + .mb-md-n2, + .my-md-n2 { + margin-bottom: -0.5rem !important; + } + .ml-md-n2, + .mx-md-n2 { + margin-left: -0.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: 0.25rem !important; + } + .mt-lg-1, + .my-lg-1 { + margin-top: 0.25rem !important; + } + .mr-lg-1, + .mx-lg-1 { + margin-right: 0.25rem !important; + } + .mb-lg-1, + .my-lg-1 { + margin-bottom: 0.25rem !important; + } + .ml-lg-1, + .mx-lg-1 { + margin-left: 0.25rem !important; + } + .m-lg-2 { + margin: 0.5rem !important; + } + .mt-lg-2, + .my-lg-2 { + margin-top: 0.5rem !important; + } + .mr-lg-2, + .mx-lg-2 { + margin-right: 0.5rem !important; + } + .mb-lg-2, + .my-lg-2 { + margin-bottom: 0.5rem !important; + } + .ml-lg-2, + .mx-lg-2 { + margin-left: 0.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: 0.25rem !important; + } + .pt-lg-1, + .py-lg-1 { + padding-top: 0.25rem !important; + } + .pr-lg-1, + .px-lg-1 { + padding-right: 0.25rem !important; + } + .pb-lg-1, + .py-lg-1 { + padding-bottom: 0.25rem !important; + } + .pl-lg-1, + .px-lg-1 { + padding-left: 0.25rem !important; + } + .p-lg-2 { + padding: 0.5rem !important; + } + .pt-lg-2, + .py-lg-2 { + padding-top: 0.5rem !important; + } + .pr-lg-2, + .px-lg-2 { + padding-right: 0.5rem !important; + } + .pb-lg-2, + .py-lg-2 { + padding-bottom: 0.5rem !important; + } + .pl-lg-2, + .px-lg-2 { + padding-left: 0.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: -0.25rem !important; + } + .mt-lg-n1, + .my-lg-n1 { + margin-top: -0.25rem !important; + } + .mr-lg-n1, + .mx-lg-n1 { + margin-right: -0.25rem !important; + } + .mb-lg-n1, + .my-lg-n1 { + margin-bottom: -0.25rem !important; + } + .ml-lg-n1, + .mx-lg-n1 { + margin-left: -0.25rem !important; + } + .m-lg-n2 { + margin: -0.5rem !important; + } + .mt-lg-n2, + .my-lg-n2 { + margin-top: -0.5rem !important; + } + .mr-lg-n2, + .mx-lg-n2 { + margin-right: -0.5rem !important; + } + .mb-lg-n2, + .my-lg-n2 { + margin-bottom: -0.5rem !important; + } + .ml-lg-n2, + .mx-lg-n2 { + margin-left: -0.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: 0.25rem !important; + } + .mt-xl-1, + .my-xl-1 { + margin-top: 0.25rem !important; + } + .mr-xl-1, + .mx-xl-1 { + margin-right: 0.25rem !important; + } + .mb-xl-1, + .my-xl-1 { + margin-bottom: 0.25rem !important; + } + .ml-xl-1, + .mx-xl-1 { + margin-left: 0.25rem !important; + } + .m-xl-2 { + margin: 0.5rem !important; + } + .mt-xl-2, + .my-xl-2 { + margin-top: 0.5rem !important; + } + .mr-xl-2, + .mx-xl-2 { + margin-right: 0.5rem !important; + } + .mb-xl-2, + .my-xl-2 { + margin-bottom: 0.5rem !important; + } + .ml-xl-2, + .mx-xl-2 { + margin-left: 0.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: 0.25rem !important; + } + .pt-xl-1, + .py-xl-1 { + padding-top: 0.25rem !important; + } + .pr-xl-1, + .px-xl-1 { + padding-right: 0.25rem !important; + } + .pb-xl-1, + .py-xl-1 { + padding-bottom: 0.25rem !important; + } + .pl-xl-1, + .px-xl-1 { + padding-left: 0.25rem !important; + } + .p-xl-2 { + padding: 0.5rem !important; + } + .pt-xl-2, + .py-xl-2 { + padding-top: 0.5rem !important; + } + .pr-xl-2, + .px-xl-2 { + padding-right: 0.5rem !important; + } + .pb-xl-2, + .py-xl-2 { + padding-bottom: 0.5rem !important; + } + .pl-xl-2, + .px-xl-2 { + padding-left: 0.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: -0.25rem !important; + } + .mt-xl-n1, + .my-xl-n1 { + margin-top: -0.25rem !important; + } + .mr-xl-n1, + .mx-xl-n1 { + margin-right: -0.25rem !important; + } + .mb-xl-n1, + .my-xl-n1 { + margin-bottom: -0.25rem !important; + } + .ml-xl-n1, + .mx-xl-n1 { + margin-left: -0.25rem !important; + } + .m-xl-n2 { + margin: -0.5rem !important; + } + .mt-xl-n2, + .my-xl-n2 { + margin-top: -0.5rem !important; + } + .mr-xl-n2, + .mx-xl-n2 { + margin-right: -0.5rem !important; + } + .mb-xl-n2, + .my-xl-n2 { + margin-bottom: -0.5rem !important; + } + .ml-xl-n2, + .mx-xl-n2 { + margin-left: -0.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; + } +} +/*# sourceMappingURL=bootstrap-grid.css.map */ \ No newline at end of file diff --git a/BPA.AspNetCore.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css b/BPA.AspNetCore.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css new file mode 100644 index 0000000..e5e74f7 --- /dev/null +++ b/BPA.AspNetCore.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap Grid 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) + */html{box-sizing:border-box;-ms-overflow-style:scrollbar}*,::after,::before{box-sizing:inherit}.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%}}.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}}.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}}.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}} +/*# sourceMappingURL=bootstrap-grid.min.css.map */ \ No newline at end of file diff --git a/BPA.AspNetCore.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css b/BPA.AspNetCore.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css new file mode 100644 index 0000000..09cf986 --- /dev/null +++ b/BPA.AspNetCore.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css @@ -0,0 +1,331 @@ +/*! + * Bootstrap Reboot 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) + * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) + */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + line-height: 1.15; + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +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: 0.5rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +abbr[title], +abbr[data-original-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; +} + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; +} + +ol ol, +ul ul, +ol ul, +ul ol { + 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]):hover, a:not([href]):not([tabindex]):focus { + color: inherit; + text-decoration: none; +} + +a:not([href]):not([tabindex]):focus { + outline: 0; +} + +pre, +code, +kbd, +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: 0.75rem; + padding-bottom: 0.75rem; + color: #6c757d; + text-align: left; + caption-side: bottom; +} + +th { + text-align: inherit; +} + +label { + display: inline-block; + margin-bottom: 0.5rem; +} + +button { + border-radius: 0; +} + +button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; +} + +input, +button, +select, +optgroup, +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; +} + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +button:not(:disabled), +[type="button"]:not(:disabled), +[type="reset"]:not(:disabled), +[type="submit"]:not(:disabled) { + cursor: pointer; +} + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + padding: 0; + border-style: none; +} + +input[type="radio"], +input[type="checkbox"] { + box-sizing: border-box; + padding: 0; +} + +input[type="date"], +input[type="time"], +input[type="datetime-local"], +input[type="month"] { + -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; +} +/*# sourceMappingURL=bootstrap-reboot.css.map */ \ No newline at end of file diff --git a/BPA.AspNetCore.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css b/BPA.AspNetCore.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css new file mode 100644 index 0000000..c804b3b --- /dev/null +++ b/BPA.AspNetCore.Test/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css @@ -0,0 +1,8 @@ +/*! + * Bootstrap Reboot 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) + * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) + */*,::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} +/*# sourceMappingURL=bootstrap-reboot.min.css.map */ \ No newline at end of file diff --git a/BPA.AspNetCore.Test/wwwroot/lib/bootstrap/dist/css/bootstrap.css b/BPA.AspNetCore.Test/wwwroot/lib/bootstrap/dist/css/bootstrap.css new file mode 100644 index 0000000..8f47589 --- /dev/null +++ b/BPA.AspNetCore.Test/wwwroot/lib/bootstrap/dist/css/bootstrap.css @@ -0,0 +1,10038 @@ +/*! + * 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; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + line-height: 1.15; + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +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: 0.5rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +abbr[title], +abbr[data-original-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; +} + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; +} + +ol ol, +ul ul, +ol ul, +ul ol { + 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]):hover, a:not([href]):not([tabindex]):focus { + color: inherit; + text-decoration: none; +} + +a:not([href]):not([tabindex]):focus { + outline: 0; +} + +pre, +code, +kbd, +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: 0.75rem; + padding-bottom: 0.75rem; + color: #6c757d; + text-align: left; + caption-side: bottom; +} + +th { + text-align: inherit; +} + +label { + display: inline-block; + margin-bottom: 0.5rem; +} + +button { + border-radius: 0; +} + +button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; +} + +input, +button, +select, +optgroup, +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; +} + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +button:not(:disabled), +[type="button"]:not(:disabled), +[type="reset"]:not(:disabled), +[type="submit"]:not(:disabled) { + cursor: pointer; +} + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + padding: 0; + border-style: none; +} + +input[type="radio"], +input[type="checkbox"] { + box-sizing: border-box; + padding: 0; +} + +input[type="date"], +input[type="time"], +input[type="datetime-local"], +input[type="month"] { + -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: 0.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, 0.1); +} + +small, +.small { + font-size: 80%; + font-weight: 400; +} + +mark, +.mark { + padding: 0.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: 0.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: 0.25rem; + background-color: #fff; + border: 1px solid #dee2e6; + border-radius: 0.25rem; + max-width: 100%; + height: auto; +} + +.figure { + display: inline-block; +} + +.figure-img { + margin-bottom: 0.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: 0.2rem 0.4rem; + font-size: 87.5%; + color: #fff; + background-color: #212529; + border-radius: 0.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-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col, +.col-auto, .col-sm-1, .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-10, .col-sm-11, .col-sm-12, .col-sm, +.col-sm-auto, .col-md-1, .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-10, .col-md-11, .col-md-12, .col-md, +.col-md-auto, .col-lg-1, .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-10, .col-lg-11, .col-lg-12, .col-lg, +.col-lg-auto, .col-xl-1, .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-10, .col-xl-11, .col-xl-12, .col-xl, +.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 th, +.table td { + padding: 0.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 th, +.table-sm td { + padding: 0.3rem; +} + +.table-bordered { + border: 1px solid #dee2e6; +} + +.table-bordered th, +.table-bordered td { + border: 1px solid #dee2e6; +} + +.table-bordered thead th, +.table-bordered thead td { + border-bottom-width: 2px; +} + +.table-borderless th, +.table-borderless td, +.table-borderless thead th, +.table-borderless tbody + tbody { + border: 0; +} + +.table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(0, 0, 0, 0.05); +} + +.table-hover tbody tr:hover { + color: #212529; + background-color: rgba(0, 0, 0, 0.075); +} + +.table-primary, +.table-primary > th, +.table-primary > td { + background-color: #b8daff; +} + +.table-primary th, +.table-primary td, +.table-primary thead th, +.table-primary tbody + tbody { + 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 > th, +.table-secondary > td { + background-color: #d6d8db; +} + +.table-secondary th, +.table-secondary td, +.table-secondary thead th, +.table-secondary tbody + tbody { + 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 > th, +.table-success > td { + background-color: #c3e6cb; +} + +.table-success th, +.table-success td, +.table-success thead th, +.table-success tbody + tbody { + 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 > th, +.table-info > td { + background-color: #bee5eb; +} + +.table-info th, +.table-info td, +.table-info thead th, +.table-info tbody + tbody { + 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 > th, +.table-warning > td { + background-color: #ffeeba; +} + +.table-warning th, +.table-warning td, +.table-warning thead th, +.table-warning tbody + tbody { + 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 > th, +.table-danger > td { + background-color: #f5c6cb; +} + +.table-danger th, +.table-danger td, +.table-danger thead th, +.table-danger tbody + tbody { + 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 > th, +.table-light > td { + background-color: #fdfdfe; +} + +.table-light th, +.table-light td, +.table-light thead th, +.table-light tbody + tbody { + 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 > th, +.table-dark > td { + background-color: #c6c8ca; +} + +.table-dark th, +.table-dark td, +.table-dark thead th, +.table-dark tbody + tbody { + 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 > th, +.table-active > td { + background-color: rgba(0, 0, 0, 0.075); +} + +.table-hover .table-active:hover { + background-color: rgba(0, 0, 0, 0.075); +} + +.table-hover .table-active:hover > td, +.table-hover .table-active:hover > th { + background-color: rgba(0, 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 th, +.table-dark td, +.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, 0.05); +} + +.table-dark.table-hover tbody tr:hover { + color: #fff; + background-color: rgba(255, 255, 255, 0.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 + 0.75rem + 2px); + padding: 0.375rem 0.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: 0.25rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.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 0.2rem rgba(0, 123, 255, 0.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(0.375rem + 1px); + padding-bottom: calc(0.375rem + 1px); + margin-bottom: 0; + font-size: inherit; + line-height: 1.5; +} + +.col-form-label-lg { + padding-top: calc(0.5rem + 1px); + padding-bottom: calc(0.5rem + 1px); + font-size: 1.25rem; + line-height: 1.5; +} + +.col-form-label-sm { + padding-top: calc(0.25rem + 1px); + padding-bottom: calc(0.25rem + 1px); + font-size: 0.875rem; + line-height: 1.5; +} + +.form-control-plaintext { + display: block; + width: 100%; + padding-top: 0.375rem; + padding-bottom: 0.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-sm, .form-control-plaintext.form-control-lg { + padding-right: 0; + padding-left: 0; +} + +.form-control-sm { + height: calc(1.5em + 0.5rem + 2px); + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; +} + +.form-control-lg { + height: calc(1.5em + 1rem + 2px); + padding: 0.5rem 1rem; + font-size: 1.25rem; + line-height: 1.5; + border-radius: 0.3rem; +} + +select.form-control[size], select.form-control[multiple] { + height: auto; +} + +textarea.form-control { + height: auto; +} + +.form-group { + margin-bottom: 1rem; +} + +.form-text { + display: block; + margin-top: 0.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: 0.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: 0.75rem; +} + +.form-check-inline .form-check-input { + position: static; + margin-top: 0; + margin-right: 0.3125rem; + margin-left: 0; +} + +.valid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 80%; + color: #28a745; +} + +.valid-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: 0.25rem 0.5rem; + margin-top: .1rem; + font-size: 0.875rem; + line-height: 1.5; + color: #fff; + background-color: rgba(40, 167, 69, 0.9); + border-radius: 0.25rem; +} + +.was-validated .form-control:valid, .form-control.is-valid { + border-color: #28a745; + padding-right: calc(1.5em + 0.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(0.375em + 0.1875rem); + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} + +.was-validated .form-control:valid:focus, .form-control.is-valid:focus { + border-color: #28a745; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.was-validated .form-control:valid ~ .valid-feedback, +.was-validated .form-control:valid ~ .valid-tooltip, .form-control.is-valid ~ .valid-feedback, +.form-control.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated textarea.form-control:valid, textarea.form-control.is-valid { + padding-right: calc(1.5em + 0.75rem); + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); +} + +.was-validated .custom-select:valid, .custom-select.is-valid { + border-color: #28a745; + padding-right: calc((1em + 0.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 0.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(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} + +.was-validated .custom-select:valid:focus, .custom-select.is-valid:focus { + border-color: #28a745; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.was-validated .custom-select:valid ~ .valid-feedback, +.was-validated .custom-select:valid ~ .valid-tooltip, .custom-select.is-valid ~ .valid-feedback, +.custom-select.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .form-control-file:valid ~ .valid-feedback, +.was-validated .form-control-file:valid ~ .valid-tooltip, .form-control-file.is-valid ~ .valid-feedback, +.form-control-file.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label { + color: #28a745; +} + +.was-validated .form-check-input:valid ~ .valid-feedback, +.was-validated .form-check-input:valid ~ .valid-tooltip, .form-check-input.is-valid ~ .valid-feedback, +.form-check-input.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .custom-control-input:valid ~ .custom-control-label, .custom-control-input.is-valid ~ .custom-control-label { + color: #28a745; +} + +.was-validated .custom-control-input:valid ~ .custom-control-label::before, .custom-control-input.is-valid ~ .custom-control-label::before { + border-color: #28a745; +} + +.was-validated .custom-control-input:valid ~ .valid-feedback, +.was-validated .custom-control-input:valid ~ .valid-tooltip, .custom-control-input.is-valid ~ .valid-feedback, +.custom-control-input.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before, .custom-control-input.is-valid:checked ~ .custom-control-label::before { + border-color: #34ce57; + background-color: #34ce57; +} + +.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before, .custom-control-input.is-valid:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before { + border-color: #28a745; +} + +.was-validated .custom-file-input:valid ~ .custom-file-label, .custom-file-input.is-valid ~ .custom-file-label { + border-color: #28a745; +} + +.was-validated .custom-file-input:valid ~ .valid-feedback, +.was-validated .custom-file-input:valid ~ .valid-tooltip, .custom-file-input.is-valid ~ .valid-feedback, +.custom-file-input.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .custom-file-input:valid:focus ~ .custom-file-label, .custom-file-input.is-valid:focus ~ .custom-file-label { + border-color: #28a745; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.invalid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 80%; + color: #dc3545; +} + +.invalid-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: 0.25rem 0.5rem; + margin-top: .1rem; + font-size: 0.875rem; + line-height: 1.5; + color: #fff; + background-color: rgba(220, 53, 69, 0.9); + border-radius: 0.25rem; +} + +.was-validated .form-control:invalid, .form-control.is-invalid { + border-color: #dc3545; + padding-right: calc(1.5em + 0.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(0.375em + 0.1875rem); + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} + +.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + +.was-validated .form-control:invalid ~ .invalid-feedback, +.was-validated .form-control:invalid ~ .invalid-tooltip, .form-control.is-invalid ~ .invalid-feedback, +.form-control.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid { + padding-right: calc(1.5em + 0.75rem); + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); +} + +.was-validated .custom-select:invalid, .custom-select.is-invalid { + border-color: #dc3545; + padding-right: calc((1em + 0.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 0.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(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} + +.was-validated .custom-select:invalid:focus, .custom-select.is-invalid:focus { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + +.was-validated .custom-select:invalid ~ .invalid-feedback, +.was-validated .custom-select:invalid ~ .invalid-tooltip, .custom-select.is-invalid ~ .invalid-feedback, +.custom-select.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .form-control-file:invalid ~ .invalid-feedback, +.was-validated .form-control-file:invalid ~ .invalid-tooltip, .form-control-file.is-invalid ~ .invalid-feedback, +.form-control-file.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label { + color: #dc3545; +} + +.was-validated .form-check-input:invalid ~ .invalid-feedback, +.was-validated .form-check-input:invalid ~ .invalid-tooltip, .form-check-input.is-invalid ~ .invalid-feedback, +.form-check-input.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .custom-control-input:invalid ~ .custom-control-label, .custom-control-input.is-invalid ~ .custom-control-label { + color: #dc3545; +} + +.was-validated .custom-control-input:invalid ~ .custom-control-label::before, .custom-control-input.is-invalid ~ .custom-control-label::before { + border-color: #dc3545; +} + +.was-validated .custom-control-input:invalid ~ .invalid-feedback, +.was-validated .custom-control-input:invalid ~ .invalid-tooltip, .custom-control-input.is-invalid ~ .invalid-feedback, +.custom-control-input.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before, .custom-control-input.is-invalid:checked ~ .custom-control-label::before { + border-color: #e4606d; + background-color: #e4606d; +} + +.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before, .custom-control-input.is-invalid:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + +.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before { + border-color: #dc3545; +} + +.was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label { + border-color: #dc3545; +} + +.was-validated .custom-file-input:invalid ~ .invalid-feedback, +.was-validated .custom-file-input:invalid ~ .invalid-tooltip, .custom-file-input.is-invalid ~ .invalid-feedback, +.custom-file-input.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .custom-file-input:invalid:focus ~ .custom-file-label, .custom-file-input.is-invalid:focus ~ .custom-file-label { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.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 .input-group, + .form-inline .custom-select { + 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: 0.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: 0.375rem 0.75rem; + font-size: 1rem; + line-height: 1.5; + border-radius: 0.25rem; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.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 0.2rem rgba(0, 123, 255, 0.25); +} + +.btn.disabled, .btn:disabled { + opacity: 0.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 0.2rem rgba(38, 143, 255, 0.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 0.2rem rgba(38, 143, 255, 0.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 0.2rem rgba(130, 138, 145, 0.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 0.2rem rgba(130, 138, 145, 0.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 0.2rem rgba(72, 180, 97, 0.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 0.2rem rgba(72, 180, 97, 0.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 0.2rem rgba(58, 176, 195, 0.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 0.2rem rgba(58, 176, 195, 0.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 0.2rem rgba(222, 170, 12, 0.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 0.2rem rgba(222, 170, 12, 0.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 0.2rem rgba(225, 83, 97, 0.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 0.2rem rgba(225, 83, 97, 0.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 0.2rem rgba(216, 217, 219, 0.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 0.2rem rgba(216, 217, 219, 0.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 0.2rem rgba(82, 88, 93, 0.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 0.2rem rgba(82, 88, 93, 0.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 0.2rem rgba(0, 123, 255, 0.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 0.2rem rgba(0, 123, 255, 0.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 0.2rem rgba(108, 117, 125, 0.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 0.2rem rgba(108, 117, 125, 0.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 0.2rem rgba(40, 167, 69, 0.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 0.2rem rgba(40, 167, 69, 0.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 0.2rem rgba(23, 162, 184, 0.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 0.2rem rgba(23, 162, 184, 0.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 0.2rem rgba(255, 193, 7, 0.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 0.2rem rgba(255, 193, 7, 0.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 0.2rem rgba(220, 53, 69, 0.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 0.2rem rgba(220, 53, 69, 0.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 0.2rem rgba(248, 249, 250, 0.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 0.2rem rgba(248, 249, 250, 0.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 0.2rem rgba(52, 58, 64, 0.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 0.2rem rgba(52, 58, 64, 0.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-lg, .btn-group-lg > .btn { + padding: 0.5rem 1rem; + font-size: 1.25rem; + line-height: 1.5; + border-radius: 0.3rem; +} + +.btn-sm, .btn-group-sm > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; +} + +.btn-block { + display: block; + width: 100%; +} + +.btn-block + .btn-block { + margin-top: 0.5rem; +} + +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} + +.fade { + transition: opacity 0.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 0.35s ease; +} + +@media (prefers-reduced-motion: reduce) { + .collapsing { + transition: none; + } +} + +.dropup, +.dropright, +.dropdown, +.dropleft { + position: relative; +} + +.dropdown-toggle { + white-space: nowrap; +} + +.dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid; + border-right: 0.3em solid transparent; + border-bottom: 0; + border-left: 0.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: 0.5rem 0; + margin: 0.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, 0.15); + border-radius: 0.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: 0.125rem; +} + +.dropup .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0; + border-right: 0.3em solid transparent; + border-bottom: 0.3em solid; + border-left: 0.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: 0.125rem; +} + +.dropright .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0; + border-bottom: 0.3em solid transparent; + border-left: 0.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: 0.125rem; +} + +.dropleft .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; +} + +.dropleft .dropdown-toggle::after { + display: none; +} + +.dropleft .dropdown-toggle::before { + display: inline-block; + margin-right: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0.3em solid; + border-bottom: 0.3em solid transparent; +} + +.dropleft .dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropleft .dropdown-toggle::before { + vertical-align: 0; +} + +.dropdown-menu[x-placement^="top"], .dropdown-menu[x-placement^="right"], .dropdown-menu[x-placement^="bottom"], .dropdown-menu[x-placement^="left"] { + right: auto; + bottom: auto; +} + +.dropdown-divider { + height: 0; + margin: 0.5rem 0; + overflow: hidden; + border-top: 1px solid #e9ecef; +} + +.dropdown-item { + display: block; + width: 100%; + padding: 0.25rem 1.5rem; + clear: both; + font-weight: 400; + color: #212529; + text-align: inherit; + white-space: nowrap; + background-color: transparent; + border: 0; +} + +.dropdown-item:hover, .dropdown-item:focus { + 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: 0.5rem 1.5rem; + margin-bottom: 0; + font-size: 0.875rem; + color: #6c757d; + white-space: nowrap; +} + +.dropdown-item-text { + display: block; + padding: 0.25rem 1.5rem; + color: #212529; +} + +.btn-group, +.btn-group-vertical { + position: relative; + display: -ms-inline-flexbox; + display: inline-flex; + vertical-align: middle; +} + +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + -ms-flex: 1 1 auto; + flex: 1 1 auto; +} + +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover { + z-index: 1; +} + +.btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active, +.btn-group-vertical > .btn:focus, +.btn-group-vertical > .btn:active, +.btn-group-vertical > .btn.active { + 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:not(:first-child), +.btn-group > .btn-group:not(:first-child) { + margin-left: -1px; +} + +.btn-group > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group > .btn-group:not(:last-child) > .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.btn-group > .btn:not(:first-child), +.btn-group > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.dropdown-toggle-split { + padding-right: 0.5625rem; + padding-left: 0.5625rem; +} + +.dropdown-toggle-split::after, +.dropup .dropdown-toggle-split::after, +.dropright .dropdown-toggle-split::after { + margin-left: 0; +} + +.dropleft .dropdown-toggle-split::before { + margin-right: 0; +} + +.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split { + padding-right: 0.375rem; + padding-left: 0.375rem; +} + +.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split { + padding-right: 0.75rem; + padding-left: 0.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:not(:first-child), +.btn-group-vertical > .btn-group:not(:first-child) { + margin-top: -1px; +} + +.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group-vertical > .btn-group:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.btn-group-vertical > .btn:not(:first-child), +.btn-group-vertical > .btn-group:not(:first-child) > .btn { + 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="radio"], +.btn-group-toggle > .btn input[type="checkbox"], +.btn-group-toggle > .btn-group > .btn input[type="radio"], +.btn-group-toggle > .btn-group > .btn input[type="checkbox"] { + 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 > .form-control, +.input-group > .form-control-plaintext, +.input-group > .custom-select, +.input-group > .custom-file { + position: relative; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + width: 1%; + margin-bottom: 0; +} + +.input-group > .form-control + .form-control, +.input-group > .form-control + .custom-select, +.input-group > .form-control + .custom-file, +.input-group > .form-control-plaintext + .form-control, +.input-group > .form-control-plaintext + .custom-select, +.input-group > .form-control-plaintext + .custom-file, +.input-group > .custom-select + .form-control, +.input-group > .custom-select + .custom-select, +.input-group > .custom-select + .custom-file, +.input-group > .custom-file + .form-control, +.input-group > .custom-file + .custom-select, +.input-group > .custom-file + .custom-file { + margin-left: -1px; +} + +.input-group > .form-control:focus, +.input-group > .custom-select:focus, +.input-group > .custom-file .custom-file-input:focus ~ .custom-file-label { + z-index: 3; +} + +.input-group > .custom-file .custom-file-input:focus { + z-index: 4; +} + +.input-group > .form-control:not(:last-child), +.input-group > .custom-select:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group > .form-control:not(:first-child), +.input-group > .custom-select: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-prepend, +.input-group-append { + display: -ms-flexbox; + display: flex; +} + +.input-group-prepend .btn, +.input-group-append .btn { + position: relative; + z-index: 2; +} + +.input-group-prepend .btn:focus, +.input-group-append .btn:focus { + z-index: 3; +} + +.input-group-prepend .btn + .btn, +.input-group-prepend .btn + .input-group-text, +.input-group-prepend .input-group-text + .input-group-text, +.input-group-prepend .input-group-text + .btn, +.input-group-append .btn + .btn, +.input-group-append .btn + .input-group-text, +.input-group-append .input-group-text + .input-group-text, +.input-group-append .input-group-text + .btn { + 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: 0.375rem 0.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: 0.25rem; +} + +.input-group-text input[type="radio"], +.input-group-text input[type="checkbox"] { + margin-top: 0; +} + +.input-group-lg > .form-control:not(textarea), +.input-group-lg > .custom-select { + height: calc(1.5em + 1rem + 2px); +} + +.input-group-lg > .form-control, +.input-group-lg > .custom-select, +.input-group-lg > .input-group-prepend > .input-group-text, +.input-group-lg > .input-group-append > .input-group-text, +.input-group-lg > .input-group-prepend > .btn, +.input-group-lg > .input-group-append > .btn { + padding: 0.5rem 1rem; + font-size: 1.25rem; + line-height: 1.5; + border-radius: 0.3rem; +} + +.input-group-sm > .form-control:not(textarea), +.input-group-sm > .custom-select { + height: calc(1.5em + 0.5rem + 2px); +} + +.input-group-sm > .form-control, +.input-group-sm > .custom-select, +.input-group-sm > .input-group-prepend > .input-group-text, +.input-group-sm > .input-group-append > .input-group-text, +.input-group-sm > .input-group-prepend > .btn, +.input-group-sm > .input-group-append > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; +} + +.input-group-lg > .custom-select, +.input-group-sm > .custom-select { + padding-right: 1.75rem; +} + +.input-group > .input-group-prepend > .btn, +.input-group > .input-group-prepend > .input-group-text, +.input-group > .input-group-append:not(:last-child) > .btn, +.input-group > .input-group-append:not(:last-child) > .input-group-text, +.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) { + 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:not(:first-child) > .btn, +.input-group > .input-group-prepend:not(:first-child) > .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) { + 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 0.2rem rgba(0, 123, 255, 0.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: 0.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: 0.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: 0.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, 0.5); +} + +.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before { + background-color: rgba(0, 123, 255, 0.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, 0.5); +} + +.custom-switch { + padding-left: 2.25rem; +} + +.custom-switch .custom-control-label::before { + left: -2.25rem; + width: 1.75rem; + pointer-events: all; + border-radius: 0.5rem; +} + +.custom-switch .custom-control-label::after { + top: calc(0.25rem + 2px); + left: calc(-2.25rem + 2px); + width: calc(1rem - 4px); + height: calc(1rem - 4px); + background-color: #adb5bd; + border-radius: 0.5rem; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out; + transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.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(0.75rem); + transform: translateX(0.75rem); +} + +.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before { + background-color: rgba(0, 123, 255, 0.5); +} + +.custom-select { + display: inline-block; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + padding: 0.375rem 1.75rem 0.375rem 0.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 0.75rem center/8px 10px; + background-color: #fff; + border: 1px solid #ced4da; + border-radius: 0.25rem; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.custom-select:focus { + border-color: #80bdff; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-select:focus::-ms-value { + color: #495057; + background-color: #fff; +} + +.custom-select[multiple], .custom-select[size]:not([size="1"]) { + height: auto; + padding-right: 0.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 + 0.5rem + 2px); + padding-top: 0.25rem; + padding-bottom: 0.25rem; + padding-left: 0.5rem; + font-size: 0.875rem; +} + +.custom-select-lg { + height: calc(1.5em + 1rem + 2px); + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1rem; + font-size: 1.25rem; +} + +.custom-file { + position: relative; + display: inline-block; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + margin-bottom: 0; +} + +.custom-file-input { + position: relative; + z-index: 2; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + margin: 0; + opacity: 0; +} + +.custom-file-input:focus ~ .custom-file-label { + border-color: #80bdff; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.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 + 0.75rem + 2px); + padding: 0.375rem 0.75rem; + font-weight: 400; + line-height: 1.5; + color: #495057; + background-color: #fff; + border: 1px solid #ced4da; + border-radius: 0.25rem; +} + +.custom-file-label::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + z-index: 3; + display: block; + height: calc(1.5em + 0.75rem); + padding: 0.375rem 0.75rem; + line-height: 1.5; + color: #495057; + content: "Browse"; + background-color: #e9ecef; + border-left: inherit; + border-radius: 0 0.25rem 0.25rem 0; +} + +.custom-range { + width: 100%; + height: calc(1rem + 0.4rem); + padding: 0; + background-color: transparent; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.custom-range:focus { + outline: none; +} + +.custom-range:focus::-webkit-slider-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-range:focus::-moz-range-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-range:focus::-ms-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-range::-moz-focus-outer { + border: 0; +} + +.custom-range::-webkit-slider-thumb { + width: 1rem; + height: 1rem; + margin-top: -0.25rem; + background-color: #007bff; + border: 0; + border-radius: 1rem; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.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: 0.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 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.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: 0.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: 0.2rem; + margin-left: 0.2rem; + background-color: #007bff; + border: 0; + border-radius: 1rem; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.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: 0.5rem; + color: transparent; + cursor: pointer; + background-color: transparent; + border-color: transparent; + border-width: 0.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 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.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: 0.5rem 1rem; +} + +.nav-link:hover, .nav-link:focus { + 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: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus { + border-color: #e9ecef #e9ecef #dee2e6; +} + +.nav-tabs .nav-link.disabled { + color: #6c757d; + background-color: transparent; + border-color: transparent; +} + +.nav-tabs .nav-link.active, +.nav-tabs .nav-item.show .nav-link { + 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: 0.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: 0.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: 0.3125rem; + padding-bottom: 0.3125rem; + margin-right: 1rem; + font-size: 1.25rem; + line-height: inherit; + white-space: nowrap; +} + +.navbar-brand:hover, .navbar-brand:focus { + 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: 0.5rem; + padding-bottom: 0.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: 0.25rem 0.75rem; + font-size: 1.25rem; + line-height: 1; + background-color: transparent; + border: 1px solid transparent; + border-radius: 0.25rem; +} + +.navbar-toggler:hover, .navbar-toggler:focus { + 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: 0.5rem; + padding-left: 0.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: 0.5rem; + padding-left: 0.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: 0.5rem; + padding-left: 0.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: 0.5rem; + padding-left: 0.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: 0.5rem; + padding-left: 0.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, 0.9); +} + +.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-light .navbar-nav .nav-link { + color: rgba(0, 0, 0, 0.5); +} + +.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus { + color: rgba(0, 0, 0, 0.7); +} + +.navbar-light .navbar-nav .nav-link.disabled { + color: rgba(0, 0, 0, 0.3); +} + +.navbar-light .navbar-nav .show > .nav-link, +.navbar-light .navbar-nav .active > .nav-link, +.navbar-light .navbar-nav .nav-link.show, +.navbar-light .navbar-nav .nav-link.active { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-light .navbar-toggler { + color: rgba(0, 0, 0, 0.5); + border-color: rgba(0, 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, 0.5); +} + +.navbar-light .navbar-text a { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-light .navbar-text a:hover, .navbar-light .navbar-text a:focus { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-dark .navbar-brand { + color: #fff; +} + +.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus { + color: #fff; +} + +.navbar-dark .navbar-nav .nav-link { + color: rgba(255, 255, 255, 0.5); +} + +.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus { + color: rgba(255, 255, 255, 0.75); +} + +.navbar-dark .navbar-nav .nav-link.disabled { + color: rgba(255, 255, 255, 0.25); +} + +.navbar-dark .navbar-nav .show > .nav-link, +.navbar-dark .navbar-nav .active > .nav-link, +.navbar-dark .navbar-nav .nav-link.show, +.navbar-dark .navbar-nav .nav-link.active { + color: #fff; +} + +.navbar-dark .navbar-toggler { + color: rgba(255, 255, 255, 0.5); + border-color: rgba(255, 255, 255, 0.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, 0.5); +} + +.navbar-dark .navbar-text a { + color: #fff; +} + +.navbar-dark .navbar-text a:hover, .navbar-dark .navbar-text a:focus { + 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, 0.125); + border-radius: 0.25rem; +} + +.card > hr { + margin-right: 0; + margin-left: 0; +} + +.card > .list-group:first-child .list-group-item:first-child { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.card > .list-group:last-child .list-group-item:last-child { + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.card-body { + -ms-flex: 1 1 auto; + flex: 1 1 auto; + padding: 1.25rem; +} + +.card-title { + margin-bottom: 0.75rem; +} + +.card-subtitle { + margin-top: -0.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: 0.75rem 1.25rem; + margin-bottom: 0; + background-color: rgba(0, 0, 0, 0.03); + border-bottom: 1px solid rgba(0, 0, 0, 0.125); +} + +.card-header:first-child { + border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0; +} + +.card-header + .list-group .list-group-item:first-child { + border-top: 0; +} + +.card-footer { + padding: 0.75rem 1.25rem; + background-color: rgba(0, 0, 0, 0.03); + border-top: 1px solid rgba(0, 0, 0, 0.125); +} + +.card-footer:last-child { + border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px); +} + +.card-header-tabs { + margin-right: -0.625rem; + margin-bottom: -0.75rem; + margin-left: -0.625rem; + border-bottom: 0; +} + +.card-header-pills { + margin-right: -0.625rem; + margin-left: -0.625rem; +} + +.card-img-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: 1.25rem; +} + +.card-img { + width: 100%; + border-radius: calc(0.25rem - 1px); +} + +.card-img-top { + width: 100%; + border-top-left-radius: calc(0.25rem - 1px); + border-top-right-radius: calc(0.25rem - 1px); +} + +.card-img-bottom { + width: 100%; + border-bottom-right-radius: calc(0.25rem - 1px); + border-bottom-left-radius: calc(0.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-img-top, + .card-group > .card:not(:last-child) .card-header { + border-top-right-radius: 0; + } + .card-group > .card:not(:last-child) .card-img-bottom, + .card-group > .card:not(:last-child) .card-footer { + 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-img-top, + .card-group > .card:not(:first-child) .card-header { + border-top-left-radius: 0; + } + .card-group > .card:not(:first-child) .card-img-bottom, + .card-group > .card:not(:first-child) .card-footer { + border-bottom-left-radius: 0; + } +} + +.card-columns .card { + margin-bottom: 0.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: 0.75rem 1rem; + margin-bottom: 1rem; + list-style: none; + background-color: #e9ecef; + border-radius: 0.25rem; +} + +.breadcrumb-item + .breadcrumb-item { + padding-left: 0.5rem; +} + +.breadcrumb-item + .breadcrumb-item::before { + display: inline-block; + padding-right: 0.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: 0.25rem; +} + +.page-link { + position: relative; + display: block; + padding: 0.5rem 0.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 0.2rem rgba(0, 123, 255, 0.25); +} + +.page-item:first-child .page-link { + margin-left: 0; + border-top-left-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.page-item:last-child .page-link { + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.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: 0.75rem 1.5rem; + font-size: 1.25rem; + line-height: 1.5; +} + +.pagination-lg .page-item:first-child .page-link { + border-top-left-radius: 0.3rem; + border-bottom-left-radius: 0.3rem; +} + +.pagination-lg .page-item:last-child .page-link { + border-top-right-radius: 0.3rem; + border-bottom-right-radius: 0.3rem; +} + +.pagination-sm .page-link { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; +} + +.pagination-sm .page-item:first-child .page-link { + border-top-left-radius: 0.2rem; + border-bottom-left-radius: 0.2rem; +} + +.pagination-sm .page-item:last-child .page-link { + border-top-right-radius: 0.2rem; + border-bottom-right-radius: 0.2rem; +} + +.badge { + display: inline-block; + padding: 0.25em 0.4em; + font-size: 75%; + font-weight: 700; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25rem; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +@media (prefers-reduced-motion: reduce) { + .badge { + transition: none; + } +} + +a.badge:hover, a.badge:focus { + text-decoration: none; +} + +.badge:empty { + display: none; +} + +.btn .badge { + position: relative; + top: -1px; +} + +.badge-pill { + padding-right: 0.6em; + padding-left: 0.6em; + border-radius: 10rem; +} + +.badge-primary { + color: #fff; + background-color: #007bff; +} + +a.badge-primary:hover, a.badge-primary:focus { + color: #fff; + background-color: #0062cc; +} + +a.badge-primary:focus, a.badge-primary.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); +} + +.badge-secondary { + color: #fff; + background-color: #6c757d; +} + +a.badge-secondary:hover, a.badge-secondary:focus { + color: #fff; + background-color: #545b62; +} + +a.badge-secondary:focus, a.badge-secondary.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); +} + +.badge-success { + color: #fff; + background-color: #28a745; +} + +a.badge-success:hover, a.badge-success:focus { + color: #fff; + background-color: #1e7e34; +} + +a.badge-success:focus, a.badge-success.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); +} + +.badge-info { + color: #fff; + background-color: #17a2b8; +} + +a.badge-info:hover, a.badge-info:focus { + color: #fff; + background-color: #117a8b; +} + +a.badge-info:focus, a.badge-info.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); +} + +.badge-warning { + color: #212529; + background-color: #ffc107; +} + +a.badge-warning:hover, a.badge-warning:focus { + color: #212529; + background-color: #d39e00; +} + +a.badge-warning:focus, a.badge-warning.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); +} + +.badge-danger { + color: #fff; + background-color: #dc3545; +} + +a.badge-danger:hover, a.badge-danger:focus { + color: #fff; + background-color: #bd2130; +} + +a.badge-danger:focus, a.badge-danger.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); +} + +.badge-light { + color: #212529; + background-color: #f8f9fa; +} + +a.badge-light:hover, a.badge-light:focus { + color: #212529; + background-color: #dae0e5; +} + +a.badge-light:focus, a.badge-light.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); +} + +.badge-dark { + color: #fff; + background-color: #343a40; +} + +a.badge-dark:hover, a.badge-dark:focus { + color: #fff; + background-color: #1d2124; +} + +a.badge-dark:focus, a.badge-dark.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); +} + +.jumbotron { + padding: 2rem 1rem; + margin-bottom: 2rem; + background-color: #e9ecef; + border-radius: 0.3rem; +} + +@media (min-width: 576px) { + .jumbotron { + padding: 4rem 2rem; + } +} + +.jumbotron-fluid { + padding-right: 0; + padding-left: 0; + border-radius: 0; +} + +.alert { + position: relative; + padding: 0.75rem 1.25rem; + margin-bottom: 1rem; + border: 1px solid transparent; + border-radius: 0.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: 0.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: 0.75rem; + background-color: #e9ecef; + border-radius: 0.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 0.6s ease; +} + +@media (prefers-reduced-motion: reduce) { + .progress-bar { + transition: none; + } +} + +.progress-bar-striped { + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.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:hover, .list-group-item-action:focus { + 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: 0.75rem 1.25rem; + margin-bottom: -1px; + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.125); +} + +.list-group-item:first-child { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.list-group-item:last-child { + margin-bottom: 0; + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.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: 0.25rem; + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; +} + +.list-group-horizontal .list-group-item:last-child { + margin-right: 0; + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.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: 0.25rem; + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-sm .list-group-item:last-child { + margin-right: 0; + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.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: 0.25rem; + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-md .list-group-item:last-child { + margin-right: 0; + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.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: 0.25rem; + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-lg .list-group-item:last-child { + margin-right: 0; + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.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: 0.25rem; + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-xl .list-group-item:last-child { + margin-right: 0; + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.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:hover, .list-group-item-primary.list-group-item-action:focus { + 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:hover, .list-group-item-secondary.list-group-item-action:focus { + 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:hover, .list-group-item-success.list-group-item-action:focus { + 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:hover, .list-group-item-info.list-group-item-action:focus { + 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:hover, .list-group-item-warning.list-group-item-action:focus { + 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:hover, .list-group-item-danger.list-group-item-action:focus { + 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:hover, .list-group-item-light.list-group-item-action:focus { + 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:hover, .list-group-item-dark.list-group-item-action:focus { + 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):hover, .close:not(:disabled):not(.disabled):focus { + 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: 0.875rem; + background-color: rgba(255, 255, 255, 0.85); + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.1); + box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + opacity: 0; + border-radius: 0.25rem; +} + +.toast:not(:last-child) { + margin-bottom: 0.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: 0.25rem 0.75rem; + color: #6c757d; + background-color: rgba(255, 255, 255, 0.85); + background-clip: padding-box; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +.toast-body { + padding: 0.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: 0.5rem; + pointer-events: none; +} + +.modal.fade .modal-dialog { + transition: -webkit-transform 0.3s ease-out; + transition: transform 0.3s ease-out; + transition: transform 0.3s ease-out, -webkit-transform 0.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-header, +.modal-dialog-scrollable .modal-footer { + -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, 0.2); + border-radius: 0.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: 0.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: 0.3rem; + border-top-right-radius: 0.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: 0.3rem; + border-bottom-left-radius: 0.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: 0.875rem; + word-wrap: break-word; + opacity: 0; +} + +.tooltip.show { + opacity: 0.9; +} + +.tooltip .arrow { + position: absolute; + display: block; + width: 0.8rem; + height: 0.4rem; +} + +.tooltip .arrow::before { + position: absolute; + content: ""; + border-color: transparent; + border-style: solid; +} + +.bs-tooltip-top, .bs-tooltip-auto[x-placement^="top"] { + padding: 0.4rem 0; +} + +.bs-tooltip-top .arrow, .bs-tooltip-auto[x-placement^="top"] .arrow { + bottom: 0; +} + +.bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^="top"] .arrow::before { + top: 0; + border-width: 0.4rem 0.4rem 0; + border-top-color: #000; +} + +.bs-tooltip-right, .bs-tooltip-auto[x-placement^="right"] { + padding: 0 0.4rem; +} + +.bs-tooltip-right .arrow, .bs-tooltip-auto[x-placement^="right"] .arrow { + left: 0; + width: 0.4rem; + height: 0.8rem; +} + +.bs-tooltip-right .arrow::before, .bs-tooltip-auto[x-placement^="right"] .arrow::before { + right: 0; + border-width: 0.4rem 0.4rem 0.4rem 0; + border-right-color: #000; +} + +.bs-tooltip-bottom, .bs-tooltip-auto[x-placement^="bottom"] { + padding: 0.4rem 0; +} + +.bs-tooltip-bottom .arrow, .bs-tooltip-auto[x-placement^="bottom"] .arrow { + top: 0; +} + +.bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^="bottom"] .arrow::before { + bottom: 0; + border-width: 0 0.4rem 0.4rem; + border-bottom-color: #000; +} + +.bs-tooltip-left, .bs-tooltip-auto[x-placement^="left"] { + padding: 0 0.4rem; +} + +.bs-tooltip-left .arrow, .bs-tooltip-auto[x-placement^="left"] .arrow { + right: 0; + width: 0.4rem; + height: 0.8rem; +} + +.bs-tooltip-left .arrow::before, .bs-tooltip-auto[x-placement^="left"] .arrow::before { + left: 0; + border-width: 0.4rem 0 0.4rem 0.4rem; + border-left-color: #000; +} + +.tooltip-inner { + max-width: 200px; + padding: 0.25rem 0.5rem; + color: #fff; + text-align: center; + background-color: #000; + border-radius: 0.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: 0.875rem; + word-wrap: break-word; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0.3rem; +} + +.popover .arrow { + position: absolute; + display: block; + width: 1rem; + height: 0.5rem; + margin: 0 0.3rem; +} + +.popover .arrow::before, .popover .arrow::after { + position: absolute; + display: block; + content: ""; + border-color: transparent; + border-style: solid; +} + +.bs-popover-top, .bs-popover-auto[x-placement^="top"] { + margin-bottom: 0.5rem; +} + +.bs-popover-top > .arrow, .bs-popover-auto[x-placement^="top"] > .arrow { + bottom: calc((0.5rem + 1px) * -1); +} + +.bs-popover-top > .arrow::before, .bs-popover-auto[x-placement^="top"] > .arrow::before { + bottom: 0; + border-width: 0.5rem 0.5rem 0; + border-top-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-top > .arrow::after, .bs-popover-auto[x-placement^="top"] > .arrow::after { + bottom: 1px; + border-width: 0.5rem 0.5rem 0; + border-top-color: #fff; +} + +.bs-popover-right, .bs-popover-auto[x-placement^="right"] { + margin-left: 0.5rem; +} + +.bs-popover-right > .arrow, .bs-popover-auto[x-placement^="right"] > .arrow { + left: calc((0.5rem + 1px) * -1); + width: 0.5rem; + height: 1rem; + margin: 0.3rem 0; +} + +.bs-popover-right > .arrow::before, .bs-popover-auto[x-placement^="right"] > .arrow::before { + left: 0; + border-width: 0.5rem 0.5rem 0.5rem 0; + border-right-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-right > .arrow::after, .bs-popover-auto[x-placement^="right"] > .arrow::after { + left: 1px; + border-width: 0.5rem 0.5rem 0.5rem 0; + border-right-color: #fff; +} + +.bs-popover-bottom, .bs-popover-auto[x-placement^="bottom"] { + margin-top: 0.5rem; +} + +.bs-popover-bottom > .arrow, .bs-popover-auto[x-placement^="bottom"] > .arrow { + top: calc((0.5rem + 1px) * -1); +} + +.bs-popover-bottom > .arrow::before, .bs-popover-auto[x-placement^="bottom"] > .arrow::before { + top: 0; + border-width: 0 0.5rem 0.5rem 0.5rem; + border-bottom-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-bottom > .arrow::after, .bs-popover-auto[x-placement^="bottom"] > .arrow::after { + top: 1px; + border-width: 0 0.5rem 0.5rem 0.5rem; + border-bottom-color: #fff; +} + +.bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^="bottom"] .popover-header::before { + position: absolute; + top: 0; + left: 50%; + display: block; + width: 1rem; + margin-left: -0.5rem; + content: ""; + border-bottom: 1px solid #f7f7f7; +} + +.bs-popover-left, .bs-popover-auto[x-placement^="left"] { + margin-right: 0.5rem; +} + +.bs-popover-left > .arrow, .bs-popover-auto[x-placement^="left"] > .arrow { + right: calc((0.5rem + 1px) * -1); + width: 0.5rem; + height: 1rem; + margin: 0.3rem 0; +} + +.bs-popover-left > .arrow::before, .bs-popover-auto[x-placement^="left"] > .arrow::before { + right: 0; + border-width: 0.5rem 0 0.5rem 0.5rem; + border-left-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-left > .arrow::after, .bs-popover-auto[x-placement^="left"] > .arrow::after { + right: 1px; + border-width: 0.5rem 0 0.5rem 0.5rem; + border-left-color: #fff; +} + +.popover-header { + padding: 0.5rem 0.75rem; + margin-bottom: 0; + font-size: 1rem; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-top-left-radius: calc(0.3rem - 1px); + border-top-right-radius: calc(0.3rem - 1px); +} + +.popover-header:empty { + display: none; +} + +.popover-body { + padding: 0.5rem 0.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 0.6s ease-in-out; + transition: transform 0.6s ease-in-out; + transition: transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out; +} + +@media (prefers-reduced-motion: reduce) { + .carousel-item { + transition: none; + } +} + +.carousel-item.active, +.carousel-item-next, +.carousel-item-prev { + display: block; +} + +.carousel-item-next:not(.carousel-item-left), +.active.carousel-item-right { + -webkit-transform: translateX(100%); + transform: translateX(100%); +} + +.carousel-item-prev:not(.carousel-item-right), +.active.carousel-item-left { + -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.active, +.carousel-fade .carousel-item-next.carousel-item-left, +.carousel-fade .carousel-item-prev.carousel-item-right { + z-index: 1; + opacity: 1; +} + +.carousel-fade .active.carousel-item-left, +.carousel-fade .active.carousel-item-right { + z-index: 0; + opacity: 0; + transition: 0s 0.6s opacity; +} + +@media (prefers-reduced-motion: reduce) { + .carousel-fade .active.carousel-item-left, + .carousel-fade .active.carousel-item-right { + transition: none; + } +} + +.carousel-control-prev, +.carousel-control-next { + 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: 0.5; + transition: opacity 0.15s ease; +} + +@media (prefers-reduced-motion: reduce) { + .carousel-control-prev, + .carousel-control-next { + transition: none; + } +} + +.carousel-control-prev:hover, .carousel-control-prev:focus, +.carousel-control-next:hover, +.carousel-control-next:focus { + color: #fff; + text-decoration: none; + outline: 0; + opacity: 0.9; +} + +.carousel-control-prev { + left: 0; +} + +.carousel-control-next { + right: 0; +} + +.carousel-control-prev-icon, +.carousel-control-next-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 0.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: 0.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: 0.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:hover, a.bg-primary:focus, +button.bg-primary:hover, +button.bg-primary:focus { + background-color: #0062cc !important; +} + +.bg-secondary { + background-color: #6c757d !important; +} + +a.bg-secondary:hover, a.bg-secondary:focus, +button.bg-secondary:hover, +button.bg-secondary:focus { + background-color: #545b62 !important; +} + +.bg-success { + background-color: #28a745 !important; +} + +a.bg-success:hover, a.bg-success:focus, +button.bg-success:hover, +button.bg-success:focus { + background-color: #1e7e34 !important; +} + +.bg-info { + background-color: #17a2b8 !important; +} + +a.bg-info:hover, a.bg-info:focus, +button.bg-info:hover, +button.bg-info:focus { + background-color: #117a8b !important; +} + +.bg-warning { + background-color: #ffc107 !important; +} + +a.bg-warning:hover, a.bg-warning:focus, +button.bg-warning:hover, +button.bg-warning:focus { + background-color: #d39e00 !important; +} + +.bg-danger { + background-color: #dc3545 !important; +} + +a.bg-danger:hover, a.bg-danger:focus, +button.bg-danger:hover, +button.bg-danger:focus { + background-color: #bd2130 !important; +} + +.bg-light { + background-color: #f8f9fa !important; +} + +a.bg-light:hover, a.bg-light:focus, +button.bg-light:hover, +button.bg-light:focus { + background-color: #dae0e5 !important; +} + +.bg-dark { + background-color: #343a40 !important; +} + +a.bg-dark:hover, a.bg-dark:focus, +button.bg-dark:hover, +button.bg-dark:focus { + 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: 0.2rem !important; +} + +.rounded { + border-radius: 0.25rem !important; +} + +.rounded-top { + border-top-left-radius: 0.25rem !important; + border-top-right-radius: 0.25rem !important; +} + +.rounded-right { + border-top-right-radius: 0.25rem !important; + border-bottom-right-radius: 0.25rem !important; +} + +.rounded-bottom { + border-bottom-right-radius: 0.25rem !important; + border-bottom-left-radius: 0.25rem !important; +} + +.rounded-left { + border-top-left-radius: 0.25rem !important; + border-bottom-left-radius: 0.25rem !important; +} + +.rounded-lg { + border-radius: 0.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 iframe, +.embed-responsive embed, +.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 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; +} + +.shadow { + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; +} + +.shadow-lg { + box-shadow: 0 1rem 3rem rgba(0, 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: 0.25rem !important; +} + +.mt-1, +.my-1 { + margin-top: 0.25rem !important; +} + +.mr-1, +.mx-1 { + margin-right: 0.25rem !important; +} + +.mb-1, +.my-1 { + margin-bottom: 0.25rem !important; +} + +.ml-1, +.mx-1 { + margin-left: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.mt-2, +.my-2 { + margin-top: 0.5rem !important; +} + +.mr-2, +.mx-2 { + margin-right: 0.5rem !important; +} + +.mb-2, +.my-2 { + margin-bottom: 0.5rem !important; +} + +.ml-2, +.mx-2 { + margin-left: 0.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: 0.25rem !important; +} + +.pt-1, +.py-1 { + padding-top: 0.25rem !important; +} + +.pr-1, +.px-1 { + padding-right: 0.25rem !important; +} + +.pb-1, +.py-1 { + padding-bottom: 0.25rem !important; +} + +.pl-1, +.px-1 { + padding-left: 0.25rem !important; +} + +.p-2 { + padding: 0.5rem !important; +} + +.pt-2, +.py-2 { + padding-top: 0.5rem !important; +} + +.pr-2, +.px-2 { + padding-right: 0.5rem !important; +} + +.pb-2, +.py-2 { + padding-bottom: 0.5rem !important; +} + +.pl-2, +.px-2 { + padding-left: 0.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: -0.25rem !important; +} + +.mt-n1, +.my-n1 { + margin-top: -0.25rem !important; +} + +.mr-n1, +.mx-n1 { + margin-right: -0.25rem !important; +} + +.mb-n1, +.my-n1 { + margin-bottom: -0.25rem !important; +} + +.ml-n1, +.mx-n1 { + margin-left: -0.25rem !important; +} + +.m-n2 { + margin: -0.5rem !important; +} + +.mt-n2, +.my-n2 { + margin-top: -0.5rem !important; +} + +.mr-n2, +.mx-n2 { + margin-right: -0.5rem !important; +} + +.mb-n2, +.my-n2 { + margin-bottom: -0.5rem !important; +} + +.ml-n2, +.mx-n2 { + margin-left: -0.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: 0.25rem !important; + } + .mt-sm-1, + .my-sm-1 { + margin-top: 0.25rem !important; + } + .mr-sm-1, + .mx-sm-1 { + margin-right: 0.25rem !important; + } + .mb-sm-1, + .my-sm-1 { + margin-bottom: 0.25rem !important; + } + .ml-sm-1, + .mx-sm-1 { + margin-left: 0.25rem !important; + } + .m-sm-2 { + margin: 0.5rem !important; + } + .mt-sm-2, + .my-sm-2 { + margin-top: 0.5rem !important; + } + .mr-sm-2, + .mx-sm-2 { + margin-right: 0.5rem !important; + } + .mb-sm-2, + .my-sm-2 { + margin-bottom: 0.5rem !important; + } + .ml-sm-2, + .mx-sm-2 { + margin-left: 0.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: 0.25rem !important; + } + .pt-sm-1, + .py-sm-1 { + padding-top: 0.25rem !important; + } + .pr-sm-1, + .px-sm-1 { + padding-right: 0.25rem !important; + } + .pb-sm-1, + .py-sm-1 { + padding-bottom: 0.25rem !important; + } + .pl-sm-1, + .px-sm-1 { + padding-left: 0.25rem !important; + } + .p-sm-2 { + padding: 0.5rem !important; + } + .pt-sm-2, + .py-sm-2 { + padding-top: 0.5rem !important; + } + .pr-sm-2, + .px-sm-2 { + padding-right: 0.5rem !important; + } + .pb-sm-2, + .py-sm-2 { + padding-bottom: 0.5rem !important; + } + .pl-sm-2, + .px-sm-2 { + padding-left: 0.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: -0.25rem !important; + } + .mt-sm-n1, + .my-sm-n1 { + margin-top: -0.25rem !important; + } + .mr-sm-n1, + .mx-sm-n1 { + margin-right: -0.25rem !important; + } + .mb-sm-n1, + .my-sm-n1 { + margin-bottom: -0.25rem !important; + } + .ml-sm-n1, + .mx-sm-n1 { + margin-left: -0.25rem !important; + } + .m-sm-n2 { + margin: -0.5rem !important; + } + .mt-sm-n2, + .my-sm-n2 { + margin-top: -0.5rem !important; + } + .mr-sm-n2, + .mx-sm-n2 { + margin-right: -0.5rem !important; + } + .mb-sm-n2, + .my-sm-n2 { + margin-bottom: -0.5rem !important; + } + .ml-sm-n2, + .mx-sm-n2 { + margin-left: -0.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: 0.25rem !important; + } + .mt-md-1, + .my-md-1 { + margin-top: 0.25rem !important; + } + .mr-md-1, + .mx-md-1 { + margin-right: 0.25rem !important; + } + .mb-md-1, + .my-md-1 { + margin-bottom: 0.25rem !important; + } + .ml-md-1, + .mx-md-1 { + margin-left: 0.25rem !important; + } + .m-md-2 { + margin: 0.5rem !important; + } + .mt-md-2, + .my-md-2 { + margin-top: 0.5rem !important; + } + .mr-md-2, + .mx-md-2 { + margin-right: 0.5rem !important; + } + .mb-md-2, + .my-md-2 { + margin-bottom: 0.5rem !important; + } + .ml-md-2, + .mx-md-2 { + margin-left: 0.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: 0.25rem !important; + } + .pt-md-1, + .py-md-1 { + padding-top: 0.25rem !important; + } + .pr-md-1, + .px-md-1 { + padding-right: 0.25rem !important; + } + .pb-md-1, + .py-md-1 { + padding-bottom: 0.25rem !important; + } + .pl-md-1, + .px-md-1 { + padding-left: 0.25rem !important; + } + .p-md-2 { + padding: 0.5rem !important; + } + .pt-md-2, + .py-md-2 { + padding-top: 0.5rem !important; + } + .pr-md-2, + .px-md-2 { + padding-right: 0.5rem !important; + } + .pb-md-2, + .py-md-2 { + padding-bottom: 0.5rem !important; + } + .pl-md-2, + .px-md-2 { + padding-left: 0.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: -0.25rem !important; + } + .mt-md-n1, + .my-md-n1 { + margin-top: -0.25rem !important; + } + .mr-md-n1, + .mx-md-n1 { + margin-right: -0.25rem !important; + } + .mb-md-n1, + .my-md-n1 { + margin-bottom: -0.25rem !important; + } + .ml-md-n1, + .mx-md-n1 { + margin-left: -0.25rem !important; + } + .m-md-n2 { + margin: -0.5rem !important; + } + .mt-md-n2, + .my-md-n2 { + margin-top: -0.5rem !important; + } + .mr-md-n2, + .mx-md-n2 { + margin-right: -0.5rem !important; + } + .mb-md-n2, + .my-md-n2 { + margin-bottom: -0.5rem !important; + } + .ml-md-n2, + .mx-md-n2 { + margin-left: -0.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: 0.25rem !important; + } + .mt-lg-1, + .my-lg-1 { + margin-top: 0.25rem !important; + } + .mr-lg-1, + .mx-lg-1 { + margin-right: 0.25rem !important; + } + .mb-lg-1, + .my-lg-1 { + margin-bottom: 0.25rem !important; + } + .ml-lg-1, + .mx-lg-1 { + margin-left: 0.25rem !important; + } + .m-lg-2 { + margin: 0.5rem !important; + } + .mt-lg-2, + .my-lg-2 { + margin-top: 0.5rem !important; + } + .mr-lg-2, + .mx-lg-2 { + margin-right: 0.5rem !important; + } + .mb-lg-2, + .my-lg-2 { + margin-bottom: 0.5rem !important; + } + .ml-lg-2, + .mx-lg-2 { + margin-left: 0.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: 0.25rem !important; + } + .pt-lg-1, + .py-lg-1 { + padding-top: 0.25rem !important; + } + .pr-lg-1, + .px-lg-1 { + padding-right: 0.25rem !important; + } + .pb-lg-1, + .py-lg-1 { + padding-bottom: 0.25rem !important; + } + .pl-lg-1, + .px-lg-1 { + padding-left: 0.25rem !important; + } + .p-lg-2 { + padding: 0.5rem !important; + } + .pt-lg-2, + .py-lg-2 { + padding-top: 0.5rem !important; + } + .pr-lg-2, + .px-lg-2 { + padding-right: 0.5rem !important; + } + .pb-lg-2, + .py-lg-2 { + padding-bottom: 0.5rem !important; + } + .pl-lg-2, + .px-lg-2 { + padding-left: 0.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: -0.25rem !important; + } + .mt-lg-n1, + .my-lg-n1 { + margin-top: -0.25rem !important; + } + .mr-lg-n1, + .mx-lg-n1 { + margin-right: -0.25rem !important; + } + .mb-lg-n1, + .my-lg-n1 { + margin-bottom: -0.25rem !important; + } + .ml-lg-n1, + .mx-lg-n1 { + margin-left: -0.25rem !important; + } + .m-lg-n2 { + margin: -0.5rem !important; + } + .mt-lg-n2, + .my-lg-n2 { + margin-top: -0.5rem !important; + } + .mr-lg-n2, + .mx-lg-n2 { + margin-right: -0.5rem !important; + } + .mb-lg-n2, + .my-lg-n2 { + margin-bottom: -0.5rem !important; + } + .ml-lg-n2, + .mx-lg-n2 { + margin-left: -0.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: 0.25rem !important; + } + .mt-xl-1, + .my-xl-1 { + margin-top: 0.25rem !important; + } + .mr-xl-1, + .mx-xl-1 { + margin-right: 0.25rem !important; + } + .mb-xl-1, + .my-xl-1 { + margin-bottom: 0.25rem !important; + } + .ml-xl-1, + .mx-xl-1 { + margin-left: 0.25rem !important; + } + .m-xl-2 { + margin: 0.5rem !important; + } + .mt-xl-2, + .my-xl-2 { + margin-top: 0.5rem !important; + } + .mr-xl-2, + .mx-xl-2 { + margin-right: 0.5rem !important; + } + .mb-xl-2, + .my-xl-2 { + margin-bottom: 0.5rem !important; + } + .ml-xl-2, + .mx-xl-2 { + margin-left: 0.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: 0.25rem !important; + } + .pt-xl-1, + .py-xl-1 { + padding-top: 0.25rem !important; + } + .pr-xl-1, + .px-xl-1 { + padding-right: 0.25rem !important; + } + .pb-xl-1, + .py-xl-1 { + padding-bottom: 0.25rem !important; + } + .pl-xl-1, + .px-xl-1 { + padding-left: 0.25rem !important; + } + .p-xl-2 { + padding: 0.5rem !important; + } + .pt-xl-2, + .py-xl-2 { + padding-top: 0.5rem !important; + } + .pr-xl-2, + .px-xl-2 { + padding-right: 0.5rem !important; + } + .pb-xl-2, + .py-xl-2 { + padding-bottom: 0.5rem !important; + } + .pl-xl-2, + .px-xl-2 { + padding-left: 0.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: -0.25rem !important; + } + .mt-xl-n1, + .my-xl-n1 { + margin-top: -0.25rem !important; + } + .mr-xl-n1, + .mx-xl-n1 { + margin-right: -0.25rem !important; + } + .mb-xl-n1, + .my-xl-n1 { + margin-bottom: -0.25rem !important; + } + .ml-xl-n1, + .mx-xl-n1 { + margin-left: -0.25rem !important; + } + .m-xl-n2 { + margin: -0.5rem !important; + } + .mt-xl-n2, + .my-xl-n2 { + margin-top: -0.5rem !important; + } + .mr-xl-n2, + .mx-xl-n2 { + margin-right: -0.5rem !important; + } + .mb-xl-n2, + .my-xl-n2 { + margin-bottom: -0.5rem !important; + } + .ml-xl-n2, + .mx-xl-n2 { + margin-left: -0.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:hover, a.text-primary:focus { + color: #0056b3 !important; +} + +.text-secondary { + color: #6c757d !important; +} + +a.text-secondary:hover, a.text-secondary:focus { + color: #494f54 !important; +} + +.text-success { + color: #28a745 !important; +} + +a.text-success:hover, a.text-success:focus { + color: #19692c !important; +} + +.text-info { + color: #17a2b8 !important; +} + +a.text-info:hover, a.text-info:focus { + color: #0f6674 !important; +} + +.text-warning { + color: #ffc107 !important; +} + +a.text-warning:hover, a.text-warning:focus { + color: #ba8b00 !important; +} + +.text-danger { + color: #dc3545 !important; +} + +a.text-danger:hover, a.text-danger:focus { + color: #a71d2a !important; +} + +.text-light { + color: #f8f9fa !important; +} + +a.text-light:hover, a.text-light:focus { + color: #cbd3da !important; +} + +.text-dark { + color: #343a40 !important; +} + +a.text-dark:hover, a.text-dark:focus { + color: #121416 !important; +} + +.text-body { + color: #212529 !important; +} + +.text-muted { + color: #6c757d !important; +} + +.text-black-50 { + color: rgba(0, 0, 0, 0.5) !important; +} + +.text-white-50 { + color: rgba(255, 255, 255, 0.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 { + *, + *::before, + *::after { + 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; + } + pre, + blockquote { + border: 1px solid #adb5bd; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + p, + h2, + h3 { + 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 th, + .table-bordered td { + border: 1px solid #dee2e6 !important; + } + .table-dark { + color: inherit; + } + .table-dark th, + .table-dark td, + .table-dark thead th, + .table-dark tbody + tbody { + border-color: #dee2e6; + } + .table .thead-dark th { + color: inherit; + border-color: #dee2e6; + } +} +/*# sourceMappingURL=bootstrap.css.map */ \ No newline at end of file diff --git a/BPA.AspNetCore.Test/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css b/BPA.AspNetCore.Test/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css new file mode 100644 index 0000000..92e3fe8 --- /dev/null +++ b/BPA.AspNetCore.Test/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css @@ -0,0 +1,7 @@ +/*! + * 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/BPA.AspNetCore.Test/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt b/BPA.AspNetCore.Test/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt new file mode 100644 index 0000000..0bdc196 --- /dev/null +++ b/BPA.AspNetCore.Test/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt @@ -0,0 +1,12 @@ +Copyright (c) .NET Foundation. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +these files except in compliance with the License. You may obtain a copy of the +License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. diff --git a/BPA.AspNetCore.Test/wwwroot/lib/jquery-validation/LICENSE.md b/BPA.AspNetCore.Test/wwwroot/lib/jquery-validation/LICENSE.md new file mode 100644 index 0000000..dc377cc --- /dev/null +++ b/BPA.AspNetCore.Test/wwwroot/lib/jquery-validation/LICENSE.md @@ -0,0 +1,22 @@ +The MIT License (MIT) +===================== + +Copyright Jörn Zaefferer + +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. diff --git a/BPA.AspNetCore.Test/wwwroot/lib/jquery/LICENSE.txt b/BPA.AspNetCore.Test/wwwroot/lib/jquery/LICENSE.txt new file mode 100644 index 0000000..e4e5e00 --- /dev/null +++ b/BPA.AspNetCore.Test/wwwroot/lib/jquery/LICENSE.txt @@ -0,0 +1,36 @@ +Copyright JS Foundation and other contributors, https://js.foundation/ + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/jquery/jquery + +The following license applies to all parts of this software except as +documented below: + +==== + +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. + +==== + +All files located in the node_modules and external directories are +externally maintained libraries used by this software which have their +own licenses; we recommend you read them, as their terms may differ from +the terms above. diff --git a/MQTTnet.sln b/MQTTnet.sln index 34abf62..3da944e 100644 --- a/MQTTnet.sln +++ b/MQTTnet.sln @@ -1,15 +1,15 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31919.166 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.32228.343 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet", "Source\MQTTnet\MQTTnet.csproj", "{3587E506-55A2-4EB3-99C7-DC01E42D25D2}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B3F60ECB-45BA-4C66-8903-8BB89CA67998}" ProjectSection(SolutionItems) = preProject + CODE-OF-CONDUCT.md = CODE-OF-CONDUCT.md LICENSE = LICENSE README.md = README.md - CODE-OF-CONDUCT.md = CODE-OF-CONDUCT.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspNetCore", "Source\MQTTnet.AspnetCore\MQTTnet.AspNetCore.csproj", "{F10C4060-F7EE-4A83-919F-FF723E72F94A}" @@ -20,17 +20,25 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.ManagedC EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.WebSocket4Net", "Source\MQTTnet.Extensions.WebSocket4Net\MQTTnet.Extensions.WebSocket4Net.csproj", "{2BD01D53-4CA5-4142-BE8D-313876395E3E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.Samples", "Samples\MQTTnet.Samples.csproj", "{71CF35F5-3327-4A91-AAF4-5340F6701771}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Samples", "Samples\MQTTnet.Samples.csproj", "{71CF35F5-3327-4A91-AAF4-5340F6701771}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Tests", "Source\MQTTnet.Tests\MQTTnet.Tests.csproj", "{B270F32A-9F3E-42EE-A989-813E35E29ADB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspNetCore.Tests", "Source\MQTTnet.AspNetCore.Tests\MQTTnet.AspNetCore.Tests.csproj", "{A238BBBF-C75F-482D-9CC3-BB34ABA9B675}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Benchmarks", "Source\MQTTnet.Benchmarks\MQTTnet.Benchmarks.csproj", "{2F516E76-AAC4-4219-B7D1-34CDD3CFF381}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.TestApp", "Source\MQTTnet.TestApp\MQTTnet.TestApp.csproj", "{175D5340-CC5B-4542-939D-4E7D15A0BC8D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.Tests", "Source\MQTTnet.Tests\MQTTnet.Tests.csproj", "{B270F32A-9F3E-42EE-A989-813E35E29ADB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.AspTestApp", "Source\MQTTnet.AspTestApp\MQTTnet.AspTestApp.csproj", "{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.AspNetCore.Tests", "Source\MQTTnet.AspNetCore.Tests\MQTTnet.AspNetCore.Tests.csproj", "{A238BBBF-C75F-482D-9CC3-BB34ABA9B675}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "bpa", "bpa", "{F7F3B037-B2AC-426E-BB9E-F41BA1F50C91}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.Benchmarks", "Source\MQTTnet.Benchmarks\MQTTnet.Benchmarks.csproj", "{2F516E76-AAC4-4219-B7D1-34CDD3CFF381}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BPA.MQTTnet", "Source\BPA.MQTTnet\BPA.MQTTnet.csproj", "{CD8AFE00-C885-4617-AB70-720B8BBABFCD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.TestApp", "Source\MQTTnet.TestApp\MQTTnet.TestApp.csproj", "{175D5340-CC5B-4542-939D-4E7D15A0BC8D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BPA.MQTTnet.AspNetCore", "Source\BPA.MQTTnet.AspnetCore\BPA.MQTTnet.AspNetCore.csproj", "{962FE385-013D-496B-9C0E-6800F8906B8E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.AspTestApp", "Source\MQTTnet.AspTestApp\MQTTnet.AspTestApp.csproj", "{72867E4C-4E15-4E8E-8FAB-AE9253286BBC}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BAP.MQTTnet.Test", "BAP.MQTTnet.Test\BAP.MQTTnet.Test.csproj", "{BA264BFA-A33C-4278-AB2E-B3184B79D620}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -82,11 +90,26 @@ Global {72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Debug|Any CPU.Build.0 = Debug|Any CPU {72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Release|Any CPU.ActiveCfg = Release|Any CPU {72867E4C-4E15-4E8E-8FAB-AE9253286BBC}.Release|Any CPU.Build.0 = Release|Any CPU + {CD8AFE00-C885-4617-AB70-720B8BBABFCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD8AFE00-C885-4617-AB70-720B8BBABFCD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD8AFE00-C885-4617-AB70-720B8BBABFCD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD8AFE00-C885-4617-AB70-720B8BBABFCD}.Release|Any CPU.Build.0 = Release|Any CPU + {962FE385-013D-496B-9C0E-6800F8906B8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {962FE385-013D-496B-9C0E-6800F8906B8E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {962FE385-013D-496B-9C0E-6800F8906B8E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {962FE385-013D-496B-9C0E-6800F8906B8E}.Release|Any CPU.Build.0 = Release|Any CPU + {BA264BFA-A33C-4278-AB2E-B3184B79D620}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BA264BFA-A33C-4278-AB2E-B3184B79D620}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BA264BFA-A33C-4278-AB2E-B3184B79D620}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BA264BFA-A33C-4278-AB2E-B3184B79D620}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution + {CD8AFE00-C885-4617-AB70-720B8BBABFCD} = {F7F3B037-B2AC-426E-BB9E-F41BA1F50C91} + {962FE385-013D-496B-9C0E-6800F8906B8E} = {F7F3B037-B2AC-426E-BB9E-F41BA1F50C91} + {BA264BFA-A33C-4278-AB2E-B3184B79D620} = {F7F3B037-B2AC-426E-BB9E-F41BA1F50C91} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {07536672-5CBC-4BE3-ACE0-708A431A7894} diff --git a/Samples/Client/Client_Subscribe_Samples.cs b/Samples/Client/Client_Subscribe_Samples.cs index 7cedf05..76f8e7d 100644 --- a/Samples/Client/Client_Subscribe_Samples.cs +++ b/Samples/Client/Client_Subscribe_Samples.cs @@ -5,7 +5,7 @@ using MQTTnet.Client; using MQTTnet.Samples.Helpers; -namespace MQTTnet.Samples.Client; +namespace MQTTnet.Samples.Client { public static class Client_Subscribe_Samples { @@ -77,4 +77,5 @@ public static class Client_Subscribe_Samples response.DumpToConsole(); } } + } } \ No newline at end of file diff --git a/Source/BPA.MQTTnet.AspnetCore/ApplicationBuilderExtensions.cs b/Source/BPA.MQTTnet.AspnetCore/ApplicationBuilderExtensions.cs new file mode 100644 index 0000000..d7bc3e3 --- /dev/null +++ b/Source/BPA.MQTTnet.AspnetCore/ApplicationBuilderExtensions.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; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using MQTTnet.Server; + +namespace MQTTnet.AspNetCore +{ + public static class ApplicationBuilderExtensions + { + [Obsolete("This class is obsolete and will be removed in a future version. The recommended alternative is to use MapMqtt inside Microsoft.AspNetCore.Builder.UseEndpoints(...).")] + public static IApplicationBuilder UseMqttEndpoint(this IApplicationBuilder app, string path = "/mqtt") + { + app.UseWebSockets(); + app.Use(async (context, next) => + { + if (!context.WebSockets.IsWebSocketRequest || context.Request.Path != path) + { + await next(); + return; + } + + string subProtocol = null; + + if (context.Request.Headers.TryGetValue("Sec-WebSocket-Protocol", out var requestedSubProtocolValues)) + { + subProtocol = MqttSubProtocolSelector.SelectSubProtocol(requestedSubProtocolValues); + } + + var adapter = app.ApplicationServices.GetRequiredService(); + using (var webSocket = await context.WebSockets.AcceptWebSocketAsync(subProtocol).ConfigureAwait(false)) + { + await adapter.RunWebSocketConnectionAsync(webSocket, context); + } + }); + + return app; + } + + public static IApplicationBuilder UseMqttServer(this IApplicationBuilder app, Action configure) + { + var server = app.ApplicationServices.GetRequiredService(); + + configure(server); + + return app; + } + } +} diff --git a/Source/BPA.MQTTnet.AspnetCore/AspNetMqttServerOptionsBuilder.cs b/Source/BPA.MQTTnet.AspnetCore/AspNetMqttServerOptionsBuilder.cs new file mode 100644 index 0000000..a95e001 --- /dev/null +++ b/Source/BPA.MQTTnet.AspnetCore/AspNetMqttServerOptionsBuilder.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 MQTTnet.Server; +using System; + +namespace MQTTnet.AspNetCore +{ + public sealed class AspNetMqttServerOptionsBuilder : MqttServerOptionsBuilder + { + public AspNetMqttServerOptionsBuilder(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + } + + public IServiceProvider ServiceProvider { get; } + } +} diff --git a/Source/BPA.MQTTnet.AspnetCore/BPA - Backup (1).MQTTnet.AspNetCore.csproj b/Source/BPA.MQTTnet.AspnetCore/BPA - Backup (1).MQTTnet.AspNetCore.csproj new file mode 100644 index 0000000..5f127b7 --- /dev/null +++ b/Source/BPA.MQTTnet.AspnetCore/BPA - Backup (1).MQTTnet.AspNetCore.csproj @@ -0,0 +1,61 @@ + + + netstandard2.0;netcoreapp2.1;netcoreapp3.1;net5.0;net6.0 + MQTTnet.AspNetCore + MQTTnet.AspNetCore + false + The contributors of MQTTnet + BPA + BPA.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 + BPA.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 + 1.0.1 + + + + True + \ + + + + + True + \ + + + + + + + RELEASE;NETSTANDARD2_0 + + + + + + + + + + + \ No newline at end of file diff --git a/Source/BPA.MQTTnet.AspnetCore/BPA - Backup.MQTTnet.AspNetCore.csproj b/Source/BPA.MQTTnet.AspnetCore/BPA - Backup.MQTTnet.AspNetCore.csproj new file mode 100644 index 0000000..95eba34 --- /dev/null +++ b/Source/BPA.MQTTnet.AspnetCore/BPA - Backup.MQTTnet.AspNetCore.csproj @@ -0,0 +1,61 @@ + + + netstandard2.0;netcoreapp2.1;netcoreapp3.1;net5.0;net6.0 + BPA.MQTTnet.AspNetCore + BPA.MQTTnet.AspNetCore + True + The contributors of MQTTnet + BPA.MQTTnet + BPA.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 + BPA.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 + 1.0.3 + + + + True + \ + + + + + True + \ + + + + + + + RELEASE;NETSTANDARD2_0 + + + + + + + + + + + \ No newline at end of file diff --git a/Source/BPA.MQTTnet.AspnetCore/BPA.MQTTnet.AspNetCore.csproj b/Source/BPA.MQTTnet.AspnetCore/BPA.MQTTnet.AspNetCore.csproj new file mode 100644 index 0000000..6b27ec9 --- /dev/null +++ b/Source/BPA.MQTTnet.AspnetCore/BPA.MQTTnet.AspNetCore.csproj @@ -0,0 +1,75 @@ + + + + netstandard2.0;netcoreapp2.1;netcoreapp3.1;net5.0;net6.0 + + MQTTnet.AspNetCore + MQTTnet.AspNetCore + false + The contributors of MQTTnet + BPA + BPA.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 + BPA.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 + \ + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + RELEASE;NETSTANDARD2_0 + + + + + + + + + + + + + + + diff --git a/Source/BPA.MQTTnet.AspnetCore/Client/MqttClientConnectionContextFactory.cs b/Source/BPA.MQTTnet.AspnetCore/Client/MqttClientConnectionContextFactory.cs new file mode 100644 index 0000000..f8e38f4 --- /dev/null +++ b/Source/BPA.MQTTnet.AspnetCore/Client/MqttClientConnectionContextFactory.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using MQTTnet.Adapter; +using MQTTnet.AspNetCore.Client.Tcp; +using MQTTnet.Formatter; +using System; +using System.Net; +using MQTTnet.Client; +using MQTTnet.Diagnostics; + +namespace MQTTnet.AspNetCore.Client +{ + public sealed class MqttClientConnectionContextFactory : IMqttClientAdapterFactory + { + public IMqttChannelAdapter CreateClientAdapter(MqttClientOptions options, MqttPacketInspector packetInspector, IMqttNetLogger logger) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + + switch (options.ChannelOptions) + { + case MqttClientTcpOptions tcpOptions: + { + var endpoint = new DnsEndPoint(tcpOptions.Server, tcpOptions.GetPort()); + var tcpConnection = new TcpConnection(endpoint); + + var formatter = new MqttPacketFormatterAdapter(options.ProtocolVersion, new MqttBufferWriter(4096, 65535)); + return new MqttConnectionContext(formatter, tcpConnection); + } + default: + { + throw new NotSupportedException(); + } + } + } + } +} diff --git a/Source/BPA.MQTTnet.AspnetCore/Client/Tcp/BufferExtensions.cs b/Source/BPA.MQTTnet.AspnetCore/Client/Tcp/BufferExtensions.cs new file mode 100644 index 0000000..6273659 --- /dev/null +++ b/Source/BPA.MQTTnet.AspnetCore/Client/Tcp/BufferExtensions.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace MQTTnet.AspNetCore.Client.Tcp +{ + public static class BufferExtensions + { + public static ArraySegment GetArray(this Memory memory) + { + return ((ReadOnlyMemory)memory).GetArray(); + } + + public static ArraySegment GetArray(this ReadOnlyMemory memory) + { + if (!MemoryMarshal.TryGetArray(memory, out var result)) + { + throw new InvalidOperationException("Buffer backed by array was expected"); + } + return result; + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet.AspnetCore/Client/Tcp/DuplexPipe.cs b/Source/BPA.MQTTnet.AspnetCore/Client/Tcp/DuplexPipe.cs new file mode 100644 index 0000000..84bbf99 --- /dev/null +++ b/Source/BPA.MQTTnet.AspnetCore/Client/Tcp/DuplexPipe.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO.Pipelines; + +namespace MQTTnet.AspNetCore.Client.Tcp +{ + public class DuplexPipe : IDuplexPipe + { + public DuplexPipe(PipeReader reader, PipeWriter writer) + { + Input = reader; + Output = writer; + } + + public PipeReader Input { get; } + + public PipeWriter Output { get; } + + public static DuplexPipePair CreateConnectionPair(PipeOptions inputOptions, PipeOptions outputOptions) + { + var input = new Pipe(inputOptions); + var output = new Pipe(outputOptions); + + var transportToApplication = new DuplexPipe(output.Reader, input.Writer); + var applicationToTransport = new DuplexPipe(input.Reader, output.Writer); + + return new DuplexPipePair(applicationToTransport, transportToApplication); + } + + // This class exists to work around issues with value tuple on .NET Framework + public readonly struct DuplexPipePair + { + public IDuplexPipe Transport { get; } + public IDuplexPipe Application { get; } + + public DuplexPipePair(IDuplexPipe transport, IDuplexPipe application) + { + Transport = transport; + Application = application; + } + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet.AspnetCore/Client/Tcp/SocketAwaitable.cs b/Source/BPA.MQTTnet.AspnetCore/Client/Tcp/SocketAwaitable.cs new file mode 100644 index 0000000..6a53a7a --- /dev/null +++ b/Source/BPA.MQTTnet.AspnetCore/Client/Tcp/SocketAwaitable.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace MQTTnet.AspNetCore.Client.Tcp +{ + public class SocketAwaitable : ICriticalNotifyCompletion + { + private static readonly Action _callbackCompleted = () => { }; + + private readonly PipeScheduler _ioScheduler; + + private Action _callback; + private int _bytesTransferred; + private SocketError _error; + + public SocketAwaitable(PipeScheduler ioScheduler) + { + _ioScheduler = ioScheduler; + } + + public bool IsCompleted => ReferenceEquals(_callback, _callbackCompleted); + + public SocketAwaitable GetAwaiter() => this; + + public int GetResult() + { + Debug.Assert(ReferenceEquals(_callback, _callbackCompleted)); + + _callback = null; + + if (_error != SocketError.Success) + { + throw new SocketException((int)_error); + } + + return _bytesTransferred; + } + + public void OnCompleted(Action continuation) + { + if (ReferenceEquals(_callback, _callbackCompleted) || + ReferenceEquals(Interlocked.CompareExchange(ref _callback, continuation, null), _callbackCompleted)) + { + Task.Run(continuation); + } + } + + public void UnsafeOnCompleted(Action continuation) + { + OnCompleted(continuation); + } + + public void Complete(int bytesTransferred, SocketError socketError) + { + _error = socketError; + _bytesTransferred = bytesTransferred; + var continuation = Interlocked.Exchange(ref _callback, _callbackCompleted); + + if (continuation != null) + { + _ioScheduler.Schedule(state => ((Action)state)(), continuation); + } + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet.AspnetCore/Client/Tcp/SocketReceiver.cs b/Source/BPA.MQTTnet.AspnetCore/Client/Tcp/SocketReceiver.cs new file mode 100644 index 0000000..a10708e --- /dev/null +++ b/Source/BPA.MQTTnet.AspnetCore/Client/Tcp/SocketReceiver.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 System.IO.Pipelines; +using System.Net.Sockets; + +namespace MQTTnet.AspNetCore.Client.Tcp +{ + public class SocketReceiver + { + private readonly Socket _socket; + private readonly SocketAsyncEventArgs _eventArgs = new SocketAsyncEventArgs(); + private readonly SocketAwaitable _awaitable; + + public SocketReceiver(Socket socket, PipeScheduler scheduler) + { + _socket = socket; + _awaitable = new SocketAwaitable(scheduler); + _eventArgs.UserToken = _awaitable; + _eventArgs.Completed += (_, e) => ((SocketAwaitable)e.UserToken).Complete(e.BytesTransferred, e.SocketError); + } + + public SocketAwaitable ReceiveAsync(Memory buffer) + { +#if NETCOREAPP2_1 + _eventArgs.SetBuffer(buffer); +#else + var segment = buffer.GetArray(); + _eventArgs.SetBuffer(segment.Array, segment.Offset, segment.Count); +#endif + if (!_socket.ReceiveAsync(_eventArgs)) + { + _awaitable.Complete(_eventArgs.BytesTransferred, _eventArgs.SocketError); + } + + return _awaitable; + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet.AspnetCore/Client/Tcp/SocketSender.cs b/Source/BPA.MQTTnet.AspnetCore/Client/Tcp/SocketSender.cs new file mode 100644 index 0000000..d4a5dcc --- /dev/null +++ b/Source/BPA.MQTTnet.AspnetCore/Client/Tcp/SocketSender.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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; +using System.IO.Pipelines; +using System.Net.Sockets; + +#if NETCOREAPP2_1 +using System.Runtime.InteropServices; +#endif + +namespace MQTTnet.AspNetCore.Client.Tcp +{ + public class SocketSender + { + private readonly Socket _socket; + private readonly SocketAsyncEventArgs _eventArgs = new SocketAsyncEventArgs(); + private readonly SocketAwaitable _awaitable; + + private List> _bufferList; + + public SocketSender(Socket socket, PipeScheduler scheduler) + { + _socket = socket; + _awaitable = new SocketAwaitable(scheduler); + _eventArgs.UserToken = _awaitable; + _eventArgs.Completed += (_, e) => ((SocketAwaitable)e.UserToken).Complete(e.BytesTransferred, e.SocketError); + } + + public SocketAwaitable SendAsync(in ReadOnlySequence buffers) + { + if (buffers.IsSingleSegment) + { + return SendAsync(buffers.First); + } + +#if NETCOREAPP2_1 + if (!_eventArgs.MemoryBuffer.Equals(Memory.Empty)) +#else + if (_eventArgs.Buffer != null) +#endif + { + _eventArgs.SetBuffer(null, 0, 0); + } + + _eventArgs.BufferList = GetBufferList(buffers); + + if (!_socket.SendAsync(_eventArgs)) + { + _awaitable.Complete(_eventArgs.BytesTransferred, _eventArgs.SocketError); + } + + return _awaitable; + } + + private SocketAwaitable SendAsync(ReadOnlyMemory memory) + { + // The BufferList getter is much less expensive then the setter. + if (_eventArgs.BufferList != null) + { + _eventArgs.BufferList = null; + } + +#if NETCOREAPP2_1 + _eventArgs.SetBuffer(MemoryMarshal.AsMemory(memory)); +#else + var segment = memory.GetArray(); + _eventArgs.SetBuffer(segment.Array, segment.Offset, segment.Count); +#endif + if (!_socket.SendAsync(_eventArgs)) + { + _awaitable.Complete(_eventArgs.BytesTransferred, _eventArgs.SocketError); + } + + return _awaitable; + } + + private List> GetBufferList(in ReadOnlySequence buffer) + { + Debug.Assert(!buffer.IsEmpty); + Debug.Assert(!buffer.IsSingleSegment); + + if (_bufferList == null) + { + _bufferList = new List>(); + } + else + { + // Buffers are pooled, so it's OK to root them until the next multi-buffer write. + _bufferList.Clear(); + } + + foreach (var b in buffer) + { + _bufferList.Add(b.GetArray()); + } + + return _bufferList; + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet.AspnetCore/Client/Tcp/TcpConnection.cs b/Source/BPA.MQTTnet.AspnetCore/Client/Tcp/TcpConnection.cs new file mode 100644 index 0000000..845a67d --- /dev/null +++ b/Source/BPA.MQTTnet.AspnetCore/Client/Tcp/TcpConnection.cs @@ -0,0 +1,272 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipelines; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace MQTTnet.AspNetCore.Client.Tcp +{ + public class TcpConnection : ConnectionContext + { + private volatile bool _aborted; + private readonly EndPoint _endPoint; + private SocketSender _sender; + private SocketReceiver _receiver; + + private Socket _socket; + private IDuplexPipe _application; + + public bool IsConnected { get; private set; } + public override string ConnectionId { get; set; } + public override IFeatureCollection Features { get; } + public override IDictionary Items { get; set; } + public override IDuplexPipe Transport { get; set; } + + public TcpConnection(EndPoint endPoint) + { + _endPoint = endPoint; + } + + public TcpConnection(Socket socket) + { + _socket = socket; + _endPoint = socket.RemoteEndPoint; + + _sender = new SocketSender(_socket, PipeScheduler.ThreadPool); + _receiver = new SocketReceiver(_socket, PipeScheduler.ThreadPool); + } +#if NETCOREAPP3_1 || NET5_0_OR_GREATER + public override ValueTask DisposeAsync() +#else + public Task DisposeAsync() +#endif + { + IsConnected = false; + + Transport?.Output.Complete(); + Transport?.Input.Complete(); + + _socket?.Dispose(); + +#if NETCOREAPP3_1 || NET5_0_OR_GREATER + + return base.DisposeAsync(); + } +#else + + return Task.CompletedTask; + } +#endif + + public async Task StartAsync() + { + if (_socket == null) + { + _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + _sender = new SocketSender(_socket, PipeScheduler.ThreadPool); + _receiver = new SocketReceiver(_socket, PipeScheduler.ThreadPool); + await _socket.ConnectAsync(_endPoint); + } + + var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); + + Transport = pair.Transport; + _application = pair.Application; + + _ = ExecuteAsync(); + + IsConnected = true; + } + + private async Task ExecuteAsync() + { + Exception sendError = null; + try + { + // Spawn send and receive logic + var receiveTask = DoReceive(); + var sendTask = DoSend(); + + // If the sending task completes then close the receive + // We don't need to do this in the other direction because the kestrel + // will trigger the output closing once the input is complete. + if (await Task.WhenAny(receiveTask, sendTask) == sendTask) + { + // Tell the reader it's being aborted + _socket.Dispose(); + } + + // Now wait for both to complete + await receiveTask; + sendError = await sendTask; + + // Dispose the socket(should noop if already called) + _socket.Dispose(); + } + catch (Exception ex) + { + Console.WriteLine($"Unexpected exception in {nameof(TcpConnection)}.{nameof(StartAsync)}: " + ex); + } + finally + { + // Complete the output after disposing the socket + _application.Input.Complete(sendError); + } + } + private async Task DoReceive() + { + Exception error = null; + + try + { + await ProcessReceives(); + } + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionReset) + { + error = new MqttCommunicationException(ex); + } + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.OperationAborted || + ex.SocketErrorCode == SocketError.ConnectionAborted || + ex.SocketErrorCode == SocketError.Interrupted || + ex.SocketErrorCode == SocketError.InvalidArgument) + { + if (!_aborted) + { + // Calling Dispose after ReceiveAsync can cause an "InvalidArgument" error on *nix. + error = ConnectionAborted(); + } + } + catch (ObjectDisposedException) + { + if (!_aborted) + { + error = ConnectionAborted(); + } + } + catch (IOException ex) + { + error = ex; + } + catch (Exception ex) + { + error = new IOException(ex.Message, ex); + } + finally + { + if (_aborted) + { + error = error ?? ConnectionAborted(); + } + + _application.Output.Complete(error); + } + } + + private async Task ProcessReceives() + { + while (true) + { + // Ensure we have some reasonable amount of buffer space + var buffer = _application.Output.GetMemory(); + + var bytesReceived = await _receiver.ReceiveAsync(buffer); + + if (bytesReceived == 0) + { + // FIN + break; + } + + _application.Output.Advance(bytesReceived); + + var flushTask = _application.Output.FlushAsync(); + + if (!flushTask.IsCompleted) + { + await flushTask; + } + + var result = flushTask.GetAwaiter().GetResult(); + if (result.IsCompleted) + { + // Pipe consumer is shut down, do we stop writing + break; + } + } + } + + private Exception ConnectionAborted() + { + return new MqttCommunicationException("Connection Aborted"); + } + + private async Task DoSend() + { + Exception error = null; + + try + { + await ProcessSends(); + } + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.OperationAborted) + { + } + catch (ObjectDisposedException) + { + } + catch (IOException ex) + { + error = ex; + } + catch (Exception ex) + { + error = new IOException(ex.Message, ex); + } + finally + { + _aborted = true; + _socket.Shutdown(SocketShutdown.Both); + } + + return error; + } + + private async Task ProcessSends() + { + while (true) + { + // Wait for data to write from the pipe producer + var result = await _application.Input.ReadAsync(); + var buffer = result.Buffer; + + if (result.IsCanceled) + { + break; + } + + var end = buffer.End; + var isCompleted = result.IsCompleted; + if (!buffer.IsEmpty) + { + await _sender.SendAsync(buffer); + } + + _application.Input.AdvanceTo(end); + + if (isCompleted) + { + break; + } + } + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet.AspnetCore/ConnectionBuilderExtensions.cs b/Source/BPA.MQTTnet.AspnetCore/ConnectionBuilderExtensions.cs new file mode 100644 index 0000000..9ea8922 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet.AspnetCore/ConnectionRouteBuilderExtensions.cs b/Source/BPA.MQTTnet.AspnetCore/ConnectionRouteBuilderExtensions.cs new file mode 100644 index 0000000..cbbe9f3 --- /dev/null +++ b/Source/BPA.MQTTnet.AspnetCore/ConnectionRouteBuilderExtensions.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Connections; + +#if NETCOREAPP3_1 +using System; +#endif + +namespace MQTTnet.AspNetCore +{ + public static class ConnectionRouteBuilderExtensions + { +#if NETCOREAPP3_1 + [Obsolete("This class is obsolete and will be removed in a future version. The recommended alternative is to use MapMqtt inside Microsoft.AspNetCore.Builder.UseEndpoints(...).")] +#endif +#if NETCOREAPP3_1 || NETCOREAPP2_1 || NETSTANDARD + public static void MapMqtt(this ConnectionsRouteBuilder connection, PathString path) + { + connection.MapConnectionHandler(path, options => + { + options.WebSockets.SubProtocolSelector = MqttSubProtocolSelector.SelectSubProtocol; + }); + } +#endif + } +} diff --git a/Source/BPA.MQTTnet.AspnetCore/EndpointRouterExtensions.cs b/Source/BPA.MQTTnet.AspnetCore/EndpointRouterExtensions.cs new file mode 100644 index 0000000..f5c7241 --- /dev/null +++ b/Source/BPA.MQTTnet.AspnetCore/EndpointRouterExtensions.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. + + +#if NETCOREAPP3_1 || NET5_0_OR_GREATER + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; + +namespace MQTTnet.AspNetCore +{ + public static class EndpointRouterExtensions + { + public static void MapMqtt(this IEndpointRouteBuilder endpoints, string pattern) + { + endpoints.MapConnectionHandler(pattern, options => + { + options.WebSockets.SubProtocolSelector = MqttSubProtocolSelector.SelectSubProtocol; + }); + } + } +} + +#endif diff --git a/Source/BPA.MQTTnet.AspnetCore/MqttConnectionContext.cs b/Source/BPA.MQTTnet.AspnetCore/MqttConnectionContext.cs new file mode 100644 index 0000000..1268d25 --- /dev/null +++ b/Source/BPA.MQTTnet.AspnetCore/MqttConnectionContext.cs @@ -0,0 +1,193 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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; +using MQTTnet.Exceptions; +using MQTTnet.Formatter; +using MQTTnet.Packets; +using System; +using System.IO.Pipelines; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Internal; + +namespace MQTTnet.AspNetCore +{ + public sealed class MqttConnectionContext : IMqttChannelAdapter + { + readonly AsyncLock _writerLock = new AsyncLock(); + + PipeReader _input; + PipeWriter _output; + + public MqttConnectionContext(MqttPacketFormatterAdapter packetFormatterAdapter, ConnectionContext connection) + { + PacketFormatterAdapter = packetFormatterAdapter ?? throw new ArgumentNullException(nameof(packetFormatterAdapter)); + Connection = connection ?? throw new ArgumentNullException(nameof(connection)); + + if (Connection.Transport != null) + { + _input = Connection.Transport.Input; + _output = Connection.Transport.Output; + } + } + + public string Endpoint + { + get + { +#if NETCOREAPP3_1 + if (Connection?.RemoteEndPoint != null) + { + return Connection.RemoteEndPoint.ToString(); + } +#endif + var connection = Http?.HttpContext?.Connection; + if (connection == null) + { + return Connection.ConnectionId; + } + + return $"{connection.RemoteIpAddress}:{connection.RemotePort}"; + } + } + + public bool IsSecureConnection => Http?.HttpContext?.Request?.IsHttps ?? false; + + public X509Certificate2 ClientCertificate => Http?.HttpContext?.Connection?.ClientCertificate; + + public ConnectionContext Connection { get; } + + public MqttPacketFormatterAdapter PacketFormatterAdapter { get; } + + public long BytesSent { get; set; } + + public long BytesReceived { get; set; } + + public bool IsReadingPacket { get; private set; } + + IHttpContextFeature Http => Connection.Features.Get(); + + public async Task ConnectAsync(CancellationToken cancellationToken) + { + if (Connection is TcpConnection tcp && !tcp.IsConnected) + { + await tcp.StartAsync().ConfigureAwait(false); + } + + _input = Connection.Transport.Input; + _output = Connection.Transport.Output; + } + + public Task DisconnectAsync(CancellationToken cancellationToken) + { + _input?.Complete(); + _output?.Complete(); + + return Task.CompletedTask; + } + + public async Task ReceivePacketAsync(CancellationToken cancellationToken) + { + var input = Connection.Transport.Input; + + try + { + while (!cancellationToken.IsCancellationRequested) + { + ReadResult readResult; + var readTask = input.ReadAsync(cancellationToken); + if (readTask.IsCompleted) + { + readResult = readTask.Result; + } + else + { + readResult = await readTask.ConfigureAwait(false); + } + + var buffer = readResult.Buffer; + + var consumed = buffer.Start; + var observed = buffer.Start; + + try + { + if (!buffer.IsEmpty) + { + if (PacketFormatterAdapter.TryDecode(buffer, out var packet, out consumed, out observed, out var received)) + { + BytesReceived += received; + return packet; + } + else + { + // we did receive something but the message is not yet complete + IsReadingPacket = true; + } + } + else if (readResult.IsCompleted) + { + throw new MqttCommunicationException("Connection Aborted"); + } + } + finally + { + // The buffer was sliced up to where it was consumed, so we can just advance to the start. + // We mark examined as buffer.End so that if we didn't receive a full frame, we'll wait for more data + // before yielding the read again. + input.AdvanceTo(consumed, observed); + } + } + } + catch (Exception e) + { + // completing the channel makes sure that there is no more data read after a protocol error + _input?.Complete(e); + _output?.Complete(e); + throw; + } + finally + { + IsReadingPacket = false; + } + + cancellationToken.ThrowIfCancellationRequested(); + return null; + } + + public void ResetStatistics() + { + BytesReceived = 0; + BytesSent = 0; + } + + 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.Join().AsMemory(); + var output = _output; + var result = await output.WriteAsync(msg, cancellationToken).ConfigureAwait(false); + if (result.IsCompleted) + { + BytesSent += msg.Length; + } + + PacketFormatterAdapter.Cleanup(); + } + } + + public void Dispose() + { + } + } +} diff --git a/Source/BPA.MQTTnet.AspnetCore/MqttConnectionHandler.cs b/Source/BPA.MQTTnet.AspnetCore/MqttConnectionHandler.cs new file mode 100644 index 0000000..b86de30 --- /dev/null +++ b/Source/BPA.MQTTnet.AspnetCore/MqttConnectionHandler.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 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 sealed class MqttConnectionHandler : ConnectionHandler, IMqttServerAdapter + { + MqttServerOptions _serverOptions; + + public Func ClientHandler { get; set; } + + public override async Task OnConnectedAsync(ConnectionContext connection) + { + // required for websocket transport to work + var transferFormatFeature = connection.Features.Get(); + if (transferFormatFeature != null) + { + transferFormatFeature.ActiveFormat = TransferFormat.Binary; + } + + var formatter = new MqttPacketFormatterAdapter(new MqttBufferWriter(_serverOptions.WriterBufferSize, _serverOptions.WriterBufferSizeMax)); + using (var adapter = new MqttConnectionContext(formatter, connection)) + { + var clientHandler = ClientHandler; + if (clientHandler != null) + { + await clientHandler(adapter).ConfigureAwait(false); + } + } + } + + public Task StartAsync(MqttServerOptions options, IMqttNetLogger logger) + { + _serverOptions = options; + + return Task.CompletedTask; + } + + public Task StopAsync() + { + return Task.CompletedTask; + } + + public void Dispose() + { + } + } +} diff --git a/Source/BPA.MQTTnet.AspnetCore/MqttHostedServer.cs b/Source/BPA.MQTTnet.AspnetCore/MqttHostedServer.cs new file mode 100644 index 0000000..81d0eee --- /dev/null +++ b/Source/BPA.MQTTnet.AspnetCore/MqttHostedServer.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.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; +using MQTTnet.Server; + +namespace MQTTnet.AspNetCore +{ + public sealed class MqttHostedServer : MqttServer, IHostedService + { + public MqttHostedServer(MqttServerOptions options, IEnumerable adapters, IMqttNetLogger logger) + : base(options, adapters, logger) + { + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _ = StartAsync(); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return StopAsync(); + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet.AspnetCore/MqttSubProtocolSelector.cs b/Source/BPA.MQTTnet.AspnetCore/MqttSubProtocolSelector.cs new file mode 100644 index 0000000..6f318b1 --- /dev/null +++ b/Source/BPA.MQTTnet.AspnetCore/MqttSubProtocolSelector.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 System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Http; + +namespace MQTTnet.AspNetCore +{ + public static class MqttSubProtocolSelector + { + public static string SelectSubProtocol(HttpRequest request) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + + string subProtocol = null; + if (request.Headers.TryGetValue("Sec-WebSocket-Protocol", out var requestedSubProtocolValues)) + { + subProtocol = SelectSubProtocol(requestedSubProtocolValues); + } + + return subProtocol; + } + + public static string SelectSubProtocol(IList requestedSubProtocolValues) + { + if (requestedSubProtocolValues == null) throw new ArgumentNullException(nameof(requestedSubProtocolValues)); + + // Order the protocols to also match "mqtt", "mqttv-3.1", "mqttv-3.11" etc. + return requestedSubProtocolValues + .OrderByDescending(p => p.Length) + .FirstOrDefault(p => p.ToLower().StartsWith("mqtt")); + } + } +} diff --git a/Source/BPA.MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs b/Source/BPA.MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs new file mode 100644 index 0000000..a83eb02 --- /dev/null +++ b/Source/BPA.MQTTnet.AspnetCore/MqttWebSocketServerAdapter.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.AspNetCore.Http; +using MQTTnet.Adapter; +using MQTTnet.Formatter; +using MQTTnet.Implementations; +using MQTTnet.Server; +using System; +using System.Net.WebSockets; +using System.Threading.Tasks; +using MQTTnet.Diagnostics; + +namespace MQTTnet.AspNetCore +{ + public sealed class MqttWebSocketServerAdapter : IMqttServerAdapter + { + IMqttNetLogger _logger = new MqttNetNullLogger(); + + public Func ClientHandler { get; set; } + + public Task StartAsync(MqttServerOptions options, IMqttNetLogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + return Task.CompletedTask; + } + + public Task StopAsync() + { + return Task.CompletedTask; + } + + public async Task RunWebSocketConnectionAsync(WebSocket webSocket, HttpContext httpContext) + { + if (webSocket == null) throw new ArgumentNullException(nameof(webSocket)); + + var endpoint = $"{httpContext.Connection.RemoteIpAddress}:{httpContext.Connection.RemotePort}"; + + var clientCertificate = await httpContext.Connection.GetClientCertificateAsync().ConfigureAwait(false); + try + { + var isSecureConnection = clientCertificate != null; + + var clientHandler = ClientHandler; + if (clientHandler != null) + { + var formatter = new MqttPacketFormatterAdapter(new MqttBufferWriter(4096, 65535)); + var channel = new MqttWebSocketChannel(webSocket, endpoint, isSecureConnection, clientCertificate); + + using (var channelAdapter = new MqttChannelAdapter(channel, formatter, null, _logger)) + { + await clientHandler(channelAdapter).ConfigureAwait(false); + } + } + } + finally + { + clientCertificate?.Dispose(); + } + } + + public void Dispose() + { + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet.AspnetCore/ReaderExtensions.cs b/Source/BPA.MQTTnet.AspnetCore/ReaderExtensions.cs new file mode 100644 index 0000000..d3b00ab --- /dev/null +++ b/Source/BPA.MQTTnet.AspnetCore/ReaderExtensions.cs @@ -0,0 +1,113 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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 +{ + public static class ReaderExtensions + { + public static bool TryDecode(this MqttPacketFormatterAdapter formatter, + in ReadOnlySequence input, + out MqttPacket packet, + out SequencePosition consumed, + out SequencePosition observed, + out int bytesRead) + { + if (formatter == null) throw new ArgumentNullException(nameof(formatter)); + + packet = null; + consumed = input.Start; + observed = input.End; + bytesRead = 0; + var copy = input; + + if (copy.Length < 2) + { + return false; + } + + var fixedHeader = copy.First.Span[0]; + if (!TryReadBodyLength(ref copy, out int headerLength, out var bodyLength)) + { + return false; + } + + if (copy.Length < bodyLength) + { + return false; + } + + var bodySlice = copy.Slice(0, bodyLength); + var buffer = bodySlice.GetMemory().ToArray(); + + var receivedMqttPacket = new ReceivedMqttPacket(fixedHeader, new ArraySegment(buffer, 0, buffer.Length), buffer.Length + 2); + + if (formatter.ProtocolVersion == MqttProtocolVersion.Unknown) + { + formatter.DetectProtocolVersion(receivedMqttPacket); + } + + packet = formatter.Decode(receivedMqttPacket); + consumed = bodySlice.End; + observed = bodySlice.End; + bytesRead = headerLength + bodyLength; + return true; + } + + static ReadOnlyMemory GetMemory(this in ReadOnlySequence input) + { + if (input.IsSingleSegment) + { + return input.First; + } + + // Should be rare + return input.ToArray(); + } + + 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; + var value = 0; + byte encodedByte; + var index = 1; + headerLength = 0; + bodyLength = 0; + + var temp = input.Slice(0, Math.Min(5, input.Length)).GetMemory().Span; + + do + { + if (index == temp.Length) + { + return false; + } + + encodedByte = temp[index]; + index++; + + value += (byte)(encodedByte & 127) * multiplier; + if (multiplier > 128 * 128 * 128) + { + throw new MqttProtocolViolationException($"Remaining length is invalid (Data={string.Join(",", temp.Slice(1, index).ToArray())})."); + } + + multiplier *= 128; + } while ((encodedByte & 128) != 0); + + input = input.Slice(index); + + headerLength = index; + bodyLength = value; + return true; + } + } +} diff --git a/Source/BPA.MQTTnet.AspnetCore/ServiceCollectionExtensions.cs b/Source/BPA.MQTTnet.AspnetCore/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..90f2515 --- /dev/null +++ b/Source/BPA.MQTTnet.AspnetCore/ServiceCollectionExtensions.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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.Implementations; +using MQTTnet.Server; + +namespace MQTTnet.AspNetCore +{ + public static class ServiceCollectionExtensions + { + 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)); + + services.AddSingleton(options); + + services.AddHostedMqttServer(); + + return services; + } + + public static IServiceCollection AddHostedMqttServer(this IServiceCollection services, Action configure = null) + { + services.AddSingleton(s => + { + var serverOptionsBuilder = new MqttServerOptionsBuilder(); + configure?.Invoke(serverOptionsBuilder); + return serverOptionsBuilder.Build(); + }); + + services.AddHostedMqttServer(); + + return services; + } + + public static IServiceCollection AddHostedMqttServerWithServices(this IServiceCollection services, Action configure) + { + services.AddSingleton(s => + { + var builder = new AspNetMqttServerOptionsBuilder(s); + configure(builder); + return builder.Build(); + }); + + services.AddHostedMqttServer(); + + return 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()); + + return services; + } + + public static IServiceCollection AddMqttWebSocketServerAdapter(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(s => s.GetService()); + + return services; + } + + public static IServiceCollection AddMqttTcpServerAdapter(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(s => s.GetService()); + + return services; + } + + public static IServiceCollection AddMqttConnectionHandler(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(s => s.GetService()); + + return services; + } + } +} diff --git a/Source/BPA.MQTTnet/Adapter/IMqttChannelAdapter.cs b/Source/BPA.MQTTnet/Adapter/IMqttChannelAdapter.cs new file mode 100644 index 0000000..9e56da7 --- /dev/null +++ b/Source/BPA.MQTTnet/Adapter/IMqttChannelAdapter.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 MQTTnet.Formatter; +using MQTTnet.Packets; +using System; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; + + +namespace MQTTnet.Adapter +{ + public interface IMqttChannelAdapter : IDisposable + { + string Endpoint { get; } + + bool IsSecureConnection { get; } + + X509Certificate2 ClientCertificate { get; } + + MqttPacketFormatterAdapter PacketFormatterAdapter { get; } + + long BytesSent { get; } + + long BytesReceived { get; } + + bool IsReadingPacket { get; } + + Task ConnectAsync(CancellationToken cancellationToken); + + Task DisconnectAsync(CancellationToken cancellationToken); + + Task SendPacketAsync(MqttPacket packet, CancellationToken cancellationToken); + + Task ReceivePacketAsync(CancellationToken cancellationToken); + + void ResetStatistics(); + } +} diff --git a/Source/BPA.MQTTnet/Adapter/IMqttClientAdapterFactory.cs b/Source/BPA.MQTTnet/Adapter/IMqttClientAdapterFactory.cs new file mode 100644 index 0000000..85f7a74 --- /dev/null +++ b/Source/BPA.MQTTnet/Adapter/IMqttClientAdapterFactory.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 MQTTnet.Client; +using MQTTnet.Diagnostics; + +namespace MQTTnet.Adapter +{ + public interface IMqttClientAdapterFactory + { + IMqttChannelAdapter CreateClientAdapter(MqttClientOptions options, MqttPacketInspector packetInspector, IMqttNetLogger logger); + } +} diff --git a/Source/BPA.MQTTnet/Adapter/IMqttServerAdapter.cs b/Source/BPA.MQTTnet/Adapter/IMqttServerAdapter.cs new file mode 100644 index 0000000..3284305 --- /dev/null +++ b/Source/BPA.MQTTnet/Adapter/IMqttServerAdapter.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; +using System.Threading.Tasks; +using MQTTnet.Diagnostics; +using MQTTnet.Server; + +namespace MQTTnet.Adapter +{ + public interface IMqttServerAdapter : IDisposable + { + Func ClientHandler { get; set; } + + Task StartAsync(MqttServerOptions options, IMqttNetLogger logger); + Task StopAsync(); + } +} diff --git a/Source/BPA.MQTTnet/Adapter/MqttChannelAdapter.cs b/Source/BPA.MQTTnet/Adapter/MqttChannelAdapter.cs new file mode 100644 index 0000000..121c4cd --- /dev/null +++ b/Source/BPA.MQTTnet/Adapter/MqttChannelAdapter.cs @@ -0,0 +1,426 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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; +using System.Runtime.InteropServices; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Channel; +using MQTTnet.Diagnostics; +using MQTTnet.Exceptions; +using MQTTnet.Formatter; +using MQTTnet.Implementations; +using MQTTnet.Internal; +using MQTTnet.Packets; + +namespace MQTTnet.Adapter +{ + public sealed class MqttChannelAdapter : Disposable, IMqttChannelAdapter + { + const uint ErrorOperationAborted = 0x800703E3; + const int ReadBufferSize = 4096; + + readonly IMqttChannel _channel; + readonly byte[] _fixedHeaderBuffer = new byte[2]; + readonly MqttNetSourceLogger _logger; + + readonly MqttPacketInspector _packetInspector; + + readonly byte[] _singleByteBuffer = new byte[1]; + + readonly AsyncLock _syncRoot = new AsyncLock(); + + long _bytesReceived; + long _bytesSent; + + 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)); + + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + _logger = logger.WithSource(nameof(MqttChannelAdapter)); + } + + public long BytesReceived => Interlocked.Read(ref _bytesReceived); + + public long BytesSent => Interlocked.Read(ref _bytesSent); + + public X509Certificate2 ClientCertificate => _channel.ClientCertificate; + + public string Endpoint => _channel.Endpoint; + + public bool IsReadingPacket { get; private set; } + + public bool IsSecureConnection => _channel.IsSecureConnection; + + public MqttPacketFormatterAdapter PacketFormatterAdapter { get; } + + public async Task ConnectAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + try + { + await _channel.ConnectAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) + { + if (!WrapAndThrowException(exception)) + { + throw; + } + } + } + + public async Task DisconnectAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + try + { + await _channel.DisconnectAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) + { + if (!WrapAndThrowException(exception)) + { + throw; + } + } + } + + public async Task ReceivePacketAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + try + { + _packetInspector?.BeginReceivePacket(); + + ReceivedMqttPacket receivedPacket; + var receivedPacketTask = ReceiveAsync(cancellationToken); + if (receivedPacketTask.IsCompleted) + { + receivedPacket = receivedPacketTask.Result; + } + else + { + receivedPacket = await receivedPacketTask.ConfigureAwait(false); + } + + if (receivedPacket.TotalLength == 0 || cancellationToken.IsCancellationRequested) + { + return null; + } + + _packetInspector?.EndReceivePacket(); + + Interlocked.Add(ref _bytesSent, receivedPacket.TotalLength); + + if (PacketFormatterAdapter.ProtocolVersion == MqttProtocolVersion.Unknown) + { + PacketFormatterAdapter.DetectProtocolVersion(receivedPacket); + } + + var packet = PacketFormatterAdapter.Decode(receivedPacket); + if (packet == null) + { + throw new MqttProtocolViolationException("Received malformed packet."); + } + + _logger.Verbose("RX ({0} bytes) <<< {1}", receivedPacket.TotalLength, packet); + + return packet; + } + catch (OperationCanceledException) + { + } + catch (ObjectDisposedException) + { + } + catch (Exception exception) + { + if (!WrapAndThrowException(exception)) + { + throw; + } + } + + return null; + } + + public void ResetStatistics() + { + Interlocked.Exchange(ref _bytesReceived, 0L); + 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) + { + _channel.Dispose(); + _syncRoot.Dispose(); + } + + base.Dispose(disposing); + } + + async Task ReadBodyLengthAsync(byte initialEncodedByte, CancellationToken cancellationToken) + { + var offset = 0; + var multiplier = 128; + var value = initialEncodedByte & 127; + int encodedByte = initialEncodedByte; + + while ((encodedByte & 128) != 0) + { + offset++; + if (offset > 3) + { + throw new MqttProtocolViolationException("Remaining length is invalid."); + } + + if (cancellationToken.IsCancellationRequested) + { + return -1; + } + + var readCount = await _channel.ReadAsync(_singleByteBuffer, 0, 1, cancellationToken).ConfigureAwait(false); + + if (cancellationToken.IsCancellationRequested) + { + return -1; + } + + if (readCount == 0) + { + return -1; + } + + _packetInspector?.FillReceiveBuffer(_singleByteBuffer); + + encodedByte = _singleByteBuffer[0]; + + value += (encodedByte & 127) * multiplier; + multiplier *= 128; + } + + return value; + } + + async Task ReadFixedHeaderAsync(CancellationToken cancellationToken) + { + // The MQTT fixed header contains 1 byte of flags and at least 1 byte for the remaining data length. + // So in all cases at least 2 bytes must be read for a complete MQTT packet. + var buffer = _fixedHeaderBuffer; + var totalBytesRead = 0; + + while (totalBytesRead < buffer.Length) + { + var bytesRead = await _channel.ReadAsync(buffer, totalBytesRead, buffer.Length - totalBytesRead, cancellationToken).ConfigureAwait(false); + + if (cancellationToken.IsCancellationRequested) + { + return ReadFixedHeaderResult.Cancelled; + } + + if (bytesRead == 0) + { + return ReadFixedHeaderResult.ConnectionClosed; + } + + totalBytesRead += bytesRead; + } + + _packetInspector?.FillReceiveBuffer(buffer); + + var hasRemainingLength = buffer[1] != 0; + if (!hasRemainingLength) + { + return new ReadFixedHeaderResult + { + FixedHeader = new MqttFixedHeader(buffer[0], 0, totalBytesRead) + }; + } + + var bodyLength = await ReadBodyLengthAsync(buffer[1], cancellationToken).ConfigureAwait(false); + + if (bodyLength == -1) + { + return new ReadFixedHeaderResult + { + IsConnectionClosed = true + }; + } + + totalBytesRead += bodyLength; + return new ReadFixedHeaderResult + { + FixedHeader = new MqttFixedHeader(buffer[0], bodyLength, totalBytesRead) + }; + } + + async Task ReceiveAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return ReceivedMqttPacket.Empty; + } + + var readFixedHeaderResult = await ReadFixedHeaderAsync(cancellationToken).ConfigureAwait(false); + + if (cancellationToken.IsCancellationRequested) + { + return ReceivedMqttPacket.Empty; + } + + if (readFixedHeaderResult.IsConnectionClosed) + { + return ReceivedMqttPacket.Empty; + } + + try + { + IsReadingPacket = true; + + var fixedHeader = readFixedHeaderResult.FixedHeader; + if (fixedHeader.RemainingLength == 0) + { + return new ReceivedMqttPacket(fixedHeader.Flags, PlatformAbstractionLayer.EmptyByteArraySegment, 2); + } + + var bodyLength = fixedHeader.RemainingLength; + var body = new byte[bodyLength]; + + var bodyOffset = 0; + var chunkSize = Math.Min(ReadBufferSize, bodyLength); + + do + { + var bytesLeft = body.Length - bodyOffset; + if (chunkSize > bytesLeft) + { + chunkSize = bytesLeft; + } + + var readBytes = await _channel.ReadAsync(body, bodyOffset, chunkSize, cancellationToken).ConfigureAwait(false); + + if (cancellationToken.IsCancellationRequested) + { + return ReceivedMqttPacket.Empty; + } + + if (readBytes == 0) + { + return ReceivedMqttPacket.Empty; + } + + bodyOffset += readBytes; + } while (bodyOffset < bodyLength); + + _packetInspector?.FillReceiveBuffer(body); + + var bodySegment = new ArraySegment(body, 0, bodyLength); + return new ReceivedMqttPacket(fixedHeader.Flags, bodySegment, fixedHeader.TotalLength); + } + finally + { + IsReadingPacket = false; + } + } + + 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; + } + + if (exception is SocketException socketException) + { + if (socketException.SocketErrorCode == SocketError.OperationAborted) + { + throw new OperationCanceledException(); + } + + if (socketException.SocketErrorCode == SocketError.ConnectionAborted) + { + throw new MqttCommunicationException(socketException); + } + } + + if (exception is COMException comException) + { + if ((uint)comException.HResult == ErrorOperationAborted) + { + throw new OperationCanceledException(); + } + } + + throw new MqttCommunicationException(exception); + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Adapter/MqttConnectingFailedException.cs b/Source/BPA.MQTTnet/Adapter/MqttConnectingFailedException.cs new file mode 100644 index 0000000..ea3cb47 --- /dev/null +++ b/Source/BPA.MQTTnet/Adapter/MqttConnectingFailedException.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; +using MQTTnet.Client; +using MQTTnet.Exceptions; + +namespace MQTTnet.Adapter +{ + public sealed class MqttConnectingFailedException : MqttCommunicationException + { + public MqttConnectingFailedException(string message, Exception innerException, MqttClientConnectResult connectResult) + : base(message, innerException) + { + Result = connectResult; + } + + public MqttClientConnectResult Result { get; } + + public MqttClientConnectResultCode ResultCode => Result?.ResultCode ?? MqttClientConnectResultCode.UnspecifiedError; + } +} diff --git a/Source/BPA.MQTTnet/Adapter/MqttPacketInspector.cs b/Source/BPA.MQTTnet/Adapter/MqttPacketInspector.cs new file mode 100644 index 0000000..c74114a --- /dev/null +++ b/Source/BPA.MQTTnet/Adapter/MqttPacketInspector.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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 MQTTnet.Diagnostics; +using MQTTnet.Formatter; +using MQTTnet.Internal; + +namespace MQTTnet.Adapter +{ + public sealed class MqttPacketInspector + { + readonly MqttNetSourceLogger _logger; + readonly AsyncEvent _asyncEvent; + + MemoryStream _receivedPacketBuffer; + + public MqttPacketInspector(AsyncEvent asyncEvent, IMqttNetLogger logger) + { + _asyncEvent = asyncEvent ?? throw new ArgumentNullException(nameof(asyncEvent)); + + if (logger == null) throw new ArgumentNullException(nameof(logger)); + _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 (!_asyncEvent.HasHandlers) + { + return; + } + + var buffer = _receivedPacketBuffer.ToArray(); + _receivedPacketBuffer.SetLength(0); + + InspectPacket(buffer, MqttPacketFlowDirection.Inbound); + } + + public void BeginSendPacket(MqttPacketBuffer buffer) + { + if (!_asyncEvent.HasHandlers) + { + return; + } + + // Create a copy of the actual packet so that the inspector gets no access + // to the internal buffers. This is waste of memory but this feature is only + // intended for debugging etc. so that this is OK. + var bufferCopy = buffer.ToArray(); + + InspectPacket(bufferCopy, MqttPacketFlowDirection.Outbound); + } + + public void FillReceiveBuffer(byte[] buffer) + { + if (!_asyncEvent.HasHandlers) + { + return; + } + + _receivedPacketBuffer?.Write(buffer, 0, buffer.Length); + } + + void InspectPacket(byte[] buffer, MqttPacketFlowDirection direction) + { + try + { + var eventArgs = new InspectMqttPacketEventArgs + { + Buffer = buffer, + Direction = direction + }; + + _asyncEvent.InvokeAsync(eventArgs).GetAwaiter().GetResult(); + } + catch (Exception exception) + { + _logger.Error(exception, "Error while inspecting packet."); + } + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Adapter/ReceivedMqttPacket.cs b/Source/BPA.MQTTnet/Adapter/ReceivedMqttPacket.cs new file mode 100644 index 0000000..74815cb --- /dev/null +++ b/Source/BPA.MQTTnet/Adapter/ReceivedMqttPacket.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace MQTTnet.Adapter +{ + public readonly struct ReceivedMqttPacket + { + public static readonly ReceivedMqttPacket Empty = new ReceivedMqttPacket(); + + public ReceivedMqttPacket(byte fixedHeader, ArraySegment body, int totalLength) + { + FixedHeader = fixedHeader; + Body = body; + TotalLength = totalLength; + } + + public byte FixedHeader { get; } + + public ArraySegment Body { get; } + + public int TotalLength { get; } + } +} diff --git a/Source/BPA.MQTTnet/BPA.MQTTnet.csproj b/Source/BPA.MQTTnet/BPA.MQTTnet.csproj new file mode 100644 index 0000000..bbc7e7b --- /dev/null +++ b/Source/BPA.MQTTnet/BPA.MQTTnet.csproj @@ -0,0 +1,97 @@ + + + + + + + + @(ReleaseNotes, '%0a') + + + + + netstandard1.3;netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0 + $(TargetFrameworks);net452;net461 + $(TargetFrameworks);uap10.0 + 7.3 + + MQTTnet + MQTTnet + false + The contributors of MQTTnet + BPA.MQTTnet + BPA.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 + BPA.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/BPA.MQTTnet/Certificates/BlobCertificateProvider.cs b/Source/BPA.MQTTnet/Certificates/BlobCertificateProvider.cs new file mode 100644 index 0000000..be2024e --- /dev/null +++ b/Source/BPA.MQTTnet/Certificates/BlobCertificateProvider.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. + +using System; +using System.Security.Cryptography.X509Certificates; + +namespace MQTTnet.Certificates +{ + public class BlobCertificateProvider : ICertificateProvider + { + public BlobCertificateProvider(byte[] blob) + { + Blob = blob ?? throw new ArgumentNullException(nameof(blob)); + } + + public byte[] Blob { get; } + + public string Password { get; set; } + + public X509Certificate2 GetCertificate() + { + if (string.IsNullOrEmpty(Password)) + { + // Use a different overload when no password is specified. Otherwise the constructor will fail. + return new X509Certificate2(Blob); + } + + return new X509Certificate2(Blob, Password); + } + } +} diff --git a/Source/BPA.MQTTnet/Certificates/ICertificateProvider.cs b/Source/BPA.MQTTnet/Certificates/ICertificateProvider.cs new file mode 100644 index 0000000..58b4a90 --- /dev/null +++ b/Source/BPA.MQTTnet/Certificates/ICertificateProvider.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. + +using System.Security.Cryptography.X509Certificates; + +namespace MQTTnet.Certificates +{ + public interface ICertificateProvider + { + X509Certificate2 GetCertificate(); + } +} diff --git a/Source/BPA.MQTTnet/Certificates/X509CertificateProvider.cs b/Source/BPA.MQTTnet/Certificates/X509CertificateProvider.cs new file mode 100644 index 0000000..3dcca23 --- /dev/null +++ b/Source/BPA.MQTTnet/Certificates/X509CertificateProvider.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if !WINDOWS_UWP +using System; +using System.Security.Cryptography.X509Certificates; + +namespace MQTTnet.Certificates +{ + public class X509CertificateProvider : ICertificateProvider + { + readonly X509Certificate2 _certificate; + + public X509CertificateProvider(X509Certificate2 certificate) + { + _certificate = certificate ?? throw new ArgumentNullException(nameof(certificate)); + } + + public X509Certificate2 GetCertificate() + { + return _certificate; + } + } +} +#endif \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Channel/IMqttChannel.cs b/Source/BPA.MQTTnet/Channel/IMqttChannel.cs new file mode 100644 index 0000000..5360ce2 --- /dev/null +++ b/Source/BPA.MQTTnet/Channel/IMqttChannel.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; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; + +namespace MQTTnet.Channel +{ + public interface IMqttChannel : IDisposable + { + string Endpoint { get; } + bool IsSecureConnection { get; } + X509Certificate2 ClientCertificate { get; } + + Task ConnectAsync(CancellationToken cancellationToken); + Task DisconnectAsync(CancellationToken cancellationToken); + + Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken); + Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken); + } +} diff --git a/Source/BPA.MQTTnet/Client/Connecting/MqttClientConnectResult.cs b/Source/BPA.MQTTnet/Client/Connecting/MqttClientConnectResult.cs new file mode 100644 index 0000000..831d14c --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Connecting/MqttClientConnectResult.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.Collections.Generic; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Client +{ + public sealed class MqttClientConnectResult + { + /// + /// Gets the result code. + /// MQTTv5 only. + /// + public MqttClientConnectResultCode ResultCode { get; internal set; } + + /// + /// Gets a value indicating whether a session was already available or not. + /// MQTTv5 only. + /// + public bool IsSessionPresent { get; internal set; } + + /// + /// Gets a value indicating whether wildcards can be used in subscriptions at the current server. + /// MQTTv5 only. + /// + public bool WildcardSubscriptionAvailable { get; internal set; } + + /// + /// Gets whether the server supports retained messages. + /// MQTTv5 only. + /// + public bool RetainAvailable { get; internal set; } + + /// + /// Gets the client identifier which was chosen by the server. + /// MQTTv5 only. + /// + public string AssignedClientIdentifier { get; internal set; } + + /// + /// Gets the authentication method. + /// MQTTv5 only. + /// + public string AuthenticationMethod { get; internal set; } + + /// + /// Gets the authentication data. + /// MQTTv5 only. + /// + public byte[] AuthenticationData { get; internal set; } + + public uint? MaximumPacketSize { get; internal set; } + + /// + /// Gets the reason string. + /// MQTTv5 only. + /// + public string ReasonString { get; internal set; } + + public ushort? ReceiveMaximum { get; internal set; } + + /// + /// Gets the maximum QoS which is supported by the server. + /// MQTTv5 only. + /// + public MqttQualityOfServiceLevel MaximumQoS { get; internal set; } + + /// + /// Gets the response information. + /// MQTTv5 only. + /// + public string ResponseInformation { get; internal set; } + + /// + /// Gets the maximum value for a topic alias. 0 means not supported. + /// MQTTv5 only. + /// + public ushort TopicAliasMaximum { get; internal set; } + + /// + /// Gets an alternate server which should be used instead of the current one. + /// MQTTv5 only. + /// + 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. + /// A value of 0 indicates that the feature is not used. + /// + public ushort ServerKeepAlive { get; internal set; } + + public uint? SessionExpiryInterval { get; internal set; } + + /// + /// Gets a value indicating whether the subscription identifiers are available or not. + /// MQTTv5 only. + /// + public bool SubscriptionIdentifiersAvailable { get; internal set; } + + /// + /// Gets a value indicating whether the shared subscriptions are available or not. + /// MQTTv5 only. + /// + public bool SharedSubscriptionAvailable { get; internal set; } + + /// + /// Gets 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. + /// MQTTv5 only. + /// + public List UserProperties { get; internal set; } + } +} diff --git a/Source/BPA.MQTTnet/Client/Connecting/MqttClientConnectResultCode.cs b/Source/BPA.MQTTnet/Client/Connecting/MqttClientConnectResultCode.cs new file mode 100644 index 0000000..a25aa07 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Connecting/MqttClientConnectResultCode.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.Client +{ + public enum MqttClientConnectResultCode + { + Success = 0, + UnspecifiedError = 128, + MalformedPacket = 129, + ProtocolError = 130, + ImplementationSpecificError = 131, + UnsupportedProtocolVersion = 132, + ClientIdentifierNotValid = 133, + BadUserNameOrPassword = 134, + NotAuthorized = 135, + ServerUnavailable = 136, + ServerBusy = 137, + Banned = 138, + BadAuthenticationMethod = 140, + TopicNameInvalid = 144, + PacketTooLarge = 149, + QuotaExceeded = 151, + PayloadFormatInvalid = 153, + RetainNotSupported = 154, + QoSNotSupported = 155, + UseAnotherServer = 156, + ServerMoved = 157, + ConnectionRateExceeded = 159 + } +} diff --git a/Source/BPA.MQTTnet/Client/Connecting/MqttClientConnectResultFactory.cs b/Source/BPA.MQTTnet/Client/Connecting/MqttClientConnectResultFactory.cs new file mode 100644 index 0000000..f32a915 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Client/Connecting/MqttClientConnectedEventArgs.cs b/Source/BPA.MQTTnet/Client/Connecting/MqttClientConnectedEventArgs.cs new file mode 100644 index 0000000..efb28e8 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Connecting/MqttClientConnectedEventArgs.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 System; + +namespace MQTTnet.Client +{ + public sealed class MqttClientConnectedEventArgs : EventArgs + { + public MqttClientConnectedEventArgs(MqttClientConnectResult connectResult) + { + ConnectResult = connectResult ?? throw new ArgumentNullException(nameof(connectResult)); + } + + /// + /// Gets the authentication result. + /// Hint: MQTT 5 feature only. + /// + public MqttClientConnectResult ConnectResult { get; } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Client/Connecting/MqttClientConnectingEventArgs.cs b/Source/BPA.MQTTnet/Client/Connecting/MqttClientConnectingEventArgs.cs new file mode 100644 index 0000000..a4b87bc --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Client/Disconnecting/MqttClientDisconnectOptions.cs b/Source/BPA.MQTTnet/Client/Disconnecting/MqttClientDisconnectOptions.cs new file mode 100644 index 0000000..3f4e88a --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Disconnecting/MqttClientDisconnectOptions.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. + +namespace MQTTnet.Client +{ + public sealed class MqttClientDisconnectOptions + { + /// + /// Gets or sets the reason code. + /// Hint: MQTT 5 feature only. + /// + public MqttClientDisconnectReason Reason { get; set; } = MqttClientDisconnectReason.NormalDisconnection; + + /// + /// Gets or sets the reason string. + /// Hint: MQTT 5 feature only. + /// + public string ReasonString { get; set; } + } +} diff --git a/Source/BPA.MQTTnet/Client/Disconnecting/MqttClientDisconnectOptionsBuilder.cs b/Source/BPA.MQTTnet/Client/Disconnecting/MqttClientDisconnectOptionsBuilder.cs new file mode 100644 index 0000000..34f16b7 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Client/Disconnecting/MqttClientDisconnectReason.cs b/Source/BPA.MQTTnet/Client/Disconnecting/MqttClientDisconnectReason.cs new file mode 100644 index 0000000..a451132 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Disconnecting/MqttClientDisconnectReason.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. + +namespace MQTTnet.Client +{ + public enum MqttClientDisconnectReason + { + NormalDisconnection = 0, + DisconnectWithWillMessage = 4, + UnspecifiedError = 128, + MalformedPacket = 129, + ProtocolError = 130, + ImplementationSpecificError = 131, + NotAuthorized = 135, + ServerBusy = 137, + ServerShuttingDown = 139, + BadAuthenticationMethod = 140, + KeepAliveTimeout = 141, + SessionTakenOver = 142, + TopicFilterInvalid = 143, + TopicNameInvalid = 144, + ReceiveMaximumExceeded = 147, + TopicAliasInvalid = 148, + PacketTooLarge = 149, + MessageRateTooHigh = 150, + QuotaExceeded = 151, + AdministrativeAction = 152, + PayloadFormatInvalid = 153, + RetainNotSupported = 154, + QosNotSupported = 155, + UseAnotherServer = 156, + ServerMoved = 157, + SharedSubscriptionsNotSupported = 158, + ConnectionRateExceeded = 159, + MaximumConnectTime = 160, + SubscriptionIdentifiersNotSupported = 161, + WildcardSubscriptionsNotSupported = 162 + } +} diff --git a/Source/BPA.MQTTnet/Client/Disconnecting/MqttClientDisconnectedEventArgs.cs b/Source/BPA.MQTTnet/Client/Disconnecting/MqttClientDisconnectedEventArgs.cs new file mode 100644 index 0000000..009e020 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Disconnecting/MqttClientDisconnectedEventArgs.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace MQTTnet.Client +{ + public sealed class MqttClientDisconnectedEventArgs : EventArgs + { + public bool ClientWasConnected { get; internal set; } + + public Exception Exception { get; internal set; } + + /// + /// Gets the authentication result. + /// Hint: MQTT 5 feature only. + /// + public MqttClientConnectResult ConnectResult { get; internal set; } + + /// + /// Gets or sets the reason. + /// Hint: MQTT 5 feature only. + /// + public MqttClientDisconnectReason Reason { get; internal set; } + + public string ReasonString { get; internal set; } + } +} diff --git a/Source/BPA.MQTTnet/Client/ExtendedAuthenticationExchange/IMqttExtendedAuthenticationExchangeHandler.cs b/Source/BPA.MQTTnet/Client/ExtendedAuthenticationExchange/IMqttExtendedAuthenticationExchangeHandler.cs new file mode 100644 index 0000000..8b66334 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/ExtendedAuthenticationExchange/IMqttExtendedAuthenticationExchangeHandler.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. + +using System.Threading.Tasks; + +namespace MQTTnet.Client +{ + public interface IMqttExtendedAuthenticationExchangeHandler + { + Task HandleRequestAsync(MqttExtendedAuthenticationExchangeContext context); + } +} diff --git a/Source/BPA.MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeContext.cs b/Source/BPA.MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeContext.cs new file mode 100644 index 0000000..c406b5e --- /dev/null +++ b/Source/BPA.MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeContext.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 System.Collections.Generic; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Client +{ + public class MqttExtendedAuthenticationExchangeContext + { + public MqttExtendedAuthenticationExchangeContext(MqttAuthPacket authPacket, MqttClient client) + { + if (authPacket == null) throw new ArgumentNullException(nameof(authPacket)); + + ReasonCode = authPacket.ReasonCode; + ReasonString = authPacket.ReasonString; + AuthenticationMethod = authPacket.AuthenticationMethod; + AuthenticationData = authPacket.AuthenticationData; + UserProperties = authPacket.UserProperties; + + Client = client ?? throw new ArgumentNullException(nameof(client)); + } + + /// + /// Gets the reason code. + /// Hint: MQTT 5 feature only. + /// + public MqttAuthenticateReasonCode ReasonCode { get; } + + /// + /// Gets the reason string. + /// Hint: MQTT 5 feature only. + /// + public string ReasonString { get; } + + /// + /// Gets the authentication method. + /// Hint: MQTT 5 feature only. + /// + public string AuthenticationMethod { get; } + + /// + /// Gets the authentication data. + /// Hint: MQTT 5 feature only. + /// + public byte[] AuthenticationData { get; } + + /// + /// Gets 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 { get; } + + public MqttClient Client { get; } + } +} diff --git a/Source/BPA.MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeData.cs b/Source/BPA.MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeData.cs new file mode 100644 index 0000000..74f1464 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/ExtendedAuthenticationExchange/MqttExtendedAuthenticationExchangeData.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 System.Collections.Generic; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Client +{ + public class MqttExtendedAuthenticationExchangeData + { + /// + /// Gets or sets the reason code. + /// Hint: MQTT 5 feature only. + /// + public MqttAuthenticateReasonCode ReasonCode { get; set; } + + /// + /// Gets or sets the reason string. + /// Hint: MQTT 5 feature only. + /// + public string ReasonString { get; set; } + + /// + /// Gets or sets the authentication data. + /// Authentication data is binary information used to transmit multiple iterations of cryptographic secrets of protocol steps. + /// The content of the authentication data is highly dependent on the specific implementation of the authentication method. + /// Hint: MQTT 5 feature only. + /// + public byte[] AuthenticationData { 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. + /// + public List UserProperties { get; } + } +} diff --git a/Source/BPA.MQTTnet/Client/MqttClient.cs b/Source/BPA.MQTTnet/Client/MqttClient.cs new file mode 100644 index 0000000..d231d87 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/MqttClient.cs @@ -0,0 +1,910 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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.Exceptions; +using MQTTnet.Internal; +using MQTTnet.PacketDispatcher; +using MQTTnet.Packets; +using MQTTnet.Protocol; +using System; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Diagnostics; +using MQTTnet.Formatter; +using MQTTnet.Implementations; + +namespace MQTTnet.Client +{ + 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; + Task _packetReceiverTask; + Task _keepAlivePacketsSenderTask; + Task _publishPacketReceiverTask; + + AsyncQueue _publishPacketReceiverQueue; + + IMqttChannelAdapter _adapter; + bool _cleanDisconnectInitiated; + volatile int _connectionStatus; + + MqttClientDisconnectReason _disconnectReason; + + DateTime _lastPacketSentTimestamp; + string _disconnectReasonString; + + public MqttClient(IMqttClientAdapterFactory channelFactory, IMqttNetLogger logger) + { + _adapterFactory = channelFactory ?? throw new ArgumentNullException(nameof(channelFactory)); + _rootLogger = logger ?? throw new ArgumentNullException(nameof(logger)); + _logger = logger.WithSource(nameof(MqttClient)); + } + + 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 MqttClientOptions Options { get; private set; } + + 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."); + + ThrowIfConnected("It is not allowed to connect with a server after the connection is established."); + + ThrowIfDisposed(); + + if (CompareExchangeConnectionStatus(MqttClientConnectionStatus.Connecting, MqttClientConnectionStatus.Disconnected) != MqttClientConnectionStatus.Disconnected) + { + throw new InvalidOperationException("Not allowed to connect while connect/disconnect is pending."); + } + + MqttClientConnectResult connectResult = null; + + try + { + 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, new MqttPacketInspector(_inspectPacketEvent, _rootLogger), _rootLogger); + _adapter = adapter; + + using (var effectiveCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(backgroundCancellationToken, cancellationToken)) + { + _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(options, effectiveCancellationToken.Token).ConfigureAwait(false); + } + + _lastPacketSentTimestamp = DateTime.UtcNow; + + 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); + } + + CompareExchangeConnectionStatus(MqttClientConnectionStatus.Connected, MqttClientConnectionStatus.Connecting); + + _logger.Info("Connected."); + + if (_connectedEvent.HasHandlers) + { + var eventArgs = new MqttClientConnectedEventArgs(connectResult); + await _connectedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + } + + return connectResult; + } + catch (Exception exception) + { + _disconnectReason = MqttClientDisconnectReason.UnspecifiedError; + + _logger.Error(exception, "Error while connecting with server."); + + await DisconnectInternalAsync(null, exception, connectResult).ConfigureAwait(false); + + throw; + } + } + + public async Task DisconnectAsync(MqttClientDisconnectOptions options, CancellationToken cancellationToken = default) + { + if (options is null) throw new ArgumentNullException(nameof(options)); + + ThrowIfDisposed(); + + var clientWasConnected = IsConnected; + if (DisconnectIsPendingOrFinished()) + { + return; + } + + try + { + _disconnectReason = MqttClientDisconnectReason.NormalDisconnection; + _cleanDisconnectInitiated = true; + + if (clientWasConnected) + { + var disconnectPacket = _packetFactories.Disconnect.Create(options); + await SendAsync(disconnectPacket, cancellationToken).ConfigureAwait(false); + } + } + finally + { + await DisconnectCoreAsync(null, null, null, clientWasConnected).ConfigureAwait(false); + } + } + + public Task PingAsync(CancellationToken cancellationToken = default) + { + return SendAndReceiveAsync(MqttPingReqPacket.Instance, cancellationToken); + } + + public Task SendExtendedAuthenticationExchangeDataAsync(MqttExtendedAuthenticationExchangeData data, CancellationToken cancellationToken = default) + { + if (data == null) throw new ArgumentNullException(nameof(data)); + + ThrowIfDisposed(); + ThrowIfNotConnected(); + + var authPacket = new MqttAuthPacket + { + // 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 = default) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + + foreach (var topicFilter in options.TopicFilters) + { + MqttTopicValidator.ThrowIfInvalidSubscribe(topicFilter.Topic); + } + + ThrowIfDisposed(); + ThrowIfNotConnected(); + + var subscribePacket = _packetFactories.Subscribe.Create(options); + subscribePacket.PacketIdentifier = _packetIdentifierProvider.GetNextPacketIdentifier(); + + var subAckPacket = await SendAndReceiveAsync(subscribePacket, cancellationToken).ConfigureAwait(false); + + return _clientSubscribeResultFactory.Create(subscribePacket, subAckPacket); + } + + public async Task UnsubscribeAsync(MqttClientUnsubscribeOptions options, CancellationToken cancellationToken = default) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + + ThrowIfDisposed(); + ThrowIfNotConnected(); + + var unsubscribePacket = _packetFactories.Unsubscribe.Create(options); + unsubscribePacket.PacketIdentifier = _packetIdentifierProvider.GetNextPacketIdentifier(); + + var unsubAckPacket = await SendAndReceiveAsync(unsubscribePacket, cancellationToken).ConfigureAwait(false); + + return _clientUnsubscribeResultFactory.Create(unsubscribePacket, unsubAckPacket); + } + + public Task PublishAsync(MqttApplicationMessage applicationMessage, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + MqttTopicValidator.ThrowIfInvalid(applicationMessage); + + ThrowIfDisposed(); + ThrowIfNotConnected(); + + var publishPacket = _packetFactories.Publish.Create(applicationMessage); + + switch (applicationMessage.QualityOfServiceLevel) + { + case MqttQualityOfServiceLevel.AtMostOnce: + { + return PublishAtMostOnce(publishPacket, cancellationToken); + } + case MqttQualityOfServiceLevel.AtLeastOnce: + { + return PublishAtLeastOnceAsync(publishPacket, cancellationToken); + } + case MqttQualityOfServiceLevel.ExactlyOnce: + { + return PublishExactlyOnceAsync(publishPacket, cancellationToken); + } + default: + { + throw new NotSupportedException(); + } + } + } + + void Cleanup() + { + try + { + _backgroundCancellationTokenSource?.Cancel(false); + } + finally + { + _backgroundCancellationTokenSource?.Dispose(); + _backgroundCancellationTokenSource = null; + + _publishPacketReceiverQueue?.Dispose(); + _publishPacketReceiverQueue = null; + + _adapter?.Dispose(); + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + Cleanup(); + } + + base.Dispose(disposing); + } + + async Task AuthenticateAsync(MqttClientOptions options, CancellationToken cancellationToken) + { + MqttClientConnectResult result; + + try + { + var connectPacket = _packetFactories.Connect.Create(options); + + var connAckPacket = await SendAndReceiveAsync(connectPacket, cancellationToken).ConfigureAwait(false); + + var clientConnectResultFactory = new MqttClientConnectResultFactory(); + result = clientConnectResultFactory.Create(connAckPacket, options.ProtocolVersion); + } + catch (Exception exception) + { + throw new MqttConnectingFailedException($"Error while authenticating. {exception.Message}", exception, null); + } + + if (result.ResultCode != MqttClientConnectResultCode.Success) + { + throw new MqttConnectingFailedException($"Connecting with MQTT server failed ({result.ResultCode}).", null, result); + } + + _logger.Verbose("Authenticated MQTT connection with server established."); + + return result; + } + + void ThrowIfNotConnected() + { + if (!IsConnected) + { + throw new MqttCommunicationException("The client is not connected."); + } + } + + void ThrowIfConnected(string message) + { + if (IsConnected) + { + throw new MqttProtocolViolationException(message); + } + } + + Task DisconnectInternalAsync(Task sender, Exception exception, MqttClientConnectResult connectResult) + { + var clientWasConnected = IsConnected; + + if (!DisconnectIsPendingOrFinished()) + { + return DisconnectCoreAsync(sender, exception, connectResult, clientWasConnected); + } + + return PlatformAbstractionLayer.CompletedTask; + } + + async Task DisconnectCoreAsync(Task sender, Exception exception, MqttClientConnectResult connectResult, bool clientWasConnected) + { + TryInitiateDisconnect(); + + try + { + if (_adapter != null) + { + _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."); + } + catch (Exception adapterException) + { + _logger.Warning(adapterException, "Error while disconnecting from adapter."); + } + + try + { + var receiverTask = WaitForTaskAsync(_packetReceiverTask, sender); + var publishPacketReceiverTask = WaitForTaskAsync(_publishPacketReceiverTask, sender); + var keepAliveTask = WaitForTaskAsync(_keepAlivePacketsSenderTask, sender); + + await Task.WhenAll(receiverTask, publishPacketReceiverTask, keepAliveTask).ConfigureAwait(false); + } + catch (Exception innerException) + { + _logger.Warning(innerException, "Error while waiting for internal tasks."); + } + finally + { + Cleanup(); + _cleanDisconnectInitiated = false; + CompareExchangeConnectionStatus(MqttClientConnectionStatus.Disconnected, MqttClientConnectionStatus.Disconnecting); + + _logger.Info("Disconnected."); + + var eventArgs = new MqttClientDisconnectedEventArgs + { + 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); + } + } + + void TryInitiateDisconnect() + { + lock (_disconnectLock) + { + try + { + _backgroundCancellationTokenSource?.Cancel(false); + } + catch (Exception exception) + { + _logger.Warning(exception, "Error while initiating disconnect."); + } + } + } + + Task SendAsync(MqttPacket packet, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + _lastPacketSentTimestamp = DateTime.UtcNow; + + return _adapter.SendPacketAsync(packet, cancellationToken); + } + + async Task SendAndReceiveAsync(MqttPacket requestPacket, CancellationToken cancellationToken) where TResponsePacket : MqttPacket + { + cancellationToken.ThrowIfCancellationRequested(); + + ushort packetIdentifier = 0; + if (requestPacket is MqttPacketWithIdentifier packetWithIdentifier) + { + packetIdentifier = packetWithIdentifier.PacketIdentifier; + } + + using (var packetAwaitable = _packetDispatcher.AddAwaitable(packetIdentifier)) + { + try + { + await SendAsync(requestPacket, cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) + { + _logger.Warning(exception, "Error when sending request packet ({0}).", requestPacket.GetType().Name); + packetAwaitable.Fail(exception); + } + + try + { + return await packetAwaitable.WaitOneAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) + { + if (exception is MqttCommunicationTimedOutException) + { + _logger.Warning("Timeout while waiting for response packet ({0}).", typeof(TResponsePacket).Name); + } + + throw; + } + } + } + + async Task TrySendKeepAliveMessagesAsync(CancellationToken cancellationToken) + { + try + { + _logger.Verbose("Start sending keep alive packets."); + + var keepAlivePeriod = Options.KeepAlivePeriod; + + while (!cancellationToken.IsCancellationRequested) + { + // Values described here: [MQTT-3.1.2-24]. + var timeWithoutPacketSent = DateTime.UtcNow - _lastPacketSentTimestamp; + + if (timeWithoutPacketSent > keepAlivePeriod) + { + await PingAsync(cancellationToken).ConfigureAwait(false); + } + + // Wait a fixed time in all cases. Calculation of the remaining time is complicated + // due to some edge cases and was buggy in the past. Now we wait several ms because the + // min keep alive value is one second so that the server will wait 1.5 seconds for a PING + // packet. + await Task.Delay(250, cancellationToken).ConfigureAwait(false); + } + } + catch (Exception exception) + { + if (_cleanDisconnectInitiated) + { + return; + } + + if (exception is OperationCanceledException) + { + return; + } + else if (exception is MqttCommunicationException) + { + _logger.Warning(exception, "Communication error while sending/receiving keep alive packets."); + } + else + { + _logger.Error(exception, "Error exception while sending/receiving keep alive packets."); + } + + await DisconnectInternalAsync(_keepAlivePacketsSenderTask, exception, null).ConfigureAwait(false); + } + finally + { + _logger.Verbose("Stopped sending keep alive packets."); + } + } + + async Task TryReceivePacketsAsync(CancellationToken cancellationToken) + { + try + { + _logger.Verbose("Start receiving packets."); + + while (!cancellationToken.IsCancellationRequested) + { + MqttPacket packet; + var packetTask = _adapter.ReceivePacketAsync(cancellationToken); + + if (packetTask.IsCompleted) + { + packet = packetTask.Result; + } + else + { + packet = await packetTask.ConfigureAwait(false); + } + + if (cancellationToken.IsCancellationRequested) + { + return; + } + + if (packet == null) + { + await DisconnectInternalAsync(_packetReceiverTask, null, null).ConfigureAwait(false); + + return; + } + + await TryProcessReceivedPacketAsync(packet, cancellationToken).ConfigureAwait(false); + } + } + catch (Exception exception) + { + if (_cleanDisconnectInitiated) + { + return; + } + + if (exception is OperationCanceledException) + { + } + else if (exception is MqttCommunicationException) + { + _logger.Warning(exception, "Communication error while receiving packets."); + } + else + { + _logger.Error(exception, "Error while receiving packets."); + } + + _packetDispatcher.FailAll(exception); + + await DisconnectInternalAsync(_packetReceiverTask, exception, null).ConfigureAwait(false); + } + finally + { + _logger.Verbose("Stopped receiving packets."); + } + } + + async Task TryProcessReceivedPacketAsync(MqttPacket packet, CancellationToken cancellationToken) + { + try + { + if (packet is MqttPublishPacket publishPacket) + { + EnqueueReceivedPublishPacket(publishPacket); + } + else if (packet is MqttPubRecPacket pubRecPacket) + { + await ProcessReceivedPubRecPacket(pubRecPacket, cancellationToken).ConfigureAwait(false); + } + else if (packet is MqttPubRelPacket pubRelPacket) + { + await ProcessReceivedPubRelPacket(pubRelPacket, cancellationToken).ConfigureAwait(false); + } + else if (packet is MqttDisconnectPacket disconnectPacket) + { + await ProcessReceivedDisconnectPacket(disconnectPacket).ConfigureAwait(false); + } + else if (packet is MqttAuthPacket authPacket) + { + await ProcessReceivedAuthPacket(authPacket).ConfigureAwait(false); + } + else if (packet is MqttPingRespPacket) + { + _packetDispatcher.TryDispatch(packet); + } + else if (packet is MqttPingReqPacket) + { + throw new MqttProtocolViolationException("The PINGREQ Packet is sent from a Client to the Server only."); + } + else + { + if (!_packetDispatcher.TryDispatch(packet)) + { + throw new MqttProtocolViolationException($"Received packet '{packet}' at an unexpected time."); + } + } + } + catch (Exception exception) + { + if (_cleanDisconnectInitiated) + { + return; + } + + if (exception is OperationCanceledException) + { + } + else if (exception is MqttCommunicationException) + { + _logger.Warning(exception, "Communication error while receiving packets."); + } + else + { + _logger.Error(exception, "Error while receiving packets."); + } + + _packetDispatcher.FailAll(exception); + + await DisconnectInternalAsync(_packetReceiverTask, exception, null).ConfigureAwait(false); + } + } + + void EnqueueReceivedPublishPacket(MqttPublishPacket publishPacket) + { + try + { + _publishPacketReceiverQueue.Enqueue(publishPacket); + } + catch (Exception exception) + { + _logger.Error(exception, "Error while queueing application message."); + } + } + + async Task ProcessReceivedPublishPackets(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + var publishPacketDequeueResult = await _publishPacketReceiverQueue.TryDequeueAsync(cancellationToken).ConfigureAwait(false); + if (!publishPacketDequeueResult.IsSuccess) + { + return; + } + + var publishPacket = publishPacketDequeueResult.Item; + var eventArgs = await HandleReceivedApplicationMessageAsync(publishPacket).ConfigureAwait(false); + + if (eventArgs.AutoAcknowledge) + { + await eventArgs.AcknowledgeAsync(cancellationToken).ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + } + catch (Exception exception) + { + _logger.Error(exception, "Error while handling application message."); + } + } + } + + Task AcknowledgeReceivedPublishPacket(MqttApplicationMessageReceivedEventArgs eventArgs, CancellationToken cancellationToken) + { + if (eventArgs.PublishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce) + { + // no response required + } + else if (eventArgs.PublishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtLeastOnce) + { + if (!eventArgs.ProcessingFailed) + { + var pubAckPacket = _packetFactories.PubAck.Create(eventArgs); + return SendAsync(pubAckPacket, cancellationToken); + } + } + else if (eventArgs.PublishPacket.QualityOfServiceLevel == MqttQualityOfServiceLevel.ExactlyOnce) + { + if (!eventArgs.ProcessingFailed) + { + var pubRecPacket = _packetFactories.PubRec.Create(eventArgs); + return SendAsync(pubRecPacket, cancellationToken); + } + } + else + { + throw new MqttProtocolViolationException("Received a not supported QoS level."); + } + + return PlatformAbstractionLayer.CompletedTask; + } + + Task ProcessReceivedPubRecPacket(MqttPubRecPacket pubRecPacket, CancellationToken cancellationToken) + { + if (!_packetDispatcher.TryDispatch(pubRecPacket)) + { + // 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 = _packetFactories.PubRel.Create(pubRecPacket, MqttApplicationMessageReceivedReasonCode.PacketIdentifierNotFound); + return SendAsync(pubRelPacket, cancellationToken); + } + + return PlatformAbstractionLayer.CompletedTask; + } + + Task ProcessReceivedPubRelPacket(MqttPubRelPacket pubRelPacket, CancellationToken cancellationToken) + { + var pubCompPacket = _packetFactories.PubComp.Create(pubRelPacket, MqttApplicationMessageReceivedReasonCode.Success); + return SendAsync(pubCompPacket, cancellationToken); + } + + Task ProcessReceivedDisconnectPacket(MqttDisconnectPacket disconnectPacket) + { + _disconnectReason = (MqttClientDisconnectReason) disconnectPacket.ReasonCode; + _disconnectReasonString = disconnectPacket.ReasonString; + + // Also dispatch disconnect to waiting threads to generate a proper exception. + _packetDispatcher.FailAll(new MqttUnexpectedDisconnectReceivedException(disconnectPacket)); + + return DisconnectInternalAsync(_packetReceiverTask, null, null); + } + + Task ProcessReceivedAuthPacket(MqttAuthPacket authPacket) + { + var extendedAuthenticationExchangeHandler = Options.ExtendedAuthenticationExchangeHandler; + if (extendedAuthenticationExchangeHandler != null) + { + return extendedAuthenticationExchangeHandler.HandleRequestAsync(new MqttExtendedAuthenticationExchangeContext(authPacket, this)); + } + + return PlatformAbstractionLayer.CompletedTask; + } + + 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 _clientPublishResultFactory.Create(null); + } + + async Task PublishAtLeastOnceAsync(MqttPublishPacket publishPacket, CancellationToken cancellationToken) + { + publishPacket.PacketIdentifier = _packetIdentifierProvider.GetNextPacketIdentifier(); + + var pubAckPacket = await SendAndReceiveAsync(publishPacket, cancellationToken).ConfigureAwait(false); + + return _clientPublishResultFactory.Create(pubAckPacket); + } + + async Task PublishExactlyOnceAsync(MqttPublishPacket publishPacket, CancellationToken cancellationToken) + { + publishPacket.PacketIdentifier = _packetIdentifierProvider.GetNextPacketIdentifier(); + + var pubRecPacket = await SendAndReceiveAsync(publishPacket, cancellationToken).ConfigureAwait(false); + + var pubRelPacket = _packetFactories.PubRel.Create(pubRecPacket, MqttApplicationMessageReceivedReasonCode.Success); + + var pubCompPacket = await SendAndReceiveAsync(pubRelPacket, cancellationToken).ConfigureAwait(false); + + return _clientPublishResultFactory.Create(pubRecPacket, pubCompPacket); + } + + async Task HandleReceivedApplicationMessageAsync(MqttPublishPacket publishPacket) + { + var applicationMessage = _applicationMessageFactory.Create(publishPacket); + var eventArgs = new MqttApplicationMessageReceivedEventArgs(Options.ClientId, applicationMessage, publishPacket, AcknowledgeReceivedPublishPacket); + + await _applicationMessageReceivedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + + return eventArgs; + } + + async Task WaitForTaskAsync(Task task, Task sender) + { + if (task == null) + { + return; + } + + if (task == sender) + { + // Return here to avoid deadlocks, but first any eventual exception in the task + // must be handled to avoid not getting an unhandled task exception + if (!task.IsFaulted) + { + return; + } + + // By accessing the Exception property the exception is considered handled and will + // not result in an unhandled task exception later by the finalizer + _logger.Warning(task.Exception, "Error while waiting for background task."); + return; + } + + try + { + await task.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + } + + bool DisconnectIsPendingOrFinished() + { + var connectionStatus = (MqttClientConnectionStatus)_connectionStatus; + + do + { + switch (connectionStatus) + { + case MqttClientConnectionStatus.Disconnected: + case MqttClientConnectionStatus.Disconnecting: + return true; + case MqttClientConnectionStatus.Connected: + case MqttClientConnectionStatus.Connecting: + // This will compare the _connectionStatus to old value and set it to "MqttClientConnectionStatus.Disconnecting" afterwards. + // So the first caller will get a "false" and all subsequent ones will get "true". + var curStatus = CompareExchangeConnectionStatus(MqttClientConnectionStatus.Disconnecting, connectionStatus); + if (curStatus == connectionStatus) + { + return false; + } + + connectionStatus = curStatus; + break; + } + } while (true); + } + + MqttClientConnectionStatus CompareExchangeConnectionStatus(MqttClientConnectionStatus value, MqttClientConnectionStatus comparand) + { + return (MqttClientConnectionStatus)Interlocked.CompareExchange(ref _connectionStatus, (int)value, (int)comparand); + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Client/MqttClientConnectionStatus.cs b/Source/BPA.MQTTnet/Client/MqttClientConnectionStatus.cs new file mode 100644 index 0000000..65833ae --- /dev/null +++ b/Source/BPA.MQTTnet/Client/MqttClientConnectionStatus.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.Client +{ + public enum MqttClientConnectionStatus + { + Disconnected, + Disconnecting, + Connected, + Connecting + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Client/MqttClientExtensions.cs b/Source/BPA.MQTTnet/Client/MqttClientExtensions.cs new file mode 100644 index 0000000..b48dac5 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/MqttClientExtensions.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.Text; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Client +{ + public static class MqttClientExtensions + { + public static Task DisconnectAsync(this MqttClient client, MqttClientDisconnectReason reason = MqttClientDisconnectReason.NormalDisconnection, string reasonString = null) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + return client.DisconnectAsync( + new MqttClientDisconnectOptions + { + Reason = reason, + ReasonString = reasonString + }, + CancellationToken.None); + } + + public static Task PublishBinaryAsync( + this MqttClient mqttClient, + string topic, + IEnumerable payload = null, + MqttQualityOfServiceLevel qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce, + bool retain = false, + CancellationToken cancellationToken = default) + { + if (mqttClient == null) + { + throw new ArgumentNullException(nameof(mqttClient)); + } + + if (topic == null) + { + throw new ArgumentNullException(nameof(topic)); + } + + var applicationMessage = new MqttApplicationMessageBuilder().WithTopic(topic) + .WithPayload(payload) + .WithRetainFlag(retain) + .WithQualityOfServiceLevel(qualityOfServiceLevel) + .Build(); + + return mqttClient.PublishAsync(applicationMessage, cancellationToken); + } + + public static Task PublishStringAsync( + this MqttClient mqttClient, + string topic, + string payload = null, + MqttQualityOfServiceLevel qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce, + bool retain = false, + CancellationToken cancellationToken = default) + { + var payloadBuffer = Encoding.UTF8.GetBytes(payload ?? string.Empty); + return mqttClient.PublishBinaryAsync(topic, payloadBuffer, qualityOfServiceLevel, retain, cancellationToken); + } + + public static Task SendExtendedAuthenticationExchangeDataAsync(this MqttClient client, MqttExtendedAuthenticationExchangeData data) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + return client.SendExtendedAuthenticationExchangeDataAsync(data, CancellationToken.None); + } + + public static Task SubscribeAsync(this MqttClient mqttClient, MqttTopicFilter topicFilter, CancellationToken cancellationToken = default) + { + if (mqttClient == null) + { + throw new ArgumentNullException(nameof(mqttClient)); + } + + if (topicFilter == null) + { + throw new ArgumentNullException(nameof(topicFilter)); + } + + var subscribeOptions = new MqttClientSubscribeOptionsBuilder().WithTopicFilter(topicFilter) + .Build(); + + return mqttClient.SubscribeAsync(subscribeOptions, cancellationToken); + } + + public static Task SubscribeAsync( + this MqttClient mqttClient, + string topic, + MqttQualityOfServiceLevel qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce, + CancellationToken cancellationToken = default) + { + if (mqttClient == null) + { + throw new ArgumentNullException(nameof(mqttClient)); + } + + if (topic == null) + { + throw new ArgumentNullException(nameof(topic)); + } + + var subscribeOptions = new MqttClientSubscribeOptionsBuilder().WithTopicFilter(topic, qualityOfServiceLevel) + .Build(); + + return mqttClient.SubscribeAsync(subscribeOptions, cancellationToken); + } + + public static Task UnsubscribeAsync(this MqttClient mqttClient, string topic, CancellationToken cancellationToken = default) + { + if (mqttClient == null) + { + throw new ArgumentNullException(nameof(mqttClient)); + } + + if (topic == null) + { + throw new ArgumentNullException(nameof(topic)); + } + + var unsubscribeOptions = new MqttClientUnsubscribeOptionsBuilder().WithTopicFilter(topic) + .Build(); + + return mqttClient.UnsubscribeAsync(unsubscribeOptions, cancellationToken); + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Client/MqttPacketIdentifierProvider.cs b/Source/BPA.MQTTnet/Client/MqttPacketIdentifierProvider.cs new file mode 100644 index 0000000..21cde64 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/MqttPacketIdentifierProvider.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. + +namespace MQTTnet.Client +{ + public sealed class MqttPacketIdentifierProvider + { + readonly object _syncRoot = new object(); + + ushort _value; + + public void Reset() + { + lock (_syncRoot) + { + _value = 0; + } + } + + public ushort GetNextPacketIdentifier() + { + lock (_syncRoot) + { + _value++; + + if (_value == 0) + { + // As per official MQTT documentation the package identifier should never be 0. + _value = 1; + } + + return _value; + } + } + } +} diff --git a/Source/BPA.MQTTnet/Client/Options/IMqttClientChannelOptions.cs b/Source/BPA.MQTTnet/Client/Options/IMqttClientChannelOptions.cs new file mode 100644 index 0000000..11ecf16 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Options/IMqttClientChannelOptions.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.Client +{ + public interface IMqttClientChannelOptions + { + MqttClientTlsOptions TlsOptions { get; } + } +} diff --git a/Source/BPA.MQTTnet/Client/Options/IMqttClientCredentialsProvider.cs b/Source/BPA.MQTTnet/Client/Options/IMqttClientCredentialsProvider.cs new file mode 100644 index 0000000..382f91e --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Client/Options/MqttClientCertificateValidationEventArgs.cs b/Source/BPA.MQTTnet/Client/Options/MqttClientCertificateValidationEventArgs.cs new file mode 100644 index 0000000..f1842cb --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Client/Options/MqttClientCredentials.cs b/Source/BPA.MQTTnet/Client/Options/MqttClientCredentials.cs new file mode 100644 index 0000000..b2d50a9 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Options/MqttClientCredentials.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. + +namespace MQTTnet.Client +{ + public sealed class MqttClientCredentials : IMqttClientCredentialsProvider + { + 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[] GetPassword(MqttClientOptions clientOptions) + { + return _password; + } + } +} diff --git a/Source/BPA.MQTTnet/Client/Options/MqttClientDefaultCertificateValidationHandler.cs b/Source/BPA.MQTTnet/Client/Options/MqttClientDefaultCertificateValidationHandler.cs new file mode 100644 index 0000000..ffc4c08 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Client/Options/MqttClientOptions.cs b/Source/BPA.MQTTnet/Client/Options/MqttClientOptions.cs new file mode 100644 index 0000000..ce5aed5 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Options/MqttClientOptions.cs @@ -0,0 +1,182 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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.Formatter; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Client +{ + public sealed class MqttClientOptions + { + /// + /// Gets or sets the authentication data. + /// Hint: MQTT 5 feature only. + /// + 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. + /// + public bool CleanSession { get; set; } = true; + + /// + /// 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; + + /// + /// Gets or sets the receive maximum. + /// This gives the maximum length of the receive messages. + /// + public ushort ReceiveMaximum { get; set; } + + /// + /// Gets or sets the request problem information. + /// Hint: MQTT 5 feature only. + /// + public bool RequestProblemInformation { get; set; } = true; + + /// + /// Gets or sets the request response information. + /// Hint: MQTT 5 feature only. + /// + public bool RequestResponseInformation { get; set; } + + /// + /// Gets or sets the session expiry interval. + /// The time after a session expires when it's not actively used. + /// + public uint SessionExpiryInterval { get; set; } + + /// + /// 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 TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(100); + + /// + /// Gets or sets the topic alias maximum. + /// This gives the maximum length of the topic alias. + /// + public ushort TopicAliasMaximum { 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. + /// + public List UserProperties { get; set; } + + /// + /// Gets or sets the content type of the will message. + /// + public string WillContentType { get; set; } + + /// + /// Gets or sets the correlation data of the will message. + /// + public byte[] WillCorrelationData { 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. + /// + public uint WillDelayInterval { get; set; } + + /// + /// Gets or sets the message expiry interval of the will message. + /// + public uint WillMessageExpiryInterval { get; set; } + + /// + /// Gets or sets the payload of the will message. + /// + public byte[] WillPayload { get; set; } + + /// + /// Gets or sets the payload format indicator of the will message. + /// + public MqttPayloadFormatIndicator WillPayloadFormatIndicator { get; set; } + + /// + /// Gets or sets the QoS level of the will message. + /// + public MqttQualityOfServiceLevel WillQualityOfServiceLevel { get; set; } + + /// + /// Gets or sets the response topic of the will message. + /// + public string WillResponseTopic { 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/BPA.MQTTnet/Client/Options/MqttClientOptionsBuilder.cs b/Source/BPA.MQTTnet/Client/Options/MqttClientOptionsBuilder.cs new file mode 100644 index 0000000..4896d2b --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Options/MqttClientOptionsBuilder.cs @@ -0,0 +1,395 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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.Formatter; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Client +{ + public sealed class MqttClientOptionsBuilder + { + readonly MqttClientOptions _options = new MqttClientOptions(); + MqttClientWebSocketProxyOptions _proxyOptions; + + MqttClientTcpOptions _tcpOptions; + MqttClientOptionsBuilderTlsParameters _tlsParameters; + MqttClientWebSocketOptions _webSocketOptions; + + public MqttClientOptions Build() + { + 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, + 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 WithAuthentication(string method, byte[] data) + { + _options.AuthenticationMethod = method; + _options.AuthenticationData = data; + return this; + } + + public MqttClientOptionsBuilder WithCleanSession(bool value = true) + { + _options.CleanSession = value; + return this; + } + + public MqttClientOptionsBuilder WithClientId(string value) + { + _options.ClientId = value; + return this; + } + + /// + /// 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.Timeout = value; + return this; + } + + public MqttClientOptionsBuilder WithConnectionUri(Uri uri) + { + 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); + } + + return this; + } + + public MqttClientOptionsBuilder WithConnectionUri(string uri) + { + return WithConnectionUri(new Uri(uri, UriKind.Absolute)); + } + + public MqttClientOptionsBuilder WithCredentials(string username, string password) + { + byte[] passwordBuffer = null; + + if (password != null) + { + passwordBuffer = Encoding.UTF8.GetBytes(password); + } + + return WithCredentials(username, passwordBuffer); + } + + public MqttClientOptionsBuilder WithCredentials(string username, byte[] password = null) + { + return WithCredentials(new MqttClientCredentials(username, password)); + } + + public MqttClientOptionsBuilder WithCredentials(IMqttClientCredentialsProvider credentials) + { + _options.Credentials = credentials; + return this; + } + + public MqttClientOptionsBuilder WithExtendedAuthenticationExchangeHandler(IMqttExtendedAuthenticationExchangeHandler handler) + { + _options.ExtendedAuthenticationExchangeHandler = handler; + return this; + } + + public MqttClientOptionsBuilder WithKeepAlivePeriod(TimeSpan value) + { + _options.KeepAlivePeriod = value; + return this; + } + + public MqttClientOptionsBuilder WithMaximumPacketSize(uint maximumPacketSize) + { + _options.MaximumPacketSize = maximumPacketSize; + return this; + } + + public MqttClientOptionsBuilder WithNoKeepAlive() + { + return WithKeepAlivePeriod(TimeSpan.Zero); + } + + public MqttClientOptionsBuilder WithProtocolVersion(MqttProtocolVersion value) + { + if (value == MqttProtocolVersion.Unknown) + { + throw new ArgumentException("Protocol version is invalid."); + } + + _options.ProtocolVersion = value; + return this; + } + + public MqttClientOptionsBuilder WithProxy( + string address, + string username = null, + string password = null, + string domain = null, + bool bypassOnLocal = false, + string[] bypassList = null) + { + _proxyOptions = new MqttClientWebSocketProxyOptions + { + Address = address, + Username = username, + Password = password, + Domain = domain, + BypassOnLocal = bypassOnLocal, + BypassList = bypassList + }; + + return this; + } + + public MqttClientOptionsBuilder WithProxy(Action optionsBuilder) + { + if (optionsBuilder == null) + { + throw new ArgumentNullException(nameof(optionsBuilder)); + } + + _proxyOptions = new MqttClientWebSocketProxyOptions(); + optionsBuilder(_proxyOptions); + return this; + } + + public MqttClientOptionsBuilder WithReceiveMaximum(ushort receiveMaximum) + { + _options.ReceiveMaximum = receiveMaximum; + return this; + } + + public MqttClientOptionsBuilder WithRequestProblemInformation(bool requestProblemInformation = true) + { + _options.RequestProblemInformation = requestProblemInformation; + return this; + } + + public MqttClientOptionsBuilder WithRequestResponseInformation(bool requestResponseInformation = true) + { + _options.RequestResponseInformation = requestResponseInformation; + return this; + } + + public MqttClientOptionsBuilder WithSessionExpiryInterval(uint sessionExpiryInterval) + { + _options.SessionExpiryInterval = sessionExpiryInterval; + return this; + } + + public MqttClientOptionsBuilder WithTcpServer(string server, int? port = null) + { + _tcpOptions = new MqttClientTcpOptions + { + Server = server, + Port = port + }; + + return this; + } + + // TODO: Consider creating _MqttClientTcpOptionsBuilder_ as overload. + public MqttClientOptionsBuilder WithTcpServer(Action optionsBuilder) + { + if (optionsBuilder == null) + { + throw new ArgumentNullException(nameof(optionsBuilder)); + } + + _tcpOptions = new MqttClientTcpOptions(); + optionsBuilder.Invoke(_tcpOptions); + + return this; + } + + public MqttClientOptionsBuilder WithTls(MqttClientOptionsBuilderTlsParameters parameters) + { + _tlsParameters = parameters; + return this; + } + + public MqttClientOptionsBuilder WithTls() + { + return WithTls(new MqttClientOptionsBuilderTlsParameters { UseTls = true }); + } + + public MqttClientOptionsBuilder WithTls(Action optionsBuilder) + { + if (optionsBuilder == null) + { + throw new ArgumentNullException(nameof(optionsBuilder)); + } + + _tlsParameters = new MqttClientOptionsBuilderTlsParameters + { + UseTls = true + }; + + optionsBuilder(_tlsParameters); + return this; + } + + public MqttClientOptionsBuilder WithTopicAliasMaximum(ushort topicAliasMaximum) + { + _options.TopicAliasMaximum = topicAliasMaximum; + return this; + } + + 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; + } + + public MqttClientOptionsBuilder WithWebSocketServer(string uri, MqttClientOptionsBuilderWebSocketParameters parameters = null) + { + _webSocketOptions = new MqttClientWebSocketOptions + { + Uri = uri, + RequestHeaders = parameters?.RequestHeaders, + CookieContainer = parameters?.CookieContainer + }; + + return this; + } + + public MqttClientOptionsBuilder WithWebSocketServer(Action optionsBuilder) + { + if (optionsBuilder == null) + { + throw new ArgumentNullException(nameof(optionsBuilder)); + } + + _webSocketOptions = new MqttClientWebSocketOptions(); + optionsBuilder.Invoke(_webSocketOptions); + + return this; + } + + public MqttClientOptionsBuilder WithWillDelayInterval(uint willDelayInterval) + { + _options.WillDelayInterval = willDelayInterval; + return this; + } + + public MqttClientOptionsBuilder WithWillPayload(byte[] willPayload) + { + _options.WillPayload = willPayload; + return this; + } + + public MqttClientOptionsBuilder WithWillQualityOfServiceLevel(MqttQualityOfServiceLevel willQualityOfServiceLevel) + { + _options.WillQualityOfServiceLevel = willQualityOfServiceLevel; + return this; + } + + public MqttClientOptionsBuilder WithWillTopic(string willTopic) + { + _options.WillTopic = willTopic; + return this; + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Client/Options/MqttClientOptionsBuilderTlsParameters.cs b/Source/BPA.MQTTnet/Client/Options/MqttClientOptionsBuilderTlsParameters.cs new file mode 100644 index 0000000..c065bb7 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Options/MqttClientOptionsBuilderTlsParameters.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 System.Collections.Generic; +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; + +namespace MQTTnet.Client +{ + public sealed class MqttClientOptionsBuilderTlsParameters + { + public bool UseTls { get; set; } + + public Func CertificateValidationHandler { get; set; } + +#if NET48 || NETCOREAPP3_1 || NET5 || NET6 + public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls12 | SslProtocols.Tls13; +#else + public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls12 | (SslProtocols)0x00003000 /*Tls13*/; +#endif + +#if WINDOWS_UWP + public IEnumerable> Certificates { get; set; } +#else + public IEnumerable Certificates { get; set; } +#endif + +#if NETCOREAPP3_1 || NET5_0_OR_GREATER + public List ApplicationProtocols { get;set; } +#endif + + public bool AllowUntrustedCertificates { get; set; } + + public bool IgnoreCertificateChainErrors { get; set; } + + public bool IgnoreCertificateRevocationErrors { get; set; } + } +} diff --git a/Source/BPA.MQTTnet/Client/Options/MqttClientOptionsBuilderWebSocketParameters.cs b/Source/BPA.MQTTnet/Client/Options/MqttClientOptionsBuilderWebSocketParameters.cs new file mode 100644 index 0000000..ba20a10 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Options/MqttClientOptionsBuilderWebSocketParameters.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 System.Net; + +namespace MQTTnet.Client +{ + public class MqttClientOptionsBuilderWebSocketParameters + { + public IDictionary RequestHeaders { get; set; } + + public CookieContainer CookieContainer { get; set; } + } +} diff --git a/Source/BPA.MQTTnet/Client/Options/MqttClientTcpOptions.cs b/Source/BPA.MQTTnet/Client/Options/MqttClientTcpOptions.cs new file mode 100644 index 0000000..479bb1c --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Options/MqttClientTcpOptions.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. + +using System.Net.Sockets; + +namespace MQTTnet.Client +{ + public sealed class MqttClientTcpOptions : IMqttClientChannelOptions + { + 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 int? Port { get; set; } + + public string Server { get; set; } + + public MqttClientTlsOptions TlsOptions { get; set; } = new MqttClientTlsOptions(); + + public override string ToString() + { + return Server + ":" + this.GetPort(); + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Client/Options/MqttClientTcpOptionsExtensions.cs b/Source/BPA.MQTTnet/Client/Options/MqttClientTcpOptionsExtensions.cs new file mode 100644 index 0000000..f214150 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Options/MqttClientTcpOptionsExtensions.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; + +namespace MQTTnet.Client +{ + public static class MqttClientTcpOptionsExtensions + { + public static int GetPort(this MqttClientTcpOptions options) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + + if (options.Port.HasValue) + { + return options.Port.Value; + } + + return !options.TlsOptions.UseTls ? 1883 : 8883; + } + } +} diff --git a/Source/BPA.MQTTnet/Client/Options/MqttClientTlsOptions.cs b/Source/BPA.MQTTnet/Client/Options/MqttClientTlsOptions.cs new file mode 100644 index 0000000..840886c --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Options/MqttClientTlsOptions.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; +using System.Collections.Generic; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; + +namespace MQTTnet.Client +{ + public sealed class MqttClientTlsOptions + { + public Func CertificateValidationHandler { get; set; } + + public bool UseTls { get; set; } + + public bool IgnoreCertificateRevocationErrors { get; set; } + + public bool IgnoreCertificateChainErrors { get; set; } + + public bool AllowUntrustedCertificates { get; set; } + +#if WINDOWS_UWP + public List Certificates { get; set; } +#else + public List Certificates { get; set; } +#endif + +#if NETCOREAPP3_1 || NET5_0_OR_GREATER + public List ApplicationProtocols { get; set; } +#endif + +#if NET48 || NETCOREAPP3_1 || NET5 || NET6 + public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls12 | SslProtocols.Tls13; +#else + public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls12 | (SslProtocols)0x00003000 /*Tls13*/; +#endif + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Client/Options/MqttClientWebSocketOptions.cs b/Source/BPA.MQTTnet/Client/Options/MqttClientWebSocketOptions.cs new file mode 100644 index 0000000..03160a8 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Options/MqttClientWebSocketOptions.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Net; + +namespace MQTTnet.Client +{ + public class MqttClientWebSocketOptions : IMqttClientChannelOptions + { + public string Uri { get; set; } + + public IDictionary RequestHeaders { get; set; } + + public ICollection SubProtocols { get; set; } = new List { "mqtt" }; + + public CookieContainer CookieContainer { get; set; } + + public MqttClientWebSocketProxyOptions ProxyOptions { get; set; } + + public MqttClientTlsOptions TlsOptions { get; set; } = new MqttClientTlsOptions(); + + public override string ToString() + { + return Uri; + } + } +} diff --git a/Source/BPA.MQTTnet/Client/Options/MqttClientWebSocketProxyOptions.cs b/Source/BPA.MQTTnet/Client/Options/MqttClientWebSocketProxyOptions.cs new file mode 100644 index 0000000..17a6e2d --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Options/MqttClientWebSocketProxyOptions.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. + +namespace MQTTnet.Client +{ + public class MqttClientWebSocketProxyOptions + { + public string Address { get; set; } + + public string Username { get; set; } + + public string Password { get; set; } + + public string Domain { get; set; } + + public bool BypassOnLocal { get; set; } + + public string[] BypassList { get; set; } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Client/Publishing/MqttClientPublishReasonCode.cs b/Source/BPA.MQTTnet/Client/Publishing/MqttClientPublishReasonCode.cs new file mode 100644 index 0000000..ee9f6c7 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Publishing/MqttClientPublishReasonCode.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. + +namespace MQTTnet.Client +{ + public enum MqttClientPublishReasonCode + { + Success = 0, + + NoMatchingSubscribers = 16, + UnspecifiedError = 128, + ImplementationSpecificError = 131, + NotAuthorized = 135, + TopicNameInvalid = 144, + PacketIdentifierInUse = 145, + QuotaExceeded = 151, + PayloadFormatInvalid = 153 + } +} diff --git a/Source/BPA.MQTTnet/Client/Publishing/MqttClientPublishResult.cs b/Source/BPA.MQTTnet/Client/Publishing/MqttClientPublishResult.cs new file mode 100644 index 0000000..47d338e --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Publishing/MqttClientPublishResult.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using MQTTnet.Packets; + +namespace MQTTnet.Client +{ + public sealed class MqttClientPublishResult + { + /// + /// Gets the packet identifier which was used for this publish. + /// + public ushort? PacketIdentifier { get; set; } + + /// + /// Gets or sets the reason code. + /// Hint: MQTT 5 feature only. + /// + public MqttClientPublishReasonCode ReasonCode { get; set; } = MqttClientPublishReasonCode.Success; + + /// + /// Gets or sets the reason string. + /// Hint: MQTT 5 feature only. + /// + public string ReasonString { 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. + /// + public IReadOnlyCollection UserProperties { get; internal set; } + } +} diff --git a/Source/BPA.MQTTnet/Client/Publishing/MqttClientPublishResultFactory.cs b/Source/BPA.MQTTnet/Client/Publishing/MqttClientPublishResultFactory.cs new file mode 100644 index 0000000..ea5bc23 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Client/Receiving/MqttApplicationMessageReceivedEventArgs.cs b/Source/BPA.MQTTnet/Client/Receiving/MqttApplicationMessageReceivedEventArgs.cs new file mode 100644 index 0000000..c288ab2 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Receiving/MqttApplicationMessageReceivedEventArgs.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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.Client +{ + public sealed class MqttApplicationMessageReceivedEventArgs : EventArgs + { + readonly Func _acknowledgeHandler; + + int _isAcknowledged; + + public MqttApplicationMessageReceivedEventArgs( + string clientId, + MqttApplicationMessage applicationMessage, + MqttPublishPacket publishPacket, + Func acknowledgeHandler) + { + ClientId = clientId; + ApplicationMessage = applicationMessage ?? throw new ArgumentNullException(nameof(applicationMessage)); + PublishPacket = publishPacket; + _acknowledgeHandler = acknowledgeHandler; + } + + internal MqttPublishPacket PublishPacket { 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; } + + public MqttApplicationMessage ApplicationMessage { get; } + + 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. + /// This value can be used in user code for custom control flow. + /// + public bool IsHandled { get; set; } + + /// + /// Gets ir sets whether the library should send MQTT ACK packets automatically if required. + /// + public bool AutoAcknowledge { get; set; } = true; + + public object Tag { get; set; } + + public Task AcknowledgeAsync(CancellationToken cancellationToken) + { + if (_acknowledgeHandler == null) + { + throw new NotSupportedException("Deferred acknowledgement of application message is not yet supported in MQTTnet server."); + } + + if (Interlocked.CompareExchange(ref _isAcknowledged, 1, 0) == 0) + { + return _acknowledgeHandler(this, cancellationToken); + } + + throw new InvalidOperationException("The application message is already acknowledged."); + } + } +} diff --git a/Source/BPA.MQTTnet/Client/Receiving/MqttApplicationMessageReceivedReasonCode.cs b/Source/BPA.MQTTnet/Client/Receiving/MqttApplicationMessageReceivedReasonCode.cs new file mode 100644 index 0000000..1a00bbd --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Receiving/MqttApplicationMessageReceivedReasonCode.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. + +namespace MQTTnet.Client +{ + public enum MqttApplicationMessageReceivedReasonCode + { + Success = 0, + NoMatchingSubscribers = 16, + UnspecifiedError = 128, + ImplementationSpecificError = 131, + NotAuthorized = 135, + TopicNameInvalid = 144, + PacketIdentifierInUse = 145, + PacketIdentifierNotFound = 146, + QuotaExceeded = 151, + PayloadFormatInvalid = 153 + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Client/Subscribing/MqttClientSubscribeOptions.cs b/Source/BPA.MQTTnet/Client/Subscribing/MqttClientSubscribeOptions.cs new file mode 100644 index 0000000..5c1c1ee --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Subscribing/MqttClientSubscribeOptions.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.Collections.Generic; +using MQTTnet.Packets; + +namespace MQTTnet.Client +{ + public sealed class MqttClientSubscribeOptions + { + /// + /// Gets or sets a list of topic filters the client wants to subscribe to. + /// Topic filters can include regular topics or wild cards. + /// + public List TopicFilters { get; set; } = new List(); + + /// + /// Gets or sets the subscription identifier. + /// 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 SubscriptionIdentifier { 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. + /// + public List UserProperties { get; set; } + } +} diff --git a/Source/BPA.MQTTnet/Client/Subscribing/MqttClientSubscribeOptionsBuilder.cs b/Source/BPA.MQTTnet/Client/Subscribing/MqttClientSubscribeOptionsBuilder.cs new file mode 100644 index 0000000..c6b5890 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Subscribing/MqttClientSubscribeOptionsBuilder.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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 +{ + public sealed class MqttClientSubscribeOptionsBuilder + { + readonly MqttClientSubscribeOptions _subscribeOptions = new MqttClientSubscribeOptions(); + + /// + /// Adds the user property to the subscribe options. + /// Hint: MQTT 5 feature only. + /// + /// The property name. + /// The property value. + /// A new instance of the class. + public MqttClientSubscribeOptionsBuilder WithUserProperty(string name, string value) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + if (value == null) throw new ArgumentNullException(nameof(value)); + + if (_subscribeOptions.UserProperties == null) + { + _subscribeOptions.UserProperties = new List(); + } + + _subscribeOptions.UserProperties.Add(new MqttUserProperty(name, value)); + + return this; + } + + public MqttClientSubscribeOptionsBuilder WithSubscriptionIdentifier(uint subscriptionIdentifier) + { + if (subscriptionIdentifier == 0) + { + throw new MqttProtocolViolationException("Subscription identifier cannot be 0."); + } + + _subscribeOptions.SubscriptionIdentifier = subscriptionIdentifier; + return this; + } + + public MqttClientSubscribeOptionsBuilder WithTopicFilter( + string topic, + MqttQualityOfServiceLevel qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce, + bool noLocal = false, + bool retainAsPublished = false, + MqttRetainHandling retainHandling = MqttRetainHandling.SendAtSubscribe) + { + return WithTopicFilter(new MqttTopicFilter + { + Topic = topic, + QualityOfServiceLevel = qualityOfServiceLevel, + NoLocal = noLocal, + RetainAsPublished = retainAsPublished, + RetainHandling = retainHandling + }); + } + + public MqttClientSubscribeOptionsBuilder WithTopicFilter(Action topicFilterBuilder) + { + if (topicFilterBuilder == null) throw new ArgumentNullException(nameof(topicFilterBuilder)); + + var internalTopicFilterBuilder = new MqttTopicFilterBuilder(); + topicFilterBuilder(internalTopicFilterBuilder); + + return WithTopicFilter(internalTopicFilterBuilder); + } + + public MqttClientSubscribeOptionsBuilder WithTopicFilter(MqttTopicFilterBuilder topicFilterBuilder) + { + if (topicFilterBuilder == null) throw new ArgumentNullException(nameof(topicFilterBuilder)); + + return WithTopicFilter(topicFilterBuilder.Build()); + } + + public MqttClientSubscribeOptionsBuilder WithTopicFilter(MqttTopicFilter topicFilter) + { + if (topicFilter == null) throw new ArgumentNullException(nameof(topicFilter)); + + if (_subscribeOptions.TopicFilters == null) + { + _subscribeOptions.TopicFilters = new List(); + } + + _subscribeOptions.TopicFilters.Add(topicFilter); + + return this; + } + + public MqttClientSubscribeOptions Build() + { + return _subscribeOptions; + } + } +} diff --git a/Source/BPA.MQTTnet/Client/Subscribing/MqttClientSubscribeResult.cs b/Source/BPA.MQTTnet/Client/Subscribing/MqttClientSubscribeResult.cs new file mode 100644 index 0000000..67b5260 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Subscribing/MqttClientSubscribeResult.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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 +{ + public sealed class MqttClientSubscribeResult + { + 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/BPA.MQTTnet/Client/Subscribing/MqttClientSubscribeResultCode.cs b/Source/BPA.MQTTnet/Client/Subscribing/MqttClientSubscribeResultCode.cs new file mode 100644 index 0000000..2213f41 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Subscribing/MqttClientSubscribeResultCode.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. + +namespace MQTTnet.Client +{ + public enum MqttClientSubscribeResultCode + { + GrantedQoS0 = 0x00, + GrantedQoS1 = 0x01, + GrantedQoS2 = 0x02, + UnspecifiedError = 0x80, + + ImplementationSpecificError = 131, + NotAuthorized = 135, + TopicFilterInvalid = 143, + PacketIdentifierInUse = 145, + QuotaExceeded = 151, + SharedSubscriptionsNotSupported = 158, + SubscriptionIdentifiersNotSupported = 161, + WildcardSubscriptionsNotSupported = 162 + } +} diff --git a/Source/BPA.MQTTnet/Client/Subscribing/MqttClientSubscribeResultFactory.cs b/Source/BPA.MQTTnet/Client/Subscribing/MqttClientSubscribeResultFactory.cs new file mode 100644 index 0000000..a061324 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Client/Subscribing/MqttClientSubscribeResultItem.cs b/Source/BPA.MQTTnet/Client/Subscribing/MqttClientSubscribeResultItem.cs new file mode 100644 index 0000000..f1fcee8 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Subscribing/MqttClientSubscribeResultItem.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 MQTTnet.Packets; + +namespace MQTTnet.Client +{ + public sealed class MqttClientSubscribeResultItem + { + /// + /// Gets or sets the topic filter. + /// The topic filter can contain topics and wildcards. + /// + public MqttTopicFilter TopicFilter { get; internal set; } + + /// + /// Gets or sets the result code. + /// Hint: MQTT 5 feature only. + /// + public MqttClientSubscribeResultCode ResultCode { get; internal set; } + } +} diff --git a/Source/BPA.MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeOptions.cs b/Source/BPA.MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeOptions.cs new file mode 100644 index 0000000..da33108 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeOptions.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using MQTTnet.Packets; + +namespace MQTTnet.Client +{ + public sealed class MqttClientUnsubscribeOptions + { + /// + /// Gets or sets a list of topic filters the client wants to unsubscribe from. + /// Topic filters can include regular topics or wild cards. + /// + public List TopicFilters { get; set; } = new List(); + + /// + /// 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 { get; set; } = new List(); + } +} diff --git a/Source/BPA.MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeOptionsBuilder.cs b/Source/BPA.MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeOptionsBuilder.cs new file mode 100644 index 0000000..b9aabe4 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeOptionsBuilder.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using MQTTnet.Packets; + +namespace MQTTnet.Client +{ + public class MqttClientUnsubscribeOptionsBuilder + { + private readonly MqttClientUnsubscribeOptions _unsubscribeOptions = new MqttClientUnsubscribeOptions(); + + /// + /// Adds the user property to the unsubscribe options. + /// Hint: MQTT 5 feature only. + /// + /// The property name. + /// The property value. + /// A new instance of the class. + public MqttClientUnsubscribeOptionsBuilder WithUserProperty(string name, string value) + { + if (name is null) throw new ArgumentNullException(nameof(name)); + if (value is null) throw new ArgumentNullException(nameof(value)); + + return WithUserProperty(new MqttUserProperty(name, value)); + } + + /// + /// Adds the user property to the unsubscribe options. + /// Hint: MQTT 5 feature only. + /// + /// The user property. + /// A new instance of the class. + public MqttClientUnsubscribeOptionsBuilder WithUserProperty(MqttUserProperty userProperty) + { + if (userProperty is null) throw new ArgumentNullException(nameof(userProperty)); + + if (_unsubscribeOptions.UserProperties is null) + { + _unsubscribeOptions.UserProperties = new List(); + } + + _unsubscribeOptions.UserProperties.Add(userProperty); + + return this; + } + + public MqttClientUnsubscribeOptionsBuilder WithTopicFilter(string topic) + { + if (topic is null) throw new ArgumentNullException(nameof(topic)); + + if (_unsubscribeOptions.TopicFilters is null) + { + _unsubscribeOptions.TopicFilters = new List(); + } + + _unsubscribeOptions.TopicFilters.Add(topic); + + return this; + } + + public MqttClientUnsubscribeOptionsBuilder WithTopicFilter(MqttTopicFilter topicFilter) + { + if (topicFilter is null) throw new ArgumentNullException(nameof(topicFilter)); + + return WithTopicFilter(topicFilter.Topic); + } + + public MqttClientUnsubscribeOptions Build() + { + return _unsubscribeOptions; + } + } +} diff --git a/Source/BPA.MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResult.cs b/Source/BPA.MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResult.cs new file mode 100644 index 0000000..6bd6940 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResult.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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 +{ + public sealed class MqttClientUnsubscribeResult + { + 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/BPA.MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultCode.cs b/Source/BPA.MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultCode.cs new file mode 100644 index 0000000..521c88d --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultCode.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.Client +{ + public enum MqttClientUnsubscribeResultCode + { + Success = 0, + NoSubscriptionExisted = 17, + UnspecifiedError = 128, + ImplementationSpecificError = 131, + NotAuthorized = 135, + TopicFilterInvalid = 143, + PacketIdentifierInUse = 145 + } +} diff --git a/Source/BPA.MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultFactory.cs b/Source/BPA.MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultFactory.cs new file mode 100644 index 0000000..0f87295 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultFactory.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 System.Collections.Generic; +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/BPA.MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultItem.cs b/Source/BPA.MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultItem.cs new file mode 100644 index 0000000..9f39035 --- /dev/null +++ b/Source/BPA.MQTTnet/Client/Unsubscribing/MqttClientUnsubscribeResultItem.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. + +namespace MQTTnet.Client +{ + public sealed class MqttClientUnsubscribeResultItem + { + /// + /// Gets or sets the topic filter. + /// The topic filter can contain topics and wildcards. + /// + public string TopicFilter { get; internal set; } + + /// + /// Gets or sets the result code. + /// Hint: MQTT 5 feature only. + /// + public MqttClientUnsubscribeResultCode ResultCode { get; internal set; } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Diagnostics/Logger/IMqttNetLogger.cs b/Source/BPA.MQTTnet/Diagnostics/Logger/IMqttNetLogger.cs new file mode 100644 index 0000000..530b976 --- /dev/null +++ b/Source/BPA.MQTTnet/Diagnostics/Logger/IMqttNetLogger.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 interface IMqttNetLogger + { + bool IsEnabled { get; } + + void Publish(MqttNetLogLevel logLevel, string source, string message, object[] parameters, Exception exception); + } +} diff --git a/Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetEventLogger.cs b/Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetEventLogger.cs new file mode 100644 index 0000000..f639d02 --- /dev/null +++ b/Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetEventLogger.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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 +{ + /// + /// This logger fires an event when a new message was published. + /// + public sealed class MqttNetEventLogger : IMqttNetLogger + { + public MqttNetEventLogger(string logId = null) + { + LogId = logId; + } + + public event EventHandler LogMessagePublished; + + public bool IsEnabled => LogMessagePublished != null; + + public string LogId { get; } + + public void Publish(MqttNetLogLevel level, string source, string message, object[] parameters, Exception exception) + { + var eventHandler = LogMessagePublished; + if (eventHandler == null) + { + // No listener is attached so we can step out. + // Keep a reference to the handler because the handler + // might be null after preparing the message. + return; + } + + if (parameters?.Length > 0 && message?.Length > 0) + { + try + { + message = string.Format(message, parameters); + } + catch (FormatException) + { + message = "MESSAGE FORMAT INVALID: " + message; + } + } + + // We only use UTC here to improve performance. Using a local date time + // would require to load the time zone settings! + var logMessage = new MqttNetLogMessage + { + LogId = LogId, + Timestamp = DateTime.UtcNow, + Source = source, + ThreadId = Environment.CurrentManagedThreadId, + Level = level, + Message = message, + Exception = exception + }; + + eventHandler.Invoke(this, new MqttNetLogMessagePublishedEventArgs(logMessage)); + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetLogLevel.cs b/Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetLogLevel.cs new file mode 100644 index 0000000..629df73 --- /dev/null +++ b/Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetLogLevel.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.Diagnostics +{ + public enum MqttNetLogLevel + { + Verbose, + + Info, + + Warning, + + Error + } +} diff --git a/Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetLogMessage.cs b/Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetLogMessage.cs new file mode 100644 index 0000000..7a806bb --- /dev/null +++ b/Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetLogMessage.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; + +namespace MQTTnet.Diagnostics +{ + public sealed class MqttNetLogMessage + { + public string LogId { get; set; } + + public DateTime Timestamp { get; set; } + + public int ThreadId { get; set; } + + public string Source { get; set; } + + public MqttNetLogLevel Level { get; set; } + + public string Message { get; set; } + + public Exception Exception { get; set; } + + public override string ToString() + { + var result = $"[{Timestamp:O}] [{LogId}] [{ThreadId}] [{Source}] [{Level}]: {Message}"; + if (Exception != null) + { + result += Environment.NewLine + Exception; + } + + return result; + } + } +} diff --git a/Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetLogMessagePublishedEventArgs.cs b/Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetLogMessagePublishedEventArgs.cs new file mode 100644 index 0000000..f5f8e05 --- /dev/null +++ b/Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetLogMessagePublishedEventArgs.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.Diagnostics +{ + public sealed class MqttNetLogMessagePublishedEventArgs : EventArgs + { + public MqttNetLogMessagePublishedEventArgs(MqttNetLogMessage logMessage) + { + LogMessage = logMessage ?? throw new ArgumentNullException(nameof(logMessage)); + } + + public MqttNetLogMessage LogMessage { get; } + } +} diff --git a/Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetNullLogger.cs b/Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetNullLogger.cs new file mode 100644 index 0000000..20208e3 --- /dev/null +++ b/Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetNullLogger.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace MQTTnet.Diagnostics +{ + /// + /// This logger does nothing with the messages. + /// + public sealed class MqttNetNullLogger : IMqttNetLogger + { + public MqttNetNullLogger() + { + IsEnabled = false; + } + + public static MqttNetNullLogger Instance { get; } = new MqttNetNullLogger(); + + public bool IsEnabled { get; } + + public void Publish(MqttNetLogLevel logLevel, string source, string message, object[] parameters, Exception exception) + { + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetSourceLogger.cs b/Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetSourceLogger.cs new file mode 100644 index 0000000..438a2f6 --- /dev/null +++ b/Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetSourceLogger.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace MQTTnet.Diagnostics +{ + public sealed class MqttNetSourceLogger + { + readonly IMqttNetLogger _logger; + readonly string _source; + + public MqttNetSourceLogger(IMqttNetLogger logger, string source) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _source = source; + } + + public bool IsEnabled => _logger.IsEnabled; + + public void Publish(MqttNetLogLevel logLevel, string message, object[] parameters, Exception exception) + { + _logger.Publish(logLevel, _source, message, parameters, exception); + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetSourceLoggerExtensions.cs b/Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetSourceLoggerExtensions.cs new file mode 100644 index 0000000..18d70a5 --- /dev/null +++ b/Source/BPA.MQTTnet/Diagnostics/Logger/MqttNetSourceLoggerExtensions.cs @@ -0,0 +1,193 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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 static class MqttNetSourceLoggerExtensions + { + /* + * The logger uses generic parameters in order to avoid boxing of parameter values like integers etc. + */ + + public static MqttNetSourceLogger WithSource(this IMqttNetLogger logger, string source) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + return new MqttNetSourceLogger(logger, source); + } + + public static void Verbose(this MqttNetSourceLogger logger, string message, TParameter1 parameter1) + { + if (!logger.IsEnabled) + { + return; + } + + logger.Publish(MqttNetLogLevel.Verbose, message, new object[] {parameter1}, null); + } + + public static void Verbose(this MqttNetSourceLogger logger, string message, TParameter1 parameter1, TParameter2 parameter2) + { + if (!logger.IsEnabled) + { + return; + } + + logger.Publish(MqttNetLogLevel.Verbose, message, new object[] {parameter1, parameter2}, null); + } + + public static void Verbose(this MqttNetSourceLogger logger, string message, TParameter1 parameter1, TParameter2 parameter2, + TParameter3 parameter3) + { + if (!logger.IsEnabled) + { + return; + } + + logger.Publish(MqttNetLogLevel.Verbose, message, new object[] {parameter1, parameter2, parameter3}, null); + } + + public static void Verbose(this MqttNetSourceLogger logger, string message) + { + if (!logger.IsEnabled) + { + return; + } + + logger.Publish(MqttNetLogLevel.Verbose, message, null, null); + } + + public static void Info(this MqttNetSourceLogger logger, string message, TParameter1 parameter1) + { + if (!logger.IsEnabled) + { + return; + } + + logger.Publish(MqttNetLogLevel.Info, message, new object[] {parameter1}, null); + } + + public static void Info(this MqttNetSourceLogger logger, string message, TParameter1 parameter1, TParameter2 parameter2) + { + if (!logger.IsEnabled) + { + return; + } + + logger.Publish(MqttNetLogLevel.Info, message, new object[] {parameter1, parameter2}, null); + } + + public static void Info(this MqttNetSourceLogger logger, string message) + { + if (!logger.IsEnabled) + { + return; + } + + logger.Publish(MqttNetLogLevel.Info, message, null, null); + } + + public static void Warning(this MqttNetSourceLogger logger, Exception exception, string message, TParameter1 parameter1) + { + if (!logger.IsEnabled) + { + return; + } + + logger.Publish(MqttNetLogLevel.Warning, message, new object[] {parameter1}, exception); + } + + public static void Warning(this MqttNetSourceLogger logger, Exception exception, string message, TParameter1 parameter1, TParameter2 parameter2) + { + if (!logger.IsEnabled) + { + return; + } + + logger.Publish(MqttNetLogLevel.Warning, message, new object[] {parameter1, parameter2}, exception); + } + + public static void Warning(this MqttNetSourceLogger logger, Exception exception, string message) + { + if (!logger.IsEnabled) + { + return; + } + + logger.Publish(MqttNetLogLevel.Warning, message, null, exception); + } + + public static void Warning(this MqttNetSourceLogger logger, string message, TParameter1 parameter1) + { + if (!logger.IsEnabled) + { + return; + } + + 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) + { + return; + } + + logger.Publish(MqttNetLogLevel.Warning, message, null, null); + } + + public static void Error(this MqttNetSourceLogger logger, Exception exception, string message, TParameter1 parameter1) + { + if (!logger.IsEnabled) + { + return; + } + + logger.Publish(MqttNetLogLevel.Error, message, new object[] {parameter1}, exception); + } + + public static void Error(this MqttNetSourceLogger logger, Exception exception, string message, TParameter1 parameter1, TParameter2 parameter2) + { + if (!logger.IsEnabled) + { + return; + } + + logger.Publish(MqttNetLogLevel.Error, message, new object[] {parameter1, parameter2}, exception); + } + + public static void Error(this MqttNetSourceLogger logger, Exception exception, string message) + { + if (!logger.IsEnabled) + { + return; + } + + logger.Publish(MqttNetLogLevel.Error, message, null, exception); + } + + public static void Error(this MqttNetSourceLogger logger, string message) + { + if (!logger.IsEnabled) + { + return; + } + + logger.Publish(MqttNetLogLevel.Error, message, null, null); + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Diagnostics/PacketInspection/InspectMqttPacketEventArgs.cs b/Source/BPA.MQTTnet/Diagnostics/PacketInspection/InspectMqttPacketEventArgs.cs new file mode 100644 index 0000000..6d9e3a6 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Diagnostics/PacketInspection/MqttPacketFlowDirection.cs b/Source/BPA.MQTTnet/Diagnostics/PacketInspection/MqttPacketFlowDirection.cs new file mode 100644 index 0000000..1d9f5af --- /dev/null +++ b/Source/BPA.MQTTnet/Diagnostics/PacketInspection/MqttPacketFlowDirection.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.Diagnostics +{ + public enum MqttPacketFlowDirection + { + Inbound, + + Outbound + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Diagnostics/Runtime/TargetFrameworkProvider.cs b/Source/BPA.MQTTnet/Diagnostics/Runtime/TargetFrameworkProvider.cs new file mode 100644 index 0000000..046c3dc --- /dev/null +++ b/Source/BPA.MQTTnet/Diagnostics/Runtime/TargetFrameworkProvider.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. + +namespace MQTTnet.Diagnostics +{ + public static class TargetFrameworkProvider + { + public static string TargetFramework + { + get + { +#if NET452 + return "net452"; +#elif NET461 + return "net461"; +#elif NET472 + return "net472"; +#elif NETSTANDARD1_3 + return "netstandard1.3"; +#elif NETSTANDARD2_0 + return "netstandard2.0"; +#elif NETSTANDARD2_1 + return "netstandard2.1"; +#elif WINDOWS_UWP + return "uap10.0"; +#elif NETCOREAPP3_1 + return "netcoreapp3.1"; +#elif NET5_0 + return "net5.0"; +#elif NET6_0 + return "net6.0"; +#endif + } + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Exceptions/MqttCommunicationException.cs b/Source/BPA.MQTTnet/Exceptions/MqttCommunicationException.cs new file mode 100644 index 0000000..6c6ba05 --- /dev/null +++ b/Source/BPA.MQTTnet/Exceptions/MqttCommunicationException.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; + +namespace MQTTnet.Exceptions +{ + public class MqttCommunicationException : Exception + { + protected MqttCommunicationException() + { + } + + public MqttCommunicationException(Exception innerException) + : base(innerException.Message, innerException) + { + } + + public MqttCommunicationException(string message, Exception innerException = null) + : base(message, innerException) + { + } + } +} diff --git a/Source/BPA.MQTTnet/Exceptions/MqttCommunicationTimedOutException.cs b/Source/BPA.MQTTnet/Exceptions/MqttCommunicationTimedOutException.cs new file mode 100644 index 0000000..5d41c1b --- /dev/null +++ b/Source/BPA.MQTTnet/Exceptions/MqttCommunicationTimedOutException.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; + +namespace MQTTnet.Exceptions +{ + public sealed class MqttCommunicationTimedOutException : MqttCommunicationException + { + public MqttCommunicationTimedOutException() : base("The operation has timed out.") + { + } + + public MqttCommunicationTimedOutException(Exception innerException) : base("The operation has timed out.", innerException) + { + } + } +} diff --git a/Source/BPA.MQTTnet/Exceptions/MqttConfigurationException.cs b/Source/BPA.MQTTnet/Exceptions/MqttConfigurationException.cs new file mode 100644 index 0000000..9e2bc58 --- /dev/null +++ b/Source/BPA.MQTTnet/Exceptions/MqttConfigurationException.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; + +namespace MQTTnet.Exceptions +{ + public class MqttConfigurationException : Exception + { + protected MqttConfigurationException() + { + } + + public MqttConfigurationException(Exception innerException) + : base(innerException.Message, innerException) + { + } + + public MqttConfigurationException(string message) + : base(message) + { + } + } +} diff --git a/Source/BPA.MQTTnet/Exceptions/MqttProtocolViolationException.cs b/Source/BPA.MQTTnet/Exceptions/MqttProtocolViolationException.cs new file mode 100644 index 0000000..e8229a8 --- /dev/null +++ b/Source/BPA.MQTTnet/Exceptions/MqttProtocolViolationException.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.Exceptions +{ + public class MqttProtocolViolationException : Exception + { + public MqttProtocolViolationException(string message) + : base(message) + { + } + } +} diff --git a/Source/BPA.MQTTnet/Exceptions/MqttUnexpectedDisconnectReceivedException.cs b/Source/BPA.MQTTnet/Exceptions/MqttUnexpectedDisconnectReceivedException.cs new file mode 100644 index 0000000..eca736f --- /dev/null +++ b/Source/BPA.MQTTnet/Exceptions/MqttUnexpectedDisconnectReceivedException.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.Collections.Generic; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Exceptions +{ + public class MqttUnexpectedDisconnectReceivedException : MqttCommunicationException + { + public MqttUnexpectedDisconnectReceivedException(MqttDisconnectPacket disconnectPacket) + : base($"Unexpected DISCONNECT (Reason code={disconnectPacket.ReasonCode}) received.") + { + ReasonCode = disconnectPacket.ReasonCode; + SessionExpiryInterval = disconnectPacket.SessionExpiryInterval; + ReasonString = disconnectPacket.ReasonString; + ServerReference = disconnectPacket.ServerReference; + UserProperties = disconnectPacket.UserProperties; + } + + public MqttDisconnectReasonCode? ReasonCode { get; } + + public uint? SessionExpiryInterval { get; } + + public string ReasonString { get; } + + public List UserProperties { get; } + + public string ServerReference { get; } + } +} diff --git a/Source/BPA.MQTTnet/Formatter/IMqttPacketFormatter.cs b/Source/BPA.MQTTnet/Formatter/IMqttPacketFormatter.cs new file mode 100644 index 0000000..24210b8 --- /dev/null +++ b/Source/BPA.MQTTnet/Formatter/IMqttPacketFormatter.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 MQTTnet.Adapter; +using MQTTnet.Packets; + +namespace MQTTnet.Formatter +{ + public interface IMqttPacketFormatter + { + MqttPacket Decode(ReceivedMqttPacket receivedMqttPacket); + + MqttPacketBuffer Encode(MqttPacket mqttPacket); + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Formatter/MqttApplicationMessageFactory.cs b/Source/BPA.MQTTnet/Formatter/MqttApplicationMessageFactory.cs new file mode 100644 index 0000000..0e80f1d --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Formatter/MqttBufferReader.cs b/Source/BPA.MQTTnet/Formatter/MqttBufferReader.cs new file mode 100644 index 0000000..4c5cc36 --- /dev/null +++ b/Source/BPA.MQTTnet/Formatter/MqttBufferReader.cs @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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.Implementations; + +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/BPA.MQTTnet/Formatter/MqttBufferWriter.cs b/Source/BPA.MQTTnet/Formatter/MqttBufferWriter.cs new file mode 100644 index 0000000..701908b --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Formatter/MqttConnAckPacketFactory.cs b/Source/BPA.MQTTnet/Formatter/MqttConnAckPacketFactory.cs new file mode 100644 index 0000000..cf8e2a3 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Formatter/MqttConnectPacketFactory.cs b/Source/BPA.MQTTnet/Formatter/MqttConnectPacketFactory.cs new file mode 100644 index 0000000..d818630 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Formatter/MqttConnectReasonCodeConverter.cs b/Source/BPA.MQTTnet/Formatter/MqttConnectReasonCodeConverter.cs new file mode 100644 index 0000000..6278a60 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Formatter/MqttDisconnectPacketFactory.cs b/Source/BPA.MQTTnet/Formatter/MqttDisconnectPacketFactory.cs new file mode 100644 index 0000000..889bb97 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Formatter/MqttFixedHeader.cs b/Source/BPA.MQTTnet/Formatter/MqttFixedHeader.cs new file mode 100644 index 0000000..cb446d6 --- /dev/null +++ b/Source/BPA.MQTTnet/Formatter/MqttFixedHeader.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. + +namespace MQTTnet.Formatter +{ + public struct MqttFixedHeader + { + public MqttFixedHeader(byte flags, int remainingLength, int totalLength) + { + Flags = flags; + RemainingLength = remainingLength; + TotalLength = totalLength; + } + + public byte Flags { get; } + + public int RemainingLength { get; } + + public int TotalLength { get; } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Formatter/MqttPacketBuffer.cs b/Source/BPA.MQTTnet/Formatter/MqttPacketBuffer.cs new file mode 100644 index 0000000..4fdd77e --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Formatter/MqttPacketFactories.cs b/Source/BPA.MQTTnet/Formatter/MqttPacketFactories.cs new file mode 100644 index 0000000..a13c2aa --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Formatter/MqttPacketFormatterAdapter.cs b/Source/BPA.MQTTnet/Formatter/MqttPacketFormatterAdapter.cs new file mode 100644 index 0000000..2dcf00b --- /dev/null +++ b/Source/BPA.MQTTnet/Formatter/MqttPacketFormatterAdapter.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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; +using MQTTnet.Formatter.V3; +using MQTTnet.Formatter.V5; +using MQTTnet.Packets; + +namespace MQTTnet.Formatter +{ + public sealed class MqttPacketFormatterAdapter + { + readonly MqttBufferReader _bufferReader = new MqttBufferReader(); + readonly MqttBufferWriter _bufferWriter; + + IMqttPacketFormatter _formatter; + + public MqttPacketFormatterAdapter(MqttBufferWriter mqttBufferWriter) + { + _bufferWriter = mqttBufferWriter ?? throw new ArgumentNullException(nameof(mqttBufferWriter)); + } + + public MqttPacketFormatterAdapter(MqttProtocolVersion protocolVersion, MqttBufferWriter bufferWriter) : this(bufferWriter) + { + UseProtocolVersion(protocolVersion); + } + + public MqttProtocolVersion ProtocolVersion { get; private set; } = MqttProtocolVersion.Unknown; + + public MqttPacket Decode(ReceivedMqttPacket receivedMqttPacket) + { + ThrowIfFormatterNotSet(); + + return _formatter.Decode(receivedMqttPacket); + } + + public void DetectProtocolVersion(ReceivedMqttPacket receivedMqttPacket) + { + var protocolVersion = ParseProtocolVersion(receivedMqttPacket); + UseProtocolVersion(protocolVersion); + } + + public MqttPacketBuffer Encode(MqttPacket packet) + { + ThrowIfFormatterNotSet(); + return _formatter.Encode(packet); + } + + public void Cleanup() + { + _bufferWriter.Cleanup(); + } + + public static IMqttPacketFormatter GetMqttPacketFormatter(MqttProtocolVersion protocolVersion, MqttBufferWriter bufferWriter) + { + if (protocolVersion == MqttProtocolVersion.Unknown) + { + throw new InvalidOperationException("MQTT protocol version is invalid."); + } + + switch (protocolVersion) + { + case MqttProtocolVersion.V500: + { + return new MqttV5PacketFormatter(bufferWriter); + } + case MqttProtocolVersion.V310: + case MqttProtocolVersion.V311: + { + return new MqttV3PacketFormatter(bufferWriter, protocolVersion); + } + default: + { + throw new NotSupportedException(); + } + } + } + + MqttProtocolVersion ParseProtocolVersion(ReceivedMqttPacket receivedMqttPacket) + { + if (receivedMqttPacket.Body.Count < 7) + { + // 2 byte protocol name length + // at least 4 byte protocol name + // 1 byte protocol level + throw new MqttProtocolViolationException("CONNECT packet must have at least 7 bytes."); + } + + _bufferReader.SetBuffer(receivedMqttPacket.Body.Array, receivedMqttPacket.Body.Offset, receivedMqttPacket.Body.Count); + + var protocolName = _bufferReader.ReadString(); + var protocolLevel = _bufferReader.ReadByte(); + + if (protocolName == "MQTT") + { + if (protocolLevel == 5) + { + return MqttProtocolVersion.V500; + } + + if (protocolLevel == 4) + { + return MqttProtocolVersion.V311; + } + + throw new MqttProtocolViolationException($"Protocol level '{protocolLevel}' not supported."); + } + + if (protocolName == "MQIsdp") + { + if (protocolLevel == 3) + { + return MqttProtocolVersion.V310; + } + + throw new MqttProtocolViolationException($"Protocol level '{protocolLevel}' not supported."); + } + + throw new MqttProtocolViolationException($"Protocol '{protocolName}' not supported."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void ThrowIfFormatterNotSet() + { + if (_formatter == null) + { + 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/BPA.MQTTnet/Formatter/MqttProtocolVersion.cs b/Source/BPA.MQTTnet/Formatter/MqttProtocolVersion.cs new file mode 100644 index 0000000..436951d --- /dev/null +++ b/Source/BPA.MQTTnet/Formatter/MqttProtocolVersion.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.Formatter +{ + public enum MqttProtocolVersion + { + Unknown = 0, + + V310 = 3, + V311 = 4, + V500 = 5 + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Formatter/MqttPubAckPacketFactory.cs b/Source/BPA.MQTTnet/Formatter/MqttPubAckPacketFactory.cs new file mode 100644 index 0000000..4d6c687 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Formatter/MqttPubCompPacketFactory.cs b/Source/BPA.MQTTnet/Formatter/MqttPubCompPacketFactory.cs new file mode 100644 index 0000000..fd4b349 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Formatter/MqttPubRecPacketFactory.cs b/Source/BPA.MQTTnet/Formatter/MqttPubRecPacketFactory.cs new file mode 100644 index 0000000..5c0726d --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Formatter/MqttPubRelPacketFactory.cs b/Source/BPA.MQTTnet/Formatter/MqttPubRelPacketFactory.cs new file mode 100644 index 0000000..a16790b --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Formatter/MqttPublishPacketFactory.cs b/Source/BPA.MQTTnet/Formatter/MqttPublishPacketFactory.cs new file mode 100644 index 0000000..90325e9 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Formatter/MqttSubAckPacketFactory.cs b/Source/BPA.MQTTnet/Formatter/MqttSubAckPacketFactory.cs new file mode 100644 index 0000000..4f5436d --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Formatter/MqttSubscribePacketFactory.cs b/Source/BPA.MQTTnet/Formatter/MqttSubscribePacketFactory.cs new file mode 100644 index 0000000..e24fe9f --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Formatter/MqttUnsubAckPacketFactory.cs b/Source/BPA.MQTTnet/Formatter/MqttUnsubAckPacketFactory.cs new file mode 100644 index 0000000..18da46a --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Formatter/MqttUnsubscribePacketFactory.cs b/Source/BPA.MQTTnet/Formatter/MqttUnsubscribePacketFactory.cs new file mode 100644 index 0000000..85cda94 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Formatter/ReadFixedHeaderResult.cs b/Source/BPA.MQTTnet/Formatter/ReadFixedHeaderResult.cs new file mode 100644 index 0000000..5866393 --- /dev/null +++ b/Source/BPA.MQTTnet/Formatter/ReadFixedHeaderResult.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. + +namespace MQTTnet.Formatter +{ + public struct ReadFixedHeaderResult + { + 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/BPA.MQTTnet/Formatter/V3/MqttV3PacketFormatter.cs b/Source/BPA.MQTTnet/Formatter/V3/MqttV3PacketFormatter.cs new file mode 100644 index 0000000..d8276bb --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Formatter/V5/MqttV5PacketDecoder.cs b/Source/BPA.MQTTnet/Formatter/V5/MqttV5PacketDecoder.cs new file mode 100644 index 0000000..5f87585 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Formatter/V5/MqttV5PacketEncoder.cs b/Source/BPA.MQTTnet/Formatter/V5/MqttV5PacketEncoder.cs new file mode 100644 index 0000000..2747848 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Formatter/V5/MqttV5PacketFormatter.cs b/Source/BPA.MQTTnet/Formatter/V5/MqttV5PacketFormatter.cs new file mode 100644 index 0000000..371cb86 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Formatter/V5/MqttV5PropertiesReader.cs b/Source/BPA.MQTTnet/Formatter/V5/MqttV5PropertiesReader.cs new file mode 100644 index 0000000..e5a3efe --- /dev/null +++ b/Source/BPA.MQTTnet/Formatter/V5/MqttV5PropertiesReader.cs @@ -0,0 +1,220 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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.Formatter.V5 +{ + public struct MqttV5PropertiesReader + { + readonly MqttBufferReader _body; + readonly int _length; + readonly int _targetOffset; + + public MqttV5PropertiesReader(MqttBufferReader body) + { + _body = body ?? throw new ArgumentNullException(nameof(body)); + + if (!body.EndOfStream) + { + _length = (int)body.ReadVariableByteInteger(); + } + else + { + _length = 0; + } + + _targetOffset = body.Position + _length; + + CollectedUserProperties = null; + CurrentPropertyId = MqttPropertyId.None; + } + + public List CollectedUserProperties { get; private set; } + + public MqttPropertyId CurrentPropertyId { get; private set; } + + public bool MoveNext() + { + while (true) + { + 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; + } + } + + public string ReadAssignedClientIdentifier() + { + return _body.ReadString(); + } + + public byte[] ReadAuthenticationData() + { + return _body.ReadBinaryData(); + } + + public string ReadAuthenticationMethod() + { + return _body.ReadString(); + } + + public string ReadContentType() + { + return _body.ReadString(); + } + + public byte[] ReadCorrelationData() + { + return _body.ReadBinaryData(); + } + + public uint ReadMaximumPacketSize() + { + return _body.ReadFourByteInteger(); + } + + public MqttQualityOfServiceLevel ReadMaximumQoS() + { + var value = _body.ReadByte(); + if (value > 1) + { + throw new MqttProtocolViolationException($"Unexpected Maximum QoS value: {value}"); + } + + return (MqttQualityOfServiceLevel)value; + } + + public uint ReadMessageExpiryInterval() + { + return _body.ReadFourByteInteger(); + } + + public MqttPayloadFormatIndicator ReadPayloadFormatIndicator() + { + return (MqttPayloadFormatIndicator)_body.ReadByte(); + } + + public string ReadReasonString() + { + return _body.ReadString(); + } + + public ushort ReadReceiveMaximum() + { + return _body.ReadTwoByteInteger(); + } + + public string ReadResponseInformation() + { + return _body.ReadString(); + } + + public string ReadResponseTopic() + { + return _body.ReadString(); + } + + public bool ReadRetainAvailable() + { + return _body.ReadByte() == 1; + } + + public ushort ReadServerKeepAlive() + { + return _body.ReadTwoByteInteger(); + } + + public string ReadServerReference() + { + return _body.ReadString(); + } + + public uint ReadSessionExpiryInterval() + { + return _body.ReadFourByteInteger(); + } + + public bool ReadSharedSubscriptionAvailable() + { + return _body.ReadByte() == 1; + } + + public uint ReadSubscriptionIdentifier() + { + return _body.ReadVariableByteInteger(); + } + + public bool ReadSubscriptionIdentifiersAvailable() + { + return _body.ReadByte() == 1; + } + + public ushort ReadTopicAlias() + { + return _body.ReadTwoByteInteger(); + } + + public ushort ReadTopicAliasMaximum() + { + return _body.ReadTwoByteInteger(); + } + + public bool ReadWildcardSubscriptionAvailable() + { + return _body.ReadByte() == 1; + } + + public uint ReadWillDelayInterval() + { + return _body.ReadFourByteInteger(); + } + + public bool RequestProblemInformation() + { + return _body.ReadByte() == 1; + } + + public bool RequestResponseInformation() + { + return _body.ReadByte() == 1; + } + + public void ThrowInvalidPropertyIdException(Type type) + { + throw new MqttProtocolViolationException($"Property ID '{CurrentPropertyId}' is not supported for package type '{type.Name}'."); + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Formatter/V5/MqttV5PropertiesWriter.cs b/Source/BPA.MQTTnet/Formatter/V5/MqttV5PropertiesWriter.cs new file mode 100644 index 0000000..3307b66 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Implementations/CrossPlatformSocket.cs b/Source/BPA.MQTTnet/Implementations/CrossPlatformSocket.cs new file mode 100644 index 0000000..1d14ffc --- /dev/null +++ b/Source/BPA.MQTTnet/Implementations/CrossPlatformSocket.cs @@ -0,0 +1,256 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Exceptions; + +namespace MQTTnet.Implementations +{ + public sealed class CrossPlatformSocket : IDisposable + { + readonly Socket _socket; + readonly Action _socketDisposeAction; + + NetworkStream _networkStream; + + public CrossPlatformSocket(AddressFamily addressFamily) + { + _socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp); + _socketDisposeAction = _socket.Dispose; + } + + public CrossPlatformSocket() + { + // Having this constructor is important because avoiding the address family as parameter + // will make use of dual mode in the .net framework. + _socket = new Socket(SocketType.Stream, ProtocolType.Tcp); + + _socketDisposeAction = _socket.Dispose; + } + + public CrossPlatformSocket(Socket socket) + { + _socket = socket ?? throw new ArgumentNullException(nameof(socket)); + _networkStream = new NetworkStream(socket, true); + + _socketDisposeAction = _socket.Dispose; + } + + public bool NoDelay + { + // 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; + 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; + set => _socket.DualMode = value; + } + + public int ReceiveBufferSize + { + get => _socket.ReceiveBufferSize; + set => _socket.ReceiveBufferSize = value; + } + + public int SendBufferSize + { + get => _socket.SendBufferSize; + set => _socket.SendBufferSize = value; + } + + public int SendTimeout + { + get => _socket.SendTimeout; + set => _socket.SendTimeout = value; + } + + public EndPoint RemoteEndPoint => _socket.RemoteEndPoint; + + public bool ReuseAddress + { + get => (int)_socket.GetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress) != 0; + set => _socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, value ? 1 : 0); + } + + public bool IsConnected => _socket.Connected; + + public async Task AcceptAsync() + { + try + { +#if NET452 || NET461 + var clientSocket = await Task.Factory.FromAsync(_socket.BeginAccept, _socket.EndAccept, null).ConfigureAwait(false); +#else + var clientSocket = await _socket.AcceptAsync().ConfigureAwait(false); +#endif + return new CrossPlatformSocket(clientSocket); + } + catch (ObjectDisposedException) + { + // This will happen when _socket.EndAccept gets called by Task library but the socket is already disposed. + return null; + } + } + + public EndPoint LocalEndPoint => _socket.LocalEndPoint; + + public void Bind(EndPoint localEndPoint) + { + if (localEndPoint is null) + { + throw new ArgumentNullException(nameof(localEndPoint)); + } + + _socket.Bind(localEndPoint); + } + + public void Listen(int connectionBacklog) + { + _socket.Listen(connectionBacklog); + } + + public async Task ConnectAsync(string host, int port, CancellationToken cancellationToken) + { + 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)) + { +#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 + } +#endif + _networkStream = new NetworkStream(_socket, true); + } + catch (SocketException socketException) + { + if (socketException.SocketErrorCode == SocketError.OperationAborted) + { + throw new OperationCanceledException(); + } + + if (socketException.SocketErrorCode == SocketError.TimedOut) + { + throw new MqttCommunicationTimedOutException(); + } + + throw new MqttCommunicationException($"Error while connecting with host '{host}:{port}'.", socketException); + } + catch (ObjectDisposedException) + { + // This will happen when _socket.EndConnect gets called by Task library but the socket is already disposed. + cancellationToken.ThrowIfCancellationRequested(); + } + } + + public async Task SendAsync(ArraySegment buffer, SocketFlags socketFlags) + { + try + { +#if NET452 || NET461 + await Task.Factory.FromAsync(SocketWrapper.BeginSend, _socket.EndSend, new SocketWrapper(_socket, buffer, socketFlags)).ConfigureAwait(false); +#else + await _socket.SendAsync(buffer, socketFlags).ConfigureAwait(false); +#endif + } + catch (ObjectDisposedException) + { + // This will happen when _socket.EndConnect gets called by Task library but the socket is already disposed. + } + } + + public async Task ReceiveAsync(ArraySegment buffer, SocketFlags socketFlags) + { + try + { +#if NET452 || NET461 + return await Task.Factory.FromAsync(SocketWrapper.BeginReceive, _socket.EndReceive, new SocketWrapper(_socket, buffer, socketFlags)).ConfigureAwait(false); +#else + return await _socket.ReceiveAsync(buffer, socketFlags).ConfigureAwait(false); +#endif + } + catch (ObjectDisposedException) + { + // This will happen when _socket.EndReceive gets called by Task library but the socket is already disposed. + return -1; + } + } + + public NetworkStream GetStream() + { + var networkStream = _networkStream; + if (networkStream == null) + { + throw new IOException("The socket is not connected."); + } + + return networkStream; + } + + public void Dispose() + { + _networkStream?.Dispose(); + _socket?.Dispose(); + } + +#if NET452 || NET461 + sealed class SocketWrapper + { + readonly Socket _socket; + readonly ArraySegment _buffer; + readonly SocketFlags _socketFlags; + + public SocketWrapper(Socket socket, ArraySegment buffer, SocketFlags socketFlags) + { + _socket = socket; + _buffer = buffer; + _socketFlags = socketFlags; + } + + public static IAsyncResult BeginSend(AsyncCallback callback, object state) + { + var socketWrapper = (SocketWrapper)state; + return socketWrapper._socket.BeginSend(socketWrapper._buffer.Array, socketWrapper._buffer.Offset, socketWrapper._buffer.Count, socketWrapper._socketFlags, callback, state); + } + + public static IAsyncResult BeginReceive(AsyncCallback callback, object state) + { + var socketWrapper = (SocketWrapper)state; + return socketWrapper._socket.BeginReceive(socketWrapper._buffer.Array, socketWrapper._buffer.Offset, socketWrapper._buffer.Count, socketWrapper._socketFlags, callback, state); + } + } +#endif + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Implementations/MqttClientAdapterFactory.cs b/Source/BPA.MQTTnet/Implementations/MqttClientAdapterFactory.cs new file mode 100644 index 0000000..9d2cbd8 --- /dev/null +++ b/Source/BPA.MQTTnet/Implementations/MqttClientAdapterFactory.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using MQTTnet.Adapter; +using MQTTnet.Diagnostics; +using MQTTnet.Formatter; +using System; +using MQTTnet.Channel; +using MQTTnet.Client; + +namespace MQTTnet.Implementations +{ + public sealed class MqttClientAdapterFactory : IMqttClientAdapterFactory + { + public IMqttChannelAdapter CreateClientAdapter(MqttClientOptions options, MqttPacketInspector packetInspector, IMqttNetLogger logger) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + + IMqttChannel channel; + switch (options.ChannelOptions) + { + case MqttClientTcpOptions _: + { + channel = new MqttTcpChannel(options); + break; + } + + case MqttClientWebSocketOptions webSocketOptions: + { + channel = new MqttWebSocketChannel(webSocketOptions); + break; + } + + default: + { + throw new NotSupportedException(); + } + } + + 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/BPA.MQTTnet/Implementations/MqttTcpChannel.Uwp.cs b/Source/BPA.MQTTnet/Implementations/MqttTcpChannel.Uwp.cs new file mode 100644 index 0000000..c43948f --- /dev/null +++ b/Source/BPA.MQTTnet/Implementations/MqttTcpChannel.Uwp.cs @@ -0,0 +1,200 @@ +// Licensed to the .NET Foundation under one or more 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; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Windows.Networking; +using Windows.Networking.Sockets; +using Windows.Security.Cryptography.Certificates; +using MQTTnet.Channel; +using MQTTnet.Client; +using MQTTnet.Server; + +namespace MQTTnet.Implementations +{ + public sealed class MqttTcpChannel : IMqttChannel + { + readonly MqttClientTcpOptions _options; + readonly int _bufferSize; + + StreamSocket _socket; + Stream _readStream; + Stream _writeStream; + + public MqttTcpChannel(MqttClientOptions clientOptions) + { + _options = (MqttClientTcpOptions)clientOptions.ChannelOptions; + _bufferSize = _options.BufferSize; + } + + public MqttTcpChannel(StreamSocket socket, X509Certificate2 clientCertificate, MqttServerOptions serverOptions) + { + _socket = socket ?? throw new ArgumentNullException(nameof(socket)); + _bufferSize = serverOptions.DefaultEndpointOptions.BufferSize; + + CreateStreams(); + + IsSecureConnection = socket.Information.ProtectionLevel >= SocketProtectionLevel.Tls12; + ClientCertificate = clientCertificate; + + Endpoint = _socket.Information.RemoteAddress + ":" + _socket.Information.RemotePort; + } + + public static Func> CustomIgnorableServerCertificateErrorsResolver { get; set; } + + public string Endpoint { get; private set; } + + public bool IsSecureConnection { get; } + + public X509Certificate2 ClientCertificate { get; } + + public async Task ConnectAsync(CancellationToken cancellationToken) + { + if (_socket == null) + { + _socket = new StreamSocket(); + _socket.Control.NoDelay = _options.NoDelay; + _socket.Control.KeepAlive = true; + } + + if (_options.TlsOptions?.UseTls != true) + { + await _socket.ConnectAsync(new HostName(_options.Server), _options.GetPort().ToString()).AsTask().ConfigureAwait(false); + } + else + { + _socket.Control.ClientCertificate = LoadCertificate(_options); + + foreach (var ignorableChainValidationResult in ResolveIgnorableServerCertificateErrors()) + { + _socket.Control.IgnorableServerCertificateErrors.Add(ignorableChainValidationResult); + } + + var socketProtectionLevel = SocketProtectionLevel.Tls12; + if (_options.TlsOptions.SslProtocol == SslProtocols.Tls11) + { + socketProtectionLevel = SocketProtectionLevel.Tls11; + } + else if (_options.TlsOptions.SslProtocol == SslProtocols.Tls) + { + socketProtectionLevel = SocketProtectionLevel.Tls10; + } + + await _socket.ConnectAsync(new HostName(_options.Server), _options.GetPort().ToString(), socketProtectionLevel).AsTask().ConfigureAwait(false); + } + + Endpoint = _socket.Information.RemoteAddress + ":" + _socket.Information.RemotePort; + + CreateStreams(); + } + + public Task DisconnectAsync(CancellationToken cancellationToken) + { + Dispose(); + return Task.FromResult(0); + } + + public Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _readStream.ReadAsync(buffer, offset, count, cancellationToken); + } + + public Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + // In the write method only the internal buffer will be filled. So here is no + // async/await required. The real network transmit is done when calling the + // Flush method. + _writeStream.Write(buffer, offset, count); + return _writeStream.FlushAsync(cancellationToken); + } + + public void Dispose() + { + TryDispose(_readStream, () => _readStream = null); + TryDispose(_writeStream, () => _writeStream = null); + TryDispose(_socket, () => _socket = null); + } + + private static Certificate LoadCertificate(IMqttClientChannelOptions options) + { + if (options.TlsOptions.Certificates == null || !options.TlsOptions.Certificates.Any()) + { + return null; + } + + if (options.TlsOptions.Certificates.Count > 1) + { + throw new NotSupportedException("Only one client certificate is supported when using 'uap10.0'."); + } + + return new Certificate(options.TlsOptions.Certificates.First().AsBuffer()); + } + + private IEnumerable ResolveIgnorableServerCertificateErrors() + { + if (CustomIgnorableServerCertificateErrorsResolver != null) + { + return CustomIgnorableServerCertificateErrorsResolver(_options); + } + + var result = new List(); + + if (_options.TlsOptions.IgnoreCertificateRevocationErrors) + { + result.Add(ChainValidationResult.RevocationInformationMissing); + //_socket.Control.IgnorableServerCertificateErrors.Add(ChainValidationResult.Revoked); Not supported. + result.Add(ChainValidationResult.RevocationFailure); + } + + if (_options.TlsOptions.IgnoreCertificateChainErrors) + { + result.Add(ChainValidationResult.IncompleteChain); + } + + if (_options.TlsOptions.AllowUntrustedCertificates) + { + result.Add(ChainValidationResult.Untrusted); + } + + return result; + } + + private void CreateStreams() + { + // Attention! Do not set the buffer for the read method. This will + // limit the internal buffer and the read operation will hang forever + // if more data than the buffer size was received. + _readStream = _socket.InputStream.AsStreamForRead(); + + _writeStream = _socket.OutputStream.AsStreamForWrite(_bufferSize); + } + + private static void TryDispose(IDisposable disposable, Action afterDispose) + { + try + { + disposable?.Dispose(); + } + catch (ObjectDisposedException) + { + } + catch (NullReferenceException) + { + } + finally + { + afterDispose(); + } + } + } +} +#endif \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Implementations/MqttTcpChannel.cs b/Source/BPA.MQTTnet/Implementations/MqttTcpChannel.cs new file mode 100644 index 0000000..f3e8462 --- /dev/null +++ b/Source/BPA.MQTTnet/Implementations/MqttTcpChannel.cs @@ -0,0 +1,291 @@ +// Licensed to the .NET Foundation under one or more 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.IO; +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 MqttClientOptions _clientOptions; + readonly Action _disposeAction; + readonly MqttClientTcpOptions _tcpOptions; + + Stream _stream; + + public MqttTcpChannel() + { + _disposeAction = Dispose; + } + + public MqttTcpChannel(MqttClientOptions clientOptions) : this() + { + _clientOptions = clientOptions ?? throw new ArgumentNullException(nameof(clientOptions)); + _tcpOptions = (MqttClientTcpOptions)clientOptions.ChannelOptions; + + IsSecureConnection = clientOptions.ChannelOptions?.TlsOptions?.UseTls == true; + } + + public MqttTcpChannel(Stream stream, string endpoint, X509Certificate2 clientCertificate) : this() + { + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + + Endpoint = endpoint; + + IsSecureConnection = stream is SslStream; + ClientCertificate = clientCertificate; + } + + public X509Certificate2 ClientCertificate { get; } + + public string Endpoint { get; private set; } + + public bool IsSecureConnection { get; } + + public async Task ConnectAsync(CancellationToken cancellationToken) + { + CrossPlatformSocket socket = null; + try + { + if (_tcpOptions.AddressFamily == AddressFamily.Unspecified) + { + socket = new CrossPlatformSocket(); + } + else + { + socket = new CrossPlatformSocket(_tcpOptions.AddressFamily); + } + + socket.ReceiveBufferSize = _tcpOptions.BufferSize; + socket.SendBufferSize = _tcpOptions.BufferSize; + 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 + // because on IPv4 only networks the setter will always throw an exception. Regardless + // of the actual value. + socket.DualMode = _tcpOptions.DualMode.Value; + } + + await socket.ConnectAsync(_tcpOptions.Server, _tcpOptions.GetPort(), cancellationToken).ConfigureAwait(false); + + cancellationToken.ThrowIfCancellationRequested(); + + var networkStream = socket.GetStream(); + + if (_tcpOptions.TlsOptions?.UseTls == true) + { + var sslStream = new SslStream(networkStream, false, InternalUserCertificateValidationCallback); + try + { +#if NETCOREAPP3_1 || NET5_0_OR_GREATER + var sslOptions = new SslClientAuthenticationOptions + { + ApplicationProtocols = _tcpOptions.TlsOptions.ApplicationProtocols, + ClientCertificates = LoadCertificates(), + EnabledSslProtocols = _tcpOptions.TlsOptions.SslProtocol, + CertificateRevocationCheckMode = _tcpOptions.TlsOptions.IgnoreCertificateRevocationErrors ? X509RevocationMode.NoCheck : X509RevocationMode.Online, + TargetHost = _tcpOptions.Server + }; + + await sslStream.AuthenticateAsClientAsync(sslOptions, cancellationToken).ConfigureAwait(false); +#else + await sslStream.AuthenticateAsClientAsync(_tcpOptions.Server, LoadCertificates(), _tcpOptions.TlsOptions.SslProtocol, + !_tcpOptions.TlsOptions.IgnoreCertificateRevocationErrors).ConfigureAwait(false); +#endif + } + catch + { +#if NETSTANDARD2_1 || NETCOREAPP3_1 || NET5_0_OR_GREATER + await sslStream.DisposeAsync().ConfigureAwait(false); +#else + sslStream.Dispose(); +#endif + + throw; + } + + _stream = sslStream; + } + else + { + _stream = networkStream; + } + + Endpoint = socket.RemoteEndPoint?.ToString(); + } + catch (Exception) + { + socket?.Dispose(); + throw; + } + } + + public Task DisconnectAsync(CancellationToken cancellationToken) + { + Dispose(); + 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(); + + try + { + var stream = _stream; + + if (stream == null) + { + return 0; + } + + if (!stream.CanRead) + { + return 0; + } + +#if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + return await stream.ReadAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false); +#else + // Workaround for: https://github.com/dotnet/corefx/issues/24430 + using (cancellationToken.Register(_disposeAction)) + { + return await stream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + } +#endif + } + catch (ObjectDisposedException) + { + // Indicate a graceful socket close. + return 0; + } + catch (IOException exception) + { + if (exception.InnerException is SocketException socketException) + { + ExceptionDispatchInfo.Capture(socketException).Throw(); + } + + throw; + } + } + + public async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + var stream = _stream; + + if (stream == null) + { + throw new MqttCommunicationException("The TCP connection is closed."); + } + +#if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + await stream.WriteAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false); +#else + // Workaround for: https://github.com/dotnet/corefx/issues/24430 + using (cancellationToken.Register(_disposeAction)) + { + await stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + } +#endif + } + catch (ObjectDisposedException) + { + throw new MqttCommunicationException("The TCP connection is closed."); + } + catch (IOException exception) + { + if (exception.InnerException is SocketException socketException) + { + ExceptionDispatchInfo.Capture(socketException).Throw(); + } + + throw; + } + } + + bool InternalUserCertificateValidationCallback(object sender, X509Certificate x509Certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + var certificateValidationHandler = _tcpOptions?.TlsOptions?.CertificateValidationHandler; + if (certificateValidationHandler != null) + { + var eventArgs = new MqttClientCertificateValidationEventArgs + { + Certificate = x509Certificate, + Chain = chain, + SslPolicyErrors = sslPolicyErrors, + ClientOptions = _tcpOptions + }; + + return certificateValidationHandler(eventArgs); + } + + return sslPolicyErrors == SslPolicyErrors.None; + } + + X509CertificateCollection LoadCertificates() + { + var certificates = new X509CertificateCollection(); + if (_tcpOptions.TlsOptions.Certificates == null) + { + return certificates; + } + + foreach (var certificate in _tcpOptions.TlsOptions.Certificates) + { + certificates.Add(certificate); + } + + return certificates; + } + } +} +#endif \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Implementations/MqttTcpServerAdapter.Uwp.cs b/Source/BPA.MQTTnet/Implementations/MqttTcpServerAdapter.Uwp.cs new file mode 100644 index 0000000..3efe840 --- /dev/null +++ b/Source/BPA.MQTTnet/Implementations/MqttTcpServerAdapter.Uwp.cs @@ -0,0 +1,128 @@ +// Licensed to the .NET Foundation under one or more 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.Formatter; +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 + { + IMqttNetLogger _rootLogger; + MqttNetSourceLogger _logger; + + MqttServerOptions _options; + StreamSocketListener _listener; + + public Func ClientHandler { get; set; } + + public async Task StartAsync(MqttServerOptions options, IMqttNetLogger logger) + { + 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(); + + // This also affects the client sockets. + _listener.Control.NoDelay = options.DefaultEndpointOptions.NoDelay; + _listener.Control.KeepAlive = true; + _listener.Control.QualityOfService = SocketQualityOfService.LowLatency; + _listener.ConnectionReceived += OnConnectionReceivedAsync; + + await _listener.BindServiceNameAsync(options.DefaultEndpointOptions.Port.ToString(), SocketProtectionLevel.PlainSocket); + } + + if (options.TlsEndpointOptions.IsEnabled) + { + throw new NotSupportedException("TLS servers are not supported when using 'uap10.0'."); + } + } + + public Task StopAsync() + { + if (_listener != null) + { + _listener.ConnectionReceived -= OnConnectionReceivedAsync; + } + + return Task.FromResult(0); + } + + public void Dispose() + { + _listener?.Dispose(); + _listener = null; + } + + async void OnConnectionReceivedAsync(StreamSocketListener sender, StreamSocketListenerConnectionReceivedEventArgs args) + { + try + { + var clientHandler = ClientHandler; + if (clientHandler != null) + { + X509Certificate2 clientCertificate = null; + + if (args.Socket.Control.ClientCertificate != null) + { + try + { + clientCertificate = new X509Certificate2(args.Socket.Control.ClientCertificate.GetCertificateBlob().ToArray()); + } + catch (Exception exception) + { + _logger.Warning(exception, "Unable to convert UWP certificate to X509Certificate2."); + } + } + + 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); + } + } + } + catch (Exception exception) + { + if (exception is ObjectDisposedException) + { + // It can happen that the listener socket is accessed after the cancellation token is already set and the listener socket is disposed. + return; + } + + _logger.Error(exception, "Error while handling client connection."); + } + finally + { + try + { + args.Socket.Dispose(); + } + catch (Exception exception) + { + _logger.Error(exception, "Error while cleaning up client connection."); + } + } + } + } +} +#endif \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Implementations/MqttTcpServerAdapter.cs b/Source/BPA.MQTTnet/Implementations/MqttTcpServerAdapter.cs new file mode 100644 index 0000000..ba1ea40 --- /dev/null +++ b/Source/BPA.MQTTnet/Implementations/MqttTcpServerAdapter.cs @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more 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; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; + +namespace MQTTnet.Implementations +{ + public sealed class MqttTcpServerAdapter : IMqttServerAdapter + { + readonly List _listeners = new List(); + + MqttServerOptions _serverOptions; + CancellationTokenSource _cancellationTokenSource; + + public Func ClientHandler { get; set; } + + public bool TreatSocketOpeningErrorAsWarning { get; set; } + + 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, logger, _cancellationTokenSource.Token); + } + + if (options.TlsEndpointOptions?.IsEnabled == true) + { + if (options.TlsEndpointOptions.CertificateProvider == null) + { + throw new ArgumentException("TLS certificate is not set."); + } + + var tlsCertificate = options.TlsEndpointOptions.CertificateProvider.GetCertificate(); + if (!tlsCertificate.HasPrivateKey) + { + throw new InvalidOperationException("The certificate for TLS encryption must contain the private key."); + } + + RegisterListeners(options.TlsEndpointOptions, tlsCertificate, logger, _cancellationTokenSource.Token); + } + + return PlatformAbstractionLayer.CompletedTask; + } + + public Task StopAsync() + { + Cleanup(); + return PlatformAbstractionLayer.CompletedTask; + } + + public void Dispose() + { + Cleanup(); + } + + void Cleanup() + { + try + { + _cancellationTokenSource?.Cancel(false); + } + finally + { + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = null; + + foreach (var listener in _listeners) + { + listener.Dispose(); + } + + _listeners.Clear(); + } + } + + void RegisterListeners(MqttServerTcpEndpointBaseOptions tcpEndpointOptions, X509Certificate2 tlsCertificate, IMqttNetLogger logger, CancellationToken cancellationToken) + { + if (!tcpEndpointOptions.BoundInterNetworkAddress.Equals(IPAddress.None)) + { + var listenerV4 = new MqttTcpServerListener(AddressFamily.InterNetwork, _serverOptions, tcpEndpointOptions, tlsCertificate, logger) + { + ClientHandler = OnClientAcceptedAsync + }; + + if (listenerV4.Start(TreatSocketOpeningErrorAsWarning, cancellationToken)) + { + _listeners.Add(listenerV4); + } + } + + if (!tcpEndpointOptions.BoundInterNetworkV6Address.Equals(IPAddress.None)) + { + var listenerV6 = new MqttTcpServerListener(AddressFamily.InterNetworkV6, _serverOptions, tcpEndpointOptions, tlsCertificate, logger) + { + ClientHandler = OnClientAcceptedAsync + }; + + if (listenerV6.Start(TreatSocketOpeningErrorAsWarning, cancellationToken)) + { + _listeners.Add(listenerV6); + } + } + } + + Task OnClientAcceptedAsync(IMqttChannelAdapter channelAdapter) + { + var clientHandler = ClientHandler; + if (clientHandler == null) + { + return Task.FromResult(0); + } + + return clientHandler(channelAdapter); + } + } +} +#endif \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Implementations/MqttTcpServerListener.cs b/Source/BPA.MQTTnet/Implementations/MqttTcpServerListener.cs new file mode 100644 index 0000000..2f7fb1a --- /dev/null +++ b/Source/BPA.MQTTnet/Implementations/MqttTcpServerListener.cs @@ -0,0 +1,246 @@ +// Licensed to the .NET Foundation under one or more 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; +using MQTTnet.Internal; +using MQTTnet.Server; +using System; +using System.IO; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; + +namespace MQTTnet.Implementations +{ + public sealed class MqttTcpServerListener : IDisposable + { + readonly MqttNetSourceLogger _logger; + readonly IMqttNetLogger _rootLogger; + readonly AddressFamily _addressFamily; + readonly MqttServerOptions _serverOptions; + readonly MqttServerTcpEndpointBaseOptions _options; + readonly MqttServerTlsTcpEndpointOptions _tlsOptions; + readonly X509Certificate2 _tlsCertificate; + + CrossPlatformSocket _socket; + IPEndPoint _localEndPoint; + + public MqttTcpServerListener( + AddressFamily addressFamily, + MqttServerOptions serverOptions, + MqttServerTcpEndpointBaseOptions tcpEndpointOptions, + X509Certificate2 tlsCertificate, + IMqttNetLogger logger) + { + _addressFamily = addressFamily; + _serverOptions = serverOptions ?? throw new ArgumentNullException(nameof(serverOptions)); + _options = tcpEndpointOptions ?? throw new ArgumentNullException(nameof(tcpEndpointOptions)); + _tlsCertificate = tlsCertificate; + _rootLogger = logger; + _logger = logger.WithSource(nameof(MqttTcpServerListener)); + + if (_options is MqttServerTlsTcpEndpointOptions tlsOptions) + { + _tlsOptions = tlsOptions; + } + } + + public Func ClientHandler { get; set; } + + public bool Start(bool treatErrorsAsWarning, CancellationToken cancellationToken) + { + try + { + var boundIp = _options.BoundInterNetworkAddress; + if (_addressFamily == AddressFamily.InterNetworkV6) + { + boundIp = _options.BoundInterNetworkV6Address; + } + + _localEndPoint = new IPEndPoint(boundIp, _options.Port); + + _logger.Info("Starting TCP listener (Endpoint='{0}', TLS={1}).", _localEndPoint, _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; + } + + if (_options.NoDelay) + { + _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; + } + catch (Exception exception) + { + if (!treatErrorsAsWarning) + { + throw; + } + + _logger.Warning(exception, "Error while creating listener socket for local end point '{0}'.", _localEndPoint); + return false; + } + } + + public void Dispose() + { + _socket?.Dispose(); + +#if !NET452 + _tlsCertificate?.Dispose(); +#endif + } + + async Task AcceptClientConnectionsAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + var clientSocket = await _socket.AcceptAsync().ConfigureAwait(false); + if (clientSocket == null) + { + continue; + } + + Task.Run(() => TryHandleClientConnectionAsync(clientSocket), cancellationToken).RunInBackground(_logger); + } + catch (OperationCanceledException) + { + } + catch (Exception exception) + { + if (exception is SocketException socketException) + { + if (socketException.SocketErrorCode == SocketError.ConnectionAborted || + socketException.SocketErrorCode == SocketError.OperationAborted) + { + continue; + } + } + + _logger.Error(exception, "Error while accepting connection at TCP listener {0} TLS={1}.", _localEndPoint, _tlsCertificate != null); + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); + } + } + } + + async Task TryHandleClientConnectionAsync(CrossPlatformSocket clientSocket) + { + Stream stream = null; + string remoteEndPoint = null; + + try + { + remoteEndPoint = clientSocket.RemoteEndPoint.ToString(); + + _logger.Verbose("Client '{0}' accepted by TCP listener '{1}, {2}'.", + remoteEndPoint, + _localEndPoint, + _addressFamily == AddressFamily.InterNetwork ? "ipv4" : "ipv6"); + + clientSocket.NoDelay = _options.NoDelay; + stream = clientSocket.GetStream(); + X509Certificate2 clientCertificate = null; + + if (_tlsCertificate != null) + { + var sslStream = new SslStream(stream, false, _tlsOptions.RemoteCertificateValidationCallback); + + await sslStream.AuthenticateAsServerAsync( + _tlsCertificate, + _tlsOptions.ClientCertificateRequired, + _tlsOptions.SslProtocol, + _tlsOptions.CheckCertificateRevocation).ConfigureAwait(false); + + stream = sslStream; + + clientCertificate = sslStream.RemoteCertificate as X509Certificate2; + + if (clientCertificate == null && sslStream.RemoteCertificate != null) + { + clientCertificate = new X509Certificate2(sslStream.RemoteCertificate.Export(X509ContentType.Cert)); + } + } + + var clientHandler = ClientHandler; + if (clientHandler != null) + { + var tcpChannel = new MqttTcpChannel(stream, remoteEndPoint, clientCertificate); + var bufferWriter = new MqttBufferWriter(_serverOptions.WriterBufferSize, _serverOptions.WriterBufferSizeMax); + var packetFormatterAdapter = new MqttPacketFormatterAdapter(bufferWriter); + + using (var clientAdapter = new MqttChannelAdapter(tcpChannel, packetFormatterAdapter, null, _rootLogger)) + { + await clientHandler(clientAdapter).ConfigureAwait(false); + } + } + } + catch (Exception exception) + { + if (exception is ObjectDisposedException) + { + // It can happen that the listener socket is accessed after the cancellation token is already set and the listener socket is disposed. + return; + } + + if (exception is SocketException socketException && + socketException.SocketErrorCode == SocketError.OperationAborted) + { + return; + } + + _logger.Error(exception, "Error while handling client connection."); + } + finally + { + try + { + stream?.Dispose(); + clientSocket?.Dispose(); + } + catch (Exception disposeException) + { + _logger.Error(disposeException, "Error while cleaning up client connection"); + } + } + + _logger.Verbose("Client '{0}' disconnected at TCP listener '{1}, {2}'.", + remoteEndPoint, + _localEndPoint, + _addressFamily == AddressFamily.InterNetwork ? "ipv4" : "ipv6"); + } + } +} +#endif \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Implementations/MqttWebSocketChannel.cs b/Source/BPA.MQTTnet/Implementations/MqttWebSocketChannel.cs new file mode 100644 index 0000000..7a3b4a9 --- /dev/null +++ b/Source/BPA.MQTTnet/Implementations/MqttWebSocketChannel.cs @@ -0,0 +1,235 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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; +using System.Net.WebSockets; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Client; + +namespace MQTTnet.Implementations +{ + public sealed class MqttWebSocketChannel : IMqttChannel + { + readonly MqttClientWebSocketOptions _options; + + AsyncLock _sendLock = new AsyncLock(); + WebSocket _webSocket; + + public MqttWebSocketChannel(MqttClientWebSocketOptions options) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + public MqttWebSocketChannel(WebSocket webSocket, string endpoint, bool isSecureConnection, X509Certificate2 clientCertificate) + { + _webSocket = webSocket ?? throw new ArgumentNullException(nameof(webSocket)); + + Endpoint = endpoint; + IsSecureConnection = isSecureConnection; + ClientCertificate = clientCertificate; + } + + public string Endpoint { get; } + + public bool IsSecureConnection { get; private set; } + + public X509Certificate2 ClientCertificate { get; } + + public async Task ConnectAsync(CancellationToken cancellationToken) + { + var uri = _options.Uri; + if (!uri.StartsWith("ws://", StringComparison.OrdinalIgnoreCase) && !uri.StartsWith("wss://", StringComparison.OrdinalIgnoreCase)) + { + if (_options.TlsOptions?.UseTls == false) + { + uri = "ws://" + uri; + } + else + { + uri = "wss://" + uri; + } + } + + var clientWebSocket = new ClientWebSocket(); + try + { + SetupClientWebSocket(clientWebSocket); + + await clientWebSocket.ConnectAsync(new Uri(uri), cancellationToken).ConfigureAwait(false); + } + catch (Exception) + { + // Prevent a memory leak when always creating new instance which will fail while connecting. + clientWebSocket.Dispose(); + throw; + } + + _webSocket = clientWebSocket; + IsSecureConnection = uri.StartsWith("wss://", StringComparison.OrdinalIgnoreCase); + } + + public async Task DisconnectAsync(CancellationToken cancellationToken) + { + if (_webSocket == null) + { + return; + } + + if (_webSocket.State == WebSocketState.Open || _webSocket.State == WebSocketState.Connecting) + { + await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, cancellationToken).ConfigureAwait(false); + } + + Cleanup(); + } + + public async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + var response = await _webSocket.ReceiveAsync(new ArraySegment(buffer, offset, count), cancellationToken).ConfigureAwait(false); + return response.Count; + } + + public async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + // The lock is required because the client will throw an exception if _SendAsync_ is + // called from multiple threads at the same time. But this issue only happens with several + // framework versions. + if (_sendLock == null) + { + return; + } + + using (await _sendLock.WaitAsync(cancellationToken).ConfigureAwait(false)) + { + await _webSocket.SendAsync(new ArraySegment(buffer, offset, count), WebSocketMessageType.Binary, true, cancellationToken).ConfigureAwait(false); + } + } + + public void Dispose() + { + Cleanup(); + } + + void SetupClientWebSocket(ClientWebSocket clientWebSocket) + { + + if (_options.ProxyOptions != null) + { + clientWebSocket.Options.Proxy = CreateProxy(); + } + + if (_options.RequestHeaders != null) + { + foreach (var requestHeader in _options.RequestHeaders) + { + clientWebSocket.Options.SetRequestHeader(requestHeader.Key, requestHeader.Value); + } + } + + if (_options.SubProtocols != null) + { + foreach (var subProtocol in _options.SubProtocols) + { + clientWebSocket.Options.AddSubProtocol(subProtocol); + } + } + + if (_options.CookieContainer != null) + { + clientWebSocket.Options.Cookies = _options.CookieContainer; + } + + if (_options.TlsOptions?.UseTls == true && _options.TlsOptions?.Certificates != null) + { + clientWebSocket.Options.ClientCertificates = new X509CertificateCollection(); + foreach (var certificate in _options.TlsOptions.Certificates) + { +#if WINDOWS_UWP + clientWebSocket.Options.ClientCertificates.Add(new X509Certificate(certificate)); +#else + clientWebSocket.Options.ClientCertificates.Add(certificate); +#endif + + } + } + + var certificateValidationHandler = _options.TlsOptions?.CertificateValidationHandler; + if (certificateValidationHandler != null) + { +#if NETSTANDARD1_3 + throw new NotSupportedException("Remote certificate validation callback is not supported when using 'netstandard1.3'."); +#elif NETSTANDARD2_0 + throw new NotSupportedException("Remote certificate validation callback is not supported when using 'netstandard2.0'."); +#elif WINDOWS_UWP + throw new NotSupportedException("Remote certificate validation callback is not supported when using 'uap10.0'."); +#elif NET452 + throw new NotSupportedException("Remote certificate validation callback is not supported when using 'net452'."); +#elif NET461 + throw new NotSupportedException("Remote certificate validation callback is not supported when using 'net461'."); +#else + 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 MqttClientCertificateValidationEventArgs + { + Certificate = certificate, + Chain = chain, + SslPolicyErrors = sslPolicyErrors, + ClientOptions = _options + }; + + return certificateValidationHandler(context); + }; +#endif + } + } + + void Cleanup() + { + _sendLock?.Dispose(); + _sendLock = null; + + try + { + _webSocket?.Dispose(); + } + catch (ObjectDisposedException) + { + } + finally + { + _webSocket = null; + } + } + + IWebProxy CreateProxy() + { + if (string.IsNullOrEmpty(_options.ProxyOptions?.Address)) + { + return null; + } + +#if WINDOWS_UWP + throw new NotSupportedException("Proxies are not supported when using 'uap10.0'."); +#elif NETSTANDARD1_3 + throw new NotSupportedException("Proxies are not supported when using 'netstandard 1.3'."); +#else + var proxyUri = new Uri(_options.ProxyOptions.Address); + + if (!string.IsNullOrEmpty(_options.ProxyOptions.Username) && !string.IsNullOrEmpty(_options.ProxyOptions.Password)) + { + var credentials = new NetworkCredential(_options.ProxyOptions.Username, _options.ProxyOptions.Password, _options.ProxyOptions.Domain); + return new WebProxy(proxyUri, _options.ProxyOptions.BypassOnLocal, _options.ProxyOptions.BypassList, credentials); + } + + return new WebProxy(proxyUri, _options.ProxyOptions.BypassOnLocal, _options.ProxyOptions.BypassList); +#endif + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Implementations/PlatformAbstractionLayer.cs b/Source/BPA.MQTTnet/Implementations/PlatformAbstractionLayer.cs new file mode 100644 index 0000000..11f3ecb --- /dev/null +++ b/Source/BPA.MQTTnet/Implementations/PlatformAbstractionLayer.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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; + +namespace MQTTnet.Implementations +{ + public static class PlatformAbstractionLayer + { +#if NET452 + public static Task CompletedTask => Task.FromResult(0); + + public static byte[] EmptyByteArray { get; } = new byte[0]; +#else + public static Task CompletedTask => Task.CompletedTask; + + 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 + try + { + System.Threading.Thread.Sleep(timeout); + } + catch (ThreadAbortException) + { + // The ThreadAbortException is not actively catched in this project. + // So we use a one which is similar and will be catched properly. + throw new OperationCanceledException(); + } +#else + Task.Delay(timeout).Wait(); +#endif + } + } +} diff --git a/Source/BPA.MQTTnet/Internal/AsyncEvent.cs b/Source/BPA.MQTTnet/Internal/AsyncEvent.cs new file mode 100644 index 0000000..3edf7a7 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Internal/AsyncEventInvocator.cs b/Source/BPA.MQTTnet/Internal/AsyncEventInvocator.cs new file mode 100644 index 0000000..5554183 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Internal/AsyncLock.cs b/Source/BPA.MQTTnet/Internal/AsyncLock.cs new file mode 100644 index 0000000..54b533c --- /dev/null +++ b/Source/BPA.MQTTnet/Internal/AsyncLock.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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; + +namespace MQTTnet.Internal +{ + public sealed class AsyncLock : IDisposable + { + readonly object _syncRoot = new object(); + readonly Task _releaser; + + SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); + + public AsyncLock() + { + _releaser = Task.FromResult((IDisposable)new Releaser(this)); + } + + public Task WaitAsync(CancellationToken cancellationToken) + { + Task task; + + // This lock is required to avoid ObjectDisposedExceptions. + // These are fired when this lock gets disposed (and thus the semaphore) + // and a worker thread tries to call this method at the same time. + // Another way would be catching all ObjectDisposedExceptions but this situation happens + // quite often when clients are disconnecting. + lock (_syncRoot) + { + task = _semaphore?.WaitAsync(cancellationToken); + } + + if (task == null) + { + throw new ObjectDisposedException("The AsyncLock is disposed."); + } + + if (task.Status == TaskStatus.RanToCompletion) + { + return _releaser; + } + + // Wait for the _WaitAsync_ method and return the releaser afterwards. + return task.ContinueWith( + (_, state) => (IDisposable)state, + _releaser.Result, + cancellationToken, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + } + + public void Dispose() + { + lock (_syncRoot) + { + _semaphore?.Dispose(); + _semaphore = null; + } + } + + internal void Release() + { + lock (_syncRoot) + { + _semaphore?.Release(); + } + } + + sealed class Releaser : IDisposable + { + readonly AsyncLock _lock; + + internal Releaser(AsyncLock @lock) + { + _lock = @lock; + } + + public void Dispose() + { + _lock.Release(); + } + } + } +} diff --git a/Source/BPA.MQTTnet/Internal/AsyncQueue.cs b/Source/BPA.MQTTnet/Internal/AsyncQueue.cs new file mode 100644 index 0000000..5605c0f --- /dev/null +++ b/Source/BPA.MQTTnet/Internal/AsyncQueue.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 System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; + +namespace MQTTnet.Internal +{ + public sealed class AsyncQueue : IDisposable + { + readonly object _syncRoot = new object(); + SemaphoreSlim _semaphore = new SemaphoreSlim(0); + ConcurrentQueue _queue = new ConcurrentQueue(); + + public int Count => _queue.Count; + + public void Enqueue(TItem item) + { + lock (_syncRoot) + { + _queue.Enqueue(item); + _semaphore?.Release(); + } + } + + public async Task> TryDequeueAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + Task task; + lock (_syncRoot) + { + if (_semaphore == null) + { + return new AsyncQueueDequeueResult(false, default); + } + + task = _semaphore.WaitAsync(cancellationToken); + } + + await task.ConfigureAwait(false); + + if (cancellationToken.IsCancellationRequested) + { + return new AsyncQueueDequeueResult(false, default); + } + + if (_queue.TryDequeue(out var item)) + { + return new AsyncQueueDequeueResult(true, item); + } + } + catch (ArgumentNullException) + { + // The semaphore throws this internally sometimes. + return new AsyncQueueDequeueResult(false, default); + } + catch (OperationCanceledException) + { + return new AsyncQueueDequeueResult(false, default); + } + } + + return new AsyncQueueDequeueResult(false, default); + } + + public AsyncQueueDequeueResult TryDequeue() + { + if (_queue.TryDequeue(out var item)) + { + return new AsyncQueueDequeueResult(true, item); + } + + return new AsyncQueueDequeueResult(false, default); + } + + public void Clear() + { + Interlocked.Exchange(ref _queue, new ConcurrentQueue()); + } + + public void Dispose() + { + lock (_syncRoot) + { + _semaphore?.Dispose(); + _semaphore = null; + } + } + } +} diff --git a/Source/BPA.MQTTnet/Internal/AsyncQueueDequeueResult.cs b/Source/BPA.MQTTnet/Internal/AsyncQueueDequeueResult.cs new file mode 100644 index 0000000..6848e1f --- /dev/null +++ b/Source/BPA.MQTTnet/Internal/AsyncQueueDequeueResult.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. + +namespace MQTTnet.Internal +{ + public class AsyncQueueDequeueResult + { + public AsyncQueueDequeueResult(bool isSuccess, TItem item) + { + IsSuccess = isSuccess; + Item = item; + } + + public bool IsSuccess { get; } + + public TItem Item { get; } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Internal/AsyncTaskCompletionSource.cs b/Source/BPA.MQTTnet/Internal/AsyncTaskCompletionSource.cs new file mode 100644 index 0000000..a77b56d --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Internal/BlockingQueue.cs b/Source/BPA.MQTTnet/Internal/BlockingQueue.cs new file mode 100644 index 0000000..b31ed09 --- /dev/null +++ b/Source/BPA.MQTTnet/Internal/BlockingQueue.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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; + +namespace MQTTnet.Internal +{ + public sealed class BlockingQueue : IDisposable + { + readonly object _syncRoot = new object(); + readonly LinkedList _items = new LinkedList(); + + ManualResetEventSlim _gate = new ManualResetEventSlim(false); + + public int Count + { + get + { + lock (_syncRoot) + { + return _items.Count; + } + } + } + + public void Enqueue(TItem item) + { + if (item == null) throw new ArgumentNullException(nameof(item)); + + lock (_syncRoot) + { + _items.AddLast(item); + _gate?.Set(); + } + } + + public TItem Dequeue(CancellationToken cancellationToken = default) + { + while (!cancellationToken.IsCancellationRequested) + { + lock (_syncRoot) + { + if (_items.Count > 0) + { + var item = _items.First.Value; + _items.RemoveFirst(); + + return item; + } + + if (_items.Count == 0) + { + _gate?.Reset(); + } + } + + _gate?.Wait(cancellationToken); + } + + throw new OperationCanceledException(); + } + + public TItem PeekAndWait(CancellationToken cancellationToken = default) + { + while (!cancellationToken.IsCancellationRequested) + { + lock (_syncRoot) + { + if (_items.Count > 0) + { + return _items.First.Value; + } + + if (_items.Count == 0) + { + _gate?.Reset(); + } + } + + _gate?.Wait(cancellationToken); + } + + throw new OperationCanceledException(); + } + + public void RemoveFirst(Predicate match) + { + if (match == null) throw new ArgumentNullException(nameof(match)); + + lock (_syncRoot) + { + if (_items.Count > 0 && match(_items.First.Value)) + { + _items.RemoveFirst(); + } + } + } + + public TItem RemoveFirst() + { + lock (_syncRoot) + { + var item = _items.First; + _items.RemoveFirst(); + + return item.Value; + } + } + + public void Clear() + { + lock (_syncRoot) + { + _items.Clear(); + } + } + + public void Dispose() + { + _gate?.Dispose(); + _gate = null; + } + } +} diff --git a/Source/BPA.MQTTnet/Internal/Disposable.cs b/Source/BPA.MQTTnet/Internal/Disposable.cs new file mode 100644 index 0000000..0adb347 --- /dev/null +++ b/Source/BPA.MQTTnet/Internal/Disposable.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; + +namespace MQTTnet.Internal +{ + public abstract class Disposable : IDisposable + { + protected bool IsDisposed { get; private set; } + + protected void ThrowIfDisposed() + { + if (IsDisposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } + + protected virtual void Dispose(bool disposing) + { + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + + if (IsDisposed) + { + return; + } + + IsDisposed = true; + + Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Source/BPA.MQTTnet/Internal/MqttPacketBus.cs b/Source/BPA.MQTTnet/Internal/MqttPacketBus.cs new file mode 100644 index 0000000..e5919c1 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Internal/MqttPacketBusItem.cs b/Source/BPA.MQTTnet/Internal/MqttPacketBusItem.cs new file mode 100644 index 0000000..9122453 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Internal/MqttPacketBusPartition.cs b/Source/BPA.MQTTnet/Internal/MqttPacketBusPartition.cs new file mode 100644 index 0000000..d870039 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Internal/MqttTaskTimeout.cs b/Source/BPA.MQTTnet/Internal/MqttTaskTimeout.cs new file mode 100644 index 0000000..abd05c3 --- /dev/null +++ b/Source/BPA.MQTTnet/Internal/MqttTaskTimeout.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Exceptions; + +namespace MQTTnet.Internal +{ + public static class MqttTaskTimeout + { + 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 + { + 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/BPA.MQTTnet/Internal/TaskExtensions.cs b/Source/BPA.MQTTnet/Internal/TaskExtensions.cs new file mode 100644 index 0000000..d4c8c4d --- /dev/null +++ b/Source/BPA.MQTTnet/Internal/TaskExtensions.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 MQTTnet.Diagnostics; +using System.Threading.Tasks; + +namespace MQTTnet.Internal +{ + public static class TaskExtensions + { + public static void RunInBackground(this Task task, MqttNetSourceLogger logger = null) + { + task?.ContinueWith(t => + { + // Consume the exception first so that we get no exception regarding the not observed exception. + var exception = t.Exception; + logger?.Error(exception, "Unhandled exception in background task."); + }, + TaskContinuationOptions.OnlyOnFaulted); + } + } +} diff --git a/Source/BPA.MQTTnet/LowLevelClient/LowLevelMqttClient.cs b/Source/BPA.MQTTnet/LowLevelClient/LowLevelMqttClient.cs new file mode 100644 index 0000000..3e325c2 --- /dev/null +++ b/Source/BPA.MQTTnet/LowLevelClient/LowLevelMqttClient.cs @@ -0,0 +1,147 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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.Adapter; +using MQTTnet.Client; +using MQTTnet.Diagnostics; +using MQTTnet.Exceptions; +using MQTTnet.Internal; +using MQTTnet.Packets; + +namespace MQTTnet.LowLevelClient +{ + public sealed class LowLevelMqttClient : IDisposable + { + readonly IMqttClientAdapterFactory _clientAdapterFactory; + readonly AsyncEvent _inspectPacketEvent = new AsyncEvent(); + readonly MqttNetSourceLogger _logger; + + readonly IMqttNetLogger _rootLogger; + + IMqttChannelAdapter _adapter; + + public LowLevelMqttClient(IMqttClientAdapterFactory clientAdapterFactory, IMqttNetLogger logger) + { + _clientAdapterFactory = clientAdapterFactory ?? throw new ArgumentNullException(nameof(clientAdapterFactory)); + + _rootLogger = logger ?? throw new ArgumentNullException(nameof(logger)); + _logger = logger.WithSource(nameof(LowLevelMqttClient)); + } + + public event Func InspectPackage + { + add => _inspectPacketEvent.AddHandler(value); + remove => _inspectPacketEvent.RemoveHandler(value); + } + + public bool IsConnected => _adapter != null; + + public async Task ConnectAsync(MqttClientOptions options, CancellationToken cancellationToken) + { + 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, new MqttPacketInspector(_inspectPacketEvent, _rootLogger), _rootLogger); + + try + { + _logger.Verbose("Trying to connect with server '{0}'.", options.ChannelOptions); + await newAdapter.ConnectAsync(cancellationToken).ConfigureAwait(false); + _logger.Verbose("Connection with server established."); + } + catch (Exception) + { + _adapter?.Dispose(); + throw; + } + + _adapter = newAdapter; + } + + public async Task DisconnectAsync(CancellationToken cancellationToken) + { + var adapter = _adapter; + if (adapter == null) + { + throw new InvalidOperationException("Low level MQTT client is not connected."); + } + + try + { + await adapter.DisconnectAsync(cancellationToken).ConfigureAwait(false); + } + catch + { + Dispose(); + throw; + } + } + + public void Dispose() + { + _adapter?.Dispose(); + _adapter = null; + } + + public async Task ReceiveAsync(CancellationToken cancellationToken) + { + var adapter = _adapter; + if (adapter == null) + { + throw new InvalidOperationException("Low level MQTT client is not connected."); + } + + try + { + var receivedPacket = await adapter.ReceivePacketAsync(cancellationToken).ConfigureAwait(false); + if (receivedPacket == null) + { + // Graceful socket close. + throw new MqttCommunicationException("The connection is closed."); + } + + return receivedPacket; + } + catch + { + Dispose(); + throw; + } + } + + public async Task SendAsync(MqttPacket packet, CancellationToken cancellationToken) + { + if (packet is null) + { + throw new ArgumentNullException(nameof(packet)); + } + + var adapter = _adapter; + if (adapter == null) + { + throw new InvalidOperationException("Low level MQTT client is not connected."); + } + + try + { + await adapter.SendPacketAsync(packet, cancellationToken).ConfigureAwait(false); + } + catch + { + Dispose(); + throw; + } + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/MQTTnet.csproj.DotSettings b/Source/BPA.MQTTnet/MQTTnet.csproj.DotSettings new file mode 100644 index 0000000..4af6589 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/MqttApplicationMessage.cs b/Source/BPA.MQTTnet/MqttApplicationMessage.cs new file mode 100644 index 0000000..ff94737 --- /dev/null +++ b/Source/BPA.MQTTnet/MqttApplicationMessage.cs @@ -0,0 +1,133 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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 sealed class MqttApplicationMessage + { + /// + /// 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 ContentType { 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. + /// + public byte[] CorrelationData { 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]. + /// + public bool Dup { 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. + /// + public uint MessageExpiryInterval { get; set; } + + /// + /// Gets or sets the payload. + /// The payload is the data bytes sent via the MQTT protocol. + /// + public byte[] Payload { 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. + /// + public MqttPayloadFormatIndicator PayloadFormatIndicator { get; set; } = MqttPayloadFormatIndicator.Unspecified; + + /// + /// 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 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 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 bool Retain { 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. + /// + public List SubscriptionIdentifiers { 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; } + + /// + /// 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 ushort TopicAlias { 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. + /// + public List UserProperties { get; set; } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/MqttApplicationMessageBuilder.cs b/Source/BPA.MQTTnet/MqttApplicationMessageBuilder.cs new file mode 100644 index 0000000..2f1674e --- /dev/null +++ b/Source/BPA.MQTTnet/MqttApplicationMessageBuilder.cs @@ -0,0 +1,339 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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; +using System.Text; +using MQTTnet.Exceptions; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet +{ + public sealed class MqttApplicationMessageBuilder + { + string _contentType; + byte[] _correlationData; + uint _messageExpiryInterval; + byte[] _payload; + MqttPayloadFormatIndicator _payloadFormatIndicator; + MqttQualityOfServiceLevel _qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce; + string _responseTopic; + bool _retain; + List _subscriptionIdentifiers; + string _topic; + ushort _topicAlias; + List _userProperties; + + public MqttApplicationMessage Build() + { + if (_topicAlias == 0 && string.IsNullOrEmpty(_topic)) + { + throw new MqttProtocolViolationException("Topic or TopicAlias is not set."); + } + + 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 + }; + + return applicationMessage; + } + + /// + /// Adds the content type to the message. + /// Hint: MQTT 5 feature only. + /// + /// + /// A new instance of the + /// + /// class. + /// + public MqttApplicationMessageBuilder WithContentType(string contentType) + { + _contentType = contentType; + return this; + } + + /// + /// 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 message expiry interval in seconds to the message. + /// Hint: MQTT 5 feature only. + /// + /// + /// The message expiry interval. + /// + /// + /// A new instance of the + /// + /// class. + /// + public MqttApplicationMessageBuilder WithMessageExpiryInterval(uint messageExpiryInterval) + { + _messageExpiryInterval = messageExpiryInterval; + return this; + } + + public MqttApplicationMessageBuilder WithPayload(byte[] payload) + { + _payload = payload; + return this; + } + + public MqttApplicationMessageBuilder WithPayload(IEnumerable payload) + { + if (payload == null) + { + _payload = null; + return this; + } + + _payload = payload as byte[] ?? payload.ToArray(); + + return this; + } + + public MqttApplicationMessageBuilder WithPayload(Stream payload) + { + if (payload == null) + { + _payload = null; + return this; + } + + return WithPayload(payload, payload.Length - payload.Position); + } + + public MqttApplicationMessageBuilder WithPayload(Stream payload, long length) + { + if (payload == null) + { + _payload = null; + return this; + } + + if (payload.Length == 0) + { + _payload = null; + } + else + { + _payload = new byte[length]; + + var totalRead = 0; + do + { + var bytesRead = payload.Read(_payload, totalRead, _payload.Length - totalRead); + if (bytesRead == 0) + { + break; + } + + totalRead += bytesRead; + } while (totalRead < length); + } + + return this; + } + + public MqttApplicationMessageBuilder WithPayload(string payload) + { + if (payload == null) + { + _payload = null; + return this; + } + + _payload = string.IsNullOrEmpty(payload) ? null : Encoding.UTF8.GetBytes(payload); + return this; + } + + /// + /// Adds the payload format indicator to the message. + /// Hint: MQTT 5 feature only. + /// + /// + /// The payload format indicator. + /// + /// + /// A new instance of the + /// + /// class. + /// + public MqttApplicationMessageBuilder WithPayloadFormatIndicator(MqttPayloadFormatIndicator payloadFormatIndicator) + { + _payloadFormatIndicator = payloadFormatIndicator; + return this; + } + + /// + /// 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 MqttApplicationMessageBuilder WithQualityOfServiceLevel(MqttQualityOfServiceLevel qualityOfServiceLevel) + { + _qualityOfServiceLevel = qualityOfServiceLevel; + return this; + } + + /// + /// Adds the response topic to the message. + /// Hint: MQTT 5 feature only. + /// + /// + /// The response topic. + /// + /// + /// A new instance of the + /// + /// class. + /// + public MqttApplicationMessageBuilder WithResponseTopic(string responseTopic) + { + _responseTopic = responseTopic; + return this; + } + + /// + /// 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 MqttApplicationMessageBuilder WithRetainFlag(bool value = true) + { + _retain = value; + return this; + } + + /// + /// Adds the subscription identifier to the message. + /// Hint: MQTT 5 feature only. + /// + /// + /// The subscription identifier. + /// + /// + /// A new instance of the + /// + /// class. + /// + public MqttApplicationMessageBuilder WithSubscriptionIdentifier(uint subscriptionIdentifier) + { + if (_subscriptionIdentifiers == null) + { + _subscriptionIdentifiers = new List(); + } + + _subscriptionIdentifiers.Add(subscriptionIdentifier); + return this; + } + + /// + /// 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 MqttApplicationMessageBuilder WithTopic(string topic) + { + _topic = topic; + return this; + } + + /// + /// Adds the topic alias to the message. + /// Hint: MQTT 5 feature only. + /// + /// + /// The topic alias. + /// + /// + /// A new instance of the + /// + /// class. + /// + public MqttApplicationMessageBuilder WithTopicAlias(ushort topicAlias) + { + _topicAlias = topicAlias; + return this; + } + + /// + /// 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 (_userProperties == null) + { + _userProperties = new List(); + } + + _userProperties.Add(new MqttUserProperty(name, value)); + return this; + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/MqttApplicationMessageExtensions.cs b/Source/BPA.MQTTnet/MqttApplicationMessageExtensions.cs new file mode 100644 index 0000000..1bfd69a --- /dev/null +++ b/Source/BPA.MQTTnet/MqttApplicationMessageExtensions.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Text; + +namespace MQTTnet +{ + public static class MqttApplicationMessageExtensions + { + public static string ConvertPayloadToString(this MqttApplicationMessage applicationMessage) + { + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + + if (applicationMessage.Payload == null) + { + return null; + } + + if (applicationMessage.Payload.Length == 0) + { + return string.Empty; + } + + return Encoding.UTF8.GetString(applicationMessage.Payload, 0, applicationMessage.Payload.Length); + } + } +} diff --git a/Source/BPA.MQTTnet/MqttFactory.cs b/Source/BPA.MQTTnet/MqttFactory.cs new file mode 100644 index 0000000..17ab896 --- /dev/null +++ b/Source/BPA.MQTTnet/MqttFactory.cs @@ -0,0 +1,165 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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; +using MQTTnet.Server; +using System; +using System.Collections.Generic; +using System.Linq; +using MQTTnet.Diagnostics; +using MqttClient = MQTTnet.Client.MqttClient; + +namespace MQTTnet +{ + public sealed class MqttFactory + { + IMqttClientAdapterFactory _clientAdapterFactory; + + public MqttFactory() : this(new MqttNetNullLogger()) + { + } + + public MqttFactory(IMqttNetLogger logger) + { + DefaultLogger = logger ?? throw new ArgumentNullException(nameof(logger)); + + _clientAdapterFactory = new MqttClientAdapterFactory(); + } + + public IMqttNetLogger DefaultLogger { get; } + + public IList> DefaultServerAdapters { get; } = new List> + { + factory => new MqttTcpServerAdapter() + }; + + public IDictionary Properties { get; } = new Dictionary(); + + public MqttFactory UseClientAdapterFactory(IMqttClientAdapterFactory clientAdapterFactory) + { + _clientAdapterFactory = clientAdapterFactory ?? throw new ArgumentNullException(nameof(clientAdapterFactory)); + return this; + } + + public LowLevelMqttClient CreateLowLevelMqttClient() + { + return CreateLowLevelMqttClient(DefaultLogger); + } + + public LowLevelMqttClient CreateLowLevelMqttClient(IMqttNetLogger logger) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + return new LowLevelMqttClient(_clientAdapterFactory, logger); + } + + public LowLevelMqttClient CreateLowLevelMqttClient(IMqttClientAdapterFactory clientAdapterFactory) + { + if (clientAdapterFactory == null) throw new ArgumentNullException(nameof(clientAdapterFactory)); + + return new LowLevelMqttClient(_clientAdapterFactory, DefaultLogger); + } + + public LowLevelMqttClient CreateLowLevelMqttClient(IMqttNetLogger logger, IMqttClientAdapterFactory clientAdapterFactory) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + if (clientAdapterFactory == null) throw new ArgumentNullException(nameof(clientAdapterFactory)); + + return new LowLevelMqttClient(_clientAdapterFactory, logger); + } + + public MqttClient CreateMqttClient() + { + return CreateMqttClient(DefaultLogger); + } + + public MqttClient CreateMqttClient(IMqttNetLogger logger) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + return new MqttClient(_clientAdapterFactory, logger); + } + + public MqttClient CreateMqttClient(IMqttClientAdapterFactory clientAdapterFactory) + { + if (clientAdapterFactory == null) throw new ArgumentNullException(nameof(clientAdapterFactory)); + + return new MqttClient(clientAdapterFactory, DefaultLogger); + } + + public MqttClient CreateMqttClient(IMqttNetLogger logger, IMqttClientAdapterFactory clientAdapterFactory) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + if (clientAdapterFactory == null) throw new ArgumentNullException(nameof(clientAdapterFactory)); + + return new MqttClient(clientAdapterFactory, logger); + } + + public MqttServer CreateMqttServer(MqttServerOptions options) + { + return CreateMqttServer(options, DefaultLogger); + } + + public MqttServer CreateMqttServer(MqttServerOptions options, IMqttNetLogger logger) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + var serverAdapters = DefaultServerAdapters.Select(a => a.Invoke(this)); + return CreateMqttServer(options, serverAdapters, logger); + } + + public MqttServer CreateMqttServer(MqttServerOptions options, IEnumerable serverAdapters, IMqttNetLogger logger) + { + if (serverAdapters == null) throw new ArgumentNullException(nameof(serverAdapters)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + return new MqttServer(options, serverAdapters, logger); + } + + public MqttServer CreateMqttServer(MqttServerOptions options, IEnumerable serverAdapters) + { + if (serverAdapters == null) throw new ArgumentNullException(nameof(serverAdapters)); + + return new MqttServer(options, serverAdapters, DefaultLogger); + } + + public MqttServerOptionsBuilder CreateServerOptionsBuilder() + { + return new MqttServerOptionsBuilder(); + } + + public MqttClientOptionsBuilder CreateClientOptionsBuilder() + { + return new MqttClientOptionsBuilder(); + } + + public MqttClientDisconnectOptionsBuilder CreateClientDisconnectOptionsBuilder() + { + return new MqttClientDisconnectOptionsBuilder(); + } + + public MqttClientSubscribeOptionsBuilder CreateSubscribeOptionsBuilder() + { + return new MqttClientSubscribeOptionsBuilder(); + } + + public MqttClientUnsubscribeOptionsBuilder CreateUnsubscribeOptionsBuilder() + { + return new MqttClientUnsubscribeOptionsBuilder(); + } + + public MqttTopicFilterBuilder CreateTopicFilterBuilder() + { + return new MqttTopicFilterBuilder(); + } + + public MqttApplicationMessageBuilder CreateApplicationMessageBuilder() + { + return new MqttApplicationMessageBuilder(); + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/MqttTopicFilterBuilder.cs b/Source/BPA.MQTTnet/MqttTopicFilterBuilder.cs new file mode 100644 index 0000000..2140745 --- /dev/null +++ b/Source/BPA.MQTTnet/MqttTopicFilterBuilder.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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 MQTTnet.Packets; + +namespace MQTTnet +{ + public sealed class MqttTopicFilterBuilder + { + /// + /// 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; + bool _noLocal; + bool _retainAsPublished; + MqttRetainHandling _retainHandling = MqttRetainHandling.SendAtSubscribe; + + public MqttTopicFilterBuilder WithTopic(string topic) + { + _topic = topic; + return this; + } + + public MqttTopicFilterBuilder WithQualityOfServiceLevel(MqttQualityOfServiceLevel qualityOfServiceLevel) + { + _qualityOfServiceLevel = qualityOfServiceLevel; + return this; + } + + public MqttTopicFilterBuilder WithAtLeastOnceQoS() + { + _qualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce; + return this; + } + + public MqttTopicFilterBuilder WithAtMostOnceQoS() + { + _qualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce; + return this; + } + + public MqttTopicFilterBuilder WithExactlyOnceQoS() + { + _qualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce; + return this; + } + + public MqttTopicFilterBuilder WithNoLocal(bool value = true) + { + _noLocal = value; + return this; + } + + public MqttTopicFilterBuilder WithRetainAsPublished(bool value = true) + { + _retainAsPublished = value; + return this; + } + + public MqttTopicFilterBuilder WithRetainHandling(MqttRetainHandling value) + { + _retainHandling = value; + return this; + } + + public MqttTopicFilter Build() + { + if (string.IsNullOrEmpty(_topic)) + { + throw new MqttProtocolViolationException("Topic is not set."); + } + + return new MqttTopicFilter + { + Topic = _topic, + QualityOfServiceLevel = _qualityOfServiceLevel, + NoLocal = _noLocal, + RetainAsPublished = _retainAsPublished, + RetainHandling = _retainHandling + }; + } + } +} diff --git a/Source/BPA.MQTTnet/MqttTopicFilterCompareResult.cs b/Source/BPA.MQTTnet/MqttTopicFilterCompareResult.cs new file mode 100644 index 0000000..5ce59a5 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/MqttTopicFilterComparer.cs b/Source/BPA.MQTTnet/MqttTopicFilterComparer.cs new file mode 100644 index 0000000..72285a8 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/PacketDispatcher/IMqttPacketAwaitable.cs b/Source/BPA.MQTTnet/PacketDispatcher/IMqttPacketAwaitable.cs new file mode 100644 index 0000000..a467edb --- /dev/null +++ b/Source/BPA.MQTTnet/PacketDispatcher/IMqttPacketAwaitable.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.Packets; +using System; + +namespace MQTTnet.PacketDispatcher +{ + public interface IMqttPacketAwaitable : IDisposable + { + MqttPacketAwaitableFilter Filter { get; } + + void Complete(MqttPacket packet); + + void Fail(Exception exception); + + void Cancel(); + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/PacketDispatcher/MqttPacketAwaitable.cs b/Source/BPA.MQTTnet/PacketDispatcher/MqttPacketAwaitable.cs new file mode 100644 index 0000000..895e2b5 --- /dev/null +++ b/Source/BPA.MQTTnet/PacketDispatcher/MqttPacketAwaitable.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 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 : MqttPacket + { + readonly AsyncTaskCompletionSource _promise = new AsyncTaskCompletionSource(); + readonly MqttPacketDispatcher _owningPacketDispatcher; + + public MqttPacketAwaitable(ushort packetIdentifier, MqttPacketDispatcher owningPacketDispatcher) + { + Filter = new MqttPacketAwaitableFilter + { + Type = typeof(TPacket), + Identifier = packetIdentifier + }; + + _owningPacketDispatcher = owningPacketDispatcher ?? throw new ArgumentNullException(nameof(owningPacketDispatcher)); + } + + public MqttPacketAwaitableFilter Filter { get; } + + public async Task WaitOneAsync(CancellationToken cancellationToken) + { + using (cancellationToken.Register(() => Fail(new MqttCommunicationTimedOutException()))) + { + var packet = await _promise.Task.ConfigureAwait(false); + return (TPacket)packet; + } + } + + public void Complete(MqttPacket packet) + { + if (packet == null) throw new ArgumentNullException(nameof(packet)); + + _promise.TrySetResult(packet); + } + + public void Fail(Exception exception) + { + if (exception == null) throw new ArgumentNullException(nameof(exception)); + + _promise.TrySetException(exception); + } + + public void Cancel() + { + _promise.TrySetCanceled(); + } + + public void Dispose() + { + _owningPacketDispatcher.RemoveAwaitable(this); + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/PacketDispatcher/MqttPacketAwaitableFilter.cs b/Source/BPA.MQTTnet/PacketDispatcher/MqttPacketAwaitableFilter.cs new file mode 100644 index 0000000..ab94969 --- /dev/null +++ b/Source/BPA.MQTTnet/PacketDispatcher/MqttPacketAwaitableFilter.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.PacketDispatcher +{ + public sealed class MqttPacketAwaitableFilter + { + public Type Type { get; set; } + + public ushort Identifier { get; set; } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/PacketDispatcher/MqttPacketDispatcher.cs b/Source/BPA.MQTTnet/PacketDispatcher/MqttPacketDispatcher.cs new file mode 100644 index 0000000..dae92f7 --- /dev/null +++ b/Source/BPA.MQTTnet/PacketDispatcher/MqttPacketDispatcher.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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; + +namespace MQTTnet.PacketDispatcher +{ + public sealed class MqttPacketDispatcher + { + readonly List _awaitables = new List(); + + public void FailAll(Exception exception) + { + if (exception == null) throw new ArgumentNullException(nameof(exception)); + + lock (_awaitables) + { + foreach (var awaitable in _awaitables) + { + awaitable.Fail(exception); + } + + _awaitables.Clear(); + } + } + + public void CancelAll() + { + lock (_awaitables) + { + foreach (var awaitable in _awaitables) + { + awaitable.Cancel(); + } + + _awaitables.Clear(); + } + } + + public bool TryDispatch(MqttPacket packet) + { + if (packet == null) throw new ArgumentNullException(nameof(packet)); + + ushort identifier = 0; + if (packet is MqttPacketWithIdentifier packetWithIdentifier) + { + identifier = packetWithIdentifier.PacketIdentifier; + } + + var packetType = packet.GetType(); + var awaitables = new List(); + + lock (_awaitables) + { + for (var i = _awaitables.Count - 1; i >= 0; i--) + { + var entry = _awaitables[i]; + + // Note: The PingRespPacket will also arrive here and has NO identifier but there + // is code which waits for it. So the code must be able to deal with filters which + // are referring to the type only (identifier is 0)! + if (entry.Filter.Type != packetType || entry.Filter.Identifier != identifier) + { + continue; + } + + awaitables.Add(entry); + _awaitables.RemoveAt(i); + } + } + + foreach (var matchingEntry in awaitables) + { + matchingEntry.Complete(packet); + } + + return awaitables.Count > 0; + } + + public MqttPacketAwaitable AddAwaitable(ushort packetIdentifier) where TResponsePacket : MqttPacket + { + var awaitable = new MqttPacketAwaitable(packetIdentifier, this); + + lock (_awaitables) + { + _awaitables.Add(awaitable); + } + + return awaitable; + } + + public void RemoveAwaitable(IMqttPacketAwaitable awaitable) + { + if (awaitable == null) throw new ArgumentNullException(nameof(awaitable)); + + lock (_awaitables) + { + _awaitables.Remove(awaitable); + } + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Packets/MqttAuthPacket.cs b/Source/BPA.MQTTnet/Packets/MqttAuthPacket.cs new file mode 100644 index 0000000..e29c751 --- /dev/null +++ b/Source/BPA.MQTTnet/Packets/MqttAuthPacket.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.Protocol; + +namespace MQTTnet.Packets +{ + /// 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 string ReasonString { get; set; } + + public List UserProperties { get; set; } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Packets/MqttConnAckPacket.cs b/Source/BPA.MQTTnet/Packets/MqttConnAckPacket.cs new file mode 100644 index 0000000..3ae7bc6 --- /dev/null +++ b/Source/BPA.MQTTnet/Packets/MqttConnAckPacket.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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 : MqttPacket + { + /// + /// Added in MQTTv5. + /// + public string AssignedClientIdentifier { get; set; } + + public byte[] AuthenticationData { get; set; } + + public string AuthenticationMethod { get; set; } + + /// + /// Added in MQTTv3.1.1. + /// + public bool IsSessionPresent { get; set; } + + public uint MaximumPacketSize { get; set; } + + public MqttQualityOfServiceLevel MaximumQoS { get; set; } + + /// + /// Added in MQTTv5. + /// + 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 $"ConnAck: [ReturnCode={ReturnCode}] [ReasonCode={ReasonCode}] [IsSessionPresent={IsSessionPresent}]"; + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Packets/MqttConnectPacket.cs b/Source/BPA.MQTTnet/Packets/MqttConnectPacket.cs new file mode 100644 index 0000000..a543f5b --- /dev/null +++ b/Source/BPA.MQTTnet/Packets/MqttConnectPacket.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.Protocol; + +namespace MQTTnet.Packets +{ + 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 byte[] WillCorrelationData { get; set; } + + public ushort KeepAlivePeriod { get; set; } + + public uint MaximumPacketSize { get; set; } + + public byte[] Password { get; set; } + + public ushort ReceiveMaximum { 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 MqttQualityOfServiceLevel WillQoS { get; set; } = MqttQualityOfServiceLevel.AtMostOnce; + + public bool WillRetain { get; set; } + + public string WillTopic { get; set; } + + public List WillUserProperties { get; set; } + + public override string ToString() + { + var passwordText = string.Empty; + + if (Password != null) + { + passwordText = "****"; + } + + return $"Connect: [ClientId={ClientId}] [Username={Username}] [Password={passwordText}] [KeepAlivePeriod={KeepAlivePeriod}] [CleanSession={CleanSession}]"; + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Packets/MqttDisconnectPacket.cs b/Source/BPA.MQTTnet/Packets/MqttDisconnectPacket.cs new file mode 100644 index 0000000..ea39e11 --- /dev/null +++ b/Source/BPA.MQTTnet/Packets/MqttDisconnectPacket.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 System.Collections.Generic; +using MQTTnet.Protocol; + +namespace MQTTnet.Packets +{ + public sealed class MqttDisconnectPacket : MqttPacket + { + /// + /// Added in MQTTv5. + /// + public MqttDisconnectReasonCode ReasonCode { get; set; } = MqttDisconnectReasonCode.NormalDisconnection; + + /// + /// Added in MQTTv5. + /// + public string ReasonString { get; set; } + + /// + /// Added in MQTTv5. + /// + public string ServerReference { get; set; } + + /// + /// Added in MQTTv5. + /// + public uint SessionExpiryInterval { get; set; } + + /// + /// Added in MQTTv5. + /// + public List UserProperties { get; set; } + + public override string ToString() + { + return $"Disconnect: [ReasonCode={ReasonCode}]"; + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Packets/MqttPacket.cs b/Source/BPA.MQTTnet/Packets/MqttPacket.cs new file mode 100644 index 0000000..9d0af89 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Packets/MqttPacketWithIdentifier.cs b/Source/BPA.MQTTnet/Packets/MqttPacketWithIdentifier.cs new file mode 100644 index 0000000..99caa7b --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Packets/MqttPingReqPacket.cs b/Source/BPA.MQTTnet/Packets/MqttPingReqPacket.cs new file mode 100644 index 0000000..9ca4764 --- /dev/null +++ b/Source/BPA.MQTTnet/Packets/MqttPingReqPacket.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.Packets +{ + public sealed class MqttPingReqPacket : MqttPacket + { + // This is a minor performance improvement. + public static readonly MqttPingReqPacket Instance = new MqttPingReqPacket(); + + public override string ToString() + { + return "PingReq"; + } + } +} diff --git a/Source/BPA.MQTTnet/Packets/MqttPingRespPacket.cs b/Source/BPA.MQTTnet/Packets/MqttPingRespPacket.cs new file mode 100644 index 0000000..d895061 --- /dev/null +++ b/Source/BPA.MQTTnet/Packets/MqttPingRespPacket.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.Packets +{ + public sealed class MqttPingRespPacket : MqttPacket + { + // This is a minor performance improvement. + public static readonly MqttPingRespPacket Instance = new MqttPingRespPacket(); + + public override string ToString() + { + return "PingResp"; + } + } +} diff --git a/Source/BPA.MQTTnet/Packets/MqttPubAckPacket.cs b/Source/BPA.MQTTnet/Packets/MqttPubAckPacket.cs new file mode 100644 index 0000000..97053b3 --- /dev/null +++ b/Source/BPA.MQTTnet/Packets/MqttPubAckPacket.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. + +using System.Collections.Generic; +using MQTTnet.Protocol; + +namespace MQTTnet.Packets +{ + public sealed class MqttPubAckPacket : MqttPacketWithIdentifier + { + /// + /// Added in MQTTv5. + /// + public MqttPubAckReasonCode ReasonCode { get; set; } = MqttPubAckReasonCode.Success; + + /// + /// Added in MQTTv5. + /// + public string ReasonString { get; set; } + + /// + /// Added in MQTTv5. + /// + public List UserProperties { get; set; } + + public override string ToString() + { + return $"PubAck: [PacketIdentifier={PacketIdentifier}] [ReasonCode={ReasonCode}]"; + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Packets/MqttPubCompPacket.cs b/Source/BPA.MQTTnet/Packets/MqttPubCompPacket.cs new file mode 100644 index 0000000..ec4dd83 --- /dev/null +++ b/Source/BPA.MQTTnet/Packets/MqttPubCompPacket.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. + +using System.Collections.Generic; +using MQTTnet.Protocol; + +namespace MQTTnet.Packets +{ + public sealed class MqttPubCompPacket : MqttPacketWithIdentifier + { + /// + /// Added in MQTTv5. + /// + public MqttPubCompReasonCode ReasonCode { get; set; } = MqttPubCompReasonCode.Success; + + /// + /// Added in MQTTv5. + /// + public string ReasonString { get; set; } + + /// + /// Added in MQTTv5. + /// + public List UserProperties { get; set; } + + public override string ToString() + { + return $"PubComp: [PacketIdentifier={PacketIdentifier}] [ReasonCode={ReasonCode}]"; + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Packets/MqttPubRecPacket.cs b/Source/BPA.MQTTnet/Packets/MqttPubRecPacket.cs new file mode 100644 index 0000000..3e080e7 --- /dev/null +++ b/Source/BPA.MQTTnet/Packets/MqttPubRecPacket.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. + +using System.Collections.Generic; +using MQTTnet.Protocol; + +namespace MQTTnet.Packets +{ + public sealed class MqttPubRecPacket : MqttPacketWithIdentifier + { + /// + /// Added in MQTTv5. + /// + public MqttPubRecReasonCode ReasonCode { get; set; } = MqttPubRecReasonCode.Success; + + /// + /// Added in MQTTv5. + /// + public string ReasonString { get; set; } + + /// + /// Added in MQTTv5. + /// + public List UserProperties { get; set; } + + public override string ToString() + { + return $"PubRec: [PacketIdentifier={PacketIdentifier}] [ReasonCode={ReasonCode}]"; + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Packets/MqttPubRelPacket.cs b/Source/BPA.MQTTnet/Packets/MqttPubRelPacket.cs new file mode 100644 index 0000000..9631da3 --- /dev/null +++ b/Source/BPA.MQTTnet/Packets/MqttPubRelPacket.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. + +using System.Collections.Generic; +using MQTTnet.Protocol; + +namespace MQTTnet.Packets +{ + public sealed class MqttPubRelPacket : MqttPacketWithIdentifier + { + /// + /// Added in MQTTv5. + /// + public MqttPubRelReasonCode ReasonCode { get; set; } = MqttPubRelReasonCode.Success; + + /// + /// Added in MQTTv5. + /// + public string ReasonString { get; set; } + + /// + /// Added in MQTTv5. + /// + public List UserProperties { get; set; } + + public override string ToString() + { + return $"PubRel: [PacketIdentifier={PacketIdentifier}] [ReasonCode={ReasonCode}]"; + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Packets/MqttPublishPacket.cs b/Source/BPA.MQTTnet/Packets/MqttPublishPacket.cs new file mode 100644 index 0000000..10aad7b --- /dev/null +++ b/Source/BPA.MQTTnet/Packets/MqttPublishPacket.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.Collections.Generic; +using MQTTnet.Protocol; + +namespace MQTTnet.Packets +{ + public sealed class MqttPublishPacket : MqttPacketWithIdentifier + { + public string ContentType { get; set; } + + public byte[] CorrelationData { get; set; } + + public bool Dup { get; set; } + + public uint MessageExpiryInterval { get; set; } + + public byte[] Payload { get; set; } + + 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 ushort TopicAlias { get; set; } + + public List UserProperties { get; set; } + + public override string ToString() + { + 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/BPA.MQTTnet/Packets/MqttSubAckPacket.cs b/Source/BPA.MQTTnet/Packets/MqttSubAckPacket.cs new file mode 100644 index 0000000..e9887c8 --- /dev/null +++ b/Source/BPA.MQTTnet/Packets/MqttSubAckPacket.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using MQTTnet.Protocol; + +namespace MQTTnet.Packets +{ + public sealed class MqttSubAckPacket : MqttPacketWithIdentifier + { + /// + /// 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; } + + /// + /// Added in MQTTv5. + /// + public string ReasonString { get; set; } + + /// + /// Added in MQTTv5. + /// + public List UserProperties { get; set; } + + public override string ToString() + { + var reasonCodesText = string.Join(",", ReasonCodes.Select(f => f.ToString())); + + return $"SubAck: [PacketIdentifier={PacketIdentifier}] [ReasonCode={reasonCodesText}]"; + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Packets/MqttSubscribePacket.cs b/Source/BPA.MQTTnet/Packets/MqttSubscribePacket.cs new file mode 100644 index 0000000..fdbbaeb --- /dev/null +++ b/Source/BPA.MQTTnet/Packets/MqttSubscribePacket.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.Collections.Generic; +using System.Linq; + +namespace MQTTnet.Packets +{ + public sealed class MqttSubscribePacket : MqttPacketWithIdentifier + { + /// + /// 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 MQTTv5. + /// + public List UserProperties { get; set; } + + public override string ToString() + { + var topicFiltersText = string.Join(",", TopicFilters.Select(f => f.Topic + "@" + f.QualityOfServiceLevel)); + return $"Subscribe: [PacketIdentifier={PacketIdentifier}] [TopicFilters={topicFiltersText}]"; + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Packets/MqttTopicFilter.cs b/Source/BPA.MQTTnet/Packets/MqttTopicFilter.cs new file mode 100644 index 0000000..8b9d806 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Packets/MqttUnsubAckPacket.cs b/Source/BPA.MQTTnet/Packets/MqttUnsubAckPacket.cs new file mode 100644 index 0000000..1d2d32f --- /dev/null +++ b/Source/BPA.MQTTnet/Packets/MqttUnsubAckPacket.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.Collections.Generic; +using System.Linq; +using MQTTnet.Protocol; + +namespace MQTTnet.Packets +{ + public sealed class MqttUnsubAckPacket : MqttPacketWithIdentifier + { + /// + /// Added in MQTTv5. + /// + public List ReasonCodes { get; set; } + + /// + /// Added in MQTTv5. + /// + public string ReasonString { get; set; } + + /// + /// Added in MQTTv5. + /// + public List UserProperties { get; set; } + + public override string ToString() + { + var reasonCodesText = string.Empty; + if (ReasonCodes != null) + { + reasonCodesText = string.Join(",", ReasonCodes?.Select(f => f.ToString())); + } + + return $"UnsubAck: [PacketIdentifier={PacketIdentifier}] [ReasonCodes={reasonCodesText}] [ReasonString={ReasonString}]"; + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Packets/MqttUnsubscribePacket.cs b/Source/BPA.MQTTnet/Packets/MqttUnsubscribePacket.cs new file mode 100644 index 0000000..cddb3b4 --- /dev/null +++ b/Source/BPA.MQTTnet/Packets/MqttUnsubscribePacket.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; + +namespace MQTTnet.Packets +{ + public sealed class MqttUnsubscribePacket : MqttPacketWithIdentifier + { + public List TopicFilters { get; set; } = new List(); + + /// + /// Added in MQTTv5. + /// + public List UserProperties { get; set; } + + public override string ToString() + { + var topicFiltersText = string.Join(",", TopicFilters); + return $"Unsubscribe: [PacketIdentifier={PacketIdentifier}] [TopicFilters={topicFiltersText}]"; + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Packets/MqttUserProperty.cs b/Source/BPA.MQTTnet/Packets/MqttUserProperty.cs new file mode 100644 index 0000000..f54c821 --- /dev/null +++ b/Source/BPA.MQTTnet/Packets/MqttUserProperty.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 System; + +namespace MQTTnet.Packets +{ + public sealed class MqttUserProperty + { + public MqttUserProperty(string name, string value) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + Value = value ?? throw new ArgumentNullException(nameof(value)); + } + + public string Name { get; } + + public string Value { get; } + + public override bool Equals(object other) + { + return Equals(other as MqttUserProperty); + } + + public bool Equals(MqttUserProperty other) + { + if (other == null) + { + return false; + } + + if (ReferenceEquals(other, this)) + { + return true; + } + + 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/BPA.MQTTnet/Properties/AssemblyInfo.cs b/Source/BPA.MQTTnet/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..9722982 --- /dev/null +++ b/Source/BPA.MQTTnet/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + diff --git a/Source/BPA.MQTTnet/Protocol/MqttAuthenticateReasonCode.cs b/Source/BPA.MQTTnet/Protocol/MqttAuthenticateReasonCode.cs new file mode 100644 index 0000000..1f59e1d --- /dev/null +++ b/Source/BPA.MQTTnet/Protocol/MqttAuthenticateReasonCode.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.Protocol +{ + public enum MqttAuthenticateReasonCode + { + Success = 0, + ContinueAuthentication = 24, + ReAuthenticate = 25 + } +} diff --git a/Source/BPA.MQTTnet/Protocol/MqttConnectReasonCode.cs b/Source/BPA.MQTTnet/Protocol/MqttConnectReasonCode.cs new file mode 100644 index 0000000..74977df --- /dev/null +++ b/Source/BPA.MQTTnet/Protocol/MqttConnectReasonCode.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.Protocol +{ + public enum MqttConnectReasonCode + { + Success = 0, + UnspecifiedError = 128, + MalformedPacket = 129, + ProtocolError = 130, + ImplementationSpecificError = 131, + UnsupportedProtocolVersion = 132, + ClientIdentifierNotValid = 133, + BadUserNameOrPassword = 134, + NotAuthorized = 135, + ServerUnavailable = 136, + ServerBusy = 137, + Banned = 138, + BadAuthenticationMethod = 140, + TopicNameInvalid = 144, + PacketTooLarge = 149, + QuotaExceeded = 151, + PayloadFormatInvalid = 153, + RetainNotSupported = 154, + QoSNotSupported = 155, + UseAnotherServer = 156, + ServerMoved = 157, + ConnectionRateExceeded = 159 + } +} diff --git a/Source/BPA.MQTTnet/Protocol/MqttConnectReturnCode.cs b/Source/BPA.MQTTnet/Protocol/MqttConnectReturnCode.cs new file mode 100644 index 0000000..0687fe7 --- /dev/null +++ b/Source/BPA.MQTTnet/Protocol/MqttConnectReturnCode.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. + +namespace MQTTnet.Protocol +{ + public enum MqttConnectReturnCode + { + ConnectionAccepted = 0x00, + ConnectionRefusedUnacceptableProtocolVersion = 0x01, + ConnectionRefusedIdentifierRejected = 0x02, + ConnectionRefusedServerUnavailable = 0x03, + ConnectionRefusedBadUsernameOrPassword = 0x04, + ConnectionRefusedNotAuthorized = 0x05 + } +} diff --git a/Source/BPA.MQTTnet/Protocol/MqttControlPacketType.cs b/Source/BPA.MQTTnet/Protocol/MqttControlPacketType.cs new file mode 100644 index 0000000..22b48e0 --- /dev/null +++ b/Source/BPA.MQTTnet/Protocol/MqttControlPacketType.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. + +namespace MQTTnet.Protocol +{ + public enum MqttControlPacketType + { + Connect = 1, + ConnAck = 2, + Publish = 3, + PubAck = 4, + PubRec = 5, + PubRel = 6, + PubComp = 7, + Subscribe = 8, + SubAck = 9, + Unsubscibe = 10, + UnsubAck = 11, + PingReq = 12, + PingResp = 13, + Disconnect = 14, + Auth = 15 + } +} diff --git a/Source/BPA.MQTTnet/Protocol/MqttDisconnectReasonCode.cs b/Source/BPA.MQTTnet/Protocol/MqttDisconnectReasonCode.cs new file mode 100644 index 0000000..b3307f7 --- /dev/null +++ b/Source/BPA.MQTTnet/Protocol/MqttDisconnectReasonCode.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. + +namespace MQTTnet.Protocol +{ + public enum MqttDisconnectReasonCode + { + NormalDisconnection = 0, + DisconnectWithWillMessage = 4, + UnspecifiedError = 128, + MalformedPacket = 129, + ProtocolError = 130, + ImplementationSpecificError = 131, + NotAuthorized = 135, + ServerBusy = 137, + ServerShuttingDown = 139, + KeepAliveTimeout = 141, + SessionTakenOver = 142, + TopicFilterInvalid = 143, + TopicNameInvalid = 144, + ReceiveMaximumExceeded = 147, + TopicAliasInvalid = 148, + PacketTooLarge = 149, + MessageRateTooHigh = 150, + QuotaExceeded = 151, + AdministrativeAction = 152, + PayloadFormatInvalid = 153, + RetainNotSupported = 154, + QoSNotSupported = 155, + UseAnotherServer = 156, + ServerMoved = 157, + SharedSubscriptionsNotSupported = 158, + ConnectionRateExceeded = 159, + MaximumConnectTime = 160, + SubscriptionIdentifiersNotSupported = 161, + WildcardSubscriptionsNotSupported = 162 + } +} diff --git a/Source/BPA.MQTTnet/Protocol/MqttPayloadFormatIndicator.cs b/Source/BPA.MQTTnet/Protocol/MqttPayloadFormatIndicator.cs new file mode 100644 index 0000000..a4446c4 --- /dev/null +++ b/Source/BPA.MQTTnet/Protocol/MqttPayloadFormatIndicator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace MQTTnet.Protocol +{ + public enum MqttPayloadFormatIndicator + { + Unspecified = 0, + CharacterData = 1 + } +} diff --git a/Source/BPA.MQTTnet/Protocol/MqttPropertyId.cs b/Source/BPA.MQTTnet/Protocol/MqttPropertyId.cs new file mode 100644 index 0000000..bce6c23 --- /dev/null +++ b/Source/BPA.MQTTnet/Protocol/MqttPropertyId.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. + +namespace MQTTnet.Protocol +{ + public enum MqttPropertyId + { + None = 0, + + PayloadFormatIndicator = 1, + MessageExpiryInterval = 2, + ContentType = 3, + ResponseTopic = 8, + CorrelationData = 9, + SubscriptionIdentifier = 11, + SessionExpiryInterval = 17, + AssignedClientIdentifier = 18, + ServerKeepAlive = 19, + AuthenticationMethod = 21, + AuthenticationData = 22, + RequestProblemInformation = 23, + WillDelayInterval = 24, + RequestResponseInformation = 25, + ResponseInformation = 26, + ServerReference = 28, + ReasonString = 31, + ReceiveMaximum = 33, + TopicAliasMaximum = 34, + TopicAlias = 35, + MaximumQoS = 36, + RetainAvailable = 37, + UserProperty = 38, + MaximumPacketSize = 39, + WildcardSubscriptionAvailable = 40, + SubscriptionIdentifiersAvailable = 41, + SharedSubscriptionAvailable = 42 + } +} diff --git a/Source/BPA.MQTTnet/Protocol/MqttPubAckReasonCode.cs b/Source/BPA.MQTTnet/Protocol/MqttPubAckReasonCode.cs new file mode 100644 index 0000000..9eded18 --- /dev/null +++ b/Source/BPA.MQTTnet/Protocol/MqttPubAckReasonCode.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. + +namespace MQTTnet.Protocol +{ + public enum MqttPubAckReasonCode + { + Success = 0, + NoMatchingSubscribers = 16, + UnspecifiedError = 128, + ImplementationSpecificError = 131, + NotAuthorized = 135, + TopicNameInvalid = 144, + PacketIdentifierInUse = 145, + QuotaExceeded = 151, + PayloadFormatInvalid = 153 + } +} diff --git a/Source/BPA.MQTTnet/Protocol/MqttPubCompReasonCode.cs b/Source/BPA.MQTTnet/Protocol/MqttPubCompReasonCode.cs new file mode 100644 index 0000000..464b90a --- /dev/null +++ b/Source/BPA.MQTTnet/Protocol/MqttPubCompReasonCode.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace MQTTnet.Protocol +{ + public enum MqttPubCompReasonCode + { + Success = 0, + PacketIdentifierNotFound = 146 + } +} diff --git a/Source/BPA.MQTTnet/Protocol/MqttPubRecReasonCode.cs b/Source/BPA.MQTTnet/Protocol/MqttPubRecReasonCode.cs new file mode 100644 index 0000000..9299b64 --- /dev/null +++ b/Source/BPA.MQTTnet/Protocol/MqttPubRecReasonCode.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. + +namespace MQTTnet.Protocol +{ + public enum MqttPubRecReasonCode + { + Success = 0, + NoMatchingSubscribers = 16, + UnspecifiedError = 128, + ImplementationSpecificError = 131, + NotAuthorized = 135, + TopicNameInvalid = 144, + PacketIdentifierInUse = 145, + QuotaExceeded = 151, + PayloadFormatInvalid = 153 + } +} diff --git a/Source/BPA.MQTTnet/Protocol/MqttPubRelReasonCode.cs b/Source/BPA.MQTTnet/Protocol/MqttPubRelReasonCode.cs new file mode 100644 index 0000000..5bca4b4 --- /dev/null +++ b/Source/BPA.MQTTnet/Protocol/MqttPubRelReasonCode.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace MQTTnet.Protocol +{ + public enum MqttPubRelReasonCode + { + Success = 0, + PacketIdentifierNotFound = 146 + } +} diff --git a/Source/BPA.MQTTnet/Protocol/MqttQualityOfServiceLevel.cs b/Source/BPA.MQTTnet/Protocol/MqttQualityOfServiceLevel.cs new file mode 100644 index 0000000..e4134b7 --- /dev/null +++ b/Source/BPA.MQTTnet/Protocol/MqttQualityOfServiceLevel.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.Protocol +{ + public enum MqttQualityOfServiceLevel + { + AtMostOnce = 0x00, + AtLeastOnce = 0x01, + ExactlyOnce = 0x02 + } +} diff --git a/Source/BPA.MQTTnet/Protocol/MqttRetainHandling.cs b/Source/BPA.MQTTnet/Protocol/MqttRetainHandling.cs new file mode 100644 index 0000000..7604ea5 --- /dev/null +++ b/Source/BPA.MQTTnet/Protocol/MqttRetainHandling.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.Protocol +{ + public enum MqttRetainHandling + { + SendAtSubscribe = 0, + + SendAtSubscribeIfNewSubscriptionOnly = 1, + + DoNotSendOnSubscribe = 2 + } +} diff --git a/Source/BPA.MQTTnet/Protocol/MqttSubscribeReasonCode.cs b/Source/BPA.MQTTnet/Protocol/MqttSubscribeReasonCode.cs new file mode 100644 index 0000000..48cc7f8 --- /dev/null +++ b/Source/BPA.MQTTnet/Protocol/MqttSubscribeReasonCode.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. + +namespace MQTTnet.Protocol +{ + public enum MqttSubscribeReasonCode + { + // Compatible with MQTTv3.1.1. + GrantedQoS0 = 0x00, + GrantedQoS1 = 0x01, + GrantedQoS2 = 0x02, + UnspecifiedError = 0x80, + + // New in MQTTv5. + ImplementationSpecificError = 131, + NotAuthorized = 135, + TopicFilterInvalid = 143, + PacketIdentifierInUse = 145, + QuotaExceeded = 151, + SharedSubscriptionsNotSupported = 158, + SubscriptionIdentifiersNotSupported = 161, + WildcardSubscriptionsNotSupported = 162 + } +} diff --git a/Source/BPA.MQTTnet/Protocol/MqttSubscribeReturnCode.cs b/Source/BPA.MQTTnet/Protocol/MqttSubscribeReturnCode.cs new file mode 100644 index 0000000..da569b6 --- /dev/null +++ b/Source/BPA.MQTTnet/Protocol/MqttSubscribeReturnCode.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.Protocol +{ + public enum MqttSubscribeReturnCode + { + SuccessMaximumQoS0 = 0x00, + SuccessMaximumQoS1 = 0x01, + SuccessMaximumQoS2 = 0x02, + Failure = 0x80 + } +} diff --git a/Source/BPA.MQTTnet/Protocol/MqttTopicValidator.cs b/Source/BPA.MQTTnet/Protocol/MqttTopicValidator.cs new file mode 100644 index 0000000..f9c9283 --- /dev/null +++ b/Source/BPA.MQTTnet/Protocol/MqttTopicValidator.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 MQTTnet.Exceptions; + +namespace MQTTnet.Protocol +{ + public static class MqttTopicValidator + { + public static void ThrowIfInvalid(MqttApplicationMessage applicationMessage) + { + if (applicationMessage == null) throw new ArgumentNullException(nameof(applicationMessage)); + + if (applicationMessage.TopicAlias == 0) + { + ThrowIfInvalid(applicationMessage.Topic); + } + } + + public static void ThrowIfInvalid(string topic) + { + if (string.IsNullOrEmpty(topic)) + { + throw new MqttProtocolViolationException("Topic should not be empty."); + } + + foreach(var @char in topic) + { + if (@char == '+') + { + throw new MqttProtocolViolationException("The character '+' is not allowed in topics."); + } + + if (@char == '#') + { + throw new MqttProtocolViolationException("The character '#' is not allowed in topics."); + } + } + } + + public static void ThrowIfInvalidSubscribe(string topic) + { + if (string.IsNullOrEmpty(topic)) + { + throw new MqttProtocolViolationException("Topic should not be empty."); + } + + 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/BPA.MQTTnet/Protocol/MqttUnsubscribeReasonCode.cs b/Source/BPA.MQTTnet/Protocol/MqttUnsubscribeReasonCode.cs new file mode 100644 index 0000000..cd85c8e --- /dev/null +++ b/Source/BPA.MQTTnet/Protocol/MqttUnsubscribeReasonCode.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.Protocol +{ + public enum MqttUnsubscribeReasonCode + { + Success = 0, + NoSubscriptionExisted = 17, + UnspecifiedError = 128, + ImplementationSpecificError = 131, + NotAuthorized = 135, + TopicFilterInvalid = 143, + PacketIdentifierInUse = 145 + } +} diff --git a/Source/BPA.MQTTnet/Server/Events/ApplicationMessageNotConsumedEventArgs.cs b/Source/BPA.MQTTnet/Server/Events/ApplicationMessageNotConsumedEventArgs.cs new file mode 100644 index 0000000..28b0e72 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Server/Events/ClientConnectedEventArgs.cs b/Source/BPA.MQTTnet/Server/Events/ClientConnectedEventArgs.cs new file mode 100644 index 0000000..2a8845e --- /dev/null +++ b/Source/BPA.MQTTnet/Server/Events/ClientConnectedEventArgs.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.Formatter; + +namespace MQTTnet.Server +{ + public sealed class ClientConnectedEventArgs : EventArgs + { + /// + /// Gets the client identifier of the connected client. + /// Hint: This identifier needs to be unique over all used clients / devices on the broker to avoid connection issues. + /// + public string ClientId { get; internal set; } + + /// + /// Gets the user name of the connected client. + /// + public string UserName { get; internal set; } + + /// + /// Gets the protocol version which is used by the connected client. + /// + public MqttProtocolVersion ProtocolVersion { get; internal set; } + + /// + /// Gets the endpoint of the connected client. + /// + public string Endpoint { get; internal set; } + } +} diff --git a/Source/BPA.MQTTnet/Server/Events/ClientDisconnectedEventArgs.cs b/Source/BPA.MQTTnet/Server/Events/ClientDisconnectedEventArgs.cs new file mode 100644 index 0000000..aeac413 --- /dev/null +++ b/Source/BPA.MQTTnet/Server/Events/ClientDisconnectedEventArgs.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 ClientDisconnectedEventArgs : 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 MqttClientDisconnectType DisconnectType { get; internal set; } + + public string Endpoint { get; internal set; } + } +} diff --git a/Source/BPA.MQTTnet/Server/Events/ClientSubscribedTopicEventArgs.cs b/Source/BPA.MQTTnet/Server/Events/ClientSubscribedTopicEventArgs.cs new file mode 100644 index 0000000..3410567 --- /dev/null +++ b/Source/BPA.MQTTnet/Server/Events/ClientSubscribedTopicEventArgs.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; +using MQTTnet.Packets; + +namespace MQTTnet.Server +{ + public sealed class ClientSubscribedTopicEventArgs : 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 the topic filter. + /// The topic filter can contain topics and wildcards. + /// + public MqttTopicFilter TopicFilter { get; internal set; } + } +} diff --git a/Source/BPA.MQTTnet/Server/Events/ClientUnsubscribedTopicEventArgs.cs b/Source/BPA.MQTTnet/Server/Events/ClientUnsubscribedTopicEventArgs.cs new file mode 100644 index 0000000..9a9f73b --- /dev/null +++ b/Source/BPA.MQTTnet/Server/Events/ClientUnsubscribedTopicEventArgs.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; + +namespace MQTTnet.Server +{ + public sealed class ClientUnsubscribedTopicEventArgs : 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 topic filter. + /// The topic filter can contain topics and wildcards. + /// + public string TopicFilter { get; internal set; } + } +} diff --git a/Source/BPA.MQTTnet/Server/Events/InterceptingPacketEventArgs.cs b/Source/BPA.MQTTnet/Server/Events/InterceptingPacketEventArgs.cs new file mode 100644 index 0000000..3f2661c --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Server/Events/InterceptingPublishEventArgs.cs b/Source/BPA.MQTTnet/Server/Events/InterceptingPublishEventArgs.cs new file mode 100644 index 0000000..09a4d76 --- /dev/null +++ b/Source/BPA.MQTTnet/Server/Events/InterceptingPublishEventArgs.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections; +using System.Threading; + +namespace MQTTnet.Server +{ + public sealed class InterceptingPublishEventArgs : EventArgs + { + public MqttApplicationMessage ApplicationMessage { get; set; } + + /// + /// 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; } + + public bool CloseConnection { get; set; } + + /// + /// Gets or sets whether the publish should be processed internally. + /// + public bool ProcessPublish { get; set; } = true; + + /// + /// Gets the response which will be sent to the client via the PUBACK etc. packets. + /// + public PublishResponse Response { get; } = new PublishResponse(); + + /// + /// 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; } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Server/Events/InterceptingSubscriptionEventArgs.cs b/Source/BPA.MQTTnet/Server/Events/InterceptingSubscriptionEventArgs.cs new file mode 100644 index 0000000..8b173f1 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Server/Events/InterceptingUnsubscriptionEventArgs.cs b/Source/BPA.MQTTnet/Server/Events/InterceptingUnsubscriptionEventArgs.cs new file mode 100644 index 0000000..648ccd6 --- /dev/null +++ b/Source/BPA.MQTTnet/Server/Events/InterceptingUnsubscriptionEventArgs.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 System; +using System.Collections; +using System.Threading; + +namespace MQTTnet.Server +{ + public sealed class InterceptingUnsubscriptionEventArgs : 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 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 the response which will be sent to the client via the UNSUBACK pocket. + /// + public UnsubscribeResponse Response { get; } = new UnsubscribeResponse(); + + /// + /// 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 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; } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Server/Events/LoadingRetainedMessagesEventArgs.cs b/Source/BPA.MQTTnet/Server/Events/LoadingRetainedMessagesEventArgs.cs new file mode 100644 index 0000000..12aa046 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Server/Events/PreparingSessionEventArgs.cs b/Source/BPA.MQTTnet/Server/Events/PreparingSessionEventArgs.cs new file mode 100644 index 0000000..e635308 --- /dev/null +++ b/Source/BPA.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 IDictionary Items { get; set; } + + public List PublishPackets { get; } = new List(); + + DateTime? SessionExpiryTimestamp { get; set; } + + public List Subscriptions { get; } = new List(); + + /// + /// Gets the will delay interval. + /// This is the time between the client disconnect and the time the will message will be sent. + /// + public uint? WillDelayInterval { 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; } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Server/Events/RetainedMessageChangedEventArgs.cs b/Source/BPA.MQTTnet/Server/Events/RetainedMessageChangedEventArgs.cs new file mode 100644 index 0000000..1d436fc --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Server/Events/SessionDeletedEventArgs.cs b/Source/BPA.MQTTnet/Server/Events/SessionDeletedEventArgs.cs new file mode 100644 index 0000000..844f7c1 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Server/Events/ValidatingConnectionEventArgs.cs b/Source/BPA.MQTTnet/Server/Events/ValidatingConnectionEventArgs.cs new file mode 100644 index 0000000..6126280 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Server/InjectedMqttApplicationMessage.cs b/Source/BPA.MQTTnet/Server/InjectedMqttApplicationMessage.cs new file mode 100644 index 0000000..0708cee --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Server/Internal/CheckSubscriptionsResult.cs b/Source/BPA.MQTTnet/Server/Internal/CheckSubscriptionsResult.cs new file mode 100644 index 0000000..7c20e8d --- /dev/null +++ b/Source/BPA.MQTTnet/Server/Internal/CheckSubscriptionsResult.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 System.Collections.Generic; +using MQTTnet.Protocol; + +namespace MQTTnet.Server +{ + public sealed class CheckSubscriptionsResult + { + public static CheckSubscriptionsResult NotSubscribed { get; } = new CheckSubscriptionsResult(); + + public bool IsSubscribed { get; set; } + + public bool RetainAsPublished { get; set; } + + public List SubscriptionIdentifiers { get; set; } + + public MqttQualityOfServiceLevel QualityOfServiceLevel { get; set; } + } +} diff --git a/Source/BPA.MQTTnet/Server/Internal/ISubscriptionChangedNotification.cs b/Source/BPA.MQTTnet/Server/Internal/ISubscriptionChangedNotification.cs new file mode 100644 index 0000000..bff6769 --- /dev/null +++ b/Source/BPA.MQTTnet/Server/Internal/ISubscriptionChangedNotification.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace MQTTnet.Server +{ + public interface ISubscriptionChangedNotification + { + void OnSubscriptionsAdded(MqttSession clientSession, List subscriptionsTopics); + + void OnSubscriptionsRemoved(MqttSession clientSession, List subscriptionTopics); + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Server/Internal/MqttClient.cs b/Source/BPA.MQTTnet/Server/Internal/MqttClient.cs new file mode 100644 index 0000000..718b485 --- /dev/null +++ b/Source/BPA.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 (_cancellationToken = new CancellationTokenSource()) + { + var cancellationToken = _cancellationToken.Token; + + try + { + Task.Run(() => SendPacketsLoop(cancellationToken), cancellationToken).RunInBackground(_logger); + + IsRunning = true; + + await ReceivePackagesLoop(cancellationToken).ConfigureAwait(false); + } + finally + { + IsRunning = false; + + _cancellationToken?.Cancel(); + _cancellationToken = null; + } + } + + _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/BPA.MQTTnet/Server/Internal/MqttClientSessionsManager.cs b/Source/BPA.MQTTnet/Server/Internal/MqttClientSessionsManager.cs new file mode 100644 index 0000000..8517888 --- /dev/null +++ b/Source/BPA.MQTTnet/Server/Internal/MqttClientSessionsManager.cs @@ -0,0 +1,647 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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.Diagnostics; +using MQTTnet.Exceptions; +using MQTTnet.Formatter; +using MQTTnet.Internal; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Server +{ + public sealed class MqttClientSessionsManager : ISubscriptionChangedNotification, IDisposable + { + 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 MqttRetainedMessagesManager _retainedMessagesManager; + readonly IMqttNetLogger _rootLogger; + + // 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 object _sessionsManagementLock = new object(); + readonly HashSet _subscriberSessions = new HashSet(); + + public MqttClientSessionsManager( + MqttServerOptions options, + MqttRetainedMessagesManager retainedMessagesManager, + MqttServerEventContainer eventContainer, + IMqttNetLogger logger) + { + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + _logger = logger.WithSource(nameof(MqttClientSessionsManager)); + _rootLogger = logger; + + _options = options ?? throw new ArgumentNullException(nameof(options)); + _retainedMessagesManager = retainedMessagesManager ?? throw new ArgumentNullException(nameof(retainedMessagesManager)); + _eventContainer = eventContainer ?? throw new ArgumentNullException(nameof(eventContainer)); + } + + public async Task CloseAllConnectionsAsync() + { + List connections; + lock (_clients) + { + connections = _clients.Values.ToList(); + _clients.Clear(); + } + + foreach (var connection in connections) + { + await connection.StopAsync(MqttDisconnectReasonCode.NormalDisconnection).ConfigureAwait(false); + } + } + + public async Task DeleteSessionAsync(string clientId) + { + MqttClient connection; + + lock (_clients) + { + _clients.TryGetValue(clientId, out connection); + } + + MqttSession session; + + lock (_sessionsManagementLock) + { + _sessions.TryGetValue(clientId, out session); + _sessions.Remove(clientId); + + if (session != null) + { + _subscriberSessions.Remove(session); + } + } + + try + { + if (connection != null) + { + await connection.StopAsync(MqttDisconnectReasonCode.NormalDisconnection).ConfigureAwait(false); + } + } + catch (Exception exception) + { + _logger.Error(exception, $"Error while deleting session '{clientId}'."); + } + + try + { + 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}'."); + } + + session?.Dispose(); + + _logger.Verbose("Session for client '{0}' deleted.", clientId); + } + + public async Task DispatchApplicationMessage(string senderId, MqttApplicationMessage applicationMessage) + { + try + { + if (applicationMessage.Retain) + { + await _retainedMessagesManager.UpdateMessage(senderId, applicationMessage).ConfigureAwait(false); + } + + var deliveryCount = 0; + List subscriberSessions; + lock (_sessionsManagementLock) + { + // only subscriber clients are of interest here. + subscriberSessions = _subscriberSessions.ToList(); + } + + // Calculate application message topic hash once for subscription checks + MqttSubscription.CalculateTopicHash(applicationMessage.Topic, out var topicHash, out _, out _); + + foreach (var session in subscriberSessions) + { + var checkSubscriptionsResult = session.SubscriptionsManager.CheckSubscriptions( + applicationMessage.Topic, + topicHash, + applicationMessage.QualityOfServiceLevel, + senderId); + + if (!checkSubscriptionsResult.IsSubscribed) + { + continue; + } + + var newPublishPacket = _packetFactories.Publish.Create(applicationMessage); + newPublishPacket.QualityOfServiceLevel = checkSubscriptionsResult.QualityOfServiceLevel; + newPublishPacket.SubscriptionIdentifiers = checkSubscriptionsResult.SubscriptionIdentifiers; + + if (newPublishPacket.QualityOfServiceLevel > 0) + { + newPublishPacket.PacketIdentifier = session.PacketIdentifierProvider.GetNextPacketIdentifier(); + } + + if (checkSubscriptionsResult.RetainAsPublished) + { + // Transfer the original retain state from the publisher. This is a MQTTv5 feature. + newPublishPacket.Retain = applicationMessage.Retain; + } + else + { + newPublishPacket.Retain = false; + } + + session.EnqueuePacket(new MqttPacketBusItem(newPublishPacket)); + deliveryCount++; + + _logger.Verbose("Client '{0}': Queued PUBLISH packet with topic '{1}'.", session.Id, applicationMessage.Topic); + } + + await FireApplicationMessageNotConsumedEvent(applicationMessage, deliveryCount, senderId); + } + catch (Exception exception) + { + _logger.Error(exception, "Unhandled exception while processing next queued application message."); + } + } + + public void Dispose() + { + _createConnectionSyncRoot?.Dispose(); + + lock (_sessionsManagementLock) + { + foreach (var sessionItem in _sessions) + { + sessionItem.Value.Dispose(); + } + } + } + + public MqttClient GetClient(string id) + { + lock (_clients) + { + if (!_clients.TryGetValue(id, out var client)) + { + throw new InvalidOperationException($"Client with ID '{id}' not found."); + } + + return client; + } + } + + public List GetClients() + { + lock (_clients) + { + return _clients.Values.ToList(); + } + } + + public Task> GetClientStatusAsync() + { + var result = new List(); + + lock (_clients) + { + foreach (var connection in _clients.Values) + { + var clientStatus = new MqttClientStatus(connection) + { + Session = new MqttSessionStatus(connection.Session) + }; + + result.Add(clientStatus); + } + } + + return Task.FromResult((IList)result); + } + + public Task> GetSessionStatusAsync() + { + var result = new List(); + + lock (_sessionsManagementLock) + { + foreach (var sessionItem in _sessions) + { + var sessionStatus = new MqttSessionStatus(sessionItem.Value); + result.Add(sessionStatus); + } + } + + return Task.FromResult((IList)result); + } + + public async Task HandleClientConnectionAsync(IMqttChannelAdapter channelAdapter, CancellationToken cancellationToken) + { + MqttClient client = null; + + try + { + var connectPacket = await ReceiveConnectPacket(channelAdapter, cancellationToken).ConfigureAwait(false); + if (connectPacket == null) + { + // Nothing was received in time etc. + return; + } + + var validatingConnectionEventArgs = await ValidateConnection(connectPacket, channelAdapter).ConfigureAwait(false); + var connAckPacket = _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; + } + + // 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); + + if (_eventContainer.ClientConnectedEvent.HasHandlers) + { + var eventArgs = new ClientConnectedEventArgs + { + 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) + { + lock (_clients) + { + _clients.Remove(client.Id); + } + + if (!_options.EnablePersistentSessions || !client.Session.IsPersistent) + { + await DeleteSessionAsync(client.Id).ConfigureAwait(false); + } + } + } + + var endpoint = client.Endpoint; + + if (client.Id != null && !client.IsTakenOver && _eventContainer.ClientDisconnectedEvent.HasHandlers) + { + var eventArgs = new ClientDisconnectedEventArgs + { + ClientId = client.Id, + DisconnectType = client.IsCleanDisconnect ? MqttClientDisconnectType.Clean : MqttClientDisconnectType.NotClean, + Endpoint = endpoint + }; + + await _eventContainer.ClientDisconnectedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + } + } + + using (var timeout = new CancellationTokenSource(_options.DefaultCommunicationTimeout)) + { + await channelAdapter.DisconnectAsync(timeout.Token).ConfigureAwait(false); + } + } + } + + public void OnSubscriptionsAdded(MqttSession clientSession, List topics) + { + lock (_sessionsManagementLock) + { + if (!clientSession.HasSubscribedTopics) + { + // first subscribed topic + _subscriberSessions.Add(clientSession); + } + + foreach (var topic in topics) + { + clientSession.AddSubscribedTopic(topic); + } + } + } + + public void OnSubscriptionsRemoved(MqttSession clientSession, List subscriptionTopics) + { + lock (_sessionsManagementLock) + { + foreach (var subscriptionTopic in subscriptionTopics) + { + clientSession.RemoveSubscribedTopic(subscriptionTopic); + } + + if (!clientSession.HasSubscribedTopics) + { + // last subscription removed + _subscriberSessions.Remove(clientSession); + } + } + } + + public void Start() + { + if (!_options.EnablePersistentSessions) + { + _sessions.Clear(); + } + } + + public async Task SubscribeAsync(string clientId, ICollection topicFilters) + { + if (clientId == null) + { + throw new ArgumentNullException(nameof(clientId)); + } + + if (topicFilters == null) + { + throw new ArgumentNullException(nameof(topicFilters)); + } + + var fakeSubscribePacket = new MqttSubscribePacket(); + fakeSubscribePacket.TopicFilters.AddRange(topicFilters); + + var clientSession = GetClientSession(clientId); + + var subscribeResult = await clientSession.SubscriptionsManager.Subscribe(fakeSubscribePacket, CancellationToken.None).ConfigureAwait(false); + + if (subscribeResult.RetainedMessages != null) + { + foreach (var retainedApplicationMessage in subscribeResult.RetainedMessages) + { + var publishPacket = _packetFactories.Publish.Create(retainedApplicationMessage.ApplicationMessage); + clientSession.EnqueuePacket(new MqttPacketBusItem(publishPacket)); + } + } + } + + public Task UnsubscribeAsync(string clientId, ICollection topicFilters) + { + if (clientId == null) + { + throw new ArgumentNullException(nameof(clientId)); + } + + if (topicFilters == null) + { + throw new ArgumentNullException(nameof(topicFilters)); + } + + var fakeUnsubscribePacket = new MqttUnsubscribePacket(); + fakeUnsubscribePacket.TopicFilters.AddRange(topicFilters); + + return GetClientSession(clientId).SubscriptionsManager.Unsubscribe(fakeUnsubscribePacket, CancellationToken.None); + } + + async Task CreateClientConnection( + MqttConnectPacket connectPacket, + MqttConnAckPacket connAckPacket, + IMqttChannelAdapter channelAdapter, + ValidatingConnectionEventArgs validatingConnectionEventArgs) + { + MqttClient connection; + + bool sessionShouldPersist; + + 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]. + // + // A Client that only wants to process messages while connected will set the Clean Start to 1 and set the Session Expiry Interval to 0. + // It will not receive Application Messages published before it connected and has to subscribe afresh to any topics that it is interested + // in each time it connects. + + // Persist if SessionExpiryInterval != 0, but may start with a clean session + sessionShouldPersist = validatingConnectionEventArgs.SessionExpiryInterval != 0; + } + else + { + // MQTT 3.1.1 section 3.1.2.4: persist only if 'not CleanSession' + // + // If CleanSession is set to 1, the Client and Server MUST discard any previous Session and start a new one. + // This Session lasts as long as the Network Connection. State data associated with this Session MUST NOT be + // reused in any subsequent Session [MQTT-3.1.2-6]. + + sessionShouldPersist = !connectPacket.CleanSession; + } + + using (await _createConnectionSyncRoot.WaitAsync(CancellationToken.None).ConfigureAwait(false)) + { + MqttSession session; + lock (_sessionsManagementLock) + { + if (!_sessions.TryGetValue(connectPacket.ClientId, out session)) + { + 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, validatingConnectionEventArgs.SessionItems, sessionShouldPersist); + } + else + { + _logger.Verbose("Reusing existing session of client '{0}'.", connectPacket.ClientId); + // Session persistence could change for MQTT 5 clients that reconnect with different SessionExpiryInterval + session.IsPersistent = sessionShouldPersist; + connAckPacket.IsSessionPresent = true; + session.Recover(); + } + } + + _sessions[connectPacket.ClientId] = session; + } + + 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); + } + + MqttClient existing; + + lock (_clients) + { + _clients.TryGetValue(connectPacket.ClientId, out existing); + connection = CreateConnection(connectPacket, channelAdapter, session); + + _clients[connectPacket.ClientId] = connection; + } + + if (existing != null) + { + existing.IsTakenOver = true; + await existing.StopAsync(MqttDisconnectReasonCode.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; + } + + MqttClient CreateConnection(MqttConnectPacket connectPacket, IMqttChannelAdapter channelAdapter, MqttSession session) + { + return new MqttClient(connectPacket, channelAdapter, session, _options, _eventContainer, this, _rootLogger); + } + + 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) + { + return; + } + + if (!_eventContainer.ApplicationMessageNotConsumedEvent.HasHandlers) + { + return; + } + + var eventArgs = new ApplicationMessageNotConsumedEventArgs + { + ApplicationMessage = applicationMessage, + SenderId = senderId + }; + + await _eventContainer.ApplicationMessageNotConsumedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + } + + MqttSession GetClientSession(string clientId) + { + lock (_sessionsManagementLock) + { + if (!_sessions.TryGetValue(clientId, out var session)) + { + throw new InvalidOperationException($"Client session '{clientId}' is unknown."); + } + + return session; + } + } + + async Task ReceiveConnectPacket(IMqttChannelAdapter channelAdapter, CancellationToken cancellationToken) + { + try + { + using (var timeoutToken = new CancellationTokenSource(_options.DefaultCommunicationTimeout)) + using (var effectiveCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(timeoutToken.Token, cancellationToken)) + { + var firstPacket = await channelAdapter.ReceivePacketAsync(effectiveCancellationToken.Token).ConfigureAwait(false); + if (firstPacket is MqttConnectPacket connectPacket) + { + return connectPacket; + } + } + } + catch (OperationCanceledException) + { + _logger.Warning("Client '{0}': Connected but did not sent a CONNECT packet.", channelAdapter.Endpoint); + } + catch (MqttCommunicationTimedOutException) + { + _logger.Warning("Client '{0}': Connected but did not sent a CONNECT packet.", channelAdapter.Endpoint); + } + + _logger.Warning("Client '{0}': First received packet was no 'CONNECT' packet [MQTT-3.1.0-1].", channelAdapter.Endpoint); + return null; + } + + async Task ValidateConnection(MqttConnectPacket connectPacket, IMqttChannelAdapter channelAdapter) + { + 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/BPA.MQTTnet/Server/Internal/MqttClientStatistics.cs b/Source/BPA.MQTTnet/Server/Internal/MqttClientStatistics.cs new file mode 100644 index 0000000..a0d6c07 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Server/Internal/MqttClientSubscriptionsManager.cs b/Source/BPA.MQTTnet/Server/Internal/MqttClientSubscriptionsManager.cs new file mode 100644 index 0000000..b1174fe --- /dev/null +++ b/Source/BPA.MQTTnet/Server/Internal/MqttClientSubscriptionsManager.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.Linq; +using System.Threading; +using System.Threading.Tasks; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Server +{ + public sealed class MqttClientSubscriptionsManager : IDisposable + { + 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( + MqttSession session, + MqttServerEventContainer eventContainer, + MqttRetainedMessagesManager retainedMessagesManager, + ISubscriptionChangedNotification subscriptionChangedNotification) + { + _session = session ?? throw new ArgumentNullException(nameof(session)); + _eventContainer = eventContainer ?? throw new ArgumentNullException(nameof(eventContainer)); + _retainedMessagesManager = retainedMessagesManager ?? throw new ArgumentNullException(nameof(retainedMessagesManager)); + _subscriptionChangedNotification = subscriptionChangedNotification; + } + + public CheckSubscriptionsResult CheckSubscriptions(string topic, ulong topicHash, MqttQualityOfServiceLevel applicationMessageQoSLevel, string senderClientId) + { + var possibleSubscriptions = new List(); + + // 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()); + } + + 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 + { + _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. + + HashSet subscriptionIdentifiers = null; + var retainAsPublished = false; + + foreach (var subscription in possibleSubscriptions) + { + if (subscription.NoLocal && senderIsReceiver) + { + // This is a MQTTv5 feature! + continue; + } + + if (MqttTopicFilterComparer.Compare(topic, subscription.Topic) != MqttTopicFilterCompareResult.IsMatch) + { + continue; + } + + if (subscription.RetainAsPublished) + { + // This is a MQTTv5 feature! + retainAsPublished = true; + } + + if ((int)subscription.GrantedQualityOfServiceLevel > maxQoSLevel) + { + maxQoSLevel = (int)subscription.GrantedQualityOfServiceLevel; + } + + if (subscription.Identifier > 0) + { + if (subscriptionIdentifiers == null) + { + subscriptionIdentifiers = new HashSet(); + } + + subscriptionIdentifiers.Add(subscription.Identifier); + } + } + + if (maxQoSLevel == -1) + { + return CheckSubscriptionsResult.NotSubscribed; + } + + var result = new CheckSubscriptionsResult + { + IsSubscribed = true, + RetainAsPublished = retainAsPublished, + SubscriptionIdentifiers = subscriptionIdentifiers?.ToList() ?? EmptySubscriptionIdentifiers, + + // Start with the same QoS as the publisher. + QualityOfServiceLevel = 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 void Dispose() + { + _subscriptionsLock.Dispose(); + } + + 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(); + + // 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 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) + { + // When any of the interceptor calls leads to a connection close the connection + // must be closed. So do not revert to false! + result.CloseConnection = true; + } + + if (!processSubscription || string.IsNullOrEmpty(finalTopicFilter.Topic)) + { + continue; + } + + var createSubscriptionResult = CreateSubscription(finalTopicFilter, subscribePacket.SubscriptionIdentifier, subscriptionEventArgs.Response.ReasonCode); + + addedSubscriptions.Add(finalTopicFilter.Topic); + + if (_eventContainer.ClientSubscribedTopicEvent.HasHandlers) + { + var eventArgs = new ClientSubscribedTopicEventArgs + { + ClientId = _session.Id, + TopicFilter = finalTopicFilter + }; + + await _eventContainer.ClientSubscribedTopicEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + } + + FilterRetainedApplicationMessages(retainedApplicationMessages, createSubscriptionResult, result); + } + + _subscriptionChangedNotification?.OnSubscriptionsAdded(_session, addedSubscriptions); + + return result; + } + + public async Task Unsubscribe(MqttUnsubscribePacket unsubscribePacket, CancellationToken cancellationToken) + { + if (unsubscribePacket == null) + { + throw new ArgumentNullException(nameof(unsubscribePacket)); + } + + var result = new MqttUnsubscribeResult(); + + var removedSubscriptions = new List(); + + await _subscriptionsLock.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + foreach (var topicFilter in unsubscribePacket.TopicFilters) + { + _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 (_subscriptionChangedNotification != null) + { + _subscriptionChangedNotification.OnSubscriptionsRemoved(_session, removedSubscriptions); + } + } + + if (_eventContainer.ClientUnsubscribedTopicEvent.HasHandlers) + { + foreach (var topicFilter in unsubscribePacket.TopicFilters) + { + var eventArgs = new ClientUnsubscribedTopicEventArgs + { + ClientId = _session.Id, + TopicFilter = topicFilter + }; + + await _eventContainer.ClientUnsubscribedTopicEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + } + } + + return result; + } + + CreateSubscriptionResult CreateSubscription(MqttTopicFilter topicFilter, uint subscriptionIdentifier, MqttSubscribeReasonCode reasonCode) + { + MqttQualityOfServiceLevel grantedQualityOfServiceLevel; + + if (reasonCode == MqttSubscribeReasonCode.GrantedQoS0) + { + grantedQualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce; + } + else if (reasonCode == MqttSubscribeReasonCode.GrantedQoS1) + { + grantedQualityOfServiceLevel = MqttQualityOfServiceLevel.AtLeastOnce; + } + else if (reasonCode == MqttSubscribeReasonCode.GrantedQoS2) + { + grantedQualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce; + } + else + { + throw new InvalidOperationException(); + } + + var subscription = new MqttSubscription( + topicFilter.Topic, + topicFilter.NoLocal, + topicFilter.RetainHandling, + topicFilter.RetainAsPublished, + grantedQualityOfServiceLevel, + subscriptionIdentifier); + + bool isNewSubscription; + + // Add to subscriptions and maintain topic hash dictionaries + + _subscriptionsLock.Wait(); + try + { + 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.Release(); + } + + return new CreateSubscriptionResult + { + IsNewSubscription = isNewSubscription, + Subscription = subscription + }; + } + + static void FilterRetainedApplicationMessages( + IList retainedApplicationMessages, + CreateSubscriptionResult createSubscriptionResult, + SubscribeResult subscribeResult) + { + for (var index = retainedApplicationMessages.Count - 1; index >= 0; index--) + { + var retainedApplicationMessage = retainedApplicationMessages[index]; + if (retainedApplicationMessage == null) + { + continue; + } + + if (createSubscriptionResult.Subscription.RetainHandling == MqttRetainHandling.DoNotSendOnSubscribe) + { + // This is a MQTT V5+ feature. + continue; + } + + if (createSubscriptionResult.Subscription.RetainHandling == MqttRetainHandling.SendAtSubscribeIfNewSubscriptionOnly && !createSubscriptionResult.IsNewSubscription) + { + // This is a MQTT V5+ feature. + continue; + } + + if (MqttTopicFilterComparer.Compare(retainedApplicationMessage.Topic, createSubscriptionResult.Subscription.Topic) != MqttTopicFilterCompareResult.IsMatch) + { + continue; + } + + var retainedMessageMatch = new MqttRetainedMessageMatch + { + ApplicationMessage = retainedApplicationMessage, + SubscriptionQualityOfServiceLevel = createSubscriptionResult.Subscription.GrantedQualityOfServiceLevel + }; + + if (subscribeResult.RetainedMessages == null) + { + subscribeResult.RetainedMessages = new List(); + } + + subscribeResult.RetainedMessages.Add(retainedMessageMatch); + + // Clear the retained message from the list because the client should receive every message only + // one time even if multiple subscriptions affect them. + retainedApplicationMessages[index] = null; + } + } + + async Task InterceptSubscribe(MqttTopicFilter topicFilter, CancellationToken cancellationToken) + { + var eventArgs = new InterceptingSubscriptionEventArgs + { + ClientId = _session.Id, + TopicFilter = topicFilter, + SessionItems = _session.Items, + Session = new MqttSessionStatus(_session), + CancellationToken = cancellationToken + }; + + if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtMostOnce) + { + eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.GrantedQoS0; + } + else if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.AtLeastOnce) + { + eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.GrantedQoS1; + } + else if (topicFilter.QualityOfServiceLevel == MqttQualityOfServiceLevel.ExactlyOnce) + { + eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.GrantedQoS2; + } + + if (topicFilter.Topic.StartsWith("$share/")) + { + eventArgs.Response.ReasonCode = MqttSubscribeReasonCode.SharedSubscriptionsNotSupported; + } + else + { + await _eventContainer.InterceptingSubscriptionEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + } + + return eventArgs; + } + + async Task InterceptUnsubscribe(string topicFilter, MqttSubscription mqttSubscription, CancellationToken cancellationToken) + { + var clientUnsubscribingTopicEventArgs = new InterceptingUnsubscriptionEventArgs + { + ClientId = _session.Id, + Topic = topicFilter, + SessionItems = _session.Items, + CancellationToken = cancellationToken + }; + + if (mqttSubscription == null) + { + clientUnsubscribingTopicEventArgs.Response.ReasonCode = MqttUnsubscribeReasonCode.NoSubscriptionExisted; + } + else + { + clientUnsubscribingTopicEventArgs.Response.ReasonCode = MqttUnsubscribeReasonCode.Success; + } + + 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/BPA.MQTTnet/Server/Internal/MqttRetainedMessagesManager.cs b/Source/BPA.MQTTnet/Server/Internal/MqttRetainedMessagesManager.cs new file mode 100644 index 0000000..3673734 --- /dev/null +++ b/Source/BPA.MQTTnet/Server/Internal/MqttRetainedMessagesManager.cs @@ -0,0 +1,147 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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.Implementations; +using MQTTnet.Internal; + +namespace MQTTnet.Server +{ + public sealed class MqttRetainedMessagesManager + { + readonly Dictionary _messages = new Dictionary(4096); + readonly AsyncLock _storageAccessLock = new AsyncLock(); + + readonly MqttServerEventContainer _eventContainer; + readonly MqttNetSourceLogger _logger; + + public MqttRetainedMessagesManager(MqttServerEventContainer eventContainer, IMqttNetLogger logger) + { + _eventContainer = eventContainer ?? throw new ArgumentNullException(nameof(eventContainer)); + + if (logger == null) throw new ArgumentNullException(nameof(logger)); + _logger = logger.WithSource(nameof(MqttRetainedMessagesManager)); + } + + public async Task Start() + { + try + { + var eventArgs = new LoadingRetainedMessagesEventArgs(); + await _eventContainer.LoadingRetainedMessagesEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + + lock (_messages) + { + _messages.Clear(); + + if (eventArgs.LoadedRetainedMessages != null) + { + foreach (var retainedMessage in eventArgs.LoadedRetainedMessages) + { + _messages[retainedMessage.Topic] = retainedMessage; + } + } + } + } + catch (Exception exception) + { + _logger.Error(exception, "Unhandled exception while loading retained messages."); + } + } + + public async Task UpdateMessage(string clientId, MqttApplicationMessage applicationMessage) + { + if (applicationMessage == null) + { + throw new ArgumentNullException(nameof(applicationMessage)); + } + + try + { + List messagesForSave = null; + var saveIsRequired = false; + + lock (_messages) + { + var hasPayload = applicationMessage.Payload != null && applicationMessage.Payload.Length > 0; + + if (!hasPayload) + { + saveIsRequired = _messages.Remove(applicationMessage.Topic); + _logger.Verbose("Client '{0}' cleared retained message for topic '{1}'.", clientId, applicationMessage.Topic); + } + else + { + if (!_messages.TryGetValue(applicationMessage.Topic, out var existingMessage)) + { + _messages[applicationMessage.Topic] = applicationMessage; + saveIsRequired = true; + } + else + { + if (existingMessage.QualityOfServiceLevel != applicationMessage.QualityOfServiceLevel || !existingMessage.Payload.SequenceEqual(applicationMessage.Payload ?? PlatformAbstractionLayer.EmptyByteArray)) + { + _messages[applicationMessage.Topic] = applicationMessage; + saveIsRequired = true; + } + } + + _logger.Verbose("Client '{0}' set retained message for topic '{1}'.", clientId, applicationMessage.Topic); + } + + if (saveIsRequired) + { + messagesForSave = new List(_messages.Values); + } + } + + if (saveIsRequired) + { + using (await _storageAccessLock.WaitAsync(CancellationToken.None).ConfigureAwait(false)) + { + var eventArgs = new RetainedMessageChangedEventArgs + { + ClientId = clientId, + ChangedRetainedMessage = applicationMessage, + StoredRetainedMessages = messagesForSave + }; + + await _eventContainer.RetainedMessageChangedEvent.InvokeAsync(eventArgs).ConfigureAwait(false); + } + } + } + catch (Exception exception) + { + _logger.Error(exception, "Unhandled exception while handling retained messages."); + } + } + + public Task> GetMessages() + { + lock (_messages) + { + var result = new List(_messages.Values); + return Task.FromResult((IList)result); + } + } + + public async Task ClearMessages() + { + lock (_messages) + { + _messages.Clear(); + } + + using (await _storageAccessLock.WaitAsync(CancellationToken.None).ConfigureAwait(false)) + { + await _eventContainer.RetainedMessagesClearedEvent.InvokeAsync(EventArgs.Empty).ConfigureAwait(false); + } + } + } +} diff --git a/Source/BPA.MQTTnet/Server/Internal/MqttServerEventContainer.cs b/Source/BPA.MQTTnet/Server/Internal/MqttServerEventContainer.cs new file mode 100644 index 0000000..f42d23d --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Server/Internal/MqttServerKeepAliveMonitor.cs b/Source/BPA.MQTTnet/Server/Internal/MqttServerKeepAliveMonitor.cs new file mode 100644 index 0000000..76deb05 --- /dev/null +++ b/Source/BPA.MQTTnet/Server/Internal/MqttServerKeepAliveMonitor.cs @@ -0,0 +1,123 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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; +using MQTTnet.Implementations; +using MQTTnet.Internal; +using MQTTnet.Protocol; + +namespace MQTTnet.Server +{ + public sealed class MqttServerKeepAliveMonitor + { + readonly MqttServerOptions _options; + readonly MqttClientSessionsManager _sessionsManager; + readonly MqttNetSourceLogger _logger; + + public MqttServerKeepAliveMonitor(MqttServerOptions options, MqttClientSessionsManager sessionsManager, IMqttNetLogger logger) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + _sessionsManager = sessionsManager ?? throw new ArgumentNullException(nameof(sessionsManager)); + + if (logger == null) throw new ArgumentNullException(nameof(logger)); + _logger = logger.WithSource(nameof(MqttServerKeepAliveMonitor)); + } + + public void Start(CancellationToken cancellationToken) + { + // The keep alive monitor spawns a real new thread (LongRunning) because it does not + // support async/await. Async etc. is avoided here because the thread will usually check + // the connections every few milliseconds and thus the context changes (due to async) are + // only consuming resources. Also there is just 1 thread for the entire server which is fine at all! + Task.Factory.StartNew(_ => DoWork(cancellationToken), cancellationToken, TaskCreationOptions.LongRunning).RunInBackground(_logger); + } + + void DoWork(CancellationToken cancellationToken) + { + try + { + _logger.Info("Starting keep alive monitor."); + + while (!cancellationToken.IsCancellationRequested) + { + TryProcessClients(); + PlatformAbstractionLayer.Sleep(_options.KeepAliveMonitorInterval); + } + } + catch (OperationCanceledException) + { + } + catch (Exception exception) + { + _logger.Error(exception, "Unhandled exception while checking keep alive timeouts."); + } + finally + { + _logger.Verbose("Stopped checking keep alive timeout."); + } + } + + void TryProcessClients() + { + var now = DateTime.UtcNow; + foreach (var client in _sessionsManager.GetClients()) + { + TryProcessClient(client, now); + } + } + + void TryProcessClient(MqttClient connection, DateTime now) + { + try + { + if (!connection.IsRunning) + { + // The connection is already dead or just created so there is no need to check it. + return; + } + + if (connection.KeepAlivePeriod == 0) + { + // The keep alive feature is not used by the current connection. + return; + } + + if (connection.ChannelAdapter.IsReadingPacket) + { + // The connection is currently reading a (large) packet. So it is obviously + // doing something and thus "connected". + return; + } + + // Values described here: [MQTT-3.1.2-24]. + // If the client sends 5 sec. the server will allow up to 7.5 seconds. + // If the client sends 1 sec. the server will allow up to 1.5 seconds. + var maxDurationWithoutPacket = connection.KeepAlivePeriod * 1.5D; + + var secondsWithoutPackage = (now - connection.Statistics.LastPacketSentTimestamp).TotalSeconds; + if (secondsWithoutPackage < maxDurationWithoutPacket) + { + // A packet was received before the timeout is affected. + return; + } + + _logger.Warning("Client '{0}': Did not receive any packet or keep alive signal.", connection.Id); + + // Execute the disconnection in background so that the keep alive monitor can continue + // with checking other connections. + // We do not need to wait for the task so no await is needed. + // Also the internal state of the connection must be swapped to "Finalizing" because the + // next iteration of the keep alive timer happens. + var _ = connection.StopAsync(MqttDisconnectReasonCode.KeepAliveTimeout); + } + catch (Exception exception) + { + _logger.Error(exception, "Client {0}: Unhandled exception while checking keep alive timeouts.", connection.Id); + } + } + } +} diff --git a/Source/BPA.MQTTnet/Server/Internal/MqttSession.cs b/Source/BPA.MQTTnet/Server/Internal/MqttSession.cs new file mode 100644 index 0000000..9bb8337 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Server/Internal/MqttSubscription.cs b/Source/BPA.MQTTnet/Server/Internal/MqttSubscription.cs new file mode 100644 index 0000000..ec53242 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Server/Internal/MqttUnsubscribeResult.cs b/Source/BPA.MQTTnet/Server/Internal/MqttUnsubscribeResult.cs new file mode 100644 index 0000000..9e482dc --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Server/Internal/SubscribeResult.cs b/Source/BPA.MQTTnet/Server/Internal/SubscribeResult.cs new file mode 100644 index 0000000..beff649 --- /dev/null +++ b/Source/BPA.MQTTnet/Server/Internal/SubscribeResult.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 SubscribeResult + { + public bool CloseConnection { get; set; } + + public List ReasonCodes { 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/BPA.MQTTnet/Server/Internal/TopicHashMaskSubscriptions.cs b/Source/BPA.MQTTnet/Server/Internal/TopicHashMaskSubscriptions.cs new file mode 100644 index 0000000..e9a35e8 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Server/MqttClientDisconnectType.cs b/Source/BPA.MQTTnet/Server/MqttClientDisconnectType.cs new file mode 100644 index 0000000..7f738c8 --- /dev/null +++ b/Source/BPA.MQTTnet/Server/MqttClientDisconnectType.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 MqttClientDisconnectType + { + Clean, + NotClean, + Takeover + } +} diff --git a/Source/BPA.MQTTnet/Server/MqttRetainedMessageMatch.cs b/Source/BPA.MQTTnet/Server/MqttRetainedMessageMatch.cs new file mode 100644 index 0000000..a54e5b7 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Server/MqttServer.cs b/Source/BPA.MQTTnet/Server/MqttServer.cs new file mode 100644 index 0000000..4084e8f --- /dev/null +++ b/Source/BPA.MQTTnet/Server/MqttServer.cs @@ -0,0 +1,367 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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.Adapter; +using MQTTnet.Diagnostics; +using MQTTnet.Implementations; +using MQTTnet.Internal; +using MQTTnet.Packets; +using MQTTnet.Protocol; + +namespace MQTTnet.Server +{ + public class MqttServer : Disposable + { + readonly MqttServerEventContainer _eventContainer = new MqttServerEventContainer(); + + readonly ICollection _adapters; + readonly MqttNetSourceLogger _logger; + readonly MqttServerOptions _options; + readonly IMqttNetLogger _rootLogger; + readonly MqttRetainedMessagesManager _retainedMessagesManager; + readonly MqttServerKeepAliveMonitor _keepAliveMonitor; + readonly MqttClientSessionsManager _clientSessionsManager; + + CancellationTokenSource _cancellationTokenSource; + + public MqttServer(MqttServerOptions options, IEnumerable adapters, IMqttNetLogger logger) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + + if (adapters == null) + { + throw new ArgumentNullException(nameof(adapters)); + } + + _adapters = adapters.ToList(); + + _rootLogger = logger ?? throw new ArgumentNullException(nameof(logger)); + _logger = logger.WithSource(nameof(MqttServer)); + + _retainedMessagesManager = new MqttRetainedMessagesManager(_eventContainer, _rootLogger); + _clientSessionsManager = new MqttClientSessionsManager(options, _retainedMessagesManager, _eventContainer, _rootLogger); + _keepAliveMonitor = new MqttServerKeepAliveMonitor(options, _clientSessionsManager, _rootLogger); + } + + public event Func ApplicationMessageNotConsumedAsync + { + add => _eventContainer.ApplicationMessageNotConsumedEvent.AddHandler(value); + remove => _eventContainer.ApplicationMessageNotConsumedEvent.RemoveHandler(value); + } + + public event Func ClientConnectedAsync + { + add => _eventContainer.ClientConnectedEvent.AddHandler(value); + remove => _eventContainer.ClientConnectedEvent.RemoveHandler(value); + } + + public event Func ClientDisconnectedAsync + { + add => _eventContainer.ClientDisconnectedEvent.AddHandler(value); + remove => _eventContainer.ClientDisconnectedEvent.RemoveHandler(value); + } + + public event Func ClientSubscribedTopicAsync + { + add => _eventContainer.ClientSubscribedTopicEvent.AddHandler(value); + remove => _eventContainer.ClientSubscribedTopicEvent.RemoveHandler(value); + } + + public event Func ClientUnsubscribedTopicAsync + { + add => _eventContainer.ClientUnsubscribedTopicEvent.AddHandler(value); + remove => _eventContainer.ClientUnsubscribedTopicEvent.RemoveHandler(value); + } + + public event Func InterceptingInboundPacketAsync + { + add => _eventContainer.InterceptingInboundPacketEvent.AddHandler(value); + remove => _eventContainer.InterceptingInboundPacketEvent.RemoveHandler(value); + } + + public event Func InterceptingOutboundPacketAsync + { + add => _eventContainer.InterceptingOutboundPacketEvent.AddHandler(value); + remove => _eventContainer.InterceptingOutboundPacketEvent.RemoveHandler(value); + } + + public event Func InterceptingPublishAsync + { + add => _eventContainer.InterceptingPublishEvent.AddHandler(value); + remove => _eventContainer.InterceptingPublishEvent.RemoveHandler(value); + } + + public event Func InterceptingSubscriptionAsync + { + add => _eventContainer.InterceptingSubscriptionEvent.AddHandler(value); + remove => _eventContainer.InterceptingSubscriptionEvent.RemoveHandler(value); + } + + public event Func InterceptingUnsubscriptionAsync + { + add => _eventContainer.InterceptingUnsubscriptionEvent.AddHandler(value); + remove => _eventContainer.InterceptingUnsubscriptionEvent.RemoveHandler(value); + } + + public event Func LoadingRetainedMessageAsync + { + add => _eventContainer.LoadingRetainedMessagesEvent.AddHandler(value); + remove => _eventContainer.LoadingRetainedMessagesEvent.RemoveHandler(value); + } + + public event Func PreparingSessionAsync + { + add => _eventContainer.PreparingSessionEvent.AddHandler(value); + remove => _eventContainer.PreparingSessionEvent.RemoveHandler(value); + } + + public event Func RetainedMessageChangedAsync + { + add => _eventContainer.RetainedMessageChangedEvent.AddHandler(value); + remove => _eventContainer.RetainedMessageChangedEvent.RemoveHandler(value); + } + + public event Func RetainedMessagesClearedAsync + { + add => _eventContainer.RetainedMessagesClearedEvent.AddHandler(value); + remove => _eventContainer.RetainedMessagesClearedEvent.RemoveHandler(value); + } + + public event Func SessionDeletedAsync + { + add => _eventContainer.SessionDeletedEvent.AddHandler(value); + remove => _eventContainer.SessionDeletedEvent.RemoveHandler(value); + } + + public event Func StartedAsync + { + add => _eventContainer.StartedEvent.AddHandler(value); + remove => _eventContainer.StartedEvent.RemoveHandler(value); + } + + public event Func StoppedAsync + { + add => _eventContainer.StoppedEvent.AddHandler(value); + remove => _eventContainer.StoppedEvent.RemoveHandler(value); + } + + public event Func ValidatingConnectionAsync + { + add => _eventContainer.ValidatingConnectionEvent.AddHandler(value); + remove => _eventContainer.ValidatingConnectionEvent.RemoveHandler(value); + } + + public bool IsStarted => _cancellationTokenSource != null; + + public Task DeleteRetainedMessagesAsync() + { + ThrowIfNotStarted(); + + return _retainedMessagesManager?.ClearMessages() ?? PlatformAbstractionLayer.CompletedTask; + } + + public Task DisconnectClientAsync(string id, MqttDisconnectReasonCode reasonCode) + { + if (id == null) + { + throw new ArgumentNullException(nameof(id)); + } + + ThrowIfNotStarted(); + + return _clientSessionsManager.GetClient(id).StopAsync(reasonCode); + } + + public Task> GetClientsAsync() + { + ThrowIfNotStarted(); + + return _clientSessionsManager.GetClientStatusAsync(); + } + + public Task> GetRetainedMessagesAsync() + { + ThrowIfNotStarted(); + + return _retainedMessagesManager.GetMessages(); + } + + public Task> GetSessionsAsync() + { + ThrowIfNotStarted(); + + return _clientSessionsManager.GetSessionStatusAsync(); + } + + public Task InjectApplicationMessage(InjectedMqttApplicationMessage injectedApplicationMessage) + { + if (injectedApplicationMessage == null) + { + throw new ArgumentNullException(nameof(injectedApplicationMessage)); + } + + if (injectedApplicationMessage.ApplicationMessage == null) + { + throw new ArgumentNullException(nameof(injectedApplicationMessage.ApplicationMessage)); + } + + MqttTopicValidator.ThrowIfInvalid(injectedApplicationMessage.ApplicationMessage.Topic); + + ThrowIfNotStarted(); + + return _clientSessionsManager.DispatchApplicationMessage(injectedApplicationMessage.SenderClientId, injectedApplicationMessage.ApplicationMessage); + } + + public async Task StartAsync() + { + ThrowIfStarted(); + + _cancellationTokenSource = new CancellationTokenSource(); + var cancellationToken = _cancellationTokenSource.Token; + + await _retainedMessagesManager.Start().ConfigureAwait(false); + _clientSessionsManager.Start(); + _keepAliveMonitor.Start(cancellationToken); + + foreach (var adapter in _adapters) + { + adapter.ClientHandler = c => OnHandleClient(c, cancellationToken); + await adapter.StartAsync(_options, _rootLogger).ConfigureAwait(false); + } + + await _eventContainer.StartedEvent.InvokeAsync(EventArgs.Empty).ConfigureAwait(false); + + _logger.Info("Started."); + } + + public async Task StopAsync() + { + try + { + if (_cancellationTokenSource == null) + { + return; + } + + _cancellationTokenSource.Cancel(false); + + await _clientSessionsManager.CloseAllConnectionsAsync().ConfigureAwait(false); + + foreach (var adapter in _adapters) + { + adapter.ClientHandler = null; + await adapter.StopAsync().ConfigureAwait(false); + } + } + finally + { + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = null; + } + + await _eventContainer.StoppedEvent.InvokeAsync(EventArgs.Empty).ConfigureAwait(false); + + _logger.Info("Stopped."); + } + + public Task SubscribeAsync(string clientId, ICollection topicFilters) + { + if (clientId == null) + { + throw new ArgumentNullException(nameof(clientId)); + } + + if (topicFilters == null) + { + throw new ArgumentNullException(nameof(topicFilters)); + } + + foreach (var topicFilter in topicFilters) + { + MqttTopicValidator.ThrowIfInvalidSubscribe(topicFilter.Topic); + } + + ThrowIfDisposed(); + ThrowIfNotStarted(); + + return _clientSessionsManager.SubscribeAsync(clientId, topicFilters); + } + + public Task UnsubscribeAsync(string clientId, ICollection 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) + { + if (disposing) + { + StopAsync().GetAwaiter().GetResult(); + + foreach (var adapter in _adapters) + { + adapter.Dispose(); + } + } + + base.Dispose(disposing); + } + + Task OnHandleClient(IMqttChannelAdapter channelAdapter, CancellationToken cancellationToken) + { + return _clientSessionsManager.HandleClientConnectionAsync(channelAdapter, cancellationToken); + } + + void ThrowIfNotStarted() + { + ThrowIfDisposed(); + + if (_cancellationTokenSource == null) + { + throw new InvalidOperationException("The MQTT server is not started."); + } + } + + void ThrowIfStarted() + { + ThrowIfDisposed(); + + if (_cancellationTokenSource != null) + { + throw new InvalidOperationException("The MQTT server is already started."); + } + } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Server/MqttServerExtensions.cs b/Source/BPA.MQTTnet/Server/MqttServerExtensions.cs new file mode 100644 index 0000000..22d8538 --- /dev/null +++ b/Source/BPA.MQTTnet/Server/MqttServerExtensions.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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.Server +{ + public static class MqttServerExtensions + { + public static Task SubscribeAsync(this MqttServer server, string clientId, params MqttTopicFilter[] topicFilters) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + if (clientId == null) throw new ArgumentNullException(nameof(clientId)); + if (topicFilters == null) throw new ArgumentNullException(nameof(topicFilters)); + + return server.SubscribeAsync(clientId, topicFilters); + } + + public static Task SubscribeAsync(this MqttServer server, string clientId, string topic) + { + if (server == null) throw new ArgumentNullException(nameof(server)); + if (clientId == null) throw new ArgumentNullException(nameof(clientId)); + if (topic == null) throw new ArgumentNullException(nameof(topic)); + + return server.SubscribeAsync(clientId, new MqttTopicFilterBuilder().WithTopic(topic).Build()); + } + } +} diff --git a/Source/BPA.MQTTnet/Server/Options/IMqttServerCertificateCredentials.cs b/Source/BPA.MQTTnet/Server/Options/IMqttServerCertificateCredentials.cs new file mode 100644 index 0000000..d0b7563 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Server/Options/MqttPendingMessagesOverflowStrategy.cs b/Source/BPA.MQTTnet/Server/Options/MqttPendingMessagesOverflowStrategy.cs new file mode 100644 index 0000000..e9dada1 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Server/Options/MqttServerCertificateCredentials.cs b/Source/BPA.MQTTnet/Server/Options/MqttServerCertificateCredentials.cs new file mode 100644 index 0000000..f0ce001 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Server/Options/MqttServerOptions.cs b/Source/BPA.MQTTnet/Server/Options/MqttServerOptions.cs new file mode 100644 index 0000000..59e46ab --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Server/Options/MqttServerOptionsBuilder.cs b/Source/BPA.MQTTnet/Server/Options/MqttServerOptionsBuilder.cs new file mode 100644 index 0000000..b6a50cc --- /dev/null +++ b/Source/BPA.MQTTnet/Server/Options/MqttServerOptionsBuilder.cs @@ -0,0 +1,210 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Net; +using System.Net.Security; +using System.Security.Authentication; +using MQTTnet.Certificates; +using System.Threading.Tasks; + +#if !WINDOWS_UWP +using System.Security.Cryptography.X509Certificates; +#endif + +// ReSharper disable UnusedMember.Global +namespace MQTTnet.Server +{ + public class MqttServerOptionsBuilder + { + readonly MqttServerOptions _options = new MqttServerOptions(); + + public MqttServerOptionsBuilder WithConnectionBacklog(int value) + { + _options.DefaultEndpointOptions.ConnectionBacklog = value; + _options.TlsEndpointOptions.ConnectionBacklog = value; + return this; + } + + public MqttServerOptionsBuilder WithMaxPendingMessagesPerClient(int value) + { + _options.MaxPendingMessagesPerClient = value; + return this; + } + + public MqttServerOptionsBuilder WithDefaultCommunicationTimeout(TimeSpan value) + { + _options.DefaultCommunicationTimeout = value; + return this; + } + + public MqttServerOptionsBuilder WithDefaultEndpoint() + { + _options.DefaultEndpointOptions.IsEnabled = true; + return this; + } + + public MqttServerOptionsBuilder WithDefaultEndpointPort(int value) + { + _options.DefaultEndpointOptions.Port = value; + return this; + } + + public MqttServerOptionsBuilder WithDefaultEndpointBoundIPAddress(IPAddress value) + { + _options.DefaultEndpointOptions.BoundInterNetworkAddress = value ?? IPAddress.Any; + return this; + } + + public MqttServerOptionsBuilder WithDefaultEndpointBoundIPV6Address(IPAddress value) + { + _options.DefaultEndpointOptions.BoundInterNetworkV6Address = value ?? IPAddress.Any; + return this; + } + + public MqttServerOptionsBuilder WithoutDefaultEndpoint() + { + _options.DefaultEndpointOptions.IsEnabled = false; + return this; + } + + public MqttServerOptionsBuilder WithEncryptedEndpoint() + { + _options.TlsEndpointOptions.IsEnabled = true; + return this; + } + + public MqttServerOptionsBuilder WithEncryptedEndpointPort(int value) + { + _options.TlsEndpointOptions.Port = value; + return this; + } + + public MqttServerOptionsBuilder WithEncryptedEndpointBoundIPAddress(IPAddress value) + { + _options.TlsEndpointOptions.BoundInterNetworkAddress = value; + return this; + } + + public MqttServerOptionsBuilder WithEncryptedEndpointBoundIPV6Address(IPAddress value) + { + _options.TlsEndpointOptions.BoundInterNetworkV6Address = value; + return this; + } + +#if !WINDOWS_UWP + public MqttServerOptionsBuilder WithEncryptionCertificate(byte[] value, IMqttServerCertificateCredentials credentials = null) + { + if (value == null) throw new ArgumentNullException(nameof(value)); + + _options.TlsEndpointOptions.CertificateProvider = new BlobCertificateProvider(value) + { + Password = credentials?.Password + }; + + return this; + } + + public MqttServerOptionsBuilder WithEncryptionCertificate(X509Certificate2 certificate) + { + if (certificate == null) throw new ArgumentNullException(nameof(certificate)); + + _options.TlsEndpointOptions.CertificateProvider = new X509CertificateProvider(certificate); + return this; + } +#endif + + public MqttServerOptionsBuilder WithEncryptionSslProtocol(SslProtocols value) + { + _options.TlsEndpointOptions.SslProtocol = value; + return this; + } + +#if !WINDOWS_UWP + public MqttServerOptionsBuilder WithClientCertificate(RemoteCertificateValidationCallback validationCallback = null, bool checkCertificateRevocation = false) + { + _options.TlsEndpointOptions.ClientCertificateRequired = true; + _options.TlsEndpointOptions.CheckCertificateRevocation = checkCertificateRevocation; + _options.TlsEndpointOptions.RemoteCertificateValidationCallback = validationCallback; + return this; + } +#endif + + public MqttServerOptionsBuilder WithoutEncryptedEndpoint() + { + _options.TlsEndpointOptions.IsEnabled = false; + return this; + } + +#if !WINDOWS_UWP + public MqttServerOptionsBuilder WithRemoteCertificateValidationCallback(RemoteCertificateValidationCallback value) + { + _options.TlsEndpointOptions.RemoteCertificateValidationCallback = value; + return this; + } +#endif + + // 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 WithDefaultEndpointReuseAddress() + { + _options.DefaultEndpointOptions.ReuseAddress = true; + return this; + } + + public MqttServerOptionsBuilder WithTlsEndpointReuseAddress() + { + _options.TlsEndpointOptions.ReuseAddress = true; + return this; + } + + public MqttServerOptionsBuilder WithPersistentSessions(bool value = true) + { + _options.EnablePersistentSessions = value; + return this; + } + + // /// + // /// 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/BPA.MQTTnet/Server/Options/MqttServerTcpEndpointBaseOptions.cs b/Source/BPA.MQTTnet/Server/Options/MqttServerTcpEndpointBaseOptions.cs new file mode 100644 index 0000000..feab55b --- /dev/null +++ b/Source/BPA.MQTTnet/Server/Options/MqttServerTcpEndpointBaseOptions.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Net; +using System.Net.Sockets; + +namespace MQTTnet.Server +{ + public abstract class MqttServerTcpEndpointBaseOptions + { + public bool IsEnabled { get; set; } + + public int Port { get; set; } + + public int ConnectionBacklog { get; set; } = 100; + + public bool NoDelay { get; set; } = true; + + public LingerOption LingerState { get; set; } = new LingerOption(true, 0); + +#if WINDOWS_UWP + public int BufferSize { get; set; } = 4096; +#endif + + public IPAddress BoundInterNetworkAddress { get; set; } = IPAddress.Any; + + public IPAddress BoundInterNetworkV6Address { get; set; } = IPAddress.IPv6Any; + + /// + /// This requires admin permissions on Linux. + /// + public bool ReuseAddress { get; set; } + } +} \ No newline at end of file diff --git a/Source/BPA.MQTTnet/Server/Options/MqttServerTcpEndpointOptions.cs b/Source/BPA.MQTTnet/Server/Options/MqttServerTcpEndpointOptions.cs new file mode 100644 index 0000000..0437da0 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Server/Options/MqttServerTlsTcpEndpointOptions.cs b/Source/BPA.MQTTnet/Server/Options/MqttServerTlsTcpEndpointOptions.cs new file mode 100644 index 0000000..ddab4ff --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Server/PublishResponse.cs b/Source/BPA.MQTTnet/Server/PublishResponse.cs new file mode 100644 index 0000000..d6c7c11 --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Server/Status/MqttClientStatus.cs b/Source/BPA.MQTTnet/Server/Status/MqttClientStatus.cs new file mode 100644 index 0000000..6707c9b --- /dev/null +++ b/Source/BPA.MQTTnet/Server/Status/MqttClientStatus.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.Threading.Tasks; +using MQTTnet.Formatter; +using MQTTnet.Protocol; + +namespace MQTTnet.Server +{ + public sealed class MqttClientStatus + { + readonly MqttClient _client; + + public MqttClientStatus(MqttClient client) + { + _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 Id => _client.Id; + + public string Endpoint => _client.Endpoint; + + public MqttProtocolVersion ProtocolVersion => _client.ChannelAdapter.PacketFormatterAdapter.ProtocolVersion; + + public DateTime ConnectedTimestamp => _client.Statistics.ConnectedTimestamp; + + public DateTime LastPacketReceivedTimestamp => _client.Statistics.LastPacketReceivedTimestamp; + + public DateTime LastPacketSentTimestamp => _client.Statistics.LastPacketSentTimestamp; + + public DateTime LastNonKeepAlivePacketReceivedTimestamp => _client.Statistics.LastNonKeepAlivePacketReceivedTimestamp; + + public long ReceivedApplicationMessagesCount => _client.Statistics.ReceivedApplicationMessagesCount; + + public long SentApplicationMessagesCount => _client.Statistics.SentApplicationMessagesCount; + + public long ReceivedPacketsCount => _client.Statistics.ReceivedPacketsCount; + + public long SentPacketsCount => _client.Statistics.SentPacketsCount; + + public MqttSessionStatus Session { get; set; } + + public long BytesSent => _client.ChannelAdapter.BytesSent; + + public long BytesReceived => _client.ChannelAdapter.BytesReceived; + + public Task DisconnectAsync() + { + return _client.StopAsync(MqttDisconnectReasonCode.NormalDisconnection); + } + + public void ResetStatistics() + { + _client.ResetStatistics(); + } + } +} diff --git a/Source/BPA.MQTTnet/Server/Status/MqttSessionStatus.cs b/Source/BPA.MQTTnet/Server/Status/MqttSessionStatus.cs new file mode 100644 index 0000000..7dfd641 --- /dev/null +++ b/Source/BPA.MQTTnet/Server/Status/MqttSessionStatus.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 System.Collections; +using System.Threading.Tasks; +using MQTTnet.Formatter; +using MQTTnet.Implementations; +using MQTTnet.Internal; + +namespace MQTTnet.Server +{ + public sealed class MqttSessionStatus + { + readonly MqttSession _session; + + public MqttSessionStatus(MqttSession session) + { + _session = session ?? throw new ArgumentNullException(nameof(session)); + } + + public string Id => _session.Id; + + public long PendingApplicationMessagesCount => _session.PendingDataPacketsCount; + + public DateTime CreatedTimestamp => _session.CreatedTimestamp; + + 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 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); + + return packetBusItem.WaitForDeliveryAsync(); + } + + public Task DeleteAsync() + { + return _session.DeleteAsync(); + } + + public Task ClearApplicationMessagesQueueAsync() + { + throw new NotImplementedException(); + } + } +} diff --git a/Source/BPA.MQTTnet/Server/SubscribeResponse.cs b/Source/BPA.MQTTnet/Server/SubscribeResponse.cs new file mode 100644 index 0000000..5b4959a --- /dev/null +++ b/Source/BPA.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/BPA.MQTTnet/Server/UnsubscribeResponse.cs b/Source/BPA.MQTTnet/Server/UnsubscribeResponse.cs new file mode 100644 index 0000000..990bde5 --- /dev/null +++ b/Source/BPA.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